Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
65e6bba
Fixed frequency units by changing dHz to cHz
KI3P Jun 16, 2026
e568d91
Speed up encoder handling and fix spectrum tuning artifacts
KI3P Jun 8, 2026
99e4e19
Add LLM-maintained Phoenix wiki
KI3P Jun 16, 2026
c95f932
Unit and documentation fixes
KI3P Jun 16, 2026
bd3474d
Wiki lint: reconcile dHz→cHz pages after rename merge
KI3P Jun 16, 2026
97d017c
Add rapid-tune audio mute & spectrum freeze option
KI3P Jun 16, 2026
9c8bb16
Add runtime-switchable, persisted sample rate (192 / 176.4 ksps)
KI3P Jul 28, 2026
ad2c3d1
Generate the sample-rate-dependent RX/TX filters at run time
KI3P Jul 29, 2026
159e1a5
Add CAT control of the DSP settings, and a hardware-in-the-loop filte…
KI3P Jul 29, 2026
fe070a0
Stamp BuildInfo.h from a pre-commit hook
KI3P Jul 29, 2026
899f859
Resolve BuildInfo.h merges in favour of whichever side is kept
KI3P Jul 29, 2026
2f650bc
Version the pre-commit hook so every clone gets it
KI3P Jul 29, 2026
942c8e5
Merge fast_encoder into rx-dsp-176k-stage-test for V1.4.0
KI3P Jul 29, 2026
29ca4dd
Bring the wiki current with the V1.4RC merge
KI3P Jul 29, 2026
5f996e5
Add the transmit-filter HIL suite and the bench tool collection
KI3P Jul 30, 2026
57864bb
Accept the optional TX parameter, per the TS-480 spec
KI3P Jul 30, 2026
e1b3c3c
Document the transmit HIL suite and the TS-480 TX finding
KI3P Jul 30, 2026
40635af
Make flash-radio read arduino.json, and fix its upload step
KI3P Jul 30, 2026
b16ec3a
Add DIGITAL mode: USB audio for FT8 and other digital modes
KI3P Jul 31, 2026
9baf83d
Add the transmit and receive bench tests, and ignore their captures
KI3P Jul 31, 2026
f58d443
Model the digital and SSB transmit chains in software
KI3P Jul 31, 2026
31293e6
Bring the README release notes and the wiki current for V1.4.0
KI3P Jul 31, 2026
679466d
Fixed a bug in the display code where the Decoder text in the Status …
KI3P Aug 3, 2026
ab69673
Accept serial time sync when CAT shares the primary Serial port
KI3P Aug 5, 2026
e2f0180
Add cross-platform scripts to set the radio clock from the PC
KI3P Aug 5, 2026
d35a4ff
Fix Set-RadioTime.ps1 .Count error on Windows PowerShell 5.1
dlsolt Aug 7, 2026
03c4535
Merge pull request #32 from dlsolt/fix-radiotime-ps1-strictmode
KI3P Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .claude/skills/filter-hil-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
name: filter-hil-test
description: Verify on real hardware that the receive DSP filters hold their frequencies when the sample rate changes. Drives the radio's I/Q receive inputs with an Analog Discovery 2's W1/W2 in quadrature, reads the demodulated audio on scope Ch1, and sweeps the CW audio filters, the 14 equaliser cells and the SSB filter at both 192 and 176.4 ksps. Use when the user asks to test the filters on hardware, verify sample-rate independence, or check that a filter change works on the real radio.
when_to_use: "test the filters on hardware", "verify sample rate independence", "HIL filter test", "check the filters on the radio", "run the filter sweep", "does the CW filter move when the sample rate changes"
argument-hint: "[--rates 192000,176400] [--tests iq,level,map,ref,cw,eq,ssb,am] [--rate-tol-pct 1.5] [...]"
allowed-tools: Bash(*venv/bin/python *filter_hil_test.py*) Bash(*venv/bin/python *plot_filter_hil.py*) Bash(*venv/bin/python *test_filter_hil.py*) Bash(ls /dev/ttyACM*) Read
user-invocable: true
---

# Filter rate-independence test, hardware in the loop

