You are viewing archived documentation for v0.29. Go to latest →

Satellite: Raspberry Pi

Deploy a Raspberry Pi as a BirdNET-NG satellite node for continuous bird audio capture.

Platform: Raspberry Pi — a dedicated, always-on Node.js agent that captures audio via ALSA and uploads to the hub over WSS.

Hardware Requirements

  • Raspberry Pi 3B+ or newer (4 or 5 recommended)
  • USB microphone or I2S MEMS microphone (e.g., Adafruit SPH0645)
  • SD card (16GB+)
  • Power supply
  • Network: Wi-Fi or Ethernet
  • Optional: GPS module (e.g., u-blox NEO-6M, Adafruit Ultimate GPS, BN-880)
  • Optional: weatherproof enclosure for outdoor deployment

Operating System

Recommended: Raspberry Pi OS Lite (64-bit) — the official headless distribution with minimal footprint. Flash it using Raspberry Pi Imager.

Any Debian-based distribution works, including:

  • Raspberry Pi OS Lite (recommended) — official, best hardware support
  • Ubuntu Server for Pi — if you prefer Ubuntu's ecosystem
  • DietPi — ultra-lightweight, good for constrained setups

Requirements: ALSA audio support, apt package manager, systemd for service management.

Tip: When flashing with Raspberry Pi Imager, enable SSH and configure Wi-Fi in the advanced settings to avoid needing a monitor and keyboard for initial setup.

Software Setup

1. Install prerequisites

# Update system
sudo apt update && sudo apt upgrade -y

# Install tools
sudo apt install -y git curl jq alsa-utils vim

# Install Node.js 24 LTS
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash -
sudo apt install -y nodejs

# Install pnpm
sudo npm install -g pnpm@latest

# Verify versions
node --version   # v24.x
pnpm --version   # 9.x+
git --version

2. Test audio capture

arecord -l                    # List audio devices — find your microphone

This will output something like:

card 0: Headphones [bcm2835 Headphones], device 0: ...
card 1: Microphone [USB Microphone], device 0: ...

The ALSA device name is plughw:<card>,<device> — so card 1, device 0 becomes plughw:1,0. Test it:

arecord -D plughw:1,0 -f S16_LE -r 48000 -c 1 -d 3 test.wav
aplay test.wav                # Verify recording works

Note: The built-in bcm2835 (card 0) is output only. USB microphones are typically card 1 or higher. Use the card number shown by arecord -l for your microphone.

3. Find your GPS coordinates

GPS coordinates are required for audio processing. BirdNET uses location to identify which species are possible in your area. Without coordinates, audio chunks will be queued but not processed — they will be marked as failed in the inference queue.

Ways to find your coordinates:

  • Google Maps: Right-click any location → the coordinates appear at the top of the context menu
  • OpenStreetMap: Browse to your location, the URL contains the coordinates (e.g., map=15/43.4228/4.6310)
  • Phone: Open any maps app, long-press your location, coordinates will be shown
  • Terminal: curl -s ipinfo.io/loc gives a rough estimate based on your IP address

Note: If your Pi has a GPS module (e.g., u-blox NEO-6M), coordinates will be updated automatically via gpsd. You can also set them later in the .env file (step 5).

4. Register the satellite

  1. Log in to the BirdNET-NG web UI
  2. Select your tenant from the dropdown in the sidebar
  3. Navigate to Tenant Admin → Satellites
  4. Click the "+ Register satellite" button
  5. Fill in:
    • Name — a descriptive name (e.g., "Garden Pi", "Rooftop North")
    • Latitude / Longitude (optional) — enter the coordinates from step 3 if available. They will be included in the generated .env configuration.
  6. Click Register
  7. A complete .env configuration will appear with all required fields
  8. Click "Copy to clipboard" and save these somewhere safe — the MQTT password cannot be retrieved later

The satellite will appear in the Satellites table with a "registered" status until it connects for the first time.

Important: Keep these credentials secure. Anyone with them can send audio data to your hub as this satellite.

5. Install

