Skip to main content

sim_lib_view_spatial/
co_use.rs

1//! Dual-glasses co-use roles and profile validation.
2
3use sim_kernel::{Error, Result};
4use sim_lib_view::SurfaceCaps;
5use sim_lib_view_device::{
6    ConsentReceipt, DeviceProfile, DeviceSurfaceCapsExt, EdgeId, GlassesClass, StalePolicy,
7    glasses_class,
8};
9
10/// A glasses peer inside one worn co-use session.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum GlassesPeer {
13    /// The Viture Luma Ultra focus surface.
14    Viture,
15    /// The Brilliant Labs Halo ambient peer.
16    Halo,
17}
18
19impl GlassesPeer {
20    /// Returns this peer's role in the shared session.
21    pub fn role(self) -> GlassesCoUseRole {
22        match self {
23            Self::Viture => GlassesCoUseRole::Main,
24            Self::Halo => GlassesCoUseRole::Peer,
25        }
26    }
27
28    /// Returns the expected glasses class for this peer.
29    pub fn expected_class(self) -> GlassesClass {
30        match self {
31            Self::Viture => GlassesClass::Stereo6Dof,
32            Self::Halo => GlassesClass::MonoHud,
33        }
34    }
35
36    /// Returns the peer's stable label.
37    pub fn label(self) -> &'static str {
38        match self {
39            Self::Viture => "viture",
40            Self::Halo => "halo",
41        }
42    }
43}
44
45/// Role of a glasses peer inside one co-use session.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum GlassesCoUseRole {
48    /// Primary focus surface for the shared session.
49    Main,
50    /// Secondary peer surface for the same canonical session.
51    Peer,
52}
53
54/// Validated peer profile and adapter-loop policy.
55#[derive(Clone, Debug, PartialEq)]
56pub struct GlassesPeerConfig {
57    /// Which glasses peer this config describes.
58    pub peer: GlassesPeer,
59    /// Role assigned to the peer.
60    pub role: GlassesCoUseRole,
61    /// The validated device profile.
62    pub profile: DeviceProfile,
63    /// Stale policy used by the peer's local adapter loop.
64    pub policy: StalePolicy,
65}
66
67/// A co-use plan bound to one session and consent receipt.
68#[derive(Clone, Debug, PartialEq)]
69pub struct GlassesCoUsePlan {
70    session: EdgeId,
71    consent: ConsentReceipt,
72    peers: Vec<GlassesPeerConfig>,
73}
74
75impl GlassesCoUsePlan {
76    /// Builds an empty plan bound to `session` and its consent receipt.
77    pub fn new(session: EdgeId, consent: ConsentReceipt) -> Result<Self> {
78        if consent.session != session {
79            return Err(Error::HostError(
80                "glasses co-use consent receipt is bound to a different session".to_owned(),
81            ));
82        }
83        Ok(Self {
84            session,
85            consent,
86            peers: Vec::new(),
87        })
88    }
89
90    /// Attach or replace one peer profile in the plan.
91    pub fn attach_caps(
92        &mut self,
93        peer: GlassesPeer,
94        caps: &SurfaceCaps,
95    ) -> Result<GlassesPeerConfig> {
96        let config = glasses_peer_config(peer, caps)?;
97        self.peers.retain(|existing| existing.peer != peer);
98        self.peers.push(config.clone());
99        Ok(config)
100    }
101
102    /// Detach one peer from the plan.
103    pub fn detach(&mut self, peer: GlassesPeer) -> Option<GlassesPeerConfig> {
104        let index = self
105            .peers
106            .iter()
107            .position(|existing| existing.peer == peer)?;
108        Some(self.peers.remove(index))
109    }
110
111    /// Returns true while any peer still holds the session.
112    pub fn is_alive(&self) -> bool {
113        !self.peers.is_empty()
114    }
115
116    /// Returns the shared session id.
117    pub fn session(&self) -> &EdgeId {
118        &self.session
119    }
120
121    /// Returns the session-bound consent receipt.
122    pub fn consent(&self) -> &ConsentReceipt {
123        &self.consent
124    }
125
126    /// Returns the attached peer configs.
127    pub fn peers(&self) -> &[GlassesPeerConfig] {
128        &self.peers
129    }
130}
131
132/// Validates caps for a specific glasses peer and returns its config.
133pub fn glasses_peer_config(peer: GlassesPeer, caps: &SurfaceCaps) -> Result<GlassesPeerConfig> {
134    let profile = caps.device_profile();
135    let class = glasses_class(&profile).ok_or_else(|| {
136        Error::HostError(format!("{} caps do not describe glasses", peer.label()))
137    })?;
138    if class != peer.expected_class() {
139        return Err(Error::HostError(format!(
140            "{} caps resolve to {class:?}, expected {:?}",
141            peer.label(),
142            peer.expected_class()
143        )));
144    }
145    Ok(GlassesPeerConfig {
146        peer,
147        role: peer.role(),
148        profile,
149        policy: peer_policy(peer),
150    })
151}
152
153fn peer_policy(peer: GlassesPeer) -> StalePolicy {
154    match peer {
155        GlassesPeer::Viture => StalePolicy::Predict,
156        GlassesPeer::Halo => StalePolicy::HoldLast,
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    use sim_lib_view_device::DeviceCapability;
165
166    use crate::{halo_loop, viture_loop};
167
168    #[test]
169    fn viture_and_halo_share_one_session() {
170        let edge = EdgeId::named("wear-session");
171        let consent = ConsentReceipt::new(
172            vec![
173                DeviceCapability::Pose.grant_symbol(),
174                DeviceCapability::Mic.grant_symbol(),
175            ],
176            60_000,
177            Vec::new(),
178            edge.clone(),
179            9,
180        );
181        let mut plan = GlassesCoUsePlan::new(edge.clone(), consent.clone()).unwrap();
182
183        let viture_caps = SurfaceCaps::from_preset("glasses-luma-ultra", "viture.co-use").unwrap();
184        let halo_caps = SurfaceCaps::from_preset("glasses-hud", "halo.co-use").unwrap();
185        let viture = plan.attach_caps(GlassesPeer::Viture, &viture_caps).unwrap();
186        let halo = plan.attach_caps(GlassesPeer::Halo, &halo_caps).unwrap();
187
188        assert_eq!(plan.session(), &edge);
189        assert_eq!(plan.consent(), &consent);
190        assert_eq!(plan.peers().len(), 2);
191        assert_eq!(viture.role, GlassesCoUseRole::Main);
192        assert_eq!(halo.role, GlassesCoUseRole::Peer);
193        assert_eq!(viture.policy, StalePolicy::Predict);
194        assert_eq!(halo.policy, StalePolicy::HoldLast);
195        assert_eq!(viture_loop(&viture.profile, 12).0.policy(), viture.policy);
196        assert_eq!(halo_loop(&halo.profile).0.policy(), halo.policy);
197
198        assert!(plan.detach(GlassesPeer::Viture).is_some());
199        assert!(plan.is_alive(), "Halo keeps the shared session alive");
200        assert_eq!(plan.peers()[0].peer, GlassesPeer::Halo);
201        assert_eq!(plan.session(), &edge);
202        assert_eq!(plan.consent(), &consent);
203    }
204}