Expand description
§hardless-core
The sans-IO core of Hardless, an open-source POS hardware emulator.
It defines what a device is and keeps track of the devices that exist. It does no I/O and
reads no clock: devices are pure state machines that receive bytes, control commands and a
now timestamp from the caller. Transports and the server own all I/O and time.
| Piece | Requirement | Types |
|---|---|---|
| Device plugin interface | FR-01 | DevicePlugin, Device, DeviceEvent, ControlCommand, Job |
| Device registry and event bus | FR-02 | Registry, BusEvent, DeviceSummary, RegistryError |
| Event log | FR-03 | EventLog, EventRecord, EventKind |
| Faults | section 10 | FaultId, FaultOptions, FaultSpec, catalogue() |
| Scenario runner | FR-04 | Scenario, Step, ScenarioRun, ScenarioReport |
| Deterministic clock | FR-05 | Clock, FakeClock, Millis |
§Usage
use hardless_core::{
ConfigError, ControlCommand, Device, DeviceEvent, DeviceId, DevicePlugin, FakeClock,
FaultId, FaultOptions, Millis, Registry,
};
/// A device that answers every byte it receives with `0x06` (ACK).
struct Echo;
impl Device for Echo {
fn kind(&self) -> &'static str { "echo" }
fn state(&self) -> serde_json::Value { serde_json::json!({}) }
fn on_host_bytes(&mut self, data: &[u8], _now: Millis) -> Vec<DeviceEvent> {
vec![DeviceEvent::ToHost(vec![0x06; data.len()].into())]
}
fn on_control(&mut self, _cmd: ControlCommand, _now: Millis) -> Vec<DeviceEvent> { vec![] }
fn on_tick(&mut self, _now: Millis) -> Vec<DeviceEvent> { vec![] }
fn next_deadline(&self) -> Option<Millis> { None }
fn faults(&self) -> Vec<FaultId> { vec![] }
fn set_fault(&mut self, _fault: FaultId, _active: bool, _opts: FaultOptions, _now: Millis)
-> Vec<DeviceEvent> { vec![] }
fn reset(&mut self, _now: Millis) -> Vec<DeviceEvent> { vec![] }
}
struct EchoPlugin;
impl DevicePlugin for EchoPlugin {
fn kind(&self) -> &'static str { "echo" }
fn config_schema(&self) -> schemars::Schema { schemars::json_schema!({ "type": "object" }) }
fn faults(&self) -> Vec<FaultId> { vec![] }
fn create(&self, _id: DeviceId, _config: serde_json::Value)
-> Result<Box<dyn Device>, ConfigError> {
Ok(Box::new(Echo))
}
}
let clock = FakeClock::new();
let mut registry = Registry::new(clock.clone());
registry.register(EchoPlugin).unwrap();
let id = registry.create("echo", Some("echo-1".parse().unwrap()), serde_json::json!({})).unwrap();
clock.advance(5);
let events = registry.host_bytes(&id, b"hi").unwrap();
assert!(matches!(&events[0], DeviceEvent::ToHost(b) if b.as_ref() == [0x06, 0x06]));
// Everything that happened is in the device's event log, stamped by the injected clock.
let log = registry.events(&id, None).unwrap();
assert_eq!(log.last().unwrap().at, Millis(5));
// Observers (dashboard, MCP) read the event bus.
let bus = registry.drain_bus();
assert_eq!(bus.len(), 1); // device:added§Scenarios (FR-04)
A Scenario is a JSON list of steps: scan, tap, wait (ms), setFault, clearFault
and expect. An expect checks a device: state (a subset match), receiptContains and
jobs, optionally waiting up to withinMs for them to become true. A run stops at the first
failing step; the rest are reported as skipped.
ScenarioRun::advance runs every step it can at the registry’s time and returns either
Progress::WaitUntil(t) or Progress::Done(report), plus device events the caller should
deliver (wedge scans, bytes for the app). It never sleeps: the server waits in real time,
tests move a FakeClock.
§Design notes
- Sans-IO. The registry reads time only through the
Clockit was given; the server passes a system clock, tests passFakeClock. No API in this crate blocks or sleeps. - Two outputs per call. Methods that feed a device return its raw
DeviceEvents so the caller (a transport) can routeToHostbytes andWedgescans back to the app. The same events are recorded in the event log, and the ones observers care about are put on the event bus. - Timestamps are monotonic. If an injected clock goes backwards, the registry keeps the last timestamp it handed out.
- Faults are open.
FaultIdis a string so plugins can define their own faults; the IDs in PRD section 10 are provided as constants and incatalogue().
§Features
ts: derives TypeScript bindings withts-rs. Used bycargo xtask gen-types, which writespackages/types.
§License
Apache-2.0
Modules§
- base64
- Standard base64 (RFC 4648, with padding) for byte payloads in bridge messages.
- bridge
- Messages of the WebSocket bridge between the browser shim and the server (PRD section 11.2). One JSON message per frame; byte payloads are base64.
Structs§
- Active
Fault - An active fault and its options.
- Bytes
- A cheaply cloneable and sliceable chunk of contiguous memory.
- Config
Error - A device config was rejected by
DevicePlugin::create. - Device
Id - The ID of a device instance, e.g.
printer-1. - Device
Summary - A device as listed by
Registry::list(andGET /api/v1/devices). - Event
Log - A bounded log that drops the oldest record when full.
- Event
Record - One entry in a device’s event log.
- Fake
Clock - A manually driven clock for tests. Clones share the same time.
- FaultId
- A fault identifier in kebab-case, e.g.
paper-out. - Fault
Option Spec - One option a fault accepts.
- Fault
Spec - A catalogue entry: one row of PRD section 10.
- Invalid
Id - Error returned when a string is not a valid
DeviceIdorFaultId. - Job
- A completed print job.
- JobId
- The ID of a print job, unique per device until the device is reset. Devices number their jobs from 1.
- LogEntry
- A diagnostic message from a device.
- Millis
- A timestamp or duration in milliseconds.
- Ndef
Record - One NDEF record, as Web NFC’s
NDEFRecordInit. - NfcReading
- A card read: the
readingevent ofNDEFReader. - Registry
- Holds plugins and device instances, routes input to devices, records everything in each
device’s event log and publishes
BusEvents. - Registry
Config - Limits for a
Registry. - Scenario
- A scenario: steps run in order, stopping at the first that fails.
- Scenario
Report - The result of a whole run (
POST /scenarios/run). - Scenario
Run - A scenario being run.
- Step
Result - The result of one step.
- Wedge
Scan - What a keyboard-wedge scanner types for one scan: the characters, how fast, and the key
that ends it. On the bridge this is the
wedge-scanmessage.
Enums§
- BusEvent
- An event on the registry’s event bus, for observers such as the dashboard and MCP server.
- Control
Command - A control action sent to a device: by a person, test or agent (scan, tap, close drawer), or by the app through a browser API that does not carry bytes (an NFC write).
- Device
Event - Something a device did, returned from every
Devicecall. - Direction
- Which way bytes travelled.
- Event
Kind - What happened, in an
EventRecord. - LogLevel
- Severity of a
LogEntry. - NfcEvent
- What an NFC reader reports, in
DeviceEvent::Nfc. - NfcRejection
- Why an NFC reader refused a write or a lock.
- Option
Default - The default value of a fault option.
- Peripheral
Signal - A signal on the wires between a device and its peripherals, modelled on the drawer-kick connector of receipt printers.
- Progress
- What
ScenarioRun::advancewants next. - Registry
Error - An error from a
Registryoperation. - Step
- One scenario step.
- Step
Status - How a step went.
- Terminator
- What a keyboard-wedge scanner sends after the code (PRD section 8.4).
Constants§
- DEFAULT_
BUS_ CAPACITY - Default number of undrained events kept on the bus.
- DEFAULT_
JOB_ CAPACITY - Default number of jobs kept per device.
- DEFAULT_
LOG_ CAPACITY - Default number of records kept per device.
- EXPECT_
POLL_ MS - How often an
expectwithwithinMsis checked again, in milliseconds. - MAX_
DEVICE_ ID_ LEN - Longest accepted
DeviceId, in bytes. - MAX_
NDEF_ BYTES - Most payload bytes in one NDEF message. The largest common tags (NTAG216) hold 888 bytes; this only stops runaway input.
- MAX_
NDEF_ RECORDS - Most records in one NDEF message. Real tags hold a handful; this only stops runaway input.
- MAX_
PERIPHERAL_ DEPTH - How many hops a chain of peripheral signals may take before it is dropped.
- MAX_
SCENARIO_ STEPS - Most steps a scenario may have.
Traits§
- Clock
- A source of time, injected into the
Registry(FR-05). - Device
- A device instance.
- Device
Plugin - A device type that can be instantiated from JSON config, e.g.
printer-escpos.
Functions§
- catalogue
- The built-in fault catalogue (PRD section 10), in table order.
- serial_
number - Parses an NFC card UID given as hex, with or without
:,-or space separators, and returns it in Web NFC’sserialNumberform: lowercase, colon-separated bytes. - validate_
ndef_ message - Checks a whole NDEF message: every record, the record count and the total size.
Type Aliases§
- Fault
Options - Options for a fault, as a JSON object (e.g.
{ "afterBytes": 64 }).