/sdr-receiver
Drive the Embedded AI Harness workbench's RTL-SDR receiver over `/api/sdr/*` — decode/analyze/power captures, the phased `acquire`, the interactive live rtl_433 console, "AI Sherlock" record→reverse-engineer, USB recovery, and the rtl_433 device database. Use this whenever the
$ npx -y skills add SensorsIot/Embedded-AI-Harness --skill sdr-receiver --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/sdr-receiver
Context preview
The summary Claude sees to decide when to auto-load this skill.
Drive the Embedded AI Harness workbench's RTL-SDR receiver over `/api/sdr/*` — decode/analyze/power captures, the phased `acquire`, the interactive live rtl_433 console, "AI Sherlock" record→reverse-engineer, USB recovery, and the rtl_433 device database. Use this whenever the
SKILL.md
sdr-receiver.SKILL.mdname: sdr-receiver
description: Drive the Embedded AI Harness testbench's RTL-SDR receiver over `/api/sdr/*` — decode/analyze/power captures, the phased `acquire`, the interactive live rtl_433 console, "AI Sherlock" record→reverse-engineer, USB recovery, and the rtl_433 device database. Use this whenever the user wants to receive, sniff, decode, analyze, or reverse-engineer an RF signal (433/315/868 MHz remotes, sensors, TPMS), read RSSI/pulse timing, recover a wedged dongle, or add a device to rtl_433 — even if they only say "rtl_433", "rtl-sdr", "sniff a remote", "what frequency is this", "signal too strong", "no signal", "AI Sherlock", or "add this remote". This is the receive-side counterpart to the transmit-only `signal-generator` skill. Always GET `/api/sdr/status` first to confirm the dongle is detected.
SDR Receiver (`/api/sdr/*`)
The testbench Pi has one RTL2832U dongle behind the `rtl_433` toolchain. Every receive operation goes through `/api/sdr/*`. It is the receive-side counterpart to the transmit-only `signal-generator` skill — never SSH in to run `rtl_433` yourself, drive the API.
**One dongle, one user.** Every capture (and the whole live console) holds a single-instance lock. While one is running, the others return `"SDR busy — a capture is already running"`. Stop the live console before a one-shot, and vice-versa.
---
Always check status first
**GET `/api/sdr/status`** before anything:
{"ok": true, "active": false, "mode": null, "freq_hz": 0,
"hardware": {"rtl_433": true, "rtl_test": true, "device": true},
"available": true}- `hardware.device: false` / `available: false` → the dongle isn't detected. Try
`POST /api/sdr/reset` (USB reset); if still absent, it's unplugged or the USB controller is wedged (see **Dongle recovery**).
- `active: true` → something is already using the dongle; stop it first.
---
API summary
Every endpoint, with its request and response shape: [FSD Appendix D.10](../../../docs/Harness-FSD.md#d10-sdr-receiver).
Only the choice between them is this skill's: `capture` decodes, `analyze` gives raw pulse timing when nothing decodes, `power` measures a level, and `acquire` runs all three in sequence when you do not yet know what is on the air.
Common body fields: `freq_hz` (default 433.92 MHz), `duration_s`, `gain` (number dB, or omit for AGC), `sample_rate` (default 250 kHz — keep low, it's a Pi Zero 2 W), `flex` (an `-X` spec).
**A dongle plugged in after boot is picked up automatically** — status re-probes while the SDR is idle, so there is no need to restart the portal. If `device` stays `false`, the dongle really is absent or not enumerating.
**Pin `gain` on anything you will compare.** On AGC the tuner rescales from whatever it saw recently, so the same quiet band reads tens of dB apart between calls and a strong carrier compresses instead of standing clear. This bites hardest on `power`, where the whole point is comparing numbers.
**Is the dongle working at all?** `pytest pytest/ -k wt1909` transmits on 86.784 MHz (5th harmonic = 433.92 MHz) and requires the dongle to see a ≥15 dB lift, then a drop. It needs no DUT and no operator. Run it before debugging a "nothing decodes" complaint — it separates a dead receive path from a signal problem in one step.
---
The two big signal traps (learned the hard way)
1. **AGC saturates a too-close source.** With the tuner's default auto-gain, a strong near-field transmitter rails the input to full scale and fills an OOK signal's off-gaps — so a crisp remote reads as a continuous / misdetected **FSK** carrier and slices to all-zero codewords. Fix: **more distance** or a **fixed `gain`** (e.g. 33–40 dB). The tell is RSSI pegged near 0 dB regardless of distance, and `rtl_433` reporting FSK for a known-OOK remote.
2. **Decode mode is empty for unknown remotes.** Plain `decode` only emits for a *known* protocol. An unrecognised remote produces nothing — and no RSSI. Use `analyze` (or the live console, which runs `-A` in every mode) to see the signal regardless of decode. **Signal presence ≠ decodability.**
---
Live console (`/api/sdr/live/*`)
A persistent `rtl_433` whose merged output a reader thread fans into a sequence-numbered ring buffer the browser fast-polls (~500 ms; nothing dropped). `-A` runs in **every** mode, so the RSSI meter shows every burst's strength even when it doesn't decode.
`POST /api/sdr/live/start` body:
{"freqs": [433920000], "mode": "decode",
"gain": 33.8, "sample_rate": 250000, "squelch": false,
"hop_interval": 5, "flex": "n=r,m=OOK_PWM,s=340,l=2068,r=13936", "isolate": false}- `freqs`: one = locked, several = **hop** (`-H hop_interval`). Bands: 433.92 /
315 / 868. Hop to *find*, then lock to a single freq to *work* (hopping listens to each band ~1/3 of the time and misses momentary presses).
- `mode`: `decode` (all built-ins) · `flex` (`-X`, `isolate:true` adds `-R 0`) ·
`analyze` (`-A` only).
- `gain`: omit for AGC; a number sets a fixed tuner gain.
- Poll `GET /api/sdr/live?since=<seq>`; each entry is `{seq, line, event}` where
`event` is the parsed JSON (or a synthetic `{rssi,snr,noise,analyzer:true}` from an analyzer `RSSI:` line). RSSI is **burst-driven** — it updates on a press, not continuously.
---
AI Sherlock — reverse-engineer an unknown remote
The record→analyze flow the console exposes as one toggle button:
1. **Start** (`/api/sdr/log/start`) — record the live stream (analyze+AGC). 2. Operator presses each key a few times. 3. **Stop** (`/api/sdr/log/stop`) — freeze the log. 4. `GET /api/sdr/log` → an assistant reads the bursts and derives: modulation/timing, the **constant preamble/device-ID**, and the **per-key varying field**, plus a decoder spec and button→code map.
`start_log` **clears** the previous recording (single in-memory buffer, lost on restart) — it's a record-analyze-now loop, not persistent storage.
---
Device database (make a
Read more
name: sdr-receiver description: Drive the Embedded AI Harness testbench's RTL-SDR receiver over `/api/sdr/*` — decode/analyze/power captures, the phased `acquire`, the interactive live rtl_433 console, "AI Sherlock" record→reverse-engineer, USB recovery, and the rtl_433 device database. Use this whenever the user wants to receive, sniff, decode, analyze, or reverse-engineer an RF signal (433/315/868 MHz remotes, sensors, TPMS), read RSSI/pulse timing, recover a wedged dongle, or add a device to rtl_433 — even if they only say "rtl_433", "rtl-sdr", "sniff a remote", "what frequency is this", "signal too strong", "no signal", "AI Sherlock", or "add this remote". This is the receive-side counterpart to the transmit-only `signal-generator` skill. Always GET `/api/sdr/status` first to confirm the dongle is detected.
SDR Receiver (`/api/sdr/*`)
The testbench Pi has one RTL2832U dongle behind the `rtl_433` toolchain. Every receive operation goes through `/api/sdr/*`. It is the receive-side counterpart to the transmit-only `signal-generator` skill — never SSH in to run `rtl_433` yourself, drive the API.
**One dongle, one user.** Every capture (and the whole live console) holds a single-instance lock. While one is running, the others return `"SDR busy — a capture is already running"`. Stop the live console before a one-shot, and vice-versa.
---
Always check status first
**GET `/api/sdr/status`** before anything:
{"ok": true, "active": false, "mode": null, "freq_hz": 0,
"hardware": {"rtl_433": true, "rtl_test": true, "device": true},
"available": true}- `hardware.device: false` / `available: false` → the dongle isn't detected. Try
`POST /api/sdr/reset` (USB reset); if still absent, it's unplugged or the USB controller is wedged (see **Dongle recovery**).
- `active: true` → something is already using the dongle; stop it first.
---
API summary
Every endpoint, with its request and response shape: [FSD Appendix D.10](../../../docs/Harness-FSD.md#d10-sdr-receiver).
Only the choice between them is this skill's: `capture` decodes, `analyze` gives raw pulse timing when nothing decodes, `power` measures a level, and `acquire` runs all three in sequence when you do not yet know what is on the air.
Common body fields: `freq_hz` (default 433.92 MHz), `duration_s`, `gain` (number dB, or omit for AGC), `sample_rate` (default 250 kHz — keep low, it's a Pi Zero 2 W), `flex` (an `-X` spec).
**A dongle plugged in after boot is picked up automatically** — status re-probes while the SDR is idle, so there is no need to restart the portal. If `device` stays `false`, the dongle really is absent or not enumerating.
**Pin `gain` on anything you will compare.** On AGC the tuner rescales from whatever it saw recently, so the same quiet band reads tens of dB apart between calls and a strong carrier compresses instead of standing clear. This bites hardest on `power`, where the whole point is comparing numbers.
**Is the dongle working at all?** `pytest pytest/ -k wt1909` transmits on 86.784 MHz (5th harmonic = 433.92 MHz) and requires the dongle to see a ≥15 dB lift, then a drop. It needs no DUT and no operator. Run it before debugging a "nothing decodes" complaint — it separates a dead receive path from a signal problem in one step.
---
The two big signal traps (learned the hard way)
1. **AGC saturates a too-close source.** With the tuner's default auto-gain, a strong near-field transmitter rails the input to full scale and fills an OOK signal's off-gaps — so a crisp remote reads as a continuous / misdetected **FSK** carrier and slices to all-zero codewords. Fix: **more distance** or a **fixed `gain`** (e.g. 33–40 dB). The tell is RSSI pegged near 0 dB regardless of distance, and `rtl_433` reporting FSK for a known-OOK remote.
2. **Decode mode is empty for unknown remotes.** Plain `decode` only emits for a *known* protocol. An unrecognised remote produces nothing — and no RSSI. Use `analyze` (or the live console, which runs `-A` in every mode) to see the signal regardless of decode. **Signal presence ≠ decodability.**
---
Live console (`/api/sdr/live/*`)
A persistent `rtl_433` whose merged output a reader thread fans into a sequence-numbered ring buffer the browser fast-polls (~500 ms; nothing dropped). `-A` runs in **every** mode, so the RSSI meter shows every burst's strength even when it doesn't decode.
`POST /api/sdr/live/start` body:
{"freqs": [433920000], "mode": "decode",
"gain": 33.8, "sample_rate": 250000, "squelch": false,
"hop_interval": 5, "flex": "n=r,m=OOK_PWM,s=340,l=2068,r=13936", "isolate": false}- `freqs`: one = locked, several = **hop** (`-H hop_interval`). Bands: 433.92 /
315 / 868. Hop to *find*, then lock to a single freq to *work* (hopping listens to each band ~1/3 of the time and misses momentary presses).
- `mode`: `decode` (all built-ins) · `flex` (`-X`, `isolate:true` adds `-R 0`) ·
`analyze` (`-A` only).
- `gain`: omit for AGC; a number sets a fixed tuner gain.
- Poll `GET /api/sdr/live?since=<seq>`; each entry is `{seq, line, event}` where
`event` is the parsed JSON (or a synthetic `{rssi,snr,noise,analyzer:true}` from an analyzer `RSSI:` line). RSSI is **burst-driven** — it updates on a press, not continuously.
---
AI Sherlock — reverse-engineer an unknown remote
The record→analyze flow the console exposes as one toggle button:
1. **Start** (`/api/sdr/log/start`) — record the live stream (analyze+AGC). 2. Operator presses each key a few times. 3. **Stop** (`/api/sdr/log/stop`) — freeze the log. 4. `GET /api/sdr/log` → an assistant reads the bursts and derives: modulation/timing, the **constant preamble/device-ID**, and the **per-key varying field**, plus a decoder spec and button→code map.
`start_log` **clears** the previous recording (single in-memory buffer, lost on restart) — it's a record-analyze-now loop, not persistent storage.
---
Device database (make a
Other skills on embedded-ai-harness.
- /build
Phase 3 of AI Closed-Loop Programming — the Build phase, and the driver of the whole loop: locate the project on the chain, name the next act, design and declare tests, dispatch code/flash/verify, correct until the tests run clean. Owns the test plan, test design, audit,
Open skill - /commission
Phase 2 of AI Closed-Loop Programming — Commissioning: prove the project's OWN never-seen-working parts (its board, its wiring, its peers/simulators), so that a failing test means the code and not the setup. The workbench itself is never commissioned by a project — its quality
Open skill - /define
Phase 0 of AI Closed-Loop Programming — Definition: engineers the WHAT the loop converges on. Writes and evolves the FSD — atomic, falsifiable, provenance-tagged requirements each carrying its verification contract — plus architecture, data model, interface definitions, state
Open skill - /esp-idf-handling
Complete ESP-IDF lifecycle: project setup, build, flash, monitor, and OTA. Automatically detects whether a workbench is available or the device is connected locally via USB. Covers sdkconfig, partition tables, esptool, RFC2217 remote flashing, GPIO download mode, OTA updates,
Open skill - /esp-pio-handling
PlatformIO lifecycle for ESP32 firmware: platformio.ini, environment selection, build, upload and serial monitor, on local USB or through the workbench. Covers what differs from ESP-IDF — the .pio/build layout, the boot_app0 image an Arduino-framework build needs, and RFC2217
Open skill - /grill-me
Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
Open skill

