Loops and devices Prototype
A game tick, a simulation step and a firmware loop are not requests. Clamp composes them with the same modules and capabilities as a web service, and a loop process pulls in no HTTP, database or async runtime.
Status. Phase 8 is complete and released: examples 11 and 12 on main. The loop and device are proofs with fake hardware; no embedded target is claimed yet.
A loop that replays exactly#
Simulation::step owns update order — position, input, random sample — and uses integer units (millimetres, milli-Celsius), so the same seed and the same inputs give the same state on every run. The tests run the fixture twice and compare every state. No promise is made about cross-platform floating-point replay; a float model needs its own, explicitly chosen determinism boundary.
let mut random = Seeded::new(42);
for delta in [1, -1, 3] {
Simulation::step(&mut state, Input { delta }, &mut random);
println!(
"step={} position={}mm velocity={}mm/step",
state.steps, state.position_mm, state.velocity_mm_per_step
);
}
The loop under the runtime#
LoopRuntime consumes explicit input ticks inside Runtime's task context. It checks cancellation between ticks, advances only the controlled clock, and stops when its owner says so. Simulation state stays borrowed from the caller — the runtime drives the loop, it does not own your world.
A two-process blueprint keeps the synchronous simulation projection apart from a service projection. The optional tokio-service feature runs a cancellable async service; the deterministic path and the default graph never enable Tokio.
A fake device#
- Replaceable contracts. Sensor, display, clock and hardware I/O are capabilities, so a test fixture and a real board are two providers of the same thing.
- Bounded input. The sensor queue holds at most eight samples. Raw readings are validated at ingestion; missing and stale samples and disconnection are reported, not guessed.
- Clean shutdown. Shutdown clears managed input and rejects later ticks. Telemetry is an optional effect, not a requirement.
CAN decoders as contributions#
Modules contribute decoders the same way they contribute HTTP routes or worker handlers. The CAN target compiles them into a sorted DecoderTable and rejects duplicate frame IDs. Unknown IDs, malformed payload sizes and invalid sensor values are target-local errors — CAN identifiers, units and freshness rules never leak into Kernel.
no_std consumers#
Example 12 is a #![no_std] library that uses alloc::Vec and one Core module identity, with no Kernel or Runtime. It proves consumer-side no_std + alloc source compatibility. It does not prove Core builds on bare metal: Core's Clock still exposes std::time::SystemTime, and that is the portability boundary to cross next.
What it costs#
| Measure | Result |
|---|---|
| Direct Rust loop | 0.627 ns/step |
| Runtime-driven loop | 5.661 ns/step — cancellation check, clock advance, iterator boundary |
| Composed binary | 669,872 B vs 446,760 B for direct Rust |
| Default dependencies | Core, Kernel, Runtime; Tokio only with tokio-service |
MQTT was deliberately left out: with a fake device and no broker-connected use case, transport code would not strengthen the evidence. Full tables on Evidence.