feat: SPE amp ON/OFF buttons and Low/Mid/High power-level control (keystroke codes best-guess from APG, pending hw verification)
This commit is contained in:
@@ -12261,6 +12261,25 @@ func (a *App) SPESetOperate(on bool) error {
|
|||||||
return a.spe.Operate(on)
|
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.
|
// GetPGXLStatus returns the amp's fan/connection state for the UI poll.
|
||||||
func (a *App) GetPGXLStatus() powergenius.Status {
|
func (a *App) GetPGXLStatus() powergenius.Status {
|
||||||
if a.pgxl == nil {
|
if a.pgxl == nil {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
GetFlexState, FlexSetPower, FlexSetTunePower, FlexTune, FlexSetVox, FlexSetVoxLevel, FlexSetVoxDelay,
|
GetFlexState, FlexSetPower, FlexSetTunePower, FlexTune, FlexSetVox, FlexSetVoxLevel, FlexSetVoxDelay,
|
||||||
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
||||||
FlexMox, FlexAmpOperate,
|
FlexMox, FlexAmpOperate,
|
||||||
GetPGXLStatus, PGXLSetFanMode, GetPGXLSettings, GetSPEStatus, SPESetOperate,
|
GetPGXLStatus, PGXLSetFanMode, GetPGXLSettings, GetSPEStatus, SPESetOperate, SPESetPower, SPESetPowerLevel,
|
||||||
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice,
|
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice,
|
||||||
FlexSetRIT, FlexSetRITFreq, FlexSetXIT, FlexSetXITFreq,
|
FlexSetRIT, FlexSetRITFreq, FlexSetXIT, FlexSetXITFreq,
|
||||||
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
|
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 ? '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'}
|
{spe.operate ? 'OPERATE' : 'STANDBY'}
|
||||||
</button>
|
</button>
|
||||||
|
{/* Power ON / OFF (best-guess keystrokes from the APG — verify on hw). */}
|
||||||
|
<div className="inline-flex rounded-lg overflow-hidden border-2 border-success/70">
|
||||||
|
<button type="button" disabled={!spe.connected}
|
||||||
|
onClick={() => SPESetPower(true).catch(() => {})}
|
||||||
|
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON</button>
|
||||||
|
<button type="button" disabled={!spe.connected}
|
||||||
|
onClick={() => SPESetPower(false).catch(() => {})}
|
||||||
|
className="px-3 py-2 text-sm font-bold bg-card text-danger border-l-2 border-success/70 hover:bg-danger/15 disabled:opacity-30">OFF</button>
|
||||||
|
</div>
|
||||||
|
{/* Output power level: Low / Mid / High. */}
|
||||||
|
<div className="inline-flex rounded-lg overflow-hidden border border-border">
|
||||||
|
{(['L', 'M', 'H'] as const).map((lvl, i) => {
|
||||||
|
const active = (spe.power_level || '').trim().toUpperCase() === lvl;
|
||||||
|
return (
|
||||||
|
<button key={lvl} type="button" disabled={!spe.connected}
|
||||||
|
onClick={() => SPESetPowerLevel(lvl).catch(() => {})}
|
||||||
|
className={cn('px-3 py-2 text-sm font-bold disabled:opacity-30', i > 0 && 'border-l border-border',
|
||||||
|
active ? 'bg-primary text-primary-foreground' : 'bg-card text-muted-foreground hover:bg-muted')}>
|
||||||
|
{powerLevelLabel(lvl)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<span className={cn('inline-flex items-center gap-1.5 text-sm', spe.connected ? 'text-muted-foreground' : 'text-danger')}>
|
<span className={cn('inline-flex items-center gap-1.5 text-sm', spe.connected ? 'text-muted-foreground' : 'text-danger')}>
|
||||||
<span className={cn('size-2 rounded-full', spe.connected ? 'bg-success' : 'bg-danger')} />
|
<span className={cn('size-2 rounded-full', spe.connected ? 'bg-success' : 'bg-danger')} />
|
||||||
{spe.connected ? (spe.tx ? 'TX' : 'RX') : t('flxp.pgOffline')}
|
{spe.connected ? (spe.tx ? 'TX' : 'RX') : t('flxp.pgOffline')}
|
||||||
|
|||||||
Vendored
+4
@@ -721,6 +721,10 @@ export function RunBackupNow():Promise<string>;
|
|||||||
|
|
||||||
export function SPESetOperate(arg1:boolean):Promise<void>;
|
export function SPESetOperate(arg1:boolean):Promise<void>;
|
||||||
|
|
||||||
|
export function SPESetPower(arg1:boolean):Promise<void>;
|
||||||
|
|
||||||
|
export function SPESetPowerLevel(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function SaveADIFFile():Promise<string>;
|
export function SaveADIFFile():Promise<string>;
|
||||||
|
|
||||||
export function SaveADIFMonitor(arg1:main.ADIFMonitorConfig):Promise<void>;
|
export function SaveADIFMonitor(arg1:main.ADIFMonitorConfig):Promise<void>;
|
||||||
|
|||||||
@@ -1398,6 +1398,14 @@ export function SPESetOperate(arg1) {
|
|||||||
return window['go']['main']['App']['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() {
|
export function SaveADIFFile() {
|
||||||
return window['go']['main']['App']['SaveADIFFile']();
|
return window['go']['main']['App']['SaveADIFFile']();
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-1
@@ -30,9 +30,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cmdOperate byte = 0x0D // toggles STANDBY ↔ OPERATE
|
cmdOperate byte = 0x0D // toggles STANDBY ↔ OPERATE (confirmed on hw)
|
||||||
cmdStatus byte = 0x90 // request the status string
|
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
|
syncHost = 0x55
|
||||||
syncAmp = 0xAA
|
syncAmp = 0xAA
|
||||||
|
|
||||||
@@ -138,6 +146,34 @@ func (c *Client) Operate(on bool) error {
|
|||||||
// ToggleOperate flips STANDBY/OPERATE unconditionally.
|
// ToggleOperate flips STANDBY/OPERATE unconditionally.
|
||||||
func (c *Client) ToggleOperate() error { return c.sendCmd(cmdOperate) }
|
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() {
|
func (c *Client) pollLoop() {
|
||||||
t := time.NewTicker(pollEvery)
|
t := time.NewTicker(pollEvery)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user