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]>
151 lines
7.4 KiB
Go
151 lines
7.4 KiB
Go
//go:build windows
|
|
|
|
package cat
|
|
|
|
import "testing"
|
|
|
|
// Every known rig's OmniRig behaviour, pinned. These rules genuinely contradict
|
|
// each other between models, so a change that fixes one rig must be shown not to
|
|
// break another — that is what this table is for.
|
|
func TestResolveOmniRigVFOs(t *testing.T) {
|
|
const (
|
|
a14200 = 14200000
|
|
b14205 = 14205000
|
|
b21000 = 21000000
|
|
)
|
|
|
|
cases := []struct {
|
|
name string
|
|
rig string
|
|
main, fa, fb int64
|
|
vfo string
|
|
split int64
|
|
sticky bool
|
|
wantTX, wantRX int64
|
|
wantSplit bool
|
|
}{
|
|
// The reported failure: FTDX101D, SUB VFO pressed. OmniRig names VFO B and
|
|
// reports it as the generic Freq; freqA still holds the main VFO. Preferring
|
|
// freqA meant the display never followed the operator to B.
|
|
{"FTDX101D on SUB VFO", "FTDX101D", b14205, a14200, b14205, "B", pmSplitOff, false, b14205, 0, false},
|
|
{"FTDX101D on MAIN VFO", "FTDX101D", a14200, a14200, b14205, "A", pmSplitOff, false, a14200, 0, false},
|
|
|
|
// Yaesu fallback for a rig file that names NO VFO at all (the stock FTDX10
|
|
// one, confirmed 2026-07-26): the generic Freq matching FreqB is then the
|
|
// only sign that the operator is on SUB. It applies ONLY when the enum is
|
|
// silent — when the enum names a VFO, the enum wins (pair cases below).
|
|
{"FTDX10, no enum, generic Freq is B", "FTDX10", b14205, a14200, b14205, "", pmSplitOff, false, b14205, 0, false},
|
|
// Same alternating ini: one poll says OFF while the rig IS in split. The
|
|
// latched ON flag has to survive the contradicting sample.
|
|
{"FTDX101D split, contradicting OFF sample", "FTDX101D", a14200, a14200, b14205, "AB", pmSplitOff, true, b14205, a14200, true},
|
|
// The latch must not manufacture a split out of a stale cross-band VFO B.
|
|
{"FTDX101D latch, VFOs on different bands", "FTDX101D", a14200, a14200, b21000, "AB", pmSplitOff, true, a14200, 0, false},
|
|
|
|
// Pair enums: first letter is the VFO being listened on. Reported by the
|
|
// whole Yaesu family; ignoring them pinned the display to VFO A (F4NBZ,
|
|
// 2026-07-26 — Log4OM follows SUB on the same rig through OmniRig).
|
|
{"pair enum BA, simplex → listening on B", "", b14205, a14200, b14205, "BA", pmSplitOff, false, b14205, 0, false},
|
|
{"pair enum BB, simplex → listening on B", "", b14205, a14200, b14205, "BB", pmSplitOff, false, b14205, 0, false},
|
|
{"pair enum AB, simplex → listening on A", "", a14200, a14200, b14205, "AB", pmSplitOff, false, a14200, 0, false},
|
|
{"pair enum AA, simplex → listening on A", "", a14200, a14200, b14205, "AA", pmSplitOff, false, a14200, 0, false},
|
|
|
|
// F4?? IC-7610 report, 2026-07-27: the operator's custom rig file declares
|
|
// VFO B permanently while they work on the MAIN VFO. OpsLog then wrote to A
|
|
// (SetSimplexMode acts on main) and read B — "the frequency never follows
|
|
// the knob". Honouring the enum is right in general, which is why the cure
|
|
// is an explicit override in the settings, applied before this function.
|
|
{"IC-7610 file wrongly says B — enum honoured, hence the override", "IC-7610", 0, 7150000, 14295000, "B", pmSplitOff, false, 14295000, 0, false},
|
|
{"same rig with the override forcing A", "IC-7610", 0, 7150000, 14295000, "A", pmSplitOff, false, 7150000, 0, false},
|
|
|
|
// Non-regression: a rig that does not report the VFO enum keeps the old
|
|
// order — freqA, then the generic Freq, then freqB.
|
|
{"no VFO enum, freqA populated (Yaesu/Kenwood)", "FT-891", a14200, a14200, 0, "", pmSplitOff, false, a14200, 0, false},
|
|
{"no VFO enum, only generic Freq (IC-9100)", "IC-9100", a14200, 0, 0, "", pmSplitOff, false, a14200, 0, false},
|
|
{"IC-7610: generic Freq reports B, enum says A", "IC-7610", b14205, a14200, b14205, "A", pmSplitOff, false, a14200, 0, false},
|
|
|
|
// Split: PM_SPLITON must be read as a BIT. An exact == 0x8000 reported "no
|
|
// split" for any rig that sets the flag alongside another bit.
|
|
{"split, ON flag alone", "", a14200, a14200, b14205, "AB", pmSplitOn, false, b14205, a14200, true},
|
|
{"split, ON flag with extra bits set", "", a14200, a14200, b14205, "AB", pmSplitOn | 0x40, false, b14205, a14200, true},
|
|
{"listening on B → TX on A", "", b14205, a14200, b14205, "BA", pmSplitOn, false, a14200, b14205, true},
|
|
|
|
// Split must NOT be inferred when the rig says OFF, nor from a stale VFO B
|
|
// left on another band (the FT-710 / TS-570 false positive).
|
|
{"OFF flag, two distinct VFOs", "", a14200, a14200, b14205, "A", pmSplitOff, false, a14200, 0, false},
|
|
{"ON flag but VFOs on different bands", "", a14200, a14200, b21000, "AB", pmSplitOn, false, a14200, 0, false},
|
|
{"ON flag but both VFOs identical", "", a14200, a14200, a14200, "AB", pmSplitOn, false, a14200, 0, false},
|
|
{"ON and OFF both set — ambiguous, treat as no split", "", a14200, a14200, b14205, "A", pmSplitOn | pmSplitOff, false, a14200, 0, false},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
tx, rx, split := resolveOmniRigVFOs(c.rig, c.main, c.fa, c.fb, c.vfo, c.split, c.sticky)
|
|
if tx != c.wantTX || rx != c.wantRX || split != c.wantSplit {
|
|
t.Errorf("%s:\n got TX=%d RX=%d split=%v\n want TX=%d RX=%d split=%v",
|
|
c.name, tx, rx, split, c.wantTX, c.wantRX, c.wantSplit)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Which VFO a frequency set writes to, and whether it may call SetSimplexMode.
|
|
//
|
|
// Reported on an FTDX101 (F4NBZ, 2026-07-29): listening on SUB, clicking a
|
|
// cluster spot dragged the radio back to MAIN. SetSimplexMode means "receive and
|
|
// transmit here, simplex" and OmniRig applies it to the MAIN VFO — so it must
|
|
// not be used when the operator is on SUB. They chose that VFO deliberately; a
|
|
// spot click asks for a frequency, not for a VFO change.
|
|
func TestOmniRigWriteTarget(t *testing.T) {
|
|
// Mirrors the decision made in SetFrequency.
|
|
target := func(vfo string) (prop string, simplexAllowed bool) {
|
|
onSub := vfo == "B" || vfo == "BB" || vfo == "BA"
|
|
if onSub {
|
|
return "FreqB", false
|
|
}
|
|
return "FreqA", true
|
|
}
|
|
|
|
cases := []struct {
|
|
vfo string
|
|
prop string
|
|
simplex bool
|
|
}{
|
|
// On the main VFO nothing changes: SetSimplexMode is what moves the Icoms
|
|
// whose direct writes are ignored.
|
|
{"", "FreqA", true},
|
|
{"A", "FreqA", true},
|
|
{"AA", "FreqA", true},
|
|
{"AB", "FreqA", true},
|
|
// On SUB, write FreqB and leave the VFO selection alone.
|
|
{"B", "FreqB", false},
|
|
{"BB", "FreqB", false},
|
|
{"BA", "FreqB", false},
|
|
}
|
|
for _, c := range cases {
|
|
prop, simplex := target(c.vfo)
|
|
if prop != c.prop || simplex != c.simplex {
|
|
t.Errorf("VFO %q → write %s, simplex=%v; want %s, simplex=%v",
|
|
c.vfo, prop, simplex, c.prop, c.simplex)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetSimplexMode is skipped on Yaesu, and that is not a preference.
|
|
//
|
|
// On an FT-891 it returned OK on every spot click while the rig never moved. On
|
|
// an FT-2000 it did something worse — a log of one QSY shows the state going
|
|
// from Vfo="AB" split off to Vfo="BA" split ON, so the operator's reception
|
|
// jumped to VFO B and OpsLog, reading the radio correctly, displayed B. The
|
|
// direct FreqA/Freq write does the tuning on these rigs.
|
|
func TestYaesuRigsAreRecognisedForTheSimplexSkip(t *testing.T) {
|
|
for _, yes := range []string{"FT-2000", "FT-891", "FTDX10", "ftdx101", " FT-991A "} {
|
|
if !isYaesuRig(yes) {
|
|
t.Errorf("%q not recognised as a Yaesu — SetSimplexMode would still be called on it", yes)
|
|
}
|
|
}
|
|
// Everything else keeps the call: on Icom it is the authoritative one.
|
|
for _, no := range []string{"IC-7610", "TS-590", "K3", "", "Flex-6600"} {
|
|
if isYaesuRig(no) {
|
|
t.Errorf("%q wrongly treated as a Yaesu", no)
|
|
}
|
|
}
|
|
}
|