feat(tci): key CW through the radio's own macro keyer
The sixth CW engine, and the one the TCI backend was left without. A SunSDR
keys from the WebSocket that already carries the CAT: no WinKeyer, no second
serial port, nothing to wire.
CW_MACROS:0,<text>; send
CW_MACROS_SPEED:<wpm>; speed
CW_MACROS_STOP; abort
Confirmed against the TCI command table in ars-ka0s/eesdr-tci — the source that
settled SPOT. CW_MACROS rather than TCI 2.0's CW_MSG: it exists from 1.6, and
OpsLog resolves its own variables, so the before/after callsign fields have
nothing to carry.
Commas and semicolons are stripped before sending. They are the protocol's own
separators: a comma would become another argument, a semicolon would end the
command with the message half keyed and the rest parsed as a command.
Only the MACRO speed is set, not the paddle keyer's — an operator who set their
paddle to 28 wpm did not ask for it to change because a macro went out at 25.
And there is no backspace: TCI can stop a message but cannot un-type one, so the
type-ahead correction the Flex engine offers is absent rather than faked.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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:<trx>,<text>; send text through the radio's keyer
|
||||
// CW_MACROS_SPEED:<wpm>; 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)
|
||||
}
|
||||
Reference in New Issue
Block a user