/signal-generator
Drive the Embedded AI Harness workbench's RF signal generator over `/api/siggen/*` — continuous carrier, Morse/CW beacon, retune, PE4302 attenuation, and frequency listing. Use this skill whenever the user wants to emit, key, retune, or attenuate an RF signal from the workbench,
$ npx -y skills add SensorsIot/Embedded-AI-Harness --skill signal-generator --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
/signal-generator
Context preview
The summary Claude sees to decide when to auto-load this skill.
Drive the Embedded AI Harness workbench's RF signal generator over `/api/siggen/*` — continuous carrier, Morse/CW beacon, retune, PE4302 attenuation, and frequency listing. Use this skill whenever the user wants to emit, key, retune, or attenuate an RF signal from the workbench,
SKILL.md
signal-generator.SKILL.mdname: signal-generator
description: Drive the Embedded AI Harness testbench's RF signal generator over `/api/siggen/*` — continuous carrier, Morse/CW beacon, retune, PE4302 attenuation, and frequency listing. Use this skill whenever the user wants to emit, key, retune, or attenuate an RF signal from the testbench, even if they only say "CW beacon", "carrier", "Morse", "Si5351", "GPCLK", "PE4302 attenuator", "DF test", "direction finder", or "80m beacon" — those are all this one API. Always check `/api/siggen/status` first to see which backend (Si5351 vs GPCLK fallback) and attenuator are physically present before choosing a backend.
Signal Generator (`/api/siggen/*`)
The testbench Pi has one signal-generator service. Two RF sources sit behind it (Si5351 on I²C, BCM2835 GPCLK on GPIO 5/6) and an optional PE4302 step attenuator can sit in the RF path. The endpoint is the same regardless of which backend is active — `/api/siggen/*` is the only entry point you should reach for.
This skill replaces the legacy `cw-beacon` skill, which only knew about GPCLK and led to wrong frequencies on testbenches that have an Si5351.
---
Always check status first
Before starting a carrier or recommending a backend, **GET `/api/siggen/status`**. The response tells you which hardware is detected:
{
"ok": true, "active": false, "backend": null,
"freq_hz": 0.0, "channel": null, "pin": null,
"atten_db": null, "morse": null,
"hardware": {"si5351": true, "gpclk": true, "pe4302": true}
}Why this matters:
- If `hardware.si5351` is `true`, you can hit any frequency exactly (333 kHz – 112.5 MHz, fractional synthesis). Don't reach for GPCLK.
- If `hardware.si5351` is `false`, you fall back to GPCLK, which only produces PLLD/N integer dividers — ~25–30 kHz frequency steps in the 80m band. Tell the user the actual `freq_hz` returned, not the requested one.
- If `hardware.pe4302` is `false`, calls to `/api/siggen/atten` will 4xx. Don't promise attenuation control.
- If `active` is already `true`, starting a new carrier replaces the old one. If the user just wants to retune, use `/api/siggen/freq` instead so the Morse keyer (if any) keeps running.
Skipping the status check is the most common failure mode of this skill — pick the wrong backend and the carrier ends up tens of kHz off frequency, or you offer attenuation that doesn't exist.
---
API summary
Every endpoint, with its request and response shape: [FSD Appendix D.11](../../../docs/Harness-FSD.md#d11-signal-generator).
`freq` retunes an active carrier without restarting the Morse keyer — use it rather than stop/start when a test must not lose keying state.
`POST /api/siggen/start`
{
"freq_hz": 3500000,
"backend": "auto",
"channel": 0,
"pin": 5,
"atten_db": 0,
"morse": {"message": "VVV DE TEST", "wpm": 15, "repeat": true}
}| Field | Type | Default | Notes | |-------|------|---------|-------| | `freq_hz` | number | required | Si5351 hits exactly; GPCLK snaps to nearest integer divider — read it back from the response | | `backend` | string | `"auto"` | `auto` / `si5351` / `gpclk`. Prefer `auto` unless you have a reason. | | `channel` | int | 0 | Si5351 output (0/1/2). Ignored by GPCLK. | | `pin` | int | 5 | GPCLK pin (5 or 6). Ignored by Si5351. | | `atten_db` | float | — | Initial PE4302 setting (0–31.5). Optional. | | `morse` | object | — | Omit → continuous carrier. Include → keyed beacon. |
`morse` shape: `{"message": str, "wpm": int|float = 15, "repeat": bool = true}`. WPM is PARIS-standard, 1–60.
The response echoes the *actual* state — always trust `freq_hz` from the response, not the request. Example:
{
"ok": true, "active": true, "backend": "si5351",
"freq_hz": 3500000.0000000005, "channel": 0, "pin": null,
"atten_db": 0.0,
"morse": {"message": "VVV DE TEST", "wpm": 15, "repeat": true}
}`POST /api/siggen/stop`
No body. Stops the carrier and disables the output. Idempotent.
`POST /api/siggen/freq`
{"freq_hz": 7100000, "channel": 0}Retunes the active carrier *without* tearing down the Morse keyer. Use this when sweeping or stepping through frequencies during a beacon transmission.
`POST /api/siggen/atten`
{"db": 12.5}Returns 4xx if PE4302 is not present. Range 0–31.5 dB in 0.5 dB steps. Pin sharing note: PE4302's LE line is GPIO 6, which is also GPCLK2 — when GPCLK is active on pin 6, attenuation control is unavailable. `Si5351 + PE4302` and `GPCLK on pin 5 + PE4302` are both safe.
`GET /api/siggen/frequencies?low=&high=&backend=`
Defaults: `low=3_500_000`, `high=4_000_000`, `backend=auto`. GPCLK returns discrete `{divider, freq_hz}` entries; Si5351 returns a single entry reporting the range as continuously tunable.
---
Driver methods (`pytest/testbench_driver.py`)
The Python driver mirrors the API one-for-one. Prefer these over raw curl when writing test scripts:
from testbench_driver import TestbenchDriver
wt = TestbenchDriver("$TESTBENCH_URL")
status = wt.siggen_status() # always first
print(status["hardware"])
wt.siggen_start(freq_hz=3_500_000) # continuous, auto backend
wt.siggen_freq(freq_hz=7_100_000) # retune in place
wt.siggen_atten(db=12.0) # PE4302
wt.siggen_start(freq_hz=3_571_000, # keyed beacon
morse={"message": "VVV DE TEST",
"wpm": 15, "repeat": True})
wt.siggen_stop()---
Recipes
Carrier at a specific frequency
status = wt.siggen_status()
backend = "si5351" if status["hardware"]["si5351"] else "gpclk"
result = wt.siggen_start(freq_hz=3_500_000, backend=backend)
# On GPCLK, result["freq_hz"] may differ from 3_500_000 by ~25 kHz —
# tell the user the actual frequency.
print(f"Actual: {result['freq_hz']/1e6:.6f} MHz")Morse beacon for a DF test (80m band)
wt.siggen_s
Read more
name: signal-generator description: Drive the Embedded AI Harness testbench's RF signal generator over `/api/siggen/*` — continuous carrier, Morse/CW beacon, retune, PE4302 attenuation, and frequency listing. Use this skill whenever the user wants to emit, key, retune, or attenuate an RF signal from the testbench, even if they only say "CW beacon", "carrier", "Morse", "Si5351", "GPCLK", "PE4302 attenuator", "DF test", "direction finder", or "80m beacon" — those are all this one API. Always check `/api/siggen/status` first to see which backend (Si5351 vs GPCLK fallback) and attenuator are physically present before choosing a backend.
Signal Generator (`/api/siggen/*`)
The testbench Pi has one signal-generator service. Two RF sources sit behind it (Si5351 on I²C, BCM2835 GPCLK on GPIO 5/6) and an optional PE4302 step attenuator can sit in the RF path. The endpoint is the same regardless of which backend is active — `/api/siggen/*` is the only entry point you should reach for.
This skill replaces the legacy `cw-beacon` skill, which only knew about GPCLK and led to wrong frequencies on testbenches that have an Si5351.
---
Always check status first
Before starting a carrier or recommending a backend, **GET `/api/siggen/status`**. The response tells you which hardware is detected:
{
"ok": true, "active": false, "backend": null,
"freq_hz": 0.0, "channel": null, "pin": null,
"atten_db": null, "morse": null,
"hardware": {"si5351": true, "gpclk": true, "pe4302": true}
}Why this matters:
- If `hardware.si5351` is `true`, you can hit any frequency exactly (333 kHz – 112.5 MHz, fractional synthesis). Don't reach for GPCLK.
- If `hardware.si5351` is `false`, you fall back to GPCLK, which only produces PLLD/N integer dividers — ~25–30 kHz frequency steps in the 80m band. Tell the user the actual `freq_hz` returned, not the requested one.
- If `hardware.pe4302` is `false`, calls to `/api/siggen/atten` will 4xx. Don't promise attenuation control.
- If `active` is already `true`, starting a new carrier replaces the old one. If the user just wants to retune, use `/api/siggen/freq` instead so the Morse keyer (if any) keeps running.
Skipping the status check is the most common failure mode of this skill — pick the wrong backend and the carrier ends up tens of kHz off frequency, or you offer attenuation that doesn't exist.
---
API summary
Every endpoint, with its request and response shape: [FSD Appendix D.11](../../../docs/Harness-FSD.md#d11-signal-generator).
`freq` retunes an active carrier without restarting the Morse keyer — use it rather than stop/start when a test must not lose keying state.
`POST /api/siggen/start`
{
"freq_hz": 3500000,
"backend": "auto",
"channel": 0,
"pin": 5,
"atten_db": 0,
"morse": {"message": "VVV DE TEST", "wpm": 15, "repeat": true}
}| Field | Type | Default | Notes | |-------|------|---------|-------| | `freq_hz` | number | required | Si5351 hits exactly; GPCLK snaps to nearest integer divider — read it back from the response | | `backend` | string | `"auto"` | `auto` / `si5351` / `gpclk`. Prefer `auto` unless you have a reason. | | `channel` | int | 0 | Si5351 output (0/1/2). Ignored by GPCLK. | | `pin` | int | 5 | GPCLK pin (5 or 6). Ignored by Si5351. | | `atten_db` | float | — | Initial PE4302 setting (0–31.5). Optional. | | `morse` | object | — | Omit → continuous carrier. Include → keyed beacon. |
`morse` shape: `{"message": str, "wpm": int|float = 15, "repeat": bool = true}`. WPM is PARIS-standard, 1–60.
The response echoes the *actual* state — always trust `freq_hz` from the response, not the request. Example:
{
"ok": true, "active": true, "backend": "si5351",
"freq_hz": 3500000.0000000005, "channel": 0, "pin": null,
"atten_db": 0.0,
"morse": {"message": "VVV DE TEST", "wpm": 15, "repeat": true}
}`POST /api/siggen/stop`
No body. Stops the carrier and disables the output. Idempotent.
`POST /api/siggen/freq`
{"freq_hz": 7100000, "channel": 0}Retunes the active carrier *without* tearing down the Morse keyer. Use this when sweeping or stepping through frequencies during a beacon transmission.
`POST /api/siggen/atten`
{"db": 12.5}Returns 4xx if PE4302 is not present. Range 0–31.5 dB in 0.5 dB steps. Pin sharing note: PE4302's LE line is GPIO 6, which is also GPCLK2 — when GPCLK is active on pin 6, attenuation control is unavailable. `Si5351 + PE4302` and `GPCLK on pin 5 + PE4302` are both safe.
`GET /api/siggen/frequencies?low=&high=&backend=`
Defaults: `low=3_500_000`, `high=4_000_000`, `backend=auto`. GPCLK returns discrete `{divider, freq_hz}` entries; Si5351 returns a single entry reporting the range as continuously tunable.
---
Driver methods (`pytest/testbench_driver.py`)
The Python driver mirrors the API one-for-one. Prefer these over raw curl when writing test scripts:
from testbench_driver import TestbenchDriver
wt = TestbenchDriver("$TESTBENCH_URL")
status = wt.siggen_status() # always first
print(status["hardware"])
wt.siggen_start(freq_hz=3_500_000) # continuous, auto backend
wt.siggen_freq(freq_hz=7_100_000) # retune in place
wt.siggen_atten(db=12.0) # PE4302
wt.siggen_start(freq_hz=3_571_000, # keyed beacon
morse={"message": "VVV DE TEST",
"wpm": 15, "repeat": True})
wt.siggen_stop()---
Recipes
Carrier at a specific frequency
status = wt.siggen_status()
backend = "si5351" if status["hardware"]["si5351"] else "gpclk"
result = wt.siggen_start(freq_hz=3_500_000, backend=backend)
# On GPCLK, result["freq_hz"] may differ from 3_500_000 by ~25 kHz —
# tell the user the actual frequency.
print(f"Actual: {result['freq_hz']/1e6:.6f} MHz")Morse beacon for a DF test (80m band)
wt.siggen_s
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