Runs `code/tools/filter_hil/filter_hil_test.py` to measure where the receive DSP
filters actually sit on the real radio, at every sample rate, and confirm they do
not move.

## Hardware setup

The rig must already be wired:

- **W1 and W2** drive the radio's I and Q receive inputs. Either order is fine —
the suite detects which is which and compensates in software.
- **Scope Ch1** reads the speaker output.
- **AD2 ground** is shared with the radio.
- CAT on `/dev/ttyACM1` (38400), diagnostics on `/dev/ttyACM0` (115200).

If the AD2 or the serial ports are missing the suite exits 2. Report that and
stop — do not retry.

## Preconditions

- **AGC must be off** on the radio. It compresses the amplitude differences the
suite measures, so the filter skirts would read flat. There is no CAT command
for it; the user must set it from the menu. The suite exits 2 if AGC is on.
- The firmware must have the `SR`, `CF`, `EQ` and `FL` CAT commands.
- The user must not touch the front panel during the run — that triggers a
settings save and would persist the test configuration.

## Running it

```bash
/home/oliver/Sync/Ham/T41/Software/Phoenix/code/tools/venv/bin/python \
/home/oliver/Sync/Ham/T41/Software/Phoenix/code/tools/filter_hil/filter_hil_test.py
```

Takes about nine minutes for both rates. Add `-v` for progress. To measure only
part of it, pass `--tests` (from `iq,level,map,ref,cw,eq,ssb,am`) or
`--rates`. `am` is off by default.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | All checks passed |
| 1 | At least one filter is not where it should be |
| 2 | Rig or radio problem: no AD2, no serial, or preflight refused (usually AGC on) |
| 3 | Interrupted; the radio may still be in the test configuration |

## What gets checked

| Group | What it measures | What a failure means |
|---|---|---|
| `rig` | I/Q wiring sense, image rejection, drive level, AGC state | The rig is not connected as documented |
| `mapping` | That an input tone at `Fs/4 + f` produces audio at `f`, at each rate | The firmware did not reconfigure the frequency shift; everything else at that rate is meaningless and is skipped |
| `cw` | The five CW audio filters' -3 dB corners, ripple and stopband | The Chebyshev design is not being regenerated for the rate |
| `eq` | All 14 equaliser cell centres and Q | The cell prototypes are not being re-transformed for the rate |
| `ssb` | The SSB filter edge versus the commanded bandwidth — **the control** | The rig or the analysis is wrong, *not* the firmware |
| `am` | The AM DC blocker corner (only when requested) | The blocker pole is not derived from the rate |

## Reading the result

The primary criterion is **rate invariance**: each frequency must land in the
same place at both sample rates, within 1.5 %. The bug this exists to catch
produces exactly **-8.125 %**, so there is a 5x margin between correct and broken.
When a result matches that figure the report says so explicitly.

View `<prefix>_cw_overlay.png` with `Read` first — both rates should lie on top
of each other and well clear of the dotted legacy-shift line. Then
`<prefix>_summary.png` for the whole picture at a glance.

The SSB filter is the control: it always derived its coefficients from the true
sample rate, so a shift there indicts the measurement rather than the radio.

Equaliser cells 0, 1 and 13 are marked *edge limited* and judged loosely on
absolute accuracy — the first two sit at the SSB low cut and the last in the
decimation skirt, which is meant to scale with the rate. Their rate-invariance
check is still applied at full strength.

## Reporting

Give one line per group with a PASS/FAIL and the headline number, then the
overall verdict. Quote the worst `delta_pct` and say whether it matches the
-8.125 % signature. Mention the Markdown report and PNG paths. If the radio's
state was not fully restored, say so prominently and name the settings that
drifted.

## Guardrails

- **Do not edit the suite in response to a failure.** A failure means the
radio's filters are not where they should be, or the rig is not connected as
documented. `test_filter_hil.py` covers the measurement maths and includes a
check that the comparison fails on a simulated -8.125 % shift — run that if you
suspect the tool rather than the radio.
- Do not widen `--rate-tol-pct` to make a run pass.
- If the run is interrupted, the AWG is silenced automatically but the radio may
be left mid-configuration; re-running the suite restores it.
127 changes: 98 additions & 29 deletions .claude/skills/flash-radio/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,119 @@ description: Compile the PhoenixSketch production firmware and flash it to the T

