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 byarecord -lfor 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/locgives 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
.envfile (step 5).
4. Register the satellite
- Log in to the BirdNET-NG web UI
- Select your tenant from the dropdown in the sidebar
- Navigate to Tenant Admin → Satellites
- Click the "+ Register satellite" button
- 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
.envconfiguration.
- Click Register
- A complete
.envconfiguration will appear with all required fields - 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_offin 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:
- RMS silence gate (microsecond cost): drops chunks below
filter_min_rms(default 0.003). Catches dead-mic / muted-room cases cheaply. - 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 aboveyamnet_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:
- Check for new versions on
origin/main - Show pending changes and ask for confirmation
- Stop the systemd service
- Pull code, install dependencies, rebuild
- 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:
- Go to Satellites page
- Expand the satellite with an outdated version (shows ⚠)
- 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 |