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.
106 lines
3.8 KiB
Go
106 lines
3.8 KiB
Go
package main
|
|
|
|
import "testing"
|
|
|
|
// A per-band tune frequency goes straight to the antenna as a command, so a
|
|
// value that is not actually in that band has to be refused rather than obeyed.
|
|
// One wrong digit sends the elements travelling to a length that is wrong for
|
|
// the band the operator is on — and on a SteppIR that journey inhibits transmit
|
|
// the whole way.
|
|
func TestNormMotorBandFreqsRefusesOutOfBand(t *testing.T) {
|
|
in := map[string]int{
|
|
"20m": 14050, // fine, CW end
|
|
"40m": 7005, // fine
|
|
"6m": 50313, // fine, FT8
|
|
"15m": 1450, // a digit lost — lands in the broadcast band
|
|
"10m": 28400000, // Hz typed where kHz was asked
|
|
"17m": 14100, // right number, wrong band
|
|
"30m": 0, // not set
|
|
"80m": 3750, // not a band this antenna covers at all
|
|
"bogus": 14100, // not a band
|
|
}
|
|
got := normMotorBandFreqs(in)
|
|
want := map[string]int{"40m": 7005, "20m": 14050, "6m": 50313}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("kept %v, want %v", got, want)
|
|
}
|
|
for k, v := range want {
|
|
if got[k] != v {
|
|
t.Errorf("%s = %d, want %d", k, got[k], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The stored form round-trips, in canonical band order rather than map order so
|
|
// the settings row does not churn between saves.
|
|
func TestMotorBandFreqsRoundTrip(t *testing.T) {
|
|
m := map[string]int{"20m": 14050, "40m": 7005, "6m": 50313}
|
|
enc := encodeMotorBandFreqs(m)
|
|
if enc != "40m=7005,20m=14050,6m=50313" {
|
|
t.Errorf("encoded %q — want canonical low→high order", enc)
|
|
}
|
|
back := decodeMotorBandFreqs(enc)
|
|
for k, v := range m {
|
|
if back[k] != v {
|
|
t.Errorf("round trip lost %s: %d → %d", k, v, back[k])
|
|
}
|
|
}
|
|
// Garbage in one entry must cost only that entry.
|
|
part := decodeMotorBandFreqs("40m=7005,20m=oops,6m=50313")
|
|
if part["40m"] != 7005 || part["6m"] != 50313 {
|
|
t.Errorf("one bad entry took the others down: %v", part)
|
|
}
|
|
if _, ok := part["20m"]; ok {
|
|
t.Errorf("kept an unparseable entry: %v", part)
|
|
}
|
|
}
|
|
|
|
// An unset band falls back to its default, which is what makes the Settings box
|
|
// safe to leave empty.
|
|
func TestMotorTuneKHzForBandFallsBack(t *testing.T) {
|
|
m := map[string]int{"20m": 14050}
|
|
if got := motorTuneKHzForBand(m, "20m"); got != 14050 {
|
|
t.Errorf("chosen frequency ignored: %d", got)
|
|
}
|
|
if got := motorTuneKHzForBand(m, "15m"); got != 21150 {
|
|
t.Errorf("15m = %d, want the 21150 default", got)
|
|
}
|
|
if got := motorTuneKHzForBand(m, "80m"); got != 0 {
|
|
t.Errorf("80m = %d, want 0 — not a motor band", got)
|
|
}
|
|
// Every default must itself be in its band, or the fallback ships the very
|
|
// fault normMotorBandFreqs exists to catch.
|
|
for _, b := range motorBands {
|
|
if got := bandForHz(int64(b.defKHz) * 1000); got != b.name {
|
|
t.Errorf("default %d kHz for %s reads as %q", b.defKHz, b.name, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The Ultrabeam's USB link answers on 19200 and on nothing else — confirmed on
|
|
// hardware, where 9600 opened the port and returned not one byte. A speed with a
|
|
// single right answer must not be storable as anything else, whatever an older
|
|
// config or a hand-edited setting says.
|
|
func TestNormMotorBaud(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
typ, transport string
|
|
in, want int
|
|
}{
|
|
{"ultrabeam", "serial", 9600, 19200},
|
|
{"ultrabeam", "serial", 0, 19200},
|
|
{"ultrabeam", "serial", 115200, 19200},
|
|
// Over TCP the speed is the adapter's business, not ours.
|
|
{"ultrabeam", "tcp", 9600, 9600},
|
|
// The SteppIR controller really does run at several speeds.
|
|
{"steppir", "serial", 4800, 4800},
|
|
{"steppir", "serial", 19200, 19200},
|
|
// Nonsense falls back rather than reaching the driver.
|
|
{"steppir", "serial", 0, 9600},
|
|
{"steppir", "serial", 999999, 9600},
|
|
} {
|
|
if got := normMotorBaud(tc.typ, tc.transport, tc.in); got != tc.want {
|
|
t.Errorf("normMotorBaud(%q, %q, %d) = %d, want %d", tc.typ, tc.transport, tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|