Compiles `code/src/PhoenixSketch/PhoenixSketch.ino` with `arduino-cli` and uploads the resulting `.hex` to the connected Teensy 4.1.

The build settings come from `code/.vscode/arduino.json` and must stay in sync with it:
## Build settings — read them, do not retype them

| Setting | Value |
| --- | --- |
| Board (FQBN base) | `teensy:avr:teensy41` |
| FQBN options | `usb=serial2,speed=600,opt=o1lto,keys=en-us` |
| Sketch | `src/PhoenixSketch/PhoenixSketch.ino` (relative to `code/`) |
| Output dir | `../ArduinoOutput` (relative to `code/`) |
| Default port | `/dev/ttyACM0` |
`code/.vscode/arduino.json` is the single source of truth. Do **not** hardcode the FQBN
in a command: this file has drifted from copies pasted into skills before (the CPU speed
and optimisation flags changed), and a stale FQBN silently builds a different binary.

Read the values into shell variables first, and use those for every subsequent command:

```bash
cd /home/oliver/Sync/Ham/T41/Software/Phoenix/code
CFG=.vscode/arduino.json
FQBN="$(jq -r '.board + ":" + .configuration' "$CFG")"
SKETCH="$(jq -r .sketch "$CFG")"
OUTDIR="$(jq -r .output "$CFG")"
PORT="$(jq -r .port "$CFG")"
echo "FQBN=$FQBN SKETCH=$SKETCH OUTDIR=$OUTDIR PORT=$PORT"
```

If those values diverge from `code/.vscode/arduino.json`, prefer the file and tell the user.
Echo them so the values used are visible in the transcript. As of 2026-07-30 that yields
`teensy:avr:teensy41:usb=serial2,speed=528,opt=o2lto,keys=en-us`, sketch
`src/PhoenixSketch/PhoenixSketch.ino`, output `../ArduinoOutput`, port `/dev/ttyACM0` —
but read the file, do not trust this paragraph.

`usb=serial2` is Dual Serial: the radio enumerates as two ports. The first
(`/dev/ttyACM0`) is diagnostics and the flashing port; the second (`/dev/ttyACM1`) is CAT
at 38400.

## Steps

1. **Check the board is connected.** Run `arduino-cli board list` and confirm a Teensy is present (it usually appears as `/dev/ttyACM0`, FQBN `teensy:avr:teensy41`). If no Teensy is listed, stop and tell the user to connect/power the radio — do not try to flash a missing device.
1. **Check the board is connected.**
```bash
arduino-cli board list
```
A connected radio shows a `Teensy Ports` row with FQBN `teensy:avr:teensy41`. The
`/dev/ttyACM*` rows show `Unknown` — that is normal, not a problem. If no Teensy row
appears at all, stop and tell the user to connect/power the radio.

2. **Compile.** From the `code/` directory:
2. **Compile and upload in one step.** This is the reliable path — see "Why one step"
below.
```bash
arduino-cli compile \
--fqbn "teensy:avr:teensy41:usb=serial2,speed=600,opt=o1lto,keys=en-us" \
--output-dir ../ArduinoOutput \
src/PhoenixSketch/PhoenixSketch.ino
arduino-cli compile --upload -p "$PORT" \
--fqbn "$FQBN" \
--output-dir "$OUTDIR" \
"$SKETCH"
```
Compiles take a while; run with a generous Bash timeout (e.g. 600000 ms / 10 min). If compilation fails, surface the first error and stop — do not flash a broken build.
Use a generous Bash timeout (600000 ms / 10 min); a cold build takes minutes, a cached
one is quick. If compilation fails, surface the first error and stop — `--upload` will
not flash a broken build, but say so explicitly rather than letting the failure look
like an upload problem.

