-
Notifications
You must be signed in to change notification settings - Fork 0
data broker
lib/DATA_BROKER/data_broker.h implements the one shared-state hub of the
firmware: a singleton DataBroker holding a Blackboard struct behind a
mutex, plus a per-topic dirty-flag word.
DataBroker::instance().update(Topic::LED, [](Blackboard& b) { b.led_mode = LedMode::MANUAL; });
Blackboard snap = DataBroker::instance().snapshot(); // full copy
LedMode mode = DataBroker::instance().read([](const Blackboard& b){ return b.led_mode; });
if (DataBroker::instance().consume(Topic::AUDIO)) { /* dirty — act on it */ }| API | Semantics |
|---|---|
update(topic, fn) |
Run fn(board) under the lock, mark topic dirty |
updateSilent(topic, fn) |
Run fn(board) under the lock, without marking topic dirty — for mirroring authoritative driver state back into the board (e.g. re-publishing the actual volume/playing/station after handling a command) rather than signaling a new command. Using update() here would re-dirty the topic every write, and a consumer that itself calls update() on the same topic while handling its own dirty flag (e.g. main.cpp's AudioCmd dispatch) would perpetually re-trigger its own consume() every loop iteration — starving genuine concurrent writers of the quiet window they need to not be clobbered |
snapshot() |
Copy of the whole board (lock held only for the copy) |
read(fn) |
Evaluate fn(const board&) under the lock, return its result |
consume(topic) |
Atomically test-and-clear the dirty bit — the "was there a new command?" primitive |
isDirty(topic) |
Non-destructive dirty check |
ACCEL, BATTERY, LDR, RTC_TIME, DANCE, LED, AUDIO, WIFI, POWER, GEO, EAR_FLASH
Topics serve two different styles of communication:
-
Telemetry (ACCEL, BATTERY, LDR, RTC_TIME, WIFI, GEO): published
periodically by
loop()/WebFetcher; consumers justsnapshot()whenever they render. Dirty bits are mostly ignored. -
Commands (LED, AUDIO, POWER, DANCE, EAR_FLASH): a producer (menu, quick
access, radio screen, dashboard, carousel timers, game element) writes a
command field and the dirty bit;
loop()consume()s it and drives the hardware, then writes the resulting state back.
sequenceDiagram
participant UI as Any UI (menu / radio / dashboard / quick access)
participant BB as DataBroker
participant L as loop() (core 1)
participant AM as AudioManager ("audio_mgr" task, core 0)
UI->>BB: update(AUDIO) { audio_cmd = PLAY, station_index = 3 }
L->>BB: consume(AUDIO) == true
L->>AM: playRadio(3) — blocking call, serialized via AudioManager::_guard
L->>BB: updateSilent(AUDIO) { audio_cmd = NONE, mirror index/name/volume/playing }
AM->>AM: "audio_mgr" task pumps the stream on its own cycle
The mirror-back write uses updateSilent, not update — a plain update()
there would re-dirty Topic::AUDIO and make this same dispatch block
re-trigger itself every subsequent loop() iteration (see main.cpp:314,
731, 1137).
audio_cmd values: NONE, PLAY, STOP, NEXT, PREV, RELOAD, VOLUME, PLAY_CLIP, PLAY_CLIP_FORCE.
power_cmd: NONE, SHUT_DOWN.
| Group | Fields | Producer → Consumer | Persistence |
|---|---|---|---|
| Accelerometer |
accel_x/y/z, accel_temp
|
loop 5 Hz → mood, dashboard | RAM |
| Battery |
battery_voltage (mV), battery_raw_adc, battery_charging, battery_standby, battery_adc_2s/1min/5min
|
loop 5 Hz → status bar, mood, diagnostics | RAM |
| Light |
ldr_ohms (EMA), ambient_dark (hysteresis 20 kΩ/30 kΩ) |
loop 5 Hz → mood | RAM |
| Time/alarm |
time, ntp_synced, ntp_last_sync_ago, alarm_enabled/hour/minute, alarm_ringing, alarm_led_effect (default 11=Matrix), alarm_dance_move (default 5=Twist), alarm_audio_enabled (default true), alarm_station_index, alarm_backup_sound (default 2=Calling) |
loop 1 Hz → time screen, carousel (alarm overlay); alarm-fire block plays effect/move/audio |
alarm_enabled/hour/minute live in the PCF8523; alarm_led_effect/dance_move/audio_enabled/station_index/backup_sound in NVS; alarm_ringing RAM-only |
| Motors |
motor_enabled (master gate), motor_pwm_multiplier (0.7–1.3 calibration scale, note the PWM output is inverted so <1.0 runs stronger), dance_move (0–14), motor_manual_control (dashboard teleop gate) |
menu/quick access/dashboard → loop |
motor_enabled and motor_pwm_multiplier in NVS; dance_move and motor_manual_control deliberately RAM-only (safety: nothing self-drives after a power cycle) |
| LEDs |
led_mode (LedMode::OFF/AUTO/MANUAL), led_effect (0 none, 1–21 animated, 22–26 saved slots), led_r/g/b, led_brightness, led_effect_brightness (30–100%, animated/saved-slot brightness only), led_eye_dim (0–100, extra knockdown for eye pixels during animated effects) |
menu/quick access/mood/dashboard → loop |
led_mode, led_effect_brightness, led_eye_dim in NVS; led_effect/led_r/g/b/led_brightness RAM-only |
| Audio |
audio_cmd, audio_station_index/name, audio_playing, audio_volume (0.4–1.0 driver scale), audio_track_title (ICY metadata), audio_clip_sound (AudioManager::Sound id for PLAY_CLIP/PLAY_CLIP_FORCE), mood_sounds_enabled (master gate for mood-triggered clips) |
UIs → loop → driver; driver state mirrored back via updateSilent
|
station/volume/playing debounced into NVS; mood_sounds_enabled in NVS |
| WiFi |
wifi_connected, wifi_ip, wifi_ssid, wifi_rssi
|
loop 5 Hz | RAM |
| Geo |
geo_valid, geo_lat/lon, city/country/cc/region/zip/timezone/offset |
WebFetcher (once per boot) → weather, time | weather cache blob in NVS |
| Power |
power_cmd, accel_wake_enabled, backlight_always_on, power_off_min + power_off_start
|
menu/carousel → loop | accel wake, backlight in NVS; power-off countdown RAM-only |
| Ear flash |
ear_flash_r/g/b, ear_flash_ms
|
GameElement (win/lose feedback) → loop/LedDriver, consumed once via db.consume()
|
RAM (one-shot request, not persistent state) |
led_effect shares one integer namespace between animated effects and saved
color slots so every selector UI (menu, quick access, dashboard) can cycle a
single range:
-
0— none (staticled_r/g/bcolor shows instead) -
1–21— animated effects (seeLedDriver, GUI & Carousel) -
LED_EFFECT_SLOT_BASE (22) … 26— saved color slots 1–5 (per-LED colors from NVS)
The broker mutex is held only inside update/updateSilent/read/snapshot/consume.
Two hard rules keep the system deadlock-free:
-
Never call a potentially blocking driver function from inside a broker
lambda. The canonical incident (originally against the now-dead
AudioWebDriver, same rule applies to today'sAudioManager): a driver accessor took the audio_guard, whose owner (the audio task) calls the metadata callback, which takes the broker lock. Calling such an accessor insidedb.update(...)produced an ABBA deadlock that froze the GUI, LEDs and shutdown. Hence: -
AudioManagerstate accessors are lock-free atomics on purpose. Fetch driver state before enteringdb.update(...)(see the comment block inmain.cpp's AUDIO consumer).
Other synchronization in the system, for context:
| Lock | Protects | Shared between |
|---|---|---|
DataBroker::_mtx |
blackboard | all tasks |
DriverBase::i2c_operations (one global) |
every I2C transaction | all tasks |
NvsStore::_mtx (recursive) |
NVS access | loop, httpd, carousel |
TimeManager::_mtx (recursive) |
time/NTP/TZ state | loop, httpd |
LedDriver::_stripMutex |
WS2812 buffer writes via set()
|
loop, httpd (live pixel edit) |
AudioManager::_guard (FreeRTOS sem) |
the shared radio/clip audio pipeline |
loop()'s AUDIO dispatch and the "audio_mgr" task |