Two halves of the same station confusion. The entry form pre-filled MY_RIG from the per-band default on every band change, so the log-time priority (the radio that is keying beats the radio that was planned) never ran — the field was no longer empty by the time the backend looked. The form now asks the active radio first, through a new ActiveRadioMyRig binding. And the manual record button started its capture from the From-radio sound card unconditionally, where the automatic recorder already knew a network Icom's audio is pushed from the 50003 stream: a manual take on the IC-7760 was a faithful recording of the Flex's DAX. Same source rule for both, and the button is offered with no sound card at all when the network stream is the source.
247 lines
8.3 KiB
Go
247 lines
8.3 KiB
Go
package main
|
|
|
|
// Several radios, and the one that is on the air.
|
|
//
|
|
// Until now the answer to "I have two rigs" was "make two profiles", which is a
|
|
// heavy instrument for a light question: a profile carries the whole station —
|
|
// logbook, callsign, awards, cluster, macros — and switching one to change
|
|
// which radio is connected takes all of that with it, including a logbook
|
|
// change if the profiles point at different databases.
|
|
//
|
|
// So radios are a LIST, the way amplifiers already are, and switching is one
|
|
// click on the CAT chip in the status bar. Nothing else about the station
|
|
// moves.
|
|
//
|
|
// The storage deliberately keeps the ACTIVE radio in the settings keys the rest
|
|
// of OpsLog already reads (cat.backend, cat.icom_port, …). Switching writes the
|
|
// chosen entry into those keys and reloads the link, so every consumer — the
|
|
// consoles, the CAT sharing, the band-follow, the panels — sees exactly what it
|
|
// saw before and needed no change at all. The list is a second store beside it,
|
|
// not a replacement for it.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"hamlog/internal/applog"
|
|
)
|
|
|
|
const (
|
|
// keyRadiosList holds the saved radios as JSON.
|
|
keyRadiosList = "cat.radios.json"
|
|
// keyRadioActive is the id of the one currently connected.
|
|
keyRadioActive = "cat.radios.active"
|
|
)
|
|
|
|
// RadioConfig is one saved radio: a name and the CAT settings that reach it.
|
|
type RadioConfig struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
// Settings is the whole CAT configuration for this radio — the same shape
|
|
// the CAT panel has always edited, so a saved radio is exactly "what the
|
|
// settings said the day it was saved".
|
|
Settings CATSettings `json:"settings"`
|
|
// MyRig is what goes into MY_RIG on a QSO made with this radio.
|
|
//
|
|
// It belongs here rather than in Operating conditions once there is more
|
|
// than one rig: the operating conditions describe a PLAN — "on 20 m I use
|
|
// the beam and the 7300" — while this is the fact of which radio is keying.
|
|
// Left empty, nothing changes: the old chain (band default, then the
|
|
// profile's rig) answers exactly as it did.
|
|
MyRig string `json:"my_rig"`
|
|
}
|
|
|
|
// radioLabel is what the status bar shows when the operator never named one.
|
|
func radioLabel(c RadioConfig, i int) string {
|
|
if n := strings.TrimSpace(c.Name); n != "" {
|
|
return n
|
|
}
|
|
if b := strings.TrimSpace(c.Settings.Backend); b != "" {
|
|
return strings.ToUpper(b)
|
|
}
|
|
return fmt.Sprintf("Radio %d", i+1)
|
|
}
|
|
|
|
// GetRadios returns the saved radios.
|
|
//
|
|
// When nothing was ever saved, the CURRENT settings are presented as the single
|
|
// entry — so an operator who has been running one rig for a year opens the list
|
|
// and finds it there, rather than an empty box suggesting their configuration
|
|
// has been lost. It is persisted on the next save, not here: reading a list
|
|
// should not write one.
|
|
func (a *App) GetRadios() ([]RadioConfig, error) {
|
|
if a.settings == nil {
|
|
return nil, fmt.Errorf("db not initialized")
|
|
}
|
|
raw := a.settingOr(keyRadiosList, "")
|
|
if strings.TrimSpace(raw) != "" {
|
|
var list []RadioConfig
|
|
if err := json.Unmarshal([]byte(raw), &list); err == nil && len(list) > 0 {
|
|
return list, nil
|
|
}
|
|
}
|
|
cur, err := a.GetCATSettings()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []RadioConfig{{ID: "radio-1", Name: "", Settings: cur}}, nil
|
|
}
|
|
|
|
// SaveRadios stores the list, giving an id to anything new.
|
|
func (a *App) SaveRadios(list []RadioConfig) error {
|
|
if a.settings == nil {
|
|
return fmt.Errorf("db not initialized")
|
|
}
|
|
for i := range list {
|
|
if strings.TrimSpace(list[i].ID) == "" {
|
|
list[i].ID = fmt.Sprintf("radio-%d-%d", time.Now().Unix(), i)
|
|
}
|
|
}
|
|
b, err := json.Marshal(list)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.settings.Set(a.ctx, keyRadiosList, string(b))
|
|
}
|
|
|
|
// ActiveRadioID is the id of the radio currently on the air, or the first one
|
|
// when nothing was ever chosen.
|
|
func (a *App) ActiveRadioID() string {
|
|
id := strings.TrimSpace(a.settingOr(keyRadioActive, ""))
|
|
list, err := a.GetRadios()
|
|
if err != nil || len(list) == 0 {
|
|
return id
|
|
}
|
|
for _, r := range list {
|
|
if r.ID == id {
|
|
return id
|
|
}
|
|
}
|
|
// The stored id names a radio that has since been deleted. The first one is
|
|
// a better answer than an empty selection: the CAT link is up, and it is up
|
|
// on SOMETHING.
|
|
return list[0].ID
|
|
}
|
|
|
|
// RadioListEntry is what the status-bar menu needs: enough to draw a row.
|
|
//
|
|
// Name is what the OPERATOR typed, empty when they typed nothing; Label is what
|
|
// to draw when there is no better idea. The two are separate because the caller
|
|
// has a better idea than we do: the status bar knows what the radio calls
|
|
// itself over CAT, and "FTDX10" beats "Radio 2" — but only where the operator
|
|
// has not given it a name of their own, which beats both.
|
|
type RadioListEntry struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Label string `json:"label"`
|
|
Backend string `json:"backend"`
|
|
Active bool `json:"active"`
|
|
}
|
|
|
|
// ListRadios is the status bar's view of the list.
|
|
func (a *App) ListRadios() []RadioListEntry {
|
|
list, err := a.GetRadios()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
active := a.ActiveRadioID()
|
|
out := make([]RadioListEntry, 0, len(list))
|
|
for i, r := range list {
|
|
out = append(out, RadioListEntry{
|
|
ID: r.ID, Name: strings.TrimSpace(r.Name), Label: radioLabel(r, i),
|
|
Backend: r.Settings.Backend, Active: r.ID == active,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SetActiveRadio connects the given radio and leaves the rest of the station
|
|
// alone.
|
|
//
|
|
// The chosen entry's settings become THE CAT settings — SaveCATSettings writes
|
|
// them and restarts the link — so switching rigs is the same operation as
|
|
// editing the CAT panel and pressing Save, which is a path that already works
|
|
// everywhere it needs to.
|
|
func (a *App) SetActiveRadio(id string) error {
|
|
list, err := a.GetRadios()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id = strings.TrimSpace(id)
|
|
for i, r := range list {
|
|
if r.ID != id {
|
|
continue
|
|
}
|
|
// Persisted BEFORE the link is rebuilt: reloadCAT can take a moment on a
|
|
// radio that is switched off, and an operator who closes OpsLog during
|
|
// that moment should still come back to the rig they chose.
|
|
a.setSetting(keyRadioActive, id)
|
|
// The list is written back as well when it was only ever implicit, so
|
|
// the first switch is also what makes the list real.
|
|
if strings.TrimSpace(a.settingOr(keyRadiosList, "")) == "" {
|
|
_ = a.SaveRadios(list)
|
|
}
|
|
applog.Printf("cat: switching to %q (%s)", radioLabel(r, i), r.Settings.Backend)
|
|
return a.SaveCATSettings(r.Settings)
|
|
}
|
|
return fmt.Errorf("no radio with id %q", id)
|
|
}
|
|
|
|
// syncActiveRadio writes the settings just saved into the active entry of the
|
|
// list, so the list and the live configuration never disagree.
|
|
//
|
|
// Only when a list actually exists: an operator with one radio who has never
|
|
// opened the list has nothing to keep in step, and writing one here would
|
|
// create a list as a side effect of saving the CAT panel.
|
|
func (a *App) syncActiveRadio(s CATSettings) {
|
|
if a.settings == nil || strings.TrimSpace(a.settingOr(keyRadiosList, "")) == "" {
|
|
return
|
|
}
|
|
list, err := a.GetRadios()
|
|
if err != nil {
|
|
return
|
|
}
|
|
id := a.ActiveRadioID()
|
|
for i := range list {
|
|
if list[i].ID == id {
|
|
list[i].Settings = s
|
|
_ = a.SaveRadios(list)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// activeRadioMyRig is the MY_RIG of the radio currently connected, or "".
|
|
//
|
|
// Consulted BEFORE the per-band default, and deliberately: the band default is
|
|
// what the operator planned to use on that band, this is which radio is
|
|
// actually on the air. When they disagree — a second rig borrowed for one
|
|
// evening on 20 m — the one that is transmitting is the true answer.
|
|
func (a *App) activeRadioMyRig() string {
|
|
if a.settings == nil || strings.TrimSpace(a.settingOr(keyRadiosList, "")) == "" {
|
|
return "" // no list was ever made: nothing to say, nothing changes
|
|
}
|
|
list, err := a.GetRadios()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
id := a.ActiveRadioID()
|
|
for _, r := range list {
|
|
if r.ID == id {
|
|
return strings.TrimSpace(r.MyRig)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ActiveRadioMyRig exposes the connected radio's MY_RIG to the frontend, which
|
|
// pre-fills the entry form's My-station fields on every band change. Without
|
|
// this the per-band default landed in the field FIRST, and the log-time
|
|
// priority (the radio that is keying beats the radio that was planned) never
|
|
// ran — the field was no longer empty by the time the backend looked.
|
|
func (a *App) ActiveRadioMyRig() string {
|
|
return a.activeRadioMyRig()
|
|
}
|