Context
The whole point of the dynamic config work is that the Radxa assigns each ESP32 its role. The firmware now receives and stores the role (RuntimeConfig::role, with isPrimary()/isStandby() helpers ready), but nothing reads it: sender selection in main.cpp still runs on compile-time DEVICE_ID == 'A'/'B' plus heartbeat state. grep shows isPrimary/isStandby/config_.role are used nowhere outside RuntimeConfig itself.
So today two firmware images (esp32_a / esp32_b env) still decide who transmits, and the provisioned role is decoration.
Scope
Drive the active-sender decision from the provisioned role instead of DEVICE_ID.
Implementation notes
main.cpp needs access to the applied config — likely a getter on TelemetrySender or moving the role check into the sender.
- Define behavior before config arrives: an unconfigured device sends nothing anyway (telemetry is gated on
configured && enabled), so the pre-config state is already safe.
- Keep the heartbeat failover logic intact — role sets the default (Primary sends, Standby quiet), heartbeat still handles takeover when the primary dies.
- Decide what happens if both devices in a rack are provisioned with the same role (host bug) — probably log loudly and fall back to standby.
- Long term this could collapse the esp32_a/esp32_b build environments into one image.
Acceptance criteria
- A device provisioned as
Primary sends telemetry; Standby stays quiet while the primary is healthy.
- Failover still works: standby takes over on heartbeat loss and yields on recovery.
DEVICE_ID no longer decides who transmits.
Context
The whole point of the dynamic config work is that the Radxa assigns each ESP32 its role. The firmware now receives and stores the role (
RuntimeConfig::role, withisPrimary()/isStandby()helpers ready), but nothing reads it: sender selection inmain.cppstill runs on compile-timeDEVICE_ID == 'A'/'B'plus heartbeat state.grepshowsisPrimary/isStandby/config_.roleare used nowhere outsideRuntimeConfigitself.So today two firmware images (esp32_a / esp32_b env) still decide who transmits, and the provisioned role is decoration.
Scope
Drive the active-sender decision from the provisioned role instead of
DEVICE_ID.Implementation notes
main.cppneeds access to the applied config — likely a getter onTelemetrySenderor moving the role check into the sender.configured && enabled), so the pre-config state is already safe.Acceptance criteria
Primarysends telemetry;Standbystays quiet while the primary is healthy.DEVICE_IDno longer decides who transmits.