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.
122 lines
3.7 KiB
Go
122 lines
3.7 KiB
Go
//go:build windows
|
|
|
|
package cat
|
|
|
|
import "fmt"
|
|
|
|
// TCIAudioController is the receive-audio capability of the TCI backend, kept
|
|
// as an interface for the same reason as the Flex and Yaesu ones: the host asks
|
|
// the manager, and a station running something else gets a clear "this backend
|
|
// does not do that" instead of a nil dereference.
|
|
type TCIAudioController interface {
|
|
StartTCIAudio(rx, rate int) error
|
|
StopTCIAudio() error
|
|
TCIAudioStatus() TCIAudioStatus
|
|
}
|
|
|
|
// TCIAudioState returns the stream's state, or (zero, false) when the active
|
|
// backend is not a TCI radio.
|
|
func (m *Manager) TCIAudioState() (TCIAudioStatus, bool) {
|
|
m.mu.RLock()
|
|
b := m.backend
|
|
m.mu.RUnlock()
|
|
if tc, ok := b.(TCIAudioController); ok {
|
|
return tc.TCIAudioStatus(), true
|
|
}
|
|
return TCIAudioStatus{}, false
|
|
}
|
|
|
|
// TCIAudioDo dispatches an audio command onto the CAT goroutine, like every
|
|
// other backend-specific control.
|
|
func (m *Manager) TCIAudioDo(fn func(TCIAudioController) error) error {
|
|
return m.exec(func(b Backend) error {
|
|
tc, ok := b.(TCIAudioController)
|
|
if !ok {
|
|
return fmt.Errorf("active CAT backend is not a TCI radio")
|
|
}
|
|
return fn(tc)
|
|
})
|
|
}
|
|
|
|
// TCIPanelController is the control console of a TCI radio — everything the
|
|
// panel reads and everything it sets.
|
|
//
|
|
// Listed one by one rather than accepted as *TCI, for the same reason the audio
|
|
// controller is: the manager hands out capabilities, not backends, and a
|
|
// station on OmniRig asking for the TCI console gets a sentence instead of a
|
|
// crash.
|
|
type TCIPanelController interface {
|
|
TCIPanel() TCIPanelState
|
|
SetDrive(v int) error
|
|
SetTuneDrive(v int) error
|
|
SetMicLevel(v int) error
|
|
SetVolume(db int) error
|
|
SetMute(on bool) error
|
|
SetAGC(mode string) error
|
|
SetSquelch(on bool) error
|
|
SetSquelchLevel(v int) error
|
|
SetNB(on bool) error
|
|
SetNR(on bool) error
|
|
SetANF(on bool) error
|
|
SetAPF(on bool) error
|
|
SetFilter(lo, hi int) error
|
|
SetRIT(on bool) error
|
|
SetXIT(on bool) error
|
|
SetRITOffset(hz int) error
|
|
SetXITOffset(hz int) error
|
|
SetLock(on bool) error
|
|
SetTune(on bool) error
|
|
}
|
|
|
|
// TCIPanelState returns the console snapshot, or (zero, false) when the active
|
|
// backend is not a TCI radio.
|
|
//
|
|
// Read WITHOUT going through the CAT goroutine: the state is a cached copy of
|
|
// what the radio pushed, guarded by its own lock, and the panel polls it several
|
|
// times a second. Queueing that behind whatever the poll loop is doing would put
|
|
// the console's smoothness at the mercy of a rig command's timeout.
|
|
func (m *Manager) TCIPanelState() (TCIPanelState, bool) {
|
|
m.mu.RLock()
|
|
b := m.backend
|
|
m.mu.RUnlock()
|
|
if tc, ok := b.(TCIPanelController); ok {
|
|
return tc.TCIPanel(), true
|
|
}
|
|
return TCIPanelState{}, false
|
|
}
|
|
|
|
// TCIPanelDo dispatches one console command onto the CAT goroutine, where every
|
|
// other write to the radio goes.
|
|
func (m *Manager) TCIPanelDo(fn func(TCIPanelController) error) error {
|
|
return m.exec(func(b Backend) error {
|
|
tc, ok := b.(TCIPanelController)
|
|
if !ok {
|
|
return fmt.Errorf("the active CAT backend is not a TCI radio")
|
|
}
|
|
return fn(tc)
|
|
})
|
|
}
|
|
|
|
// TCICWController is the radio's CW keyer, as the CW engine uses it.
|
|
//
|
|
// Three methods, and no backspace: TCI can stop a message but cannot un-type one
|
|
// (see tci_cw.go). Kept as its own interface rather than folded into the console
|
|
// one so a CW engine does not have to depend on forty panel setters to key a
|
|
// message.
|
|
type TCICWController interface {
|
|
SendCW(text string) error
|
|
StopCW() error
|
|
SetCWSpeed(wpm int) error
|
|
}
|
|
|
|
// TCICWDo dispatches one keyer command onto the CAT goroutine.
|
|
func (m *Manager) TCICWDo(fn func(TCICWController) error) error {
|
|
return m.exec(func(b Backend) error {
|
|
tc, ok := b.(TCICWController)
|
|
if !ok {
|
|
return fmt.Errorf("the active CAT backend is not a TCI radio")
|
|
}
|
|
return fn(tc)
|
|
})
|
|
}
|