1use sim_kernel::{Error, Expr, Symbol};
27use sim_value::{access, build};
28
29pub const SURFACE_NAMESPACE: &str = "surface";
31
32pub const CAPS_KIND: &str = "caps";
34
35pub const SURFACE_PRESETS: &[&str] = &[
41 "cli",
42 "tui",
43 "webui",
44 "watch",
45 "watch-glance",
46 "watch-glance-large",
47 "watch-sport",
48 "watch-sleep",
49 "glasses",
50 "glasses-hud",
51 "glasses-hud-camera",
52 "glasses-3dof",
53 "glasses-stereo",
54 "glasses-luma-ultra",
55 "phone",
56 "desktop",
57];
58
59#[derive(Clone, Debug, PartialEq)]
66pub struct SurfaceCaps {
67 pub client_id: String,
69 pub preset: Symbol,
71 pub display: Expr,
73 pub input: Expr,
75 pub output: Expr,
77 pub transport: Expr,
79 pub privacy: Expr,
81 pub rate: Expr,
83 pub streams: Expr,
85 pub codecs: Vec<Symbol>,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum SurfaceError {
94 NotCaps,
96 MissingField(&'static str),
98 BadField(&'static str),
100 Field(String),
106}
107
108impl core::fmt::Display for SurfaceError {
109 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110 match self {
111 SurfaceError::NotCaps => write!(f, "value is not a surface/caps map"),
112 SurfaceError::MissingField(name) => write!(f, "surface caps missing field: {name}"),
113 SurfaceError::BadField(name) => write!(f, "surface caps field has wrong shape: {name}"),
114 SurfaceError::Field(message) => write!(f, "surface caps field: {message}"),
115 }
116 }
117}
118
119impl std::error::Error for SurfaceError {}
120
121impl From<Error> for SurfaceError {
122 fn from(err: Error) -> Self {
127 SurfaceError::Field(err.to_string())
128 }
129}
130
131impl SurfaceCaps {
132 pub fn from_preset(preset_name: &str, client_id: impl Into<String>) -> Option<Self> {
136 let mut caps = preset(preset_name)?;
137 caps.client_id = client_id.into();
138 Some(caps)
139 }
140
141 pub fn to_expr(&self) -> Expr {
143 build::map(vec![
144 (
145 "kind",
146 Expr::Symbol(Symbol::qualified(SURFACE_NAMESPACE, CAPS_KIND)),
147 ),
148 ("client-id", build::text(self.client_id.clone())),
149 ("preset", Expr::Symbol(self.preset.clone())),
150 ("display", self.display.clone()),
151 ("input", self.input.clone()),
152 ("output", self.output.clone()),
153 ("transport", self.transport.clone()),
154 ("privacy", self.privacy.clone()),
155 ("rate", self.rate.clone()),
156 ("streams", self.streams.clone()),
157 (
158 "codecs",
159 build::list(self.codecs.iter().cloned().map(Expr::Symbol).collect()),
160 ),
161 ])
162 }
163
164 pub fn from_expr(expr: &Expr) -> Result<Self, SurfaceError> {
166 let Expr::Map(entries) = expr else {
167 return Err(SurfaceError::NotCaps);
168 };
169 match access::entry_field(entries, "kind") {
170 Some(Expr::Symbol(kind))
171 if kind.namespace.as_deref() == Some(SURFACE_NAMESPACE)
172 && &*kind.name == CAPS_KIND => {}
173 _ => return Err(SurfaceError::NotCaps),
174 }
175 let client_id = match access::entry_field(entries, "client-id") {
176 Some(Expr::String(text)) => text.clone(),
177 Some(_) => return Err(SurfaceError::BadField("client-id")),
178 None => return Err(SurfaceError::MissingField("client-id")),
179 };
180 let preset = match access::entry_field(entries, "preset") {
181 Some(Expr::Symbol(symbol)) => symbol.clone(),
182 Some(_) => return Err(SurfaceError::BadField("preset")),
183 None => return Err(SurfaceError::MissingField("preset")),
184 };
185 let display = map_field(entries, "display")?;
186 let input = map_field(entries, "input")?;
187 let output = optional_map_field(entries, "output")?;
188 let transport = map_field(entries, "transport")?;
189 let privacy = map_field(entries, "privacy")?;
190 let rate = match access::entry_field(entries, "rate") {
191 Some(value @ Expr::Map(_)) => value.clone(),
192 Some(_) => return Err(SurfaceError::BadField("rate")),
193 None => rate_map(1, 1, 1000),
194 };
195 let streams = optional_map_field(entries, "streams")?;
196 let codecs = match access::entry_field(entries, "codecs") {
197 Some(Expr::List(items)) => {
198 let mut out = Vec::with_capacity(items.len());
199 for item in items {
200 let Expr::Symbol(symbol) = item else {
201 return Err(SurfaceError::BadField("codecs"));
202 };
203 out.push(symbol.clone());
204 }
205 out
206 }
207 Some(_) => return Err(SurfaceError::BadField("codecs")),
208 None => return Err(SurfaceError::MissingField("codecs")),
209 };
210 Ok(SurfaceCaps {
211 client_id,
212 preset,
213 display,
214 input,
215 output,
216 transport,
217 privacy,
218 rate,
219 streams,
220 codecs,
221 })
222 }
223
224 pub fn preset_name(&self) -> &str {
226 &self.preset.name
227 }
228
229 pub fn input_flag(&self, name: &str) -> bool {
231 matches!(access::field(&self.input, name), Some(Expr::Bool(true)))
232 }
233
234 pub fn display_density(&self) -> Option<Symbol> {
236 match access::field(&self.display, "density") {
237 Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
238 _ => None,
239 }
240 }
241
242 pub fn accepts_codec(&self, codec: &str) -> bool {
244 self.codecs.iter().any(|symbol| &*symbol.name == codec)
245 }
246}
247
248pub fn preset(name: &str) -> Option<SurfaceCaps> {
253 let (display, input, output, transport, privacy, rate, streams) = match name {
254 "cli" => (
255 display_map(&[("density", sym("dense")), ("color", sym("ansi"))]),
256 input_map(&["keyboard"]),
257 output_map(&["screen"]),
258 transport_map("tty", 1, false),
259 privacy_map("local", 60_000),
260 rate_map(1, 1, 1000),
261 streams_map(&[]),
262 ),
263 "tui" => (
264 display_map(&[("density", sym("dense")), ("color", sym("ansi256"))]),
265 input_map(&["keyboard", "pointer"]),
266 output_map(&["screen"]),
267 transport_map("tty", 1, false),
268 privacy_map("local", 60_000),
269 rate_map(1, 1, 1000),
270 streams_map(&[]),
271 ),
272 "webui" => (
273 display_map(&[("density", sym("regular")), ("color", sym("truecolor"))]),
274 input_map(&["keyboard", "pointer", "touch", "wheel", "file-drop"]),
275 output_map(&["screen"]),
276 transport_map("websocket", 40, false),
277 privacy_map("session", 600_000),
278 rate_map(5, 30, 500),
279 streams_map(&[]),
280 ),
281 "watch" => (
282 watch_display_map("generic-round-watch", 480, 48),
283 input_map(&[
284 "button",
285 "touch",
286 "tap",
287 "raise",
288 "mic",
289 "crown",
290 "haptic-ack",
291 ]),
292 watch_output_map(),
293 transport_map_with_links("phone-relay", 250, true, &["phone-relay", "ble"]),
294 privacy_map("local", 60_000),
295 watch_rate_map(),
296 streams_map(&["heart-rate", "motion", "battery", "connection"]),
297 ),
298 "watch-glance" => (
299 watch_display_map("amazfit-t-rex-3-pro-44", 466, 44),
300 input_map(&["button", "touch", "tap", "raise", "mic", "haptic-ack"]),
301 watch_output_map(),
302 transport_map_with_links(
303 "phone-relay",
304 250,
305 true,
306 &["modeled", "phone-relay", "ble", "zepp-export"],
307 ),
308 privacy_map("local", 60_000),
309 watch_rate_map(),
310 streams_map(&[
311 "heart-rate",
312 "motion",
313 "location",
314 "environment",
315 "battery",
316 "connection",
317 ]),
318 ),
319 "watch-glance-large" => (
320 watch_display_map("amazfit-t-rex-3-pro-48", 480, 48),
321 input_map(&["button", "touch", "tap", "raise", "mic", "haptic-ack"]),
322 watch_output_map(),
323 transport_map_with_links(
324 "phone-relay",
325 250,
326 true,
327 &["modeled", "phone-relay", "ble", "zepp-export"],
328 ),
329 privacy_map("local", 60_000),
330 watch_rate_map(),
331 streams_map(&[
332 "heart-rate",
333 "motion",
334 "location",
335 "environment",
336 "battery",
337 "connection",
338 ]),
339 ),
340 "watch-sport" => (
341 watch_display_map("amazfit-t-rex-3-pro-48", 480, 48),
342 input_map(&["button", "touch", "tap", "raise", "mic", "haptic-ack"]),
343 watch_output_map(),
344 transport_map_with_links(
345 "phone-relay",
346 250,
347 true,
348 &["modeled", "phone-relay", "ble", "zepp-export"],
349 ),
350 privacy_map("local", 60_000),
351 watch_rate_map(),
352 streams_map(&[
353 "heart-rate",
354 "motion",
355 "location",
356 "environment",
357 "battery",
358 "connection",
359 ]),
360 ),
361 "watch-sleep" => (
362 watch_display_map("amazfit-t-rex-3-pro-44", 466, 44),
363 input_map(&["button", "tap", "raise", "haptic-ack"]),
364 output_map(&["screen", "haptic", "tone"]),
365 transport_map_with_links(
366 "phone-relay",
367 250,
368 true,
369 &["modeled", "phone-relay", "zepp-export"],
370 ),
371 privacy_map("local", 60_000),
372 watch_rate_map(),
373 streams_map(&["heart-rate", "motion", "battery"]),
374 ),
375 "glasses" => (
376 glasses_display_map(
377 "generic-display",
378 "mono",
379 (1280, 720),
380 40,
381 "display-only",
382 &[],
383 &["screen"],
384 ),
385 input_map(&["button"]),
386 output_map(&["screen"]),
387 transport_map_with_links("usb", 20, false, &["usb"]),
388 privacy_map("local", 60_000),
389 rate_map(1, 1, 1000),
390 streams_map(&[]),
391 ),
392 "glasses-hud" => (
393 halo_display_map(&[]),
394 halo_input_map(false),
395 halo_output_map(),
396 halo_transport_map(),
397 privacy_map("local", 60_000),
398 rate_map(5, 30, 200),
399 streams_map(&["motion", "mic", "battery", "connection"]),
400 ),
401 "glasses-hud-camera" => (
402 halo_display_map(&["camera"]),
403 halo_input_map(true),
404 halo_output_map(),
405 halo_transport_map(),
406 privacy_map("local", 60_000),
407 rate_map(5, 30, 200),
408 streams_map(&["motion", "camera", "mic", "battery", "connection"]),
409 ),
410 "glasses-3dof" => (
411 viture_display_map("3dof"),
412 input_map(&["head", "button"]),
413 output_map(&["screen"]),
414 transport_map_with_links("usb", 20, false, &["usb", "phone-relay"]),
415 privacy_map("local", 60_000),
416 rate_map(60, 120, 50),
417 streams_map(&["motion"]),
418 ),
419 "glasses-stereo" => (
420 viture_display_map("display-only"),
421 input_map(&["button"]),
422 output_map(&["screen"]),
423 transport_map_with_links("usb", 20, false, &["usb"]),
424 privacy_map("local", 60_000),
425 rate_map(1, 1, 1000),
426 streams_map(&[]),
427 ),
428 "glasses-luma-ultra" => (
429 viture_display_map("6dof"),
430 input_map(&["gaze", "head", "hand", "button"]),
431 output_map(&["screen", "hud"]),
432 transport_map_with_links("usb", 10, false, &["usb"]),
433 privacy_map("local", 60_000),
434 rate_map(60, 120, 25),
435 streams_map(&[
436 "pose",
437 "motion",
438 "camera",
439 "depth-camera",
440 "hand",
441 "vio-status",
442 ]),
443 ),
444 "phone" => (
445 display_map(&[("density", sym("compact")), ("color", sym("truecolor"))]),
446 input_map(&["touch", "voice", "camera"]),
447 output_map(&["screen", "speaker", "mic"]),
448 transport_map("relay", 120, true),
449 privacy_map("session", 300_000),
450 rate_map(5, 30, 500),
451 streams_map(&["motion"]),
452 ),
453 "desktop" => (
454 display_map(&[("density", sym("dense")), ("color", sym("truecolor"))]),
455 input_map(&["keyboard", "pointer", "wheel", "file-drop"]),
456 output_map(&["screen"]),
457 transport_map("local", 1, false),
458 privacy_map("session", 600_000),
459 rate_map(60, 120, 100),
460 streams_map(&[]),
461 ),
462 _ => return None,
463 };
464 Some(SurfaceCaps {
465 client_id: name.to_owned(),
466 preset: Symbol::qualified(SURFACE_NAMESPACE, name),
467 display,
468 input,
469 output,
470 transport,
471 privacy,
472 rate,
473 streams,
474 codecs: vec![
475 Symbol::qualified(SURFACE_NAMESPACE, "lisp"),
476 Symbol::qualified(SURFACE_NAMESPACE, "json"),
477 ],
478 })
479}
480
481use sim_value::build::sym;
482
483fn display_map(extra: &[(&str, Expr)]) -> Expr {
484 let mut entries: Vec<(&str, Expr)> = vec![("media", build::list(Vec::new()))];
485 entries.extend(extra.iter().map(|(k, v)| (*k, v.clone())));
486 build::map(entries)
487}
488
489fn input_map(flags: &[&str]) -> Expr {
490 build::map(flags.iter().map(|flag| (*flag, Expr::Bool(true))).collect())
491}
492
493fn output_map(flags: &[&str]) -> Expr {
494 input_map(flags)
495}
496
497fn streams_map(flags: &[&str]) -> Expr {
498 input_map(flags)
499}
500
501fn watch_display_map(model: &str, px: u64, size_mm: u64) -> Expr {
502 display_map(&[
503 ("class", sym("watch")),
504 ("shape", sym("round")),
505 ("model", sym(model)),
506 ("px", build::list(vec![build::uint(px), build::uint(px)])),
507 ("size-mm", build::uint(size_mm)),
508 ("color", sym("truecolor")),
509 ("max-hz", build::uint(1)),
510 ("density", sym("glance")),
511 ])
512}
513
514fn watch_output_map() -> Expr {
515 output_map(&["screen", "haptic", "face", "tone", "speaker", "mic"])
516}
517
518fn glasses_display_map(
519 model: &str,
520 display: &str,
521 px: (u64, u64),
522 fov_deg: u64,
523 tracking: &str,
524 cameras: &[&str],
525 anchor_spaces: &[&str],
526) -> Expr {
527 let mut entries = vec![
528 ("class", sym("glasses")),
529 ("model", sym(model)),
530 ("display", sym(display)),
531 ("fov-deg", build::uint(fov_deg)),
532 ("tracking-class", sym(tracking)),
533 ("cameras", sym_list(cameras)),
534 ("anchor-spaces", sym_list(anchor_spaces)),
535 ("density", sym("glance")),
536 ];
537 match display {
538 "mono" => {
539 entries.push(("mono", Expr::Bool(true)));
540 entries.push(("mono-px", px_pair(px.0, px.1)));
541 }
542 "stereo" => {
543 entries.push(("stereo", Expr::Bool(true)));
544 entries.push(("per-eye-px", px_pair(px.0, px.1)));
545 }
546 "none" => entries.push(("none", Expr::Bool(true))),
547 _ => {}
548 }
549 display_map(&entries)
550}
551
552fn halo_display_map(cameras: &[&str]) -> Expr {
553 let mut display = match glasses_display_map(
554 "brilliant-halo",
555 "mono",
556 (256, 256),
557 20,
558 "hud",
559 cameras,
560 &["screen"],
561 ) {
562 Expr::Map(entries) => entries,
563 _ => unreachable!(),
564 };
565 display.push((Expr::Symbol(build::keyword("lines")), build::uint(1)));
566 Expr::Map(display)
567}
568
569fn halo_input_map(camera: bool) -> Expr {
570 if camera {
571 input_map(&["voice", "tap", "button", "camera", "haptic-ack"])
572 } else {
573 input_map(&["voice", "tap", "button", "haptic-ack"])
574 }
575}
576
577fn halo_output_map() -> Expr {
578 output_map(&["screen", "hud", "audio", "speaker", "haptic"])
579}
580
581fn halo_transport_map() -> Expr {
582 transport_map_with_links(
583 "bluetooth",
584 80,
585 true,
586 &["bluetooth", "web-bluetooth", "phone-relay"],
587 )
588}
589
590fn viture_display_map(tracking: &str) -> Expr {
591 glasses_display_map(
592 "viture-luma-ultra",
593 "stereo",
594 (1920, 1200),
595 52,
596 tracking,
597 &["rgb-camera", "depth-camera"],
598 &["head", "world", "hand"],
599 )
600}
601
602fn px_pair(w: u64, h: u64) -> Expr {
603 build::list(vec![build::uint(w), build::uint(h)])
604}
605
606fn sym_list(names: &[&str]) -> Expr {
607 build::list(names.iter().map(|name| sym(name)).collect())
608}
609
610fn transport_map(kind: &str, round_trip_ms: u64, offline_queue: bool) -> Expr {
611 transport_map_with_links(kind, round_trip_ms, offline_queue, &[])
612}
613
614fn transport_map_with_links(
615 kind: &str,
616 round_trip_ms: u64,
617 offline_queue: bool,
618 links: &[&str],
619) -> Expr {
620 build::map(vec![
621 ("kind", build::sym(kind)),
622 ("round-trip-ms", build::uint(round_trip_ms)),
623 ("offline-queue", Expr::Bool(offline_queue)),
624 ("ordered", Expr::Bool(true)),
625 (
626 "links",
627 build::list(links.iter().map(|link| build::sym(link)).collect()),
628 ),
629 ])
630}
631
632fn privacy_map(class: &str, retain_ms: u64) -> Expr {
633 build::map(vec![
634 ("class", build::sym(class)),
635 ("retain-ms", build::uint(retain_ms)),
636 ("private-fields", build::list(Vec::new())),
637 ])
638}
639
640fn rate_map(content_hz: u64, adapt_hz: u64, max_stale_ms: u64) -> Expr {
641 build::map(vec![
642 ("content-hz", build::uint(content_hz)),
643 ("adapt-hz", build::uint(adapt_hz)),
644 ("max-stale-ms", build::uint(max_stale_ms)),
645 ])
646}
647
648fn watch_rate_map() -> Expr {
649 rate_map(1, 1, 4000)
650}
651
652fn map_field(entries: &[(Expr, Expr)], name: &'static str) -> Result<Expr, SurfaceError> {
653 match access::entry_required(entries, name, "surface caps").map_err(SurfaceError::from)? {
658 value @ Expr::Map(_) => Ok(value.clone()),
659 _ => Err(SurfaceError::BadField(name)),
660 }
661}
662
663fn optional_map_field(entries: &[(Expr, Expr)], name: &'static str) -> Result<Expr, SurfaceError> {
664 match access::entry_field(entries, name) {
665 Some(value @ Expr::Map(_)) => Ok(value.clone()),
666 Some(_) => Err(SurfaceError::BadField(name)),
667 None => Ok(build::map(Vec::new())),
668 }
669}
670
671#[cfg(test)]
672#[path = "surface_tests.rs"]
673mod tests;