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.
65 lines
2.0 KiB
Go
65 lines
2.0 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The build gate is only as good as the moments it runs at.
|
|
//
|
|
// It used to run at startup alone, on the profile active then — and a fresh
|
|
// install has NO callsign at that point. The operator typed theirs afterwards,
|
|
// so a denied call ran for the whole session and reached the telemetry, which
|
|
// reads the very same field. Every place that can make a denied callsign the
|
|
// ACTIVE one has to re-ask.
|
|
func TestCallGateRunsWhereverTheActiveCallsignChanges(t *testing.T) {
|
|
src, err := os.ReadFile("app.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, fn := range []string{
|
|
"func (a *App) startup(ctx context.Context) {", // launch
|
|
"func (a *App) SaveStationSettings(s StationSettings) error {", // the call is entered here
|
|
"func (a *App) SaveProfile(p profile.Profile) (profile.Profile, error) {",
|
|
"func (a *App) ActivateProfile(id int64) error {",
|
|
} {
|
|
body := funcBody(t, string(src), fn)
|
|
if !strings.Contains(body, "enforceCallGate(") {
|
|
t.Errorf("%s can change the active callsign but never re-checks the gate", fn)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The hashes are the whole mechanism: a truncated or re-cased entry silently
|
|
// matches nothing, and nothing in the running program would ever say so.
|
|
func TestDeniedCallHashesAreWellFormed(t *testing.T) {
|
|
if len(deniedCallHashes) == 0 {
|
|
t.Fatal("the deny list is empty — the gate has stopped gating")
|
|
}
|
|
for h := range deniedCallHashes {
|
|
if len(h) != 64 {
|
|
t.Errorf("hash %q is %d chars, want 64", h, len(h))
|
|
}
|
|
if h != strings.ToLower(h) {
|
|
t.Errorf("hash %q is not lower-case, so it can never match", h)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A slashed call must reduce to the real one, or /P is a way round the gate.
|
|
func TestDenyBaseCall(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"f4xyz": "F4XYZ",
|
|
" F4XYZ ": "F4XYZ",
|
|
"F4XYZ/P": "F4XYZ",
|
|
"TM/F4XYZ": "F4XYZ",
|
|
"F4XYZ/MM": "F4XYZ",
|
|
"": "",
|
|
} {
|
|
if got := denyBaseCall(in); got != want {
|
|
t.Errorf("denyBaseCall(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|