Skip to main content

device_envoy_core/wasm/
cyd_web.rs

1//! A device abstraction for complete CYD browser applications.
2//!
3//! [`start`] creates the stable browser supervisor and returns [`Handle`]. The
4//! supervisor constructs fresh [`Capabilities`] for each run, allowing the
5//! application to select focused capabilities for unchanged generic core code.
6//! [`Command`] communicates application policy back to the supervisor.
7//!
8//! ## Compiled browser-shell example
9//!
10//! The HTML page supplies a `<canvas id="cyd-canvas">`; Rust supplies the
11//! presentation metadata and an async application function. The returned
12//! [`Handle`] is the stable JavaScript-facing control surface.
13//!
14//! ```rust,no_run
15//! use core::convert::Infallible;
16//! use device_envoy_core::{
17//!     button::Button,
18//!     cyd::{CydDisplay, display::Orientation},
19//!     dns::Dns,
20//!     wasm::cyd_web::{
21//!         self, Capabilities, Command, Config, Handle, Notice, NoticeSeverity, PageInfo,
22//!     },
23//! };
24//! use embedded_graphics::{
25//!     mono_font::ascii::FONT_6X10,
26//!     pixelcolor::{Rgb888, RgbColor},
27//! };
28//! use wasm_bindgen::JsValue;
29//!
30//! const CONFIG: Config = Config::new(
31//!     "counter-demo",
32//!     Orientation::Landscape,
33//!     Rgb888::BLACK,
34//!     Rgb888::WHITE,
35//!     &FONT_6X10,
36//! );
37//! const PAGE: PageInfo = PageInfo::new(
38//!     "Counter",
39//!     "A touch-controlled counter",
40//!     "The same application logic used by the hardware builds.",
41//!     "Touch the display; BOOT resets.",
42//!     "https://example.invalid/counter.rs",
43//! );
44//!
45//! async fn inner_main(
46//!     mut capabilities: Capabilities,
47//! ) -> Result<Command, Infallible> {
48//!     assert_eq!(capabilities.cyd.display().screen_size().width, 320);
49//!     assert!(!capabilities.button.is_pressed());
50//!     capabilities.clock_sync.show();
51//!     capabilities.wifi_simulator.reset();
52//!     let addresses = capabilities.dns_simulator.resolve("example.com").await?;
53//!     assert!(!addresses.is_empty());
54//!     Ok(Command::Stop)
55//! }
56//!
57//! fn launch() -> Result<(), JsValue> {
58//!     assert_eq!(CONFIG.storage_namespace, "counter-demo");
59//!     assert_eq!(CONFIG.initial_orientation, Orientation::Landscape);
60//!     assert_eq!(CONFIG.background_color, Rgb888::BLACK);
61//!     assert_eq!(CONFIG.foreground_color, Rgb888::WHITE);
62//!     assert_eq!(CONFIG.font.character_size.width, FONT_6X10.character_size.width);
63//!     assert_eq!(PAGE.title, "Counter");
64//!     assert!(!PAGE.preview.is_empty());
65//!     assert!(!PAGE.description.is_empty());
66//!     assert!(!PAGE.controls.is_empty());
67//!     assert!(PAGE.core_code_url.ends_with("counter.rs"));
68//!
69//!     let handle: Handle = cyd_web::start("cyd-canvas", CONFIG, PAGE, inner_main)?;
70//!     handle.touch_down(20.0, 30.0);
71//!     handle.touch_move(22.0, 32.0);
72//!     handle.touch_up();
73//!     handle.boot_down();
74//!     handle.boot_up();
75//!     assert!(!handle.orientation_is_inverted());
76//!     assert_eq!(handle.page_title(), "Counter");
77//!     assert_eq!(handle.page_preview(), PAGE.preview);
78//!     assert_eq!(handle.page_description(), PAGE.description);
79//!     assert_eq!(handle.page_controls(), PAGE.controls);
80//!     assert_eq!(handle.page_core_code_url(), PAGE.core_code_url);
81//!     handle.set_clock_time_of_day(12 * 60 * 60)?;
82//!     handle.use_live_clock();
83//!     handle.clock_control_is_visible();
84//!     if let Some(notice) = handle.take_notice() {
85//!         inspect_notice(notice);
86//!     }
87//!     handle.request_restart();
88//!     handle.clear_storage_and_restart();
89//!     Ok(())
90//! }
91//!
92//! fn inspect_notice(notice: Notice) {
93//!     assert!(!notice.id().is_empty());
94//!     notice.severity();
95//!     notice.detail();
96//! }
97//!
98//! const NOTICE_SEVERITIES: [NoticeSeverity; 3] = [
99//!     NoticeSeverity::Info,
100//!     NoticeSeverity::Warning,
101//!     NoticeSeverity::Fatal,
102//! ];
103//!
104//! fn every_command(index: u8) -> Command {
105//!     match index {
106//!         0 => Command::Restart,
107//!         1 => Command::CalibrationNotNeeded,
108//!         2 => Command::ResetWifi,
109//!         3 => Command::Reorientate(Orientation::Portrait),
110//!         _ => Command::Stop,
111//!     }
112//! }
113//! ```
114
115use core::{
116    future::Future,
117    pin::Pin,
118    task::{Context, Poll, Waker},
119};
120use std::{
121    cell::{Cell, RefCell},
122    fmt::Debug,
123    rc::Rc,
124};
125
126use embassy_futures::select::{Either, select};
127use embedded_graphics::{mono_font::MonoFont, pixelcolor::Rgb888};
128use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
129use web_sys::{HtmlCanvasElement, window};
130
131use super::{
132    ButtonWasm, ClockSyncWasm, CydSimulatorControlWasm, CydSimulatorWasm, CydWasm,
133    DnsSimulatorWasm, FlashBlockWasm, WifiSimulatorWasm,
134};
135use crate::cyd::display::Orientation;
136use crate::flash_block::FlashBlock as _;
137
138#[derive(Clone, Copy)]
139/// Presentation and persistent-storage settings for a [`Capabilities`]
140/// session.
141/// The compiled browser-shell example on [`crate::wasm::cyd_web`] constructs
142/// and reads every field.
143pub struct Config {
144    /// Namespace used for orientation and simulated Wi-Fi state.
145    pub storage_namespace: &'static str,
146    /// Orientation used when no saved orientation exists.
147    pub initial_orientation: Orientation,
148    /// Canvas background color.
149    pub background_color: Rgb888,
150    /// Canvas foreground color.
151    pub foreground_color: Rgb888,
152    /// Font used by the simulated display.
153    pub font: &'static MonoFont<'static>,
154}
155
156impl Config {
157    /// Construct presentation settings for [`start`].
158    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
159    pub const fn new(
160        storage_namespace: &'static str,
161        initial_orientation: Orientation,
162        background_color: Rgb888,
163        foreground_color: Rgb888,
164        font: &'static MonoFont<'static>,
165    ) -> Self {
166        Self {
167            storage_namespace,
168            initial_orientation,
169            background_color,
170            foreground_color,
171            font,
172        }
173    }
174}
175
176#[derive(Clone, Copy)]
177/// Browser-facing metadata displayed by the shared CYD simulator shell.
178/// The compiled browser-shell example on [`crate::wasm::cyd_web`] constructs
179/// and reads every field.
180pub struct PageInfo {
181    /// Page title.
182    pub title: &'static str,
183    /// Short preview text.
184    pub preview: &'static str,
185    /// Longer application description.
186    pub description: &'static str,
187    /// Human-readable interaction instructions.
188    pub controls: &'static str,
189    /// Link to the platform-neutral application source.
190    pub core_code_url: &'static str,
191}
192
193impl PageInfo {
194    /// Construct page metadata for [`start`].
195    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
196    pub const fn new(
197        title: &'static str,
198        preview: &'static str,
199        description: &'static str,
200        controls: &'static str,
201        core_code_url: &'static str,
202    ) -> Self {
203        Self {
204            title,
205            preview,
206            description,
207            controls,
208            core_code_url,
209        }
210    }
211}
212
213/// Complete capability container supplied to each application run.
214///
215/// A launcher receives this value from [`start`], selects focused capabilities,
216/// and returns a [`Command`].
217/// The compiled browser-shell example on [`crate::wasm::cyd_web`] reads every
218/// field.
219///
220/// ```rust,no_run
221/// # use core::convert::Infallible;
222/// # use device_envoy_core::{cyd::CydDisplay, wasm::cyd_web};
223/// #
224/// async fn inner_main(
225///     mut capabilities: cyd_web::Capabilities,
226/// ) -> Result<cyd_web::Command, Infallible> {
227///     capabilities.clock_sync.show();
228///     assert_eq!(capabilities.cyd.display().screen_size().width, 320);
229///     Ok(cyd_web::Command::Stop)
230/// }
231/// #
232/// # fn receives_capabilities(_capabilities: cyd_web::Capabilities) {}
233/// ```
234pub struct Capabilities {
235    /// CYD display and touch capability.
236    pub cyd: CydWasm,
237    /// BOOT-button capability.
238    pub button: ButtonWasm,
239    /// Browser-backed clock capability.
240    pub clock_sync: ClockSyncWasm,
241    /// Simulated Wi-Fi capability.
242    pub wifi_simulator: WifiSimulatorWasm,
243    /// Deterministic simulated DNS capability.
244    pub dns_simulator: DnsSimulatorWasm,
245}
246
247/// Result requested by an application after one run.
248/// The compiled browser-shell example on [`crate::wasm::cyd_web`] constructs
249/// every variant.
250pub enum Command {
251    /// Restart the current session.
252    Restart,
253    /// Report that physical calibration is unnecessary in the browser.
254    CalibrationNotNeeded,
255    /// Clear simulated Wi-Fi state and restart.
256    ResetWifi,
257    /// Persist and apply a new display orientation.
258    Reorientate(Orientation),
259    /// Stop the supervisor.
260    Stop,
261}
262
263#[wasm_bindgen(js_name = CydWebNoticeSeverity)]
264#[derive(Clone, Copy, Debug, Eq, PartialEq)]
265/// Severity assigned to a framework notice.
266///
267/// The compiled browser-shell example on [`crate::wasm::cyd_web`] constructs
268/// every variant.
269pub enum NoticeSeverity {
270    /// Informational notice.
271    Info,
272    /// Recoverable warning.
273    Warning,
274    /// Terminal runtime failure.
275    Fatal,
276}
277
278#[wasm_bindgen(js_name = CydWebNotice)]
279#[derive(Clone, Debug)]
280/// Typed notice emitted by the framework for the shared browser shell.
281/// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
282pub struct Notice {
283    id: String,
284    severity: NoticeSeverity,
285    detail: Option<String>,
286}
287
288impl Notice {
289    fn new(id: impl Into<String>, severity: NoticeSeverity) -> Self {
290        Self {
291            id: id.into(),
292            severity,
293            detail: None,
294        }
295    }
296    fn fatal(detail: String) -> Self {
297        Self {
298            id: "runtime-error".into(),
299            severity: NoticeSeverity::Fatal,
300            detail: Some(detail),
301        }
302    }
303}
304
305#[wasm_bindgen(js_class = CydWebNotice)]
306impl Notice {
307    /// Return the stable notice identifier.
308    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
309    pub fn id(&self) -> String {
310        self.id.clone()
311    }
312    /// Return the notice severity.
313    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
314    pub fn severity(&self) -> NoticeSeverity {
315        self.severity
316    }
317    /// Return optional diagnostic detail.
318    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
319    pub fn detail(&self) -> Option<String> {
320        self.detail.clone()
321    }
322}
323
324#[derive(Clone, Copy)]
325enum HostRequest {
326    Restart,
327    ClearStorage,
328}
329struct LifecycleState {
330    request: Option<HostRequest>,
331    waker: Option<Waker>,
332}
333#[derive(Clone)]
334struct LifecycleSignal {
335    state: Rc<RefCell<LifecycleState>>,
336}
337
338impl LifecycleSignal {
339    fn new() -> Self {
340        Self {
341            state: Rc::new(RefCell::new(LifecycleState {
342                request: None,
343                waker: None,
344            })),
345        }
346    }
347    fn request(&self, request: HostRequest) {
348        let waker = {
349            let mut state = self.state.borrow_mut();
350            state.request = Some(request);
351            state.waker.take()
352        };
353        if let Some(waker) = waker {
354            waker.wake();
355        }
356    }
357    async fn wait(&self) -> HostRequest {
358        LifecycleRequestFuture {
359            signal: self.clone(),
360        }
361        .await
362    }
363}
364struct LifecycleRequestFuture {
365    signal: LifecycleSignal,
366}
367impl Future for LifecycleRequestFuture {
368    type Output = HostRequest;
369    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
370        let mut state = self.signal.state.borrow_mut();
371        if let Some(request) = state.request.take() {
372            Poll::Ready(request)
373        } else {
374            state.waker = Some(context.waker().clone());
375            Poll::Pending
376        }
377    }
378}
379
380struct SupervisorState {
381    live_control: Option<CydSimulatorControlWasm>,
382    notices: std::collections::VecDeque<Notice>,
383    orientation: Orientation,
384    stopped: bool,
385    page_info: PageInfo,
386    clock_time_of_day: Rc<Cell<Option<u32>>>,
387    clock_control_visible: Rc<Cell<bool>>,
388}
389
390#[wasm_bindgen(js_name = CydWebAppHandle)]
391/// Stable browser control handle returned by [`start`].
392/// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
393pub struct Handle {
394    state: Rc<RefCell<SupervisorState>>,
395    lifecycle_signal: LifecycleSignal,
396}
397
398impl Handle {
399    fn new(state: Rc<RefCell<SupervisorState>>, lifecycle_signal: LifecycleSignal) -> Self {
400        Self {
401            state,
402            lifecycle_signal,
403        }
404    }
405    fn with_control(&self, action: impl FnOnce(&CydSimulatorControlWasm)) {
406        let state = self.state.borrow();
407        if !state.stopped {
408            if let Some(control) = state.live_control.as_ref() {
409                action(control);
410            }
411        }
412    }
413}
414
415#[wasm_bindgen(js_class = CydWebAppHandle)]
416impl Handle {
417    /// Press the simulated touch panel at canvas coordinates.
418    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
419    pub fn touch_down(&self, position_x: f32, position_y: f32) {
420        self.with_control(|control| control.touch_down(position_x, position_y));
421    }
422    /// Move the simulated touch point.
423    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
424    pub fn touch_move(&self, position_x: f32, position_y: f32) {
425        self.with_control(|control| control.touch_move(position_x, position_y));
426    }
427    /// Release the simulated touch panel.
428    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
429    pub fn touch_up(&self) {
430        self.with_control(CydSimulatorControlWasm::touch_up);
431    }
432    /// Press the simulated BOOT button.
433    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
434    pub fn boot_down(&self) {
435        self.with_control(CydSimulatorControlWasm::boot_down);
436    }
437    /// Release the simulated BOOT button.
438    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
439    pub fn boot_up(&self) {
440        self.with_control(CydSimulatorControlWasm::boot_up);
441    }
442    /// Return whether the current orientation is inverted.
443    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
444    pub fn orientation_is_inverted(&self) -> bool {
445        self.state
446            .borrow()
447            .live_control
448            .as_ref()
449            .is_some_and(CydSimulatorControlWasm::orientation_is_inverted)
450    }
451    /// Remove and return the oldest pending framework notice.
452    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
453    pub fn take_notice(&self) -> Option<Notice> {
454        self.state.borrow_mut().notices.pop_front()
455    }
456    /// Request an application restart.
457    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
458    pub fn request_restart(&self) {
459        self.lifecycle_signal.request(HostRequest::Restart);
460    }
461    /// Clear framework storage and restart the application.
462    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
463    pub fn clear_storage_and_restart(&self) {
464        self.lifecycle_signal.request(HostRequest::ClearStorage);
465    }
466    /// Return whether the application has requested the clock control.
467    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
468    pub fn clock_control_is_visible(&self) -> bool {
469        self.state.borrow().clock_control_visible.get()
470    }
471    /// Set the simulated local time, in seconds after midnight.
472    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
473    pub fn set_clock_time_of_day(&self, seconds_of_day: u32) -> Result<(), JsValue> {
474        if seconds_of_day >= 86_400 {
475            return Err(JsValue::from_str(
476                "time of day must be between 0 and 86399 seconds",
477            ));
478        }
479        self.state
480            .borrow()
481            .clock_time_of_day
482            .set(Some(seconds_of_day));
483        Ok(())
484    }
485    /// Restore the browser's live local clock.
486    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
487    pub fn use_live_clock(&self) {
488        self.state.borrow().clock_time_of_day.set(None);
489    }
490    /// Return the configured page title.
491    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
492    pub fn page_title(&self) -> String {
493        self.state.borrow().page_info.title.into()
494    }
495    /// Return the configured preview text.
496    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
497    pub fn page_preview(&self) -> String {
498        self.state.borrow().page_info.preview.into()
499    }
500    /// Return the configured page description.
501    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
502    pub fn page_description(&self) -> String {
503        self.state.borrow().page_info.description.into()
504    }
505    /// Return the configured interaction instructions.
506    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
507    pub fn page_controls(&self) -> String {
508        self.state.borrow().page_info.controls.into()
509    }
510    /// Return the configured platform-neutral source URL.
511    /// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
512    pub fn page_core_code_url(&self) -> String {
513        self.state.borrow().page_info.core_code_url.into()
514    }
515}
516
517/// Start a browser CYD application in the shared simulator shell.
518/// See the compiled browser-shell example on [`crate::wasm::cyd_web`].
519pub fn start<Run, Error>(
520    canvas_id: &str,
521    config: Config,
522    page_info: PageInfo,
523    inner_main: Run,
524) -> Result<Handle, JsValue>
525where
526    Run: AsyncFnMut(Capabilities) -> Result<Command, Error> + 'static,
527    Error: Debug + 'static,
528{
529    let canvas = canvas(canvas_id)?;
530    let mut orientation_flash_block =
531        FlashBlockWasm::new(&format!("{}/orientation", config.storage_namespace))
532            .map_err(|error| JsValue::from_str(&format!("orientation storage: {error:?}")))?;
533    let orientation = orientation_flash_block
534        .load::<Orientation>()
535        .map_err(|error| JsValue::from_str(&format!("orientation load: {error:?}")))?
536        .unwrap_or(config.initial_orientation);
537    let simulator = CydSimulatorWasm::new_with_style(
538        canvas.clone(),
539        orientation,
540        config.background_color,
541        config.foreground_color,
542        config.font,
543    )?;
544    let (cyd, button, control) = simulator.into_parts();
545    let state = Rc::new(RefCell::new(SupervisorState {
546        live_control: Some(control),
547        notices: std::collections::VecDeque::new(),
548        orientation,
549        stopped: false,
550        page_info,
551        clock_time_of_day: Rc::new(Cell::new(None)),
552        clock_control_visible: Rc::new(Cell::new(false)),
553    }));
554    let lifecycle_signal = LifecycleSignal::new();
555    let handle = Handle::new(state.clone(), lifecycle_signal.clone());
556    wasm_bindgen_futures::spawn_local(supervise(
557        canvas,
558        config,
559        orientation_flash_block,
560        state,
561        lifecycle_signal,
562        inner_main,
563        Some((cyd, button)),
564    ));
565    Ok(handle)
566}
567
568fn canvas(canvas_id: &str) -> Result<HtmlCanvasElement, JsValue> {
569    let document = window()
570        .ok_or_else(|| JsValue::from_str("browser window unavailable"))?
571        .document()
572        .ok_or_else(|| JsValue::from_str("document unavailable"))?;
573    document
574        .get_element_by_id(canvas_id)
575        .ok_or_else(|| JsValue::from_str("canvas element unavailable"))?
576        .dyn_into::<HtmlCanvasElement>()
577        .map_err(Into::into)
578}
579
580async fn supervise<Run, Error>(
581    canvas: HtmlCanvasElement,
582    config: Config,
583    mut orientation_flash_block: FlashBlockWasm,
584    state: Rc<RefCell<SupervisorState>>,
585    lifecycle_signal: LifecycleSignal,
586    mut inner_main: Run,
587    initial_session: Option<(CydWasm, ButtonWasm)>,
588) where
589    Run: AsyncFnMut(Capabilities) -> Result<Command, Error> + 'static,
590    Error: Debug + 'static,
591{
592    let mut session = initial_session;
593    loop {
594        let (cyd, button) = match session.take() {
595            Some(session) => session,
596            None => {
597                let orientation = state.borrow().orientation;
598                match CydSimulatorWasm::new_with_style(
599                    canvas.clone(),
600                    orientation,
601                    config.background_color,
602                    config.foreground_color,
603                    config.font,
604                ) {
605                    Ok(simulator) => {
606                        let (cyd, button, control) = simulator.into_parts();
607                        state.borrow_mut().live_control = Some(control);
608                        (cyd, button)
609                    }
610                    Err(error) => {
611                        fatal(&state, format!("simulator construction failed: {error:?}"));
612                        break;
613                    }
614                }
615            }
616        };
617        let (clock_time_of_day, clock_control_visible) = {
618            let state_ref = state.borrow();
619            (
620                state_ref.clock_time_of_day.clone(),
621                state_ref.clock_control_visible.clone(),
622            )
623        };
624        let clock_sync =
625            ClockSyncWasm::new_with_control_state(clock_time_of_day, clock_control_visible);
626        let application = Capabilities {
627            cyd,
628            button,
629            clock_sync,
630            wifi_simulator: WifiSimulatorWasm::new(config.storage_namespace),
631            dns_simulator: DnsSimulatorWasm::standard(),
632        };
633        let command = match select(inner_main(application), lifecycle_signal.wait()).await {
634            Either::First(result) => match result {
635                Ok(command) => command,
636                Err(error) => {
637                    fatal(&state, format!("application failed: {error:?}"));
638                    break;
639                }
640            },
641            Either::Second(request) => {
642                match apply_host_request(request, &config, &mut orientation_flash_block, &state) {
643                    Ok(()) => Command::Restart,
644                    Err(error) => {
645                        fatal(&state, error);
646                        break;
647                    }
648                }
649            }
650        };
651        release_control(&state);
652        match command {
653            Command::Stop => break,
654            Command::Restart => {}
655            Command::ResetWifi => {
656                WifiSimulatorWasm::new(config.storage_namespace).reset();
657                state
658                    .borrow_mut()
659                    .notices
660                    .push_back(Notice::new("wifi-simulated", NoticeSeverity::Info));
661            }
662            Command::CalibrationNotNeeded => state
663                .borrow_mut()
664                .notices
665                .push_back(Notice::new("calibration-not-needed", NoticeSeverity::Info)),
666            Command::Reorientate(orientation) => {
667                if let Err(error) = orientation_flash_block.save(&orientation) {
668                    fatal(&state, format!("orientation save failed: {error:?}"));
669                    break;
670                }
671                state.borrow_mut().orientation = orientation;
672            }
673        }
674    }
675    release_control(&state);
676    state.borrow_mut().stopped = true;
677}
678
679fn release_control(state: &Rc<RefCell<SupervisorState>>) {
680    let control = state.borrow_mut().live_control.take();
681    if let Some(control) = control {
682        control.reset_transient_state();
683    }
684}
685fn fatal(state: &Rc<RefCell<SupervisorState>>, message: String) {
686    state.borrow_mut().notices.push_back(Notice::fatal(message));
687}
688fn apply_host_request(
689    request: HostRequest,
690    config: &Config,
691    flash: &mut FlashBlockWasm,
692    state: &Rc<RefCell<SupervisorState>>,
693) -> Result<(), String> {
694    if matches!(request, HostRequest::ClearStorage) {
695        flash
696            .clear()
697            .map_err(|error| format!("storage clear failed: {error:?}"))?;
698        state.borrow_mut().orientation = config.initial_orientation;
699    }
700    Ok(())
701}