Measured rather than guessed: the whole repository was cross-compiled for
linux/amd64 and the gaps closed one by one. There were fewer than expected.
Flex and TCI were never Windows-specific — they carried //go:build windows by
inheritance and import nothing but net and gorilla/websocket. Untagged, no code
change. The two backends a Linux operator is most likely to own were already
portable.
Audio was 560 lines, not 2287: only devices.go and engine.go touch WASAPI, while
manager.go, recorder.go, wav.go and mp3.go were pure Go wearing the tag by
association. The whole platform surface is seven functions, now implemented a
second time on PulseAudio through github.com/jfreymuth/pulse — pure Go over the
server socket, so the no-cgo rule survives, and PipeWire answers the same
protocol. The fixed 16 kHz mono format and the server-side resampling mirror
what AUTOCONVERTPCM does on Windows, for the same reason.
OmniRig is the only real loss, and its backend still EXISTS off Windows rather
than being compiled out of app.go: a settings database is portable, so an
operator moving a profile across keeps "omnirig" saved and must be told to pick
a native backend instead of meeting a nil one.
The parts where Linux is not Windows, and where a compile-only stub would have
been a silent bug:
- data dir: still beside the binary, but ~/.local/share/OpsLog/data when that
folder belongs to the system — decided by trying the write, because /opt and
/usr/local are writable on some stations and not others.
- single instance: an flock, not a pid file. The kernel drops it however the
process dies, so a crash leaves nothing to delete by hand. This is the guard
that stops two instances fighting over the rig frequency.
- update: simpler here. Unix renames over a running binary, so the deferred
swap the Windows path needs a detached helper for is unreachable.
- tasklist/taskkill become /proc and SIGTERM; the boot log moves out of /tmp,
which is wiped exactly when the evidence is wanted.
- serial ports sorted naturally: /dev/ttyUSB10 was landing between USB1 and
USB2, the same trap COM10 fell into.
release.ps1 now cross-builds and vets for linux before it builds the exe, and
refuses the release if that fails — a port rots one unguarded x/sys/windows call
at a time.
Nothing has been executed on Linux yet: Wails needs webkit2gtk and cgo there, so
the binary must be built on Linux. scripts/linux-setup.sh checks the machine and
does it; BUILDING-LINUX.md is the manual version.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
120 lines
3.7 KiB
Go
120 lines
3.7 KiB
Go
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)
|
|
})
|
|
}
|