With one rig, MY_RIG belonged in Operating conditions and nowhere else. With several, that becomes the wrong place: operating conditions describe a PLAN — 'on 20 m I use the beam and the 7300' — while the question a logged QSO asks is which radio was keying. So each saved radio carries its own MY_RIG, consulted BEFORE the per-band station. When the two disagree — a second rig borrowed for one evening on 20 m — the one that is transmitting is the true answer. Left empty it changes nothing at all: the old chain (band default, then the profile's rig) answers exactly as it did, which is what every single-radio station will keep doing without touching a setting.
238 lines
7.9 KiB
Go
238 lines
7.9 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 ""
|
|
}
|