1use sim_kernel::{Expr, Symbol};
4use sim_lib_view::SurfaceCaps;
5use sim_value::{access, build};
6
7use crate::ladder::DeviceTier;
8use crate::rate::{RateClass, RateError};
9
10pub const DEVICE_PROFILE_NAMESPACE: &str = "device";
12
13pub const DEVICE_PROFILE_KIND: &str = "profile";
15
16#[derive(Clone, Debug, PartialEq)]
22pub struct DeviceProfile {
23 pub kind: Symbol,
25 pub tier: DeviceTier,
27 pub display: Vec<Symbol>,
29 pub input: Vec<Symbol>,
31 pub output: Vec<Symbol>,
33 pub links: Vec<Symbol>,
35 pub streams: Vec<Symbol>,
37 pub rate: RateClass,
39 pub policy: Expr,
41}
42
43#[derive(Clone, Debug, PartialEq)]
45pub struct DeviceProfileParts {
46 pub kind: Symbol,
48 pub display: Vec<Symbol>,
50 pub input: Vec<Symbol>,
52 pub output: Vec<Symbol>,
54 pub links: Vec<Symbol>,
56 pub streams: Vec<Symbol>,
58 pub rate: RateClass,
60 pub policy: Expr,
62}
63
64impl DeviceProfile {
65 pub fn new(parts: DeviceProfileParts) -> Self {
67 let mut profile = Self {
68 kind: parts.kind,
69 tier: DeviceTier::Display,
70 display: dedup_symbols(parts.display),
71 input: dedup_symbols(parts.input),
72 output: dedup_symbols(parts.output),
73 links: dedup_symbols(parts.links),
74 streams: dedup_symbols(parts.streams),
75 rate: parts.rate,
76 policy: parts.policy,
77 };
78 profile.tier = derive_tier(&profile);
79 profile
80 }
81
82 pub fn from_surface_caps(caps: &SurfaceCaps) -> Self {
84 let display = display_symbols(&caps.display);
85 let input = flag_symbols(&caps.input);
86 let explicit_output = flag_symbols(&caps.output);
87 let output = output_symbols(&display, &input, &explicit_output);
88 let links = link_symbols(&caps.transport);
89 let explicit_streams = flag_symbols(&caps.streams);
90 let streams = stream_symbols(&caps.input, &explicit_streams);
91 let rate = RateClass::from_expr(&caps.rate).unwrap_or_else(|_| RateClass::safe_default());
92 Self::new(DeviceProfileParts {
93 kind: Symbol::new(caps.preset_name().to_owned()),
94 display,
95 input,
96 output,
97 links,
98 streams,
99 rate,
100 policy: caps.privacy.clone(),
101 })
102 }
103
104 pub fn to_expr(&self) -> Expr {
106 build::map(vec![
107 (
108 "kind",
109 Expr::Symbol(Symbol::qualified(
110 DEVICE_PROFILE_NAMESPACE,
111 DEVICE_PROFILE_KIND,
112 )),
113 ),
114 ("device-kind", Expr::Symbol(self.kind.clone())),
115 ("tier", Expr::Symbol(self.tier.to_symbol())),
116 ("display", symbol_list(&self.display)),
117 ("input", symbol_list(&self.input)),
118 ("output", symbol_list(&self.output)),
119 ("links", symbol_list(&self.links)),
120 ("streams", symbol_list(&self.streams)),
121 ("rate", self.rate.to_expr()),
122 ("policy", self.policy.clone()),
123 ])
124 }
125
126 pub fn from_expr(expr: &Expr) -> Result<Self, DeviceProfileError> {
128 let Expr::Map(entries) = expr else {
129 return Err(DeviceProfileError::NotProfile);
130 };
131 match access::entry_field(entries, "kind") {
132 Some(Expr::Symbol(kind))
133 if kind.namespace.as_deref() == Some(DEVICE_PROFILE_NAMESPACE)
134 && kind.name.as_ref() == DEVICE_PROFILE_KIND => {}
135 _ => return Err(DeviceProfileError::NotProfile),
136 }
137 let kind = match access::entry_field(entries, "device-kind") {
138 Some(Expr::Symbol(symbol)) => symbol.clone(),
139 Some(_) => return Err(DeviceProfileError::BadField("device-kind")),
140 None => return Err(DeviceProfileError::MissingField("device-kind")),
141 };
142 let tier = match access::entry_field(entries, "tier") {
143 Some(Expr::Symbol(symbol)) => {
144 DeviceTier::from_symbol(symbol).ok_or(DeviceProfileError::BadField("tier"))?
145 }
146 Some(_) => return Err(DeviceProfileError::BadField("tier")),
147 None => return Err(DeviceProfileError::MissingField("tier")),
148 };
149 let display = symbol_vec(entries, "display")?;
150 let input = symbol_vec(entries, "input")?;
151 let output = symbol_vec(entries, "output")?;
152 let links = symbol_vec(entries, "links")?;
153 let streams = symbol_vec(entries, "streams")?;
154 let rate = match access::entry_field(entries, "rate") {
155 Some(value) => RateClass::from_expr(value).map_err(DeviceProfileError::Rate)?,
156 None => return Err(DeviceProfileError::MissingField("rate")),
157 };
158 let policy = match access::entry_field(entries, "policy") {
159 Some(value) => value.clone(),
160 None => return Err(DeviceProfileError::MissingField("policy")),
161 };
162 let profile = Self {
163 kind,
164 tier,
165 display: dedup_symbols(display),
166 input: dedup_symbols(input),
167 output: dedup_symbols(output),
168 links: dedup_symbols(links),
169 streams: dedup_symbols(streams),
170 rate,
171 policy,
172 };
173 let derived = derive_tier(&profile);
174 if tier != derived {
175 return Err(DeviceProfileError::TierMismatch {
176 declared: tier,
177 derived,
178 });
179 }
180 Ok(profile)
181 }
182}
183
184pub trait DeviceSurfaceCapsExt {
186 fn device_profile(&self) -> DeviceProfile;
188
189 fn device_rate(&self) -> RateClass;
192}
193
194impl DeviceSurfaceCapsExt for SurfaceCaps {
195 fn device_profile(&self) -> DeviceProfile {
196 DeviceProfile::from_surface_caps(self)
197 }
198
199 fn device_rate(&self) -> RateClass {
200 RateClass::from_expr(&self.rate).unwrap_or_else(|_| RateClass::safe_default())
201 }
202}
203
204#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206pub enum GlassesClass {
207 MonoHud,
209 Stereo6Dof,
211 DisplayOnly,
213}
214
215pub fn glasses_class(profile: &DeviceProfile) -> Option<GlassesClass> {
220 if !is_glasses_profile(profile) {
221 return None;
222 }
223 if has_symbol(&profile.display, "stereo") && has_symbol(&profile.streams, "pose") {
224 Some(GlassesClass::Stereo6Dof)
225 } else if has_symbol(&profile.display, "hud")
226 || (has_symbol(&profile.display, "mono") && has_symbol(&profile.output, "hud"))
227 {
228 Some(GlassesClass::MonoHud)
229 } else {
230 Some(GlassesClass::DisplayOnly)
231 }
232}
233
234pub fn derive_tier(profile: &DeviceProfile) -> DeviceTier {
236 if has_symbol(&profile.display, "stereo") && has_symbol(&profile.streams, "pose") {
237 DeviceTier::Rich
238 } else if has_any(
239 &profile.output,
240 &["haptic", "face", "hud", "speaker", "tone"],
241 ) {
242 DeviceTier::Actuator
243 } else if !profile.streams.is_empty() {
244 DeviceTier::Sensor
245 } else {
246 DeviceTier::Display
247 }
248}
249
250pub fn tier_preset(tier: DeviceTier) -> DeviceProfile {
252 match tier {
253 DeviceTier::Display => DeviceProfile::new(DeviceProfileParts {
254 kind: Symbol::new("display"),
255 display: symbols(&["flat"]),
256 input: Vec::new(),
257 output: symbols(&["screen"]),
258 links: symbols(&["usb"]),
259 streams: Vec::new(),
260 rate: RateClass::safe_default(),
261 policy: build::map(Vec::new()),
262 }),
263 DeviceTier::Sensor => DeviceProfile::new(DeviceProfileParts {
264 kind: Symbol::new("sensor"),
265 display: symbols(&["flat"]),
266 input: Vec::new(),
267 output: Vec::new(),
268 links: symbols(&["bluetooth"]),
269 streams: symbols(&["motion"]),
270 rate: RateClass::watch(),
271 policy: build::map(Vec::new()),
272 }),
273 DeviceTier::Actuator => DeviceProfile::new(DeviceProfileParts {
274 kind: Symbol::new("actuator"),
275 display: symbols(&["round"]),
276 input: symbols(&["tap"]),
277 output: symbols(&["haptic"]),
278 links: symbols(&["phone-relay"]),
279 streams: symbols(&["battery"]),
280 rate: RateClass::watch(),
281 policy: build::map(Vec::new()),
282 }),
283 DeviceTier::Rich => DeviceProfile::new(DeviceProfileParts {
284 kind: Symbol::new("rich"),
285 display: symbols(&["stereo", "hud"]),
286 input: symbols(&["voice"]),
287 output: symbols(&["hud", "speaker"]),
288 links: symbols(&["lan-ws"]),
289 streams: symbols(&["pose", "motion"]),
290 rate: RateClass::stereo(),
291 policy: build::map(Vec::new()),
292 }),
293 }
294}
295
296pub fn device_profile_demo() -> Expr {
298 tier_preset(DeviceTier::Rich).to_expr()
299}
300
301#[derive(Clone, Debug, PartialEq, Eq)]
303pub enum DeviceProfileError {
304 NotProfile,
306 MissingField(&'static str),
308 BadField(&'static str),
310 Rate(RateError),
312 TierMismatch {
314 declared: DeviceTier,
316 derived: DeviceTier,
318 },
319}
320
321impl core::fmt::Display for DeviceProfileError {
322 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
323 match self {
324 DeviceProfileError::NotProfile => write!(f, "value is not a device/profile map"),
325 DeviceProfileError::MissingField(name) => {
326 write!(f, "device profile missing field: {name}")
327 }
328 DeviceProfileError::BadField(name) => {
329 write!(f, "device profile field has wrong shape: {name}")
330 }
331 DeviceProfileError::Rate(err) => write!(f, "device profile rate: {err}"),
332 DeviceProfileError::TierMismatch { declared, derived } => write!(
333 f,
334 "device profile tier mismatch: declared {}, derived {}",
335 declared.token(),
336 derived.token()
337 ),
338 }
339 }
340}
341
342impl std::error::Error for DeviceProfileError {}
343
344fn symbol_vec(
345 entries: &[(Expr, Expr)],
346 name: &'static str,
347) -> Result<Vec<Symbol>, DeviceProfileError> {
348 match access::entry_field(entries, name) {
349 Some(Expr::List(items)) => items
350 .iter()
351 .map(|item| match item {
352 Expr::Symbol(symbol) => Ok(symbol.clone()),
353 _ => Err(DeviceProfileError::BadField(name)),
354 })
355 .collect(),
356 Some(_) => Err(DeviceProfileError::BadField(name)),
357 None => Err(DeviceProfileError::MissingField(name)),
358 }
359}
360
361fn symbol_list(symbols: &[Symbol]) -> Expr {
362 build::list(symbols.iter().cloned().map(Expr::Symbol).collect())
363}
364
365fn display_symbols(display: &Expr) -> Vec<Symbol> {
366 let mut out = Vec::new();
367 push_symbol_field(&mut out, display, "display");
368 if matches!(access::field(display, "stereo"), Some(Expr::Bool(true))) {
369 push_symbol(&mut out, "stereo");
370 }
371 if matches!(access::field(display, "mono"), Some(Expr::Bool(true))) {
372 push_symbol(&mut out, "mono");
373 }
374 if matches!(access::field(display, "none"), Some(Expr::Bool(true))) {
375 push_symbol(&mut out, "none");
376 }
377 if access::field(display, "lines").is_some() {
378 push_symbol(&mut out, "hud");
379 }
380 push_symbol_field(&mut out, display, "shape");
381 push_symbol_field(&mut out, display, "density");
382 push_symbol_field(&mut out, display, "tracking-class");
383 push_symbol_list_field(&mut out, display, "anchor-spaces");
384 if out.is_empty() && matches!(display, Expr::Map(entries) if !entries.is_empty()) {
385 push_symbol(&mut out, "flat");
386 }
387 out
388}
389
390fn flag_symbols(map: &Expr) -> Vec<Symbol> {
391 let Expr::Map(entries) = map else {
392 return Vec::new();
393 };
394 let mut out = Vec::new();
395 for (key, value) in entries {
396 if !matches!(value, Expr::Bool(true)) {
397 continue;
398 }
399 if let Expr::Symbol(symbol) = key
400 && symbol.namespace.is_none()
401 {
402 push_existing(&mut out, symbol.clone());
403 }
404 }
405 out
406}
407
408fn output_symbols(display: &[Symbol], input: &[Symbol], explicit: &[Symbol]) -> Vec<Symbol> {
409 let mut out = explicit.to_vec();
410 if !has_symbol(display, "none") && !display.is_empty() {
411 push_symbol(&mut out, "screen");
412 }
413 if has_symbol(display, "hud") {
414 push_symbol(&mut out, "hud");
415 }
416 if has_symbol(input, "haptic-ack") {
417 push_symbol(&mut out, "haptic");
418 }
419 out
420}
421
422fn link_symbols(transport: &Expr) -> Vec<Symbol> {
423 let mut out = Vec::new();
424 if let Some(Expr::List(items)) = access::field(transport, "links") {
425 for item in items {
426 if let Expr::Symbol(symbol) = item {
427 push_existing(&mut out, symbol.clone());
428 }
429 }
430 }
431 match access::field(transport, "kind") {
432 Some(Expr::Symbol(kind)) if kind.name.as_ref() == "relay" => {
433 push_symbol(&mut out, "phone-relay");
434 }
435 Some(Expr::Symbol(kind)) if kind.name.as_ref() == "websocket" => {
436 push_symbol(&mut out, "lan-ws");
437 }
438 Some(Expr::Symbol(kind)) if matches!(kind.name.as_ref(), "tty" | "local") => {
439 push_symbol(&mut out, "usb");
440 }
441 Some(Expr::Symbol(kind)) => push_existing(&mut out, kind.clone()),
442 _ => {}
443 }
444 out
445}
446
447fn stream_symbols(input: &Expr, explicit: &[Symbol]) -> Vec<Symbol> {
448 let mut out = explicit.to_vec();
449 if matches!(access::field(input, "camera"), Some(Expr::Bool(true))) {
450 push_symbol(&mut out, "motion");
451 }
452 out
453}
454
455fn push_symbol_field(out: &mut Vec<Symbol>, map: &Expr, name: &str) {
456 if let Some(Expr::Symbol(symbol)) = access::field(map, name) {
457 push_existing(out, symbol.clone());
458 }
459}
460
461fn push_symbol_list_field(out: &mut Vec<Symbol>, map: &Expr, name: &str) {
462 if let Some(Expr::List(items)) = access::field(map, name) {
463 for item in items {
464 if let Expr::Symbol(symbol) = item {
465 push_existing(out, symbol.clone());
466 }
467 }
468 }
469}
470
471fn is_glasses_profile(profile: &DeviceProfile) -> bool {
472 let name = profile.kind.name.as_ref();
473 name == "glasses" || name.starts_with("glasses-")
474}
475
476fn symbols(names: &[&str]) -> Vec<Symbol> {
477 names.iter().map(|name| Symbol::new(*name)).collect()
478}
479
480pub(crate) fn has_symbol(symbols: &[Symbol], name: &str) -> bool {
481 symbols
482 .iter()
483 .any(|symbol| symbol.namespace.is_none() && symbol.name.as_ref() == name)
484}
485
486pub(crate) fn has_any(symbols: &[Symbol], names: &[&str]) -> bool {
487 names.iter().any(|name| has_symbol(symbols, name))
488}
489
490pub(crate) fn push_symbol(out: &mut Vec<Symbol>, name: &str) {
491 push_existing(out, Symbol::new(name.to_owned()));
492}
493
494pub(crate) fn push_existing(out: &mut Vec<Symbol>, symbol: Symbol) {
495 if !out.iter().any(|existing| existing == &symbol) {
496 out.push(symbol);
497 }
498}
499
500fn dedup_symbols(symbols: Vec<Symbol>) -> Vec<Symbol> {
501 let mut out = Vec::new();
502 for symbol in symbols {
503 push_existing(&mut out, symbol);
504 }
505 out
506}