6. ESP32 NODE NETWORK (Tier 0)
The controller is an ESP32-S3 in a DIN-rail industrial enclosure, running on 7-36V DC direct ("automotive") β wires to 12V truck power, no buck needed. ESPHome-ready. Tier 0 is the always-on, crash-proof I/O + safety layer; it never sleeps and survives anything the Linux boxes do (graceful degradation, Β§4.4).
rev 3.6 β FINALIZED: digital-output controller + remote serviceable relay panel. After surveying the field (Waveshare, KinCony KC868/Pi5-ARM, Erqos, Norvi, Olimex), the owner's serviceability instinct won: don't put soldered relays on the controller (a dead onboard relay = down a channel / board surgery). Instead the controller has digital outputs that TRIGGER an external automotive relay panel with socketed, swappable Bosch relays. A dead relay = a 10-second, $3 plug-in swap. The controller has nothing that switches a load, so nothing on it wears out. Option B DECIDED (rev 3.31, Β§6.5): TWO boards β a FRONT/cab board and a BED/camp board, Ethernet between them, each its own OTA ESPHome firmware (the bed is a real subsystem β combiner + tent + 3+ light sets + growth).
THE CONTROLLER β Waveshare ESP32-S3-POE-ETH-8DI-8DO, ALWAYS-ON (start with ONE):
β’ 8 DIGITAL OUTPUTS β Darlington transistor, optocoupler-isolated, 500mA sink,
built-in freewheeling diode β drive Bosch relay coils DIRECTLY (no extra flyback diode)
β’ 8 DIGITAL INPUTS β optocoupler isolated (door, ignition, reverse, dash button, sensors)
β’ Power: ALWAYS-ON 12V via 7-36V screw terminal (NOT PoE β PoE dies with the camera switch)
β’ Ethernet: W5500 (wired DATA link to the always-on switch; frees WiFi/BT radio for BLE proximity)
β’ Also onboard: RS485 + CAN (unused for now β see CAN note) ; ESP32-S3, ESPHome
β’ NO onboard relays β nothing on the controller to fail or wear
Jobs: fire DO triggers Β· read DIs Β· LVD + thermal cutoff (ADC) Β· proximity Β· send WOL/power control.
Publishes to / subscribes from the Pi 3 B+ MQTT broker (Pi holds the smart wake logic).
THE SWITCHING β external automotive relay/fuse panel (the swappable muscle):
β’ A relay/fuse block β rev 3.23 part: True MODS 60/80A 5-pin kits (interlocking sockets gang into
the panel; 87a left empty; observe 85/86 polarity if diode-suppressed) + a separate ATO fuse block
(the kit has no fuses). Bussmann RTMR remains the integrated alternative.
β’ Each DO output β one relay coil (pin 85); coil pin 86 β panel-local 12V; DO sinks coil to ground
β’ Carries the real load current (30-40A capable) + integrates the fuses
β’ SERVICEABLE: dead relay β pull + plug a $2-5 Bosch in, ~10 sec, no board touched
β’ Handles high-current/inrush/inductive loads (light bar 18A, compressor) β no separate relay needed;
the panel relays ARE the relays, sized per load
Trigger wiring: DOβpanel = 20-22 AWG, or owned SOLID Cat5 (24 AWG, one Cat5 = 8 triggers).
β οΈ Secure solid Cat5 every ~12in + strain-relieve both ends (solid fatigues at flex points
under vibration); use stranded only where it crosses something that moves (door jambs).
Load wiring (relayβload): sized to load β 16 AWG β€5A / 14 AWG long / 12 AWG 18A bar β FUSED.
WROOM-32: RETIRED to spare. No always-on CAN node is needed (wake = DI wires; CAN = OptiPlex, see Β§8).
SENSOR node: folded into the controller's spare DIs + ADC (cab temp, battery voltage) unless it strains it.
Wake word ("Hey JARVIS") is NOT on any ESP32. Porcupine runs on the OptiPlex 5080 once it is awake. Hardware wake (from hibernate) is handled by door/proximity events routed through Pi 3 B+ β WOL. This eliminates the need for a dedicated wake-word ESP32 node.
Programming β single unified ESPHome firmware. One chip, one YAML codebase. ESPHome handles all of it: DO triggers (output/switch), DI (binary_sensor), LVD (ADC + threshold + hysteresis), the on-road light interlock (reads CAN speed via the OptiPlex over MQTT, or a DI), BLE proximity, and the WOL/power-control magic packet (small UDP lambda). None needs hard-real-time. (You don't "keep" the factory firmware β ESPHome replaces the throwaway demo; the unerasable ROM bootloader makes re-flash/recovery foolproof. OTA updates push over the wired network from the OptiPlex at the 3am window β Β§5.4.)
ESP32-S3 "AI" = TinyML β tiny neural nets (KB-few MB) via ESP-DL / TF-Lite Micro / microWakeWord. Good for: wake word, simple sound/vibration classification, binary present/absent decisions. CANNOT run an LLM, analyze camera video, or do face/object recognition β LLM work is the OptiPlex's (β Fireworks for heavy reasoning); camera object-detection is deferred to the Pi 5 + Hailo-8L (~1yr out).
REJECTED controller options (rev 3.6 survey):
- Onboard-relay boards (Waveshare 8DI-8RO-C, KinCony A8v3/A16) β soldered relays = a dead one is board surgery / down a channel. Lost to the DO + swappable-panel approach.
- KC868-H32BS (32-ch, $260) β overkill + overpriced for ~12-14 loads.
- ARM boards (KinCony Pi5R/Pi5M, Raspberry Pi CM5) β powerful (full Linux, runs any program) but relays/safety belong on a crash-proof microcontroller, not a Linux box that can hang/corrupt. If ever used, only with an ESP32 watchdog + ESP32-held battery cutoff. Brain stays the owned Pi 3 B+.
- ESP32-S3-RS485-CAN gateway β WiFi-only, no Ethernet β fails the wired-first rule.
- MrDIY ESP32 CAN Shield ($56) / Waveshare Pi Zero relay HAT ($38) β redundant / too weak.
6.1 Manual overrides & RS485 Modbus IO expansion (rev 3.10)
Goal: add physical override switches (and any extra inputs) without burning the 8DI-8DO's 8 DI, which are already spoken for (door, ignition, reverse, dash button, cab-temp/batt-V).
The expansion path β RS485 Modbus RTU. The 8DI-8DO has an onboard RS485 port. Hang a Modbus RTU IO module on that 2-wire bus; the 8DI-8DO is the master and polls it (up to 247 devices on one bus). Each added module brings its own DI/DO β so a cab switch panel lives on the RS485 module, and the main board's 8 DI stay free.
- Module choice by environment:
- Isolated industrial Modbus IO module β for harsh / long / engine-bay runs (optoisolated, 12V-tolerant inputs, screw terminals).
- Cheap $4 TTL module (R4PIN08-class, jumper-selectable 8DI / 8DO / 4-4 / etc.) β fine for a short-run cab panel only. β οΈ Its inputs are 3.3/5V TTL β wire each switch as a dry contact to GROUND, never to 12V (12V destroys it). Non-isolated bare board β keep it in the cab, not under the hood.
Two manual-switch wiring patterns (different tradeoffs):
- DI-input switch (stays in sync). Switch β a DI (on the main board or the RS485 module). The ESP32 reads it and fires the output, so voice and the physical switch agree on state and either can toggle. β best when the ESP32 is alive. β does nothing if the ESP32 is dead.
- Parallel-at-the-coil bypass (fail-safe). A switch in parallel with the relay coil's trigger (pin 85 β ground), so flipping it energizes the relay regardless of the ESP32. β works even if the ESP32 is dead. β the ESP32 doesn't know you did it (state desyncs) β use only for true limp-home loads.
Dead-relay limp-home. Because switching is on the swappable Bosch panel (Β§6), a dead relay = a $3 plug-in swap. Carry a spare Bosch relay; for a no-spare situation, a gutted-relay bypass plug (dead relay shell with 30β87 jumpered, or a toggle across them) hard-closes that load by hand.
6.2 RS485 switch panel + stateful illuminated switch (rev 3.11)
Goal: a cab switch panel where each manual switch agrees with voice/app state, run home on ONE cable instead of a wire per switch. Three companion wiring sheets live in
projects/carputer/(and the dropzone): **control-wiring-optionA-modbus.pdf,control-wiring-optionB-dualboard.pdf,switch-led-stateful.pdf**.
The panel is a Modbus slave behind the switches, not wires home. RS485 is a transport (differential serial carrying Modbus RTU) β a bare switch can't speak it. Put a fixed-function Modbus RTU IO module at the panel; wire toggles to its terminals (short local runs); the 8DI-8DO polls it as master over one cable. A TTLβRS485 adapter (e.g. H1-HBB0150-10) or an industrial repeater/isolator are bus plumbing only β neither reads a switch; don't substitute them for the IO module.
- Confirmed part: Waveshare Modbus RTU IO 8CH (SKU 26244, ~$24). 8 isolated DI (PNP/NPN β accepts dry-contact OR switched-12V, both optoisolated β so no TTL "ground-only" landmine) + 8Γ DO (500mA Darlington open-drain, drives Bosch coils directly). 7β36V, DIN-rail, addr 1β255, cascadable, resettable fuse + TVS. This is the Β§6.1 module, now nailed down β prefer it over the $4 TTL board for anything past a trivial cab run.
- Optional sibling on the same bus: Modbus RTU Analog Input 8CH (~$25). 8Γ 12-bit 0β10V / 4β20mA β for air-tank PSI, tank levels, extra temps. Different Modbus address, same two wires. Future/optional (pack-V already comes from the WonVon shunt).
- One cable = ~4 conductors, not 2. The module needs power where it's mounted: one pair RS485 A/B, one pair 12V+GND β one run of owned solid Cat5 covers it, spare pairs for later. 120Ξ© termination at the bus ends for long/fast runs; short cab runs are forgiving. Daisy-chain multiple panels (front + bed) on the one bus, each its own address.
Stateful illuminated switch (the "button + voice agree, LED shows truth" pattern). State lives in the Waveshare as the single source of truth β so use a MOMENTARY pushbutton with an INDEPENDENT LED (4 wires: 2 contact + 2 LED). Not a latching switch β a mechanical latch would hold a position voice can't change and lie about state.
- Contact:
NO β DIx,COM β GND. Each press = a pulse β Waveshare toggles the load's state. - LED:
+12V β (series resistor if the LED isn't 12V-rated) β LED+,LEDβ β DOy; the DO sinks when state = ON so the lamp tracks state. The DI and the LED-driving DO both live on the panel module (short, local); the load relay's DO can be anywhere. - Result: button press or voice toggles the one state; that state drives both the load relay and the switch LED β so button and voice always agree and the LED never lies. ESPHome: momentary
binary_sensoron_press: switch.toggle; a template switch whose on/off actions set both DOs; voice/app call the same switch. Seeswitch-led-stateful.pdf.
Example part + 2-switch wiring: DMWD 12mm metal anti-vandal momentary, 4-pin, halo-ring LED, 12-24V, IP66 (~$2/ea in a 5-pack β buy the MOMENTARY, not latching, version). Its 12-24V LED has a built-in resistor β LED+ β 12V, LEDβ β DO, no external resistor; verify pin polarity with a meter (markings are tiny). Contact is only 2A-rated but that's irrelevant β it carries signal to a DI, never load current. Per switch: NOβDIx, COMβGND, LED+β12V rail, LEDββDOx; share the 12V + GND rails; panel jumpers 22-24 AWG (Cat5 strands fine, mA only). Debounce in ESPHome. See switch-panel-2sw-dmwd.pdf (2Γ = Rock + Fog).
Gesture commands (momentary + software = free). Because the button is momentary and state is in software, ESPHome can layer behaviors: short press = toggle (on_click); press-and-hold = on for 10 min then auto-off (hold β script + delay); multi-click = extra actions (on_multi_click, e.g. 4-press = rock + fog together). β οΈ Color-change on 2-press is NOT possible with the DMWD (single fixed-color LED) β that needs an RGB-ring switch (separate R/G/B = 3 DO per switch; on/off = 7 colors, or PWM on the Waveshare's native pins for full mixing). One placement nuance: multi-click timing is sharper on the Waveshare's NATIVE DI (instant edge) than on the polled Modbus module (~100-300ms poll blurs fast clicks) β so put gesture switches on a native DI, simple on/off overrides on the module.
β DECIDED (rev 3.31): OPTION B β two clusters, by LOCATION. Two Waveshare ESP32-S3-POE-ETH-8DI-8DO boards (~$43 each): a FRONT/cab board and a BED board, talking over one Ethernet (Cat5e) run. Picked because the bed is a real subsystem, not "a battery" β rooftop tent on racks, 3+ rack light sets, tent lights, a tailgate hangout zone, and unknown future additions. A local bed board gives short local wiring, local camp control (a tailgate switch panel wired right to it), keeps the safety-critical combiner control loop local (no truck-length bus in it), keeps the bed working even if the cabβbed cable is cut, and leaves spare DO/DI + an RS485 port for "the more things later." Firmware is a non-issue: each board runs its own small ESPHome YAML, OTA over the network (flash once via USB, all later updates push wirelessly); you only touch firmware to add physical hardware or change core logic β scenes/schedules/automations that combine existing outputs live in the Flask/PWA layer (no reflash). See Β§6.5 for the cluster split + bed board I/O map.
Combiner timing (still relevant): the sequence is second-scale (~15s ΞV converge, 0.5s step gaps) and the 125A fuse + BMS are the fast protection, not the ESP32. With Option B the combiner runs locally on the bed board (native DO β RLY-O5/RLY-PC coils, bed-V via the DC Monitor on the bed board's RS485) β so the control loop never crosses the truck-length link at all.
6.3 ESP32 reboot behavior & firmware boot rules (rev 3.22)
During any reboot (watchdog, brownout, crash, and EVERY OTA update) the DO opto-drivers go dark β every board-driven relay drops. What that means per subsystem:
- Combiner: fail-safe + self-healing. Contactor + pre-charge open β banks isolate. Mid-COMBINED, the contactor breaks under load (it's rated for that); the front bank carries everything via the trunk for the ~5-15s boot, then the state machine re-reads the ADCs (ΞVβ0 β band 1) and recombines. (Sheet 6.)
- Modbus-module loads (lights): unaffected β the module is independently powered and holds its DO states while the master reboots; lights don't blink. Check/configure its comms-watchdog (if present) deliberately.
- Pi / Switch A / Cudy: unaffected (direct fused feeds, not relay-gated).
- β οΈ O1 = the real casualty: the converter relay dropping is a hard power cut to a RUNNING OptiPlex (fs-corruption risk β the Β§5.4 deep-storage cut is safe only because hibernation happens first). Latching is the fix, but it can't go in the Bosch panel β a latching relay needs set + reset coils, more terminals than the 5-pin ISO socket has, so there's no "latching Bosch relay" to drop in. Two real paths:
- (A, recommended) Standalone 12V 10A MAGNETIC latching relay module β "ZERO POWER HOLD" β outside the panel. The key spec phrase when shopping is literally "magnetic latching β¦ zero power hold": it's a true magnetic-latching relay (mechanical hold, zero standby current). β οΈ Do NOT grab the cheap "touch / MCU / push-to-start" toggle modules that flood the search β those have an onboard chip drawing continuous standby current (defeats 0W) and a single toggle input. β₯10A contacts (O1 peak ~8A); avoid 1-2A bare latching relays (HFD2/MEC β too small). Drive: ideally a board with SET + RESET inputs (two DOs force known states). If it's a single-toggle board (common), it still works but add a read-back (below). CONFIRMED part: Electronics-Salon MD-D262T/12V (single relay, DIN housing; internal = TE/Schrack RT424F12 2-coil bistable). Control terminals V+ / S / R, both active-LOW β a perfect match for the open-drain DOs:
V+β 12V Β·Sβ DO_a (pulse low ~300ms = ON) Β·Rβ DO_b (pulse low ~300ms = CUT)- Switch X (the muscle):
Cxβ 12V,Sxβ converter 12V (SET=on, RESET=cut) - Switch Y (free read-back):
Cyβ 12V,Syβ DI7 (true mechanical state β more reliable than sensing 19V; a contact can't mislead) - Firmware: ~300ms pulse (datasheet min 30ms); NEVER pull S and R low at the same time (hard rule);
restore_mode: ALWAYS_OFFon both DOs + read DI7 on boot to learn state (don't pulse on boot). - Specs that fit: ~50mA for ~300ms per change, then ZERO (true 0W hold); 15A making (converter inrush OK); β40-85Β°C; AgNi contacts. Holds through reboot/OTA; dead-ESP32 leaves the 5080 running (itself an alarm).
- (B, no special part) Wire a standard panel relay NC (normally-closed): power flows when the coil is unpowered β a reboot keeps the 5080 ON. Tradeoff: cutting for deep storage needs the coil held energized (~1W) β not true 0W (pair with the rotary/manual cut for long storage). NC = reboot-safe but ~1W; module = both.
- $0 interim regardless: only OTA the ESP32 while the OptiPlex is hibernated.
Firmware boot rules (must be in the YAML):
restore_mode: ALWAYS_OFFon O5/O6 (and O3) β never restore relay state from flash; the battery may have been swapped while powered off. Re-derive from ADCs every boot.- Force-merge confirmation does not persist across reboot β fresh confirm required.
- ~10s boot-settle delay before energizing anything (also prevents relay-chatter in brownout reboot loops); never energize O5 until the ADCs are read and banded. O3 additionally gets min/max-pulse-width guards so a boot glitch can't fake a power-button press.
6.4 Waveshare module reference β solutions on tap (rev 3.30)
The point: because the build already runs an RS485 Modbus bus off the controller, almost any future sense/control need is a $20-27 DIN-rail module on the same two wires β give it a Modbus address and poll it. All are 7-36V, isolated (optocoupler + TVS + resettable fuse), rail-mount, cascade up to 247. Keep this table as the "what solves X" lookup.
| Need | Module (Waveshare) | SKU | $ | Notes |
|---|---|---|---|---|
| Switches + LEDs (dash panel) | Modbus RTU IO 8CH | 26244 | 24 | 8 isolated DI (PNP/NPN, dry-or-12V) + 8Γ 500mA DO. Selected for the dash panel (Β§6.1/Β§6.2). |
| Bank voltage sense (combiner ΞV) | Modbus RTU DC Monitor | 33931 | 27 | 4-ch 16-bit 0-36V; current Β±8A only (not main banks β keep the shunt); needs common GND. Selected (Β§5.9). |
| Analog sensors (PSI/level/temp) | Modbus RTU Analog Input 8CH | 25821 | 25 | 12-bit 0-10V / 4-20mA (NOT >10V β divider or DC Monitor for battery V). 4-20mA air-tank PSI, tank floats, temp probes. |
| Dimmable / RGB LED | Modbus RTU PWM Output 4CH | 33921 | 22 | 1Hz-200kHz PWM signal (<30mA) β drive a MOSFET gate for the power. Only if dimming is revived (was dropped rev 3.6). |
| 0-10V driver dimming | Modbus RTU Analog Output 8CH | 26419 | 20 | 0-10V / 4-20mA out β for LED drivers / fan controllers that take a 0-10V dim input. |
| More relays over the bus | Modbus RTU Relay 8CH / Latching (C) | 25739/30460 | 29/39 | If you want bus-driven relays somewhere instead of the Bosch panel (latching = holds on power loss). |
| Always-on LAN switch | Industrial 5-port Gigabit switch (DIN) | β | 25 | Candidate for "Switch A" (the always-on LAN) β DIN-mount matches the rail. |
| OptiPlex USB expansion | Industrial USB hub (metal/DIN, 4-port) | β | 15-18 | The powered hub for Β§4.8 (CAN/mic/phone stay direct). |
| GPS (24/7) | L76K GPS HAT / module | β | 13-23 | Put on the always-on Pi (not OptiPlex USB) β time + geo even while the 5080 sleeps. |
| Parked CAN watch | 2-CH CAN HAT for Pi | 17912 | 22 | Only if you later want CAN while parked (default = CAN on OptiPlex when awake, Β§8). |
| Split/isolate the RS485 bus | Isolated RS485 Hub (4-port) | 32247 | 18 | Branch the bus (cab vs bed) with isolation + signal relay if it grows. |
| OptiPlex talks Modbus direct | USB-TO-RS485 (C) | 34620 | 12 | Lets the 5080 poll the Modbus modules itself when awake (bypass the ESP32 master). |
| Small DC load current | (DC Monitor, above) | 33931 | β | Β±8A bidirectional, 0.8mA res β for a <8A circuit, not the banks. |
6.5 Tier-0 architecture β TWO CLUSTERS (Option B, rev 3.31)
Two Waveshare ESP32-S3-POE-ETH-8DI-8DO boards (~$43 ea), split by LOCATION, talking over one always-on Ethernet (Cat5e) run. Each is 12V-fed (NOT PoE β must survive the camera switch sleeping), each runs its own small ESPHome firmware (OTA over the network), each publishes to / subscribes from the Pi 3 B+ MQTT broker.
FRONT board (cab) βββββββββββββββ βββββββββββββββ BED board (bed/camp)
OptiPlex power (O1 latching), β Ethernet β COMBINER (contactor + pre-charge),
accessory cut, camera/PoE gate, β (Cat5e, MQTT) β 3+ rack light sets, tent lights,
front lighting, wake/sleep, βββββ Switch A βββββ€ tailgate camp switch panel,
dash switch panel (Modbus IO) β (always-on) β DC Monitor (bed-V) on local RS485,
+ DC Monitor / sensors on its β β + SPARE DO/DI + RS485 for growth
own short RS485 β β Powered from the BED BUS (live via trunk)
Why two (the decision): the bed is a genuine second subsystem (rooftop tent, racks, 3+ light sets, hangout zone, unknown future loads), not "a removable battery." A local bed board = short local wiring, local camp control, the safety-critical combiner loop stays local (no truck-length bus in it), the bed keeps working if the cabβbed link is cut, and spare I/O for later. The old "WROOM-32 = the 2nd board reserve" idea is retired β the 2nd board is a 2nd Waveshare in the bed; the WROOM-32 stays a bench spare.
BED board I/O map (starting point β adjust as the camp zone fills):
- DO1 β RLY-O5 (Bosch) β contactor coil Β· DO2 β RLY-PC β pre-charge Β· DO3-5 β 3 tent/camp rack light sets (rearward, each β a relay) Β· DO6 β tent lights Β· DO7-8 β SPARE (fridge / fan / pump / awning / lighting-scene loads later)
- DI1 β combiner override Β· DI2 β bed-V read-back if used Β· DI3-8 β camp switches / spare (or run the camp switches on a tailgate Modbus IO 8CH so the DI stay free + the switch LEDs get DO β the Β§6.2 stateful-switch pattern, in the bed)
- RS485 β DC Monitor (bed bank V) + future bed Modbus modules Β· Ethernet β Switch A (front) Β· CAN β spare (inverter telemetry later?) Β· Power β bed bus, 7-36V terminal
- Combiner runs its full state machine locally here (Β§5.9 / sheet 6); fail-safe NO contactor + the same reboot rules (Β§6.3).
FRONT board: OptiPlex power, accessories, camera/PoE gate, wake logic, the dash Modbus IO panel, and the KC front-bumper light rack (forward-facing β on-road interlock Β§5.7 #4 + diode-OR manual KC switch fail-safe) + any front rock/underbody. It no longer carries the combiner (that moved to the bed). (The bed board's "3 rack light sets" = the rearward TENT/CAMP lights, not the KC front rack.)
β οΈ Wiring sheets need a B-revision: the
full-wiring-*sheets currently show one front controller driving the combiner (sheet 6). They need redrawing to move the combiner + bed lights onto the bed board. Flagged as a to-do.