feat: FlexRadio TX power per band and mode class
Settings → FlexRadio, beside the per-band antennas and applied the same way: when the band or the mode changes. 1.5 kW that is fine on SSB has no business going into an FT8 signal on the same band. Three classes rather than one row per mode, because what decides the power is duty cycle and what the amplifier will take of it, not the mode's name — FT8 and RTTY punish an amplifier the same way, SSB and AM do not. The classification is pinned by a test: a digital mode sorted as phone would deliver exactly the 1.5 kW this exists to prevent. An empty box means "leave the power alone", never zero watts, and an unknown mode changes nothing — setting a transmit power from a name we do not recognise is not a good guess. Unlike the antennas, a locked band does not skip it: the lock means "do not let the rig drag my log entry around", not "let the amplifier keep whatever the last mode left".
This commit is contained in:
@@ -12230,6 +12230,106 @@ func (a *App) FlexApplyBandAntenna(band string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// keyFlexBandPower stores the per-band, per-mode TX power map (global,
|
||||
// machine-local — it describes this station's amplifier and antennas).
|
||||
const keyFlexBandPower = "flex.band_power"
|
||||
|
||||
// FlexBandPower is the TX power to set on a band, per class of mode.
|
||||
//
|
||||
// Three classes rather than per-mode: what decides the power is the duty cycle
|
||||
// and the amplifier's tolerance for it, not the mode's name. FT8 and RTTY hurt
|
||||
// an amplifier the same way; SSB and AM do not.
|
||||
//
|
||||
// Zero means "leave the power alone" — an empty box must not silently mean
|
||||
// zero watts, and an operator who configures 20 m only does not want every
|
||||
// other band reset when they get there.
|
||||
type FlexBandPower struct {
|
||||
Phone int `json:"phone"`
|
||||
CW int `json:"cw"`
|
||||
Digi int `json:"digi"`
|
||||
}
|
||||
|
||||
// GetFlexBandPower returns the band→power map (band key uppercased, "20M").
|
||||
func (a *App) GetFlexBandPower() (map[string]FlexBandPower, error) {
|
||||
out := map[string]FlexBandPower{}
|
||||
if a.settings == nil {
|
||||
return out, nil
|
||||
}
|
||||
v, _ := a.settings.GetGlobal(a.ctx, keyFlexBandPower)
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return out, nil
|
||||
}
|
||||
_ = json.Unmarshal([]byte(v), &out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SaveFlexBandPower persists the band→power map.
|
||||
func (a *App) SaveFlexBandPower(m map[string]FlexBandPower) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.settings.SetGlobal(a.ctx, keyFlexBandPower, string(b))
|
||||
}
|
||||
|
||||
// flexPowerClass sorts an ADIF mode into the three classes the power table uses.
|
||||
// An unknown mode returns "" and changes nothing — guessing would be setting a
|
||||
// transmit power from a name we do not recognise.
|
||||
func flexPowerClass(mode string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(mode)) {
|
||||
case "SSB", "USB", "LSB", "AM", "FM", "DV":
|
||||
return "phone"
|
||||
case "CW", "CWR":
|
||||
return "cw"
|
||||
case "FT8", "FT4", "JT65", "JT9", "JS8", "RTTY", "PSK", "PSK31", "PSK63",
|
||||
"BPSK", "QPSK", "MFSK", "OLIVIA", "CONTESTI", "DATA", "DIGITALVOICE",
|
||||
"DIGU", "DIGL", "Q65", "MSK144", "WSPR", "FST4", "FST4W", "ARDOP", "VARA":
|
||||
return "digi"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// FlexApplyBandPower sets the configured TX power for a band and mode.
|
||||
//
|
||||
// Called on band AND mode changes: the whole point is that switching to FT8 on
|
||||
// a band where 1.5 kW is fine for SSB does not put 1.5 kW into a 100% duty-cycle
|
||||
// signal. No mapping, or a zero, leaves the radio alone.
|
||||
func (a *App) FlexApplyBandPower(band, mode string) error {
|
||||
if a.cat == nil {
|
||||
return nil
|
||||
}
|
||||
band = strings.ToUpper(strings.TrimSpace(band))
|
||||
class := flexPowerClass(mode)
|
||||
if band == "" || class == "" {
|
||||
return nil
|
||||
}
|
||||
m, _ := a.GetFlexBandPower()
|
||||
e, ok := m[band]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
pct := 0
|
||||
switch class {
|
||||
case "phone":
|
||||
pct = e.Phone
|
||||
case "cw":
|
||||
pct = e.CW
|
||||
case "digi":
|
||||
pct = e.Digi
|
||||
}
|
||||
if pct <= 0 {
|
||||
return nil
|
||||
}
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
applog.Printf("flex: %s %s → TX power %d%%", band, class, pct)
|
||||
return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetRFPower(pct) })
|
||||
}
|
||||
|
||||
// RIT/XIT on the active slice. The offset is kept by the radio when the switch is
|
||||
// off, so turning it back on restores it — the UI must not zero it on toggle.
|
||||
func (a *App) FlexSetRIT(on bool) error {
|
||||
|
||||
Reference in New Issue
Block a user