package cat import ( "fmt" "strings" ) // CW keying over TCI — a sixth CW engine, so a SunSDR needs no WinKeyer and no // second serial port: the radio's own macro keyer is driven over the WebSocket // that already carries the CAT. // // The commands, from the TCI command table (confirmed against ars-ka0s/eesdr-tci, // the same source that settled SPOT): // // CW_MACROS:,; send text through the radio's keyer // CW_MACROS_SPEED:; the speed those macros are keyed at // CW_MACROS_STOP; abort what is being keyed // CW_MACROS_EMPTY; the radio saying the buffer has run dry // // CW_MSG (TCI 2.0) does the same with separate before/after callsign fields. // CW_MACROS is used instead because it exists from 1.6 and OpsLog resolves the // variables itself — the text handed here is already what should go on the air. // // Notably absent: there is no backspace. The FlexRadio CWX keyer can un-type // what has not been sent yet; TCI can only stop. So the type-ahead correction // the Flex engine offers is not offered here rather than faked. // tciCWTextLimit caps one macro. A runaway paste down a WebSocket that also // carries audio is worth refusing, and no real CW message is this long. const tciCWTextLimit = 512 // SendCW keys a message through the radio's macro keyer. func (t *TCI) SendCW(text string) error { msg := sanitiseTCICW(text) if msg == "" { return nil } return t.send(fmt.Sprintf("cw_macros:0,%s;", msg)) } // StopCW aborts the message being keyed. func (t *TCI) StopCW() error { return t.send("cw_macros_stop;") } // SetCWSpeed sets the macro keyer speed in words per minute. // // Only the MACRO speed: the paddle keyer has its own (CW_KEYER_SPEED) and an // operator who has set their paddle to 28 wpm did not ask the logger to change // it because a macro went out at 25. func (t *TCI) SetCWSpeed(wpm int) error { if wpm < 5 { wpm = 5 } if wpm > 60 { wpm = 60 } return t.send(fmt.Sprintf("cw_macros_speed:%d;", wpm)) } // sanitiseTCICW makes a message safe to put in a TCI command. // // Commas and semicolons are the protocol's own separators — a comma inside the // text would be read as another argument and a semicolon would end the command // early, keying half a message and leaving the rest to be parsed as a command of // its own. Neither belongs in Morse anyway. func sanitiseTCICW(text string) string { s := strings.ToUpper(strings.TrimSpace(text)) s = strings.NewReplacer(",", " ", ";", " ", "\r", " ", "\n", " ").Replace(s) if len(s) > tciCWTextLimit { s = s[:tciCWTextLimit] } return strings.TrimSpace(s) }