Skip to main content

hardless_core/
bridge.rs

1//! Messages of the WebSocket bridge between the browser shim and the server (PRD section
2//! 11.2). One JSON message per frame; byte payloads are base64.
3//!
4//! Every message has `v: 1`, a `type`, and optionally an `id`. A client message with an
5//! `id` is answered with `ack` (`replyTo` = that id) or `error`.
6
7use serde::{Deserialize, Serialize};
8
9use crate::device::WedgeScan;
10use crate::ids::DeviceId;
11use crate::nfc::{NdefRecord, NfcReading};
12use crate::registry::DeviceSummary;
13use crate::Bytes;
14
15/// The protocol version in every message's `v`.
16pub const BRIDGE_VERSION: u8 = 1;
17
18/// A message from the shim to the server.
19///
20/// ```
21/// use hardless_core::bridge::{ClientFrame, ClientMessage};
22/// let frame: ClientFrame = serde_json::from_str(
23///     r#"{ "v": 1, "type": "write", "id": "7", "deviceId": "printer-1", "data": "G0A=" }"#,
24/// ).unwrap();
25/// assert_eq!(frame.id.as_deref(), Some("7"));
26/// assert!(matches!(frame.message, ClientMessage::Write { ref data, .. } if data.as_ref() == b"\x1b@"));
27/// ```
28#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
30pub struct ClientFrame {
31    /// Always [`BRIDGE_VERSION`].
32    pub v: u8,
33    /// Correlates the server's `ack` or `error`.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    #[cfg_attr(feature = "ts", ts(optional))]
36    pub id: Option<String>,
37    /// The message.
38    #[serde(flatten)]
39    #[cfg_attr(feature = "ts", ts(flatten))]
40    pub message: ClientMessage,
41}
42
43/// The body of a [`ClientFrame`].
44#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
45#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
46#[serde(tag = "type", rename_all = "kebab-case")]
47pub enum ClientMessage {
48    /// First message: who is connecting and which APIs it shims.
49    #[serde(rename_all = "camelCase")]
50    Hello {
51        /// The page's origin.
52        origin: String,
53        /// The shim version.
54        shim_version: String,
55        /// Shimmed APIs: `serial`, `usb`, `nfc`, `wedge`.
56        apis: Vec<String>,
57    },
58    /// `SerialPort.open()`.
59    #[serde(rename_all = "camelCase")]
60    Open {
61        /// The device.
62        device_id: DeviceId,
63        /// The options passed to `open()`, e.g. `{ "baudRate": 9600 }`.
64        #[serde(default)]
65        options: serde_json::Value,
66    },
67    /// `SerialPort.close()`.
68    #[serde(rename_all = "camelCase")]
69    Close {
70        /// The device.
71        device_id: DeviceId,
72    },
73    /// `SerialPort.setSignals()`: output signals set by the app (PRD section 8.1).
74    #[serde(rename_all = "camelCase")]
75    Signals {
76        /// The device.
77        device_id: DeviceId,
78        /// The signals that were set.
79        signals: SerialSignals,
80    },
81    /// Bytes written by the app.
82    #[serde(rename_all = "camelCase")]
83    Write {
84        /// The device.
85        device_id: DeviceId,
86        /// The bytes, base64 on the wire.
87        #[serde(with = "crate::base64::serde_bytes")]
88        #[cfg_attr(feature = "ts", ts(type = "string"))]
89        data: Bytes,
90    },
91    /// `NDEFReader.write()`: write records to the card on an NFC reader. The server replies
92    /// `ack` once written, or `error` (`invalid_state` if no card is on the reader, so the
93    /// shim waits for the next tap; `not_allowed` if the card refuses).
94    #[serde(rename_all = "camelCase")]
95    NfcWrite {
96        /// The NFC reader.
97        device_id: DeviceId,
98        /// The records to write.
99        records: Vec<NdefRecord>,
100        /// Whether existing data may be replaced (default `true`).
101        #[serde(default, skip_serializing_if = "Option::is_none")]
102        #[cfg_attr(feature = "ts", ts(optional))]
103        overwrite: Option<bool>,
104    },
105    /// `NDEFReader.makeReadOnly()`: lock the card on an NFC reader; replies as `nfc-write`.
106    #[serde(rename_all = "camelCase")]
107    NfcMakeReadOnly {
108        /// The NFC reader.
109        device_id: DeviceId,
110    },
111}
112
113/// Output signals from `SerialPort.setSignals()`; only the ones the app set are present.
114///
115/// ```
116/// use hardless_core::bridge::SerialSignals;
117/// let s: SerialSignals = serde_json::from_str(r#"{ "dataTerminalReady": true }"#).unwrap();
118/// assert_eq!(s.data_terminal_ready, Some(true));
119/// assert_eq!(s.request_to_send, None);
120/// ```
121#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
122#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
123#[serde(rename_all = "camelCase")]
124pub struct SerialSignals {
125    /// Data Terminal Ready (DTR).
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    #[cfg_attr(feature = "ts", ts(optional))]
128    pub data_terminal_ready: Option<bool>,
129    /// Request To Send (RTS).
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    #[cfg_attr(feature = "ts", ts(optional))]
132    pub request_to_send: Option<bool>,
133    /// Break.
134    #[serde(rename = "break", default, skip_serializing_if = "Option::is_none")]
135    #[cfg_attr(feature = "ts", ts(rename = "break", optional))]
136    pub brk: Option<bool>,
137}
138
139/// A message from the server to the shim.
140#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
141#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
142pub struct ServerFrame {
143    /// Always [`BRIDGE_VERSION`].
144    pub v: u8,
145    /// The message.
146    #[serde(flatten)]
147    #[cfg_attr(feature = "ts", ts(flatten))]
148    pub message: ServerMessage,
149}
150
151impl ServerFrame {
152    /// A frame with the current protocol version.
153    pub fn new(message: ServerMessage) -> Self {
154        Self {
155            v: BRIDGE_VERSION,
156            message,
157        }
158    }
159}
160
161/// The body of a [`ServerFrame`].
162#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
163#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
164#[serde(tag = "type", rename_all = "kebab-case")]
165pub enum ServerMessage {
166    /// The full device list, for the port picker and `getPorts()`. Sent after `hello` and
167    /// whenever a device is added, removed or changes state.
168    Devices {
169        /// Every device.
170        devices: Vec<DeviceSummary>,
171    },
172    /// A client message with an `id` succeeded.
173    #[serde(rename_all = "camelCase")]
174    Ack {
175        /// The client message's `id`.
176        reply_to: String,
177    },
178    /// Device -> host bytes.
179    #[serde(rename_all = "camelCase")]
180    Data {
181        /// The device.
182        device_id: DeviceId,
183        /// The bytes, base64 on the wire.
184        #[serde(with = "crate::base64::serde_bytes")]
185        #[cfg_attr(feature = "ts", ts(type = "string"))]
186        data: Bytes,
187    },
188    /// The device became reachable.
189    #[serde(rename_all = "camelCase")]
190    Connect {
191        /// The device.
192        device_id: DeviceId,
193    },
194    /// The device stopped being reachable.
195    #[serde(rename_all = "camelCase")]
196    Disconnect {
197        /// The device.
198        device_id: DeviceId,
199    },
200    /// A keyboard-wedge scanner typed a scan: the shim presses the keys on the focused
201    /// element (PRD section 8.4). Sent only to shims whose `hello` listed `wedge`.
202    #[serde(rename_all = "camelCase")]
203    WedgeScan {
204        /// The scanner.
205        device_id: DeviceId,
206        /// What to type: `code`, `delayMs` and `terminator`.
207        #[serde(flatten)]
208        #[cfg_attr(feature = "ts", ts(flatten))]
209        scan: WedgeScan,
210    },
211    /// An NFC reader read a card: the shim fires `reading` on scanning `NDEFReader`s. Sent
212    /// only to shims whose `hello` listed `nfc`.
213    #[serde(rename_all = "camelCase")]
214    NfcReading {
215        /// The NFC reader.
216        device_id: DeviceId,
217        /// `serialNumber` and `records`.
218        #[serde(flatten)]
219        #[cfg_attr(feature = "ts", ts(flatten))]
220        reading: NfcReading,
221    },
222    /// An NFC reader failed to read a card: the shim fires `readingerror`. `records` is
223    /// empty.
224    #[serde(rename_all = "camelCase")]
225    NfcError {
226        /// The NFC reader.
227        device_id: DeviceId,
228        /// `serialNumber` and (empty) `records`.
229        #[serde(flatten)]
230        #[cfg_attr(feature = "ts", ts(flatten))]
231        reading: NfcReading,
232    },
233    /// Something failed; `replyTo` names the client message, if any.
234    #[serde(rename_all = "camelCase")]
235    Error {
236        /// A machine-readable code, see [`BridgeErrorCode`].
237        code: BridgeErrorCode,
238        /// A human-readable message.
239        message: String,
240        /// The client message's `id`.
241        #[serde(default, skip_serializing_if = "Option::is_none")]
242        #[cfg_attr(feature = "ts", ts(optional))]
243        reply_to: Option<String>,
244    },
245}
246
247/// Error codes in bridge `error` messages. The shim maps them to `DOMException` names.
248#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
249#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
250#[serde(rename_all = "snake_case")]
251pub enum BridgeErrorCode {
252    /// No such device (`NotFoundError`).
253    NotFound,
254    /// The device is disconnected or went away (`NetworkError`).
255    Network,
256    /// The port is not open, or already open (`InvalidStateError`).
257    InvalidState,
258    /// The device refused, e.g. a read-only NFC card (`NotAllowedError`).
259    NotAllowed,
260    /// The message could not be understood.
261    BadMessage,
262    /// The token is missing or wrong.
263    Unauthorized,
264    /// Anything else.
265    Internal,
266}