hardless-core 0.1.0

Sans-IO core of Hardless: device plugin interface, registry, event log, faults and clock.
Documentation

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