Skip to content

fix(dvk): import converts to the radio's 24 kHz mono and uses the wiki's two-step upload; slot names keep their spaces. Principle I. - #6247

Merged
jensenpat merged 3 commits into
mainfrom
fix/dvk-wiki-conformance
Oct 7, 2026
Merged

jensenpat merged 3 commits into
mainfrom
fix/dvk-wiki-conformance

Conversation

@ten9876

@ten9876 ten9876 commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes #6244.

This brings the Digital Voice Keyer in line with the SmartSDR API wiki DVK page, as checked on a FLEX-8600 running firmware 4.2.20.41343.

Import WAV now works.

  • Handshake. Upload follows the wiki's two steps: dvk upload id=N (empty reply; it only names the slot), then file upload <size> dvk_recording, whose reply gives the port (falls back to 42607). The old code read a port from the empty first reply, so every import failed.
  • Format. The DVK plays audio at 24 kHz from the first channel and doesn't resample. A 2 s 48 kHz file previews in 4.00 s, a 2 s 24 kHz file in 2.00 s. The old validator accepted only 48 kHz float, so it allowed exactly the files that play at half speed.
  • Conversion. The new DvkWavConverter accepts PCM 8/16/24/32-bit or 32-bit float, plain or EXTENSIBLE, with chunks in any order, 1 to 8 channels and 8 to 384 kHz. It mixes to mono, resamples to 24 kHz with the existing r8brain Resampler, and sends 16-bit PCM. Clips over 10 s are refused.

Slot names keep their spaces. CommandParser::parseKVs now keeps a quoted value whole, so name="CQ Contest" no longer shows as CQ. This also fixes the SmartLink path, which only carries parsed key/values. The workaround that replaced any name starting with "Recording" is gone.

Smaller changes:

  • Removed the Delete item from the slot menu. It sent dvk remove, which the wiki reserves, and the slot disappeared until the radio restarted.

  • Clear sends only dvk clear. Firmware 4.2.20 keeps the slot's name, and the client leaves it, because the radio is authoritative and the wiki can be wrong (maintainer ruling in review). A row whose slot is gone resets to empty.

  • One operation at a time, from the moment it is sent. A new start needs the DVK idle, no start of ours still awaiting its reply, and no WAV import or export running. A sent start counts as pending until its reply, because the radio publishes the status before the reply. Clear and Import also wait for idle. STOP and Escape reach a start that is still pending.

  • The stop commands no longer send an id the wiki doesn't use. enabled=0 reads as Disabled, as in FlexLib's DVKStatus.

  • Rename removes | as well as quotes and allows the radio's 61 UTF-8 bytes, trimming without splitting a surrogate pair. The recording bar counts toward the 10 s limit.

  • The wiki's error codes now have readable messages:

    Code Message
    50004001 DVK requires an active SmartSDR+ subscription
    50001000 radio could not parse the command
    E2000000 no such slot, or an upload is already pending
    50000053 radio file server busy
  • A 50004001 refusal on any DVK path now marks DVK as not licensed in the availability gate, because the radio's enabled flag is always 1. That covers the dvk commands and all three transfer legs (dvk upload, the file-upload port, dvk download). The flag is read through RadioModel::dvkLicenseRefused() beside the other entitlement inputs. It and the slot list both clear on disconnect, so slots no longer carry over to the next radio.

  • Transfers:

    • A port the radio named but would not connect gets one retry on 42607, as FirmwareUploader does.
    • Export accepts a connection only from the radio. Another LAN host that reaches the listening port first is dropped.
    • Import and Export stay greyed for 3 s after an upload, because firmware 4.2.20's file server answers 50000053 for about 2.2 s afterwards.
  • PLAY and the F1–F12 buttons are marked as keying TX, so the automation bridge refuses them.

