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:
@@ -0,0 +1,71 @@
|
||||
package winkeyer
|
||||
|
||||
import "testing"
|
||||
|
||||
// The init sequence sent to a K1EL keyer, byte for byte.
|
||||
//
|
||||
// This exists because of a bug that no amount of reading caught and that a
|
||||
// byte-level trace from a real WK2 settled in one line: the dit/dah ratio was
|
||||
// sent as command 0x11, which is SET KEY COMPENSATION in milliseconds. A
|
||||
// neutral ratio of 50 therefore 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 operator saw "one element, then it stalls".
|
||||
//
|
||||
// The command NUMBERS are the contract with the hardware. A test on the values
|
||||
// is the only thing standing between a typo here and a keyer that misbehaves in
|
||||
// a way nobody can debug from the UI.
|
||||
func TestApplyConfigCommands(t *testing.T) {
|
||||
cmds := configCommands(Config{
|
||||
WPM: 25, Weight: 50, Ratio: 50, LeadInMs: 0, TailMs: 0, Sidetone: 0,
|
||||
})
|
||||
|
||||
// Every command that must be present, with the value it must carry.
|
||||
want := map[byte][]byte{
|
||||
0x02: {25}, // speed
|
||||
0x03: {50}, // weighting — 50 is neutral, and this one WAS right
|
||||
0x17: {50}, // dit/dah ratio — the corrected opcode
|
||||
0x11: {0x00}, // key compensation explicitly cleared
|
||||
}
|
||||
seen := map[byte][]byte{}
|
||||
for _, c := range cmds {
|
||||
if len(c) < 2 {
|
||||
t.Fatalf("command %X has no argument", c)
|
||||
}
|
||||
seen[c[0]] = c[1:]
|
||||
}
|
||||
for op, args := range want {
|
||||
got, ok := seen[op]
|
||||
if !ok {
|
||||
t.Errorf("command 0x%02X is not sent at all", op)
|
||||
continue
|
||||
}
|
||||
if len(got) != len(args) || (len(args) > 0 && got[0] != args[0]) {
|
||||
t.Errorf("command 0x%02X carries % X, want % X", op, got, args)
|
||||
}
|
||||
}
|
||||
|
||||
// The ratio must NEVER go out as 0x11 again: that is the whole bug.
|
||||
if v, ok := seen[0x11]; ok && len(v) > 0 && v[0] != 0 {
|
||||
t.Errorf("0x11 (key compensation) carries %d — it must be 0, not the ratio", v[0])
|
||||
}
|
||||
}
|
||||
|
||||
// A keyer left with compensation by an earlier version keeps it in EEPROM, so
|
||||
// the fix has to CLEAR it rather than merely stop setting it.
|
||||
func TestKeyCompensationIsCleared(t *testing.T) {
|
||||
for _, ratio := range []int{33, 50, 66} {
|
||||
cmds := configCommands(Config{WPM: 20, Weight: 50, Ratio: ratio})
|
||||
found := false
|
||||
for _, c := range cmds {
|
||||
if c[0] == 0x11 {
|
||||
found = true
|
||||
if c[1] != 0 {
|
||||
t.Errorf("ratio %d: key compensation sent as %d, want 0", ratio, c[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("ratio %d: key compensation is never cleared", ratio)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user