Ultrabeam on a serial port never worked, and three faults were stacked so
each hid the next:
- Stop() did not wait for the poll loop, so a stopped client kept the COM
port. Every later client then failed with "Serial port busy" — the
program holding it being OpsLog itself.
- startUltrabeam tore the old client down CONCURRENTLY with starting the
new one, and "Test connection" built a second client on a port already
ours. Harmless over TCP, fatal on a port with one owner.
- A silent serial port returns (0, nil) and bufio retries that a hundred
times: a 4 s timeout became ~7 minutes of a frozen poll loop logging
nothing.
The controller then answered at once. Confirmed on hardware: the USB cable
presents TWO COM ports, only the second reaches the controller, and only at
19200 baud — so the speed is pinned in code (an FTDI cable opens at any
speed, and a wrong one is indistinguishable from a dead controller) and the
port field says which one to pick. The first exchange after each connect is
hex-dumped, which separates silence from a wrong baud from a misread frame.
Databases now carry only the tables their role needs. Every target used to
get the whole migration set, so a shared MySQL logbook grew settings and
station_profiles tables nothing ever wrote to — an operator inspecting the
server could not tell which copy was authoritative. Statements are filtered
by role, unknown tables are kept in both (fail-safe), and existing databases
are cleaned once, dropping only EMPTY tables. Settings → Database gains a
Compact button, since SQLite frees pages inside the file and never shrinks it.
Also:
- Awards: the callsigns behind a cell open the QSL Manager on Paper QSL,
searched, ready for the card dates.
- The record button no longer goes missing after an update: whether manual
recording is possible is a per-profile question that was asked once, at
startup, before the profile was known.
- Alert rules and filter presets confirm that they were saved.
- Spot clicks on the radio panadapter carry the POTA park into F3.
- The build gate is re-checked wherever the active callsign can change; it
ran at startup alone, and a fresh install has no callsign then.
86 lines
3.3 KiB
Go
86 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The tracking mode decides how often a motorized antenna's elements run, so a
|
|
// value that fails to parse must not silently become the most aggressive
|
|
// setting. Anything unrecognised — including the empty string every config
|
|
// written before this option existed contains — has to land on the threshold
|
|
// mode, which is exactly what those configs already did.
|
|
func TestNormMotorTrackMode(t *testing.T) {
|
|
for _, tc := range []struct{ in, want string }{
|
|
{"always", motorTrackAlways},
|
|
{"ALWAYS", motorTrackAlways},
|
|
{" band ", motorTrackBand},
|
|
{"step", motorTrackStep},
|
|
{"", motorTrackStep}, // never configured — behaves as before
|
|
{"everytime", motorTrackStep}, // near-miss, not "always"
|
|
{"per-band", motorTrackStep}, // near-miss, not "band"
|
|
{"25", motorTrackStep}, // a step value fed in by mistake
|
|
} {
|
|
if got := normMotorTrackMode(tc.in); got != tc.want {
|
|
t.Errorf("normMotorTrackMode(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Band mode compares the band the rig is on against the band the antenna was
|
|
// last commanded for. That comparison is bandForHz, and the property it has to
|
|
// have is that two frequencies far apart within one band agree while two
|
|
// frequencies close together across a band edge do not — otherwise the antenna
|
|
// either never moves or moves on every QSY.
|
|
func TestBandForHzDrivesBandTracking(t *testing.T) {
|
|
same := [][2]int64{
|
|
{14000000, 14350000}, // both ends of 20 m — one band, no move
|
|
{7000000, 7200000}, // 40 m
|
|
{50000000, 52000000}, // 6 m, a wide one
|
|
}
|
|
for _, p := range same {
|
|
if a, b := bandForHz(p[0]), bandForHz(p[1]); a != b || a == "" {
|
|
t.Errorf("%.3f MHz is %q but %.3f MHz is %q — band mode would re-tune inside one band",
|
|
float64(p[0])/1e6, a, float64(p[1])/1e6, b)
|
|
}
|
|
}
|
|
// A small QSY that crosses from 30 m into 20 m has to read as a band change.
|
|
if a, b := bandForHz(10150000), bandForHz(14000000); a == b {
|
|
t.Errorf("30 m and 20 m both read %q — band mode would never re-tune between them", a)
|
|
}
|
|
}
|
|
|
|
// A "Test connection" on a serial port must not open a port OpsLog already
|
|
// holds: a COM port has one owner, and the owner is the running antenna client.
|
|
// Building a second client made the two fight — one of them logging "Serial port
|
|
// busy" for the rest of the session.
|
|
func TestTestUltrabeamReusesTheLiveSerialClient(t *testing.T) {
|
|
src, err := os.ReadFile("app.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := funcBody(t, string(src), "func (a *App) TestUltrabeam(s UltrabeamSettings) error {")
|
|
if !strings.Contains(body, "a.liveMotorFor(s)") {
|
|
t.Error("TestUltrabeam builds a client without first checking whether the live one already owns that port")
|
|
}
|
|
// The check has to come BEFORE the second client is created, or it is no check.
|
|
if i, j := strings.Index(body, "a.liveMotorFor(s)"), strings.Index(body, "newMotorClient(s)"); i < 0 || j < 0 || i > j {
|
|
t.Error("the live-client check must precede newMotorClient")
|
|
}
|
|
}
|
|
|
|
// funcBody returns the source of a function, up to the closing brace in column 0.
|
|
func funcBody(t *testing.T, src, signature string) string {
|
|
t.Helper()
|
|
i := strings.Index(src, signature)
|
|
if i < 0 {
|
|
t.Fatalf("function not found: %s", signature)
|
|
}
|
|
rest := src[i:]
|
|
if j := strings.Index(rest, "\n}\n"); j >= 0 {
|
|
return rest[:j]
|
|
}
|
|
return rest
|
|
}
|