Skip to content

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

ChannelDirectionQoSDescription
audioSatellite → Hub1 (at-least-once)Audio chunk upload (base64 WAV)
telemetrySatellite → Hub1 (at-least-once)Battery, storage, CPU, GPS
heartbeatSatellite → Hub1 (at-least-once)Lightweight keepalive every 30s
configHub → Satellite2 (exactly-once)Recording profile push
ackHub → Satellite1 (at-least-once)Chunk acknowledgment

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

json
{
  "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

json
{
  "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) or null for mains-powered devices
  • storageFreeBytes: available disk space, null if unavailable
  • cpuTemp: degrees Celsius, null on mobile (not accessible)
  • All fields except satelliteId, tenantId, timestamp are nullable

Heartbeat

json
{
  "satelliteId": "uuid",
  "tenantId": "uuid",
  "uptimeSeconds": 3600,
  "state": "recording",
  "timestamp": "2026-03-22T10:30:00Z"
}

State values:

StateMeaning
recordingActively capturing audio
pausedUser paused recording (mobile)
scheduled_offOutside recording window (scheduler)
errorCapture 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 every 30 seconds by both Pi satellites and mobile app.

Config Push

json
{
  "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
}

Schedule references:

  • sunrise / sunset: start/end are minutes relative to sun event (negative = before)
  • absolute: start/end are HH:MM 24-hour format

The satellite resolves relative times locally using its GPS coordinates and the NOAA solar algorithm.

Acknowledgment

json
{
  "chunkId": "uuid",
  "status": "stored"
}

Status: stored (success) or error (with error field containing message).

Connection

ClientProtocolURL
Pi SatelliteMQTTSmqtts://mqtt.example.com:8883
Mobile AppWSSwss://mqtt.example.com
  • MQTTS: TLS terminated by Traefik, forwarded as plain MQTT to Mosquitto
  • WSS: WebSocket over TLS for browser/Capacitor compatibility
  • Client ID: satellite-{satelliteId} or mobile-{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 %u username 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 where last_seen_at is 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

Distributed bird sound identification