feat(fsk): FSK/AFSK/PSK text send and receive - #9
Open
tcpreplay-dev wants to merge 7 commits into
Open
tcpreplay-dev wants to merge 7 commits into
tcpreplay-dev wants to merge 7 commits into
Conversation
Pure rename, no behavior change. The CW Send dialog and its controller are about to grow a second session mode, so the "Cw" in the type names stops being accurate. CwSendController -> TextSendController CwSendDialog -> TextSendDialog test_cwsendcontroller -> test_textsendcontroller RadioSettings' cwSendImmediateMode accessors and the "cwSend/immediateMode" key are untouched — renaming those would orphan persisted settings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The K4 renders KY text as CW or as the active DATA sub-mode's signal, and
decodes both through the same TD/TB plumbing, so AFSK-A / FSK-D / PSK-D can
reuse the CW Send dialog end to end. The bottom-bar button now reads
PTT / CW / AFSK / FSK / PSK and opens the dialog in the last four. DATA-A is
excluded — it exists for external software and decodes no text.
Controller, keyed off a new SessionMode:
* CW keeps its sacrificial 'E' lead-in; FSK prepends nothing, since the
signal is generated digitally and any invented character would print on
the far end. The 60-char KY limit is shared with the lead-in.
* FSK wraps a transmission in TX; / RX; with a settle delay after and a
hang delay before. Whoever opens the bracket owns closing it, so a mode
change, stall or abort all close it; only a dead link drops it silently.
* Stall timeout from the DR data rate rather than keyer WPM.
The dialog's title, prosign legend and highlighting, and case forcing follow
the session. A CW <-> FSK switch drops a divider rather than clearing, since
the controller's offsets are monotonic and survive its reset.
Text decode is enabled automatically for a receiver in an FSK sub-mode and
restored when it leaves, only ever undoing what it did.
"Active receiver" now follows B SET for CW as well as FSK, via new
RadioState::activeMode()/activeDataSubMode()/activeDataRate() — previously
sub RX in CW left the button reading "PTT".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QK4 is a remote head for the real radio, so the dialog opens with the same picture the front panel shows: A and B squares with each receiver's mode beneath, and the TX arrow between them. Clicking the arrow switches TX sides via SW145; — the same CAT the right-panel SPLIT button uses, which is the path already proven against the radio. While keyed, the transmitting VFO's square goes TxRed, driven off transmitStateChanged so it tracks real keying. Geometry and color are copied from the main display rather than re-approximated, and the RX/TX headings become the K4's filled tags. Also fixes a stall that was arithmetic, not a link fault: a character at RTTY45 budgeted 760ms against a 700ms TX hang, so any typing gap closed the bracket and the next character had to cover a whole re-key cycle inside its own deadline. Hang goes to 3s, with a 3s FSK timeout floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Send immediately" and "Hold until Enter" were two booleans expressing three mutually exclusive behaviors, badly: with Hold checked, Send immediately silently did nothing. Replaced by one combo — "Send on: Each character / Each word / Enter key", persisted as textSend/sendMode and migrated from the old bool. "Each character" is absent in FSK, where every character costs a KY0 round trip and typing faster loses text. Enter now sends while holding, and ends what it sends with a space — consecutive Enter-released lines were running together on the air. The TX indicator no longer greys out when it can't be switched: disabling the button handed it to Qt's disabled palette, so it rendered grey while the radio was keyed. It stays enabled and ignores the click. Sub RX decode never came on when Sub RX was switched on with VFO B already in FSK. The K4 does not echo a TD SET — same as DT and DR — so RadioState kept believing the decoder was off until a reconnect re-queried it. sendTextDecodeCmd() now applies what it asked for optimistically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sent text is split into one pane per transmitting VFO, each headed with its frequency, so a line's pane records the frequency it went out on. That meant mapping the controller's monotonic character offsets through an explicit segment list, since the stream now renders across two panes and the dialog still inserts dividers the controller never counts. The strip is completed to match the main display: both frequencies with the same shading, VFO B dimmed with its mode label when Sub RX is off, SPLIT ON/OFF under the arrow, and the SUB / DIV badges. formatFrequency() moves into RadioUtils so it isn't duplicated. Switching TX to B no longer requires Sub RX — that is ordinary split operation — which is also why SPLIT ON/OFF had to become visible here. Includes a crash fix and the suite's first widget test. updateVfoStrip() ran partway through the constructor while the TX pane headers it now fills were still null, segfaulting on open. Nothing in the suite constructed a widget, so nothing could have caught it; tests/test_textsenddialog.cpp builds the dialog and drives every RadioState signal it observes, and was confirmed to reproduce the crash with the fix reverted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
Author
Review of the whole stack, plus a night left receiving:
* Sent text could land in a hidden pane. The Sub TX pane's visibility
required VFO B to be in the session's mode, but the pane that RECEIVES
sent text is chosen by splitEnabled() alone — so with split switched on
from the main window and B in CW or DATA-A, every character went into a
hidden widget. The text still went on the air; the dialog showed
nothing. Visibility now starts from activeTxIsSub().
* A macro clicked during a stall silently destroyed its text.
finishPendingWord() cleared the input unconditionally while commitText()
returned early on m_stalled, and the macro buttons were never disabled.
commitText() now reports whether it accepted the text, no caller clears
the input when it didn't, and the buttons grey out for the stall.
* The optimistic text-decode push re-entered its own handler. All three
setTextDecode* setters emit textDecodeChanged synchronously, and that
handler exists to sync the window FROM the radio — re-entered mid-push
it wrote our own values back into the window they came from, reading
whichever fields weren't set yet: enough to clamp the -1 "never
received" lines sentinel into the window and flip auto-threshold off in
a CW session. Now bracketed by a guard the handler checks, with the
lines value captured before the send.
* The RX decode buffers were unbounded. Both trimmed only by block count,
and blockCount only grows on a newline — decoded RTTY/PSK arrives from
TB as a character stream with no line breaks, so both documents stayed
at one block and neither trim ever fired. A night of receive is one
QTextDocument block growing without bound, which is what made the app
sluggish. Both now enforce a 16 KB character cap alongside the line cap.
This also fixes the floating TEXT DECODE windows, which had the same
bug independently of this stack.
The TX panes stay uncapped deliberately: they only grow with what the
operator types, and trimming them would shift every docStart in the segment
map.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The K4 renders its own TX decode row locally and publishes nothing: a raw CAT capture during an M1 playback showed no command carrying the text, in TB or anywhere else. So the dialog can't show what a message memory sent — but it can show that one went out, and when, in sequence with typed text. That same capture settled what TB's first field is. It ramps 1,2,4,3,...,0 across a playback and is identical on TB and TB$ at the same instant, and no frame with a non-zero value ever carries text. It is a global TX-buffer level, not a TX/RX discriminator — the text in a TB frame is always received text. RadioState now parses and exposes it. An idle -> busy edge on that level, while TextSendController is inactive, is a transmission we didn't queue. The dialog writes "--- M1 message sent ---" into the TX pane, through the same path as the session divider so the offset segments stay correct. The button label comes from QK4 having sent the switch tap itself moments earlier: M1-M4 are bare SW taps with no CAT echo, so there is no other way to know which memory ran. A press on the radio's front panel, or one older than 5s, falls back to "--- radio message sent ---". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Stacked on #8.
The K4 renders
KYtext as CW or as the active DATA sub-mode's signal, and decodes both through the sameTD/TBplumbing — so AFSK-A / FSK-D / PSK-D reuse the CW Send dialog end to end. The bottom-bar button reads PTT / CW / AFSK / FSK / PSK and opens the dialog in the last four. DATA-A is excluded: it exists for external software and decodes no text.Controller (
CwSendController→TextSendController, first commit is a pure rename):Elead-in; FSK prepends nothing, since the signal is generated digitally and any invented character would print on the far end.TX;/RX;. Whoever opens the bracket owns closing it — a mode change, stall or abort all close it; only a dead link drops it silently.DRdata rate rather than keyer WPM.Dialog: session-aware title, prosigns and case forcing; sent text split into one pane per transmitting VFO so a line records the frequency it went out on; and the K4's own A / TX / B strip, with both frequencies, SPLIT ON/OFF, SUB / DIV, and a clickable TX arrow that switches sides via
SW145;.Text decode is switched on automatically for a receiver in an FSK sub-mode and restored when it leaves, only ever undoing what it did.
Also fixes, from live on-air use:
TDSET (same asDTandDR), so decode state is now updated optimistically; without it Sub RX decode never appeared until a reconnect.Testing: 16 test targets pass. Adds
tests/test_textsenddialog.cpp, the suite's first widget-level test — a null-deref in the dialog constructor shipped because nothing built a widget. It was confirmed to reproduce that crash with the fix reverted.Two things remain unverified against hardware and are noted in the code: whether
KYauto-keys in data modes (theTX;/RX;bracket is correct either way), and whetherRDYcarriesDT/TD.🤖 Generated with Claude Code