Skip to main content

sim_lib_view/
bringup.rs

1//! Hardware bring-up ledger for glasses lanes.
2//!
3//! The ledger separates claimed hardware facts from verified route enablement.
4//! CI and modeled/stub paths use the default fixture with every lane unverified;
5//! a real hardware lane becomes enableable only after its entry is explicitly
6//! marked verified with firmware or version evidence.
7
8use sim_kernel::{Error, Expr, Result, Symbol};
9use sim_value::{access, build};
10
11/// Namespace for glasses bring-up ledger expressions.
12pub const GLASSES_BRINGUP_NAMESPACE: &str = "glasses";
13
14/// Kind tag for glasses bring-up ledger expressions.
15pub const GLASSES_BRINGUP_KIND: &str = "bringup";
16
17/// Built-in hardware-free glasses bring-up fixture name.
18pub const GLASSES_BRINGUP_FIXTURE: &str = "glasses/bringup";
19
20/// Committed quoted glasses bring-up fixture text.
21pub const GLASSES_BRINGUP_FIXTURE_TEXT: &str = include_str!("../glasses/bringup");
22
23/// Viture Carina 6DoF pose lane.
24pub const VITURE_CARINA_LANE: &str = "viture_carina";
25
26/// Viture 3DoF IMU lane.
27pub const VITURE_LEGACY_IMU_LANE: &str = "viture_legacy";
28
29/// Viture UVC/stereo camera lane.
30pub const VITURE_UVC_CAMERA_LANE: &str = "viture_uvc_cam";
31
32/// Halo direct BLE display and input lane.
33pub const HALO_BLE_DIRECT_LANE: &str = "halo_ble_direct";
34
35/// Halo Web Bluetooth display and input lane.
36pub const HALO_WEB_BLUETOOTH_LANE: &str = "halo_web_bt";
37
38/// Halo phone-relay display and input lane.
39pub const HALO_PHONE_RELAY_LANE: &str = "halo_phone_relay";
40
41/// Halo one-shot camera lane.
42pub const HALO_CAMERA_LANE: &str = "halo_camera";
43
44/// All hardware lanes that require bring-up verification before real use.
45pub 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/// One hardware lane's claimed facts and verification state.
56#[derive(Clone, Debug, PartialEq)]
57pub struct BringUpEntry {
58    /// Stable hardware lane token.
59    pub lane: Symbol,
60    /// Claimed hardware facts for this lane, stored as authored SIM data.
61    pub claims: Expr,
62    /// True only after a human hardware audit verifies the lane.
63    pub verified: bool,
64    /// Firmware identifier recorded with the verification evidence.
65    pub firmware: Option<String>,
66    /// Hardware, bridge, or provider version recorded with the verification evidence.
67    pub version: Option<String>,
68    /// Human-readable notes about the lane and its evidence.
69    pub notes: Vec<String>,
70}
71
72impl BringUpEntry {
73    /// Builds an unverified entry for one lane.
74    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    /// Encodes the entry as a SIM expression.
86    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    /// Decodes one entry expression whose map key supplied `lane`.
98    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/// Hardware bring-up ledger for all glasses lanes.
124#[derive(Clone, Debug, PartialEq)]
125pub struct BringUpLedger {
126    /// One entry per hardware lane.
127    pub entries: Vec<BringUpEntry>,
128    /// Notes that apply to the full ledger.
129    pub notes: Vec<String>,
130}
131
132impl BringUpLedger {
133    /// Builds the hardware-free default ledger.
134    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    /// Decodes a glasses bring-up ledger expression.
148    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    /// Encodes the ledger as a SIM expression.
176    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    /// Returns the entry for `lane`, if the ledger contains it.
200    pub fn entry(&self, lane: &str) -> Option<&BringUpEntry> {
201        self.entries.iter().find(|entry| lane_matches(entry, lane))
202    }
203
204    /// Returns the mutable entry for `lane`, if the ledger contains it.
205    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    /// Enables a lane only when its verified flag is true.
212    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
231/// Returns the built-in glasses bring-up fixture names.
232pub fn glasses_bringup_fixture_names() -> [&'static str; 1] {
233    [GLASSES_BRINGUP_FIXTURE]
234}
235
236/// Returns the named glasses bring-up fixture.
237pub 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
244/// Builds the default glasses bring-up fixture.
245pub 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}