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