1use std::collections::VecDeque;
4
5use sim_kernel::{Error, EventKind, EventLedger, Expr, Ref, Result, Symbol};
6
7pub type LedgerRef = Ref;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub enum LinkKind {
13 Direct,
15 Relay,
17 Local,
19 Custom(Symbol),
21}
22
23impl LinkKind {
24 pub fn import() -> Self {
26 Self::Custom(link_symbol("import"))
27 }
28
29 pub fn modeled() -> Self {
31 Self::Custom(link_symbol("modeled"))
32 }
33
34 pub fn ble_host() -> Self {
36 Self::Custom(link_symbol("ble-host"))
37 }
38
39 pub const fn relay() -> Self {
41 Self::Relay
42 }
43
44 pub fn zepp_bridge() -> Self {
46 Self::Custom(link_symbol("zepp-bridge"))
47 }
48
49 pub fn wifi_if_verified() -> Self {
51 Self::Custom(link_symbol("wifi-if-verified"))
52 }
53
54 pub fn as_symbol(&self) -> Symbol {
56 match self {
57 Self::Direct => link_symbol("direct"),
58 Self::Relay => link_symbol("relay"),
59 Self::Local => link_symbol("local"),
60 Self::Custom(symbol) => symbol.clone(),
61 }
62 }
63
64 pub fn from_symbol(symbol: Symbol) -> Self {
66 if symbol.namespace.as_deref() == Some("device/link") {
67 match symbol.name.as_ref() {
68 "direct" => Self::Direct,
69 "relay" => Self::Relay,
70 "local" => Self::Local,
71 _ => Self::Custom(symbol),
72 }
73 } else {
74 Self::Custom(symbol)
75 }
76 }
77
78 pub fn to_expr(&self) -> Expr {
80 Expr::Symbol(self.as_symbol())
81 }
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct DeviceEdgeProfile {
87 device: Symbol,
88 provider: Symbol,
89}
90
91impl DeviceEdgeProfile {
92 pub fn new(device: Symbol, provider: Symbol) -> Self {
94 Self { device, provider }
95 }
96
97 pub fn device(&self) -> &Symbol {
99 &self.device
100 }
101
102 pub fn provider(&self) -> &Symbol {
104 &self.provider
105 }
106
107 pub fn to_expr(&self) -> Expr {
109 sim_value::build::map(vec![
110 ("device", Expr::Symbol(self.device.clone())),
111 ("provider", Expr::Symbol(self.provider.clone())),
112 ])
113 }
114}
115
116#[derive(Clone, Debug)]
124pub struct DeviceEdgeSession {
125 id: Symbol,
126 link: LinkKind,
127 ledger: LedgerRef,
128 profile: Option<DeviceEdgeProfile>,
129 route_order: Vec<LinkKind>,
130 queued_commands: VecDeque<Expr>,
131 flushed_commands: Vec<Expr>,
132 events: EventLedger,
133 consent: Option<Expr>,
134}
135
136impl DeviceEdgeSession {
137 pub fn new(id: Symbol, link: LinkKind) -> Result<Self> {
139 let ledger = Ref::Symbol(id.clone());
140 Self::with_ledger(id, link, ledger)
141 }
142
143 pub fn with_ledger(id: Symbol, link: LinkKind, ledger: LedgerRef) -> Result<Self> {
145 let mut session = Self {
146 id,
147 link,
148 ledger,
149 profile: None,
150 route_order: Vec::new(),
151 queued_commands: VecDeque::new(),
152 flushed_commands: Vec::new(),
153 events: EventLedger::new(),
154 consent: None,
155 };
156 session.record_event("open")?;
157 Ok(session)
158 }
159
160 pub fn id(&self) -> &Symbol {
162 &self.id
163 }
164
165 pub fn link(&self) -> &LinkKind {
167 &self.link
168 }
169
170 pub fn ledger_ref(&self) -> &LedgerRef {
172 &self.ledger
173 }
174
175 pub fn device_profile(&self) -> Option<&DeviceEdgeProfile> {
177 self.profile.as_ref()
178 }
179
180 pub fn bind_profile(&mut self, profile: DeviceEdgeProfile) -> Result<()> {
182 self.profile = Some(profile);
183 self.record_event("profile-bound")
184 }
185
186 pub fn route_order(&self) -> &[LinkKind] {
188 &self.route_order
189 }
190
191 pub fn set_route_order(&mut self, route_order: Vec<LinkKind>) -> Result<()> {
193 if route_order.is_empty() {
194 return Err(Error::HostError(
195 "device edge route order cannot be empty".to_owned(),
196 ));
197 }
198 self.route_order = route_order;
199 self.record_event("route-order")
200 }
201
202 pub fn queue_command(&mut self, command: Expr) -> Result<()> {
204 self.queued_commands.push_back(command);
205 self.record_event("command-queued")
206 }
207
208 pub fn queued_command_count(&self) -> usize {
210 self.queued_commands.len()
211 }
212
213 pub fn flushed_commands(&self) -> &[Expr] {
215 &self.flushed_commands
216 }
217
218 pub fn flush_queued_commands(&mut self) -> Result<Vec<Expr>> {
220 let flushed = self.queued_commands.drain(..).collect::<Vec<_>>();
221 if !flushed.is_empty() {
222 self.flushed_commands.extend(flushed.clone());
223 self.record_event("command-flushed")?;
224 }
225 Ok(flushed)
226 }
227
228 pub fn event_ledger(&self) -> &EventLedger {
230 &self.events
231 }
232
233 pub fn bound_consent(&self) -> Option<&Expr> {
235 self.consent.as_ref()
236 }
237
238 pub fn bound_consent_session(&self) -> Option<Symbol> {
240 self.consent
241 .as_ref()
242 .and_then(|consent| sim_value::access::field_sym(consent, "session"))
243 }
244
245 pub fn bind_consent(&mut self, consent: Expr) -> Result<()> {
247 match sim_value::access::field_sym(&consent, "session") {
248 Some(session) if session == self.id => {}
249 Some(session) => {
250 return Err(Error::HostError(format!(
251 "consent session '{session}' does not match device edge session '{}'",
252 self.id
253 )));
254 }
255 None => {
256 return Err(Error::HostError(
257 "device edge consent is missing session".to_owned(),
258 ));
259 }
260 }
261 self.consent = Some(consent);
262 self.record_event("consent-bound")
263 }
264
265 pub fn rebind(&mut self, link: LinkKind) -> Result<()> {
267 self.link = link;
268 self.record_event("rebind")
269 }
270
271 pub fn rebind_and_flush(&mut self, link: LinkKind) -> Result<Vec<Expr>> {
273 self.rebind(link)?;
274 self.flush_queued_commands()
275 }
276
277 fn record_event(&mut self, name: &str) -> Result<()> {
278 self.events.push(
279 self.ledger.clone(),
280 EventKind::Trace(Ref::Symbol(Symbol::qualified("device/edge", name))),
281 )?;
282 Ok(())
283 }
284}
285
286pub fn watch_route_order() -> Vec<LinkKind> {
288 vec![
289 LinkKind::import(),
290 LinkKind::modeled(),
291 LinkKind::ble_host(),
292 LinkKind::relay(),
293 LinkKind::zepp_bridge(),
294 LinkKind::wifi_if_verified(),
295 ]
296}
297
298pub fn register_watch_edge_session(
300 id: Symbol,
301 profile: DeviceEdgeProfile,
302 ledger: LedgerRef,
303 initial_link: LinkKind,
304) -> Result<DeviceEdgeSession> {
305 let mut session = DeviceEdgeSession::with_ledger(id, initial_link, ledger)?;
306 session.bind_profile(profile)?;
307 session.set_route_order(watch_route_order())?;
308 Ok(session)
309}
310
311fn link_symbol(name: &str) -> Symbol {
312 Symbol::qualified("device/link", name.to_owned())
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 use sim_value::build;
320
321 #[test]
322 fn device_session_survives_route_swap() {
323 let id = Symbol::qualified("device/session", "route-swap");
324 let consent = build::map(vec![
325 ("kind", build::qsym("device", "consent-receipt")),
326 ("session", Expr::Symbol(id.clone())),
327 ("seq", build::uint(7)),
328 ]);
329 let mut session = DeviceEdgeSession::new(id.clone(), LinkKind::Direct).unwrap();
330 session.bind_consent(consent.clone()).unwrap();
331
332 let ledger = session.ledger_ref().clone();
333 let events_before = session.event_ledger().len_for_run(&ledger);
334 session.rebind(LinkKind::Relay).unwrap();
335
336 assert_eq!(session.id(), &id);
337 assert_eq!(session.ledger_ref(), &ledger);
338 assert_eq!(session.link(), &LinkKind::Relay);
339 assert_eq!(session.bound_consent(), Some(&consent));
340 assert_eq!(session.bound_consent_session(), Some(id));
341 assert_eq!(
342 session.event_ledger().len_for_run(&ledger),
343 events_before + 1
344 );
345 }
346
347 #[test]
348 fn consent_must_match_device_session() {
349 let id = Symbol::qualified("device/session", "route-swap");
350 let other = Symbol::qualified("device/session", "other");
351 let consent = build::map(vec![
352 ("kind", build::qsym("device", "consent-receipt")),
353 ("session", Expr::Symbol(other)),
354 ("seq", build::uint(7)),
355 ]);
356 let mut session = DeviceEdgeSession::new(id, LinkKind::Direct).unwrap();
357
358 let err = session.bind_consent(consent).unwrap_err();
359
360 assert!(matches!(
361 err,
362 Error::HostError(message) if message.contains("does not match")
363 ));
364 assert!(session.bound_consent().is_none());
365 }
366
367 #[test]
368 fn watch_session_flushes_queue_after_reconnect() {
369 let id = Symbol::qualified("device/session", "watch-trex3pro-48");
370 let ledger = Ref::Symbol(Symbol::qualified("device/ledger", "watch-trex3pro-48"));
371 let profile = DeviceEdgeProfile::new(
372 Symbol::qualified("device", "amazfit-t-rex-3-pro"),
373 Symbol::qualified("device/provider", "watch-wristbridge"),
374 );
375 let consent = build::map(vec![
376 ("kind", build::qsym("device", "consent-receipt")),
377 ("session", Expr::Symbol(id.clone())),
378 ("seq", build::uint(9)),
379 ]);
380 let command = build::map(vec![
381 ("kind", build::qsym("view-wrist", "command")),
382 ("command", build::qsym("watch/command", "notify")),
383 ("title", build::text("route")),
384 ]);
385
386 let mut session = register_watch_edge_session(
387 id.clone(),
388 profile.clone(),
389 ledger.clone(),
390 LinkKind::Direct,
391 )
392 .unwrap();
393 session.bind_consent(consent.clone()).unwrap();
394 session.queue_command(command.clone()).unwrap();
395
396 let events_before = session.event_ledger().len_for_run(&ledger);
397 let flushed = session.rebind_and_flush(LinkKind::relay()).unwrap();
398
399 assert_eq!(session.id(), &id);
400 assert_eq!(session.ledger_ref(), &ledger);
401 assert_eq!(session.link(), &LinkKind::Relay);
402 assert_eq!(session.device_profile(), Some(&profile));
403 assert_eq!(session.bound_consent(), Some(&consent));
404 assert_eq!(session.bound_consent_session(), Some(id));
405 assert_eq!(session.route_order(), watch_route_order().as_slice());
406 assert_eq!(flushed, vec![command.clone()]);
407 assert_eq!(session.flushed_commands(), &[command]);
408 assert_eq!(session.queued_command_count(), 0);
409 assert_eq!(
410 session.event_ledger().len_for_run(&ledger),
411 events_before + 2
412 );
413 }
414}