3. **Flash.** If the port discovered in step 1 differs from `/dev/ttyACM0`, use that one. From the `code/` directory:
3. **Confirm the board came back.** A successful flash reboots the Teensy and the ports
re-enumerate:
```bash
arduino-cli upload \
-p /dev/ttyACM0 \
--fqbn "teensy:avr:teensy41:usb=serial2,speed=600,opt=o1lto,keys=en-us" \
--input-dir ../ArduinoOutput \
src/PhoenixSketch/PhoenixSketch.ino
sleep 4 && ls -l /dev/ttyACM*
```
The Teensy loader may prompt the user to press the physical button on the board — mention this if upload appears to hang.
The timestamps should be seconds old. If the radio is meant to be usable afterwards,
a `printf 'IF;' > /dev/ttyACM1` should answer.

4. **Report.** One or two lines: compile result, upload result, notable warnings. The
tree builds with many pre-existing `-Wunused-*` warnings — do not report those as new.

## Expected output, and what is not an error

- **`Teensy should be selected from "teensy ports" rather than "Serial ports"`** — a
harmless warning printed on every upload. Ignore it.
- **`New upload port: /dev/ttyACM0 (serial)`** — success.
- A successful run ends with the memory-usage table and no `Failed uploading:` line.
Check the exit status rather than eyeballing, since the warning above looks alarming.

## Why one step, and how to re-flash without recompiling

A separate `arduino-cli upload` **fails** on this setup in two different ways, both
verified on 2026-07-30:

| Command | Result |
| --- | --- |
| `upload --input-dir ../ArduinoOutput [sketch]` | `Teensy Loader could not find the file PhoenixSketch.ino` — fails with or without the trailing sketch argument |
| `upload --input-file ../ArduinoOutput/PhoenixSketch.ino.hex` | `Teensy Loader is unable to read your compiled sketch` |
| `upload --input-file /abs/path/.../PhoenixSketch.ino.hex` | **works** |
| `compile --upload -p PORT` | **works** |

The Teensy loader cannot resolve relative paths. So to re-flash an existing build without
recompiling, pass an **absolute** path to the `.hex`:

```bash
arduino-cli upload -p "$PORT" --fqbn "$FQBN" \
--input-file /home/oliver/Sync/Ham/T41/Software/Phoenix/ArduinoOutput/PhoenixSketch.ino.hex
```

Otherwise prefer step 2 — arduino-cli caches the build, so re-running `compile --upload`
against an unchanged tree costs little.

## If the upload hangs or the board does not reboot

4. **Report.** Summarise in 1–2 lines: whether compile succeeded, whether upload succeeded, and any notable warnings.
The Teensy's auto-reboot into the bootloader is unreliable. If the upload sits waiting,
ask the user to **press the PROGRAM button on the Teensy**, then re-run the step-2
command. Do not keep retrying without telling them — nothing will happen until the button
is pressed.

## Args

- `compile-only` — run step 2 only; skip the flash step. Useful when no board is connected or the user just wants to verify the build.
- `flash-only` — skip compile and re-flash whatever is already in `../ArduinoOutput`. Only valid if that directory contains a recent build for this sketch; otherwise fall back to the full compile+flash.
- `compile-only` — run the compile without `--upload`; skip the flash. Useful when no
board is connected or the user just wants to verify the build.
- `flash-only` — skip the compile and re-flash what is already in `$OUTDIR`, using the
absolute-path `--input-file` form above. Only valid if that directory holds a recent
build of this sketch; otherwise fall back to a full compile and say that you did.

## Notes

- Always run from `code/` so the relative paths in `arduino.json` resolve correctly.
- Do **not** invoke this skill to build the unit tests — those use CMake under `code/test/build/` and are a separate workflow.
- The `code/build/` directory is the CMake test build; the firmware artifacts go in `ArduinoOutput/` at the repo root.
- Always run from `code/` so the relative paths in `arduino.json` resolve.
- Do **not** use this skill for the unit tests — those are CMake under `code/test/build/`
and are a separate workflow (`run-tests`).
- `code/build/` is the CMake test build; firmware artifacts go to `ArduinoOutput/` at the
repo root.
- There is no `code/lib/` in this tree. If a future build needs in-tree forked libraries,
add `--libraries lib` — but do not pass it while that directory is absent.
77 changes: 0 additions & 77 deletions .claude/skills/flash/SKILL.md

This file was deleted.

Loading
Loading