diff --git a/app.go b/app.go index 11d9616..daf6f6f 100644 --- a/app.go +++ b/app.go @@ -182,10 +182,16 @@ const ( keyAntGeniusHost = "antgenius.host" keyAntGeniusPassword = "antgenius.password" // remote/AUTH password (blank on LAN) - // PowerGenius XL (4O3A) amplifier fan-mode control — Hardware → PowerGenius. - keyPGXLEnabled = "pgxl.enabled" - keyPGXLHost = "pgxl.host" - keyPGXLPort = "pgxl.port" + // Amplifier control — Hardware → Amplifier (PowerGenius XL over TCP; SPE Expert + // over USB serial or an RS232-to-Ethernet bridge). Keys keep the pgxl.* prefix + // for backward compatibility with existing saved settings. + keyPGXLEnabled = "pgxl.enabled" + keyPGXLHost = "pgxl.host" + keyPGXLPort = "pgxl.port" + keyPGXLType = "pgxl.type" // "pgxl" | "spe13" | "spe15" | "spe2k" + keyPGXLTransport = "pgxl.transport" // "tcp" | "serial" + keyPGXLComPort = "pgxl.com_port" + keyPGXLBaud = "pgxl.baud" // WinKeyer CW keyer (serial) — Hardware → CW Keyer. keyWKEnabled = "winkeyer.enabled" @@ -12131,19 +12137,25 @@ func (a *App) AntGeniusDeselect(port int) error { // ── PowerGenius XL (4O3A) amplifier fan control (TCP, default port 9008) ───── -// PGXLSettings is the JSON shape for the Hardware → PowerGenius panel. +// PGXLSettings is the JSON shape for the Hardware → Amplifier panel. It covers +// the 4O3A PowerGenius XL (TCP) and the SPE Expert amps (USB serial or an +// RS232-to-Ethernet bridge). The type name is kept for binding compatibility. type PGXLSettings struct { - Enabled bool `json:"enabled"` - Host string `json:"host"` - Port int `json:"port"` + Enabled bool `json:"enabled"` + Type string `json:"type"` // "pgxl" | "spe13" | "spe15" | "spe2k" + Transport string `json:"transport"` // "tcp" | "serial" + Host string `json:"host"` // TCP transport + Port int `json:"port"` // TCP transport + ComPort string `json:"com_port"` // serial transport + Baud int `json:"baud"` // serial transport } func (a *App) GetPGXLSettings() (PGXLSettings, error) { - out := PGXLSettings{Port: 9008} + out := PGXLSettings{Type: "pgxl", Transport: "tcp", Port: 9008, Baud: 115200} if a.settings == nil { return out, fmt.Errorf("db not initialized") } - m, err := a.settings.GetMany(a.ctx, keyPGXLEnabled, keyPGXLHost, keyPGXLPort) + m, err := a.settings.GetMany(a.ctx, keyPGXLEnabled, keyPGXLHost, keyPGXLPort, keyPGXLType, keyPGXLTransport, keyPGXLComPort, keyPGXLBaud) if err != nil { return out, err } @@ -12152,6 +12164,16 @@ func (a *App) GetPGXLSettings() (PGXLSettings, error) { if p, _ := strconv.Atoi(m[keyPGXLPort]); p > 0 && p <= 65535 { out.Port = p } + if t := strings.TrimSpace(m[keyPGXLType]); t != "" { + out.Type = t + } + if tr := strings.TrimSpace(m[keyPGXLTransport]); tr != "" { + out.Transport = tr + } + out.ComPort = m[keyPGXLComPort] + if b, _ := strconv.Atoi(m[keyPGXLBaud]); b > 0 { + out.Baud = b + } return out, nil } @@ -12162,10 +12184,23 @@ func (a *App) SavePGXLSettings(s PGXLSettings) error { if s.Port <= 0 || s.Port > 65535 { s.Port = 9008 } + if s.Baud <= 0 { + s.Baud = 115200 + } + if s.Type == "" { + s.Type = "pgxl" + } + if s.Transport == "" { + s.Transport = "tcp" + } for k, v := range map[string]string{ - keyPGXLEnabled: boolStr(s.Enabled), - keyPGXLHost: strings.TrimSpace(s.Host), - keyPGXLPort: strconv.Itoa(s.Port), + keyPGXLEnabled: boolStr(s.Enabled), + keyPGXLHost: strings.TrimSpace(s.Host), + keyPGXLPort: strconv.Itoa(s.Port), + keyPGXLType: s.Type, + keyPGXLTransport: s.Transport, + keyPGXLComPort: strings.TrimSpace(s.ComPort), + keyPGXLBaud: strconv.Itoa(s.Baud), } { if err := a.settings.Set(a.ctx, k, v); err != nil { return err @@ -12185,7 +12220,17 @@ func (a *App) startPGXL() { a.pgxl = nil } s, err := a.GetPGXLSettings() - if err != nil || !s.Enabled || strings.TrimSpace(s.Host) == "" { + if err != nil || !s.Enabled { + return + } + // Only the PowerGenius XL (TCP) is driven for now. SPE Expert control (serial / + // RS232-to-Ethernet) is a separate protocol, not yet implemented — its settings + // are stored but no client is started. + if s.Type != "" && s.Type != "pgxl" { + applog.Printf("amplifier: type %q selected — control not implemented yet (settings saved)", s.Type) + return + } + if strings.TrimSpace(s.Host) == "" { return } a.pgxl = powergenius.New(s.Host, s.Port) diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index ac3b3cb..d64caab 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -276,7 +276,7 @@ const SECTION_LABELS: Partial> = { winkeyer: 'CW Keyer', antenna: 'Ultrabeam / Steppir', antgenius: 'Antenna Genius', - pgxl: 'Power Genius', + pgxl: 'Amplifier', flex: 'FlexRadio', relayauto: 'Relay auto-control', audio: 'Audio devices', @@ -1013,8 +1013,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan // Antenna Genius (4O3A) switch settings — TCP port is fixed at 9007. const [antgenius, setAntgenius] = useState<{ enabled: boolean; host: string; password: string }>({ enabled: false, host: '', password: '' }); - // PowerGenius XL (4O3A) amp fan-control settings. - const [pgxl, setPgxl] = useState<{ enabled: boolean; host: string; port: number }>({ enabled: false, host: '', port: 9008 }); + // Amplifier control settings (PowerGenius XL over TCP; SPE Expert over serial/IP). + const [pgxl, setPgxl] = useState<{ enabled: boolean; type: string; transport: string; host: string; port: number; com_port: string; baud: number }>( + { enabled: false, type: 'pgxl', transport: 'tcp', host: '', port: 9008, com_port: '', baud: 115200 }); // WinKeyer CW keyer settings + macro editor. type WKMac = { label: string; text: string }; @@ -2645,36 +2646,89 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan } function PGXLPanelSettings() { + const isPGXL = pgxl.type === 'pgxl'; + const isSerial = pgxl.transport === 'serial'; return ( <> - +
-
-
- - setPgxl((s) => ({ ...s, host: e.target.value }))} - placeholder="192.168.1.70" - className="font-mono" - /> -
+ +
- - setPgxl((s) => ({ ...s, port: parseInt(e.target.value) || 9008 }))} - className="font-mono" - /> + +
+ {/* PowerGenius is TCP-only; SPE amps connect over USB serial or an + RS232-to-Ethernet bridge, so they offer both. */} + {!isPGXL && ( +
+ + +
+ )}
+ + {isSerial ? ( +
+
+ +
+ + +
+
+
+ + setPgxl((s) => ({ ...s, baud: parseInt(e.target.value) || 115200 }))} className="font-mono" /> +
+
+ ) : ( +
+
+ + setPgxl((s) => ({ ...s, host: e.target.value }))} + placeholder="192.168.1.70" className="font-mono" /> +
+
+ + setPgxl((s) => ({ ...s, port: parseInt(e.target.value) || 9008 }))} className="font-mono" /> +
+
+ )} + + {!isPGXL && ( +

+ SPE Expert control is not wired up yet — these settings are saved, but OpsLog can't command the amp until its serial protocol is implemented. +

+ )}
); diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 2438574..675f8e8 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -133,7 +133,7 @@ const en: Dict = { 'uscty.backfillDone': '{s} US QSOs scanned · {c} counties, {g} grids filled.', 'station.title': 'Station Control', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'No heading read', 'station.pattern': 'Pattern', 'station.bi': 'Bi', 'station.retract': 'Retract elements', 'station.moving': 'MOVING', 'station.elements': 'Elements (mm)', 'station.read': 'Read', 'station.readLengths': 'Read current element lengths from the controller', 'station.noLengths': 'Lengths unknown — click Read to fetch them from the controller.', 'station.element': 'Element', 'station.reflector': 'Reflector', 'station.driven': 'Driven', 'station.director': 'Dir', 'station.set': 'Set', 'station.elementsHint': 'Each press lengthens/shortens the element by 2 mm (like the physical console). Verify which element responds on your antenna.', 'station.setExactLen': 'Click to type the exact current length (fixes the baseline if the auto-read is off).', 'station.atMax': 'Controller refused — the element is likely at its maximum length for this band, so it can\'t extend further.', 'station.go': 'Go', 'station.stop': 'Stop', 'station.dragHint': 'Drag a widget by its card to reorder. Pick a column count to lay them out in a grid.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Add device', 'station.editDevice': 'Edit device', 'station.empty': 'No relay boards yet. Add a WebSwitch 1216H or a KMTronic 8-relay board to control your station power and accessories.', 'station.online': 'Online', 'station.offline': 'Offline', 'station.edit': 'Edit', 'station.delete': 'Delete', 'station.relay': 'Relay', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': 'Device type', 'station.name': 'Name', 'station.host': 'Host / IP', 'station.user': 'Username', 'station.pass': 'Password', 'station.optional': 'optional', 'station.ftdiSerial': 'FTDI serial number', 'station.detect': 'Detect', 'station.ftdiHint': 'The Denkovi board is driven via FTDI bit-bang (not the COM port). Pick its serial (e.g. DAE0006K). Needs the FTDI D2XX driver installed.', 'station.channels': 'Relays', 'station.comPort': 'COM port', 'station.noPorts': 'No ports found', 'station.usbRelayHint': 'Cheap USB-serial relay boards (CH340/LCUS) using the A0 command protocol. If yours does not switch, tell me its model / command set.', 'station.labels': 'Relay labels', 'station.cancel': 'Cancel', 'station.save': 'Save', 'sec.awards': 'Awards', 'sec.cat': 'CAT interface', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'CW Keyer', - 'sec.antenna': 'Ultrabeam / Steppir', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Power Genius', 'sec.flex': 'FlexRadio', 'sec.audio': 'Audio devices', + 'sec.antenna': 'Ultrabeam / Steppir', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Amplifier', 'sec.flex': 'FlexRadio', 'sec.audio': 'Audio devices', 'sec.relayauto': 'Relay auto-control', 'relayauto.hint': 'Automatically switch Station Control relays from the rig frequency / band (like PstRotator). Each relay: a frequency window (ON inside, OFF outside) or a set of bands. Relays are set up in the Station Control panel.', 'relayauto.enable': 'Enable relay auto-control', @@ -446,7 +446,7 @@ const fr: Dict = { 'uscty.backfillDone': '{s} QSO US analysés · {c} comtés, {g} locators remplis.', 'station.title': 'Contrôle station', 'station.rotator': 'Rotator', 'station.rotatorNoRead': 'Azimut non lu', 'station.pattern': 'Diagramme', 'station.bi': 'Bi', 'station.retract': 'Rétracter les éléments', 'station.moving': 'EN MOUVEMENT', 'station.elements': 'Éléments (mm)', 'station.read': 'Lire', 'station.readLengths': 'Lire les longueurs actuelles depuis le contrôleur', 'station.noLengths': 'Longueurs inconnues — clique sur Lire pour les récupérer depuis le contrôleur.', 'station.element': 'Élément', 'station.reflector': 'Réflecteur', 'station.driven': 'Radiateur', 'station.director': 'Dir', 'station.set': 'Régler', 'station.elementsHint': "Chaque appui allonge/raccourcit l'élément de 2 mm (comme le pupitre). Vérifie quel élément répond sur ton antenne.", 'station.setExactLen': "Clique pour taper la longueur actuelle exacte (recale la base si la lecture auto est fausse).", 'station.atMax': "Refusé par le contrôleur — l'élément est probablement en butée (longueur max pour cette bande), il ne peut plus s'allonger.", 'station.go': 'Aller', 'station.stop': 'Stop', 'station.dragHint': 'Glisse une carte pour réordonner. Choisis un nombre de colonnes pour la disposition.', 'station.colsAuto': 'Auto', 'station.addDevice': 'Ajouter un appareil', 'station.editDevice': "Modifier l'appareil", 'station.empty': "Aucune carte relais. Ajoute un WebSwitch 1216H ou une carte KMTronic 8 relais pour piloter l'alimentation et les accessoires de ta station.", 'station.online': 'En ligne', 'station.offline': 'Hors ligne', 'station.edit': 'Modifier', 'station.delete': 'Supprimer', 'station.relay': 'Relais', 'station.on': 'ON', 'station.off': 'OFF', 'station.type': "Type d'appareil", 'station.name': 'Nom', 'station.host': 'Hôte / IP', 'station.user': "Nom d'utilisateur", 'station.pass': 'Mot de passe', 'station.optional': 'optionnel', 'station.ftdiSerial': 'Numéro de série FTDI', 'station.detect': 'Détecter', 'station.ftdiHint': "La carte Denkovi se pilote en FTDI bit-bang (pas via le port COM). Choisis son numéro de série (ex. DAE0006K). Nécessite le driver FTDI D2XX installé.", 'station.channels': 'Relais', 'station.comPort': 'Port COM', 'station.noPorts': 'Aucun port', 'station.usbRelayHint': "Cartes USB-série bon marché (CH340/LCUS) protocole A0. Si la tienne ne commute pas, donne-moi le modèle / jeu de commandes.", 'station.labels': 'Libellés des relais', 'station.cancel': 'Annuler', 'station.save': 'Enregistrer', 'sec.awards': 'Diplômes', 'sec.cat': 'Interface CAT', 'sec.rotator': 'Rotator', 'sec.winkeyer': 'Manipulateur CW', - 'sec.antenna': 'Antenne motorisée', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Power Genius', 'sec.flex': 'FlexRadio', 'sec.audio': 'Périphériques audio', + 'sec.antenna': 'Antenne motorisée', 'sec.antgenius': 'Antenna Genius', 'sec.pgxl': 'Amplificateur', 'sec.flex': 'FlexRadio', 'sec.audio': 'Périphériques audio', 'sec.relayauto': 'Relais automatiques', 'relayauto.hint': "Commute automatiquement les relais du Station Control selon la fréquence / bande du poste (comme PstRotator). Par relais : une plage de fréquence (ON dedans, OFF dehors) ou un ensemble de bandes. Les relais se configurent dans le panneau Station Control.", 'relayauto.enable': 'Activer les relais automatiques', diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 0b1075d..9a617eb 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -2161,8 +2161,12 @@ export namespace main { } export class PGXLSettings { enabled: boolean; + type: string; + transport: string; host: string; port: number; + com_port: string; + baud: number; static createFrom(source: any = {}) { return new PGXLSettings(source); @@ -2171,8 +2175,12 @@ export namespace main { constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); this.enabled = source["enabled"]; + this.type = source["type"]; + this.transport = source["transport"]; this.host = source["host"]; this.port = source["port"]; + this.com_port = source["com_port"]; + this.baud = source["baud"]; } } export class POTAUnmatched {