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