ryu_hardware/feed.rs
1//! The dashboard *feed* seam: the minimal contract the hardware device-dashboard
2//! renderer + the display-nudge loop need from the Home-dashboards capability,
3//! inverted so this kernel crate has ZERO compile-time dependency on
4//! `ryu_dashboards`.
5//!
6//! ## Why this exists
7//!
8//! A hardware device (TRMNL model) renders a Home dashboard onto its e-ink / LCD
9//! panel: the device polls Core, Core renders the device's bound dashboard to a
10//! panel image. That render reads dashboard widgets + rasterizes them — a
11//! *dashboards* concern that had been welded into `ryu_hardware::api` as a direct
12//! `ryu_dashboards::DashboardEngine` field. Dashboards is now a swappable,
13//! out-of-process app; a kernel crate cannot hard-link it.
14//!
15//! [`DashboardFeed`] is the inversion. It exposes ONLY what the renderer + nudge
16//! loop need (render a device's dashboard, read/write its config + binding,
17//! subscribe to change events), in terms of plain owned types — never a
18//! `ryu_dashboards` type. Core provides the impl:
19//!
20//! - in-process (`InProcDashboardFeed`) — wraps the in-process engine;
21//! - out-of-process (`dashboards_client::DashboardsClient`) — proxies to the
22//! `ryu-dashboards` sidecar over loopback (+ its SSE stream for change events).
23//!
24//! The device *auth* (per-device Bearer verification against the registry) stays
25//! Core-side; only the render + data cross this seam.
26
27use serde_json::Value;
28
29/// A device's panel geometry echoed in the display manifest + config `screen`
30/// object. Computed by the feed impl from the device class + prefs (so the panel
31/// constants live with the renderer, not here), and carried back as plain data.
32#[derive(Clone, Debug)]
33pub struct ScreenProfile {
34 pub w: u32,
35 pub h: u32,
36 pub bit_depth: u8,
37 /// Wire palette string (`"mono"` / `"rgba"` / `"rgb565"`).
38 pub palette: String,
39 pub rotation: u16,
40}
41
42/// The display-manifest facts for a device: the content revision (so the device
43/// can skip an unchanged re-download), its poll interval, and its panel geometry.
44#[derive(Clone, Debug)]
45pub struct DeviceManifest {
46 pub rev: String,
47 pub refresh_rate: u32,
48 pub screen: ScreenProfile,
49}
50
51/// A rendered device image plus the metadata the display endpoint returns.
52#[derive(Clone, Debug)]
53pub struct RenderedImage {
54 pub bytes: Vec<u8>,
55 /// `image/png` or `application/octet-stream` (packed mono / rgb565).
56 pub content_type: String,
57 /// Content hash the device caches against (`?rev=`).
58 pub rev: String,
59}
60
61/// The outcome of a device-dashboard write.
62#[derive(Clone, Debug)]
63pub struct SetDeviceResult {
64 pub dashboard_id: String,
65 pub refresh_rate: u32,
66}
67
68/// A device → dashboard binding (the nudge loop's work list).
69#[derive(Clone, Debug)]
70pub struct DeviceBinding {
71 pub device_id: String,
72 pub dashboard_id: String,
73}
74
75/// The dashboards capability, seen through the narrow hole the hardware surface
76/// needs. Implemented by Core (in-process or sidecar-backed).
77#[async_trait::async_trait]
78pub trait DashboardFeed: Send + Sync {
79 /// The display manifest facts for a device (renders internally to compute the
80 /// current `rev`). `device_type` is the RHP wire string; `prefs` the device's
81 /// saved prefs (may carry a `screen` override).
82 async fn device_manifest(
83 &self,
84 device_id: &str,
85 device_name: &str,
86 device_type: &str,
87 prefs: &Value,
88 ) -> Result<DeviceManifest, String>;
89
90 /// Render a device's dashboard image. Returns `None` when `known_rev` still
91 /// matches the freshly-rendered content (the caller answers `304`).
92 async fn device_image(
93 &self,
94 device_id: &str,
95 device_name: &str,
96 device_type: &str,
97 prefs: &Value,
98 known_rev: Option<&str>,
99 ) -> Result<Option<RenderedImage>, String>;
100
101 /// The device-dashboard config JSON (binding + widgets + screen).
102 async fn device_config(
103 &self,
104 device_id: &str,
105 device_name: &str,
106 device_type: &str,
107 prefs: &Value,
108 ) -> Result<Value, String>;
109
110 /// Set a device's poll interval and/or replace its widget selection.
111 async fn set_device_config(
112 &self,
113 device_id: &str,
114 device_name: &str,
115 refresh_rate: Option<u32>,
116 widgets: Option<Value>,
117 ) -> Result<SetDeviceResult, String>;
118
119 /// Drop a device's dashboard binding (on device revoke). Best-effort.
120 async fn delete_device(&self, device_id: &str);
121
122 /// Every device → dashboard binding (the nudge loop resolves which device(s)
123 /// bind a changed dashboard).
124 async fn list_bindings(&self) -> Result<Vec<DeviceBinding>, String>;
125
126 /// Subscribe to dashboard change events, yielding the changed `dashboard_id`.
127 /// The impl owns any reconnect/backoff (a loopback SSE can drop); the nudge
128 /// loop just drains the channel. A dropped subscription is latency-only — the
129 /// device still re-polls on its own cadence.
130 async fn subscribe_changes(&self) -> tokio::sync::mpsc::Receiver<String>;
131}