MQTT Protocol
BirdNET-NG uses MQTT for all satellite-to-hub communication. The broker is Mosquitto 2.x with the built-in Dynamic Security Plugin.
Topic Structure
birdnet/{tenant_id}/{satellite_id}/{channel}
Channels
| Channel | Direction | QoS | Description |
|---|---|---|---|
audio |
Satellite → Hub | 1 (at-least-once) | Audio chunk upload (base64 WAV) |
telemetry |
Satellite → Hub | 1 (at-least-once) | Battery, storage, CPU, GPS |
heartbeat |
Satellite → Hub | 1 (at-least-once) | Lightweight keepalive every 30s |
config |
Hub → Satellite | 2 (exactly-once) | Recording profile + filter settings push |
ack |
Hub → Satellite | 1 (at-least-once) | Chunk acknowledgment |
::: tip Why QoS 1 for telemetry? Telemetry was originally QoS 0 (fire-and-forget), but lost telemetry packets caused satellites to appear offline. Upgraded to QoS 1 for reliable delivery. :::
Message Formats
Audio Chunk
{
"chunkId": "uuid",
"satelliteId": "uuid",
"tenantId": "uuid",
"recordedAt": "2026-03-22T10:30:00Z",
"durationMs": 3000,
"sampleRate": 48000,
"latitude": 43.5659,
"longitude": 3.905,
"audio": "<base64-encoded WAV>"
}
3-second WAV at 48kHz mono, base64-encoded. The hub stores the audio in MinIO, inserts a DB record, and enqueues an inference job.
Telemetry
{
"satelliteId": "uuid",
"tenantId": "uuid",
"batteryLevel": 85,
"storageFreeBytes": 1073741824,
"cpuTemp": 42.5,
"networkLatencyMs": null,
"packetLossPct": null,
"uptimeSeconds": 3600,
"timestamp": "2026-03-22T10:30:00Z"
}
batteryLevel: percentage (0–100) ornullfor mains-powered devicesstorageFreeBytes: available disk space,nullif unavailablecpuTemp: degrees Celsius,nullon mobile (not accessible)- All fields except
satelliteId,tenantId,timestampare nullable
Heartbeat
{
"satelliteId": "uuid",
"tenantId": "uuid",
"uptimeSeconds": 3600,
"state": "recording",
"noiseFloorRms": 0.0012,
"version": "0.4.6",
"timestamp": "2026-03-22T10:30:00Z"
}
noiseFloorRms: last observed RMS amplitude of incoming chunks (telemetry for tuning the silence gate)version: (optional) satellite package version string; stored on the satellites table by the hub
State values:
| State | Meaning |
|---|---|
recording |
Actively capturing audio |
paused |
User paused recording (mobile) |
scheduled_off |
Outside recording window (scheduler) |
error |
Capture or connection error |
The hub updates satellites.last_seen_at and satellites.heartbeat_state on each heartbeat. This decouples online status from audio chunk sending — a satellite filtering all chunks (silence) still shows as online.
Sent at a configurable interval (default 30 seconds, adjustable in tenant settings) by both Pi satellites and mobile app.
Config Push
{
"satelliteId": "uuid",
"tenantId": "uuid",
"recordingProfile": {
"type": "dawn_chorus",
"schedule": [
{ "start": "-30", "end": "120", "reference": "sunrise" }
],
"sampleRate": 48000,
"chunkDurationMs": 3000,
"overlapMs": 0,
"gain": 1.0
},
"retentionHours": 48,
"heartbeatIntervalMs": 30000,
"filter": {
"enabled": true,
"minRms": 0.003,
"peakSnr": 1.5,
"birdBandCheck": true,
"noiseFloorAlpha": 0.02
}
}
Schedule references:
sunrise/sunset: start/end are minutes relative to sun event (negative = before)absolute: start/end areHH:MM24-hour format
The satellite resolves relative times locally using its GPS coordinates and the NOAA solar algorithm.
Config sync: The hub pushes config to all online satellites when tenant settings change. Satellites also send an empty-overrides config-request immediately after connecting, so they pick up the current effective config on every reconnect / fresh install. Filter settings (filterEnabled / filterMinRms / yamnetMinBirdProb / excludedYamnetCategories) control the on-device RMS silence gate and YAMNet VAD per-category drop pipeline.
Acknowledgment
{
"chunkId": "uuid",
"status": "stored"
}
Status: stored (success) or error (with error field containing message).
Connection
| Client | Protocol | URL |
|---|---|---|
| Pi Satellite | WSS | wss://mqtt.example.com |
| Mobile App | WSS | wss://mqtt.example.com |
- WSS: WebSocket over TLS, uses standard HTTPS port 443 — works through firewalls without extra configuration
- Client ID:
satellite-{satelliteId}ormobile-{satelliteId} - Auth: username = satellite ID, password = generated at registration
Security
- Mosquitto Dynamic Security Plugin manages per-satellite credentials
- Each satellite can only publish/subscribe to its own topics (ACL via
%uusername substitution) - Hub ingester subscribes to
birdnet/+/+/audio,birdnet/+/+/telemetry,birdnet/+/+/heartbeat - Credentials are provisioned at registration (
POST /api/satellites) and revoked on deletion
Offline Behavior
- The satellite offline timeout is configurable per tenant (default: 5 minutes)
SatelliteMonitor(hub) checks every 60 seconds for satellites wherelast_seen_atis older than the timeout- Heartbeat ensures the satellite stays online even when no audio passes the pre-filter
- When offline, the satellite queues chunks in its local SQLite outbox and drains them on reconnect