fix: WinKeyer 2 sent the dit/dah ratio to the key-compensation command

The byte trace from a real WK2 settled in one line what no amount of reading
could: "TX 11 32". Command 0x11 is SET KEY COMPENSATION in milliseconds, not the
dit/dah ratio — that is 0x17. So a neutral ratio of 50 asked for 50 ms of extra
key-down on EVERY element. At 25 wpm a dit is 48 ms, so elements more than
doubled and ran into each other: the reported "it sends one element, then a long
pause".

The ratio now goes out as 0x17, and the compensation is explicitly set to 0 —
merely stopping the wrong command would leave an affected keyer misbehaving,
since it keeps the value in EEPROM until something writes over it.

The init sequence is now built by a separate function so the BYTES are testable,
and a test pins each command number. These numbers are the contract with the
hardware, and a wrong one produces a fault that cannot be diagnosed from the UI
at all — this one cost the operator weeks and needed a trace to find.

This is why the fix waited for the trace rather than being guessed: the plausible
guesses (mode register, sidetone, WK1-vs-WK2 differences) were all wrong, and any
of them shipped blind would have broken the keyers that work today.
This commit is contained in:
2026-07-29 14:53:18 +02:00
parent c7737aabd6
commit 15fdbce22a
3 changed files with 98 additions and 5 deletions
+25 -5
View File
@@ -243,21 +243,39 @@ func (m *Manager) connectSerial(cfg Config) error {
return nil
}
// applyConfig pushes the keying parameters to the device.
func (m *Manager) applyConfig(c Config) error {
// configCommands is the init sequence for a config — separated from the sending
// so the BYTES can be tested. The command numbers are the contract with the
// hardware, and a wrong one produces a keyer that misbehaves in a way no amount
// of reading the UI explains.
func configCommands(c Config) [][]byte {
cmds := [][]byte{
{0x0E, modeRegister(c)}, // set mode register (paddle mode, swap, autospace…)
{0x02, byte(c.WPM)}, // set speed (WPM)
{0x03, byte(c.Weight)}, // set weighting
{0x04, byte(c.LeadInMs / 10), byte(c.TailMs / 10)}, // PTT lead-in / tail (10 ms units)
{0x11, byte(c.Ratio)}, // set dit/dah ratio
// Dit/dah ratio is 0x17. It was being sent as 0x11, which on a WinKeyer 2
// is SET KEY COMPENSATION — in milliseconds. So a neutral ratio of 50 was
// read as 50 ms of extra key-down on every element: at 25 wpm a dit is
// 48 ms, so each element more than doubled and ran into the next. That is
// the reported "it sends one element then stalls", and it was in the trace
// as "TX 11 32".
{0x17, byte(c.Ratio)},
// And clear the compensation explicitly. A keyer left at 50 ms by the
// previous version — or by another program — keeps it in EEPROM, so
// merely stopping the wrong command would not fix an affected keyer.
{0x11, 0x00},
}
// Sidetone: <0x01 n>. Bit6 enables, low nibble selects the pitch divisor.
cmds = append(cmds, []byte{0x01, sidetoneCode(c.Sidetone)})
if c.Farnsworth > 0 {
cmds = append(cmds, []byte{0x0D, byte(c.Farnsworth)}) // Farnsworth WPM
}
for _, cmd := range cmds {
return cmds
}
// applyConfig pushes the keying parameters to the device.
func (m *Manager) applyConfig(c Config) error {
for _, cmd := range configCommands(c) {
if err := m.write(cmd); err != nil {
return err
}
@@ -478,10 +496,12 @@ func cmdName(b []byte) string {
return "clear buffer"
case 0x0D:
return "farnsworth wpm"
case 0x17:
return "set dit/dah ratio"
case 0x0E:
return "set mode register"
case 0x11:
return "0x11 (WK2: key compensation / WK3: see datasheet)"
return "set key compensation (ms)"
case 0x15:
return "request status"
default: