prop/docs/plans/2026-07-21-propmonitor-client-software.md

188 lines
7.4 KiB
Markdown

# 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
1. **Boot registration:** On first run, the client creates a unique hardware ID (e.g. `/etc/machine-id` or a MAC-address-derived UUID) and reports it to the API. The API maps this to the monitor record provisioned by the admin.
2. **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.
3. **Measurement loop:** For each integration window:
- Tune SDR to the configured frequency
- Record passband IQ for `config_integration_s` seconds
- Compute noise floor, signal peak/avg, SNR, signal active fraction
- Upload via `POST /api/v1/measurements`
4. **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`)
```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
1. **Pull config, don't push:** Client polls `GET /api/v1/monitors/:token` on startup and periodically (every 5 minutes) for config changes. No long-lived connection needed.
2. **Token is the identity:** The token binds the client to a specific provisioned monitor record. Multiple clients cannot share a token.
3. **Self-registration via hardware_id:** On first connect, the client submits its `hardware_id` which 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.
4. **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 (`PATCH` from 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:**
```json
{
"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:**
```json
{
"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
1. Add `GET /api/v1/monitors/:token` endpoint (returns monitor config)
2. Add `GET /api/v1/monitors/by-hardware/:hardware_id` endpoint (registration lookup)
3. Add `POST /api/v1/measurements` endpoint with token auth
4. Write Python client prototype (`propmonitor/`)
5. 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 measurements
- `lib/microwaveprop/beacon_monitors/beacon_monitor.ex` — schema with token, hardware_id, etc.