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

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

  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)

# 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:

{
  "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

  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
  • 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.