Skip to main content

hardless_core/
log.rs

1//! The per-device event log (FR-03).
2
3use std::collections::VecDeque;
4
5use bytes::Bytes;
6use serde::{Deserialize, Serialize};
7
8use crate::bridge::SerialSignals;
9use crate::device::{ControlCommand, LogLevel, PeripheralSignal, WedgeScan};
10use crate::fault::{FaultId, FaultOptions};
11use crate::ids::{DeviceId, JobId};
12use crate::nfc::NfcEvent;
13use crate::time::Millis;
14
15/// Default number of records kept per device.
16pub const DEFAULT_LOG_CAPACITY: usize = 10_000;
17
18/// Which way bytes travelled.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
20#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
21#[serde(rename_all = "kebab-case")]
22pub enum Direction {
23    /// App -> device.
24    HostToDevice,
25    /// Device -> app.
26    DeviceToHost,
27}
28
29/// What happened, in an [`EventRecord`].
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
31#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
32#[serde(tag = "type", rename_all = "kebab-case")]
33pub enum EventKind {
34    /// The device was created.
35    Created {
36        /// Device kind.
37        kind: String,
38    },
39    /// Host -> device bytes.
40    BytesIn {
41        /// The bytes.
42        #[serde(rename = "dataHex", with = "crate::hex::serde_bytes")]
43        #[cfg_attr(feature = "ts", ts(rename = "dataHex", type = "string"))]
44        data: Bytes,
45    },
46    /// Device -> host bytes.
47    BytesOut {
48        /// The bytes.
49        #[serde(rename = "dataHex", with = "crate::hex::serde_bytes")]
50        #[cfg_attr(feature = "ts", ts(rename = "dataHex", type = "string"))]
51        data: Bytes,
52    },
53    /// A control command was sent to the device.
54    Control {
55        /// The command.
56        command: ControlCommand,
57    },
58    /// A fault was turned on or off.
59    #[serde(rename_all = "camelCase")]
60    Fault {
61        /// The fault.
62        fault_id: FaultId,
63        /// Whether it is now on.
64        active: bool,
65        /// Its options.
66        #[cfg_attr(
67            feature = "ts",
68            ts(as = "std::collections::HashMap<String, serde_json::Value>")
69        )]
70        options: FaultOptions,
71    },
72    /// The device state changed.
73    StateChanged {
74        /// Snapshot of the new state.
75        state: serde_json::Value,
76    },
77    /// The device became reachable.
78    Connected,
79    /// The device stopped being reachable.
80    Disconnected,
81    /// A print job closed.
82    #[serde(rename_all = "camelCase")]
83    JobCompleted {
84        /// The job.
85        job_id: JobId,
86    },
87    /// A device diagnostic.
88    Log {
89        /// Severity.
90        level: LogLevel,
91        /// Message.
92        message: String,
93    },
94    /// The device was reset.
95    Reset,
96    /// The app set serial output signals (DTR, RTS, break).
97    Signals {
98        /// The signals that were set.
99        signals: SerialSignals,
100    },
101    /// A peripheral signal between attached devices, recorded in both devices' logs.
102    Peripheral {
103        /// The sending device.
104        from: DeviceId,
105        /// The receiving device.
106        to: DeviceId,
107        /// The signal.
108        signal: PeripheralSignal,
109    },
110    /// A keyboard-wedge scanner typed a scan into the page.
111    Wedge {
112        /// What was typed.
113        scan: WedgeScan,
114    },
115    /// An NFC reader read a card, failed to, or answered a write from the app.
116    Nfc {
117        /// What happened.
118        event: NfcEvent,
119    },
120}
121
122impl EventKind {
123    /// The wire name of this kind, e.g. `"bytes-in"`.
124    pub fn name(&self) -> &'static str {
125        match self {
126            EventKind::Created { .. } => "created",
127            EventKind::BytesIn { .. } => "bytes-in",
128            EventKind::BytesOut { .. } => "bytes-out",
129            EventKind::Control { .. } => "control",
130            EventKind::Fault { .. } => "fault",
131            EventKind::StateChanged { .. } => "state-changed",
132            EventKind::Connected => "connected",
133            EventKind::Disconnected => "disconnected",
134            EventKind::JobCompleted { .. } => "job-completed",
135            EventKind::Log { .. } => "log",
136            EventKind::Reset => "reset",
137            EventKind::Signals { .. } => "signals",
138            EventKind::Peripheral { .. } => "peripheral",
139            EventKind::Wedge { .. } => "wedge",
140            EventKind::Nfc { .. } => "nfc",
141        }
142    }
143}
144
145/// One entry in a device's event log.
146///
147/// `seq` is unique and increasing across all devices in a registry, so clients can poll with
148/// `since=<last seq>`. `at` never goes backwards.
149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
150#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
151#[serde(rename_all = "camelCase")]
152pub struct EventRecord {
153    /// Sequence number.
154    pub seq: u64,
155    /// Monotonic timestamp.
156    pub at: Millis,
157    /// The device.
158    pub device_id: DeviceId,
159    /// What happened.
160    #[serde(flatten)]
161    #[cfg_attr(feature = "ts", ts(flatten))]
162    pub kind: EventKind,
163}
164
165impl EventRecord {
166    /// The direction of a byte, keystroke or NFC record, `None` for everything else (used by
167    /// the log filter).
168    pub fn direction(&self) -> Option<Direction> {
169        match &self.kind {
170            EventKind::BytesIn { .. }
171            | EventKind::Control {
172                command: ControlCommand::NfcWrite { .. } | ControlCommand::NfcMakeReadOnly,
173            } => Some(Direction::HostToDevice),
174            EventKind::BytesOut { .. }
175            | EventKind::Wedge { .. }
176            | EventKind::Nfc {
177                event: NfcEvent::Reading(_) | NfcEvent::ReadError { .. },
178            } => Some(Direction::DeviceToHost),
179            _ => None,
180        }
181    }
182}
183
184/// A bounded log that drops the oldest record when full.
185///
186/// ```
187/// use hardless_core::{EventKind, EventLog, EventRecord, Millis};
188/// let mut log = EventLog::new(2);
189/// for seq in 1..=3 {
190///     log.push(EventRecord { seq, at: Millis(0), device_id: "d".parse().unwrap(), kind: EventKind::Reset });
191/// }
192/// assert_eq!(log.iter().map(|r| r.seq).collect::<Vec<_>>(), [2, 3]);
193/// ```
194#[derive(Clone, Debug)]
195pub struct EventLog {
196    capacity: usize,
197    records: VecDeque<EventRecord>,
198}
199
200impl EventLog {
201    /// An empty log holding at most `capacity` records (at least one).
202    pub fn new(capacity: usize) -> Self {
203        let capacity = capacity.max(1);
204        Self {
205            capacity,
206            records: VecDeque::with_capacity(capacity.min(1024)),
207        }
208    }
209
210    /// Appends a record, dropping the oldest if the log is full.
211    pub fn push(&mut self, record: EventRecord) {
212        if self.records.len() == self.capacity {
213            self.records.pop_front();
214        }
215        self.records.push_back(record);
216    }
217
218    /// Records oldest first.
219    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &EventRecord> + ExactSizeIterator {
220        self.records.iter()
221    }
222
223    /// Records with `seq` greater than `since`, oldest first.
224    pub fn since(&self, since: u64) -> impl Iterator<Item = &EventRecord> {
225        // Records are in `seq` order, so skip the old ones with a binary search.
226        let start = self.records.partition_point(|r| r.seq <= since);
227        self.records.range(start..)
228    }
229
230    /// Number of records.
231    pub fn len(&self) -> usize {
232        self.records.len()
233    }
234
235    /// Whether the log is empty.
236    pub fn is_empty(&self) -> bool {
237        self.records.is_empty()
238    }
239
240    /// Maximum number of records.
241    pub fn capacity(&self) -> usize {
242        self.capacity
243    }
244
245    /// Removes every record.
246    pub fn clear(&mut self) {
247        self.records.clear();
248    }
249}