diff --git a/CHANGELOG.md b/CHANGELOG.md index 0975eb8..b1c4733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ and [`docs/requirements/system-requirements.md`](docs/requirements/system-requir ### Added +- **DTMF keypad for FM.** A **DTMF** button on the FM panel opens a keypad + (0-9, A-D, *, #) that sends tones to the radio - for repeater and link + control that is otherwise out of reach over a remote link. + +### Added + - **On-screen macro buttons.** A new **Fn -> MACROS** tab runs the same macros as the K-Pod F1-F8 switches, so you get one-tap CAT macros with or without a K-Pod. Assign them under Settings; a macro that transmits is arm-gated just diff --git a/app/src/main.rs b/app/src/main.rs index ec0336f..d05ffac 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -151,6 +151,8 @@ struct App { // keeps its own bank. memories: Vec, memories_open: bool, + /// DTMF keypad popup open (FR-FM-02). + dtmf_open: bool, memory_name: String, // Peer cache + settings dialog (FR-CFG-04, FR-UI-23). peers: k4_config::PeerCache, @@ -616,6 +618,9 @@ enum Message { /// Run an assigned macro from the on-screen MACROS tab (FR-MACRO-01): send /// its CAT string, gated by the TX arm exactly as the K-Pod press is. RunMacro(usize), + /// DTMF keypad (FR-FM-02): open/close, and send one digit. + ToggleDtmf, + DtmfDigit(char), /// Edit a K-Pod slot's free-form CAT macro string (slot index, text). KpodButtonCatChanged(usize, String), /// Apply a preset (by label) to a K-Pod slot, filling its label + CAT. @@ -929,6 +934,7 @@ impl App { seeded: false, memories: prefs.memories.clone(), memories_open: false, + dtmf_open: false, memory_name: String::new(), peers, settings_open: false, @@ -2021,6 +2027,14 @@ impl App { self.kpod_buttons = k4_config::default_kpod_buttons(); self.push_kpod_buttons(); } + Message::ToggleDtmf => self.dtmf_open = !self.dtmf_open, + Message::DtmfDigit(d) => { + // FM-only, SET-only. The encoder refuses a non-DTMF char, so a + // stray key sends nothing. + if let Some(cmd) = k4_protocol::cat::send_dtmf(d) { + self.send(WorkerCmd::Cat(cmd)); + } + } Message::RunMacro(idx) => { // Same path a K-Pod press takes: the raw CAT string through the // arm-gated seam, so a macro that keys is refused (and flashes @@ -2084,6 +2098,10 @@ impl App { self.memories_open = false; return Task::none(); } + if self.dtmf_open { + self.dtmf_open = false; + return Task::none(); + } if self.settings_open { self.settings_open = false; return Task::none(); @@ -3844,6 +3862,20 @@ impl App { .color(rxv), ) .push(step("+", 1)) + // DTMF keypad (FR-FM-02): a compact button opening a 4×4 keypad + // popup, so the keys do not crowd this row. FM only, which this + // panel already is. + .push(Space::with_width(Length::Fixed(10.0))) + .push( + Button::new(Text::new("DTMF").size(11)) + .style(btn_style(if self.dtmf_open { + BtnKind::Active + } else { + BtnKind::Plain + })) + .padding([4, 8]) + .on_press(Message::ToggleDtmf), + ) .into() } @@ -6972,6 +7004,8 @@ impl App { stack![content, self.rx_popup_overlay(p)].into() } else if self.memories_open { stack![content, self.memories_overlay()].into() + } else if self.dtmf_open { + stack![content, self.dtmf_overlay()].into() } else if self.settings_open { stack![content, settings_card].into() } else if self.about_open { @@ -7739,6 +7773,53 @@ impl App { /// Client-side, because the radio's own memory-channel command (`MC`) is /// "[Pending] TBD" in the Programmer's Reference — there is no documented /// way to reach the K4's memories over CAT. + /// The DTMF keypad popup (FR-FM-02): a 4×4 grid of the standard telephone + /// layout plus A–D and `*`/`#`, each key sending one `DM` digit. FM only. + /// Sending, not storing — the 6 stored-sequence memories the gap analysis + /// mentions are a later addition, and the 1750 Hz burst has no documented + /// CAT command, so neither is here. + fn dtmf_overlay(&self) -> Element<'_, Message> { + let dim = role_color(ui::ColorRole::Inactive); + let key = |d: char| -> Element { + Button::new(Text::new(d.to_string()).size(16).center()) + .style(btn_style(BtnKind::Plain)) + .padding([8, 0]) + .width(Length::Fixed(52.0)) + .on_press(Message::DtmfDigit(d)) + .into() + }; + let mut grid = Column::new().spacing(6); + for r in 0..4 { + let mut row = Row::new().spacing(6); + for c in 0..4 { + row = row.push(key(k4_protocol::cat::DTMF_DIGITS[r * 4 + c])); + } + grid = grid.push(row); + } + let card = Container::new( + Column::new() + .spacing(10) + .push( + Row::new() + .spacing(12) + .align_y(Alignment::Center) + .push(Text::new("DTMF").size(12).color(dim)) + .push(horizontal_space()) + .push(small_btn("Close", Message::ToggleDtmf)), + ) + .push( + Text::new("Each key sends a DTMF tone (FM only).") + .size(11) + .color(dim), + ) + .push(grid), + ) + .style(panel_style) + .padding(18) + .width(Length::Fixed(260.0)); + modal_scrim(card.into()) + } + fn memories_overlay(&self) -> Element<'_, Message> { let dim = role_color(ui::ColorRole::Inactive); let mut list = Column::new().spacing(4); diff --git a/crates/k4-protocol/src/cat.rs b/crates/k4-protocol/src/cat.rs index ff856f7..5fe32b0 100644 --- a/crates/k4-protocol/src/cat.rs +++ b/crates/k4-protocol/src/cat.rs @@ -602,6 +602,21 @@ pub fn set_pl_tone(index: u8, on: bool) -> String { format!("PL{:02}{};", index.clamp(1, 50), on as u8) } +/// The valid DTMF digits (`DM`), in keypad order (`FR-FM-02`). +pub const DTMF_DIGITS: [char; 16] = [ + '1', '2', '3', 'A', '4', '5', '6', 'B', '7', '8', '9', 'C', '*', '0', '#', 'D', +]; + +/// Send one DTMF digit (`DM`, FM mode only): `0`–`9`, `A`–`D`, `*`, `#`. +/// +/// Returns `None` for anything not a DTMF digit rather than sending a malformed +/// command. SET-only on the radio (no read-back). +/// +/// trace: FR-FM-02 +pub fn send_dtmf(digit: char) -> Option { + DTMF_DIGITS.contains(&digit).then(|| format!("DM{digit};")) +} + /// DVR voice-message playback (`PB`): message 1–8, or 0 to cancel play/record. /// /// trace: FR-DVR-01 diff --git a/crates/k4-protocol/tests/cat.rs b/crates/k4-protocol/tests/cat.rs index a00bd71..92352ad 100644 --- a/crates/k4-protocol/tests/cat.rs +++ b/crates/k4-protocol/tests/cat.rs @@ -718,3 +718,22 @@ fn fr_data_02_data_rate_encodes() { "clamped to the 1-bit field" ); } + +/// DTMF digits encode as `DM;`, and non-digits are refused. +/// trace: FR-FM-02 +#[test] +fn fr_fm_02_dtmf_encodes_valid_digits_only() { + use k4_protocol::cat::{send_dtmf, DTMF_DIGITS}; + assert_eq!(send_dtmf('5'), Some("DM5;".to_string())); + assert_eq!(send_dtmf('A'), Some("DMA;".to_string())); + assert_eq!(send_dtmf('*'), Some("DM*;".to_string())); + assert_eq!(send_dtmf('#'), Some("DM#;".to_string())); + // Every keypad digit encodes. + for d in DTMF_DIGITS { + assert!(send_dtmf(d).is_some(), "{d} must be a valid DTMF digit"); + } + // Non-DTMF characters are refused, not sent malformed. + assert_eq!(send_dtmf('E'), None); + assert_eq!(send_dtmf(';'), None); + assert_eq!(send_dtmf(' '), None); +} diff --git a/docs/requirements/system-requirements.md b/docs/requirements/system-requirements.md index 61c888b..0c84750 100644 --- a/docs/requirements/system-requirements.md +++ b/docs/requirements/system-requirements.md @@ -1,7 +1,7 @@ --- title: "System Requirements Specification" status: Draft -version: "0.47" +version: "0.48" updated: 2026-07-22 version: "0.40" updated: 2026-07-21 @@ -282,6 +282,7 @@ syntax per the Programmer's Reference D12, cross-checked vs QK4 (`R-EXT-03`).* | `FR-DATA-02` | **select the DATA rate** for the active receiver (`DR`/`DR$`) beside the DATA sub-mode strip: 45 / 75 baud in AFSK-A and FSK-D, BPSK31 / BPSK63 in PSK-D. The rate has no meaning in DATA-A, so the control is hidden there; the label follows the sub-mode, since the single `DR` bit means different rates in different sub-modes. | STK-03 | S | T | `set_data_rate(sub, r)` encodes `DR[$]r` clamped to one bit; `DR`/`DR$` parse per receiver; `data_rate_labels` returns the sub-mode's pair or `None` for DATA-A. | | `FR-ANT-02` | **show the operator's own antenna names** (`ACN`) on the TX-antenna control instead of a bare number: `AN` 1–3 maps to `ACN` slots 1–3, so a named antenna reads e.g. `DIPOLE`, an unnamed one `ANT 1`. Read-only display; the name is set at the radio. | STK-03 | S | T | `ACN` parses per slot (1–5), the clear form (`ACNn~`) resets to unnamed; `tx_antenna_label` returns the custom name alone (no prefix, so a 6-char name does not wrap the fixed switch cell) or the default `ANT n`. | | `FR-MACRO-01` | provide **on-screen quick-access macro buttons** (Fn → MACROS) that run the same **K-Pod function-switch** macros (`FR-KPOD-06`), so a station without a K-Pod still gets one-tap CAT macros. Only assigned slots (non-empty CAT) get a button, labelled with the operator's own name or an `Ft/h` fallback. Each macro is sent through the **arm-gated seam**, so one that keys is refused (and flashes ARM TX) while disarmed, exactly as the physical switch is. | STK-03 | S | T | `macro_label` returns the display label or `None` for an unassigned slot; the MACROS tab renders one button per assigned slot; a press sends the slot's CAT via the gated path. | +| `FR-FM-02` | provide a **DTMF keypad** for FM mode (`DM`): a 4×4 popup of `0`–`9`, `A`–`D`, `*`, `#`, each key sending one DTMF tone, for remote repeater/link control that is otherwise impossible over the link. Opened from the FM panel. | STK-03 | S | T | `send_dtmf(digit)` encodes `DM;` for a valid digit and returns `None` otherwise; the keypad sends via the standard command path. | | `FR-SCAN-01` | **start/stop memory scan** (`SW149`) and display scan-in-progress from the `IF` `s` flag. | STK-02 | C | T | The `IF` `s` field (index 29) sets `scanning`; the SCAN control emits `SW149;` and lights while scanning. | | `FR-VOX-01` | control **VOX** on/off per transmit mode (PRG `VX`). | STK-06 | C | T | `set_vox(mode,on)` encodes `VX<0/1>;`. | | `FR-VOX-02` | adjust **VOX gain** (`VG`) and **anti-VOX** (`VI`) levels. | STK-03 | C | T | `set_vox_gain('V',20)`=`VGV020;`, `set_antivox(15)`=`VI015;`. | @@ -376,3 +377,4 @@ syntax per the Programmer's Reference D12, cross-checked vs QK4 (`R-EXT-03`).* | 2026-07-25 | 0.45 | DC0SK | Added FR-DATA-02 (DATA rate select `DR`/`DR$`), a Med backlog item and a clean radio-side control — unlike the audio-character item (`MX`/`BL`/`FX`/`AL`) next to it in the gap analysis, which was **left unbuilt** because it entangles with the unresolved remote-stream question (the `AG` lesson: the K4 streams RX audio at a fixed level regardless of the radio's own AF/mix, so radio-side audio routing may not reach the remote listener). `DR` has no such ambiguity: it sets the demodulator rate. The one subtlety is that the single rate *bit* means different things per sub-mode — 45/75 baud in AFSK-A/FSK-D, BPSK31/63 in PSK-D, nothing in DATA-A — so the label is a pure function of the sub-mode (`data_rate_labels`), and the control is hidden where it does not apply. Mirrors the existing `DT`/`DT$` sub-mode plumbing. | | 2026-07-25 | 0.46 | DC0SK | Added FR-ANT-02 (show `ACN` antenna names), scoped to the **TX antenna** — the one antenna whose `ACN` slot mapping D12 documents unambiguously (`AN` 1–3 ↔ `ACN` 1–3; `ACN1DIPOLE` names ANT1). The RX antennas keep their fixed names (Off/RX2/=TX/XVTR/RX1/ATU1–3), whose mapping to `ACN` slots 4–5 is not documented and would be a guess; the `ACT` TX-mask rotation is already handled radio-side when the switch is tapped. Read-only: names are set at the radio. One layout point caught on screen: the name was first shown as `ANT: `, but a 6-character name plus the prefix **wrapped** the fixed 92 px switch cell to two lines, making it taller than its neighbours — a vertical FR-UI-STABLE-01 violation — so the prefix was dropped and the name shown alone, which fits even for all-wide-character names. | | 2026-07-25 | 0.47 | DC0SK | Added FR-MACRO-01 (on-screen macro buttons), a clean reuse of the existing K-Pod macro table (`FR-KPOD-06`) — the 16-slot label+CAT assignments already live in the config and are **not** feature-gated, so the on-screen buttons work whether or not a K-Pod is attached. Put on a new Fn → MACROS tab, where there is no main-view space pressure. The safety story is free: a macro is sent through `SendRawCat` → `Session::send`, the same seam that gates the K-Pod press, so a macro containing `TX;` is refused while disarmed and flashes ARM TX (FR-TX-SAFE-06) with no extra code. Note also the case-insensitive gate (FR-TX-SAFE-03) matters here — a hand-typed macro in lower case is still gated. Investigated FR-MTR-05 on the way and found its power/SWR readout **already delivered** by FR-MTR-03 (the TX meter draws `nnn W` and SWR numerically); its `V`/`I` part needs `SI`, whose response format D12 marks 'documented in a future revision', so it is not buildable now. | +| 2026-07-25 | 0.48 | DC0SK | Added FR-FM-02 as a **DTMF keypad** (`DM`) — the part of the gap-analysis item that is both buildable and useful remotely: sending DTMF for repeater/link control cannot be done any other way over the link. A 4×4 popup opened from the FM panel, one `DM;` per key. **Scoped down from the gap analysis on purpose:** the '6 stored DTMF sequences' are config work deferred to a follow-up, and the **1750 Hz tone burst has no documented CAT command** in D12 (searched), so it is not buildable now rather than guessed at — the `RO`/`RA` lesson. `send_dtmf` refuses a non-DTMF character rather than emitting a malformed `DM`. | diff --git a/docs/test/coverage.generated.md b/docs/test/coverage.generated.md index 397dfbc..fecb01c 100644 --- a/docs/test/coverage.generated.md +++ b/docs/test/coverage.generated.md @@ -57,6 +57,7 @@ Legend: ✅ test-traced · 🟡 waived (see r3-waivers.md) · ⚪ not test-requi | `FR-FIL-02` | C | T | ✅ | | `FR-FIL-03` | C | D | ⚪ | | `FR-FM-01` | C | T | ✅ | +| `FR-FM-02` | S | T | ✅ | | `FR-KEY-01` | S | T | ✅ | | `FR-KEY-02` | C | T | ✅ | | `FR-KPOD-01` | S | T | ✅ | diff --git a/docs/test/test-strategy.md b/docs/test/test-strategy.md index fafa391..2f7c39f 100644 --- a/docs/test/test-strategy.md +++ b/docs/test/test-strategy.md @@ -1,7 +1,7 @@ --- title: "Test Strategy & Traceability" status: Draft -version: "3.10" +version: "3.8" updated: 2026-07-22 authors: - Simon Keimer (DC0SK) @@ -421,10 +421,11 @@ FR-SES-MULTI, FR-DIAG-02, etc. — get `TC` IDs when promoted to `Approved`.)* | 2026-07-22 | 2.8 | DC0SK | **FR-MEM-01 — frequency memories**, the last High item in the backlog and genuinely greenfield. The bank is the **app's own, not the radio's**: `MC` (memory channel) is `[Pending] TBD` in D12, so there is no documented way to reach the K4's memories, and an undocumented guess here would write over channels the operator relies on. Stored entries live in the preferences file. Logic kept in `ui.rs` and tested without a window: duplicate refusal (same frequency *and* mode — the easy accident is storing the VFO twice; the same net on CW and on SSB is a real second entry, so that is kept), a 64-entry bound that **refuses rather than evicting**, stale-index-safe delete, and labels for unnamed entries. **A verification limit, stated rather than papered over:** the intended gesture is tapping the MHz digits, as the radio does — but when no frequency has been reported the readout is plain text with **no digit hit-areas at all**, so the gesture is untestable while disconnected *and the feature would have been unreachable there*. That is what the `MEM` button fixes; it also answers the discoverability problem, since nobody guesses that tapping a digit opens a memory bank. Screenshot-verified open, and the store button — which silently did nothing without a frequency — now reads *no frequency yet — connect first*. Store and recall against a live VFO remain **unverified**: they need a radio. 296 tests. | | 2026-07-22 | 2.9 | DC0SK | **FR-MEM-01 verified end-to-end without a radio**, closing the gap 1.93 left open. The route was already in the repo and unused for this purpose: `--demo` seeds a snapshot with VFO A on 14.074 USB, which is all the memory bank needs — it stores what the *app* believes the VFO is. Driven under an isolated `XDG_CONFIG_HOME` so the operator's real config was never touched. Confirmed: the **MHz-digit gesture opens the window** (the thing that could not be tested at all while disconnected); store writes the row *and* the config file; **storing the same VFO twice is refused** (two further clicks, still one row, still one `[[prefs.memories]]`); recall closes the window; delete removes the row and empties the file; a named entry reads `20m FT8 — 14.0740 USB` and clears the name field; and the entry **survives a restart**, which tests the load path that saving alone does not. Only the CAT emission on recall remains radio-only. **Two tooling traps worth keeping.** (a) An earlier miss on the MHz digit was not a bug — the click landed on a **dot separator**, which is not a hit area; a sweep across x found the live digit. Assuming the feature was broken would have been wrong. (b) `pkill -f "target/release/k4remote"` **kills the shell running the command**, because that pattern matches the shell's own command line — the tool call died silently, with no output, and looked like an app crash. `pkill -x k4remote` matches the process name only. 296 tests. | | 2026-07-22 | 2.9 | DC0SK | **FR-TX-SAFE-06 — ARM TX flashes when it refuses.** Raised by DC0SK: pressing a transmit control with the arm off gave no visible answer. The interlock was working; it just said so in the diagnostics console, which is a different window and usually closed — so it behaved correctly and looked broken, which is the state most likely to make an operator hammer the control or hunt a fault that is not there. **The flash already existed** and was wired at exactly two call sites, the PTT *hotkey* and tune. Not the PTT *button*, not XMIT or the other switch emulations, not DVR playback, text send, or raw console commands. That is the same per-call-site pattern that let the switch emulations transmit while disarmed in 0.4.0 — the arm gate was moved to the seam then, and the *feedback* for it was not. Now driven by the refusal itself: the gate raises a counter, the UI flashes on any change, so every route is covered by construction rather than by remembering to add one. **A silent path found on the way:** `WorkerCmd::Key` discarded `begin_tx`'s result with `let _ =`, and `begin_tx` signals refusal as `Ok(false)`, not an error. So pressing the **PTT button** with the arm off did nothing whatsoever — no flash, no log line, nothing. Only the hotkey had feedback, which is exactly why the report described the wanted behaviour as "like PTT". The blink formula moved out of an inline expression in the view into a tested helper; it now starts lit so the refusal registers at once and ends dark so the control settles rather than snapping out of a lit phase. 302 tests. | -| 2026-07-25 | 3.0 | DC0SK | Release **v0.7.0**. Minor: two new features (frequency memories, AF recorder) and a TX-TEST readback, plus behavioural change across the RX/TX controls. Highlights, nearly all found by DC0SK operating a live K4: the slider read-back fight (values snapping backwards mid-drag; APF taking ~2 s) traced to an unconditional resync that had been worked around twice before and never generalised; a **lowercase bypass of the TX-arm interlock** — the gate matched upper-case mnemonics only, so `tx;` sailed through — found because a `DAMP1;` report exposed the `DA` gap first; and **ARM TX flashing on any refusal**, which surfaced that the PTT *button* was completely silent when disarmed while only the hotkey had feedback. Also: the digital-audio commands brought behind the arm with an auto-repeat-aware emergency stop, the no-bouncing-wireframe sweep continued (and a width estimate that had been wrapping `SHIFT` for months, in the repo's own screenshots, fixed), the NB chip made to fit its box, two rows of vertical space reclaimed, and a DATA-mode frame-height regression from that layout work fixed before the tag. The release process itself carried a lesson worth recording: two builds this cycle were handed over missing a feature — once an unmerged branch, once a merge loop whose second `git merge` silently no-op'd on an unresolved index — so a build is now checked for a feature-unique string in the compiled binary before handoff. Version bumped in Cargo.toml (workspace), lockfile, README, user manual. 314 tests. | -| 2026-07-25 | 3.1 | DC0SK | **TX TEST now flashes, distinct from a real transmit** (FR-TX-TUNE-01), the item v0.7.0 left explicitly unfinished. v0.7.0 made the TEST *button* light; the transmit *indicator* still showed the same steady red `● TX` whether RF was leaving the antenna or the radio was in test mode keying with no power. Those are opposite facts and must not look alike. `tx_indicator` is a pure three-state helper — Idle / OnAir / Test{lit} — with **test taking precedence over on-air**, because keying while in test mode is still no-power and "TEST" is the fact that matters; the flash is driven off the existing 100 ms tick (`flash_phase`, ~500 ms) so no new state was added. The indicator reads `TEST` in amber, alternating with an outlined resting state, never the red reserved for real RF. Verified on screen by seeding test mode in `--demo` and capturing a burst: the label alternates lit/dim across frames. The failure this closes is the dangerous one — transmitting into silence unaware, or the inverse, reading a real transmit as test. 315 tests. | +| 2026-07-25 | 3.1 | DC0SK | Release **v0.7.0**. Minor: two new features (frequency memories, AF recorder) and a TX-TEST readback, plus behavioural change across the RX/TX controls. Highlights, nearly all found by DC0SK operating a live K4: the slider read-back fight (values snapping backwards mid-drag; APF taking ~2 s) traced to an unconditional resync that had been worked around twice before and never generalised; a **lowercase bypass of the TX-arm interlock** — the gate matched upper-case mnemonics only, so `tx;` sailed through — found because a `DAMP1;` report exposed the `DA` gap first; and **ARM TX flashing on any refusal**, which surfaced that the PTT *button* was completely silent when disarmed while only the hotkey had feedback. Also: the digital-audio commands brought behind the arm with an auto-repeat-aware emergency stop, the no-bouncing-wireframe sweep continued (and a width estimate that had been wrapping `SHIFT` for months, in the repo's own screenshots, fixed), the NB chip made to fit its box, two rows of vertical space reclaimed, and a DATA-mode frame-height regression from that layout work fixed before the tag. The release process itself carried a lesson worth recording: two builds this cycle were handed over missing a feature — once an unmerged branch, once a merge loop whose second `git merge` silently no-op'd on an unresolved index — so a build is now checked for a feature-unique string in the compiled binary before handoff. Version bumped in Cargo.toml (workspace), lockfile, README, user manual. 314 tests. | +| 2026-07-25 | 3.2 | DC0SK | **TX TEST now flashes, distinct from a real transmit** (FR-TX-TUNE-01), the item v0.7.0 left explicitly unfinished. v0.7.0 made the TEST *button* light; the transmit *indicator* still showed the same steady red `● TX` whether RF was leaving the antenna or the radio was in test mode keying with no power. Those are opposite facts and must not look alike. `tx_indicator` is a pure three-state helper — Idle / OnAir / Test{lit} — with **test taking precedence over on-air**, because keying while in test mode is still no-power and "TEST" is the fact that matters; the flash is driven off the existing 100 ms tick (`flash_phase`, ~500 ms) so no new state was added. The indicator reads `TEST` in amber, alternating with an outlined resting state, never the red reserved for real RF. Verified on screen by seeding test mode in `--demo` and capturing a burst: the label alternates lit/dim across frames. The failure this closes is the dangerous one — transmitting into silence unaware, or the inverse, reading a real transmit as test. 315 tests. | | 2026-07-25 | 3.3 | DC0SK | **FR-VFO-LOCK-01 — a locked VFO refuses the app's tuning**, a Med backlog item, built read-only. The app now reads `LK` (VFO A) / `LK$` (VFO B) and refuses its own digit, click-QSY and wheel gestures on a locked VFO, showing a LOCK badge that brightens on a refused gesture. Two deliberate choices. **Read-only**: the requirement is read-back + refusal, not setting the lock, and D12 documents only `LK$` (the `$` sub-VFO form) — encoding a `set-lock` blind risks the `RO`/`RA` wire-format class of bug, so it is left for a hardware-verified follow-up. **Refusal is not silent**: the badge brightens, applying the PTT-button lesson where a disarmed press did nothing at all. Unknown lock state is permissive so a fresh connection is not blocked before the radio has reported. The `$`-before-bare longest-prefix-first parse and the toggle-echo-preserves-state handling both follow patterns already used for `MD$`/`TS`. Verified: state parsing and the pure `tuning_allowed` helper by test; the LOCK badge on screen by seeding a locked VFO in `--demo`. 316 tests. | | 2026-07-25 | 3.4 | DC0SK | **FR-DATA-02 — DATA rate select** (`DR`/`DR$`), continuing the backlog. Picked over the higher-listed audio-character item deliberately: `MX`/`BL`/`FX`/`AL` all touch the **remote-stream question** this session already got burned on with `AG` — whether a radio-side audio setting reaches the remote listener at all — so building them blind would repeat that mistake, and they are left hardware-blocked. `DR` is unambiguous: it sets the demodulator rate. The design point worth keeping is that one wire *bit* carries different meanings per sub-mode (45/75 baud vs BPSK31/63), so the label is computed from the sub-mode (`data_rate_labels`) rather than the bit, and the control is hidden in DATA-A where there is no rate. Verified: encoder and per-receiver parse by test, the label-per-sub-mode logic by test, and the buttons on screen by seeding FSK-D in `--demo` (`45 Bd`/`75 Bd`, `75 Bd` lit). 320 tests. | -| 2026-07-25 | 3.6 | DC0SK | **FR-ANT-02 — antenna names** (`ACN`), continuing the clean-effect backlog picks. Scoped to the **TX antenna**, the only one with a documented `ACN`-slot mapping (`AN` 1–3 ↔ `ACN` 1–3); the RX antennas' fixed names stay, since their mapping to slots 4–5 is undocumented and guessing it is the `RO`/`RA` trap. Read-only. **A width bug caught on screen and fixed before commit:** the first cut read `ANT: DIPOLE`, and a 6-character name plus the `ANT: ` prefix wrapped the fixed 92 px switch cell to two lines — taller than its neighbours, the vertical form of the wireframe bounce this project keeps hitting. Verified by seeding the worst case (`WWWMMM`, all wide glyphs): it wrapped with the prefix, fits on one line without it. Name shown alone now. Also a process note: two scripted test-insertions this turn silently no-op'd because they anchored on tests from *unmerged* branches not present here — caught by grepping for the test after, added against a real anchor. 319 tests. | -| 2026-07-25 | 3.7 | DC0SK | **FR-MACRO-01 — on-screen macro buttons**, reusing the K-Pod macro table on a new Fn → MACROS tab. Deliberate reuse: the 16-slot label+CAT table is already in config and not feature-gated, so the buttons work without a K-Pod, and because a press goes through the same `Session::send` seam the physical switch uses, the arm gate and the refusal flash (FR-TX-SAFE-06) — and the lower-case gate (FR-TX-SAFE-03) for hand-typed macros — all apply for free. Verified: the label/assignment logic (`macro_label`) by test, and the MACROS tab on screen in `--demo`, showing the 12 seeded Elecraft sample macros wrapping to a second row at eight. **A backlog finding recorded rather than built:** FR-MTR-05's power/SWR readout is already shipped (FR-MTR-03 draws `nnn W` + SWR on the TX meter), and its V/I half depends on `SI`, whose format D12 leaves 'for a future revision' — so like FR-VFO-STEP-01 it is partly already-met and partly not-yet-buildable. 322 tests. | -| 2026-07-25 | 3.10 | DC0SK | **DATA sub-mode (and rate) switching was laggy** — reported by DC0SK on the radio. The same read-back fight the sliders had (1.97): `rx_data_submode`/`rx_data_rate` read straight from the snapshot, so tapping a sub-mode sent `DT` but the button did not light until the radio's echo came back. Fixed the same way — an optimistic `Opt` override per field, set on tap, preferred by the accessor, and reconciled against the radio each tick (confirm-or-expire). The `DR` rate had the identical gap (added in 3.4 without an override); fixed alongside. The general lesson holds: any control that reads the radio's state directly, rather than through a mirror the send updates, will feel laggy — the override is the standing fix and should be the default for a new radio-backed control. | +| 2026-07-25 | 3.5 | DC0SK | **FR-ANT-02 — antenna names** (`ACN`), continuing the clean-effect backlog picks. Scoped to the **TX antenna**, the only one with a documented `ACN`-slot mapping (`AN` 1–3 ↔ `ACN` 1–3); the RX antennas' fixed names stay, since their mapping to slots 4–5 is undocumented and guessing it is the `RO`/`RA` trap. Read-only. **A width bug caught on screen and fixed before commit:** the first cut read `ANT: DIPOLE`, and a 6-character name plus the `ANT: ` prefix wrapped the fixed 92 px switch cell to two lines — taller than its neighbours, the vertical form of the wireframe bounce this project keeps hitting. Verified by seeding the worst case (`WWWMMM`, all wide glyphs): it wrapped with the prefix, fits on one line without it. Name shown alone now. Also a process note: two scripted test-insertions this turn silently no-op'd because they anchored on tests from *unmerged* branches not present here — caught by grepping for the test after, added against a real anchor. 319 tests. | +| 2026-07-25 | 3.6 | DC0SK | **FR-MACRO-01 — on-screen macro buttons**, reusing the K-Pod macro table on a new Fn → MACROS tab. Deliberate reuse: the 16-slot label+CAT table is already in config and not feature-gated, so the buttons work without a K-Pod, and because a press goes through the same `Session::send` seam the physical switch uses, the arm gate and the refusal flash (FR-TX-SAFE-06) — and the lower-case gate (FR-TX-SAFE-03) for hand-typed macros — all apply for free. Verified: the label/assignment logic (`macro_label`) by test, and the MACROS tab on screen in `--demo`, showing the 12 seeded Elecraft sample macros wrapping to a second row at eight. **A backlog finding recorded rather than built:** FR-MTR-05's power/SWR readout is already shipped (FR-MTR-03 draws `nnn W` + SWR on the TX meter), and its V/I half depends on `SI`, whose format D12 leaves 'for a future revision' — so like FR-VFO-STEP-01 it is partly already-met and partly not-yet-buildable. 322 tests. | +| 2026-07-25 | 3.7 | DC0SK | **DATA sub-mode (and rate) switching was laggy** — reported by DC0SK on the radio. The same read-back fight the sliders had (1.97): `rx_data_submode`/`rx_data_rate` read straight from the snapshot, so tapping a sub-mode sent `DT` but the button did not light until the radio's echo came back. Fixed the same way — an optimistic `Opt` override per field, set on tap, preferred by the accessor, and reconciled against the radio each tick (confirm-or-expire). The `DR` rate had the identical gap (added in 3.4 without an override); fixed alongside. The general lesson holds: any control that reads the radio's state directly, rather than through a mirror the send updates, will feel laggy — the override is the standing fix and should be the default for a new radio-backed control. | +| 2026-07-25 | 3.8 | DC0SK | **FR-FM-02 — DTMF keypad** (`DM`), a 4×4 popup from the FM panel sending one tone per key. The one part of the gap-analysis item worth building: DTMF for repeater/link control is impossible remotely otherwise. **Two parts deliberately not built:** the 6 stored sequences (config work, deferred) and the 1750 Hz burst — which has **no documented CAT command** (D12 searched), so it is un-buildable, not merely un-built, and guessing a command is the `RO`/`RA` trap. `send_dtmf` refuses a non-DTMF character rather than emitting a malformed `DM`. Verified: the encoder over every keypad digit by test; the popup on screen by forcing it open in `--demo` (the standard telephone grid). A note on method: several xdotool click attempts missed the FM-row DTMF button by ~40 px, and rather than keep guessing coordinates I confirmed the overlay by forcing its open flag — the wiring mirrors the working memories/about overlays. 324 tests. |