Machine on ESP32-S3
Status: v0.1 — review answers folded in, 2026-07-31. Deliberately short — structure over detail. Nothing implemented yet.
Goal
Deploy the pstop_c machine role on the same ESP32-S3 platform as the
remote: a self-contained box that bonds remotes, runs the safety decision
dual-core, and actuates the robot's stop circuit through relays. The pure
software machine (host/machine_app_runner) remains fully supported — same
library, same wire protocol, same config semantics; this is a second
machine form factor, not a replacement.
Reused from the remote (unchanged infrastructure)
- Hardware: Waveshare ESP32-S3-ETH (PoE), same enclosure family.
- Connectivity: microlink (Tailscale/WireGuard/DERP), eth → wifi
supervised uplinks. Own tailnet identity derived from eFuse MAC, prefix
distinguishes the role:
machn-01<mac24>(remotes keeppstop-). - Platform services: NVS-persisted settings + admin UI, boot-count
safety ladder, direct OTA (
/admin/api/ota),/state.jsontelemetry (withfw_ver/fw_sha), reset-history, panic log. - pstop_c untouched, as always: the machine role is what the library already provides; everything below is the shell around it.
Dual-core RX lockstep (mirror of the remote's TX lockstep)
The remote's pattern: two cores independently encode, byte-compare, transmit only on agreement. The machine inverts it:
- Each core runs its own pstop_c machine instance (own state, own remotes table) and independently decodes + validates every received datagram and computes (verdict, reply bytes).
- A comparator (same 100 ms tick discipline as the remote) compares the two cores' verdicts AND reply bytes. Agree → actuate relays to the agreed state, send the reply. Disagree → treat as internal fault: both relay outputs commanded open (STOP), reply withheld (the remote sees silence and stops, exactly like a remote-side mismatch), mismatch counter incremented (purple-equivalent indication, persistent-only per the current ring policy).
- Liveness (
check_heartbeats) runs per core on the shared monotonic clock; a timeout verdict must also agree before it opens the relays — with the caveat that either core alone opening its relay already stops the robot (see below), so disagreement is never less safe.
Relay output + feedback (replaces the remote's button inputs)
[DESCOPED 2026-08 — feedback OFF by default, REVERSIBLE.] The relay feedback / read-back described in this section is compiled out by default via the Kconfig option
CONFIG_MACHN_RELAY_FEEDBACK(defaultn), pending a new machine hardware revision whose wiring makes the divider voltages unreadable. The relay drive (below) and the series de-energize-to-safe stop are unchanged. Set the Kconfig toyto restore feedback. Full detail + safety-case impact: docs/design/relay_feedback_descope.md.
Two opto-isolated relays, contacts wired in series in the robot's stop circuit — either core alone can break the circuit:
- Core 0 drives relay A, core 1 drives relay B (one GPIO each through opto-isolated drivers, reusing the remote's E-stop loop pins). Energized = circuit closed = robot may run; de-energized = STOP. Power loss, brownout, reset, or a hung core all fail to STOP.
- Feedback = output-voltage sensing: a resistor divider on each
relay's switched output, read as a digital input by BOTH cores. After
any commanded transition the observed voltage must match within
RELAY_FEEDBACK_MS(TBD from relay switching time) and is also checked continuously at every comparator tick. - Fault semantics — nothing latches. Transient conditions (a momentary core disagreement, a momentary loss of remote signal) already self-recover exactly as on the remote: the comparator withholds/opens for the duration and resumes when agreement returns. A feedback contradiction (e.g. output still live after commanding open — welded contact / shorted driver) raises a persistent relay-fault indication for as long as the condition is observed: telemetry flag, fault counter, and the machine keeps both outputs commanded open. Safety does not depend on latching — the series partner relay breaks the circuit — but the indication persists so a single-channel box gets serviced; it clears by itself when feedback returns to sanity.
Reporting to the ROS2 computer
The ESP32 machine replaces the runner's decision role but the robot computer still needs live state. Two channels, both read-only:
/state.json gains machine-role fields: robot verdict, restart
state, arming owner, per-remote table (bonded/heartbeat age), relay
commanded/observed states, mismatch + fault counters. A thin ROS2
node (successor of the archived protective_stop_node) polls it at
5 Hz and republishes as topics — robot_status, relay_state,
fault. No new firmware surface; no UDP stream.
The stop circuit itself is the relay chain — the ROS2 reporting path carries no safety responsibility.
Fleet / OTA policy (config-gated, per decision)
- The machine runs the same fleet check-in code but registers with
device_type: "machine"in the check-in payload (new field, remotes implicitly"remote"). - Server-side enforcement (required, management-console work): the console
must refuse to assign remote firmware to
device_type != remote— machines get their own image lineage or nothing. This needs a handoff item to the console session before any machine unit ships. - Belt-and-braces on the device: machine builds set
auto_updatedefault off, and the OTA-apply path verifies the incoming image's project name (machn_machinevspstop_remotein the app descriptor) and rejects a mismatch regardless of what the server says. - Direct OTA (
/admin/api/ota) remains the recovery path, same as today.
Config
The machine.toml knobs move to NVS + admin UI (same pattern as the
remote's settings): admission allow/denylist (/api/admission, optional —
who may bond; re-arm authority is each remote's own announced role),
max_missed_heartbeats, min_stop_ms
(→ library delay_between_stop_ms), machine_device_id, listen port.
Persisted, live-applied where safe.
Out of scope for v1
Certification argumentation; multi-machine coordination (remote side already handles up to 4 machines — an ESP32 machine is just one of them); LED ring on the machine box (onboard RGB only for v1, TBD); console MACHINES-tab integration beyond the existing announce.
Resolved in review (2026-07-31)
- Relay: opto-isolated module; feedback via resistor divider on the
switched output read as digital IO. Exact part TBD during hardware
pass;
RELAY_FEEDBACK_MSset from its switching time. No fault latching: indication onlyRevised 2026-07-31 after bench test (disconnected feedback looked fully green): a feedback contradiction persisting ≥ ~1 s now stops the robot — both relays open, replies STOP, re-arming refused while the fault is present. Still no latch: clears with the fault; re-arm requires a fresh remote gesture. Named telemetry:relay_fault_a/b(consecutive contradiction ticks) +relay_stopin/state.json; commanded/ observed ine_hi*/e_lo*.- ROS2 reporting: poll
/state.jsonat 5 Hz; no UDP stream. - Arming: remote gestures only — the machine box has no arm input.
- Naming: remotes keep
pstop; machines usemachn.