Skip to main content

Crate hardless_core

Crate hardless_core 

Source
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.

PieceRequirementTypes
Device plugin interfaceFR-01DevicePlugin, Device, DeviceEvent, ControlCommand, Job
Device registry and event busFR-02Registry, BusEvent, DeviceSummary, RegistryError
Event logFR-03EventLog, EventRecord, EventKind
Faultssection 10FaultId, FaultOptions, FaultSpec, catalogue()
Scenario runnerFR-04Scenario, Step, ScenarioRun, ScenarioReport
Deterministic clockFR-05Clock, 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 Clock it was given; the server passes a system clock, tests pass FakeClock. 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 route ToHost bytes and Wedge scans 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. FaultId is a string so plugins can define their own faults; the IDs in PRD section 10 are provided as constants and in catalogue().

§Features

  • ts: derives TypeScript bindings with ts-rs. Used by cargo xtask gen-types, which writes packages/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§

ActiveFault
An active fault and its options.
Bytes
A cheaply cloneable and sliceable chunk of contiguous memory.
ConfigError
A device config was rejected by DevicePlugin::create.
DeviceId
The ID of a device instance, e.g. printer-1.
DeviceSummary
A device as listed by Registry::list (and GET /api/v1/devices).
EventLog
A bounded log that drops the oldest record when full.
EventRecord
One entry in a device’s event log.
FakeClock
A manually driven clock for tests. Clones share the same time.
FaultId
A fault identifier in kebab-case, e.g. paper-out.
FaultOptionSpec
One option a fault accepts.
FaultSpec
A catalogue entry: one row of PRD section 10.
InvalidId
Error returned when a string is not a valid DeviceId or FaultId.
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.
NdefRecord
One NDEF record, as Web NFC’s NDEFRecordInit.
NfcReading
A card read: the reading event of NDEFReader.
Registry
Holds plugins and device instances, routes input to devices, records everything in each device’s event log and publishes BusEvents.
RegistryConfig
Limits for a Registry.
Scenario
A scenario: steps run in order, stopping at the first that fails.
ScenarioReport
The result of a whole run (POST /scenarios/run).
ScenarioRun
A scenario being run.
StepResult
The result of one step.
WedgeScan
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-scan message.

Enums§

BusEvent
An event on the registry’s event bus, for observers such as the dashboard and MCP server.
ControlCommand
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).
DeviceEvent
Something a device did, returned from every Device call.
Direction
Which way bytes travelled.
EventKind
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.
OptionDefault
The default value of a fault option.
PeripheralSignal
A signal on the wires between a device and its peripherals, modelled on the drawer-kick connector of receipt printers.
Progress
What ScenarioRun::advance wants next.
RegistryError
An error from a Registry operation.
Step
One scenario step.
StepStatus
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 expect with withinMs is 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.
DevicePlugin
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’s serialNumber form: lowercase, colon-separated bytes.
validate_ndef_message
Checks a whole NDEF message: every record, the record count and the total size.

Type Aliases§

FaultOptions
Options for a fault, as a JSON object (e.g. { "afterBytes": 64 }).