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.
64 lines
2.1 KiB
Go
64 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// deniedCallHashes lists station callsigns not permitted to run this build,
|
|
// stored as SHA-256 hex of the base call so the calls themselves appear nowhere
|
|
// in the source or the compiled binary. Enforcement is best-effort by design —
|
|
// the call is entered by the operator and can be changed — so this only turns
|
|
// away straightforward use, not a determined one.
|
|
var deniedCallHashes = map[string]struct{}{
|
|
"2282a88b3e5e4eebc6b8174d005bb46d32025758b7741bdcf34f2b3621c02205": {},
|
|
"0ee09fe60817a2a4982f5e5b14a60b8dbb11cff55b3d36661213c4cbdd0933ea": {},
|
|
"46fb61e71afb40627cb6493a6a59c9d4d0231ee3cad1a2bf3fcc4cdfce36fabc": {},
|
|
"d1ae6212ec057f9d5c6f379fb57acedac7c94142c9d49667dc04b2016d03d1b6": {},
|
|
"0741c9e394b42f43191899105553b47155ddc3026da12b5360701f9c181ff123": {},
|
|
"ab4926a3a0ab76d41b5b99cd3ad0683584970c341c29427c1dfa4b3c329ce415": {},
|
|
}
|
|
|
|
// callDenied reports whether a callsign is on deniedCallHashes. The call is
|
|
// reduced to its base form (upper-cased, portable prefix/suffix dropped) the
|
|
// same way extsvc does, so F4XYZ/P matches F4XYZ.
|
|
func callDenied(call string) bool {
|
|
base := denyBaseCall(call)
|
|
if base == "" {
|
|
return false
|
|
}
|
|
sum := sha256.Sum256([]byte(base))
|
|
_, bad := deniedCallHashes[hex.EncodeToString(sum[:])]
|
|
return bad
|
|
}
|
|
|
|
// denyBaseCall mirrors extsvc.baseCall: for a slashed form it returns the
|
|
// longest token (the real call), otherwise the call itself, upper-cased.
|
|
func denyBaseCall(s string) string {
|
|
s = strings.ToUpper(strings.TrimSpace(s))
|
|
if !strings.Contains(s, "/") {
|
|
return s
|
|
}
|
|
best := ""
|
|
for _, part := range strings.Split(s, "/") {
|
|
if len(part) > len(best) {
|
|
best = part
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
// enforceCallGate exits the process silently when any of the supplied callsigns
|
|
// is denied. Called once at startup, after the profiles are loaded: the first
|
|
// launch is where the operator enters and saves the call, so a denied build
|
|
// simply does not come back up on the next launch. No window, no message.
|
|
func enforceCallGate(calls ...string) {
|
|
for _, c := range calls {
|
|
if callDenied(c) {
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
}
|