Skip to content

feat(fsk): FSK/AFSK/PSK text send and receive - #9

Open
tcpreplay-dev wants to merge 7 commits into
feature/cw-rx-decodefrom
feature/fsk-text-send
Open

tcpreplay-dev wants to merge 7 commits into
feature/cw-rx-decodefrom
feature/fsk-text-send

Conversation

@tcpreplay-dev

Copy link
Copy Markdown
Owner

Stacked on #8.

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 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):

  • 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.
  • FSK brackets a transmission in 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.
  • Stall timeout from the DR data 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:

  • "Active receiver" now follows B SET for CW as well as FSK — sub RX in CW previously left the button reading "PTT".
  • The K4 does not echo a TD SET (same as DT and DR), so decode state is now updated optimistically; without it Sub RX decode never appeared until a reconnect.
  • A stall while typing slowly that was arithmetic, not a link fault: a character at RTTY45 budgeted 760ms against a 700ms TX hang.

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 KY auto-keys in data modes (the TX;/RX; bracket is correct either way), and whether RDY carries DT/TD.

🤖 Generated with Claude Code

tcpreplay-dev and others added 5 commits August 30, 2026 22:28
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>
@tcpreplay-dev

Copy link
Copy Markdown
Owner Author

Sample Screen Shot:
image

tcpreplay-dev and others added 2 commits August 31, 2026 12:18
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>
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.

1 participant