chore: release v0.27.12
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
||||
"hamlog/internal/antgenius"
|
||||
"hamlog/internal/applog"
|
||||
"hamlog/internal/audio"
|
||||
"hamlog/internal/autocall"
|
||||
"hamlog/internal/award"
|
||||
"hamlog/internal/awardref"
|
||||
"hamlog/internal/backup"
|
||||
@@ -54,6 +55,7 @@ import (
|
||||
"hamlog/internal/powergenius"
|
||||
"hamlog/internal/profile"
|
||||
"hamlog/internal/pskr"
|
||||
"hamlog/internal/pskrtgt"
|
||||
"hamlog/internal/psu"
|
||||
"hamlog/internal/qslcard"
|
||||
"hamlog/internal/qso"
|
||||
@@ -100,6 +102,7 @@ const (
|
||||
keyStationCallsign = "station.callsign"
|
||||
keyStationOperator = "station.operator"
|
||||
keyWatchlistContestPattern = "watchlist.contest_pattern" // auto-add contest calls containing this
|
||||
keyWatchlistContestCalls = "watchlist.contest_calls" // …and these, named one by one
|
||||
keyStationMyGrid = "station.my_grid"
|
||||
keyStationCountry = "station.my_country"
|
||||
keyStationSOTA = "station.my_sota_ref"
|
||||
@@ -754,6 +757,31 @@ type App struct {
|
||||
// It is the source that makes VHF detection work at all: the cluster and RBN
|
||||
// carry a handful of 6 m spots where PSK Reporter carries hundreds.
|
||||
pskr *pskr.Watcher
|
||||
// pskTgt is the SECOND PSK Reporter connection: the per-station analysis behind
|
||||
// the FT decodes panel. Separate from a.pskr because the two want opposite
|
||||
// slices of the same feed — everything heard near here, against everything
|
||||
// about one callsign anywhere. Built on first use, nil while the panel is
|
||||
// closed.
|
||||
pskTgtMu sync.Mutex
|
||||
pskTgt *pskrtgt.Watcher
|
||||
// Auto-call: the engine, the period being collected, and the last transmit
|
||||
// state the decoder reported. One mutex — every field here is touched by the
|
||||
// UDP event loop and by the two-second sweeper, and never for long.
|
||||
acMu sync.Mutex
|
||||
ac *autocall.Engine
|
||||
// Keyed by RECEIVER. Two decoders have two independent slot clocks — an
|
||||
// FT8 window and an FT4 one do not even share a period length — and one
|
||||
// buffer for both cut every slot into fragments, each judged as a period of
|
||||
// its own. The engine then counted a missed period several times a slot.
|
||||
acPeriod map[string]string
|
||||
acAt map[string]time.Time
|
||||
acTR map[string]int
|
||||
acBuf map[string][]acDecode
|
||||
acTX autocall.TXState
|
||||
acReason string
|
||||
acLastJudge time.Time
|
||||
// acTXPeriod is the last transmit period already counted — see autoCallNoteTX.
|
||||
acTXPeriod string
|
||||
// Self-spot throttle: when and on what frequency we last announced ourselves.
|
||||
// Held in memory only — a restart legitimately re-announces the station.
|
||||
selfSpotMu sync.Mutex
|
||||
@@ -769,12 +797,20 @@ type App struct {
|
||||
|
||||
// WSJT-X decode highlighting (message 13) — see app_wsjt_highlight.go.
|
||||
wsjtHighlightOn atomic.Bool
|
||||
wsjtHLMu sync.Mutex
|
||||
wsjtHLSent map[string]string
|
||||
watchPattern atomic.Value // auto-contest pattern (string), loaded at startup
|
||||
operating *operating.Repo
|
||||
udp *udp.Manager
|
||||
udpRepo *udp.Repo
|
||||
// Greying out what is already worked is a sub-option of the same feature —
|
||||
// read on the decode path, so an atomic rather than a settings lookup per
|
||||
// decode.
|
||||
wsjtHLWorkedOn atomic.Bool
|
||||
wsjtHLMu sync.Mutex
|
||||
wsjtHLSent map[string]string
|
||||
watchPattern atomic.Value // auto-contest pattern (string), loaded at startup
|
||||
// The named contest callsigns, as a set, loaded with the pattern and read on
|
||||
// the same hot path — one spot per station on the band, so no settings
|
||||
// lookup and no split per spot.
|
||||
watchCalls atomic.Value // map[string]struct{}
|
||||
operating *operating.Repo
|
||||
udp *udp.Manager
|
||||
udpRepo *udp.Repo
|
||||
// Program id of the last decoding application that reported its status.
|
||||
// Halt Tx is routed by id, and the panel's Halt button must work even when
|
||||
// nothing is transmitting at that instant — so the id is remembered from
|
||||
@@ -930,6 +966,7 @@ type App struct {
|
||||
opLon float64
|
||||
opSet bool
|
||||
opCall string // active profile callsign, cached for outbound UDP emitters
|
||||
opGrid string // active profile locator, cached the same way and for the same reason
|
||||
|
||||
// Dedup for the frequency/mode outbound UDP emitters (PstRotator, N1MM): only
|
||||
// send when the frequency or mode actually changes.
|
||||
@@ -979,6 +1016,7 @@ func (a *App) refreshOperatorGrid() {
|
||||
return
|
||||
}
|
||||
a.opCall = strings.ToUpper(strings.TrimSpace(p.Callsign))
|
||||
a.opGrid = strings.ToUpper(strings.TrimSpace(p.MyGrid))
|
||||
lat, lon, ok := gridToLatLon(p.MyGrid)
|
||||
if !ok {
|
||||
return
|
||||
@@ -1487,7 +1525,9 @@ func (a *App) startup(ctx context.Context) {
|
||||
a.pota = pota.New(func(format string, args ...any) { applog.Printf(format, args...) })
|
||||
a.startWatchlistClubLog()
|
||||
a.watchPattern.Store(strings.ToUpper(strings.TrimSpace(a.settingOr(keyWatchlistContestPattern, ""))))
|
||||
a.watchCalls.Store(parseContestCalls(a.settingOr(keyWatchlistContestCalls, "")))
|
||||
a.wsjtHighlightOn.Store(a.settingOr(keyWsjtHighlight, "0") == "1")
|
||||
a.wsjtHLWorkedOn.Store(a.settingOr(keyWsjtHLWorked, "0") == "1")
|
||||
go a.pota.Run(a.ctx)
|
||||
|
||||
// DX Cluster (multi-server): the spot callback enriches each spot
|
||||
@@ -1616,6 +1656,9 @@ func (a *App) startup(ctx context.Context) {
|
||||
// PSK Reporter. After the operator's grid is known: without it there is no
|
||||
// distance to measure and no receiver squares to filter on, so it stays down.
|
||||
a.startBandOpenFeed()
|
||||
// Auto-call. After the watch list and the logbook: the ladder is meaningless
|
||||
// without either, and it must never call anybody on a log it cannot read.
|
||||
a.startAutoCall()
|
||||
// One-time tidy-up of a field nothing used to record. Background, once.
|
||||
a.backfillDistancesOnce()
|
||||
|
||||
@@ -1879,6 +1922,7 @@ func (a *App) shutdown(ctx context.Context) {
|
||||
// logger has spent seconds tearing down ports.
|
||||
applog.Printf("shutdown: closing autostart programs")
|
||||
a.CloseAutostartPrograms()
|
||||
a.stopPSKTarget() // one TLS socket to a public broker; nothing to flush
|
||||
applog.Printf("shutdown: stopping UDP")
|
||||
if a.udp != nil {
|
||||
a.udp.StopAll()
|
||||
@@ -14328,6 +14372,17 @@ func (a *App) consumeUDPEvents() {
|
||||
map[bool]string{true: "", false: " — nothing we send can start a transmission while this is false"}[now.enabled])
|
||||
}
|
||||
}
|
||||
// Auto-call counts what actually goes on the air from here: a reply
|
||||
// we sent is a request, and this is the answer to whether the
|
||||
// decoder acted on it.
|
||||
acTX := autocall.TXState{
|
||||
Transmitting: ev.Transmitting, Enabled: ev.TxEnabled,
|
||||
DXCall: ev.DXCall, Msg: ev.TxMessage, Instance: ev.ProgramID,
|
||||
}
|
||||
a.autoCallSetTX(acTX)
|
||||
if ev.Transmitting {
|
||||
a.autoCallNoteTX(acTX)
|
||||
}
|
||||
wruntime.EventsEmit(a.ctx, "udp:tx_state", map[string]any{
|
||||
"msg": ev.TxMessage,
|
||||
"transmitting": ev.Transmitting,
|
||||
@@ -14385,10 +14440,20 @@ func (a *App) consumeUDPEvents() {
|
||||
// false on a Replay's resent history — shown, never auto-answered.
|
||||
"is_new": ev.DecodeIsNew,
|
||||
})
|
||||
// Auto-call sees the decode after the panel, never before: the list
|
||||
// the operator reads must not wait on a decision about calling.
|
||||
a.autoCallFeed(autocall.Decode{
|
||||
Call: ev.DecodeCall, Band: bandForHz(ev.DecodeFreqHz), Mode: ev.Mode,
|
||||
Msg: ev.DecodeMsg, SNR: ev.DecodeSNR, At: at, TRPeriod: ev.DecodeTRPeriod,
|
||||
Instance: ev.ProgramID, CQ: ev.DecodeCQ,
|
||||
Ms: ev.DecodeMs, DT: ev.DecodeDT, AudioHz: int64(ev.DecodeAudioHz),
|
||||
ModeRaw: ev.DecodeModeRaw, MsgRaw: ev.DecodeMsgRaw, LowConf: ev.DecodeLowConf,
|
||||
IsNew: ev.DecodeIsNew,
|
||||
})
|
||||
// Log-aware colour in the decoder's own window (see
|
||||
// app_wsjt_highlight.go). After the emit: painting must never delay
|
||||
// the panel.
|
||||
a.maybeHighlightDecode(ev.ProgramID, ev.DecodeCall, bandForHz(ev.DecodeFreqHz))
|
||||
a.maybeHighlightDecode(ev.ProgramID, ev.DecodeCall, bandForHz(ev.DecodeFreqHz), ev.Mode)
|
||||
// A WSJT-X decode (heard station). Render it on the FlexRadio
|
||||
// panadapter when the option is on; green + SNR comment, auto-expiring
|
||||
// after the configured duration. De-duped per call in the Flex backend.
|
||||
@@ -15167,6 +15232,53 @@ func (a *App) IcomSetPower(on bool) error {
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetPower(on) })
|
||||
}
|
||||
|
||||
// icomBandCodes maps a band label to the code the band stacking registers use
|
||||
// (CI-V 0x1A 0x01). Deliberately partial.
|
||||
//
|
||||
// The HF codes 01-09 are the same on every Icom that has HF, and are safe. What
|
||||
// is NOT the same is everything above them: 50 MHz is 10 on an HF+6 rig, while
|
||||
// an IC-9700 has no HF at all and numbers its three bands 144/430/1200 as 1/2/3.
|
||||
// A band missing here is not a failure — the caller falls back to sending a
|
||||
// frequency, which is what the button did before — so an unlisted band or an
|
||||
// unlisted model loses nothing, and a GUESSED code would land the operator on
|
||||
// another band entirely.
|
||||
func icomBandCodes(model string) map[string]int {
|
||||
m := strings.ToUpper(model)
|
||||
if strings.Contains(m, "9700") {
|
||||
return map[string]int{"2": 1, "70cm": 2, "23cm": 3}
|
||||
}
|
||||
// HF + 6 m: the 7300 / 7610 / 7760 / 7100 / 7851 / 705 / 9100 class. 60 m and
|
||||
// 4 m are absent on purpose — they are not a band key on these radios.
|
||||
return map[string]int{
|
||||
"160": 1, "80": 2, "40": 3, "30": 4, "20": 5,
|
||||
"17": 6, "15": 7, "12": 8, "10": 9, "6": 0x10,
|
||||
}
|
||||
}
|
||||
|
||||
// IcomRecallBand puts the radio where its own band stacking register says the
|
||||
// operator last was on that band — the front panel's band key, from here.
|
||||
//
|
||||
// reg is 1, 2 or 3, so pressing the same band button again walks through the
|
||||
// registers exactly as the radio's key does. Returns the frequency landed on;
|
||||
// an error means nothing was changed and the caller should send its own
|
||||
// frequency instead.
|
||||
func (a *App) IcomRecallBand(band string, reg int) (int64, error) {
|
||||
if a.cat == nil {
|
||||
return 0, fmt.Errorf("cat not initialized")
|
||||
}
|
||||
code, ok := icomBandCodes(a.cat.State().Rig)[strings.ToLower(strings.TrimSpace(band))]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("no band stacking register for %s on this radio", band)
|
||||
}
|
||||
var hz int64
|
||||
err := a.cat.IcomDo(func(ic cat.IcomController) error {
|
||||
var e error
|
||||
hz, e = ic.RecallBandStack(code, reg)
|
||||
return e
|
||||
})
|
||||
return hz, err
|
||||
}
|
||||
|
||||
// IcomSetScope enables/disables the spectrum-scope waveform stream.
|
||||
func (a *App) IcomSetScope(on bool) error {
|
||||
if a.cat == nil {
|
||||
@@ -16577,6 +16689,14 @@ func (a *App) reloadAfterProfileSwitch() {
|
||||
// per-profile settings too — rebuilt so a switch doesn't keep showing the
|
||||
// previous profile's view of who is worth chasing.
|
||||
a.startWatchlistClubLog()
|
||||
// Auto-call is per profile — attempts, the "call only" callsign, whether it
|
||||
// is on at all — and it TRANSMITS. A switch that left the previous profile's
|
||||
// settings running would have the wrong station calling on the wrong log.
|
||||
// Re-applied rather than restarted: the loop is one goroutine for the life
|
||||
// of the process, and applyAutoCall clears the target when the feature is
|
||||
// off in the profile just activated.
|
||||
a.applyAutoCall()
|
||||
a.autoCallEngine().Reset()
|
||||
}
|
||||
|
||||
// DuplicateProfile clones an existing profile under newName. Useful when
|
||||
|
||||
Reference in New Issue
Block a user