Skip to main content

irbt_proto/
messages.rs

1//! Message types. See the crate docs: variant order here is wire ABI.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5use core::fmt;
6use core::str::FromStr;
7
8use serde::{Deserialize, Serialize};
9
10/// A raw 32-bit NEC code as decoded by the firmware's IR receiver
11/// (address, ~address, command, ~command, MSB-first).
12pub type IrCode = u32;
13
14/// Top-level unit on the wire. `Request` flows host → device; everything
15/// else flows device → host.
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
17pub enum Frame {
18    Request(Request),
19    Response(Response),
20    Event(Event),
21    Log(Log),
22}
23
24#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
25pub struct Request {
26    /// Echoed back in the matching [`Response`].
27    pub id: u32,
28    pub command: Command,
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
32pub enum Command {
33    Ping,
34    GetInfo,
35    GetStatus,
36    /// Returns one [`KeymapPage`] starting at `offset`.
37    ListKeymap {
38        offset: u16,
39    },
40    /// Add or replace a mapping; persisted immediately.
41    SetMapping(Mapping),
42    RemoveMapping {
43        ir: IrCode,
44    },
45    /// Remove every mapping.
46    ClearKeymap,
47    /// Restore the firmware's compiled-in default keymap.
48    ResetKeymap,
49    /// Bind the next received IR code to `key`. Replies immediately; the
50    /// outcome arrives later as [`Event::Learned`], [`Event::LearnTimedOut`]
51    /// or [`Event::LearnFailed`].
52    Learn {
53        key: HidKey,
54        timeout_ms: u32,
55    },
56    CancelLearn,
57    /// Send a key press + release over BLE HID, bypassing IR.
58    SendHid {
59        key: HidKey,
60    },
61    /// Transmit a NEC code through the IR blaster.
62    SendIr {
63        code: IrCode,
64    },
65    /// Enable/disable [`Event::IrReceived`] notifications.
66    SetMonitor {
67        enabled: bool,
68    },
69    /// Returns [`BuildInfo`]: git commit, toolchain and component versions.
70    GetBuildInfo,
71}
72
73#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
74pub struct Response {
75    pub id: u32,
76    pub result: Result<Reply, String>,
77}
78
79#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
80pub enum Reply {
81    /// Generic success for commands with nothing to return.
82    Ok,
83    Pong,
84    Info(DeviceInfo),
85    Status(Status),
86    Keymap(KeymapPage),
87    BuildInfo(BuildInfo),
88}
89
90/// Unsolicited device → host notifications.
91#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
92pub enum Event {
93    /// Only sent while monitoring is enabled ([`Command::SetMonitor`]).
94    IrReceived {
95        /// For a repeat frame this is the code being repeated (0 if none yet).
96        code: IrCode,
97        repeat: bool,
98        /// The key this code is mapped to, if any.
99        key: Option<HidKey>,
100    },
101    Learned(Mapping),
102    LearnTimedOut,
103    BleConnection {
104        connected: bool,
105    },
106    /// A code arrived in learn mode but couldn't be stored (e.g. keymap full).
107    LearnFailed {
108        reason: String,
109    },
110}
111
112#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
113pub struct Log {
114    pub level: LogLevel,
115    /// One log line, without the trailing newline.
116    pub message: String,
117}
118
119#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
120pub enum LogLevel {
121    Error,
122    Warn,
123    Info,
124    Debug,
125    Verbose,
126}
127
128#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
129pub struct DeviceInfo {
130    pub protocol_version: u16,
131    pub firmware_version: String,
132    pub max_mappings: u16,
133}
134
135/// How the firmware was built. A free-form component list, so new entries
136/// need no protocol change.
137#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
138pub struct BuildInfo {
139    /// `git describe` output and commit date, e.g. `c7f30e8-dirty 2026-10-27`.
140    pub git: String,
141    /// Toolchain, ESP-IDF and crate versions, in display order.
142    pub components: Vec<Component>,
143}
144
145#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
146pub struct Component {
147    pub name: String,
148    pub version: String,
149}
150
151#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
152pub struct Status {
153    pub uptime_ms: u64,
154    pub ble_connected: bool,
155    pub mappings: u16,
156    pub monitor: bool,
157    /// Key currently waiting for an IR code, if learn mode is active.
158    pub learning: Option<HidKey>,
159    /// Latest battery reading; `None` if the firmware has no battery
160    /// monitor or hasn't taken a reading yet.
161    pub battery: Option<Battery>,
162}
163
164#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
165pub struct Battery {
166    /// Battery voltage, measured on the battery side of the input diode.
167    pub millivolts: u16,
168    /// Estimated charge (0-100); `None` if no battery is fitted.
169    pub percent: Option<u8>,
170}
171
172#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub struct Mapping {
174    pub ir: IrCode,
175    pub key: HidKey,
176}
177
178#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
179pub struct KeymapPage {
180    pub offset: u16,
181    pub total: u16,
182    pub entries: Vec<Mapping>,
183}
184
185/// Keys the bridge can send to Google TV.
186///
187/// Append-only (wire ABI); keep [`HidKey::ALL`] and the name table in sync.
188/// The serde renames only affect self-describing formats (JSON/TOML on the
189/// host); postcard encodes the variant index.
190#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
191#[serde(rename_all = "kebab-case")]
192pub enum HidKey {
193    Up,
194    Down,
195    Left,
196    Right,
197    Select,
198    Back,
199    Home,
200    Menu,
201    #[serde(rename = "vol-up")]
202    VolumeUp,
203    #[serde(rename = "vol-down")]
204    VolumeDown,
205    Mute,
206    PlayPause,
207    #[serde(rename = "next")]
208    NextTrack,
209    #[serde(rename = "prev")]
210    PrevTrack,
211    Power,
212}
213
214/// Which HID report a key is sent in, and its usage ID there.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum HidUsage {
217    /// Keyboard/Keypad page (0x07).
218    Keyboard(u8),
219    /// Consumer page (0x0C).
220    Consumer(u16),
221}
222
223impl HidKey {
224    pub const ALL: [HidKey; 15] = [
225        HidKey::Up,
226        HidKey::Down,
227        HidKey::Left,
228        HidKey::Right,
229        HidKey::Select,
230        HidKey::Back,
231        HidKey::Home,
232        HidKey::Menu,
233        HidKey::VolumeUp,
234        HidKey::VolumeDown,
235        HidKey::Mute,
236        HidKey::PlayPause,
237        HidKey::NextTrack,
238        HidKey::PrevTrack,
239        HidKey::Power,
240    ];
241
242    /// Stable, CLI-friendly name (`vol-up`, `play-pause`, …).
243    #[must_use]
244    pub const fn name(self) -> &'static str {
245        match self {
246            HidKey::Up => "up",
247            HidKey::Down => "down",
248            HidKey::Left => "left",
249            HidKey::Right => "right",
250            HidKey::Select => "select",
251            HidKey::Back => "back",
252            HidKey::Home => "home",
253            HidKey::Menu => "menu",
254            HidKey::VolumeUp => "vol-up",
255            HidKey::VolumeDown => "vol-down",
256            HidKey::Mute => "mute",
257            HidKey::PlayPause => "play-pause",
258            HidKey::NextTrack => "next",
259            HidKey::PrevTrack => "prev",
260            HidKey::Power => "power",
261        }
262    }
263
264    /// HID usage per the USB HID Usage Tables (navigation keys on the
265    /// keyboard page, Android TV system/media keys on the consumer page).
266    #[must_use]
267    pub const fn usage(self) -> HidUsage {
268        match self {
269            HidKey::Up => HidUsage::Keyboard(0x52),
270            HidKey::Down => HidUsage::Keyboard(0x51),
271            HidKey::Left => HidUsage::Keyboard(0x50),
272            HidKey::Right => HidUsage::Keyboard(0x4F),
273            HidKey::Select => HidUsage::Keyboard(0x28),
274            HidKey::Back => HidUsage::Consumer(0x0224),
275            HidKey::Home => HidUsage::Consumer(0x0223),
276            HidKey::Menu => HidUsage::Consumer(0x0040),
277            HidKey::VolumeUp => HidUsage::Consumer(0x00E9),
278            HidKey::VolumeDown => HidUsage::Consumer(0x00EA),
279            HidKey::Mute => HidUsage::Consumer(0x00E2),
280            HidKey::PlayPause => HidUsage::Consumer(0x00CD),
281            HidKey::NextTrack => HidUsage::Consumer(0x00B5),
282            HidKey::PrevTrack => HidUsage::Consumer(0x00B6),
283            HidKey::Power => HidUsage::Consumer(0x0030),
284        }
285    }
286}
287
288impl fmt::Display for HidKey {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        f.write_str(self.name())
291    }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct UnknownKey;
296
297impl fmt::Display for UnknownKey {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        f.write_str("unknown key name")
300    }
301}
302
303impl core::error::Error for UnknownKey {}
304
305impl FromStr for HidKey {
306    type Err = UnknownKey;
307
308    /// Case-insensitive; accepts `_` in place of `-`.
309    fn from_str(s: &str) -> Result<Self, Self::Err> {
310        HidKey::ALL
311            .into_iter()
312            .find(|key| {
313                let name = key.name().as_bytes();
314                name.len() == s.len()
315                    && name
316                        .iter()
317                        .zip(s.bytes())
318                        .all(|(&n, c)| n == c.to_ascii_lowercase() || (n == b'-' && c == b'_'))
319            })
320            .ok_or(UnknownKey)
321    }
322}