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]>
86 lines
3.4 KiB
Go
86 lines
3.4 KiB
Go
package cat
|
|
|
|
import "testing"
|
|
|
|
// The reported symptom: with spot mirroring on, the same station appeared two
|
|
// or three times on the panorama.
|
|
//
|
|
// Its cause is not in OpsLog's spot pipeline — one cluster line produces one
|
|
// SendSpot. It is that a popular DX station IS spotted two or three times, by
|
|
// different operators within the same minute, and no two of them agree on the
|
|
// frequency to better than a few tens of hertz. The backend assumed ExpertSDR
|
|
// replaced a spot bearing a callsign it already had; it keys on the frequency
|
|
// too, so each of those became its own marker.
|
|
func TestSameStationSpottedBySeveralOperatorsIsDrawnOnce(t *testing.T) {
|
|
tci := &TCI{spotsEnabled: true}
|
|
|
|
draw, del := tci.noteSpot("UN7GK", 14025000)
|
|
if !draw || del {
|
|
t.Fatalf("first spot: draw=%v delete=%v, want draw and nothing to delete", draw, del)
|
|
}
|
|
// The same station, two more spotters, a few tens of hertz apart.
|
|
for _, hz := range []int64{14025120, 14024900} {
|
|
if draw, del := tci.noteSpot("UN7GK", hz); draw || del {
|
|
t.Errorf("re-spot at %d Hz: draw=%v delete=%v, want nothing sent — this is the duplicate marker", hz, draw, del)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A station that really moves must still move on the panorama, and the marker
|
|
// left where it was must go. Deleting first is the whole difference between
|
|
// "the spot follows the station" and "the station collects markers".
|
|
func TestAStationThatMovesReplacesItsMarker(t *testing.T) {
|
|
tci := &TCI{spotsEnabled: true}
|
|
tci.noteSpot("UN7GK", 14025000)
|
|
|
|
draw, del := tci.noteSpot("UN7GK", 14032000) // 7 kHz up: a real QSY
|
|
if !draw || !del {
|
|
t.Fatalf("after a QSY: draw=%v delete=%v, want the old marker deleted and a new one drawn", draw, del)
|
|
}
|
|
// And the new position becomes the reference, so spotters agreeing with it
|
|
// are quiet again.
|
|
if draw, _ := tci.noteSpot("UN7GK", 14032100); draw {
|
|
t.Error("a spot at the station's new frequency was drawn again")
|
|
}
|
|
}
|
|
|
|
// Case matters nowhere in ham radio, and the cluster is not consistent about it.
|
|
func TestSpotMemoryIgnoresCase(t *testing.T) {
|
|
tci := &TCI{spotsEnabled: true}
|
|
tci.noteSpot("un7gk", 14025000)
|
|
if draw, _ := tci.noteSpot("UN7GK", 14025000); draw {
|
|
t.Error("the same call in another case was treated as a different station")
|
|
}
|
|
}
|
|
|
|
// Two different stations are two markers — the whole point of the panorama.
|
|
func TestDifferentStationsEachGetAMarker(t *testing.T) {
|
|
tci := &TCI{spotsEnabled: true}
|
|
tci.noteSpot("UN7GK", 14025000)
|
|
draw, del := tci.noteSpot("ZD7BG", 14025050) // 50 Hz away, a different operator
|
|
if !draw {
|
|
t.Error("a second station near the first was swallowed as a duplicate")
|
|
}
|
|
if del {
|
|
t.Error("deleting by callsign would have removed a spot this station never had")
|
|
}
|
|
}
|
|
|
|
// The connection drops and comes back: Connect sends spot_clear, so the
|
|
// panorama is empty. If the memory survived that, the next spot for each of
|
|
// those calls would be suppressed as "already drawn" onto an empty panorama —
|
|
// the operator's spots would simply stop appearing until they changed
|
|
// frequency.
|
|
func TestReconnectingForgetsWhatWasDrawn(t *testing.T) {
|
|
tci := &TCI{spotsEnabled: true}
|
|
tci.noteSpot("UN7GK", 14025000)
|
|
|
|
tci.mu.Lock()
|
|
tci.spotFreq = map[string]int64{} // what Connect does alongside spot_clear
|
|
tci.mu.Unlock()
|
|
|
|
if draw, del := tci.noteSpot("UN7GK", 14025000); !draw || del {
|
|
t.Errorf("after a reconnect: draw=%v delete=%v, want it drawn again and nothing deleted", draw, del)
|
|
}
|
|
}
|