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}