Skip to content

feat(halikey): Straight Key / Bug mode - #3

Open
tcpreplay-dev wants to merge 6 commits into
feature/halikey-paddle-swapfrom
feature/halikey-straight-key-bug
Open

tcpreplay-dev wants to merge 6 commits into
feature/halikey-paddle-swapfrom
feature/halikey-straight-key-bug

Conversation

@tcpreplay-dev

Copy link
Copy Markdown
Owner

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 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 roughly 20–450ms continuously. Gaps become KZ ; letter
spaces.

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 alternatives

Each of these was tried against real hardware and measured with millisecond CAT traces:

Mechanism Result
TX;/RX; on 9204 / 9205 Accepted, but no carrier in CW mode
TX;/RX; on 9200 PTT only — rapid alternation merges into one transmission
TUNE via SW16; (toggle) Real carrier, but a toggle has no fixed point; stuck the transmitter repeatedly
TUNE via TU1;/TU0; (absolute) Real carrier, but commits to a cycle per request on every port; operator-paced keying degrades badly
TUNE, flooded Fast elements, but their length is set by the radio's command parser rather than the operator, so it can't carry Morse
KZ elements Real CW at full speed

KZ is what the radio actually exposes for remote keying — it's why the iambic keyer has
always worked.

Two measurement notes worth recording, since both produced misleading results along the way:

  • KZL is inert for KZ elements on this path. Element length follows KS.
  • The ~380ms TX;RX; intervals seen throughout are CW transmit-state hang, not
    element length. CAT state pushes cannot be used to measure the RF envelope — only a
    receiver can.

Limitations

  • ~one element of RF latency. The duration isn't known until release. Local sidetone is
    driven from the contact edge and stays real-time, so sending feel is unaffected.
  • Element length capped near 450ms by the radio's minimum keyer speed (8 WPM).
  • No weight or swing. A straight key's analog envelope can't survive this protocol.
  • The radio's speed display churns while sending, since KS is being used as the length
    control. 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.
IambicKeyerTests passes.

tcpreplay-dev and others added 6 commits August 25, 2026 14:33
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
tcpreplay-dev force-pushed the feature/halikey-straight-key-bug branch from 8eb716b to 7615498 Compare August 25, 2026 21:34
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