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 output = output_symbols(&display, &input);
87 let links = link_symbols(&caps.transport);
88 let streams = stream_symbols(&caps.input);
89 let rate = RateClass::from_expr(&caps.rate).unwrap_or_else(|_| RateClass::safe_default());
90 Self::new(DeviceProfileParts {
91 kind: Symbol::new(caps.preset_name().to_owned()),
92 display,
93 input,
94 output,
95 links,
96 streams,
97 rate,
98 policy: caps.privacy.clone(),
99 })
100 }
101
102 pub fn to_expr(&self) -> Expr {
104 build::map(vec![
105 (
106 "kind",
107 Expr::Symbol(Symbol::qualified(
108 DEVICE_PROFILE_NAMESPACE,
109 DEVICE_PROFILE_KIND,
110 )),
111 ),
112 ("device-kind", Expr::Symbol(self.kind.clone())),
113 ("tier", Expr::Symbol(self.tier.to_symbol())),
114 ("display", symbol_list(&self.display)),
115 ("input", symbol_list(&self.input)),
116 ("output", symbol_list(&self.output)),
117 ("links", symbol_list(&self.links)),
118 ("streams", symbol_list(&self.streams)),
119 ("rate", self.rate.to_expr()),
120 ("policy", self.policy.clone()),
121 ])
122 }
123
124 pub fn from_expr(expr: &Expr) -> Result<Self, DeviceProfileError> {
126 let Expr::Map(entries) = expr else {
127 return Err(DeviceProfileError::NotProfile);
128 };
129 match access::entry_field(entries, "kind") {
130 Some(Expr::Symbol(kind))
131 if kind.namespace.as_deref() == Some(DEVICE_PROFILE_NAMESPACE)
132 && kind.name.as_ref() == DEVICE_PROFILE_KIND => {}
133 _ => return Err(DeviceProfileError::NotProfile),
134 }
135 let kind = match access::entry_field(entries, "device-kind") {
136 Some(Expr::Symbol(symbol)) => symbol.clone(),
137 Some(_) => return Err(DeviceProfileError::BadField("device-kind")),
138 None => return Err(DeviceProfileError::MissingField("device-kind")),
139 };
140 let tier = match access::entry_field(entries, "tier") {
141 Some(Expr::Symbol(symbol)) => {
142 DeviceTier::from_symbol(symbol).ok_or(DeviceProfileError::BadField("tier"))?
143 }
144 Some(_) => return Err(DeviceProfileError::BadField("tier")),
145 None => return Err(DeviceProfileError::MissingField("tier")),
146 };
147 let display = symbol_vec(entries, "display")?;
148 let input = symbol_vec(entries, "input")?;
149 let output = symbol_vec(entries, "output")?;
150 let links = symbol_vec(entries, "links")?;
151 let streams = symbol_vec(entries, "streams")?;
152 let rate = match access::entry_field(entries, "rate") {
153 Some(value) => RateClass::from_expr(value).map_err(DeviceProfileError::Rate)?,
154 None => return Err(DeviceProfileError::MissingField("rate")),
155 };
156 let policy = match access::entry_field(entries, "policy") {
157 Some(value) => value.clone(),
158 None => return Err(DeviceProfileError::MissingField("policy")),
159 };
160 let profile = Self {
161 kind,
162 tier,
163 display: dedup_symbols(display),
164 input: dedup_symbols(input),
165 output: dedup_symbols(output),
166 links: dedup_symbols(links),
167 streams: dedup_symbols(streams),
168 rate,
169 policy,
170 };
171 let derived = derive_tier(&profile);
172 if tier != derived {
173 return Err(DeviceProfileError::TierMismatch {
174 declared: tier,
175 derived,
176 });
177 }
178 Ok(profile)
179 }
180}
181
182pub trait DeviceSurfaceCapsExt {
184 fn device_profile(&self) -> DeviceProfile;
186
187 fn device_rate(&self) -> RateClass;
190}
191
192impl DeviceSurfaceCapsExt for SurfaceCaps {
193 fn device_profile(&self) -> DeviceProfile {
194 DeviceProfile::from_surface_caps(self)
195 }
196
197 fn device_rate(&self) -> RateClass {
198 RateClass::from_expr(&self.rate).unwrap_or_else(|_| RateClass::safe_default())
199 }
200}
201
202pub fn derive_tier(profile: &DeviceProfile) -> DeviceTier {
204 if has_symbol(&profile.display, "stereo") && has_symbol(&profile.streams, "pose") {
205 DeviceTier::Rich
206 } else if has_any(
207 &profile.output,
208 &["haptic", "face", "hud", "speaker", "tone"],
209 ) {
210 DeviceTier::Actuator
211 } else if !profile.streams.is_empty() {
212 DeviceTier::Sensor
213 } else {
214 DeviceTier::Display
215 }
216}
217
218pub fn tier_preset(tier: DeviceTier) -> DeviceProfile {
220 match tier {
221 DeviceTier::Display => DeviceProfile::new(DeviceProfileParts {
222 kind: Symbol::new("display"),
223 display: symbols(&["flat"]),
224 input: Vec::new(),
225 output: symbols(&["screen"]),
226 links: symbols(&["usb"]),
227 streams: Vec::new(),
228 rate: RateClass::safe_default(),
229 policy: build::map(Vec::new()),
230 }),
231 DeviceTier::Sensor => DeviceProfile::new(DeviceProfileParts {
232 kind: Symbol::new("sensor"),
233 display: symbols(&["flat"]),
234 input: Vec::new(),
235 output: Vec::new(),
236 links: symbols(&["bluetooth"]),
237 streams: symbols(&["motion"]),
238 rate: RateClass::watch(),
239 policy: build::map(Vec::new()),
240 }),
241 DeviceTier::Actuator => DeviceProfile::new(DeviceProfileParts {
242 kind: Symbol::new("actuator"),
243 display: symbols(&["round"]),
244 input: symbols(&["tap"]),
245 output: symbols(&["haptic"]),
246 links: symbols(&["phone-relay"]),
247 streams: symbols(&["battery"]),
248 rate: RateClass::watch(),
249 policy: build::map(Vec::new()),
250 }),
251 DeviceTier::Rich => DeviceProfile::new(DeviceProfileParts {
252 kind: Symbol::new("rich"),
253 display: symbols(&["stereo", "hud"]),
254 input: symbols(&["voice"]),
255 output: symbols(&["hud", "speaker"]),
256 links: symbols(&["lan-ws"]),
257 streams: symbols(&["pose", "motion"]),
258 rate: RateClass::stereo(),
259 policy: build::map(Vec::new()),
260 }),
261 }
262}
263
264pub fn device_profile_demo() -> Expr {
266 tier_preset(DeviceTier::Rich).to_expr()
267}
268
269#[derive(Clone, Debug, PartialEq, Eq)]
271pub enum DeviceProfileError {
272 NotProfile,
274 MissingField(&'static str),
276 BadField(&'static str),
278 Rate(RateError),
280 TierMismatch {
282 declared: DeviceTier,
284 derived: DeviceTier,
286 },
287}
288
289impl core::fmt::Display for DeviceProfileError {
290 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
291 match self {
292 DeviceProfileError::NotProfile => write!(f, "value is not a device/profile map"),
293 DeviceProfileError::MissingField(name) => {
294 write!(f, "device profile missing field: {name}")
295 }
296 DeviceProfileError::BadField(name) => {
297 write!(f, "device profile field has wrong shape: {name}")
298 }
299 DeviceProfileError::Rate(err) => write!(f, "device profile rate: {err}"),
300 DeviceProfileError::TierMismatch { declared, derived } => write!(
301 f,
302 "device profile tier mismatch: declared {}, derived {}",
303 declared.token(),
304 derived.token()
305 ),
306 }
307 }
308}
309
310impl std::error::Error for DeviceProfileError {}
311
312fn symbol_vec(
313 entries: &[(Expr, Expr)],
314 name: &'static str,
315) -> Result<Vec<Symbol>, DeviceProfileError> {
316 match access::entry_field(entries, name) {
317 Some(Expr::List(items)) => items
318 .iter()
319 .map(|item| match item {
320 Expr::Symbol(symbol) => Ok(symbol.clone()),
321 _ => Err(DeviceProfileError::BadField(name)),
322 })
323 .collect(),
324 Some(_) => Err(DeviceProfileError::BadField(name)),
325 None => Err(DeviceProfileError::MissingField(name)),
326 }
327}
328
329fn symbol_list(symbols: &[Symbol]) -> Expr {
330 build::list(symbols.iter().cloned().map(Expr::Symbol).collect())
331}
332
333fn display_symbols(display: &Expr) -> Vec<Symbol> {
334 let mut out = Vec::new();
335 if matches!(access::field(display, "stereo"), Some(Expr::Bool(true))) {
336 push_symbol(&mut out, "stereo");
337 }
338 if access::field(display, "lines").is_some() {
339 push_symbol(&mut out, "hud");
340 }
341 push_symbol_field(&mut out, display, "shape");
342 push_symbol_field(&mut out, display, "density");
343 if out.is_empty() && matches!(display, Expr::Map(entries) if !entries.is_empty()) {
344 push_symbol(&mut out, "flat");
345 }
346 out
347}
348
349fn flag_symbols(map: &Expr) -> Vec<Symbol> {
350 let Expr::Map(entries) = map else {
351 return Vec::new();
352 };
353 let mut out = Vec::new();
354 for (key, value) in entries {
355 if !matches!(value, Expr::Bool(true)) {
356 continue;
357 }
358 if let Expr::Symbol(symbol) = key
359 && symbol.namespace.is_none()
360 {
361 push_existing(&mut out, symbol.clone());
362 }
363 }
364 out
365}
366
367fn output_symbols(display: &[Symbol], input: &[Symbol]) -> Vec<Symbol> {
368 let mut out = Vec::new();
369 if !has_symbol(display, "none") && !display.is_empty() {
370 push_symbol(&mut out, "screen");
371 }
372 if has_symbol(display, "hud") || has_symbol(display, "stereo") {
373 push_symbol(&mut out, "hud");
374 }
375 if has_symbol(input, "haptic-ack") {
376 push_symbol(&mut out, "haptic");
377 }
378 out
379}
380
381fn link_symbols(transport: &Expr) -> Vec<Symbol> {
382 let mut out = Vec::new();
383 match access::field(transport, "kind") {
384 Some(Expr::Symbol(kind)) if kind.name.as_ref() == "relay" => {
385 push_symbol(&mut out, "phone-relay");
386 }
387 Some(Expr::Symbol(kind)) if kind.name.as_ref() == "websocket" => {
388 push_symbol(&mut out, "lan-ws");
389 }
390 Some(Expr::Symbol(kind)) if matches!(kind.name.as_ref(), "tty" | "local") => {
391 push_symbol(&mut out, "usb");
392 }
393 Some(Expr::Symbol(kind)) => push_existing(&mut out, kind.clone()),
394 _ => {}
395 }
396 out
397}
398
399fn stream_symbols(input: &Expr) -> Vec<Symbol> {
400 let mut out = Vec::new();
401 if matches!(access::field(input, "camera"), Some(Expr::Bool(true))) {
402 push_symbol(&mut out, "motion");
403 }
404 out
405}
406
407fn push_symbol_field(out: &mut Vec<Symbol>, map: &Expr, name: &str) {
408 if let Some(Expr::Symbol(symbol)) = access::field(map, name) {
409 push_existing(out, symbol.clone());
410 }
411}
412
413fn symbols(names: &[&str]) -> Vec<Symbol> {
414 names.iter().map(|name| Symbol::new(*name)).collect()
415}
416
417pub(crate) fn has_symbol(symbols: &[Symbol], name: &str) -> bool {
418 symbols
419 .iter()
420 .any(|symbol| symbol.namespace.is_none() && symbol.name.as_ref() == name)
421}
422
423pub(crate) fn has_any(symbols: &[Symbol], names: &[&str]) -> bool {
424 names.iter().any(|name| has_symbol(symbols, name))
425}
426
427pub(crate) fn push_symbol(out: &mut Vec<Symbol>, name: &str) {
428 push_existing(out, Symbol::new(name.to_owned()));
429}
430
431pub(crate) fn push_existing(out: &mut Vec<Symbol>, symbol: Symbol) {
432 if !out.iter().any(|existing| existing == &symbol) {
433 out.push(symbol);
434 }
435}
436
437fn dedup_symbols(symbols: Vec<Symbol>) -> Vec<Symbol> {
438 let mut out = Vec::new();
439 for symbol in symbols {
440 push_existing(&mut out, symbol);
441 }
442 out
443}