Context
Today the two controllers (A/B) share a single physical sensor-bus pair — one intake OneWire
chain (ONE_WIRE_BUS_INTAKE=4) and one exhaust chain (ONE_WIRE_BUS_EXHAUST=21). Access is
arbitrated by RelayControl::BusOwner (NONE/A/B), which RoleManager claims/releases on
failover, so only one controller is electrically connected to the sensors at any moment. The two
firmware images differ only by a compile-time -DDEVICE_ID (A=65 / B=66).
Problem
The shared/switched bus is a single point of failure: a bad switch, stuck relay, or fault on the
shared harness takes out sensing for both controllers. It also couples "who can read sensors"
to "who transmits," and adds arbitration complexity (claim/release, settle delays) that exists
only because the bus is shared.
Proposal
Separate the two masters electrically. Each controller gets its own dedicated intake + exhaust
OneWire buses with direct, unswitched access to its own sensors — no SPDT handoff, no BusOwner
arbitration. Heartbeat failover then governs only telemetry transmit ownership, decoupled from
sensor access.
Decisions (resolved)
- Sensor placement: co-located redundant. A's sensor N and B's sensor N sit at the same
physical intake/exhaust point, so the two controllers read the same airflow points independently.
- SPDT/relay switches: fully removed from the BOM — the design has no switches.
- Firmware image: unified into one image; no compile-time A/B (
DEVICE_ID dropped). The host
assigns identity/role at runtime.
- Host-unreachable fallback: since identity now comes from the host, if the host is
unreachable we require a manual write to the DB (device_map MAC→rack_id/role) to identify/
provision a device out-of-band.
Tasks
Files affected
ESP32-Firmware/platformio.ini — collapse esp32_a/esp32_b (currently differ only by DEVICE_ID) into one image
ESP32-Firmware/include/DeviceConfig.h — remove DEVICE_ID #error guard, INTAKE_SWITCH_PIN/EXHAUST_SWITCH_PIN; per-controller OneWire pins
ESP32-Firmware/src/RoleManager.cpp + include/RoleManager.h — remove bus claim/release/settle logic
ESP32-Firmware/src/RelayControl.cpp + include/RelayControl.h — remove BusOwner sensor-bus switching
ESP32-Firmware/src/TemperatureBus.cpp + include/TemperatureBus.h — per-controller bus init/read
ESP32-Firmware/src/main.cpp — telemetry assembly (buses[] inlet/exhaust), failover gating, runtime identity
README.md — "Requirements: Sensor Bus Configuration" + wiring
Connecting_ESP32_Telemetry_to_Radxa.md, docs/mqtt-contract.md — if sensor payload buses / provisioning change
Acceptance criteria
- Wiring diagram/BOM shows two independent intake+exhaust bus pairs, sensors co-located per index, no switches.
- A single unified firmware image runs on both controllers; identity/role assigned by the host at runtime.
- Firmware reads each controller's own bus with no SPDT/
BusOwner arbitration.
- Documented manual-DB fallback exists for host-unreachable provisioning.
- README Sensor Bus Configuration requirements updated to match.
Relates to #11 (config schema), #12 (provisioned role), #24 (broker bootstrap).
Context
Today the two controllers (A/B) share a single physical sensor-bus pair — one intake OneWire
chain (
ONE_WIRE_BUS_INTAKE=4) and one exhaust chain (ONE_WIRE_BUS_EXHAUST=21). Access isarbitrated by
RelayControl::BusOwner(NONE/A/B), whichRoleManagerclaims/releases onfailover, so only one controller is electrically connected to the sensors at any moment. The two
firmware images differ only by a compile-time
-DDEVICE_ID(A=65 / B=66).Problem
The shared/switched bus is a single point of failure: a bad switch, stuck relay, or fault on the
shared harness takes out sensing for both controllers. It also couples "who can read sensors"
to "who transmits," and adds arbitration complexity (claim/release, settle delays) that exists
only because the bus is shared.
Proposal
Separate the two masters electrically. Each controller gets its own dedicated intake + exhaust
OneWire buses with direct, unswitched access to its own sensors — no SPDT handoff, no
BusOwnerarbitration. Heartbeat failover then governs only telemetry transmit ownership, decoupled from
sensor access.
Decisions (resolved)
physical intake/exhaust point, so the two controllers read the same airflow points independently.
DEVICE_IDdropped). The hostassigns identity/role at runtime.
unreachable we require a manual write to the DB (
device_mapMAC→rack_id/role) to identify/provision a device out-of-band.
Tasks
controller), sensors co-located per index; switches removed
-DDEVICE_IDfromplatformio.iniand theDeviceConfig.h#errorguard; removeINTAKE_SWITCH_PIN/EXHAUST_SWITCH_PIN; give each controller itsown intake+exhaust OneWire pins
RelayControlBusOwnerswitching and the claim/release + settle logic inRoleManager; each controller reads its ownTemperatureBusdirectlyof the same points) vs standby silent per current failover rule; update schema accordingly
(
device_map) so devices can be provisioned out-of-bandFiles affected
ESP32-Firmware/platformio.ini— collapseesp32_a/esp32_b(currently differ only byDEVICE_ID) into one imageESP32-Firmware/include/DeviceConfig.h— removeDEVICE_ID#errorguard,INTAKE_SWITCH_PIN/EXHAUST_SWITCH_PIN; per-controller OneWire pinsESP32-Firmware/src/RoleManager.cpp+include/RoleManager.h— remove bus claim/release/settle logicESP32-Firmware/src/RelayControl.cpp+include/RelayControl.h— removeBusOwnersensor-bus switchingESP32-Firmware/src/TemperatureBus.cpp+include/TemperatureBus.h— per-controller bus init/readESP32-Firmware/src/main.cpp— telemetry assembly (buses[]inlet/exhaust), failover gating, runtime identityREADME.md— "Requirements: Sensor Bus Configuration" + wiringConnecting_ESP32_Telemetry_to_Radxa.md,docs/mqtt-contract.md— if sensor payload buses / provisioning changeAcceptance criteria
BusOwnerarbitration.Relates to #11 (config schema), #12 (provisioned role), #24 (broker bootstrap).