1use sim_kernel::{Error, Expr, Result, Symbol};
9use sim_value::{access, build};
10
11pub const GLASSES_BRINGUP_NAMESPACE: &str = "glasses";
13
14pub const GLASSES_BRINGUP_KIND: &str = "bringup";
16
17pub const GLASSES_BRINGUP_FIXTURE: &str = "glasses/bringup";
19
20pub const GLASSES_BRINGUP_FIXTURE_TEXT: &str = include_str!("../glasses/bringup");
22
23pub const VITURE_CARINA_LANE: &str = "viture_carina";
25
26pub const VITURE_LEGACY_IMU_LANE: &str = "viture_legacy";
28
29pub const VITURE_UVC_CAMERA_LANE: &str = "viture_uvc_cam";
31
32pub const HALO_BLE_DIRECT_LANE: &str = "halo_ble_direct";
34
35pub const HALO_WEB_BLUETOOTH_LANE: &str = "halo_web_bt";
37
38pub const HALO_PHONE_RELAY_LANE: &str = "halo_phone_relay";
40
41pub const HALO_CAMERA_LANE: &str = "halo_camera";
43
44pub const GLASSES_BRINGUP_LANES: [&str; 7] = [
46 VITURE_CARINA_LANE,
47 VITURE_LEGACY_IMU_LANE,
48 VITURE_UVC_CAMERA_LANE,
49 HALO_BLE_DIRECT_LANE,
50 HALO_WEB_BLUETOOTH_LANE,
51 HALO_PHONE_RELAY_LANE,
52 HALO_CAMERA_LANE,
53];
54
55#[derive(Clone, Debug, PartialEq)]
57pub struct BringUpEntry {
58 pub lane: Symbol,
60 pub claims: Expr,
62 pub verified: bool,
64 pub firmware: Option<String>,
66 pub version: Option<String>,
68 pub notes: Vec<String>,
70}
71
72impl BringUpEntry {
73 pub fn unverified(lane: &str, claims: Expr, notes: Vec<String>) -> Self {
75 Self {
76 lane: Symbol::new(lane),
77 claims,
78 verified: false,
79 firmware: None,
80 version: None,
81 notes,
82 }
83 }
84
85 pub fn to_expr(&self) -> Expr {
87 build::map(vec![
88 ("lane", Expr::Symbol(self.lane.clone())),
89 ("claims", self.claims.clone()),
90 ("verified", Expr::Bool(self.verified)),
91 ("firmware", optional_text(self.firmware.as_deref())),
92 ("version", optional_text(self.version.as_deref())),
93 ("notes", string_list(&self.notes)),
94 ])
95 }
96
97 pub fn from_expr(lane: Symbol, expr: &Expr) -> Result<Self> {
99 let context = "glasses bring-up entry";
100 let entries = access::map_entries(expr, context)?;
101 if let Some(Expr::Symbol(field_lane)) = access::entry_field(entries, "lane")
102 && field_lane != &lane
103 {
104 return Err(Error::Eval(format!(
105 "{context} lane {} does not match map key {}",
106 field_lane.as_qualified_str(),
107 lane.as_qualified_str()
108 )));
109 }
110 let claims = access::entry_required(entries, "claims", context)?.clone();
111 access::map_entries(&claims, "glasses bring-up claims")?;
112 Ok(Self {
113 lane,
114 claims,
115 verified: access::entry_required_bool(entries, "verified", context)?,
116 firmware: required_optional_string(entries, "firmware", context)?,
117 version: required_optional_string(entries, "version", context)?,
118 notes: optional_string_list(entries, "notes", context)?,
119 })
120 }
121}
122
123#[derive(Clone, Debug, PartialEq)]
125pub struct BringUpLedger {
126 pub entries: Vec<BringUpEntry>,
128 pub notes: Vec<String>,
130}
131
132impl BringUpLedger {
133 pub fn default_glasses() -> Self {
135 Self {
136 entries: GLASSES_BRINGUP_LANES
137 .iter()
138 .copied()
139 .map(default_entry)
140 .collect(),
141 notes: vec![
142 "verified flags change only with hardware bring-up ledger evidence".to_owned(),
143 ],
144 }
145 }
146
147 pub fn from_expr(expr: &Expr) -> Result<Self> {
149 let context = "glasses bring-up ledger";
150 let entries = access::map_entries(expr, context)?;
151 let kind = access::entry_required_sym(entries, "kind", context)?;
152 let expected_kind = Symbol::qualified(GLASSES_BRINGUP_NAMESPACE, GLASSES_BRINGUP_KIND);
153 if kind != &expected_kind {
154 return Err(Error::Eval(format!(
155 "{context} kind must be {}",
156 expected_kind.as_qualified_str()
157 )));
158 }
159
160 let lanes =
161 access::map_entries(access::entry_required(entries, "lanes", context)?, context)?;
162 let mut decoded = Vec::with_capacity(lanes.len());
163 for (key, value) in lanes {
164 let lane = lane_key(key)?;
165 decoded.push(BringUpEntry::from_expr(lane, value)?);
166 }
167 let ledger = Self {
168 entries: decoded,
169 notes: optional_string_list(entries, "notes", context)?,
170 };
171 ledger.require_all_lanes()?;
172 Ok(ledger)
173 }
174
175 pub fn to_expr(&self) -> Expr {
177 Expr::Map(vec![
178 (
179 build::sym("kind"),
180 Expr::Symbol(Symbol::qualified(
181 GLASSES_BRINGUP_NAMESPACE,
182 GLASSES_BRINGUP_KIND,
183 )),
184 ),
185 (build::sym("fixture"), build::text(GLASSES_BRINGUP_FIXTURE)),
186 (
187 build::sym("lanes"),
188 Expr::Map(
189 self.entries
190 .iter()
191 .map(|entry| (Expr::Symbol(entry.lane.clone()), entry.to_expr()))
192 .collect(),
193 ),
194 ),
195 (build::sym("notes"), string_list(&self.notes)),
196 ])
197 }
198
199 pub fn entry(&self, lane: &str) -> Option<&BringUpEntry> {
201 self.entries.iter().find(|entry| lane_matches(entry, lane))
202 }
203
204 pub fn entry_mut(&mut self, lane: &str) -> Option<&mut BringUpEntry> {
206 self.entries
207 .iter_mut()
208 .find(|entry| lane_matches(entry, lane))
209 }
210
211 pub fn enable_lane(&self, lane: &str) -> Result<()> {
213 match self.entry(lane) {
214 Some(entry) if entry.verified => Ok(()),
215 _ => Err(Error::HostError(format!("lane {lane} not verified"))),
216 }
217 }
218
219 fn require_all_lanes(&self) -> Result<()> {
220 for lane in GLASSES_BRINGUP_LANES {
221 if self.entry(lane).is_none() {
222 return Err(Error::Eval(format!(
223 "glasses bring-up ledger is missing lane {lane}"
224 )));
225 }
226 }
227 Ok(())
228 }
229}
230
231pub fn glasses_bringup_fixture_names() -> [&'static str; 1] {
233 [GLASSES_BRINGUP_FIXTURE]
234}
235
236pub fn glasses_bringup_fixture(name: &str) -> Option<Expr> {
238 match name {
239 GLASSES_BRINGUP_FIXTURE => Some(default_glasses_bringup_fixture()),
240 _ => None,
241 }
242}
243
244pub fn default_glasses_bringup_fixture() -> Expr {
246 BringUpLedger::default_glasses().to_expr()
247}
248
249fn default_entry(lane: &str) -> BringUpEntry {
250 match lane {
251 VITURE_CARINA_LANE => BringUpEntry::unverified(
252 lane,
253 build::map(vec![
254 ("device", build::sym("viture-luma-ultra")),
255 ("route", build::sym("carina")),
256 ("sample", build::sym("pose-6dof")),
257 ]),
258 vec!["Carina pose frames gate the rich Viture reprojector".to_owned()],
259 ),
260 VITURE_LEGACY_IMU_LANE => BringUpEntry::unverified(
261 lane,
262 build::map(vec![
263 ("device", build::sym("viture-luma-ultra")),
264 ("route", build::sym("legacy-imu")),
265 ("sample", build::sym("pose-3dof")),
266 ]),
267 vec!["3DoF IMU frames support the display-only fallback".to_owned()],
268 ),
269 VITURE_UVC_CAMERA_LANE => BringUpEntry::unverified(
270 lane,
271 build::map(vec![
272 ("device", build::sym("viture-luma-ultra")),
273 ("route", build::sym("uvc-camera")),
274 ("sample", build::sym("camera-frame")),
275 ]),
276 vec!["UVC camera frames require by-reference storage and consent".to_owned()],
277 ),
278 HALO_BLE_DIRECT_LANE => BringUpEntry::unverified(
279 lane,
280 build::map(vec![
281 ("device", build::sym("halo")),
282 ("route", build::sym("ble-direct")),
283 ("sample", build::sym("lua-diff-frame")),
284 ]),
285 vec!["Direct BLE carries bounded Halo display diffs and tap input".to_owned()],
286 ),
287 HALO_WEB_BLUETOOTH_LANE => BringUpEntry::unverified(
288 lane,
289 build::map(vec![
290 ("device", build::sym("halo")),
291 ("route", build::sym("web-bluetooth")),
292 ("sample", build::sym("lua-diff-frame")),
293 ]),
294 vec!["Web Bluetooth carries bounded Halo display diffs and tap input".to_owned()],
295 ),
296 HALO_PHONE_RELAY_LANE => BringUpEntry::unverified(
297 lane,
298 build::map(vec![
299 ("device", build::sym("halo")),
300 ("route", build::sym("phone-relay")),
301 ("sample", build::sym("lua-diff-frame")),
302 ]),
303 vec!["Phone relay carries bounded Halo display diffs and tap input".to_owned()],
304 ),
305 HALO_CAMERA_LANE => BringUpEntry::unverified(
306 lane,
307 build::map(vec![
308 ("device", build::sym("halo")),
309 ("route", build::sym("camera")),
310 ("sample", build::sym("camera-frame")),
311 (
312 "resolution",
313 build::list(vec![build::uint(640), build::uint(480)]),
314 ),
315 ]),
316 vec!["Halo camera frames stay one-shot and consent-gated".to_owned()],
317 ),
318 _ => unreachable!("unknown glasses bring-up lane"),
319 }
320}
321
322fn lane_key(expr: &Expr) -> Result<Symbol> {
323 match expr {
324 Expr::Symbol(symbol) if symbol.namespace.is_none() => Ok(symbol.clone()),
325 Expr::String(value) => Ok(Symbol::new(value.as_str())),
326 _ => Err(Error::Eval(
327 "glasses bring-up lane key must be a bare symbol or string".to_owned(),
328 )),
329 }
330}
331
332fn lane_matches(entry: &BringUpEntry, lane: &str) -> bool {
333 entry.lane.namespace.is_none() && entry.lane.name.as_ref() == lane
334}
335
336fn optional_text(value: Option<&str>) -> Expr {
337 value.map(build::text).unwrap_or(Expr::Nil)
338}
339
340fn string_list(values: &[String]) -> Expr {
341 build::list(values.iter().cloned().map(build::text).collect())
342}
343
344fn required_optional_string(
345 entries: &[(Expr, Expr)],
346 name: &str,
347 context: &'static str,
348) -> Result<Option<String>> {
349 match access::entry_required(entries, name, context)? {
350 Expr::Nil => Ok(None),
351 Expr::String(value) => Ok(Some(value.clone())),
352 _ => Err(Error::TypeMismatch {
353 expected: "string or nil",
354 found: "non-string",
355 }),
356 }
357}
358
359fn optional_string_list(
360 entries: &[(Expr, Expr)],
361 name: &str,
362 context: &'static str,
363) -> Result<Vec<String>> {
364 let Some(value) = access::entry_field(entries, name) else {
365 return Ok(Vec::new());
366 };
367 let Expr::List(items) = value else {
368 return Err(Error::TypeMismatch {
369 expected: "list",
370 found: "non-list",
371 });
372 };
373 items
374 .iter()
375 .map(|item| match item {
376 Expr::String(value) => Ok(value.clone()),
377 _ => Err(Error::TypeMismatch {
378 expected: "string",
379 found: "non-string",
380 }),
381 })
382 .collect::<Result<Vec<_>>>()
383 .map_err(|error| Error::Eval(format!("{context} {name}: {error}")))
384}