package main import ( "fmt" "strconv" "strings" "hamlog/internal/applog" "hamlog/internal/psu" ) // ── Bench power supply (Modbus RTU) ────────────────────────────────────────── // // A programmable supply feeding the shack, switched on and off from OpsLog so // the station comes up and goes down with the logbook rather than by reaching // behind the desk. // // OpsLog READS the supply's measurements and its set points, and WRITES exactly // one thing: the output on/off. The register map has the voltage and current // set points and the three protection trip levels as writable too, and none of // them belong to a logbook — a wrong value there is 30 V where a radio expected // 13.8. See internal/psu for the map and where it comes from. const ( keyPSUEnabled = "psu.enabled" keyPSUPort = "psu.com_port" keyPSUBaud = "psu.baud" keyPSUAddress = "psu.address" // Modbus slave address, 1…15 on this family ) // PSUSettings is the JSON shape for the Hardware → Power supply panel. type PSUSettings struct { Enabled bool `json:"enabled"` ComPort string `json:"com_port"` Baud int `json:"baud"` // 9600 from the factory Address int `json:"address"` // 1 from the factory } // GetPSUSettings returns the persisted supply config. func (a *App) GetPSUSettings() (PSUSettings, error) { out := PSUSettings{Baud: 9600, Address: 1} if a.settings == nil { return out, fmt.Errorf("db not initialized") } m, err := a.settings.GetMany(a.ctx, keyPSUEnabled, keyPSUPort, keyPSUBaud, keyPSUAddress) if err != nil { return out, err } out.Enabled = m[keyPSUEnabled] == "1" out.ComPort = m[keyPSUPort] if v, e := strconv.Atoi(m[keyPSUBaud]); e == nil && v > 0 { out.Baud = v } if v, e := strconv.Atoi(m[keyPSUAddress]); e == nil && v >= 1 && v <= 250 { out.Address = v } return out, nil } // SavePSUSettings persists the config and (re)starts or stops the client. func (a *App) SavePSUSettings(s PSUSettings) error { if a.settings == nil { return fmt.Errorf("db not initialized") } if s.Baud <= 0 { s.Baud = 9600 } // The manual gives 1…15 for the address field and 1…250 for the address // SETTING register. Clamped to the wider range and defaulted to 1: an // address of 0 is the Modbus broadcast, which never answers, so accepting it // would give a supply that is present and permanently "not responding". if s.Address < 1 || s.Address > 250 { s.Address = 1 } for k, v := range map[string]string{ keyPSUEnabled: boolStr(s.Enabled), keyPSUPort: strings.TrimSpace(s.ComPort), keyPSUBaud: strconv.Itoa(s.Baud), keyPSUAddress: strconv.Itoa(s.Address), } { if err := a.settings.Set(a.ctx, k, v); err != nil { return err } } a.restartAsync("psu", a.startPSU) return nil } // startPSU stops any running client and starts a fresh one if the supply is // enabled and has a port. Safe to call repeatedly (startup, settings save, // profile switch). func (a *App) startPSU() { if a.psu != nil { go a.psu.Stop() a.psu = nil } s, err := a.GetPSUSettings() if err != nil { applog.Printf("psu: not started — settings unavailable: %v", err) return } if !s.Enabled { return } if strings.TrimSpace(s.ComPort) == "" { applog.Printf("psu: not started — no serial port configured") return } applog.Printf("psu: starting on %s @ %d baud, Modbus address %d", s.ComPort, s.Baud, s.Address) a.psu = psu.New(psu.Config{ComPort: s.ComPort, Baud: s.Baud, Address: byte(s.Address)}) _ = a.psu.Start() } // GetPSUStatus returns the supply's last polled state for the UI. func (a *App) GetPSUStatus() psu.Status { if a.psu == nil { return psu.Status{} } return a.psu.GetStatus() } // SetPSUOutput switches the supply's output. The only write OpsLog makes to it. func (a *App) SetPSUOutput(on bool) error { if a.psu == nil { return fmt.Errorf("the power supply is not enabled in Settings") } return a.psu.SetOutput(on) }