# 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 ` 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.