feat(halikey): Straight Key / Bug mode - #3
Open
tcpreplay-dev wants to merge 6 commits into
Open
tcpreplay-dev wants to merge 6 commits into
tcpreplay-dev wants to merge 6 commits into
Conversation
tcpreplay-dev
force-pushed
the
feature/halikey-straight-key-bug
branch
from
August 23, 2026 19:55
526ae57 to
2beee8e
Compare
tcpreplay-dev
force-pushed
the
feature/halikey-straight-key-bug
branch
2 times, most recently
from
August 23, 2026 22:29
16e73d3 to
ce45422
Compare
4 tasks
tcpreplay-dev
marked this pull request as ready for review
August 25, 2026 17:39
Bypasses the iambic keyer entirely when enabled. Raw paddle contact closures (DIT, or DIT+DAH for a semi-automatic bug) drive TX;/RX; directly instead of the KZ element protocol, matching how a hand key or bug physically keys a transmitter — arbitrary-duration keydown, no element shaping. Verified against real K4 hardware: TX;/RX; toggles TQ0/TQ1 cleanly with no side effects observed at QRP power into a dummy load. Adds SidetoneGenerator::startHold()/stopHold() for continuous local sidetone feedback while the key is held, since playSingleDit/Dah only support fixed-length elements. Guards against a stuck key on HaliKey disconnect, radio disconnect, and mode-disable mid-press, plus a 3-minute watchdog as a last-resort backstop.
- DIT is now always ignored in Straight Key / Bug mode, even for the V1.4 pttStateChanged fallback path — previously it still fed the iambic keyer, so a mono-plug key or the paddle's dit lever would key the transmitter. Only DAH drives the straight-key edge. - Sidetone hold (startHold/stopHold) redesigned around a bounded 30ms lookahead buffer fed by a 10ms timer instead of matched 30ms/30ms chunk+interval, which underran under scheduling jitter and caused scratchy audio and lag at high WPM. - Revert the VXC1; VOX-CW auto-arm added to chase "transmitter doesn't key in straight-key mode" — confirmed against real hardware that arming VOX-CW does not fix it; TX; is accepted but produces no carrier on the K4/0 remote protocol (9204/9205), only on the legacy port (9200). Root cause and next steps documented on CwController::handleStraightKeyEdge(). - Straight Key UI help text updated to match (DAH-only wiring, known TX limitation called out explicitly). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…state
Checkpoint ("state A") for the version running during the first live test,
where keying "sounded like the bug plugged into the back of the radio."
Known bug, reinstated deliberately: gating each SW16; send on
m_cachedIsTransmitting causes a silent, permanent lockout if that cache
ever reads stale mid-session (confirmed on real hardware at ~35 WPM) —
see the doc comment on CwController::handleStraightKeyEdge() for the
full failure mode and what was tried instead.
Not a fix. Recoverable reference point while deciding the next approach.
Belongs with d056625 ("state A") — split out because it was staged separately. No behavior change.
Replaces the TUNE-based keying attempt. Each contact closure is timed and, on release, emitted as a single KZ element whose length is set by solving the keyer speed for the held duration: KZ.; for holds up to 150ms, KZ-; above it, covering ~20-450ms continuously. Gaps become KZ ; letter spaces. Hardware findings behind this (K4, measured with millisecond CAT traces): - TX;/RX; produces no carrier in CW mode on the K4/0 remote protocol (9204 TLS and 9205 plain alike), and only PTT on legacy CAT 9200, where rapid alternation merges into one continuous transmission. - TUNE keys a real carrier but commits to a cycle per request on every port, so operator-paced keying degrades badly. Flooding TUNE does yield fast elements, but their length is set by the radio's command parser rather than the operator, so it cannot carry Morse. Both the SW16; toggle and absolute TU1;/TU0; were tried; the toggle additionally has no fixed point and stuck the transmitter repeatedly on real hardware. - KZL is inert for KZ elements on this path. Element length follows KS. - ~380ms TX;/RX; intervals are CW transmit-state hang, not element length; CAT state pushes cannot be used to measure the RF envelope. KZ is what the radio actually exposes for remote keying, which is why the iambic keyer has always worked. Cost is ~one element of RF latency, since the duration is unknown until release; local sidetone is driven from the contact edge and stays real-time, so sending feel is unaffected. Straight-key mode borrows KS per element and restores the operator's speed when disabled or disconnected; letter-gap detection uses the speed captured at enable rather than the per-element churn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sends one KZD<gap>U<duration>; per element, 4-digit milliseconds, where D is the key-up gap preceding the element and U is how long the key was actually held. The operator's real timing goes on the air: weighting, spacing and element length all carry through, up to the 9999ms field limit. Replaces the previous approach, which solved keyer speed per element and fired KZ.;/KZ-;. That was built on the belief that the K4/0 remote protocol had no arbitrary-duration keying primitive. It has one; this is it. Dropping the workaround removes all three of its defects: elements quantized to dit/dah, a ~450ms ceiling from the 8 WPM clamp, and churning the operator's KS setting as a side effect of sending. Verified against hardware before implementing: - Two KZD/U commands sent back-to-back play as two distinct elements, so the radio queues them. No send-side pacing or throttling is needed. - U2000 plays a 2s element, so the field is not clamped at 1000 the way KZL is. m_k4BusyUntilMs tracks when the radio finishes what it has been given. The radio applies D as silence after whatever is still playing, so any interval it has already spent idle is subtracted from D — otherwise a pause would be counted twice, once in real time and again by the radio. A gap longer than the backlog drives this to zero, so any pause in a QSO resynchronizes and the next element goes out immediately. Timing is carried inside the command, so fidelity no longer depends on when the command reaches the wire — only the edge timestamps must be precise. Those are taken on the HaliKey worker thread against a free-running QElapsedTimer, matching IambicKeyer::m_pressClock. Also fixed in passing: - The stuck-key watchdog was dead code: kStraightKeyMaxHoldMs was never read and start() was never called anywhere. Now armed on key-down, since an element is only emitted on release and a wedged contact would otherwise mean silence on the air plus an endless sidetone. - KS was restored twice when the mode was disabled. Remaining limitation: the radio lags by one element, inherent to not knowing a duration until the key is released. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tcpreplay-dev
force-pushed
the
feature/halikey-straight-key-bug
branch
from
August 25, 2026 21:34
8eb716b to
7615498
Compare
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.
Adds a Straight Key / Bug mode to the HaliKey CW keyer, alongside the paddle-swap
toggle in #1 (this stacks on that branch). Bypasses the iambic keyer so a straight key or
semi-automatic bug plugged into the PC keys the K4 over the network.
Draft — the keying mechanism works but element timing is quantized; see limitations below.
How it keys
Each contact closure is timed and, on release, emitted as a single
KZelement whoselength is set by solving the keyer speed for the held duration —
KZ.;for holds up to150ms,
KZ-;above it, covering roughly 20–450ms continuously. Gaps becomeKZ ;letterspaces.
DIT is always ignored; DAH is the sole input. On HaliKey V1.4 the dit lever and the foot
pedal are electrically indistinguishable, so routing DIT into this mode would key on a
pedal press, and a mono-plug key shorts both. Wire the key to DAH.
Why
KZ, and not the obvious alternativesEach of these was tried against real hardware and measured with millisecond CAT traces:
TX;/RX;on 9204 / 9205TX;/RX;on 9200SW16;(toggle)TU1;/TU0;(absolute)KZelementsKZis what the radio actually exposes for remote keying — it's why the iambic keyer hasalways worked.
Two measurement notes worth recording, since both produced misleading results along the way:
KZLis inert forKZelements on this path. Element length followsKS.TX;→RX;intervals seen throughout are CW transmit-state hang, notelement length. CAT state pushes cannot be used to measure the RF envelope — only a
receiver can.
Limitations
driven from the contact edge and stays real-time, so sending feel is unaffected.
KSis being used as the lengthcontrol. The operator's speed is captured on enable and restored on disable/disconnect.
A genuine remote CW key-down primitive would make all of this unnecessary; a support
request to Elecraft is drafted separately.
Testing
Built and exercised against a real K4 (dummy load, reduced power) throughout development.
IambicKeyerTestspasses.