Command-plane ratchet (#5262/#5554), disclosed: the file upload … dvk_recording leg goes through the existing RadioModel::requestFileUploadPort(), so DvkWavTransfer adds no new wire text. Its two dvk verbs (upload and download) go through one m_model->sendCmdPublic call, with a test hook beside it, so the DvkWavTransfer.cpp row drops from 2 to 1. This is a funnel, not a removal: both dvk transfer verbs are still sent from above the seam, as before this PR.

The TCI heading "DVK record/play" actually covered slice quick-record and is renamed. aetherd-touchpoints.md is regenerated because MainWindow.cpp now includes DvkModel.h explicitly.

Accessibility (#4896), second commit:

  • The panel, the REC/STOP/PLAY/PREV buttons and the status-bar DVK indicator now have plain accessible names, so a screen reader no longer reads glyphs like "black circle REC". Each button's description says what it does, including which ones transmit.
  • Each slot row and its F-key button announce the slot's number, name and length, for example "Slot 3: CQ Contest, 4.2s" and "Play slot 3: CQ Contest". These update and are announced when a slot is recorded, renamed or cleared.
  • The status line announces state changes, transfer results and radio refusals. The 100 ms elapsed-time updates and upload percentages stay silent.
  • The rename field names its slot and states the 61-character limit, which characters are removed, and that Enter saves and Escape cancels.
  • The indicator's "requires SmartSDR+" reason is now in its accessible description, because a tooltip is never announced.
  • Text labels keep their own text as their accessible name. They get object names only (dvkSlotName3, dvkSlotLength3, …), which also give the automation bridge stable targets.

Parser blast radius, audited. Among the status lines FlexLib parses, quoted values appear only in DVK names, transverter name="…" (single word; FlexLib itself splits on spaces) and Rapid-M "text=…". In the Rapid-M case the quote comes before the key, so the join never triggers. A line without a quoted value containing spaces parses exactly as before, and an unterminated quote still splits the old way. The radio info reply has its own comma parser and isn't affected.

Constitution principle honored

Principles I and II. The wire behaviour follows FlexLib's DVK.cs and the vendor's SmartSDR API wiki, checked against a real radio. Where firmware 4.2.20 differs from the wiki (clear keeps the name, downloads are mono), the radio wins: the code follows the firmware, and the comment names the version.

Test plan

  • Local build passes, including the full ctest suite: 667 tests, offscreen.
  • Behaviour verified on a real radio (FLEX-8600, firmware 4.2.20). Nothing was transmitted.
    • Wire probe. I checked the two-step handshake, the 24 kHz playback rate (by timing previews) and the busy window of about 2.2 s after an upload. I also confirmed that clear keeps the name and that downloads are mono.
    • Converter output. I uploaded what the shipping converter produced from 48 kHz stereo and 44.1 kHz float files. The radio reported duration=2000, and they previewed in 1.99 s and 2.00 s.
    • GUI smoke test on the maintainer's display, driven through the automation bridge (X11).
      • Import from the slot menu sent dvk upload id=1, then file upload 96044 dvk_recording. The radio reported duration=2000 and the row shows 2.0s.
      • Preview ran 2.0 s.
      • REC pressed during preview sent nothing and didn't stay checked.
      • Rename to CQ Contest | KK7GWY became CQ Contest KK7GWY, and the full name came back from the radio.
      • Export produced a 24 kHz mono 16-bit file.
      • Clear sent dvk clear. The radio kept the name, then set_name "Recording 1" reset it, and the row shows Recording 1 / Empty.
      • The slot menu has no Delete item. Clear and Export are disabled on an empty slot.
    • The radio was left as found: slots empty, default names.
  • Tests, all registered in tests/tests.cmake:
    • dvk_wav_converter_test (20 cases): length, pitch by zero crossings, and level. A click at 0.5 s must land at output sample 12000 from every rate between 8 and 192 kHz, which fails at each resampled rate if the group-delay trim is removed.
    • dvk_model_status_test (39 cases), through the real CommandParser with status lines captured from firmware 4.2.20: admission, the refusal latch, Clear leaving the name, and surrogate-safe trimming.
    • dvk_panel_admission_test (7 cases), the reviewer's probe on a real DvkPanel: PREV then REC before an echo sends one start, nothing starts during a transfer, STOP reaches a pending start, and PLAY and the F-keys carry the TX marker.
    • dvk_wav_transfer_generation_test: the two-step handshake, each transfer leg's 50004001 closing the gate, the settle window, the 42607 retry, and the export peer check.
    • dvk_availability_gate_test: the refusal cases.
  • Review fixes verified on the radio (FLEX-8600, firmware 4.2.20, offscreen with a scratch profile, no transmit): an import, then an export that passed the peer check (96,044 bytes, nothing rejected), then a renamed slot cleared with its name kept. The slots were restored to their defaults afterwards.
  • Accessibility: I launched the build offscreen with a scratch HOME and read the widget tree through the bridge. The panel, all four buttons, every slot row, each F-key button, the progress bars and the indicator carry the expected names and descriptions. tools/check_a11y.py reports nothing for DvkPanel. Its four MainWindow.cpp warnings are on ATU/AGC menu actions and are also present on main. Not checked with a real screen reader.
  • Local static checks: check_command_plane, check_test_registration, check_engine_boundary, check_ci_test_gate, check_qsettings, check_network_timeouts, gen_touchpoint_manifest --check, gen_seam_probe_table --check.

Not verified live:

  • PLAY and the F1–F12 keys. They transmit, so I didn't drive them; their code path only gained the idle check.
  • The unlicensed (50004001) path. The test radio is licensed, so that path is covered by unit tests only.
  • Under native Wayland the bridge cannot open the slot context menu, because a synthesized event carries no input serial for the popup. The smoke test ran under XWayland. Real mouse input isn't affected.

Follow-up, not in this PR:

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls
  • Code is clean-room. Firmware behaviour was observed on the wire (Principle IV); nothing was decompiled.
  • All meter UI uses MeterSmoother: no meter UI touched
  • Documentation: no doc described the old import format. CHANGELOG is untouched.
  • Security-sensitive: the PR parses an operator-supplied WAV file (a RIFF chunk walker with bounded reads, reviewed adversarially in the bot review), and export now rejects a connection that doesn't come from the radio. No GHSA applies.

🤖 Generated with Claude Code

…i's two-step upload; slot names keep their spaces. Principle I.

Brings the Digital Voice Keyer in line with the SmartSDR API wiki
(TCPIP-dvk), checked on a FLEX-8600 running fw 4.2.20.41343. Closes #6244.

Import WAV
- Upload is now the documented two-step handshake: `dvk upload id=N`
  (empty reply, names the slot), then `file upload <size> dvk_recording`,
  whose reply carries the port (42607 fallback, as FirmwareUploader).
  The old code read a port from `dvk upload`'s empty reply and always failed.
- The radio plays a slot at 24 kHz from its first channel and does not
  resample: a 2 s 48 kHz file previewed for 4.00 s, a 2 s 24 kHz file for
  2.00 s. The 48 kHz float-only validator is replaced by DvkWavConverter:
  any PCM 8/16/24/32 or float WAV (plain or EXTENSIBLE, any chunk order,
  any channel count) is averaged to mono, resampled to 24 kHz with the
  existing r8brain Resampler and sent as 16-bit PCM. Clips over 10 s are
  refused. Converted 48 kHz stereo and 44.1 kHz float files previewed on
  the radio for 1.99 s and 2.00 s with duration=2000.

Slots and status
- CommandParser::parseKVs keeps a quoted value whole, so
  `name="CQ Contest"` no longer becomes "CQ". This also fixes the SmartLink
  path, which only carries parsed key/values. The "starts with Recording"
  workaround that overwrote user names is gone.
- Delete (`dvk remove`, reserved by the wiki; the slot vanished until a
  radio restart) is removed from the slot menu. Once the radio accepts a
  clear, set_name restores the default name, because fw 4.2.20 `clear`
  keeps the old name.
- A row whose slot the model no longer holds resets to an empty default.
- Stop verbs drop the id the wiki does not take. enabled=0 reads as
  Disabled, as FlexLib's DVKStatus does.
- New operations start only from idle (the radio accepts overlaps and can
  end up inconsistent); Clear and Import wait for idle too.
- Rename strips `|` as well as quotes and allows the radio's 61 bytes.
- The recording bar counts toward the 10 s limit.

Errors and licensing
- dvkErrorString names the wiki's 50004001, 50001000, E2000000 and
  50000053 codes.
- A 50004001 refusal marks DVK as not licensed in the availability gate,
  since `enabled` is always 1. The flag clears on disconnect, together
  with the slots, which no longer carry over to the next radio.

Command plane: the three transfer verbs leave through one
m_model->sendCmdPublic funnel (with a test hook beside it), so the
DvkWavTransfer row drops 2 -> 1. This is a funnel, not a removal: three
DVK verbs remain above the seam. The TCI "DVK record/play" heading was
really slice quick-record and is renamed.

Tests: dvk_wav_converter_test (14), dvk_model_status_test (30, through
the real CommandParser), new two-step handshake cases in
dvk_wav_transfer_generation_test, refusal cases in
dvk_availability_gate_test. The full ctest suite passes locally.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 requested review from a team as code owners October 7, 2026 15:35
@ten9876 ten9876 self-assigned this Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 4
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

…s for screen readers. Principle VIII.

Part of #6244; accessibility tracking #4896.

- The panel, REC/STOP/PLAY/PREV buttons and the status-bar indicator get
  plain accessible names; the glyph-prefixed button text ("● REC") would
  otherwise be read aloud. Each button's description says what it does,
  including which ones transmit.
- Each slot row and its F-key button name the slot, its name and its
  length ("Slot 3: CQ Contest, 4.2s"; "Play slot 3: CQ Contest"), and are
  updated and announced when a slot is recorded, renamed or cleared.
- The status line announces state changes, transfer results and radio
  refusals. The 100 ms elapsed-time tick and upload percentages stay
  silent.
- The rename field names the slot and states the 61-character limit,
  the removed characters and Enter/Escape.
- The indicator's reason (SmartSDR+ required) rides its accessible
  description as well as the tooltip, which is never announced.
- Text labels keep their own text as the accessible name and get object
  names only (dvkSlotName3, dvkSlotLength3, …), which also give the
  automation bridge stable targets.

The status-bar indicator is still a clickable QLabel (the a11y doc's
interactive-label anti-pattern); converting it is out of scope here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 force-pushed the fix/dvk-wiki-conformance branch from 4c38a5c to e9584dc Compare October 7, 2026 15:48
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 4
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

1 similar comment
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 4
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. Issue fit

Mostly yes — this is a strong, unusually well-evidenced PR. All seven findings in #6244 are addressed, and the two that actually broke import (the single-step handshake, and a validator that accepted exactly the files that play at half speed) are fixed at the root rather than patched. The new DvkWavConverter is the part I attacked hardest and it held up: I could not construct an out-of-bounds read, an overflow, or a negative length in it.

Two items from the issue thread are not covered, and three places where the PR made a choice the triage comment explicitly reserved for the maintainer. Details below.

One gap worth naming up front: issue #6244 is still open and labelled maintainer-review, and the triage comment (2026-10-07 15:21) predates this PR (15:35). That comment proposed a four-way split (A/B/C/D) "the maintainer can authorize independently" and flagged exactly two questions it wanted ruled on first — B's seam choice and C's remove question. This PR implements all four pieces plus a fifth issue, and resolves both reserved questions in the opposite direction from the triage's recommendation. The code is good; the authorization isn't there yet.

2. Scope

File / group What it changes Claimed? Verdict
src/core/DvkWavConverter.{h,cpp}, CMakeLists.txt:1066, tests/dvk_wav_converter_test.cpp, tests/tests.cmake:3144 New WAV decode → mono → 24 kHz → PCM16 converter + 14-case test Yes (#6244 §2) In scope. New file is registered in CORE_SOURCES and the test target — not a dead addition.
src/core/DvkWavTransfer.{h,cpp}, tests/dvk_wav_transfer_generation_test.cpp Two-step dvk upload → file upload … dvk_recording; validateWavFile removed; 64 MB source cap Yes (#6244 §1) In scope
src/core/backends/flex/CommandParser.cpp:12-28 Global quote-aware parseKVs Yes (#6244 §3) — but the triage asked for the DVK seam instead Needs maintainer decision (Blocker 2)
src/models/DvkModel.{h,cpp}, src/models/RadioModel.cpp:8155, tests/dvk_model_status_test.cpp Stop verbs drop id, remove() deleted, wiki error table, enabled=0→Disabled, reset() on disconnect, clear→set_name Yes (#6244 §4/§5/§7) In scope, except the clear→set_name behavior (Blocker 4)
src/gui/DvkAvailabilityGate.h, tests/dvk_availability_gate_test.cpp, src/gui/MainWindow.cpp:10427 50004001 → sticky NotLicensed, additive to #4210's fail-open Yes (#6244 §5) In scope — and it preserves #4210 exactly as the triage asked
src/gui/DvkPanel.cpp:566 "Delete" menu item removed Yes (#6244 §4) — but the triage reserved drop/alias/confirm Needs maintainer decision (Blocker 3)
src/core/TciProtocol.cpp:1427, TciProtocol.h:136 Section comment renamed "DVK record/play" → "slice quick-record" Yes (#6244 §7) In scope. Comment-only; I diffed both files and found no other delta.
docs/architecture/aetherd-touchpoints.md:227 models/DvkModel.h count 1 → 2 Yes, disclosed In scope, but see nit — this is the decoupling burndown moving backwards
tools/check_command_plane.py:107 DvkWavTransfer.cpp baseline 2 → 1 Yes, disclosed as "a funnel, not a removal" In scope. The disclosure is accurate: three verbs still leave above the seam, all through one sendCommand.
Commit e9584dc — src/gui/DvkPanel.{cpp,h}, src/gui/MainWindow.cpp:5790,10479 Accessible names/descriptions for the panel, four buttons, 12 rows, 12 F-keys, rename field, status-bar indicator Closes #4896, not #6244 Unbundle (Blocker 1)

Everything else in the diff is explained by #6244. No build-config drift, no vendored trees, no formatting churn, no CHANGELOG.md entry, and the body's self-certification checks out except for one box — see Blocker 5.

3. Blockers

1. e9584dc closes a different issue — please unbundle. The a11y commit addresses #4896, is 100% of the changes to DvkPanel.h and the MainWindow.cpp:5790 hunk, and carries no dependency on the #6244 fix beyond DvkModel::defaultName / kMaxNameBytes. The triage asked for #6244 itself to be split into four independently authorizable pieces; shipping those four plus a fifth issue in one PR is the opposite direction. It is cleanly separable — one git cherry-pick onto a branch off main with Closes #4896. The work itself looks good and I have no objection to it landing; it just shouldn't land on #6244's authorization.

2. src/core/backends/flex/CommandParser.cpp:12-28 — the quote-aware parser went into the shared parseKVs, which is the seam the triage explicitly argued against. Needs a maintainer ruling.

    const QStringList tokens = body.split(' ', Qt::SkipEmptyParts);
    for (qsizetype i = 0; i < tokens.size(); ++i) {
        QString token = tokens[i];
        if (token.contains(QLatin1String("=\"")) && token.count('"') == 1) {

The triage comment said: "I'd scope this to the DVK seam rather than making parseKVs quote-aware — that function has ~10 callers… Extract the quoted span there." It named RadioModel.cpp:11484 as the seam and RadioModel.cpp:9317-9320 (the meter raw-intercept) as existing precedent. I confirmed that precedent is live and that there are two of it in the tree today — the meter #-separated line at RadioModel.cpp:9318-9321 and the profile list at :9338-9347, both of which bypass parseKVs for exactly this reason.

I re-ran the PR's own blast-radius audit independently and it holds up on the inputs I could enumerate: 7 call sites across RadioModel.cpp (2), RadioStatusOwnership.h (2), TransmitInhibitPolicy.h (1), DvkModel.cpp (1) plus the parser itself; the only quoted-value statuses are DVK name and xvtr name; a two-quote token (name="23cm") is left alone by the count('"') == 1 guard so the xvtr path is bit-identical; "text=…" has no =" substring so Rapid-M never triggers the join; and the meter/profile lines carry no quotes. So I could not break it. But "I could not break it" on a shared protocol parser is not the same as a ruling, and the triage's objection was about the blast radius, not about whether this particular audit was thorough. That's the maintainer's call, not mine or the author's.

3. src/gui/DvkPanel.cpp:566 — removing "Delete" resolves a question the triage reserved. The triage wrote: "let the maintainer choose between dropping the menu item, aliasing it to Clear, or keeping it behind a confirmation" — and noted that remove is not invented on our side: it cites FlexLib shipping DVKCommandType.DeleteRecording → "remove" at DVK.cs:357. Under Principle I, FlexLib is protocol authority, and the wiki's "reserved" plus the "gone until restart" claim are both unverified live (the reporter's own probe never sent remove). I cannot check DVK.cs myself — reference/ is not in this checkout — so I'm relaying the triage's citation, not confirming it. Either way, a verb FlexLib implements being removed from our UI wants a ruling.

4. src/models/DvkModel.cpp:85-88 — this contradicts the PR body's own Principle I claim, and it destroys an operator's slot name.

        // fw 4.2.20 `clear` erases the audio but keeps the name; the wiki says
        // the slot returns to its default name, so ask for that once it lands.
        if (verb == QLatin1String("clear")) {
            setName(id, defaultName(id));
        }

The body says: "Where firmware 4.2.20 differs from the wiki (clear keeps the name), the code follows the firmware and the comment names the version." The code does the opposite — it follows the wiki by issuing a command the firmware did not ask for. The comment is honest about what it does; the body's claim is not.

The behavioral consequence: an operator names slot 3 "CQ Contest", records into it, then clears it to re-record. On real SmartSDR the name survives. On AetherSDR it becomes "Recording 3". The author's own smoke test records this happening and reads it as the fix working. Pick one — either drop the re-assert and let the name stand as the firmware leaves it, or keep it and correct the body. I'd lean toward dropping it: it's a synthesized write over radio-owned state in a Multi-Flex-visible slot, and it's not what the shipping reference client does. Maintainer's call.

5. ## Checklist — "No security-sensitive changes" is not right. This PR adds a hand-rolled RIFF chunk walker with raw pointer arithmetic and a memcpy type-pun, parsing an arbitrary operator-supplied file. That is the definition of a security-sensitive change, and the harness classified it as one. The parser turned out to be clean (see §5), so nothing bad came of the mis-check — but the box should read the other way so reviewers know to look.

4. Nits (non-blocking)

  • src/gui/DvkPanel.cpp:317-325 — STOP and Escape go dead in the Unknown state, while start stays open. canStartOperation() deliberately fails open on Unknown (DvkModel.h:34), so REC can start before any dvk status has arrived. But stopActiveOperation() switches on m_model->status() and default: breaks — so with status still Unknown, STOP and Esc send nothing. The old code fell back to m_selectedSlot. The REC toggle itself still stops (it calls recStop() unconditionally on uncheck), so this is recoverable, not a stuck transmitter — but the asymmetry is backwards: if start fails open, stop should too. Suggest default: falling through to the previously-active id, or at minimum recStop() when m_recBtn->isChecked().

  • src/gui/DvkPanel.cpp:287-300 — the NameChanged burst isn't throttled. docs/a11y.md:132 says "A noisy updateAccessibility is worse than no updateAccessibility — prefer slightly stale to spammy." updateSlotAccessibility fires unconditionally: 12 times in the constructor on not-yet-visible widgets, 12 more as the slot list loads on connect, and 12 more on reset() at disconnect (DvkModel.cpp:218-220 emits recordingChanged for every gone slot). A connect/disconnect cycle is ~24 announcements. Cheap fix — only announce when the widget is visible and the string actually changed:

        const QString name_ = QString("Play slot %1: %2").arg(id).arg(name);
        if (play->accessibleName() != name_ && play->isVisible()) {
            QAccessibleEvent ev(play, QAccessible::NameChanged);
            QAccessible::updateAccessibility(&ev);
        }
        play->setAccessibleName(name_);
  • src/core/DvkWavConverter.cpp:94 — std::lround can be reached with NaN (credit: security-audit (CodeGuard), codeguard-0-input-validation-injection). decodeSample zeroes only non-finite inputs, so eight finite channels near FLT_MAX sum to inf; the resampler can turn that into NaN; std::clamp(NaN, -1, 1) returns NaN unmodified (neither comparison is true), so std::lround(NaN) is reached — unspecified result plus FE_INVALID. Not memory-unsafe and the input is the operator's own file, so garbage-in-garbage-out rather than a vulnerability. One-line fix in decodeSample:

            return std::isfinite(value) ? std::clamp(value, -1.0F, 1.0F) : 0.0F;
  • src/core/DvkWavTransfer.cpp:436-441 — the fallback port is weaker than all three siblings. FirmwareUploader, WaveformInstaller and ProfileTransfer all define the same 42607, and all three also retry on 42607 when the connect to the parsed port fails (FirmwareUploader.cpp:269-277, WaveformInstaller.cpp:351-357). This one falls back only when the reply body is unparseable. Also worth noting the triage pointed at RadioModel::requestFileUploadPort(size, kind, cb) as an existing helper that emits exactly this command; the PR hand-formats the string instead, which is fine for the funnel but duplicates the verb.

  • The ~2.2 s file-server busy window from #6244 §6 isn't handled. The triage called this out specifically: we drop m_transferring on socket close, so notBusy goes true while the radio's file server is still busy, and a prompt second Import or Export earns 50000053. The PR confirmed the window live and added the readable message for 50000053, but no cooldown or status-line-driven completion. Now that the error reads well this is much less bad, so: nit, not a gap worth holding the PR for.

  • src/core/backends/flex/CommandParser.cpp:13-14 — "An unterminated quote splits as before" is only true when no later token carries a quote. Given dvk id=1 name=" duration=0 other="x", the forward scan joins through to other=", swallowing duration. Malformed-input-only, and the old code mangled it differently, so no regression — but the comment and the body both overstate the invariant.

  • tests/dvk_wav_converter_test.cpp:148-168 — the group-delay trim is not actually asserted. estimateHz and rms both measure only [size/10, size-size/10), and the exact-length check is satisfied by the trim-then-resize(wanted) pad regardless of whether lead is right. So the 14 cases would still pass with lead set to 0 or doubled. For a 1 s clip the 10% skirt is 100 ms and a 2.0% transition band gives roughly 10–20 ms of group delay, so the whole quantity under test lives in the discarded region. Worth one assertion on head alignment (e.g. first-sample phase, or RMS of the first 5 ms against the source). Related: QsoPcmConverter.cpp:70-90 solves exactly this finite-file alignment problem and deliberately rounds the prefix up to an exact rational output-frame boundary — "instead of rounding a fractional trim and shifting the finite file's content." DvkWavConverter.cpp:198 does the fractional llround that comment warns about; at 44.1 k → 24 k that's a sub-sample (~20 µs) shift, inaudible for voice, but the sibling has the better pattern if you want it.

  • src/models/DvkModel.cpp:71-73 — clean.chop(1) walks UTF-16 code units to satisfy a UTF-8 byte budget, so a name ending in an emoji or a non-BMP character can be chopped to a lone surrogate before the loop exits. Also DvkPanel.cpp:635 sets setMaxLength(kMaxNameBytes), which Qt applies as characters, and the rename field's description says "61 characters" — the radio's limit is 61 bytes. Non-ASCII names hit the silent server-side trim first.

  • docs/architecture/aetherd-touchpoints.md:227 — the burndown moved backwards by one. models/DvkModel.h goes 1 → 2 because MainWindow.cpp now includes it to reach DvkModel::licenseRefusedChanged. Disclosed and honest, and the manifest is correctly regenerated — but it's a new GUI→model coupling in a manifest whose job is to shrink. Routing licenseRefused() through RadioModel (which already owns licenseFeatureSeen/licenseFeatureEnabled for the other two gate inputs) would keep the count at 1 and make the gate's three arguments come from one place.

  • src/gui/MainWindow.cpp:5790 — m_dvkIndicator stays a click-handled QLabel. docs/a11y.md:255-256 says the minimum remedy is setFocusPolicy(Qt::TabFocus) plus a keyPressEvent handler, not just a name. The PR adds the name and description and discloses the rest as a follow-up; the doc also says the lint is "a nudge, not a gate", so this is fine as-is — just noting the doc's floor isn't met yet.

  • None of the three new/extended test targets are in CI's merge gate. grep -n dvk .github/workflows/ci.yml returns nothing, so dvk_wav_converter_test, dvk_model_status_test and the new dvk_wav_transfer_generation_test cases are registered but not -R-selected. Not this PR's problem to fix, but "CI is green" says nothing about them.

  • Pre-existing, route separately (credit: security-audit (CodeGuard), codeguard-0-file-handling-and-uploads): the DVK download path listens on QHostAddress::Any and writes whatever the first accepting peer sends into the operator's save path, with no check of m_client->peerAddress() against m_model->radioAddress() (DvkWavTransfer.cpp:190, handleNewConnection → receiveDownloadBytes). Anyone on the LAN who wins the race controls the exported file. This is on main and outside this PR's changed regions — flagging so it gets its own issue rather than riding here.

5. Socket-test disclosure

  • tests/dvk_wav_transfer_generation_test.cpp — existing registered target, already owns a socket (QTcpServer::listen() for the download-generation cases). This PR extends it with four upload cases that construct a QTcpSocket via the production path. Those cases never reach the wire: handleUploadPortReceived's connect lambda is guarded by transfer->m_model (DvkWavTransfer.cpp:474) and the tests pass DvkWavTransfer(nullptr) with a m_commandSender stub, so the socket stays an inert object. The subject is our own transfer state machine, not a synthesized radio peer — correct layer, and no fake firmware was introduced. Not run by CI (no dvk entry in ci.yml's -R filters).
  • tests/dvk_wav_converter_test.cpp, tests/dvk_model_status_test.cpp — socket-free, and dvk_model_status_test drives the real CommandParser, which is the right layer for a wire-parsing change.
  • No socket test or fake peer was removed.

6. What I tried to break (and couldn't)

  • The new chunk walker, hard. pos = body + size + (size & 1) with size a quint32 widened to qint64 cannot overflow and advances ≥ 8 per iteration, so the pos + 8 <= wav.size() guard both terminates and keeps the pos+4 size read in bounds. available = min(size, wav.size() - body) means a lying chunk size can't widen any read; available >= 16 covers f+0..15 and the extensible sub-format tag at f+24..25 is correctly gated on available >= 26. blockAlign >= channels * bytesPerSample makes decodeSample's c*bps + bps stay inside the frame and rules out a zero stride, and frames = dataBytes / blockAlign keeps the last frame at or before data + dataBytes. Truncated data size, data-before-fmt, a LIST chunk first, duplicate fmt chunks, a 0xFFFE tag with a short chunk, and 0-byte data all land on an error path rather than a read. The memcpy is a fixed-sizeof 4-byte type-pun with no computable size.
  • The unbounded resampler feed. convertForRadio hands up to 3.84 M samples to a Resampler built with the default maxBlockSamples = 4096, and Resampler.cpp:36-38 says in so many words that r8brain "does not bounds-check against aMaxInLen; exceeding it silently overflows internal filter buffers." It survives only because Resampler::process chunks internally at :40-42. Worth knowing the margin is that thin, but it is safe as written. Separately, the 10 s duration check runs before the decode allocation, so the 64 MB source cap can't translate into a large resample — frames is bounded by 10 × 384 kHz regardless.
  • The trim arithmetic going negative or truncating. lead >= 0, wanted > 0, first ∈ [0, outCount], count = min(wanted, outCount - first), and count <= wanted so resize(wanted) only ever pads. In-bounds even when converted is empty. And the trim's direction is right: QsoPcmConverter.cpp:74 ("Resampler already prepended delay silent source frames") and Resampler.cpp:174-178 together establish that output lags input by the group delay after prewarm(), so subtracting lead at the head and drain()ing the tail is the correct finite-stream contract, not a double-count.
  • The stale-callback class the triage warned about (#5665). Both legs of the new handshake bind to generation and a fresh requestId, and m_portRequestId is zeroed and re-set between them, so a duplicated or late leg-1 reply is dropped (m_portRequestId != requestId) and a cancel between legs is caught by isCurrent(generation). The file upload reply also checks code != 0 before the port fallback, so a radio refusal can't be laundered into a connect to 42607.
  • The parser blast radius, independently. Enumerated all 7 parseKVs call sites and every status shape I could find that carries a quote or a space: xvtr name="23cm" (two quotes → untouched, bit-identical), Rapid-M "text=…" (no =" substring → never triggers), meter #-separated and profile ^-separated lines (no quotes, and both bypass parseKVs anyway). The join loop is linear, not quadratic, and end is bounded by tokens.size(). The audit in the body is accurate as far as I could push it — which is why Blocker 2 is a ruling request, not a defect report.
  • The removed guards. validateWavFile is gone and has no remaining callers; MAX_FILE_SIZE is still live on the download path (DvkWavTransfer.cpp:267) and in dvk_wav_transfer_test.cpp:52, so nothing dangles. The converted payload maxes at ~480 KB, well under it. DvkModel::remove() is gone with no stragglers. The blockSignals(false) un-check dance was replaced by an onStatusChanged re-sync that does set all three buttons from the model (DvkPanel.cpp:357-367) — I checked, it isn't a silent drop.
  • Could the new tests pass against the unfixed code? No. dvk_wav_transfer_generation_test:318 asserts a second command (file upload 24044 dvk_recording) that the old code never sent, and asserts no socket opens before the second reply. The converter test asserts exact output length at 24 kHz, which fails outright if resampling is skipped. These are real regression tests, not implementation echoes — with the one exception in the nits (the head-alignment trim isn't actually covered).
  • Could not check, and did not pretend to: everything above is reasoned from the head checkout at /tmp/aetherclaude/pr-6247, not reproduced at runtime — I have no build, no test run, and no GUI. Specifically unverifiable from here: the FLEX-8600 / fw 4.2.20 wire claims (24 kHz playback, the 2.2 s busy window, clear keeping the name), the DVK.cs:357 DeleteRecording citation (reference/ isn't in this checkout), whether QProgressBar::setValue fires its own QAccessibleValueChangeEvent and so undercuts the "upload percentages stay silent" claim, and anything a real screen reader would do. The live-radio evidence in the test plan is specific and self-consistent enough that I'm taking it at face value.

7. Recommendation

Needs maintainer decision. The engineering here is genuinely good — the converter is memory-safe under adversarial reading, the two-step handshake is correctly generation-bound, the 50004001 latch is additive to #4210 rather than a reversal of it, and the disclosure discipline (the command-plane ratchet, the parser blast radius, the "not verified live" list) is exactly right. I found no correctness blocker in the code.

What stands in the way is authorization, not quality. Concretely: (a) cherry-pick e9584dc out to its own PR against #4896; (b) get a ruling on the parseKVs seam and on dropping remove, both of which the triage reserved before this PR existed; (c) reconcile Blocker 4 — the clear→set_name re-assert and the body's "follows the firmware" sentence can't both stand. Fix the "No security-sensitive changes" checkbox while you're in there. With the a11y commit split out and those two rulings in hand, the remainder is ready.

Thanks for the unusually good report-plus-fix pair on this one — separating "what the radio actually did" from "what the wiki says", and timing the previews to prove the 24 kHz rate, is what made the half-speed bug findable at all.


🤖 aethersdr-agent · cost: $12.9825 · model: claude-opus-5

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

Partially — request changes. Reviewed e9584dcb65d20eb657a66d2960780942539d34e6 against #6244. The two-step upload, mono/24k/PCM16 conversion, quoted names, stop syntax, readable errors, removed reserved Delete action, empty-row reset and disconnect reset address the reported defects. The recording-progress and accessibility changes stay on the same affected surface. However, the overlap guard still admits a second command before the first status echo, and upload license refusals bypass the new availability latch. Those are reproducible gaps in #6244 items 6 and 5, respectively.

Scope

File/group What changes Claimed in body? Verdict
src/core/DvkWavConverter.{cpp,h}, CMakeLists.txt Decode/downmix/resample/encode imported WAVs; register converter Yes In scope
src/core/DvkWavTransfer.{cpp,h} Two-step upload, source cap, conversion and reply hooks Yes In scope; new raw command placement needs correction below
src/core/backends/flex/CommandParser.cpp Keep quoted values containing spaces together Yes In scope; shared-parser blast radius inspected
src/models/DvkModel.{cpp,h}, src/models/RadioModel.cpp Correct commands/names/status/errors; license latch; disconnect reset Yes In scope
src/gui/DvkPanel.{cpp,h} Admission checks, slot reset, recording progress, rename limit, accessible names/events Yes In scope
src/gui/DvkAvailabilityGate.h, src/gui/MainWindow.cpp Refusal-aware availability and accessible indicator reason Yes In scope
src/core/TciProtocol.{cpp,h} Correct misleading section heading Yes In scope; comments only
Four changed/added DVK tests, tests/tests.cmake Converter, status/parser, transfer identity/handshake and license-gate checks Yes In scope
tools/check_command_plane.py, docs/architecture/aetherd-touchpoints.md Funnel-count reduction; explicit DvkModel include recorded Yes Disclosure matches diff; passing count is not proof of seam conversion

All 22 files are explained by the issue and the disclosed accessibility work. The two commit author dates and subjects fit this work; both signatures are GitHub-verified. No CHANGELOG, unrelated default, dependency, theme or protocol extension is bundled. Accessibility names on the controls already being changed follow docs/agents/gui.md and docs/a11y.md; I do not require a separate PR merely because the same work also advances #4896.

Blockers

  1. src/gui/DvkPanel.cpp:198, :222, :309; src/models/DvkModel.h:34 — the overlap guard does not cover a pending start. canStartOperation() reads only the last radio status, and sending a start leaves that status Idle until its echo arrives. A second control therefore passes the same guard. A socket-free Qt widget probe using the actual head DvkPanel and DvkModel seeded slot 1 and Idle, clicked PREV, then clicked REC without injecting an intervening status:

    before first status echo: dvk preview_start id=1 ; dvk rec_start id=1
    

    This is precisely the REC-during-preview ordering item 6 asks the client to prevent; network latency makes the pending interval real. The same probe with merge-base panel/model/transfer sources produces the same output, so this is an unresolved reported bug, not a newly introduced regression. The new test only checks admission after Preview status has already arrived.

    The probe also showed the transfer gap:

    during pending upload (isTransferring=1): dvk rec_start id=1
    

    Add a connection-scoped local pending-operation/admission guard shared by the start buttons and F-key path, and include active transfer state. Clear it on the appropriate refusal/status/disconnect paths while preserving STOP and keeping radio-reported live state authoritative. Add a socket-free panel/command-injection regression for two starts before an echo, plus REC during transfer. No synthetic firmware peer is needed. The demo cannot reproduce Flex DVK because SimBackend explicitly sets hasVoiceKeyer=false; this evidence is widget replay, not a live-radio observation.

  2. src/core/DvkWavTransfer.cpp:396 and :429 (also :162) — transfer refusals never reach the license latch. The new latch is set only by DvkModel::handleCommandResponse(), but the transfer callbacks call only the static error-string helper and finish(). Import is especially important: it is available on an empty slot and can be an unlicensed user's first action.

    I constructed a disconnected RadioModel plus its real DvkWavTransfer, injected only command replies through the new test hook, and returned 50004001 to the first upload leg. No socket connected or listened:

    transfer command: dvk upload id=3
    after upload refusal: licenseRefused=0, transferring=0
    after model-command refusal: licenseRefused=1
    

    The final line is the control: the same model's normal command-response path sets the flag correctly. With the documented enabled=1/unknown-entitlement state, the transfer refusal leaves the availability predicate open. Route transfer command errors into the same connection-scoped refusal handling before completing the operation, including download and both upload legs, and extend the injected-transfer tests to assert the latch/availability result. This is a missed sibling path in the claimed #6244 item-5 fix.

  3. src/core/DvkWavTransfer.cpp:407 — a new raw Flex command is added above the seam despite the smaller counter. The new file upload %1 dvk_recording literal is above src/core/backends/, and the funnel forwards it through sendCmdPublic. The governing standing notice is explicit (docs/agents/backends.md:13): “check that your change does not add to any item listed there — ... no new raw Flex wire text above the seam”. The PR's disclosure correctly describes the funnel, but disclosure does not supply an exception to that rule.

    Both strict gates pass: command-plane totals move 369→368 and the transfer row 2→1 because the checker counts forwarding calls. Inspection shows that no transfer verb moved behind the seam and a third command template was added. Use the existing RadioModel::requestFileUploadPort(size, kind, callback) at src/models/RadioModel.cpp:9436 for this leg, preserving the captured generation/request identity and a socket-free injection seam; it already owns this command encoding. This avoids inventing a new interface or broadly rerouting the subsystem. Do not treat the smaller counter as a completed conversion.

Nits — explicitly non-blocking

  • src/models/DvkModel.cpp:71; src/gui/DvkPanel.cpp:638: trim Unicode by code point and describe a byte limit. A name consisting of 59 ASCII characters plus an emoji leaves a lone high surrogate after chop(1); the probe reports trailingHighSurrogate=1. The radio accepts 61 UTF-8 bytes, not 61 characters. Preserve surrogate pairs during truncation and say “61 UTF-8 bytes” in the field description. A drop-in replacement for the trim loop is:

      while (clean.toUtf8().size() > kMaxNameBytes) {
          const qsizetype last = clean.size() - 1;
          if (last > 0 && clean.at(last).isLowSurrogate()
              && clean.at(last - 1).isHighSurrogate()) {
              clean.chop(2);
          } else {
              clean.chop(1);
          }
      }
  • src/core/DvkWavConverter.cpp:157: the body's “any channel count” claim should say 1–8 channels. The implementation intentionally rejects more than eight. State the actual supported range rather than widening the code to match the prose.

  • src/models/DvkModel.cpp:84: correct the body's firmware-following claim. The clear callback deliberately sends an additional rename to achieve the wiki's default-name behavior despite firmware preserving the name. That is a user-requested clear workflow, not an unsolicited state-restoration loop. The body discloses the extra command elsewhere, but “the code follows the firmware” is inaccurate for this case. I did not verify SmartSDR's GUI behavior or retest clear on hardware.

  • src/gui/DvkPanel.cpp:162 and :72; src/gui/MainWindow.cpp:5796: keep the disclosed TX-marker and clickable-label gaps tracked. Bridge tree inspection confirms the PLAY/F-key widgets lack a keying marker and the indicator remains a mouse-handled QLabel. These are inherited, disclosed limitations rather than newly created paths. No PLAY/F-key invocation was attempted in this review.

What I verified and tried to break

  • Built the PR head in /tmp/aethersdr-pr6247-review: named targets AetherSDR, dvk_wav_converter_test, dvk_model_status_test, dvk_wav_transfer_generation_test, dvk_availability_gate_test, dvk_wav_upload_test. Qt 6.12, shared core, -O2 -g1; all five focused CTests pass. No broad suite was run, so the author's 666-test claim was not independently reproduced.
  • Mutation evidence: linking the status test against the actual merge-base parser makes 7/30 assertions fail (23/30 pass). A converter mutation that bypasses resampling makes 9/14 cases fail (5/14 pass), showing the length/pitch checks catch the half-speed defect. Mutants were separate /tmp executables; PR sources stayed unchanged. Read the new callbacks against cancellation, duplicated replies, same-slot restart and deletion; generation/request/QPointer checks survive those paths, and the generation/upload tests pass. The new first leg's lifecycle behavior was also inspected, rather than claiming live-radio proof from injected replies.
  • Socket-free empirical probes: reproduced the pending-operation and license gaps above; compared the overlap scenario against the merge-base panel/model/transfer sources. That comparison reused the head shared library for supporting dependencies and is not a full merge-base app build. Checked the raw-command addition against the strict command-plane gate; ran engine, capability and manifest gates on both trees. EB findings remain 102, capability booleans remain 70, both manifests reproduce, and the sole manifest growth is the disclosed DvkModel includer count 1→2. Colour-audit counts remain flat. The four MainWindow a11y warnings are reproduced on the merge base, and DvkPanel has none.
  • Bridge session: launched only my head build offscreen, with fresh AETHER_SETTINGS_DIR, socket /tmp/pr6247-demo/bridge.sock, identity pr6247-review, and AETHER_AUTOMATION_NO_TX=1. Used connect list, explicitly selected DEMO-0001, connect wait, get radio, dumpTree and disconnect. The model returned model="AetherSDR Demo", serial="DEMO-0001", transmitting=false; after disconnect connected=false. The tree confirms names/descriptions for all twelve rows/F-buttons/progress bars and REC/STOP/PLAY/PREV plus the indicator. Sim has no voice keyer, so it cannot settle Flex import/preview/refusal behavior. The review instance was closed. No live radio was connected or transmitter keyed, including in the widget probes.
  • Protocol/process audit: fetched the vendor DVK wiki and checked the two-step handshake, stop syntax, idle-before-operation guidance and reserved remove semantics against it. The local scratch checkout has no FlexLib reference, so its exact DVKStatus source and the reported 24k timing/2.2s busy-window measurements remain unverified here. A separately invocable automated code-review skill was unavailable. I read the previous bot review but do not adopt its claims that triage suggestions are maintainer rulings, that the accessibility changes necessarily require unbundling, or that this generation test opens a listener: every fixture uses a null model and avoids the successful download branch.

CI and recommendation

Request changes. All required checks passed on the head, but their pull_request runs were created 2026-10-07 15:48:49 UTC, while current main 121340c08 is dated 16:14:44 UTC. With strict: false, green does not establish a build against current main. Both commits are signed and verified.

The core import fix and its rate/name regression tests are substantial and reproduce locally. Complete the pending-operation and transfer-refusal paths, remove the new above-seam command literal via the existing helper, then rerun the focused tests and fresh CI. The firmware-dependent positive claims still need the author's disclosed hardware evidence; this review did not claim to reproduce them on the demo.

GitHub rejects a formal REQUEST_CHANGES review from the authenticated PR-author account. This review therefore uses COMMENT; the substantive recommendation remains Request changes.

…ach the license latch, and Clear leaves the radio's name. Principle II.

Addresses the review on #6247 (#6244).

Admission (#6244 item 6)
- DvkModel now holds the whole admission rule: idle (or no status yet),
  no start of ours still awaiting its reply, and no WAV transfer running.
  A start is pending from the moment it is sent; the radio publishes the
  status before the reply, so the reply settles it either way. Two presses
  before the first echo now send one start, and REC or an F-key during an
  import or export sends nothing.
- STOP and Escape stop a start that is still pending, so stop is never
  weaker than start before the first status arrives.

License latch (#6244 item 5)
- A 50004001 on any transfer leg (dvk upload, the file-upload port, dvk
  download) reaches the same latch as the dvk commands, so an unlicensed
  radio's first Import also closes the gate. The latch is read through
  RadioModel::dvkLicenseRefused() and signalled as licenseFeaturesChanged(),
  beside the other entitlement inputs; MainWindow no longer includes
  DvkModel (touchpoint 2 -> 1).

Seam
- The `file upload <n> dvk_recording` leg goes through the existing
  RadioModel::requestFileUploadPort(); DvkWavTransfer no longer formats it.
  Tests replace it through a hook beside the command sender.

Radio over wiki (maintainer ruling)
- Clear sends only `dvk clear`. fw 4.2.20 keeps the slot's name, and the
  client no longer renames it to match the wiki.

Transfer
- A port the radio named but would not connect gets one retry on 42607,
  as FirmwareUploader does; an error after connecting stays a failure.
- Export accepts only the radio as its peer; another LAN host that reaches
  the listening port first is dropped and the wait continues.
- fw 4.2.20's file server answers 50000053 for ~2.2 s after an upload, so
  Import and Export stay greyed, and a transfer is refused locally, for 3 s
  after an upload.

Smaller
- PLAY and the F-keys are marked as keying TX for the automation bridge.
- Name trimming keeps surrogate pairs whole; the rename field states the
  61 UTF-8 byte limit.
- F-key name announcements fire only for a visible, real change.
- Non-finite samples encode as silence (std::clamp passes NaN through).
- The parser comment states its actual invariant.

Tests
- dvk_panel_admission_test (new, 7 cases): the reviewer's probe on a real
  DvkPanel: PREV then REC before an echo sends one start, starts during a
  transfer send nothing, STOP reaches a pending start, TX markers.
- dvk_model_status_test (39): admission, refusal latch, Clear leaving the
  name, surrogate-safe trimming.
- dvk_wav_transfer_generation_test: each transfer leg's 50004001 latches
  and closes the gate; transfer admission; the settle window; the 42607
  retry; the export peer check.
- dvk_wav_converter_test (20): a click at 0.5 s must land at output sample
  12000 from 8 k to 192 kHz, which fails at every resampled rate if the
  group-delay trim is removed.

Verified on the FLEX-8600 (fw 4.2.20), offscreen with a scratch profile and
no transmit: import, export through the peer check, and Clear keeping the
renamed slot's name. The slots were restored to their defaults afterwards.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Both reviews are addressed in b8bac9d4c. Item by item:

ten9876's review

Blocker 1: admission before the first echo, and during a transfer. Fixed.

  • DvkModel now holds the whole admission rule: idle (or no status yet), no start of ours still awaiting its reply, and no WAV transfer running.
  • A start is pending from the moment it is sent. The radio publishes the status before the reply, so the reply settles the pending start whether it succeeded or was refused.
  • DvkWavTransfer reports its running state to the model.
  • STOP and Escape stop a start that is still pending.
  • dvk_panel_admission_test replays your probe on a real DvkPanel. PREV then REC (then F1) before any echo now sends only dvk preview_start id=1. REC or F1 during a transfer sends nothing. The test is socket-free and needs no firmware peer.

Blocker 2: transfer refusals bypassed the license latch. Fixed.

  • A refusal on any transfer leg (dvk upload, the file-upload port, dvk download) calls DvkModel::noteRefusal(), the same latch the dvk commands use.
  • dvk_wav_transfer_generation_test returns 50004001 to each leg in turn and asserts both the latch and dvkIndicatorBlocker(...) == NotLicensed.

Blocker 3: a new raw command above the seam. Fixed.

  • The port leg now calls RadioModel::requestFileUploadPort(size, "dvk_recording", cb). DvkWavTransfer no longer formats file upload.
  • The generation and request identity are preserved, and tests inject the leg through a hook beside the command sender.
  • The PR body now describes the remaining funnel as exactly the two pre-existing dvk verbs.

Nits:

  • Name trimming: I used your surrogate-safe loop, and the rename field now says "61 UTF-8 bytes". There's a test with 59 ASCII characters plus an emoji.
  • "Any channel count": the body now says 1–8 channels (and 8–384 kHz), as the code enforces.
  • The firmware claim: this is now true. You ruled the radio authoritative, so Clear sends only dvk clear. Firmware 4.2.20 keeps the name and the client leaves it; this was re-verified on the 8600.
  • TX markers and the clickable label: PLAY and the F-keys are now marked with markTxKeying(), which the bridge reports as keying: true. The indicator's keyboard access is tracked in Status-bar keyer indicators are click-only QLabels (no keyboard access) #6257.

The bot's review

  • Unbundling the a11y commit: the maintainer kept it in this PR, as ten9876's review allowed.
  • parseKVs seam: the maintainer ruled to keep the shared parser, because SmartLink delivers only parsed key/values. The comment now states the actual invariant: a quote left open joins up to the next token holding a quote.
  • Removing Delete: this was already the maintainer's decision, made before the review.
  • Clear and set_name: resolved by the "radio is authoritative" ruling above. The follow-up set_name is gone.
  • Security checkbox: corrected. The body now marks the WAV parser and the export peer check as security-relevant.
  • STOP in the Unknown state: fixed together with blocker 1.
  • Announcement burst: an F-key's name change is announced only when it's visible and the name actually changed.
  • NaN reaching lround: non-finite samples now encode as silence.
  • Fallback port: a port the radio named but wouldn't connect gets one retry on 42607. An error after connecting stays a failure; dvk_wav_upload_test caught my first attempt, which retried on write errors too.
  • Busy window: Import and Export stay greyed, and a transfer is refused locally, for 3 s after an upload.
  • Group-delay trim not tested: you were right that nothing asserted it.
    • The new test puts a click at 0.5 s and requires it to land at output sample 12000 from 8, 24, 44.1, 48, 96 and 192 kHz.
    • With the trim removed, it fails at every resampled rate.
    • My first attempt at an alignment check used the tone. It aliased onto the tone's period at 96 kHz and briefly made me suspect the trim. An impulse comparison showed groupDelayInputFrames() matches the measured delay to within one sample at every rate, so the trim was right and the check was wrong.
  • Touchpoint count going backwards: fixed. The flag is read through RadioModel::dvkLicenseRefused() and signalled as licenseFeaturesChanged(), so MainWindow no longer includes DvkModel (DvkModel.h 2 → 1). TxKeyingMarker.h goes 14 → 15 for the new TX markers.
  • Export accepting any peer: fixed here, as the maintainer asked. Export accepts a connection only from the radio's address (v4-mapped allowed), and a different peer is dropped while the wait continues. This was verified live: a 96,044-byte export passed with nothing rejected.
  • dvk tests missing from ci.yml: the project runs registered tests through tests.cmake (the sanitizer lanes) rather than a ci.yml filter, so I left ci.yml alone.

Verification

  • Tests: the full ctest run passes all 667 tests offscreen.
  • Static checks: check_command_plane, check_test_registration, check_engine_boundary, check_ci_test_gate, check_qsettings, check_network_timeouts and check_capability_records pass. The touchpoint manifest was regenerated, and check_a11y reports nothing for DvkPanel.
  • Live run (FLEX-8600, firmware 4.2.20, offscreen with a scratch profile, nothing transmitted): an import, then an export through the peer check, then Clear on a renamed slot with the name kept. All twelve slots were restored to defaults afterwards.

#6251 (the DVK theming pass) is stacked on this branch and will be rebased onto b8bac9d4c.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 4 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 4
Files with findings: 1
Total findings : 4

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@jensenpat jensenpat self-assigned this Oct 7, 2026

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Approve. Reviewed b8bac9d4ce748785b61f167e759280bab3383427. This matches #6244: the import handshake, the 24 kHz mono conversion, quoted slot names, removal of dvk remove, the wiki error strings plus the 50004001 license latch, and one-operation admission from the moment a start is sent. The third commit answers the earlier review on e9584dcb. Flex-only; other families do not share DvkModel or CommandParser.

No code blockers. Required checks (build, check-macos, check-windows, Static checks) are green on this SHA. The branch is 9 commits behind main (2a89515d6); a dry merge onto that main is textually clean. The per-PR CI gate does not run the new DVK tests.

Scope

Area What changed Fit
Import handshake / format DvkWavConverter to 24 kHz mono 16-bit; dvk upload then RadioModel::requestFileUploadPort(..., "dvk_recording") #6244 items 1–2
Names parseKVs keeps a quoted value whole; the Recording* rewrite is gone; rename strips \", ', | and trims to 61 UTF-8 bytes without splitting a surrogate item 3 and item 7
Delete / clear Delete menu and dvk remove are gone. Clear sends only dvk clear (fw 4.2.20 keeps the name) item 4; Principle II
Errors / license Wiki strings; 50004001 latches through RadioModel::dvkLicenseRefused() from commands and all three transfer legs; enabled=0 is Disabled; reset on disconnect item 5
Admission Pending from send until the *_start reply; transfer and the 3 s post-upload settle block import/export; STOP reaches a pending start; PLAY and F-keys carry the TX marker item 6
A11y Names and descriptions on the same panel, slots, and indicator Disclosed, same surface, maintainer-authored
Elsewhere TCI heading rename; touchpoint count TxKeyingMarker.h 14→15; command-plane row 2→1 because both dvk verbs leave through one sendCmdPublic Disclosed funnel, not a new wire template

parseKVs is shared. A line with no open quote parses as before. Profile, meter, and GPS status still use their own parsers. The join runs only for a token that contains =" and a single quote.

Verification

Scratch tree /tmp/aether-review-pr-6247-grok at b8bac9d4, Qt 6.12.0, RelWithDebInfo. The app was not built.

Offscreen, 4/4: dvk_wav_converter_test, dvk_model_status_test, dvk_panel_admission_test, dvk_availability_gate_test.

Mutations in that tree, then restored (clean git status, tests passed again):

  • Dropping the pending check: model 38/39, panel 4/7, including the double-start and STOP-before-echo cases.
  • Leaving the resampler group delay in place: converter 14/20. Clicks from 8/44.1/48/96/192 kHz missed sample 12000 (the 8 kHz one landed at 19128).

dvk_wav_transfer_generation_test was not built or run. It is a modified socket-owning target: the new upload cases construct a QTcpSocket, and connectToHost runs only when RadioModel is non-null (these cases pass nullptr). The existing download path still calls QTcpServer::listen. CI registers it in tests/tests.cmake. The new handshake, license latch, settle window, 42607 retry, and peer predicate were read, not executed.

I did not remeasure the FLEX-8600 24 kHz playback rate or the ~2.2 s file-server window. Those remain the author's fw 4.2.20 observations. The converter tests show the client emits 24 kHz mono of the source length and pitch.

Non-blocking follow-ups

These are not merge prerequisites.

  • Export stays enabled during record, preview, and playback. Clear and Import wait for idle. A useful check is an export attempt while status is preview or playback, expecting no dvk download.
  • If a success reply were processed while status was still Idle, pending would clear and a second start could be sent. The code relies on the radio publishing status before the reply, which is what fw 4.2.20 did for the author.
  • The transfer generation test's connect and listen paths are still unexecuted. The useful socket-free coverage is already the injected command sequence; what remains is a refused connect taking the single 42607 retry, and a non-radio peer being dropped on export accept.

@jensenpat
jensenpat merged commit 248bccd into main Oct 7, 2026
7 checks passed
@jensenpat
jensenpat deleted the fix/dvk-wiki-conformance branch October 7, 2026 20:50
@ten9876

ten9876 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

#6247 merged at b8bac9d4c before I could push a second remediation pass. That pass covers an automated review of b8bac9d4c plus my own pass over those fixes, and it is now #6262 (rebased on main):

  • a later license feature … enabled=1 clears a 50004001 refusal
  • Import re-checks DVK admission when the upload starts
  • reset() leaves the transfer flag to the transfer that owns it
  • the busy window starts only after a connected upload
  • the 42607 retry also fires on the connect timeout
  • export shows why it turned a connection away
  • STOP before the echo carries the pending slot
  • an unterminated quote stops at the next quoted key
  • slot-name announcements are coalesced
  • the four MainWindow ATU/AGC actions put their disabled reason on statusTip

Each fix has a test that goes red without it. On main, 248bccd23 has CI green. Full Suite's only failure, control_receive_test, already failed on 66f393220 before this merge (#6248), and the Canary link error is also on that earlier commit.

ten9876 added a commit that referenced this pull request Oct 7, 2026
…admission when it starts, and transfers stay honest across disconnects. Principle VIII. (#6262)

## Summary

Follow-up to #6247 (#6244). A second remediation pass on #6247 produced
these fixes, but #6247 merged before they could be pushed. Each fix is
below. All of them are on the same DVK surface, and the a11y fold-in is
a maintainer call.

- **License latch:** a `license feature name=digital_voice_keyer
enabled=1` status clears an earlier `50004001` refusal, so a
subscription activated mid-session re-enables DVK without a reconnect.
The radio's licensed status outranks the earlier refusal.
- **Import admission:** Import re-checks DVK admission when the upload
actually starts. The menu item was enabled before a modal file dialog,
and the DVK may have started recording or playing while the dialog was
open.
- **Transfer flag on disconnect:** `DvkModel::reset()` leaves the
transfer flag to `DvkWavTransfer`, which clears it when its own transfer
ends, because a transfer can outlive the connection by its timeout.
- **Busy window:** it starts only after an upload that actually
connected. A refused `dvk upload` no longer turns the next try into
"file server still busy".
- **Upload port:** a named port that never answers also gets the single
retry on 42607, when the connect timeout fires, and an unparseable port
reply is logged.
- **Export peer check:** export now says why it turned a connection away
("Ignored a connection from X; waiting for the radio at Y…") instead of
timing out silently. The maintainer ruled to keep the check strict to
the radio's address.
- **STOP before the echo:** its reply carries the pending start's slot,
not −1.
- **Parser:** a quoted value left open stops at the next `key="` token,
so it can't swallow the keys after it.
- **Screen readers:** F-key name changes are announced one slot at a
time. Slot loads and disconnect resets, which change many slots at once,
stay silent.
- **MainWindow a11y (maintainer: fold in):** the four ATU/AGC menu
actions (`m_swrScanAction`, `m_preTuneAction`, `m_clearAtuAction`,
`m_agcTCalibrationMenuAction`) put their disabled reason on `statusTip`
as well as the tooltip. `check_a11y` on the touched GUI files goes from
4 findings to 0.

## Constitution principle honored

**Principle VIII.** Each fix comes with a test that fails without it.

## Test plan

- [x] Full local `ctest` on this branch: 671 of 672 pass. The one
failure is `control_receive_test`, which already fails on `main` (CI
Full Suite on `66f393220`, before any DVK change); it is tracked in
#6248.
- [x] `dvk_model_status_test` (47 cases), which adds:
- RadioModel license status clearing the refusal (only for DVK, and only
`enabled=1`)
  - the pending-slot stop tag
  - the transfer flag surviving `reset()`
  - the quote-join stop
- [x] `dvk_wav_transfer_generation_test` adds three cases:
  - Import refused while the DVK is busy
  - a refused upload leaving no busy window
  - the timeout retry on 42607
- [x] **Checked by inverting the fix:** with the license clear, the
quote stop or the busy-window guard removed, its test goes red, then
green again once the file is restored byte-for-byte.
- [x] Static gates: `check_engine_boundary` (same 102 findings as
`main`), `check_capability_records`, `check_command_plane`,
`check_test_registration`, `check_ci_test_gate` and
`gen_touchpoint_manifest --check` all pass.

## Checklist

- [x] Commits are signed
- [x] No new flat-key `AppSettings` calls
- [x] Clean-room
- [x] No meter UI touched
- [x] No CHANGELOG entry
- [x] **Security-relevant:** this touches the export peer check (no
change to its policy; it adds a visible reason)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 7, 2026
…ibility events. Principle VIII. (#6264)

## Summary

Fixes the quoted-name regression introduced by #6262, which its
[review](#6262 (review))
found, and folds in that review's accessibility finding (maintainer
call).

**Parser.** Given `id=1 name="CQ a=" duration=1000`, the parser read
`a="` as the start of another quoted field, and the DVK model displayed
`CQ`.
- A token now closes the open quoted value unless it opens another
quoted field.
- A token whose only quote is its last character always closes. So `CQ
a=`, `CQ =` and `CQ a= b=` stay whole, and the following `duration`
survives.
- One shape is genuinely ambiguous. On a malformed line, a lone
`label="` reads as the close, because it looks exactly like a valid name
ending in `=`. A test pins that case and checks that the keys after it
survive.
- The #6247 guard still holds: an unterminated value does not swallow a
later `bar="x"`.

**Accessibility.** `setAccessibleName()` and `QLabel::setText()` emit
`NameChanged` themselves, and the panel was adding events of its own on
top. The review measured 2 events for a single rename and 12–24 for a
slot load or disconnect.
- The panel no longer adds any events of its own.
- F-key and row names are set only when they change.
- The status line's accessible name holds the last announced state. Each
state change is now one event, and the 10 Hz elapsed-time text and
transfer progress add none.

## Constitution principle honored

**Principle VIII.** Each claim is backed by a test that fails without
the change. FlexLib's DVK status parsing keeps everything between the
quotes, equals signs included (Principle I).

## Validation

- **Parser:** against the merged `bc1d7b80f`, the new assertions fail 6
of 55, one per name ending in `=`. With this fix,
`dvk_model_status_test` passes 56/56, with each looped assertion naming
its value.
- **Accessibility events:** `dvk_panel_admission_test` (11/11) counts
`NameChanged` events through `QAccessible::installUpdateHandler`:
  - reloading unchanged slots: no F-key events
  - a rename: exactly one
- a state change: one status event, and nothing from the following ticks
  - a disconnect: one status event plus one for the renamed slot

  Against the previous panel, three of these fail.
- `dvk_panel_theme_test` passes 33/33.
- **Full `ctest`:** passes except `control_receive_test`. Its fix
(#6263) landed on `main` after this branch's base.
- **Static gates:** engine boundary, capability records, command plane,
touchpoint manifest, test registration and the CI gate read identically
on the merge base and the head. `check_a11y` reports nothing on
`DvkPanel`.
- **Not verified:** socket-free and offscreen only. No live radio, no
transmitting, and no real screen reader.

## Checklist

- [x] Signed commits
- [x] Socket-free regression coverage, and each guard checked by
inverting it
- [x] No new settings keys, dependencies or ratchet changes
- [x] No CHANGELOG change

Authored with OpenAI Codex. Landed with Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 12, 2026
… Principle VIII.

An audit of every page against the 106 PRs in v26.10.2, each fact checked
against the source at the release commit:

- Menu paths: AetherMap, AetherModem, Callsign Lookup and FreeDV Reporter
  are on View, and Waveforms is on Tools.
- New coverage: Windows Firewall rules and the Network Diagnostics page
  (#6289), live CW contacts (#6268), NB2 on the HL2 and ANAN-G2 (#5966),
  ANAN-G2 AGC-T and per-radio memory (#6282, #6129), HL2 state restore
  and its level changes (#6259, #6254, #5975), DVK import (#6247, #6262,
  #6251), Copy Assist from unprocessed audio (#6216), keyboard-operable
  status-bar indicators (#6267), the KPA1500 (#5905), CTR2 extensions
  and the AetherKnob (#6338), live PPM (#6082), and Icom COMP and the
  squelch scale (#6174, #6184).
- Known issues whose GitHub issues are closed are removed.
- The style guide now asks for US spelling, and every page follows it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@aethersdr-agent aethersdr-agent Bot mentioned this pull request Oct 12, 2026
2 tasks
ten9876 added a commit that referenced this pull request Oct 12, 2026
… and re-take the Stable snapshot. Principle VIII. (#6396)

## Summary

This brings the user guide (docs.aethersdr.com) up to date with
everything in v26.10.2 and re-takes the Stable snapshot so the default
site shows it. Docs only, apart from the TCI page prose in
`tools/docs/gen_reference.py`; no application source, test or CI
changes.

Every page was audited against the 106 PRs in the release. Every fact
that was added or kept was checked against the source at `f753dbfc4`,
the v26.10.2 release commit, not taken from the PR bodies alone.

## What changed

- **Menu paths.**
- AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on
**Tools**. They are created on View and then moved to Tools
(`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's
example says **Tools → AetherMap...**.
  - **Show live CW contacts** is on **View**.
  - Waveforms is on **Tools**, not File.
  - The VUDU applet button was called TXDSP.
  - Labels now match the source's `...` vs `…`.
- **Gaps from this cycle.**
- **Windows and Network Diagnostics:** Windows Firewall rules and the
Network Diagnostics Windows Firewall page (#6289); the `aether.network`
and `aether.nvafx` log categories (#6289, #6308).
  - **Remote access:** Tailscale shim revocation semantics (#6325).
  - **Sleep, App Nap and power throttling (#6195):** per-OS notes.
- **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level,
multi-interface discovery and a discovery troubleshooting entry (#6259,
#6254, #5975, #6009, #6293).
- **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply
voltage and link status (#6282, #6129, #6137, #6211).
  - **NB2** on the HL2 and ANAN-G2 (#5966).
- **DVK:** WAV import requirements, the slot menu without Delete, status
text and licensing (#6247, #6262, #6251).
  - **Copy Assist** from unprocessed audio (#6216).
  - **Live CW contacts** replace the decoder's contact card (#6268).
  - **Status-bar indicators** work from the keyboard (#6267).
- **Volume:** controllers, hotkeys and TCI `volume` follow the active
output (#6129, #6260).
- **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the
AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes
(#6031, #6038).
  - **RTL-SDR:** live PPM (#6082).
  - **Icom:** COMP and the MK2 squelch scale (#6174, #6184).
- **Known issues.** Entries whose GitHub issues are closed are removed
(each checked with `gh issue view`). As decided, this includes three
closed today without a demonstrated fix: #5107, #4824 and #5665.
- **US spelling.** `docs-style-guide.md` asked for British spelling; it
now asks for US, by the maintainer's decision, and every page is
converted. The generated TCI pages are converted at their source, the
prose strings in `tools/docs/gen_reference.py`. Code spans, links and
URLs were left alone. UI labels were checked against the source strings,
and none of them are British. Two in-page anchors and two Log Analyzer
rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings.
- **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the
release tree with these corrections, before `main` was merged in, so
Stable carries no post-release docs. `docs/` also has `main`'s
post-release pages, in US spelling. Stable alone keeps the IC-9700
waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so
26.10.2 still has it. The PDF manual already attached to the v26.10.2
release keeps the tag's docs.

## Validation

- `npm ci && npm run build` in `docs/user`: success. This is the
broken-link and broken-anchor check.
- `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules.
- `python3 tools/docs/gen_reference.py --check`: up to date (8 pages).
- `git diff --check`: clean.
- Menu paths on a build of this branch (offscreen, isolated settings, no
radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign
Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools
→ Waveforms...`, `View → Show live CW contacts`.

## Screenshots now out of date (not recaptured)

These need a recapture pass on the radio:
- **Old panadapter title strip (#6386):** `panadapter`, `main-window`,
`first-session-*`, `multi-slice-two-slices`, `split-pair`,
`tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`,
`default-light-theme`.
- **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`,
`aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`,
`connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`,
`rx-agc-calibration`.
- **Other:**
  - `title-bar` (lowercase tab states, #6281);
  - `menu-help` (#6294);
  - `display-panel` (#6371);
  - `support-diagnostics-dialog`;
  - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0);
  - `about-window` (v26.10.1);
  - `radio-setup-qrz`.

## Found during the audit, not fixed here

- **App string bug:** `TailnetShimDialog.cpp:628` tells the user to
"Connect remotely with Connect → Manual"; the real path is **File →
Connect to Radio... → Connect by IP**.
- **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the
slider position as dB, which is wrong on ANAN. #6282 lists this as a
follow-up.
- **MIDI Master Volume on non-Flex radios:** it is refused while PC
Audio is off, whereas hotkeys, FlexControl and HID drive the line out.
The docs describe the code as it is.
- **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the
source strings are `FMN` and `CWR`. Left as is until someone confirms
what the mode menu displays.
- **IC-705 COMP readback:** inferred from the MK2 (#6174) and not
verified on an IC-705.
- **Not yet documented:** the #6230 Radio Health row "Forward responses
skipped", and the Store/MSIX package.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DVK diverges from the SmartSDR API wiki: import is broken and plays at the wrong rate, slot names are cut off

2 participants