Satellite: Android

Use an Android phone as an Otavi satellite for portable bird audio capture.

Platform: Android — a Capacitor-based mobile app with native AudioRecord plugin, background recording, and in-app registration.

Device Requirements

  • Android 8.0 (Oreo) or newer
  • Working microphone
  • GPS (for location-aware species filtering)
  • Network connectivity (Wi-Fi or mobile data)

Getting the APK

Easiest path is to grab the latest release from your hub. After vhub-0.32.7+:

  1. Log in to the web UI as any tenant member.
  2. Open the Satellites page.
  3. Click Get mobile app vX.Y.Z in the page header. The browser downloads the APK; transfer it to the phone via USB, cloud drive, or any sideload path that works for you.

Hub admins can also pick a specific historical version from Hub settings → Mobile (each row has a Download button).

Updating from 1.15.x or earlier to 1.16.0+: release builds are now signed with a dedicated offline key instead of the old debug key. Android refuses to install an APK signed with a different key over an existing app, so this one upgrade needs a manual uninstall of the old app, then install of 1.16.0. Re-registering the phone re-attaches it to its existing satellite record (matched by device id), so history is kept. Every later update installs in one tap as usual.

Building locally

If you need a custom build (signed with your own keystore, different hub URL baked in at build time, etc.):

cd packages/mobile
./build-apk.sh

The script reads .env from the repo root (auto-derives OTAVI_MOBILE_HUB_URL from OTAVI_APP_FQDN and OTAVI_MOBILE_HUB_API_KEY from OTAVI_HUB_INTERNAL_API_KEY), builds a release-signed APK inside Docker, and uploads it to the running hub as a new release.

Signing phone 1.16.0: the release keystore lives at ${OTAVI_APP_VOLUMES}/mobile/release.keystore, outside git, with its password in .env as OTAVI_MOBILE_KEYSTORE_PASSWORD. The script stages it into the build context and passes the password as a BuildKit secret, so neither ends up in an image layer. Generate it once with keytool (see .env.example) and back it up with the volume tree: the key is the app's identity, and without it no installed copy can ever be updated again.

Android Permissions

The app requests these permissions at startup:

Permission Why
RECORD_AUDIO Capture audio from the microphone
ACCESS_FINE_LOCATION GPS coordinates for geo-aware inference

If permissions are denied, the app shows an "Open Settings" button to navigate to Android's app permission screen.

Setup Flow

1. Sign In

