1use sim_kernel::{Error, Expr, Result, Symbol};
4use sim_lib_view::SurfaceCaps;
5use sim_lib_view_device::{ConsentReceipt, EdgeId, FrameClock, StalePolicy};
6use sim_lib_view_spatial::{
7 GlassesCoUseRole, GlassesPeer, HaloGlanceLoop, VitureReprojectLoop, glasses_peer_config,
8 halo_loop, viture_loop,
9};
10use sim_value::{access, build};
11
12use crate::{Broadcast, EditRow, SurfaceBinding, SurfaceHub, SurfaceRole};
13
14const WORKSPACE_PANE: &str = "glasses-workspace";
15const DEFAULT_MAX_PREDICT_MS: u64 = 12;
16
17pub struct GlassesCoUseSession {
23 hub: SurfaceHub,
24 session: EdgeId,
25 consent: ConsentReceipt,
26 resource: Symbol,
27 pane: Symbol,
28 viture_surface: Symbol,
29 halo_surface: Symbol,
30 viture_loop: Option<VitureReprojectLoop>,
31 halo_loop: Option<HaloGlanceLoop>,
32 viture_clock: Option<FrameClock>,
33 halo_clock: Option<FrameClock>,
34}
35
36impl GlassesCoUseSession {
37 pub fn new(
39 session: EdgeId,
40 consent: ConsentReceipt,
41 resource: Symbol,
42 initial_workspace: Expr,
43 handle_seed: sim_kernel::HandleSeed,
44 ) -> Result<Self> {
45 if consent.session != session {
46 return Err(Error::HostError(
47 "glasses co-use consent receipt is bound to a different session".to_owned(),
48 ));
49 }
50 let mut hub = SurfaceHub::new(handle_seed);
51 hub.seed(resource.clone(), initial_workspace);
52 let viture_surface = glasses_surface(&session, GlassesPeer::Viture);
53 let halo_surface = glasses_surface(&session, GlassesPeer::Halo);
54 Ok(Self {
55 hub,
56 session,
57 consent,
58 resource,
59 pane: Symbol::new(WORKSPACE_PANE),
60 viture_surface,
61 halo_surface,
62 viture_loop: None,
63 halo_loop: None,
64 viture_clock: None,
65 halo_clock: None,
66 })
67 }
68
69 pub fn attach_viture(&mut self, caps: SurfaceCaps) -> Result<Expr> {
71 let config = glasses_peer_config(GlassesPeer::Viture, &caps)?;
72 let (loop_, clock) = viture_loop(&config.profile, DEFAULT_MAX_PREDICT_MS);
73 self.hub.register_surface_with_role(
74 self.viture_surface.clone(),
75 caps,
76 surface_role_for(config.role),
77 );
78 self.viture_loop = Some(loop_);
79 self.viture_clock = Some(clock);
80 self.hub.open(
81 &self.viture_surface,
82 self.pane.clone(),
83 self.resource.clone(),
84 )
85 }
86
87 pub fn attach_halo(&mut self, caps: SurfaceCaps) -> Result<Expr> {
89 let config = glasses_peer_config(GlassesPeer::Halo, &caps)?;
90 let (loop_, clock) = halo_loop(&config.profile);
91 self.hub.register_surface_with_role(
92 self.halo_surface.clone(),
93 caps,
94 surface_role_for(config.role),
95 );
96 self.halo_loop = Some(loop_);
97 self.halo_clock = Some(clock);
98 self.hub
99 .open(&self.halo_surface, self.pane.clone(), self.resource.clone())
100 }
101
102 pub fn detach_viture(&mut self) -> Vec<SurfaceBinding> {
104 self.viture_loop = None;
105 self.viture_clock = None;
106 self.hub.detach_surface(&self.viture_surface)
107 }
108
109 pub fn submit_from(&mut self, peer: GlassesPeer, intent: &Expr) -> Result<Vec<Broadcast>> {
111 let surface = self.surface(peer).clone();
112 self.hub.submit(&surface, &self.pane, intent)
113 }
114
115 pub fn commit_from(
117 &mut self,
118 peer: GlassesPeer,
119 intent: &Expr,
120 new_workspace: Expr,
121 ) -> Result<Vec<Broadcast>> {
122 let surface = self.surface(peer).clone();
123 self.hub
124 .commit_value_from(&surface, &self.pane, intent, new_workspace)
125 }
126
127 pub fn acknowledge_review_from_halo(
129 &mut self,
130 tap_intent: &Expr,
131 mission: Symbol,
132 ) -> Result<Vec<Broadcast>> {
133 let current = self.workspace()?.clone();
134 let review = access::field(¤t, "review")
135 .cloned()
136 .unwrap_or_else(|| build::map(Vec::new()));
137 let review = access::set(&review, "mission", Expr::Symbol(mission));
138 let review = access::set(&review, "status", build::sym("acked"));
139 let review = access::set(&review, "acked-by", Expr::Symbol(self.halo_surface.clone()));
140 let next = access::set(¤t, "review", review);
141 self.commit_from(GlassesPeer::Halo, tap_intent, next)
142 }
143
144 pub fn session(&self) -> &EdgeId {
146 &self.session
147 }
148
149 pub fn consent(&self) -> &ConsentReceipt {
151 &self.consent
152 }
153
154 pub fn workspace(&self) -> Result<&Expr> {
156 self.hub.canonical(&self.resource).ok_or_else(|| {
157 Error::HostError(format!(
158 "glasses workspace '{}' is not seeded",
159 self.resource
160 ))
161 })
162 }
163
164 pub fn ledger(&self) -> &[EditRow] {
166 self.hub.ledger()
167 }
168
169 pub fn live_bindings(&self) -> Vec<SurfaceBinding> {
171 self.hub.bindings_for_resource(&self.resource)
172 }
173
174 pub fn surface(&self, peer: GlassesPeer) -> &Symbol {
176 match peer {
177 GlassesPeer::Viture => &self.viture_surface,
178 GlassesPeer::Halo => &self.halo_surface,
179 }
180 }
181
182 pub fn role(&self, peer: GlassesPeer) -> Option<SurfaceRole> {
184 self.hub.surface_role(self.surface(peer))
185 }
186
187 pub fn viture_loop_policy(&self) -> Option<StalePolicy> {
189 self.viture_loop.as_ref().map(|loop_| loop_.policy())
190 }
191
192 pub fn halo_loop_policy(&self) -> Option<StalePolicy> {
194 self.halo_loop.as_ref().map(|loop_| loop_.policy())
195 }
196}
197
198pub fn glasses_surface(session: &EdgeId, peer: GlassesPeer) -> Symbol {
200 Symbol::qualified(
201 "device/peer",
202 format!(
203 "{}:{}",
204 session.as_symbol().as_qualified_str(),
205 peer.label()
206 ),
207 )
208}
209
210fn surface_role_for(role: GlassesCoUseRole) -> SurfaceRole {
211 match role {
212 GlassesCoUseRole::Main => SurfaceRole::Main,
213 GlassesCoUseRole::Peer => SurfaceRole::Peer,
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 use sim_lib_intent::{Origin, intent};
222 use sim_lib_view_device::{DeviceCapability, StalePolicy};
223
224 fn session() -> GlassesCoUseSession {
225 let edge = EdgeId::named("wear-session");
226 let consent = ConsentReceipt::new(
227 vec![
228 DeviceCapability::Pose.grant_symbol(),
229 DeviceCapability::Mic.grant_symbol(),
230 ],
231 60_000,
232 Vec::new(),
233 edge.clone(),
234 7,
235 );
236 GlassesCoUseSession::new(
237 edge,
238 consent,
239 build::keyword("workspace"),
240 workspace(),
241 sim_kernel::HandleSeed::new(0x5745_4205),
242 )
243 .unwrap()
244 }
245
246 fn workspace() -> Expr {
247 build::map(vec![
248 ("title", Expr::String("Bridge review".to_owned())),
249 ("status", build::sym("pending")),
250 ("focus", build::sym("main-panel")),
251 (
252 "review",
253 build::map(vec![
254 ("mission", build::qsym("bridge", "packet-review")),
255 ("status", build::sym("pending")),
256 ]),
257 ),
258 ])
259 }
260
261 fn attach_both(session: &mut GlassesCoUseSession) {
262 let viture = SurfaceCaps::from_preset("glasses-luma-ultra", "viture.co-use").unwrap();
263 let halo = SurfaceCaps::from_preset("glasses-hud", "halo.co-use").unwrap();
264 sim_lib_scene::validate_scene(&session.attach_viture(viture).unwrap()).unwrap();
265 sim_lib_scene::validate_scene(&session.attach_halo(halo).unwrap()).unwrap();
266 }
267
268 fn edit_field(tick: u64, field: &str, value: Expr) -> Expr {
269 intent(
270 "edit-field",
271 Origin::human(tick),
272 vec![
273 ("target", workspace()),
274 (
275 "path",
276 Expr::List(vec![Expr::Vector(vec![
277 Expr::Symbol(build::keyword("k")),
278 Expr::Symbol(build::keyword(field)),
279 ])]),
280 ),
281 ("value", value),
282 ],
283 )
284 }
285
286 fn invoke(tick: u64, op: &str) -> Expr {
287 intent(
288 "invoke",
289 Origin::human(tick),
290 vec![
291 ("target", build::sym("workspace")),
292 ("op", Expr::Symbol(Symbol::qualified("glasses/input", op))),
293 ("args", Expr::List(Vec::new())),
294 ],
295 )
296 }
297
298 #[test]
299 fn viture_and_halo_share_one_session() {
300 let mut session = session();
301 attach_both(&mut session);
302
303 assert_eq!(session.role(GlassesPeer::Viture), Some(SurfaceRole::Main));
304 assert_eq!(session.role(GlassesPeer::Halo), Some(SurfaceRole::Peer));
305 assert_eq!(
306 session.viture_loop_policy(),
307 Some(StalePolicy::Predict),
308 "Viture keeps its own reprojector loop"
309 );
310 assert_eq!(
311 session.halo_loop_policy(),
312 Some(StalePolicy::HoldLast),
313 "Halo keeps its own glance loop"
314 );
315 assert_eq!(session.live_bindings().len(), 2);
316
317 let halo_voice = edit_field(1, "title", Expr::String("Halo voice edit".to_owned()));
318 let broadcasts = session
319 .submit_from(GlassesPeer::Halo, &halo_voice)
320 .expect("Halo ASR edit commits through the hub");
321 assert_broadcasts_to_both(&session, &broadcasts);
322 assert_eq!(
323 access::field(session.workspace().unwrap(), "title"),
324 Some(&Expr::String("Halo voice edit".to_owned()))
325 );
326 assert_eq!(session.ledger().len(), 1);
327
328 let viture_intent = invoke(2, "pinch");
329 let next = access::set(
330 session.workspace().unwrap(),
331 "focus",
332 build::sym("viture-hand"),
333 );
334 let broadcasts = session
335 .commit_from(GlassesPeer::Viture, &viture_intent, next)
336 .expect("decoded Viture hand Intent commits once");
337 assert_broadcasts_to_both(&session, &broadcasts);
338 assert_eq!(
339 access::field(session.workspace().unwrap(), "focus"),
340 Some(&build::sym("viture-hand"))
341 );
342 assert_eq!(session.ledger().len(), 2);
343 assert_eq!(session.ledger()[1].tick, 2);
344 }
345
346 #[test]
347 fn halo_tap_acks_viture_card_and_detach_keeps_session_alive() {
348 let mut session = session();
349 attach_both(&mut session);
350 let consent = session.consent().clone();
351 let edge = session.session().clone();
352
353 let tap = invoke(3, "double-tap");
354 let broadcasts = session
355 .acknowledge_review_from_halo(&tap, Symbol::qualified("bridge", "packet-review"))
356 .expect("Halo tap acknowledges the Viture review card");
357 assert_broadcasts_to_both(&session, &broadcasts);
358 let review = access::field(session.workspace().unwrap(), "review").unwrap();
359 assert_eq!(access::field(review, "status"), Some(&build::sym("acked")));
360 assert_eq!(
361 access::field(review, "acked-by"),
362 Some(&Expr::Symbol(session.surface(GlassesPeer::Halo).clone()))
363 );
364 assert_eq!(session.ledger().len(), 1);
365
366 let removed = session.detach_viture();
367 assert_eq!(removed.len(), 1);
368 assert_eq!(removed[0].surface, *session.surface(GlassesPeer::Viture));
369 assert_eq!(session.viture_loop_policy(), None);
370 assert_eq!(session.live_bindings().len(), 1);
371 assert_eq!(
372 session.live_bindings()[0].surface,
373 *session.surface(GlassesPeer::Halo)
374 );
375 assert_eq!(session.consent(), &consent);
376 assert_eq!(session.session(), &edge);
377
378 let halo_voice = edit_field(4, "status", build::sym("halo-only"));
379 let broadcasts = session
380 .submit_from(GlassesPeer::Halo, &halo_voice)
381 .expect("Halo keeps editing after Viture detaches");
382 assert_eq!(broadcasts.len(), 1);
383 assert_eq!(broadcasts[0].surface, *session.surface(GlassesPeer::Halo));
384 assert_eq!(session.ledger().len(), 2);
385
386 let before_reattach_rows = session.ledger().len();
387 let viture = SurfaceCaps::from_preset("glasses-luma-ultra", "viture.co-use").unwrap();
388 sim_lib_scene::validate_scene(&session.attach_viture(viture).unwrap()).unwrap();
389 assert_eq!(session.role(GlassesPeer::Viture), Some(SurfaceRole::Main));
390 assert_eq!(session.live_bindings().len(), 2);
391 assert_eq!(
392 session.ledger().len(),
393 before_reattach_rows,
394 "reattach opens a projection but does not append an edit row"
395 );
396 assert_eq!(session.consent(), &consent);
397 }
398
399 fn assert_broadcasts_to_both(session: &GlassesCoUseSession, broadcasts: &[Broadcast]) {
400 assert!(
401 broadcasts
402 .iter()
403 .any(|broadcast| broadcast.surface == *session.surface(GlassesPeer::Viture)),
404 "Viture did not receive a broadcast"
405 );
406 assert!(
407 broadcasts
408 .iter()
409 .any(|broadcast| broadcast.surface == *session.surface(GlassesPeer::Halo)),
410 "Halo did not receive a broadcast"
411 );
412 }
413}