From 7d7d175ede530662a2e5ca37d930193987a0980d Mon Sep 17 00:00:00 2001 From: rouggy Date: Tue, 21 Jul 2026 01:25:55 +0200 Subject: [PATCH] feat: SPE amp ON/OFF buttons and Low/Mid/High power-level control (keystroke codes best-guess from APG, pending hw verification) --- app.go | 19 ++++++++++++++ frontend/src/components/FlexPanel.tsx | 25 +++++++++++++++++- frontend/wailsjs/go/main/App.d.ts | 4 +++ frontend/wailsjs/go/main/App.js | 8 ++++++ internal/spe/spe.go | 38 ++++++++++++++++++++++++++- 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index c96cb48..0548bec 100644 --- a/app.go +++ b/app.go @@ -12261,6 +12261,25 @@ func (a *App) SPESetOperate(on bool) error { return a.spe.Operate(on) } +// SPESetPower turns the SPE amp on (true, POWER key) or off (false, OFF key). +func (a *App) SPESetPower(on bool) error { + if a.spe == nil { + return fmt.Errorf("SPE amplifier not connected — enable it in Settings → Amplifier") + } + if on { + return a.spe.PowerOn() + } + return a.spe.PowerOff() +} + +// SPESetPowerLevel selects the SPE output power level ("L"/"M"/"H"). +func (a *App) SPESetPowerLevel(level string) error { + if a.spe == nil { + return fmt.Errorf("SPE amplifier not connected — enable it in Settings → Amplifier") + } + return a.spe.SetPowerLevel(level) +} + // GetPGXLStatus returns the amp's fan/connection state for the UI poll. func (a *App) GetPGXLStatus() powergenius.Status { if a.pgxl == nil { diff --git a/frontend/src/components/FlexPanel.tsx b/frontend/src/components/FlexPanel.tsx index b877341..0bd2fb5 100644 --- a/frontend/src/components/FlexPanel.tsx +++ b/frontend/src/components/FlexPanel.tsx @@ -4,7 +4,7 @@ import { GetFlexState, FlexSetPower, FlexSetTunePower, FlexTune, FlexSetVox, FlexSetVoxLevel, FlexSetVoxDelay, FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic, FlexMox, FlexAmpOperate, - GetPGXLStatus, PGXLSetFanMode, GetPGXLSettings, GetSPEStatus, SPESetOperate, + GetPGXLStatus, PGXLSetFanMode, GetPGXLSettings, GetSPEStatus, SPESetOperate, SPESetPower, SPESetPowerLevel, FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice, FlexSetRIT, FlexSetRITFreq, FlexSetXIT, FlexSetXITFreq, FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel, @@ -814,6 +814,29 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number spe.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}> {spe.operate ? 'OPERATE' : 'STANDBY'} + {/* Power ON / OFF (best-guess keystrokes from the APG — verify on hw). */} +
+ + +
+ {/* Output power level: Low / Mid / High. */} +
+ {(['L', 'M', 'H'] as const).map((lvl, i) => { + const active = (spe.power_level || '').trim().toUpperCase() === lvl; + return ( + + ); + })} +
{spe.connected ? (spe.tx ? 'TX' : 'RX') : t('flxp.pgOffline')} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 5225b79..99854e4 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -721,6 +721,10 @@ export function RunBackupNow():Promise; export function SPESetOperate(arg1:boolean):Promise; +export function SPESetPower(arg1:boolean):Promise; + +export function SPESetPowerLevel(arg1:string):Promise; + export function SaveADIFFile():Promise; export function SaveADIFMonitor(arg1:main.ADIFMonitorConfig):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index ed793b9..3d080c4 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -1398,6 +1398,14 @@ export function SPESetOperate(arg1) { return window['go']['main']['App']['SPESetOperate'](arg1); } +export function SPESetPower(arg1) { + return window['go']['main']['App']['SPESetPower'](arg1); +} + +export function SPESetPowerLevel(arg1) { + return window['go']['main']['App']['SPESetPowerLevel'](arg1); +} + export function SaveADIFFile() { return window['go']['main']['App']['SaveADIFFile'](); } diff --git a/internal/spe/spe.go b/internal/spe/spe.go index 52133c9..9212816 100644 --- a/internal/spe/spe.go +++ b/internal/spe/spe.go @@ -30,9 +30,17 @@ import ( ) const ( - cmdOperate byte = 0x0D // toggles STANDBY ↔ OPERATE + cmdOperate byte = 0x0D // toggles STANDBY ↔ OPERATE (confirmed on hw) cmdStatus byte = 0x90 // request the status string + // Best-guess keystroke codes reconstructed from the APG command table after + // correcting the PDF's note-wrap misalignment (anchored to the confirmed + // OPERATE=0x0D): OFF=0x0A, POWER=0x0B. The POWER key doubles as power-on (when + // the amp is off) and the output-level cycle L→M→H (when it's on) — same as the + // single physical POWER button. To be verified on hardware. + cmdOff byte = 0x0A // OFF key — switch the amplifier off + cmdPower byte = 0x0B // POWER key — turn on / cycle output power level + syncHost = 0x55 syncAmp = 0xAA @@ -138,6 +146,34 @@ func (c *Client) Operate(on bool) error { // ToggleOperate flips STANDBY/OPERATE unconditionally. func (c *Client) ToggleOperate() error { return c.sendCmd(cmdOperate) } +// PowerOn presses the POWER key (turns the amp on when it's off). +func (c *Client) PowerOn() error { return c.sendCmd(cmdPower) } + +// PowerOff presses the OFF key (switches the amp off). +func (c *Client) PowerOff() error { return c.sendCmd(cmdOff) } + +// SetPowerLevel cycles the output power level (L/M/H) to the requested one by +// tapping the POWER key until the reported level matches, giving the amp time to +// report each change. `level` is "L", "M" or "H" (case-insensitive). +func (c *Client) SetPowerLevel(level string) error { + want := strings.ToUpper(strings.TrimSpace(level)) + if want == "" { + return nil + } + // At most 3 taps to walk the 3-way cycle back around to the target. + for i := 0; i < 3; i++ { + cur := strings.ToUpper(strings.TrimSpace(c.GetStatus().PowerLevel)) + if cur == want { + return nil + } + if err := c.sendCmd(cmdPower); err != nil { + return err + } + time.Sleep(1 * time.Second) // let the amp apply it and the poll refresh status + } + return nil +} + func (c *Client) pollLoop() { t := time.NewTicker(pollEvery) defer t.Stop()