7.4 KiB
propmonitor Client Software — Design Plan
Goal: Define the propmonitor client that runs on RPi-based SDR hardware, authenticates with an API token, fetches its config, and periodically uploads beacon reception measurements.
Architecture
┌──────────────────────┐ POST /api/v1/measurements ┌──────────────────────┐
│ propmonitor client │ ──────────────────────────────► │ Microwaveprop API │
│ (RPi + SDR) │ │ (Elixir / Phoenix) │
│ │ ◄────────────────────────────── │ │
│ │ GET /api/v1/monitors/:token │ │
└──────────────────────┘ (config) └──────────────────────┘
Client responsibilities
- Boot registration: On first run, the client creates a unique hardware ID (e.g.
/etc/machine-idor a MAC-address-derived UUID) and reports it to the API. The API maps this to the monitor record provisioned by the admin. - Config fetch (
GET /api/v1/monitors/:token): The server returns the monitor's active config — which beacon to listen for, what frequency, integration window, and mode. - Measurement loop: For each integration window:
- Tune SDR to the configured frequency
- Record passband IQ for
config_integration_sseconds - Compute noise floor, signal peak/avg, SNR, signal active fraction
- Upload via
POST /api/v1/measurements
- Token-based auth: The API token (set when the admin provisions the monitor) is the only credential. No user account needed.
Data flow
Monitor token ──► GET /api/v1/monitors/:token ──► { name, beacon, frequency_hz, integration_s, mode }
│
▼
SDR tunes to frequency_hz
│
▼
Record for integration_s seconds
│
▼
Compute SNR, noise floor, etc.
│
▼
POST /api/v1/measurements ──► { monitor_id, beacon_id, frequency_hz,
snr_avg_db, snr_peak_db, noise_floor_dbfs,
gain_db, integration_s, measured_at, ... }
Endpoints the client uses
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/monitors/:token |
Fetch monitor config (name, beacon, frequency_hz, integration_s, mode, lat, lon) |
GET |
/api/v1/beacons |
List known beacons (for the user to select which to monitor) |
POST |
/api/v1/measurements |
Upload a measurement batch |
Client config file (~/.config/propmonitor/config.toml)
# Required: token assigned by admin
token = "abc123..."
# Optional overrides (if not set, fetched from API)
frequency_hz = 144_000_000
integration_s = 60
mode = "wspr"
# SDR device
sdr_type = "rtl-sdr"
sdr_gain_db = 40.0
On each run the client fetches the latest config from the API, merging file-based overrides on top.
Key design decisions
- Pull config, don't push: Client polls
GET /api/v1/monitors/:tokenon startup and periodically (every 5 minutes) for config changes. No long-lived connection needed. - Token is the identity: The token binds the client to a specific provisioned monitor record. Multiple clients cannot share a token.
- Self-registration via hardware_id: On first connect, the client submits its
hardware_idwhich links it to the correct admin-provisioned monitor record. If no matching hardware_id exists, the server rejects the registration and the admin must provision it first. - Battery-friendly: The client sleeps between integration windows. For a 60-second integration on a 5-minute cycle, duty cycle is ~20%.
Language choice
Python for initial implementation (fast prototyping, excellent SDR library support via pyrtlsdr / hackrf), with an eye toward a Rust rewrite for battery-constrained solar-powered deployments.
Hardware targets
- Raspberry Pi 3/4/5 + RTL-SDR v3 (entry-level, ~$30)
- Raspberry Pi 5 + HackRF One / Airspy HF+ Discovery (mid-range)
- Raspberry Pi 5 + LimeSDR / USRP B200 (advanced, for wideband monitoring)
Future considerations
- Webhook config pushes: Server pushes new config (
PATCHfrom admin UI) via a lightweight MQTT or SSE mechanism. - OTA firmware updates: The client checks for new versions on startup via a version endpoint.
- Solar/battery optimization: Deeper sleep states, batch uploads, variable integration windows.
- Offline mode: Cache the config and buffer measurements if the network is unavailable.
API validation endpoints needed
GET /api/v1/monitors/by-hardware/:hardware_id
Returns the monitor record for a given hardware_id. Used during registration to map the device to its admin-provisioned record.
Response:
{
"id": "uuid",
"name": "Rooftop East",
"token": "abc123...",
"config_frequency_hz": 144000000,
"config_integration_s": 60,
"config_mode": "wspr",
"beacon_id": "uuid"
}
POST /api/v1/measurements
Uploads a measurement. The client must set the Authorization: Bearer <token> header.
Request body:
{
"frequency_hz": 144000000,
"integration_s": 60,
"passband_hz": 200.0,
"gain_db": 40.0,
"noise_floor_dbfs": -85.3,
"signal_peak_dbfs": -72.1,
"signal_avg_dbfs": -75.4,
"snr_peak_db": 13.2,
"snr_avg_db": 9.9,
"signal_active_fraction": 0.85,
"measured_at": "2026-07-21T18:30:00Z",
"propmonitor_version": "0.1.0"
}
The beacon_id and monitor_id are resolved server-side from the token and the beacon matching the frequency + configured beacon.
Measurement ingestion pipeline
POST /api/v1/measurements
│
▼
BeaconMeasurementsController.create(conn, params)
│
▼
Look up monitor by token (Authorization header)
│
▼
Resolve beacon_id from configured beacon (monitor.beacon_id)
(or match by frequency if no explicit beacon configured)
│
▼
BeaconMeasurements.create_measurement(%{
monitor_id: resolved_monitor_id,
beacon_id: resolved_beacon_id,
frequency_hz: params.frequency_hz,
integration_s: params.integration_s,
passband_hz: params.passband_hz,
gain_db: params.gain_db,
noise_floor_dbfs: params.noise_floor_dbfs,
...
})
│
▼
Insert into beacon_measurements table
│
▼
Update monitor.last_seen_at
Implementation order
- Add
GET /api/v1/monitors/:tokenendpoint (returns monitor config) - Add
GET /api/v1/monitors/by-hardware/:hardware_idendpoint (registration lookup) - Add
POST /api/v1/measurementsendpoint with token auth - Write Python client prototype (
propmonitor/) - Dogfood: deploy to a test RPi with an RTL-SDR
Related files
lib/microwaveprop_web/controllers/— API v1 controller (where new endpoints go)lib/microwaveprop/beacon_measurements.ex— context for measurementslib/microwaveprop/beacon_monitors/beacon_monitor.ex— schema with token, hardware_id, etc.