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

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.

Two manual-switch wiring patterns (different tradeoffs):

  1. 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.
  2. 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.

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.

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:

Firmware boot rules (must be in the YAML):

  1. restore_mode: ALWAYS_OFF on 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.
  2. Force-merge confirmation does not persist across reboot β€” fresh confirm required.
  3. ~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.

NeedModule (Waveshare)SKU$Notes
Switches + LEDs (dash panel)Modbus RTU IO 8CH26244248 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 Monitor33931274-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 8CH258212512-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 LEDModbus RTU PWM Output 4CH33921221Hz-200kHz PWM signal (<30mA) β†’ drive a MOSFET gate for the power. Only if dimming is revived (was dropped rev 3.6).
0-10V driver dimmingModbus RTU Analog Output 8CH26419200-10V / 4-20mA out β€” for LED drivers / fan controllers that take a 0-10V dim input.
More relays over the busModbus RTU Relay 8CH / Latching (C)25739/3046029/39If you want bus-driven relays somewhere instead of the Bosch panel (latching = holds on power loss).
Always-on LAN switchIndustrial 5-port Gigabit switch (DIN)β€”25Candidate for "Switch A" (the always-on LAN) β€” DIN-mount matches the rail.
OptiPlex USB expansionIndustrial USB hub (metal/DIN, 4-port)β€”15-18The powered hub for Β§4.8 (CAN/mic/phone stay direct).
GPS (24/7)L76K GPS HAT / moduleβ€”13-23Put on the always-on Pi (not OptiPlex USB) β†’ time + geo even while the 5080 sleeps.
Parked CAN watch2-CH CAN HAT for Pi1791222Only if you later want CAN while parked (default = CAN on OptiPlex when awake, Β§8).
Split/isolate the RS485 busIsolated RS485 Hub (4-port)3224718Branch the bus (cab vs bed) with isolation + signal relay if it grows.
OptiPlex talks Modbus directUSB-TO-RS485 (C)3462012Lets 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):

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.


β€Ή 5. POWER ARCHITECTURE (the keystone)7. CAMERA SYSTEM + NVR β€Ί