install.sh is the only deployment path for both fresh installs and upgrades. It self-bootstraps pnpm via corepack, builds the workspace, deploys to /opt/birdnet-satellite/ via pnpm deploy (resolves workspace deps), bootstraps the YAMNet Python venv with apt's numpy, auto-detects the USB audio device, writes the systemd unit, and starts the service.

# Clone
git clone <your-git-server>/birdnet-ng.git ~/birdnet
cd ~/birdnet

# Drop the .env from the registration step next to .env.example
cp packages/satellite/.env.example packages/satellite/.env
nano packages/satellite/.env   # paste the values from the registration step

# Install — handles everything end-to-end
sudo ./packages/satellite/install.sh

install.sh is idempotent; re-run it for upgrades or to reapply config changes (it'll prompt only for any var still unset and reuse the rest from .env).

Connection: The satellite connects via WSS (WebSocket Secure) over standard HTTPS port 443. No extra firewall ports needed.

6. Verify

The systemd service is already enabled and started by install.sh. Tail the journal to confirm:

sudo systemctl status birdnet-satellite
sudo journalctl -u birdnet-satellite -f    # Connect, YAMNet ready, audio capture starting

Filesystem layout

Path Owner Purpose
/opt/birdnet-satellite/dist/index.js root Service entry (what systemd runs)
/opt/birdnet-satellite/node_modules/ root Runtime deps (deployed via pnpm deploy)
/opt/birdnet-satellite/yamnet-venv/ root Python env for the YAMNet sidecar
/opt/birdnet-satellite/scripts/yamnet_vad.py root YAMNet sidecar script
/opt/birdnet-satellite/models/yamnet.tflite root YAMNet model + class map
/opt/birdnet-satellite/data/outbox.db root Local SQLite outbox queue
/opt/birdnet-satellite/data/audio/ root Buffered WAV chunks awaiting upload
~/birdnet/packages/satellite/.env install user Config (gitignored, hand-edited)
~/birdnet/packages/satellite/scripts/birdnet-update.sh install user Update script the hub triggers
/etc/systemd/system/birdnet-satellite.service root systemd unit (regenerated by install.sh)
/etc/systemd/system/birdnet-update.service root Transient unit; exists only during a hub-triggered update

Logs go to journald — no file path. Tail with sudo journalctl -u birdnet-satellite -f.

install.sh is idempotent — re-running it rewrites everything in /opt/... and the systemd unit, but preserves data/ (mid-deploy purge would lose unsent chunks) and yamnet-venv/ (avoids a slow pip reinstall on Pi 3). The .env in the checkout is the single source of truth and is never touched mid-run except to be re-read.

Reset from scratch (preserves .env)

.env lives in the checkout; deployment + data live inside /opt/birdnet-satellite/.

# Stop service + any pending update jobs
sudo systemctl stop birdnet-satellite
sudo systemctl reset-failed birdnet-update.service 2>/dev/null

# Optionally preserve the YAMNet venv to skip a slow pip reinstall on Pi 3
sudo mv /opt/birdnet-satellite/yamnet-venv /tmp/birdnet-yamnet-venv.bak

# Wipe the deployment dir (includes /opt/birdnet-satellite/data/ —
# outbox + buffered audio go here too now)
sudo rm -rf /opt/birdnet-satellite

# Wipe build artifacts in the checkout (.env is preserved automatically)
rm -rf ~/birdnet/node_modules ~/birdnet/packages/*/node_modules ~/birdnet/packages/*/dist

# Restore the venv if you stashed it
sudo mkdir -p /opt/birdnet-satellite
sudo mv /tmp/birdnet-yamnet-venv.bak /opt/birdnet-satellite/yamnet-venv

# Run installer
sudo ~/birdnet/packages/satellite/install.sh

For a truly clean slate (re-clone too): copy ~/birdnet/packages/satellite/.env aside, rm -rf ~/birdnet, re-clone, restore .env at the same path.

GPS Setup

For live GPS positioning instead of static coordinates:

sudo apt install -y gpsd gpsd-clients

Edit /etc/default/gpsd:

DEVICES="/dev/ttyUSB0"    # or /dev/serial0 for GPIO UART
GPSD_OPTIONS="-n"
START_DAEMON="true"
sudo systemctl enable --now gpsd
cgps                         # Verify GPS fix

Add to .env:

GPS_MODE=gpsd
GPSD_HOST=localhost
GPSD_PORT=2947

When GPS mode is gpsd, the satellite reads live coordinates from the GPS daemon and updates the hub via telemetry. Falls back to static coordinates if GPS has no fix.

Recording Profiles

Profiles control when the satellite captures audio. They are pushed from the hub via MQTT and can be changed on the Satellites page.

Profile Schedule Description
continuous 24/7 Always recording
dawn_chorus sunrise-30min to sunrise+2h Peak bird activity
night_migration sunset+30min to sunset+12h Nocturnal flight calls
low_power sunrise+/-30min, sunset+/-30min Dawn and dusk only

Sunrise and sunset times are calculated automatically from the satellite's GPS coordinates using the NOAA solar algorithm. Times update daily as day length changes with seasons.

When outside a recording window, the satellite:

  • Pauses the capture loop (no CPU/disk usage)
  • Continues sending heartbeats (stays "online" on the hub)
  • Reports state as scheduled_off in the heartbeat
  • Re-checks the schedule every 5 minutes

Heartbeat

The satellite sends a lightweight heartbeat at a configurable interval (default 30 seconds, set from hub tenant settings) via MQTT QoS 1. This keeps the satellite showing as "online" on the hub even when all audio chunks are filtered out (silence).

The heartbeat includes the satellite's current state (recording, scheduled_off, error) and noise_floor_rms for filter calibration, visible on the Satellites page.

On-Device Audio Filtering

The satellite filters audio in two stages before upload:

  1. RMS silence gate (microsecond cost): drops chunks below filter_min_rms (default 0.003). Catches dead-mic / muted-room cases cheaply.
  2. YAMNet VAD (~30 ms cost on a Pi 4): a Python sidecar runs Google's YAMNet TFLite model and computes per-category probability sums:
    • bird (AudioSet 67/103/106/107) — kept when above yamnet_min_bird_prob (default 0.05)
    • amphibian (frog, 127), insect (cricket/insect, 121-122), anthropogenic (44 vehicle/tool/alarm/firework classes), human_voice (17 speech/laughter/breathing classes), other_animal (10 dog/cat/cattle classes)

A chunk passes when bird ≥ threshold AND no excluded category outscores bird. The set of excluded categories comes from each tenant's drop_*_at_satellite toggles (default true), pushed via MQTT and overridable per satellite.

The Python sidecar uses ai-edge-litert (with a tflite-runtime fallback for older Pythons). YAMNet fails open if the model or runtime is missing — i.e. only the cheap RMS gate runs.

The satellite reports per-category drop counts and a live RMS sample in the heartbeat. Filtering is automatically disabled for simulate and replay capture modes.

Updating

Manual update

Run the included update script:

cd ~/birdnet-ng
./packages/satellite/scripts/birdnet-update.sh

This will:

  1. Check for new versions on origin/main
  2. Show pending changes and ask for confirmation
  3. Stop the systemd service
  4. Pull code, install dependencies, rebuild
  5. Restart the service

Use --force to skip the confirmation prompt (useful for automation).

Remote update from hub

Admins can trigger an update from the web UI:

  1. Go to Satellites page
  2. Expand the satellite with an outdated version (shows ⚠)
  3. Click the Update button next to the version

This sends an MQTT command to the satellite, which runs the update script automatically. The satellite will briefly go offline during the update and reconnect with the new version.

Git credentials

To avoid entering credentials on every git pull:

git config --global credential.helper store

Next time you authenticate, credentials are saved in ~/.git-credentials. For GitHub, use a personal access token as the password.

Troubleshooting

Symptom Check
Satellite shows offline Verify MQTT credentials, check journalctl -u birdnet-satellite
No detections Check if all chunks are filtered (low audio activity), verify worker is running
GPS unavailable Check cgps for fix, verify /dev/ttyUSB0 permissions
High CPU temp Normal for Pi 4 under load; add heatsink or fan
Storage filling up Lower outbox_hard_size_mb in tenant settings (or per-satellite override) and check outbox drain