Enter your hub URL (e.g. https://birdnet.example.com) and log in with your credentials.

If your account has MFA enabled (TOTP / passkeys), the app prompts for a 6-digit code after the password. Toggle to "Use backup code" if your authenticator is unavailable. Hub-side this calls /api/auth/login/mfa/{totp,backup}?returnToken=true hub 0.32.6; pre-phone 1.4.1 the mobile flow couldn't complete MFA at all.

After registration the app stores a satellite-scoped API key phone 1.4.0 instead of the session JWT. The key is long-lived and survives password changes or session expiry, so the satellite keeps working without re-login. Existing installs auto-exchange their stored JWT for an API key on first launch after upgrade.

2. Register Satellite

Choose a name for this satellite and select the team it belongs to. GPS location is acquired automatically after registration -- no need to enter coordinates during registration.

When the team already has phones you registered (or team phones you manage as an admin), the screen asks "Does this phone replace one registered before?" Pick the old phone after a reinstall or when moving to a new phone: this one carries on as it, with its settings (on-device BirdNET, filter thresholds, storage caps) and its history. The entry this install used until then, if any, is archived with its data. Answer "no" for a genuinely new device. A reinstall always gives the phone a new device ID, so without this answer it would start a new entry on the team's defaults.

3. GPS Requirement

Before recording starts, the app requires a GPS fix or manually entered coordinates. This is needed because BirdNET uses location + date to filter its species list for regionally plausible species.

If GPS is unavailable:

  • A yellow "GPS Location Required" prompt appears
  • Retry GPS: attempt auto-acquisition again
  • Open Settings: enter latitude/longitude manually

4. Record

Once GPS is set, the app connects to the hub via MQTT (WebSocket over TLS). The app starts in a paused state -- tap the recording status bar to begin capturing 3-second audio chunks. It starts recording by itself when Record on launch is on in Settings, or when an app update interrupted a recording (since 1.26.0). Audio is filtered on-device with an RMS silence gate followed by a YAMNet VAD (native Capacitor plugin via org.tensorflow:tensorflow-lite); filter thresholds and per-category drop settings are received from the hub.

Recording Screen

The recording screen features a tap-to-record status bar -- the large recording rectangle is the toggle for starting and stopping capture. The app starts in a paused state by default.

While recording, the Live tab shows a scrolling live spectrogram phone 1.11.0 of the audio being captured, 0–12 kHz bottom-to-top. It streams only while the screen is on and pauses when the app is backgrounded.

A badge shows where identification runs: On-device AI (phone 1.12.0, set the satellite to device inference mode at the hub) or Hub AI. In device mode the phone classifies chunks locally with a bundled BirdNET model and uploads only detections; otherwise it uploads audio for the hub to analyze. A "Hub-managed" chip appears when an admin has locked the config. Bird names follow the tenant's primary language.

Stat Description
Chunks Sent Audio chunks uploaded to hub
Filtered Chunks rejected by adaptive filter
Last Sent Time since last successful upload
Hub Connection status (Connected / Connecting / Offline)
GPS Fix status (Fixed / Acquiring / Not set)
Name Satellite display name

Below the chunk audit, two collapsible cards:

  • Chunks (N): per-chunk pipeline audit, last 16 statuses as colored dots (sent / live / silence / low-bird / category-excluded). Expand for the full list with timestamps + RMS / bird-prob values.
  • Détections (N) phone 1.3.0: live feed of detections from chunks this phone uploaded. REST backfill from /api/detections?satelliteId=<self>&limit=50 on mount, then live MQTT prepend via the new /detection topic. Each row shows species (common + scientific), confidence %, timestamp, and badges for rare / first-of-day / first-of-season.

Settings Page

Settings is a full page (not a popup), accessible from the recording screen.

Satellite Name

Editable field that syncs to the hub. Visible on the Satellites page in the web UI.

Location

Clear toggle between GPS and manual location modes:

Control Description
GPS / Manual toggle Switch between automatic GPS and manual coordinate entry
Latitude / Longitude Manual coordinate entry (when in manual mode)
Use GPS Acquire position from device GPS
Save Apply manually entered coordinates
Auto-update GPS Toggle periodic GPS updates (on by default)
Interval Update frequency: 30s, 1min, 2min, 5min, 10min

::: tip Battery Savings For a stationary phone, turn off GPS auto-update and set coordinates manually. GPS polling is the largest battery drain after screen-on. :::

Keep Screen On

Toggle to prevent the phone from locking. Uses the Capacitor keep-awake plugin.

Verbose Chunk Logging

Toggle to enable detailed per-chunk log entries for diagnostics (filter decisions, RMS values, SNR).

Recording Profile

Read-only display of the hub-assigned profile, with resolved sunrise/sunset times and active recording windows. Profiles are changed from the hub (Satellites page), not in-app.

Audio Filter Settings

All filter settings are received from the hub via MQTT and applied locally. The on-device pipeline is RMS silence gate + YAMNet VAD (per-category bird-likelihood scoring via the bundled tensorflow-lite AAR). Tenant-wide defaults and per-satellite overrides for yamnet_min_bird_prob and the five drop_*_at_satellite toggles are configured in the hub.

Satellite Info

  • Satellite ID -- UUID (read-only)
  • Hub -- hub URL (read-only)

Unregister

Type UNREGISTER to confirm. This:

  • Stops recording
  • Disconnects from the hub
  • Clears the saved configuration
  • Does not delete the satellite record on the hub

To re-register: sign in again and register. On the same install the device ID reconnects to the existing satellite record; after a reinstall, pick the old phone when asked whether this one replaces it.

Background Mode

The app uses the Capacitor background mode plugin to continue recording when minimized. A persistent notification shows "Otavi -- Recording bird sounds..."

::: warning Battery Background recording with screen off is the most battery-efficient mode. Keep-screen-on with background mode will drain battery faster. :::

Heartbeat

The app sends a heartbeat at a configurable interval (default 30 seconds) via MQTT QoS 1. The interval is set from hub tenant settings. This keeps the satellite showing as "online" on the hub even when all audio chunks are filtered. The heartbeat reports the current state:

  • recording -- actively capturing
  • paused -- user pressed Pause

Telemetry

Reported every 60 seconds:

Metric Source
Battery level Capacitor Device plugin
Storage free navigator.storage.estimate()
GPS coordinates Geolocation plugin (if auto-update is on)
Uptime Elapsed time since app start

CPU temperature is not available on mobile -- shown as "--" on the hub.

Log Viewer

Collapsible panel at the bottom of the recording screen. Captures:

  • Audio capture events (chunk sent, filtered)
  • MQTT connection changes
  • GPS updates
  • Errors and warnings

Logs are in-memory only (max 200 entries). Use "Copy All" to export, "Clear" to reset.

Troubleshooting

Symptom Solution
"Could not start audio source" Another app is using the microphone. Close other audio apps.
"Microphone permission not granted" Tap "Open Settings" and enable the microphone permission.
Satellite shows offline Check hub connection status. Verify hub URL and credentials.
GPS unavailable Enable location services. Try "Use GPS" in Settings. Some indoor locations have no GPS signal.
App not installed (update fails) The APK was built with a different signing key. Uninstall first, then install the new APK.
Duplicate satellites A reinstall gives the phone a new device ID. Unregister, register again and pick the old phone when asked whether this one replaces it: the duplicate is archived with its data.

Updates on the phone

The app checks the hub for a newer APK at launch, every 6 hours, and when the hub nudges it after an upload. A pending release shows as a banner. On Wi-Fi while charging the phone downloads and verifies it by itself, so the banner's button becomes "Install now" and only the Android install sheet is left; on mobile data nothing is fetched until you tap. A release uploaded with "force update" (the rollback path) shows a blocking dialog instead of a banner. Installing an update restarts the app: if it was recording, it records again as soon as it reopens (tap "Open" at the end of the install).

Species sheet

Tap the latest detection or a species in today's list on the Live screen to open what the hub knows about it: its photos, swipe to see more, each credited, the times this phone heard it today, the traits and the Wikipedia summary. Names and texts follow the phone's language; where the hub has no text in that language, it shows the English one. The sheet needs the hub to be reachable; answers are kept until the app restarts.

Photos show whole, at their own proportions. Tap one to see it full screen: pinch or double-tap to zoom where your fingers are, drag to look around, swipe sideways for the next photo and down to close; a tap hides or shows the credit and the close button. Zooming in downloads the full-resolution copy of that photo (1 to 4 MB for most), once per viewing and only when it weighs 8 MB or less; larger scans zoom on the lighter copy already on screen. The back button closes the full-screen photo, then the sheet.