chore: release v0.20.7
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"hamlog/internal/acom"
|
||||
"hamlog/internal/adif"
|
||||
"hamlog/internal/alerts"
|
||||
"hamlog/internal/antgenius"
|
||||
@@ -44,15 +45,15 @@ import (
|
||||
"hamlog/internal/operating"
|
||||
"hamlog/internal/pota"
|
||||
"hamlog/internal/powergenius"
|
||||
"hamlog/internal/spe"
|
||||
"hamlog/internal/profile"
|
||||
"hamlog/internal/qslcard"
|
||||
"hamlog/internal/qso"
|
||||
"hamlog/internal/rotator/pst"
|
||||
"hamlog/internal/relaydev"
|
||||
"hamlog/internal/rotator/pst"
|
||||
"hamlog/internal/rotgenius"
|
||||
"hamlog/internal/settings"
|
||||
"hamlog/internal/solar"
|
||||
"hamlog/internal/spe"
|
||||
"hamlog/internal/steppir"
|
||||
"hamlog/internal/uls"
|
||||
"hamlog/internal/ultrabeam"
|
||||
@@ -446,8 +447,8 @@ type App struct {
|
||||
// wcbm is an in-memory "CALL|BAND|MODE" worked-index so the alert engine never
|
||||
// queries the DB per cluster spot (an FT8 firehose would swamp a remote MySQL).
|
||||
// Loaded once, appended to on each log, rebuilt after bulk changes.
|
||||
wcbm map[string]struct{}
|
||||
wcbmMu sync.RWMutex
|
||||
wcbm map[string]struct{}
|
||||
wcbmMu sync.RWMutex
|
||||
pota *pota.Cache
|
||||
uls *uls.Store // US callsign→county/grid (offline FCC ULS), lazily opened
|
||||
awardRefs *awardref.Repo
|
||||
@@ -466,6 +467,7 @@ type App struct {
|
||||
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
|
||||
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
|
||||
spe *spe.Client // SPE Expert amplifier (serial/TCP); nil when disabled or not SPE
|
||||
acom *acom.Client // ACOM 500S/600S/700S/1200S/2020S amplifier (serial/TCP); nil when disabled or not ACOM
|
||||
audioMgr *audio.Manager
|
||||
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
|
||||
solar *solar.Manager // live space-weather (SFI/SSN/A/K) for the header + QSO stamping
|
||||
@@ -481,27 +483,27 @@ type App struct {
|
||||
|
||||
alertStore *alerts.Store // DX-cluster spot alert rules (global JSON)
|
||||
|
||||
startupProfile string // --profile <name> from the command line (activate at startup)
|
||||
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
|
||||
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
|
||||
startupProfile string // --profile <name> from the command line (activate at startup)
|
||||
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
|
||||
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
|
||||
pttMu sync.Mutex
|
||||
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
|
||||
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
|
||||
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
||||
relayAutoLast map[string]bool // deviceID|relay → last applied on/off, so we only switch on a real change
|
||||
relayAutoOn atomic.Bool // cached "auto-control enabled" so the CAT hot path skips work when off
|
||||
relayDrvMu sync.Mutex // guards the cached relay drivers below
|
||||
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
|
||||
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
|
||||
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
|
||||
relayAutoLast map[string]bool // deviceID|relay → last applied on/off, so we only switch on a real change
|
||||
relayAutoOn atomic.Bool // cached "auto-control enabled" so the CAT hot path skips work when off
|
||||
relayDrvMu sync.Mutex // guards the cached relay drivers below
|
||||
relayDrv map[string]cachedRelay // deviceID → live driver (reused across polls; stateful boards can't be reopened per call)
|
||||
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
|
||||
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
|
||||
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
|
||||
startupErr string // captured for surfacing to the frontend
|
||||
dbPath string // active database file (may be a user-chosen location)
|
||||
logDb *sql.DB // QSO logbook connection — MySQL when the shared backend is enabled, else == db (local SQLite)
|
||||
dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup
|
||||
dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite
|
||||
offlineQ *offlineq.Queue // ADIF outbox: QSOs logged while the DB was unreachable
|
||||
offlineMode bool // last write failed because the DB was unreachable
|
||||
pttPort serial.Port // open serial port while PTT (RTS/DTR) is asserted
|
||||
pttKeyedMethod string // "cat" | "rts" | "dtr" while keyed; "" when idle
|
||||
pttGen int64 // bumped on every key; a delayed unkey only fires if unchanged (guards against a stale release cutting a new transmission)
|
||||
startupErr string // captured for surfacing to the frontend
|
||||
dbPath string // active database file (may be a user-chosen location)
|
||||
logDb *sql.DB // QSO logbook connection — MySQL when the shared backend is enabled, else == db (local SQLite)
|
||||
dbBackend string // "sqlite" | "mysql" — the logbook backend actually opened at startup
|
||||
dbBackendErr string // non-empty when a configured MySQL backend failed and we fell back to SQLite
|
||||
offlineQ *offlineq.Queue // ADIF outbox: QSOs logged while the DB was unreachable
|
||||
offlineMode bool // last write failed because the DB was unreachable
|
||||
|
||||
catFlexSpots bool // push cluster spots to the FlexRadio panadapter
|
||||
catFlexDecodeSpots bool // push WSJT-X decodes (heard stations) to the panadapter
|
||||
@@ -511,11 +513,13 @@ type App struct {
|
||||
liveBand string
|
||||
liveMode string
|
||||
livePublishTimer *time.Timer // debounced live-status publish on activity change
|
||||
liveLastQSOAt time.Time // when this operator last logged a NEW contact — drives online/offline
|
||||
awardSnapMu sync.Mutex // guards the award QSO snapshot
|
||||
awardSnap []qso.QSO // light-scanned + enriched logbook snapshot reused across award computations
|
||||
awardSnapRev string // logbook revision the snapshot was built at ("" = none)
|
||||
dataDir string // <exeDir>/data — holds config.json, logs, cty.dat
|
||||
liveLastQSOAt time.Time // when this operator last logged a NEW contact — drives online/offline
|
||||
liveTableMu sync.Mutex // guards liveTableFor
|
||||
liveTableFor *sql.DB // logbook whose live_status DDL has been ensured (once per connection, not per call)
|
||||
awardSnapMu sync.Mutex // guards the award QSO snapshot
|
||||
awardSnap []qso.QSO // light-scanned + enriched logbook snapshot reused across award computations
|
||||
awardSnapRev string // logbook revision the snapshot was built at ("" = none)
|
||||
dataDir string // <exeDir>/data — holds config.json, logs, cty.dat
|
||||
|
||||
// shuttingDown gates beforeClose re-entry: the first user attempt to
|
||||
// close fires shutdown tasks (backup, future LoTW upload, ...) while
|
||||
@@ -859,9 +863,9 @@ func (a *App) startup(ctx context.Context) {
|
||||
applog.Printf("startup: logbook backend = %s", backend)
|
||||
a.logDb = logbookConn
|
||||
a.qso = qso.NewRepo(logbookConn)
|
||||
a.backfillAwardRefsOnce() // one-time: materialise award_refs for pre-existing QSOs
|
||||
go a.rebuildWorkedIndex() // in-memory worked-index for per-spot alert checks
|
||||
go a.adifMonitorLoop() // watch external ADIF files (fldigi, N1MM…) for new QSOs
|
||||
a.backfillAwardRefsOnce() // one-time: materialise award_refs for pre-existing QSOs
|
||||
go a.rebuildWorkedIndex() // in-memory worked-index for per-spot alert checks
|
||||
go a.adifMonitorLoop() // watch external ADIF files (fldigi, N1MM…) for new QSOs
|
||||
a.relayAutoOn.Store(a.GetRelayAuto().Enabled) // prime the relay auto-control hot-path flag
|
||||
|
||||
// cty.dat for offline DXCC / country resolution. Cached on disk; first
|
||||
@@ -1955,7 +1959,7 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
|
||||
if err == nil {
|
||||
q.ID = id
|
||||
a.noteWorked(q.Callsign, q.Band, q.Mode) // keep the alert worked-index fresh (in-memory)
|
||||
a.noteLiveQSO() // multi-op: flip this operator back "online" (publishes async)
|
||||
a.noteLiveQSO() // multi-op: flip this operator back "online" (publishes async)
|
||||
// Announce the log RIGHT AWAY so the grid/UI refresh at once and the entry
|
||||
// form clears immediately — the operator is not made to wait on the DB.
|
||||
wruntime.EventsEmit(a.ctx, "qso:logged", id)
|
||||
@@ -4334,8 +4338,8 @@ func (a *App) ExportAwardForCatalog(code string, version int) (string, error) {
|
||||
return "", fmt.Errorf("load references: %w", err)
|
||||
}
|
||||
entry := struct {
|
||||
Def award.Def `json:"def"`
|
||||
References []awardref.Ref `json:"references,omitempty"`
|
||||
Def award.Def `json:"def"`
|
||||
References []awardref.Ref `json:"references,omitempty"`
|
||||
}{Def: def, References: refs}
|
||||
b, err := json.MarshalIndent(entry, "", " ")
|
||||
if err != nil {
|
||||
@@ -6309,7 +6313,7 @@ type clusterQueue struct {
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
buf []clusterEvent
|
||||
head int // index of the next event to pop (waste reclaimed by compaction)
|
||||
head int // index of the next event to pop (waste reclaimed by compaction)
|
||||
closed bool
|
||||
}
|
||||
|
||||
@@ -11342,7 +11346,6 @@ func (a *App) SaveRotatorSettings(s RotatorSettings) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// RotatorHeading is the live antenna heading for the status bar.
|
||||
type RotatorHeading struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -11461,14 +11464,14 @@ func boolStr(b bool) string {
|
||||
|
||||
// StationDevice is one configured relay board for the Station Control tab.
|
||||
type StationDevice struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "webswitch" | "kmtronic" | "denkovi" | "usbrelay"
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"` // IP for network boards; FTDI serial for denkovi; COM port for usbrelay
|
||||
User string `json:"user,omitempty"` // KMTronic HTTP auth (blank = none)
|
||||
Pass string `json:"pass,omitempty"`
|
||||
Channels int `json:"channels,omitempty"` // usbrelay only: number of relays (1/2/4/8/16)
|
||||
Labels []string `json:"labels"` // per-relay label (index 0 = relay 1)
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "webswitch" | "kmtronic" | "denkovi" | "usbrelay"
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"` // IP for network boards; FTDI serial for denkovi; COM port for usbrelay
|
||||
User string `json:"user,omitempty"` // KMTronic HTTP auth (blank = none)
|
||||
Pass string `json:"pass,omitempty"`
|
||||
Channels int `json:"channels,omitempty"` // usbrelay only: number of relays (1/2/4/8/16)
|
||||
Labels []string `json:"labels"` // per-relay label (index 0 = relay 1)
|
||||
}
|
||||
|
||||
// deviceRelayCount is the relay count for a configured device — fixed by type,
|
||||
@@ -11741,12 +11744,12 @@ type motorAntenna interface {
|
||||
// convention, so commands pass straight through).
|
||||
type ubAdapter struct{ c *ultrabeam.Client }
|
||||
|
||||
func (a ubAdapter) Start() error { return a.c.Start() }
|
||||
func (a ubAdapter) Stop() { a.c.Stop() }
|
||||
func (a ubAdapter) SetFrequency(k, d int) error { return a.c.SetFrequency(k, d) }
|
||||
func (a ubAdapter) SetDirection(d int) error { return a.c.SetDirection(d) }
|
||||
func (a ubAdapter) Retract() error { return a.c.Retract() }
|
||||
func (a ubAdapter) LastSetKHz() int { return a.c.LastSetKHz() }
|
||||
func (a ubAdapter) Start() error { return a.c.Start() }
|
||||
func (a ubAdapter) Stop() { a.c.Stop() }
|
||||
func (a ubAdapter) SetFrequency(k, d int) error { return a.c.SetFrequency(k, d) }
|
||||
func (a ubAdapter) SetDirection(d int) error { return a.c.SetDirection(d) }
|
||||
func (a ubAdapter) Retract() error { return a.c.Retract() }
|
||||
func (a ubAdapter) LastSetKHz() int { return a.c.LastSetKHz() }
|
||||
func (a ubAdapter) SetElement(n, mm int) error { return a.c.ModifyElement(n, mm) }
|
||||
func (a ubAdapter) ReadElements() ([]int, error) { return a.c.ReadElements() }
|
||||
func (a ubAdapter) Elements() []int {
|
||||
@@ -12269,11 +12272,12 @@ func (a *App) AntGeniusDeselect(port int) error {
|
||||
// ── PowerGenius XL (4O3A) amplifier fan control (TCP, default port 9008) ─────
|
||||
|
||||
// PGXLSettings is the JSON shape for the Hardware → Amplifier panel. It covers
|
||||
// the 4O3A PowerGenius XL (TCP) and the SPE Expert amps (USB serial or an
|
||||
// RS232-to-Ethernet bridge). The type name is kept for binding compatibility.
|
||||
// the 4O3A PowerGenius XL (TCP), the SPE Expert amps and the ACOM S-series amps
|
||||
// (both: USB serial or an RS232-to-Ethernet bridge). The type name is kept for
|
||||
// binding compatibility.
|
||||
type PGXLSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Type string `json:"type"` // "pgxl" | "spe13" | "spe15" | "spe2k"
|
||||
Type string `json:"type"` // "pgxl" | "spe13" | "spe15" | "spe2k" | "acom500" | "acom600" | "acom700" | "acom1200" | "acom2020"
|
||||
Transport string `json:"transport"` // "tcp" | "serial"
|
||||
Host string `json:"host"` // TCP transport
|
||||
Port int `json:"port"` // TCP transport
|
||||
@@ -12354,6 +12358,10 @@ func (a *App) startPGXL() {
|
||||
go a.spe.Stop()
|
||||
a.spe = nil
|
||||
}
|
||||
if a.acom != nil {
|
||||
go a.acom.Stop()
|
||||
a.acom = nil
|
||||
}
|
||||
s, err := a.GetPGXLSettings()
|
||||
if err != nil || !s.Enabled {
|
||||
return
|
||||
@@ -12366,13 +12374,18 @@ func (a *App) startPGXL() {
|
||||
_ = a.pgxl.Start()
|
||||
return
|
||||
}
|
||||
// SPE Expert — USB serial or an RS232-to-Ethernet bridge.
|
||||
// SPE Expert / ACOM — USB serial or an RS232-to-Ethernet bridge.
|
||||
if s.Transport == "serial" && strings.TrimSpace(s.ComPort) == "" {
|
||||
return
|
||||
}
|
||||
if s.Transport == "tcp" && strings.TrimSpace(s.Host) == "" {
|
||||
return
|
||||
}
|
||||
if model, ok := strings.CutPrefix(s.Type, "acom"); ok {
|
||||
a.acom = acom.New(acom.Config{Model: model + "S", Transport: s.Transport, ComPort: s.ComPort, Baud: s.Baud, Host: s.Host, Port: s.Port})
|
||||
_ = a.acom.Start()
|
||||
return
|
||||
}
|
||||
a.spe = spe.New(spe.Config{Transport: s.Transport, ComPort: s.ComPort, Baud: s.Baud, Host: s.Host, Port: s.Port})
|
||||
_ = a.spe.Start()
|
||||
}
|
||||
@@ -12413,6 +12426,35 @@ func (a *App) SPESetPowerLevel(level string) error {
|
||||
return a.spe.SetPowerLevel(level)
|
||||
}
|
||||
|
||||
// GetACOMStatus returns the ACOM amplifier state for the UI poll.
|
||||
func (a *App) GetACOMStatus() acom.Status {
|
||||
if a.acom == nil {
|
||||
return acom.Status{}
|
||||
}
|
||||
return a.acom.GetStatus()
|
||||
}
|
||||
|
||||
// ACOMSetOperate puts the ACOM amp in OPERATE (true) or STANDBY (false) — the
|
||||
// protocol has explicit commands for each, unlike the SPE's toggle key.
|
||||
func (a *App) ACOMSetOperate(on bool) error {
|
||||
if a.acom == nil {
|
||||
return fmt.Errorf("ACOM amplifier not connected — enable it in Settings → Amplifier")
|
||||
}
|
||||
return a.acom.Operate(on)
|
||||
}
|
||||
|
||||
// ACOMSetPower turns the ACOM amp on (true, a DTR/RTS pulse — serial only, needs
|
||||
// the power-on pins wired in the cable) or off (false, the OFF data command).
|
||||
func (a *App) ACOMSetPower(on bool) error {
|
||||
if a.acom == nil {
|
||||
return fmt.Errorf("ACOM amplifier not connected — enable it in Settings → Amplifier")
|
||||
}
|
||||
if on {
|
||||
return a.acom.PowerOn()
|
||||
}
|
||||
return a.acom.PowerOff()
|
||||
}
|
||||
|
||||
// GetPGXLStatus returns the amp's fan/connection state for the UI poll.
|
||||
func (a *App) GetPGXLStatus() powergenius.Status {
|
||||
if a.pgxl == nil {
|
||||
|
||||
Reference in New Issue
Block a user