Skip to main content

Events — Status, Heartbeat, Log

:::info Status Implemented. Source: docs/user-device-mqtt-contract.md §5–§6, §8a, docs/transmitter-mqtt-contract.md §4–§5, §9. :::

Device → backend traffic, published without waiting for a command.

Status — lifecycle state

Published retained, QoS 1, on every state transition, and once immediately after every (re)connect — even if the state didn't change while disconnected.

Transmitter status

{
"v": "1.0",
"status": "PLAYING",
"prev": "ONLINE",
"reason": "azan_trigger",
"fw": "1.4.2",
"mac": "A0:B7:65:12:34:56",
"ssid": "Masjid Guest WiFi",
"ip": "192.168.1.50",
"rssi": -61,
"temp_c": 41.2,
"uptime_s": 86340,
"config_version": 7,
"detail": {"stream_url": "https://…/{mosqueId}_azaan", "position_s": 42},
"ts": 1789000000
}
StatusMeaningdetail carries
UNKNOWNNever used by firmware — backend-only default before first contact
CONNECTINGBooted / reconnecting Wi-Fi or stream not yet openattempt count
ONLINEIdle, ready to play
PLAYINGActively outputting audiostream_url or source:"local", position_s
UPDATINGOTA firmware update in progressprogress_pct, target_fw
STREAM_ERRORTried to play, stream unreachable/failed ≥3 retriesstream_url, error
CONFIG_ERRORReceived config it cannot applyoffending keys, error
OFFLINEOnly broker LWT or graceful shutdownreason: lwt / shutdown / reboot
boot → CONNECTING → ONLINE ⇄ PLAYING
│ │
│ ├→ STREAM_ERROR → (retry) → PLAYING | ONLINE
│ └→ UPDATING → (reboot) → CONNECTING
└→ CONFIG_ERROR → (new config) → ONLINE
any state → OFFLINE (LWT / shutdown)

Stream player status

Same shape, plus two states unique to a member-owned, mosque-mutable unit:

{
"v": "1.0",
"status": "PLAYING",
"prev": "ONLINE",
"reason": "azan_cmd",
"fw": "1.0.4",
"mac": "A0:B7:65:AA:BB:CC",
"ssid": "Home WiFi",
"ip": "192.168.1.71",
"rssi": -68,
"uptime_s": 86340,
"config_version": 3,
"detail": {"stream_url": "https://…/{mosqueId}_azaan", "position_s": 42},
"ts": 1789000000
}
StatusMeaningdetail carries
UNBOUNDConnected and healthy, but not entitled to any mosque's azaan — awaiting approval, membership lapsed, no mosque bound, or the entitlement lease expired. Bluetooth works normally.reason: awaiting_approval / membership_lapsed / no_mosque / lease_expired
MUTEDAuthorized and healthy, but silenced by the user — quiet hours, manual mute, or a disabled prayerreason
PLAYING...source: stream / local / bluetooth; bluetooth carries no content metadata
boot → CONNECTING → UNBOUND ⇄ ONLINE ⇄ PLAYING
▲ │ │
│ │ ├→ STREAM_ERROR → (retry) → PLAYING | ONLINE
config arrives with │ │ └→ UPDATING → (reboot) → CONNECTING
no stream_url, or │ ├→ CONFIG_ERROR → (new config) → ONLINE
unbind_mosque cmd ──┘ └→ MUTED ⇄ ONLINE (user mute / quiet hours)

any state → OFFLINE (LWT / shutdown)

UNBOUND and MUTED both matter for delivery analytics: a unit that deliberately doesn't play is a correct outcome, not a fault — see Broadcast Audit for how refused acks and these states are counted separately from real failures.

Full concept detail: Device Lifecycle and Playback Entitlement (the entitlement lease behind lease_expired).

MQTT down, audio still playing

Both contracts are explicit: an MQTT disconnect must never stop audio. A device keeps playing and keeps retrying the broker; the broker's Last Will fires OFFLINE, the panel shows "OFFLINE (was PLAYING)" until the device reconnects and republishes true state. A stream player additionally bounds this autonomy with the entitlement lease — see Playback Entitlement.

Heartbeat — telemetry

Not retained, QoS 0. First heartbeat immediately after connect, then every heartbeat_secs (re-read each cycle so config changes apply without a reconnect).

Transmitter (default 60s, min 15s):

{
"v": "1.0",
"status": "ONLINE",
"rssi": -61,
"ip": "192.168.1.50",
"uptime_s": 86400,
"free_heap": 148000,
"volume": 80,
"config_version": 7,
"ts": 1789000060
}

Stream player (default 300s, min 60s — a looser cadence deliberately, so a fleet of thousands of home units doesn't dominate the broker's message budget for responsiveness nobody needs):

{
"v": "1.0",
"status": "ONLINE",
"rssi": -68,
"ip": "192.168.1.71",
"uptime_s": 86400,
"free_heap": 151000,
"volume": 65,
"config_version": 3,
"ts": 1789000300
}

status is duplicated in heartbeat so a lost retained status self-heals. The backend marks a device OFFLINE (stale) after 3 missed heartbeats even without the LWT firing.

Log — optional diagnostics

Enabled via config log_enabled: true (transmitter) or a temporary set_log_level command (stream player, self-expiring). Throttled ≤1 msg/s, ≤512 B each; drop rather than queue when offline.

{"v":"1.0","level":"warn","msg":"stream reconnect #2","ts":1789000200}

Contains no audio content and, for a stream player, nothing about what a member plays over Bluetooth beyond whether the radio is enabled.

Remote diagnostics (stream player)

A stream player lives in a member's house — nobody will visit it. Everything needed to diagnose a fault must be obtainable over the wire, and must be answerable while UNBOUND:

NeedMechanism
Current truth on demandget_state → full status incl. capabilities, config_schema, hw_rev
Reachable at allping with round-trip latency
What it has been doingmosque_device_events timeline
Why it is not playingGET /devices/{id}/whyeffective_playback_state + deciding field
Signal qualityrssi + ssid in heartbeat
Audio path aliveplay_test — short local tone, no stream, no azaan
Which physical boxidentify — LED blink / chirp
Detail beyond the abovetemporary set_log_level escalation

diag_bundle returns one ack with the last N status transitions, config version, uptime, reset reason, free heap, Wi-Fi and broker counters, and the last error — one round trip, since the common case is a unit only intermittently reachable.