Skip to main content

sim_run_core/
device_host.rs

1use std::fmt;
2
3use sim_kernel::{Cx, Error as KernelError, Result, Symbol};
4
5/// Stream-facing profile advertised by a composed device route.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct DeviceProfile {
8    /// Stable device identity.
9    pub device: Symbol,
10    /// Sample streams this device can emit.
11    pub streams: Vec<Symbol>,
12    /// Input controls accepted by the device.
13    pub inputs: Vec<Symbol>,
14    /// Output actuators exposed by the device.
15    pub outputs: Vec<Symbol>,
16    /// Sample kinds the provider may return.
17    pub sample_kinds: Vec<Symbol>,
18}
19
20impl DeviceProfile {
21    /// Builds a device profile from stable route metadata.
22    pub fn new(
23        device: Symbol,
24        streams: Vec<Symbol>,
25        inputs: Vec<Symbol>,
26        outputs: Vec<Symbol>,
27        sample_kinds: Vec<Symbol>,
28    ) -> Self {
29        Self {
30            device,
31            streams,
32            inputs,
33            outputs,
34            sample_kinds,
35        }
36    }
37
38    /// Builds the deterministic modeled edge profile used by tests and docs.
39    pub fn modeled_edge() -> Self {
40        Self::new(
41            Symbol::qualified("device", "modeled-edge"),
42            vec![
43                Symbol::qualified("device/stream", "battery"),
44                Symbol::qualified("device/stream", "motion"),
45            ],
46            vec![Symbol::qualified("device/input", "button")],
47            vec![
48                Symbol::qualified("device/output", "screen"),
49                Symbol::qualified("device/output", "haptic"),
50            ],
51            vec![Symbol::qualified("device/sample", "caps")],
52        )
53    }
54
55    /// Returns whether this profile advertises `sample_kind`.
56    pub fn supports_sample_kind(&self, sample_kind: &Symbol) -> bool {
57        self.sample_kinds.contains(sample_kind)
58    }
59}
60
61/// Placement locality advertised by a device site.
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum DeviceSiteLocality {
64    /// Device adapter runs at the device or edge boundary.
65    EdgeLocal,
66    /// Site runs on the host but not at the device edge.
67    HostLocal,
68    /// Site crosses a remote transport boundary.
69    Remote,
70}
71
72/// Export-record-style descriptor for a device route site.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct DeviceSite {
75    /// Stable site symbol exported by the route provider.
76    pub symbol: Symbol,
77    /// Device profile carried by this site export.
78    pub profile: DeviceProfile,
79    /// Surface codec for device samples and commands.
80    pub surface_codec_id: Symbol,
81    /// Locality used by placement validation.
82    pub locality: DeviceSiteLocality,
83}
84
85impl DeviceSite {
86    /// Builds a device site descriptor.
87    pub fn new(
88        symbol: Symbol,
89        profile: DeviceProfile,
90        surface_codec_id: Symbol,
91        locality: DeviceSiteLocality,
92    ) -> Self {
93        Self {
94            symbol,
95            profile,
96            surface_codec_id,
97            locality,
98        }
99    }
100
101    /// Builds a device or edge-local site descriptor.
102    pub fn edge_local(symbol: Symbol, profile: DeviceProfile, surface_codec_id: Symbol) -> Self {
103        Self::new(
104            symbol,
105            profile,
106            surface_codec_id,
107            DeviceSiteLocality::EdgeLocal,
108        )
109    }
110
111    /// Builds a host-local site descriptor.
112    pub fn host_local(symbol: Symbol, profile: DeviceProfile, surface_codec_id: Symbol) -> Self {
113        Self::new(
114            symbol,
115            profile,
116            surface_codec_id,
117            DeviceSiteLocality::HostLocal,
118        )
119    }
120
121    /// Builds a remote site descriptor.
122    pub fn remote(symbol: Symbol, profile: DeviceProfile, surface_codec_id: Symbol) -> Self {
123        Self::new(
124            symbol,
125            profile,
126            surface_codec_id,
127            DeviceSiteLocality::Remote,
128        )
129    }
130
131    /// Returns whether this site is local enough for a latency-critical adapter.
132    pub fn is_edge_local(&self) -> bool {
133        self.locality == DeviceSiteLocality::EdgeLocal
134    }
135}
136
137/// Placement plan for a device surface encoder and adapter pair.
138#[derive(Clone, Debug, PartialEq, Eq)]
139pub struct DevicePlacement {
140    /// Site that encodes samples and commands for the selected surface codec.
141    pub encoder: DeviceSite,
142    /// Latency-critical adapter site placed at the device edge.
143    pub adapter: DeviceSite,
144}
145
146impl DevicePlacement {
147    /// Builds a device placement plan from an encoder and adapter site.
148    pub fn new(encoder: DeviceSite, adapter: DeviceSite) -> Self {
149        Self { encoder, adapter }
150    }
151
152    /// Validates placement invariants for live device operation.
153    pub fn validate(&self) -> std::result::Result<(), DevicePlacementError> {
154        if self.adapter.is_edge_local() {
155            Ok(())
156        } else {
157            Err(DevicePlacementError::AdapterMustBeEdgeLocal)
158        }
159    }
160}
161
162/// Device placement validation error.
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub enum DevicePlacementError {
165    /// The latency-critical adapter is not device or edge local.
166    AdapterMustBeEdgeLocal,
167}
168
169impl fmt::Display for DevicePlacementError {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            Self::AdapterMustBeEdgeLocal => f.write_str("device adapter must be edge-local"),
173        }
174    }
175}
176
177impl std::error::Error for DevicePlacementError {}
178
179/// Provider that opens one composed device session.
180pub trait DeviceProvider: Send {
181    /// Opens a provider-owned device session.
182    fn open(&self) -> Result<Box<dyn DeviceSession>>;
183}
184
185/// Open device session used by a composed route.
186pub trait DeviceSession: Send {
187    /// Returns the profile for this session.
188    fn profile(&self) -> &DeviceProfile;
189
190    /// Starts sample or command processing.
191    fn start(&mut self) -> Result<()>;
192
193    /// Stops sample or command processing and releases session resources.
194    fn stop(&mut self) -> Result<()>;
195}
196
197/// Hardware-free provider used when no concrete device provider is installed.
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct StubProvider {
200    profile: DeviceProfile,
201}
202
203impl StubProvider {
204    /// Builds a stub provider for the supplied profile.
205    pub fn new(profile: DeviceProfile) -> Self {
206        Self { profile }
207    }
208
209    /// Returns the profile this stub advertises for browse and placement.
210    pub fn profile(&self) -> &DeviceProfile {
211        &self.profile
212    }
213
214    /// Builds an unopened stub session for provider-surface validation.
215    pub fn session(&self) -> StubSession {
216        StubSession::new(self.profile.clone())
217    }
218}
219
220impl DeviceProvider for StubProvider {
221    fn open(&self) -> Result<Box<dyn DeviceSession>> {
222        Ok(Box::new(self.session()))
223    }
224}
225
226/// Hardware-free session used by headless device composition.
227#[derive(Clone, Debug, PartialEq, Eq)]
228pub struct StubSession {
229    profile: DeviceProfile,
230    started: bool,
231}
232
233impl StubSession {
234    /// Builds a stub session for the supplied profile.
235    pub fn new(profile: DeviceProfile) -> Self {
236        Self {
237            profile,
238            started: false,
239        }
240    }
241
242    /// Returns whether this stub session has been started.
243    pub fn is_started(&self) -> bool {
244        self.started
245    }
246}
247
248impl DeviceSession for StubSession {
249    fn profile(&self) -> &DeviceProfile {
250        &self.profile
251    }
252
253    fn start(&mut self) -> Result<()> {
254        self.started = true;
255        Ok(())
256    }
257
258    fn stop(&mut self) -> Result<()> {
259        self.started = false;
260        Ok(())
261    }
262}
263
264/// Route selected by a product verb before device host composition.
265#[derive(Clone, Debug, PartialEq, Eq)]
266pub struct RouteArg {
267    symbol: Symbol,
268}
269
270impl RouteArg {
271    /// Builds a route from a stable route symbol.
272    pub fn new(symbol: Symbol) -> Self {
273        Self { symbol }
274    }
275
276    /// Builds the hardware-free route used by headless device composition.
277    pub fn headless() -> Self {
278        Self::new(Symbol::qualified("device/route", "headless"))
279    }
280
281    /// Returns the stable route symbol.
282    pub fn symbol(&self) -> &Symbol {
283        &self.symbol
284    }
285}
286
287/// Policy applied when a device sample or rendered frame becomes stale.
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub enum DeviceHostStalePolicy {
290    /// Keep the last accepted sample or rendered frame visible.
291    HoldLast,
292    /// Predict briefly from the last accepted sample, then clamp to the last frame.
293    PredictClamp,
294    /// Replace stale output with a blank sample or surface frame.
295    Blank,
296    /// Refuse stale output until the provider yields a fresh sample.
297    Refuse,
298}
299
300/// Consent policy carried by device verbs into host composition.
301#[derive(Clone, Debug, PartialEq, Eq)]
302pub enum DeviceConsentPolicy {
303    /// The route is explicitly headless and requires no interactive receipt.
304    Headless,
305    /// A human-visible receipt must be associated with the route before use.
306    RequireReceipt {
307        /// Stable subject that owns the receipt.
308        subject: Symbol,
309    },
310}
311
312/// Coarse rate class for pacing the device adapter loop.
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub enum DeviceRateClass {
315    /// Low-rate sensor stream with no local controls or surface output.
316    Sparse,
317    /// Interactive device with local controls but no surface output.
318    Interactive,
319    /// Surface-capable device with local output that benefits from tight pacing.
320    Surface,
321}
322
323impl DeviceRateClass {
324    fn interval_ms(self) -> u64 {
325        match self {
326            Self::Sparse => 1_000,
327            Self::Interactive => 100,
328            Self::Surface => 50,
329        }
330    }
331}
332
333/// Derives the adapter-loop rate class from stream-facing device metadata.
334pub fn derive_device_rate_class(profile: &DeviceProfile) -> DeviceRateClass {
335    if !profile.outputs.is_empty() {
336        DeviceRateClass::Surface
337    } else if !profile.inputs.is_empty() {
338        DeviceRateClass::Interactive
339    } else {
340        DeviceRateClass::Sparse
341    }
342}
343
344/// Input required to compose a device host session.
345#[derive(Clone, Debug, PartialEq, Eq)]
346pub struct DeviceHostSpec {
347    /// Stream-facing device profile selected by the product verb.
348    pub profile: DeviceProfile,
349    /// Route selected by the product verb.
350    pub route: RouteArg,
351    /// Placement plan for the surface encoder and edge-local adapter.
352    pub placement: DevicePlacement,
353    /// Stale-sample policy used by the adapter loop.
354    pub stale: DeviceHostStalePolicy,
355    /// Consent policy required before the device route is used.
356    pub consent: DeviceConsentPolicy,
357}
358
359impl DeviceHostSpec {
360    /// Builds a device host composition request.
361    pub fn new(
362        profile: DeviceProfile,
363        route: RouteArg,
364        placement: DevicePlacement,
365        stale: DeviceHostStalePolicy,
366        consent: DeviceConsentPolicy,
367    ) -> Self {
368        Self {
369            profile,
370            route,
371            placement,
372            stale,
373            consent,
374        }
375    }
376}
377
378/// Provider source selected during device host composition.
379#[derive(Clone, Copy, Debug, PartialEq, Eq)]
380pub enum DeviceProviderKind {
381    /// A concrete provider instance supplied the session.
382    Instance,
383    /// No concrete provider was supplied, so the helper composed a stub session.
384    Stub,
385}
386
387/// One scheduled adapter-loop tick.
388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389pub struct AdapterTick {
390    sequence: u64,
391    interval_ms: u64,
392}
393
394impl AdapterTick {
395    /// Returns the monotonic tick sequence.
396    pub fn sequence(&self) -> u64 {
397        self.sequence
398    }
399
400    /// Returns the delay before this tick in milliseconds.
401    pub fn interval_ms(&self) -> u64 {
402        self.interval_ms
403    }
404}
405
406/// Pacing plan for a composed device adapter loop.
407#[derive(Clone, Debug, PartialEq, Eq)]
408pub struct DeviceAdapterLoopPlan {
409    rate_class: DeviceRateClass,
410    stale: DeviceHostStalePolicy,
411    route: RouteArg,
412    device: Symbol,
413    sequence: u64,
414}
415
416impl DeviceAdapterLoopPlan {
417    /// Builds a pacing plan from the selected profile, route, and stale policy.
418    pub fn for_profile(
419        profile: &DeviceProfile,
420        route: RouteArg,
421        stale: DeviceHostStalePolicy,
422    ) -> Self {
423        Self {
424            rate_class: derive_device_rate_class(profile),
425            stale,
426            route,
427            device: profile.device.clone(),
428            sequence: 0,
429        }
430    }
431
432    /// Returns the rate class used by this adapter loop.
433    pub fn rate_class(&self) -> DeviceRateClass {
434        self.rate_class
435    }
436
437    /// Returns the stale-sample policy used by this adapter loop.
438    pub fn stale_policy(&self) -> DeviceHostStalePolicy {
439        self.stale
440    }
441
442    /// Returns the route paced by this adapter loop.
443    pub fn route(&self) -> &RouteArg {
444        &self.route
445    }
446
447    /// Returns the selected device symbol.
448    pub fn device(&self) -> &Symbol {
449        &self.device
450    }
451
452    /// Returns the current tick sequence without advancing it.
453    pub fn sequence(&self) -> u64 {
454        self.sequence
455    }
456
457    /// Returns the delay between adapter ticks in milliseconds.
458    pub fn interval_ms(&self) -> u64 {
459        self.rate_class.interval_ms()
460    }
461
462    /// Advances the adapter clock by one paced tick.
463    pub fn next_tick(&mut self) -> AdapterTick {
464        self.sequence += 1;
465        AdapterTick {
466            sequence: self.sequence,
467            interval_ms: self.interval_ms(),
468        }
469    }
470}
471
472/// Surface-hub join plan produced by device host composition.
473#[derive(Clone, Debug, PartialEq, Eq)]
474pub struct DeviceSurfaceHubJoin {
475    route: RouteArg,
476    device: Symbol,
477    adapter_site: Symbol,
478}
479
480impl DeviceSurfaceHubJoin {
481    /// Builds a surface-hub join plan.
482    pub fn new(route: RouteArg, device: Symbol, adapter_site: Symbol) -> Self {
483        Self {
484            route,
485            device,
486            adapter_site,
487        }
488    }
489
490    /// Returns the route associated with the hub join.
491    pub fn route(&self) -> &RouteArg {
492        &self.route
493    }
494
495    /// Returns the device associated with the hub join.
496    pub fn device(&self) -> &Symbol {
497        &self.device
498    }
499
500    /// Returns the edge-local adapter site joined to the hub.
501    pub fn adapter_site(&self) -> &Symbol {
502        &self.adapter_site
503    }
504}
505
506/// Composed device host session returned to per-device verbs.
507pub struct DeviceEdgeSession {
508    spec: DeviceHostSpec,
509    provider_kind: DeviceProviderKind,
510    session: Box<dyn DeviceSession>,
511    adapter_loop: DeviceAdapterLoopPlan,
512    hub_join: DeviceSurfaceHubJoin,
513    live: bool,
514}
515
516impl DeviceEdgeSession {
517    /// Returns whether host composition joined a device session.
518    pub fn is_live(&self) -> bool {
519        self.live
520    }
521
522    /// Returns the provider source used for this session.
523    pub fn provider_kind(&self) -> DeviceProviderKind {
524        self.provider_kind
525    }
526
527    /// Returns the selected stream-facing profile.
528    pub fn profile(&self) -> &DeviceProfile {
529        &self.spec.profile
530    }
531
532    /// Returns the selected route.
533    pub fn route(&self) -> &RouteArg {
534        &self.spec.route
535    }
536
537    /// Returns the validated device placement.
538    pub fn placement(&self) -> &DevicePlacement {
539        &self.spec.placement
540    }
541
542    /// Returns the selected stale-sample policy.
543    pub fn stale_policy(&self) -> DeviceHostStalePolicy {
544        self.spec.stale
545    }
546
547    /// Returns the selected consent policy.
548    pub fn consent_policy(&self) -> &DeviceConsentPolicy {
549        &self.spec.consent
550    }
551
552    /// Returns the immutable adapter-loop pacing plan.
553    pub fn adapter_loop(&self) -> &DeviceAdapterLoopPlan {
554        &self.adapter_loop
555    }
556
557    /// Returns the mutable adapter-loop pacing plan.
558    pub fn adapter_loop_mut(&mut self) -> &mut DeviceAdapterLoopPlan {
559        &mut self.adapter_loop
560    }
561
562    /// Returns the surface-hub join plan.
563    pub fn hub_join(&self) -> &DeviceSurfaceHubJoin {
564        &self.hub_join
565    }
566
567    /// Returns the composed device session.
568    pub fn device_session(&self) -> &dyn DeviceSession {
569        self.session.as_ref()
570    }
571
572    /// Returns the composed device session mutably.
573    pub fn device_session_mut(&mut self) -> &mut dyn DeviceSession {
574        self.session.as_mut()
575    }
576}
577
578/// Installs the base device boot requirements into an existing context.
579pub fn install_device_bases(cx: &mut Cx) -> Result<()> {
580    cx.factory().nil().map(|_| ())
581}
582
583/// Composes a device host session using a hardware-free stub provider.
584pub fn compose_device_host(cx: &mut Cx, spec: DeviceHostSpec) -> Result<DeviceEdgeSession> {
585    let provider = StubProvider::new(spec.profile.clone());
586    join_device_session(cx, spec, DeviceProviderKind::Stub, provider.open()?)
587}
588
589/// Composes a device host session using a supplied provider instance.
590pub fn compose_device_host_with_provider<P>(
591    cx: &mut Cx,
592    spec: DeviceHostSpec,
593    provider: &P,
594) -> Result<DeviceEdgeSession>
595where
596    P: DeviceProvider + ?Sized,
597{
598    join_device_session(cx, spec, DeviceProviderKind::Instance, provider.open()?)
599}
600
601fn join_device_session(
602    cx: &mut Cx,
603    spec: DeviceHostSpec,
604    provider_kind: DeviceProviderKind,
605    session: Box<dyn DeviceSession>,
606) -> Result<DeviceEdgeSession> {
607    install_device_bases(cx)?;
608    spec.placement
609        .validate()
610        .map_err(|error| KernelError::HostError(error.to_string()))?;
611    let mut session = session;
612    if session.profile() != &spec.profile {
613        return Err(KernelError::HostError(format!(
614            "device provider profile {} did not match requested profile {}",
615            session.profile().device,
616            spec.profile.device
617        )));
618    }
619    session.start()?;
620    let adapter_loop =
621        DeviceAdapterLoopPlan::for_profile(&spec.profile, spec.route.clone(), spec.stale);
622    let hub_join = DeviceSurfaceHubJoin::new(
623        spec.route.clone(),
624        spec.profile.device.clone(),
625        spec.placement.adapter.symbol.clone(),
626    );
627    Ok(DeviceEdgeSession {
628        spec,
629        provider_kind,
630        session,
631        adapter_loop,
632        hub_join,
633        live: true,
634    })
635}