Changelog: Raspberry Pi satellite
Every release of the Raspberry Pi satellite, newest first. It has had its own version number since hub 0.28; earlier releases are on the hub page, when every component shared one version. A Pi installs a release with birdnet-update.sh, or when a hub admin presses Update on its page.
1.6
1.6.5
Fixes
- The audio filter switch now means on the Pi what the protocol and the phone say: off, neither the silence gate nor the YAMNet gate runs (nor its category drops), and every chunk goes on to BirdNET, on the Pi or at the hub. The Pi used to keep YAMNet running with the filter off, so a satellite set to "filter off" still dropped chunks it judged birdless.
1.6.4
Fixes
- Before its first configuration from the hub, the Pi runs with the hub's defaults (from the shared settings registry): the range filter at 0.01 (it was 0.03) and the five pre-upload drop categories on (it dropped none until told to). The configuration the hub sends at every connection still decides.
1.6.3
Upgrade notes
- A Pi on 1.6.2 or older needs this update twice: the second run applies it.
Fixes
- On-device BirdNET can start on a Pi. birdnetlib imports librosa and audioread without declaring them, Python 3.13 dropped modules they still use, and birdnetlib only knew the old TFLite runtime while the Pi environment has its successor: none of that was installed or bridged, so device mode never loaded and every chunk went to hub inference.
- The installer and the updater now install one shared package list, and the sidecar uses ai-edge-litert under the name birdnetlib expects. Tested on Python 3.11 and 3.13 with the bundled recordings.
- A self-check confirms the BirdNET sidecar loads before the service restarts.
- The installer also deploys the BirdNET sidecar script it used to leave to the first update.
1.6.2
Upgrade notes
- Because an update runs the previous version of the updater script, a Pi on 1.6.1 or older needs this update twice: the second run applies the fix.
Fixes
- On-device BirdNET works again on a Pi installed before device mode existed. The updater maintained a Python environment inside the checkout, while the service runs the one install.sh created under /opt/birdnet-satellite, so birdnetlib never reached the BirdNET sidecar ("No module named birdnetlib") and every chunk fell back to hub inference.
- The updater now maintains the environment the service uses and checks that birdnetlib imports.
1.6.1
Fixes
- A team transfer pushed from the hub moves the Pi to its new team again: 1.6.0 still rewrote the old TENANT_ID line, which the renamed .env no longer reads, so the Pi stayed on its previous team. The rewrite now sets OTAVI_SAT_TENANT_ID and removes any leftover old line.
1.6.0
Upgrade notes
- Every setting in the Pi's .env is now OTAVI_SAT_* (OTAVI_SAT_ID, OTAVI_SAT_MQTT_BROKER_URL, OTAVI_SAT_CAPTURE_MODE, ...).
- Nothing to do by hand for the rename: the update converts the file in place with a backup, the installer does the same before reading it, and the app still honours the old names with a warning at startup.
1.5
1.5.0
Features
- The YAMNet gate drops a chunk for an excluded category (human voice, dogs, machines...) only when the bird score is weak (under 0.5); a clear call uploads whatever surrounds it.
- The bird score now includes the specific call classes (chirp, squawk, pigeon, coo, crow, caw, owl, hoot, flight).
- Whistling, breathing and domestic animals no longer count against birds.
- The silence floor defaults to off; the hub pushes the same.
Chores
- The gate follows the same rule as the phone app, shared in @birdnet-ng/shared.
1.4
1.4.0
Features
- Pi device inference now honors the tenant's hub-side thresholds instead of its own defaults:
min_confidence(viasetMinConfidence) andrange_filter_threshold(passed through the birdnetlib sidecar asfilter_threshold), both applied live from the pushedConfigMessage. The Pi already range-filtered via birdnetlib's lat/lon/week meta model; this aligns the threshold with the hub.
1.3
1.3.0
Upgrade notes
- Critical sites can now use device mode without the prior reliability caveat.
Fixes
- On-device inference, Phase 2.1 (reliability): device-inference hits now go through the outbox instead of a best-effort publish, closing the offline data-loss window flagged in 1.2.0. Chunks with no detection are still dropped on-device and never enqueued.
- A detection produced while the hub is unreachable is persisted to the sql.js outbox (new nullable
detectionscolumn carrying the model id + detection envelope) and the uploader drains it onto theinference-resultchannel when connectivity returns, with the same store-and-forward guarantees and multi-pass purge as hub-mode audio. - The outbox row id is the chunk id, so the hub ack clears the right row.
1.2
1.2.0
Upgrade notes
install.shandbirdnet-update.shinstall birdnetlib into the venv (optional; failure leaves the satellite on hub mode).- Known v1 limitation: device detections are best-effort, not stored in the outbox, so a hit produced while the hub is unreachable is lost. Keep critical sites on hub mode until the outbox-backed path lands.
- Hardware-untested (no Pi available here): the sidecar ML and the hub ingest path are verified, the Pi capture wiring is field-pending. Device inference was verified against fixtures (robin, cuckoo correctly identified through the sidecar).
Features
- On-device inference, Phase 2 (Pi): when the hub puts a satellite in
devicemode, the Pi runs BirdNET locally (a birdnetlib sidecar mirroring the YAMNet one: long-lived process, JSON-line protocol, applies the lat/lon/week range filter so results match the hub) after the YAMNet gate, and uploads detections + audio together on theinference-resultchannel only for hits. Chunks with no detection never leave the device. - When inference is unavailable (sidecar loading, or birdnetlib not installed) the chunk falls back to hub-side inference, so device mode degrades safely.
1.1
1.1.0
Upgrade notes
- Update-available detection needs no Pi-side work: the hub already compares each heartbeat's version against the latest
vsat-*tag and offers the update from the Satellites page.
Features
- Per-chunk audit trail (parity with the mobile app's chunk history): every sent/filtered decision lands in a 200-entry ring persisted with the filter counters, and the hub's "Get logs" action now returns it as a "Recent chunk events" section ahead of the journal, so "why is this Pi quiet" is answerable per chunk without verbose logging.
Fixes
- YAMNet sidecar resilience: the respawn-on-exit watchdog gains exponential backoff (5 s doubling to a 5 min cap, reset once the sidecar reports ready) so a broken venv stops spinning.
score()fails open instantly while the YAMNet sidecar is down instead of queueing onto a dead stdin until timeout.
1.0
1.0.3
Upgrade notes
- Update the Pi via
birdnet-update.sh(rolls to the latestvsat-*tag =vsat-1.0.3). Counters as of the next graceful shutdown will be picked up on the following start.
Fixes
- Filter pipeline counters now survive Pi reboots.
filter-stats.tsheldtotalProcessed,totalSent,rejectedSilence,rejectedYamnet,rejected{Amphibian,Insect,Anthropogenic,HumanVoice,OtherAnimal,Other}in a single process-locallet stats = createEmpty(), so on every Pi restart (reboot,birdnet-update.sh, systemd restart) the Node process reloaded, the module re-initialized and the counters snapped back to zero. The hub UI's "Filtre audio" card showed the reset immediately on the first post-restart heartbeat even though chunks kept flowing through the pipeline; outbox bytes were not affected (they live in sqlite and already persisted), only the chunk-pipeline counters. - New
initFilterStats(dataDir)reads${dataDir}/filter-stats.jsonif present and merges it over an empty record (so new fields default cleanly when an older on-disk file lacks them); it is called fromindex.tsright afteroutbox.init(). - Each
recordChunkSent/recordChunkFilteredmarks the stats dirty and a 5 s debounce flushes them to disk viawriteFileSync, the same cadence as the outbox persist, so SD-card write amplification is bounded. flushFilterStats()is called from the SIGINT / SIGTERM shutdown hook so a clean stop doesn't lose the last unwritten window.
1.0.2
Upgrade notes
- Update the Pi via
birdnet-update.sh(rolls to the latestvsat-*tag =vsat-1.0.2).
Fixes
- The heartbeat now reports real on-disk audio bytes, not just tracked outbox bytes. The orphan pass in Pi 1.0.1 reclaims leaked WAVs but doesn't surface their existence to the hub UI, and until reconciled the satellite reported
outboxStats.totalBytes(sum ofsize_bytesfrom outbox rows), so a Pi holding 3.5 GB of orphans showed "14 MB used" in the hub UI right up until the next 5-min sweep. OutboxQueue.stats()now also returnsdiskBytes, a freshreaddirSync+statSyncwalk of${dataDir}/audio/that captures orphan files too. Cost: onereaddirSync+ NstatSyncper heartbeat (~30 s cadence), sub-millisecond for a clean Pi with a few hundred chunks and ~100–500 ms for a leaking Pi with 70k orphans, dropping rapidly once the orphan pass runs.outboxStats.diskBytesflows through the heartbeat MQTT message (optional field in the shared type; older satellites omit it).- The hub UI hub 0.32.3 reads
diskBytesand showsmax(diskBytes, totalBytes)as "Used", plus a separate "Untracked" row whenever the delta is nonzero.
1.0.1
Upgrade notes
- Update the Pi via
birdnet-update.sh(rolls to the latestvsat-*tag =vsat-1.0.1).
Fixes
- Outbox purge now reclaims orphan WAVs, not just rows it already knows about. Symptom: a Pi's storage exploded past the configured
outbox_hard_size_mbcap and old chunks weren't being dropped even though the 5-min sweep was running. Root cause:purgeByPolicydecided everything fromSELECT SUM(size_bytes) FROM outbox, so anything on disk in${dataDir}/audio/that no outbox row pointed at was invisible to it and the size-based passes were blind to real usage. - First known source of orphans, a filter-time unlink that silently failed: when RMS or YAMNet rejects a chunk,
capture.tsdoestry { unlinkSync(filePath); } catch {}, and if unlink throws (FS race, permission) the catch swallows it and the file lingers. - Second known source of orphans, a crash between WAV write and
enqueue: the WAV is written first and the outbox row inserted after, so if anything throws in between, the outer try/catch absorbs the exception and the file is on disk with no row. - New orphan pass at the head of the four-pass sweep walks
${dataDir}/audio/, builds the tracked-paths set fromSELECT file_path FROM outbox(matched by basename) and deletes withunlinkSyncany file that is both not tracked and older than 5 minutes. The min-age guard prevents racing with an in-flight write. PurgeReportgainsorphanPass+orphanBytesFreed, and the sweep log line includesorphanMbFreedso operators see it working.
1.0.0
Chores
- Initial standalone release, functionally identical to the satellite portion of
vhub-0.28.0. - Future Pi-side changes (capture pipeline, on-device filtering, outbox, install/update scripts, telemetry shape) bump this stream independently.