chore: release v0.19.3
This commit is contained in:
@@ -47,6 +47,7 @@ import (
|
||||
"hamlog/internal/qso"
|
||||
"hamlog/internal/rotator/pst"
|
||||
"hamlog/internal/settings"
|
||||
"hamlog/internal/solar"
|
||||
"hamlog/internal/ultrabeam"
|
||||
"hamlog/internal/winkeyer"
|
||||
|
||||
@@ -420,6 +421,7 @@ type App struct {
|
||||
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
|
||||
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
|
||||
|
||||
// NET Control: persistent net definitions/rosters (global JSON) + the live
|
||||
// session (in-memory only — active stations currently in QSO).
|
||||
@@ -874,6 +876,16 @@ func (a *App) startup(ctx context.Context) {
|
||||
},
|
||||
)
|
||||
|
||||
// Live space-weather (solar flux, sunspots, A/K indices) for the header strip
|
||||
// and per-QSO stamping. Refreshes in the background; pushes a UI event on each
|
||||
// update so the strip re-reads.
|
||||
a.solar = solar.NewManager(func() {
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "solar:update")
|
||||
}
|
||||
})
|
||||
a.solar.Start()
|
||||
|
||||
// Digital Voice Keyer + QSO recorder (WASAPI). Idle until used.
|
||||
a.audioMgr = audio.NewManager(func() {
|
||||
st := a.dvkStatus()
|
||||
@@ -1116,6 +1128,9 @@ func (a *App) shutdown(ctx context.Context) {
|
||||
if a.udp != nil {
|
||||
a.udp.StopAll()
|
||||
}
|
||||
if a.solar != nil {
|
||||
a.solar.Stop()
|
||||
}
|
||||
// Stop CAT so the backend disconnects cleanly. Critical for the Icom network
|
||||
// backend: without this the rig never gets a disconnect and holds its single
|
||||
// control session for minutes, refusing every new login (even from the Icom
|
||||
@@ -1643,6 +1658,7 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
|
||||
a.applyClublogException(&q, false) // override entity for date-ranged DXpeditions
|
||||
a.refineDistrictZones(&q) // W6 → CQ3/ITU6 for zone-split countries
|
||||
a.applyQSLDefaults(&q)
|
||||
a.applySolar(&q) // stamp SFI / A / K (and SSN as an extra) from live space-weather
|
||||
// Fill the contacted operator's e-mail from the (cached) lookup so the
|
||||
// recording can be auto-sent. Cheap: the entry already looked the call up.
|
||||
if strings.TrimSpace(q.Email) == "" && a.lookup != nil {
|
||||
@@ -1666,6 +1682,58 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) {
|
||||
return id, err
|
||||
}
|
||||
|
||||
// applySolar stamps the current space-weather onto a QSO before it's saved: the
|
||||
// standard ADIF SFI / A_INDEX / K_INDEX fields (only when the QSO doesn't already
|
||||
// carry them), and the sunspot number as an APP_OPSLOG_SSN extra (SSN has no
|
||||
// standard ADIF field). No-op when the feed hasn't loaded yet.
|
||||
func (a *App) applySolar(q *qso.QSO) {
|
||||
if a.solar == nil {
|
||||
return
|
||||
}
|
||||
d := a.solar.Get()
|
||||
if !d.OK {
|
||||
return
|
||||
}
|
||||
if q.SFI == nil {
|
||||
if v, err := strconv.ParseFloat(strings.TrimSpace(d.SFI), 64); err == nil {
|
||||
q.SFI = &v
|
||||
}
|
||||
}
|
||||
if q.AIndex == nil {
|
||||
if v, err := strconv.ParseFloat(strings.TrimSpace(d.AIndex), 64); err == nil {
|
||||
q.AIndex = &v
|
||||
}
|
||||
}
|
||||
if q.KIndex == nil {
|
||||
if v, err := strconv.ParseFloat(strings.TrimSpace(d.KIndex), 64); err == nil {
|
||||
q.KIndex = &v
|
||||
}
|
||||
}
|
||||
if ssn := strings.TrimSpace(d.SSN); ssn != "" {
|
||||
if q.Extras == nil {
|
||||
q.Extras = map[string]string{}
|
||||
}
|
||||
if _, ok := q.Extras["APP_OPSLOG_SSN"]; !ok {
|
||||
q.Extras["APP_OPSLOG_SSN"] = ssn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSolarData returns the latest space-weather snapshot for the header strip.
|
||||
func (a *App) GetSolarData() solar.Data {
|
||||
if a.solar == nil {
|
||||
return solar.Data{}
|
||||
}
|
||||
return a.solar.Get()
|
||||
}
|
||||
|
||||
// RefreshSolar forces an immediate re-fetch of the space-weather feed.
|
||||
func (a *App) RefreshSolar() {
|
||||
if a.solar != nil {
|
||||
go a.solar.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// StationInfoComputed bundles the data we resolve live from the
|
||||
// profile's callsign + grid: country, ARRL DXCC#, CQ zone, ITU zone,
|
||||
// lat/lon. Used by the Settings UI to show the "what will be stamped on
|
||||
|
||||
Reference in New Issue
Block a user