Files
OpsLog/internal/cat/tci_manager.go
T
rouggy 766b0f95a4 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.
2026-08-28 13:01:55 +02:00

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)
})
}