Repository navigation
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
Conversation
…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>
Accessibility check findingsThis PR touches
See 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>
4c38a5c to
e9584dc
Compare
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
1 similar comment
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
There was a problem hiding this comment.
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 theUnknownstate, while start stays open.canStartOperation()deliberately fails open onUnknown(DvkModel.h:34), so REC can start before anydvkstatus has arrived. ButstopActiveOperation()switches onm_model->status()anddefault: breaks — so with status stillUnknown, STOP and Esc send nothing. The old code fell back tom_selectedSlot. The REC toggle itself still stops (it callsrecStop()unconditionally on uncheck), so this is recoverable, not a stuck transmitter — but the asymmetry is backwards: if start fails open, stop should too. Suggestdefault:falling through to the previously-active id, or at minimumrecStop()whenm_recBtn->isChecked(). -
src/gui/DvkPanel.cpp:287-300— theNameChangedburst isn't throttled.docs/a11y.md:132says "A noisyupdateAccessibilityis worse than noupdateAccessibility— prefer slightly stale to spammy."updateSlotAccessibilityfires unconditionally: 12 times in the constructor on not-yet-visible widgets, 12 more as the slot list loads on connect, and 12 more onreset()at disconnect (DvkModel.cpp:218-220emitsrecordingChangedfor 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::lroundcan be reached with NaN (credit: security-audit (CodeGuard),codeguard-0-input-validation-injection).decodeSamplezeroes only non-finite inputs, so eight finite channels nearFLT_MAXsum toinf; the resampler can turn that into NaN;std::clamp(NaN, -1, 1)returns NaN unmodified (neither comparison is true), sostd::lround(NaN)is reached — unspecified result plusFE_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 indecodeSample: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,WaveformInstallerandProfileTransferall define the same42607, 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 atRadioModel::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_transferringon socket close, sonotBusygoes true while the radio's file server is still busy, and a prompt second Import or Export earns50000053. The PR confirmed the window live and added the readable message for50000053, 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. Givendvk id=1 name=" duration=0 other="x", the forward scan joins through toother=", swallowingduration. 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.estimateHzandrmsboth measure only[size/10, size-size/10), and the exact-length check is satisfied by the trim-then-resize(wanted)pad regardless of whetherleadis right. So the 14 cases would still pass withleadset 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-90solves 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:198does the fractionalllroundthat 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. AlsoDvkPanel.cpp:635setssetMaxLength(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.hgoes 1 → 2 becauseMainWindow.cppnow includes it to reachDvkModel::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. RoutinglicenseRefused()throughRadioModel(which already ownslicenseFeatureSeen/licenseFeatureEnabledfor 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_dvkIndicatorstays a click-handledQLabel.docs/a11y.md:255-256says the minimum remedy issetFocusPolicy(Qt::TabFocus)plus akeyPressEventhandler, 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.ymlreturns nothing, sodvk_wav_converter_test,dvk_model_status_testand the newdvk_wav_transfer_generation_testcases 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 onQHostAddress::Anyand writes whatever the first accepting peer sends into the operator's save path, with no check ofm_client->peerAddress()againstm_model->radioAddress()(DvkWavTransfer.cpp:190,handleNewConnection→receiveDownloadBytes). Anyone on the LAN who wins the race controls the exported file. This is onmainand 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 aQTcpSocketvia the production path. Those cases never reach the wire:handleUploadPortReceived's connect lambda is guarded bytransfer->m_model(DvkWavTransfer.cpp:474) and the tests passDvkWavTransfer(nullptr)with am_commandSenderstub, 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 (nodvkentry inci.yml's-Rfilters).tests/dvk_wav_converter_test.cpp,tests/dvk_model_status_test.cpp— socket-free, anddvk_model_status_testdrives the realCommandParser, 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)withsizeaquint32widened toqint64cannot overflow and advances ≥ 8 per iteration, so thepos + 8 <= wav.size()guard both terminates and keeps thepos+4size read in bounds.available = min(size, wav.size() - body)means a lying chunk size can't widen any read;available >= 16coversf+0..15and the extensible sub-format tag atf+24..25is correctly gated onavailable >= 26.blockAlign >= channels * bytesPerSamplemakesdecodeSample'sc*bps + bpsstay inside the frame and rules out a zero stride, andframes = dataBytes / blockAlignkeeps the last frame at or beforedata + dataBytes. Truncateddatasize,data-before-fmt, aLISTchunk first, duplicatefmtchunks, a0xFFFEtag with a short chunk, and 0-bytedataall land on an error path rather than a read. Thememcpyis a fixed-sizeof4-byte type-pun with no computable size. - The unbounded resampler feed.
convertForRadiohands up to 3.84 M samples to aResamplerbuilt with the defaultmaxBlockSamples = 4096, andResampler.cpp:36-38says in so many words that r8brain "does not bounds-check againstaMaxInLen; exceeding it silently overflows internal filter buffers." It survives only becauseResampler::processchunks 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 —framesis bounded by10 × 384 kHzregardless. - The trim arithmetic going negative or truncating.
lead >= 0,wanted > 0,first ∈ [0, outCount],count = min(wanted, outCount - first), andcount <= wantedsoresize(wanted)only ever pads. In-bounds even whenconvertedis empty. And the trim's direction is right:QsoPcmConverter.cpp:74("Resampler already prependeddelaysilent source frames") andResampler.cpp:174-178together establish that output lags input by the group delay afterprewarm(), so subtractingleadat the head anddrain()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
generationand a freshrequestId, andm_portRequestIdis 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 byisCurrent(generation). Thefile uploadreply also checkscode != 0before the port fallback, so a radio refusal can't be laundered into a connect to 42607. - The parser blast radius, independently. Enumerated all 7
parseKVscall sites and every status shape I could find that carries a quote or a space: xvtrname="23cm"(two quotes → untouched, bit-identical), Rapid-M"text=…"(no="substring → never triggers), meter#-separated and profile^-separated lines (no quotes, and both bypassparseKVsanyway). The join loop is linear, not quadratic, andendis bounded bytokens.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.
validateWavFileis gone and has no remaining callers;MAX_FILE_SIZEis still live on the download path (DvkWavTransfer.cpp:267) and indvk_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. TheblockSignals(false)un-check dance was replaced by anonStatusChangedre-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:318asserts 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,clearkeeping the name), theDVK.cs:357 DeleteRecordingcitation (reference/isn't in this checkout), whetherQProgressBar::setValuefires its ownQAccessibleValueChangeEventand 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
left a comment
There was a problem hiding this comment.
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
-
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 headDvkPanelandDvkModelseeded 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=1This 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=1Add 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
SimBackendexplicitly setshasVoiceKeyer=false; this evidence is widget replay, not a live-radio observation. -
src/core/DvkWavTransfer.cpp:396and:429(also:162) — transfer refusals never reach the license latch. The new latch is set only byDvkModel::handleCommandResponse(), but the transfer callbacks call only the static error-string helper andfinish(). Import is especially important: it is available on an empty slot and can be an unlicensed user's first action.I constructed a disconnected
RadioModelplus its realDvkWavTransfer, injected only command replies through the new test hook, and returned50004001to 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=1The 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. -
src/core/DvkWavTransfer.cpp:407— a new raw Flex command is added above the seam despite the smaller counter. The newfile upload %1 dvk_recordingliteral is abovesrc/core/backends/, and the funnel forwards it throughsendCmdPublic. 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)atsrc/models/RadioModel.cpp:9436for 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 afterchop(1); the probe reportstrailingHighSurrogate=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:162and: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 targetsAetherSDR,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
/tmpexecutables; 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, identitypr6247-review, andAETHER_AUTOMATION_NO_TX=1. Usedconnect list, explicitly selectedDEMO-0001,connect wait,get radio,dumpTreeanddisconnect. The model returnedmodel="AetherSDR Demo",serial="DEMO-0001",transmitting=false; after disconnectconnected=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>
|
Both reviews are addressed in ten9876's reviewBlocker 1: admission before the first echo, and during a transfer. Fixed.
Blocker 2: transfer refusals bypassed the license latch. Fixed.
Blocker 3: a new raw command above the seam. Fixed.
Nits:
The bot's review
Verification
#6251 (the DVK theming pass) is stacked on this branch and will be rebased onto |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
jensenpat
left a comment
There was a problem hiding this comment.
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
previeworplayback, expecting nodvk 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.
|
#6247 merged at
Each fix has a test that goes red without it. On |
…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>
…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>
… 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>
… 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>
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.
dvk upload id=N(empty reply; it only names the slot), thenfile 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.DvkWavConverteraccepts 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 r8brainResampler, and sends 16-bit PCM. Clips over 10 s are refused.Slot names keep their spaces.
CommandParser::parseKVsnow keeps a quoted value whole, soname="CQ Contest"no longer shows asCQ. 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
idthe wiki doesn't use.enabled=0reads as Disabled, as in FlexLib'sDVKStatus.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:
5000400150001000E200000050000053A
50004001refusal on any DVK path now marks DVK as not licensed in the availability gate, because the radio'senabledflag 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 throughRadioModel::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:
FirmwareUploaderdoes.50000053for 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_recordingleg goes through the existingRadioModel::requestFileUploadPort(), soDvkWavTransferadds no new wire text. Its twodvkverbs (upload and download) go through onem_model->sendCmdPubliccall, with a test hook beside it, so theDvkWavTransfer.cpprow drops from 2 to 1. This is a funnel, not a removal: bothdvktransfer 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.mdis regenerated becauseMainWindow.cppnow includesDvkModel.hexplicitly.Accessibility (#4896), second commit:
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 radioinforeply has its own comma parser and isn't affected.Constitution principle honored
Principles I and II. The wire behaviour follows FlexLib's
DVK.csand the vendor's SmartSDR API wiki, checked against a real radio. Where firmware 4.2.20 differs from the wiki (clearkeeps the name, downloads are mono), the radio wins: the code follows the firmware, and the comment names the version.Test plan
ctestsuite: 667 tests, offscreen.clearkeeps the name and that downloads are mono.duration=2000, and they previewed in 1.99 s and 2.00 s.dvk upload id=1, thenfile upload 96044 dvk_recording. The radio reportedduration=2000and the row shows 2.0s.CQ Contest | KK7GWYbecameCQ Contest KK7GWY, and the full name came back from the radio.dvk clear. The radio kept the name, thenset_name "Recording 1"reset it, and the row shows Recording 1 / Empty.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 realCommandParserwith 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 realDvkPanel: 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's50004001closing the gate, the settle window, the 42607 retry, and the export peer check.dvk_availability_gate_test: the refusal cases.tools/check_a11y.pyreports nothing forDvkPanel. Its fourMainWindow.cppwarnings are on ATU/AGC menu actions and are also present on main. Not checked with a real screen reader.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:
50004001) path. The test radio is licensed, so that path is covered by unit tests only.Follow-up, not in this PR:
Checklist
docs/COMMIT-SIGNING.md)AppSettingscallsMeterSmoother: no meter UI touched🤖 Generated with Claude Code