feat: SPE Expert amplifier control (serial/TCP) — OPERATE toggle + live status
Implements the SPE Application Programmer's Guide protocol: packets 0x55 0x55 0x55|CNT|DATA|CHK (sum%256), OPERATE key 0x0D (toggles STANDBY/OPERATE) and STATUS request 0x90 whose 0xAA-framed reply is a 19-field CSV (mode, RX/TX, band, power level, output W, SWR, V/I, temp, warnings, alarms). internal/spe polls status ~1/s over USB serial (go.bug.st/serial, 8N1) or TCP (RS232-to- Ethernet bridge) — same codec, different transport. Wired via startPGXL (starts the SPE client for spe* types), bindings GetSPEStatus / SPESetOperate, and a live status card + OPERATE/STANDBY toggle in the Amplifier settings panel. Only the two example-anchored commands are sent (safe); other keystroke codes were ambiguous in the guide's table. Untested on hardware.
This commit is contained in:
@@ -45,6 +45,7 @@ import (
|
||||
"hamlog/internal/operating"
|
||||
"hamlog/internal/pota"
|
||||
"hamlog/internal/powergenius"
|
||||
"hamlog/internal/spe"
|
||||
"hamlog/internal/profile"
|
||||
"hamlog/internal/qslcard"
|
||||
"hamlog/internal/qso"
|
||||
@@ -465,6 +466,7 @@ type App struct {
|
||||
motorInhibited atomic.Bool // TX currently inhibited by the motor-antenna watcher
|
||||
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
|
||||
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
|
||||
spe *spe.Client // SPE Expert amplifier (serial/TCP); nil when disabled or not SPE
|
||||
audioMgr *audio.Manager
|
||||
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
|
||||
solar *solar.Manager // live space-weather (SFI/SSN/A/K) for the header + QSO stamping
|
||||
@@ -12219,22 +12221,48 @@ func (a *App) startPGXL() {
|
||||
go a.pgxl.Stop()
|
||||
a.pgxl = nil
|
||||
}
|
||||
if a.spe != nil {
|
||||
go a.spe.Stop()
|
||||
a.spe = nil
|
||||
}
|
||||
s, err := a.GetPGXLSettings()
|
||||
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)
|
||||
if s.Type == "" || s.Type == "pgxl" {
|
||||
if strings.TrimSpace(s.Host) == "" {
|
||||
return
|
||||
}
|
||||
a.pgxl = powergenius.New(s.Host, s.Port)
|
||||
_ = a.pgxl.Start()
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(s.Host) == "" {
|
||||
// SPE Expert — USB serial or an RS232-to-Ethernet bridge.
|
||||
if s.Transport == "serial" && strings.TrimSpace(s.ComPort) == "" {
|
||||
return
|
||||
}
|
||||
a.pgxl = powergenius.New(s.Host, s.Port)
|
||||
_ = a.pgxl.Start()
|
||||
if s.Transport == "tcp" && strings.TrimSpace(s.Host) == "" {
|
||||
return
|
||||
}
|
||||
a.spe = spe.New(spe.Config{Transport: s.Transport, ComPort: s.ComPort, Baud: s.Baud, Host: s.Host, Port: s.Port})
|
||||
_ = a.spe.Start()
|
||||
}
|
||||
|
||||
// GetSPEStatus returns the SPE Expert amplifier state for the UI poll.
|
||||
func (a *App) GetSPEStatus() spe.Status {
|
||||
if a.spe == nil {
|
||||
return spe.Status{}
|
||||
}
|
||||
return a.spe.GetStatus()
|
||||
}
|
||||
|
||||
// SPESetOperate puts the SPE amp in OPERATE (true) or STANDBY (false). The amp has
|
||||
// a single OPERATE toggle key, so this sends it only when the state must change.
|
||||
func (a *App) SPESetOperate(on bool) error {
|
||||
if a.spe == nil {
|
||||
return fmt.Errorf("SPE amplifier not connected — enable it in Settings → Amplifier")
|
||||
}
|
||||
return a.spe.Operate(on)
|
||||
}
|
||||
|
||||
// GetPGXLStatus returns the amp's fan/connection state for the UI poll.
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
GetRotatorSettings, SaveRotatorSettings, TestRotator, RotatorPark, RotatorStop,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetPGXLSettings, SavePGXLSettings,
|
||||
GetPGXLSettings, SavePGXLSettings, GetSPEStatus, SPESetOperate,
|
||||
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
|
||||
GetAudioSettings, SaveAudioSettings, ListAudioInputDevices, ListAudioOutputDevices, PickAudioFolder, TestPTT,
|
||||
GetClublogCtyInfo, SetClublogCtyEnabled, DownloadClublogCty,
|
||||
@@ -648,6 +648,49 @@ type RelayRuleUI = { device_id: string; relay: number; mode: string; freq_lo_khz
|
||||
type StationDevUI = { id: string; type: string; name: string; labels: string[] };
|
||||
const RELAY_BANDS = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '4m', '2m', '70cm'];
|
||||
const relayCountUI = (type: string) => (type === 'kmtronic' || type === 'denkovi' ? 8 : 5);
|
||||
|
||||
// Live SPE Expert amplifier status + OPERATE/STANDBY toggle. Module-scoped (not a
|
||||
// nested component) so it isn't remounted on every parent render. Polls once a
|
||||
// second while shown.
|
||||
function SPEStatusCard() {
|
||||
const [st, setSt] = useState<any>({ connected: false });
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const tick = () => GetSPEStatus().then((s) => alive && setSt(s || {})).catch(() => {});
|
||||
tick();
|
||||
const id = window.setInterval(tick, 1000);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
const operate = !!st.operate;
|
||||
return (
|
||||
<div className="rounded-md border border-border p-3 space-y-2 text-xs max-w-xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('size-2 rounded-full', st.connected ? 'bg-success animate-pulse' : 'bg-danger')} />
|
||||
<span className="font-semibold">{st.connected ? `SPE ${st.model || 'Expert'}` : 'SPE — not connected'}</span>
|
||||
{!st.connected && st.last_error && <span className="text-danger truncate text-[10px]">{st.last_error}</span>}
|
||||
<span className="flex-1" />
|
||||
<Button size="sm" variant={operate ? 'default' : 'outline'} disabled={!st.connected}
|
||||
onClick={() => SPESetOperate(!operate).catch(() => {})}
|
||||
title="Toggle OPERATE / STANDBY">
|
||||
{operate ? 'OPERATE' : 'STANDBY'}
|
||||
</Button>
|
||||
</div>
|
||||
{st.connected && (
|
||||
<div className="grid grid-cols-4 gap-x-3 gap-y-1 font-mono text-[11px]">
|
||||
<div>{st.tx ? 'TX' : 'RX'}</div>
|
||||
<div>Band {st.band}</div>
|
||||
<div>Pwr {st.power_level}</div>
|
||||
<div>{st.output_w} W</div>
|
||||
<div>SWR {Number(st.swr_ant ?? 0).toFixed(2)}</div>
|
||||
<div>{st.temp_c}°C</div>
|
||||
<div>{st.volt_pa} V</div>
|
||||
<div>{st.curr_pa} A</div>
|
||||
{(st.warnings || st.alarms) && <div className="col-span-4 text-warning">⚠ {st.warnings} {st.alarms}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function RelayAutoPanel() {
|
||||
const { t } = useI18n();
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
@@ -2724,9 +2767,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPGXL && pgxl.enabled && <SPEStatusCard />}
|
||||
{!isPGXL && (
|
||||
<p className="text-xs text-warning">
|
||||
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.
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
SPE control uses the amplifier's proprietary serial protocol (OPERATE toggle + live status). Save to (re)connect. Band / power-level / antenna are still managed on the amp from the transceiver CAT.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Vendored
+5
@@ -11,6 +11,7 @@ import {awardref} from '../models';
|
||||
import {cluster} from '../models';
|
||||
import {extsvc} from '../models';
|
||||
import {powergenius} from '../models';
|
||||
import {spe} from '../models';
|
||||
import {solar} from '../models';
|
||||
import {winkeyer} from '../models';
|
||||
import {alerts} from '../models';
|
||||
@@ -406,6 +407,8 @@ export function GetRotatorHeading():Promise<main.RotatorHeading>;
|
||||
|
||||
export function GetRotatorSettings():Promise<main.RotatorSettings>;
|
||||
|
||||
export function GetSPEStatus():Promise<spe.Status>;
|
||||
|
||||
export function GetSecretStatus():Promise<main.SecretStatus>;
|
||||
|
||||
export function GetSlotStats():Promise<qso.SlotStats>;
|
||||
@@ -716,6 +719,8 @@ export function RotatorStop():Promise<void>;
|
||||
|
||||
export function RunBackupNow():Promise<string>;
|
||||
|
||||
export function SPESetOperate(arg1:boolean):Promise<void>;
|
||||
|
||||
export function SaveADIFFile():Promise<string>;
|
||||
|
||||
export function SaveADIFMonitor(arg1:main.ADIFMonitorConfig):Promise<void>;
|
||||
|
||||
@@ -770,6 +770,10 @@ export function GetRotatorSettings() {
|
||||
return window['go']['main']['App']['GetRotatorSettings']();
|
||||
}
|
||||
|
||||
export function GetSPEStatus() {
|
||||
return window['go']['main']['App']['GetSPEStatus']();
|
||||
}
|
||||
|
||||
export function GetSecretStatus() {
|
||||
return window['go']['main']['App']['GetSecretStatus']();
|
||||
}
|
||||
@@ -1390,6 +1394,10 @@ export function RunBackupNow() {
|
||||
return window['go']['main']['App']['RunBackupNow']();
|
||||
}
|
||||
|
||||
export function SPESetOperate(arg1) {
|
||||
return window['go']['main']['App']['SPESetOperate'](arg1);
|
||||
}
|
||||
|
||||
export function SaveADIFFile() {
|
||||
return window['go']['main']['App']['SaveADIFFile']();
|
||||
}
|
||||
|
||||
@@ -4108,6 +4108,53 @@ export namespace solar {
|
||||
|
||||
}
|
||||
|
||||
export namespace spe {
|
||||
|
||||
export class Status {
|
||||
connected: boolean;
|
||||
last_error?: string;
|
||||
model?: string;
|
||||
operate: boolean;
|
||||
tx: boolean;
|
||||
input?: string;
|
||||
band?: string;
|
||||
power_level?: string;
|
||||
output_w: number;
|
||||
swr_atu: number;
|
||||
swr_ant: number;
|
||||
volt_pa: number;
|
||||
curr_pa: number;
|
||||
temp_c: number;
|
||||
warnings?: string;
|
||||
alarms?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Status(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.connected = source["connected"];
|
||||
this.last_error = source["last_error"];
|
||||
this.model = source["model"];
|
||||
this.operate = source["operate"];
|
||||
this.tx = source["tx"];
|
||||
this.input = source["input"];
|
||||
this.band = source["band"];
|
||||
this.power_level = source["power_level"];
|
||||
this.output_w = source["output_w"];
|
||||
this.swr_atu = source["swr_atu"];
|
||||
this.swr_ant = source["swr_ant"];
|
||||
this.volt_pa = source["volt_pa"];
|
||||
this.curr_pa = source["curr_pa"];
|
||||
this.temp_c = source["temp_c"];
|
||||
this.warnings = source["warnings"];
|
||||
this.alarms = source["alarms"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace udp {
|
||||
|
||||
export class Config {
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
// Package spe drives the SPE Expert 1.3K-FA / 1.5K-FA / 2K-FA amplifiers over
|
||||
// their proprietary serial protocol (SPE "Application Programmer's Guide" rev 1.1).
|
||||
// The amp is reached either directly over USB (a virtual COM port) or over TCP via
|
||||
// an RS232-to-Ethernet bridge — both are just an io.ReadWriteCloser to this code.
|
||||
//
|
||||
// Wire format (host → amp): 0x55 0x55 0x55 | CNT | DATA… | CHK
|
||||
// CNT = number of DATA bytes, CHK = sum(DATA) mod 256.
|
||||
// Status reply (amp → host): 0xAA 0xAA 0xAA | LEN | <LEN CSV bytes> | chk0 chk1 | CR LF
|
||||
// LEN is 0x43 (67); the payload is 19 comma-separated fixed fields.
|
||||
//
|
||||
// This MVP implements the two commands anchored by worked examples in the guide:
|
||||
// OPERATE (0x0D, toggles STANDBY↔OPERATE) and STATUS (0x90). Other keystroke codes
|
||||
// exist but the guide's command table did not extract unambiguously, so they are
|
||||
// left out rather than risk sending the wrong key to the amplifier.
|
||||
package spe
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.bug.st/serial"
|
||||
)
|
||||
|
||||
const (
|
||||
cmdOperate byte = 0x0D // toggles STANDBY ↔ OPERATE
|
||||
cmdStatus byte = 0x90 // request the status string
|
||||
|
||||
syncHost = 0x55
|
||||
syncAmp = 0xAA
|
||||
|
||||
dialTimeout = 5 * time.Second
|
||||
ioTimeout = 3 * time.Second
|
||||
pollEvery = 800 * time.Millisecond
|
||||
)
|
||||
|
||||
// Status is the decoded amplifier state for the UI.
|
||||
type Status struct {
|
||||
Connected bool `json:"connected"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
Model string `json:"model,omitempty"` // "20K" / "13K"
|
||||
Operate bool `json:"operate"` // true = OPERATE, false = STANDBY
|
||||
TX bool `json:"tx"` // true = transmitting
|
||||
Input string `json:"input,omitempty"` // "1" / "2"
|
||||
Band string `json:"band,omitempty"` // raw 2-char band code
|
||||
PowerLevel string `json:"power_level,omitempty"` // L / M / H
|
||||
OutputW int `json:"output_w"`
|
||||
SWRATU float64 `json:"swr_atu"`
|
||||
SWRAnt float64 `json:"swr_ant"`
|
||||
VoltPA float64 `json:"volt_pa"`
|
||||
CurrPA float64 `json:"curr_pa"`
|
||||
TempC int `json:"temp_c"` // heatsink (upper) temperature
|
||||
Warnings string `json:"warnings,omitempty"`
|
||||
Alarms string `json:"alarms,omitempty"`
|
||||
}
|
||||
|
||||
// Config selects the transport.
|
||||
type Config struct {
|
||||
Transport string // "serial" | "tcp"
|
||||
ComPort string // serial
|
||||
Baud int // serial
|
||||
Host string // tcp
|
||||
Port int // tcp
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
cfg Config
|
||||
|
||||
mu sync.Mutex // serialises access to the connection
|
||||
conn io.ReadWriteCloser
|
||||
r *bufio.Reader
|
||||
|
||||
statusMu sync.RWMutex
|
||||
status Status
|
||||
|
||||
stop chan struct{}
|
||||
running bool
|
||||
}
|
||||
|
||||
func New(cfg Config) *Client {
|
||||
if cfg.Baud <= 0 {
|
||||
cfg.Baud = 115200
|
||||
}
|
||||
return &Client{cfg: cfg, stop: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
if c.running {
|
||||
return nil
|
||||
}
|
||||
c.running = true
|
||||
go c.pollLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Stop() {
|
||||
if !c.running {
|
||||
return
|
||||
}
|
||||
c.running = false
|
||||
close(c.stop)
|
||||
c.mu.Lock()
|
||||
c.dropLocked()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) GetStatus() Status {
|
||||
c.statusMu.RLock()
|
||||
defer c.statusMu.RUnlock()
|
||||
return c.status
|
||||
}
|
||||
|
||||
func (c *Client) setErr(err error) {
|
||||
c.statusMu.Lock()
|
||||
c.status.Connected = false
|
||||
c.status.LastError = err.Error()
|
||||
c.statusMu.Unlock()
|
||||
}
|
||||
|
||||
// Operate toggles the amplifier between STANDBY and OPERATE (the amp has a single
|
||||
// OPERATE key that flips the state, so we send it only when the desired state
|
||||
// differs from the last-read one).
|
||||
func (c *Client) Operate(on bool) error {
|
||||
if c.GetStatus().Operate == on {
|
||||
return nil
|
||||
}
|
||||
return c.sendCmd(cmdOperate)
|
||||
}
|
||||
|
||||
// ToggleOperate flips STANDBY/OPERATE unconditionally.
|
||||
func (c *Client) ToggleOperate() error { return c.sendCmd(cmdOperate) }
|
||||
|
||||
func (c *Client) pollLoop() {
|
||||
t := time.NewTicker(pollEvery)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.stop:
|
||||
return
|
||||
case <-t.C:
|
||||
if err := c.ensureConn(); err != nil {
|
||||
c.setErr(fmt.Errorf("connect: %w", err))
|
||||
continue
|
||||
}
|
||||
if err := c.sendCmd(cmdStatus); err != nil {
|
||||
c.mu.Lock()
|
||||
c.dropLocked()
|
||||
c.mu.Unlock()
|
||||
c.setErr(err)
|
||||
continue
|
||||
}
|
||||
c.readStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ensureConn() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn != nil {
|
||||
return nil
|
||||
}
|
||||
var rwc io.ReadWriteCloser
|
||||
var err error
|
||||
if c.cfg.Transport == "tcp" {
|
||||
var nc net.Conn
|
||||
nc, err = net.DialTimeout("tcp", net.JoinHostPort(c.cfg.Host, strconv.Itoa(c.cfg.Port)), dialTimeout)
|
||||
rwc = nc
|
||||
} else {
|
||||
rwc, err = serial.Open(c.cfg.ComPort, &serial.Mode{BaudRate: c.cfg.Baud})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = rwc
|
||||
c.r = bufio.NewReader(rwc)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) dropLocked() {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
c.r = nil
|
||||
}
|
||||
}
|
||||
|
||||
// sendCmd frames one keystroke code and writes it. Single-byte payload → CHK is
|
||||
// the code itself.
|
||||
func (c *Client) sendCmd(code byte) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
if nc, ok := c.conn.(net.Conn); ok {
|
||||
_ = nc.SetWriteDeadline(time.Now().Add(ioTimeout))
|
||||
}
|
||||
pkt := []byte{syncHost, syncHost, syncHost, 0x01, code, code}
|
||||
_, err := c.conn.Write(pkt)
|
||||
return err
|
||||
}
|
||||
|
||||
// readStatus reads one amp packet and, when it's a status string, decodes it. ACK
|
||||
// packets (short) are consumed and ignored.
|
||||
func (c *Client) readStatus() {
|
||||
c.mu.Lock()
|
||||
r := c.r
|
||||
if nc, ok := c.conn.(net.Conn); ok && nc != nil {
|
||||
_ = nc.SetReadDeadline(time.Now().Add(ioTimeout))
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
// Sync on three 0xAA bytes.
|
||||
run := 0
|
||||
for run < 3 {
|
||||
b, err := r.ReadByte()
|
||||
if err != nil {
|
||||
c.mu.Lock()
|
||||
c.dropLocked()
|
||||
c.mu.Unlock()
|
||||
c.setErr(err)
|
||||
return
|
||||
}
|
||||
if b == syncAmp {
|
||||
run++
|
||||
} else {
|
||||
run = 0
|
||||
}
|
||||
}
|
||||
length, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data := make([]byte, int(length))
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return
|
||||
}
|
||||
// Status strings are the long ones (LEN 0x43 = 67). Short packets are ACKs
|
||||
// (1 checksum byte, no CRLF) — nothing else to consume for those.
|
||||
if length >= 40 {
|
||||
// consume the 2 checksum bytes + CR LF
|
||||
_, _ = r.Discard(4)
|
||||
c.decodeCSV(string(data))
|
||||
} else {
|
||||
_, _ = r.Discard(1) // ACK checksum
|
||||
}
|
||||
}
|
||||
|
||||
// decodeCSV parses the 19-field comma-separated status payload.
|
||||
func (c *Client) decodeCSV(payload string) {
|
||||
f := strings.Split(payload, ",")
|
||||
get := func(i int) string {
|
||||
if i < len(f) {
|
||||
return strings.TrimSpace(f[i])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
pf := func(s string) float64 { v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64); return v }
|
||||
pi := func(s string) int { v, _ := strconv.Atoi(strings.TrimSpace(s)); return v }
|
||||
|
||||
c.statusMu.Lock()
|
||||
defer c.statusMu.Unlock()
|
||||
c.status.Connected = true
|
||||
c.status.LastError = ""
|
||||
c.status.Model = get(0)
|
||||
c.status.Operate = get(1) == "O"
|
||||
c.status.TX = get(2) == "T"
|
||||
c.status.Input = get(4)
|
||||
c.status.Band = get(5)
|
||||
c.status.PowerLevel = get(8)
|
||||
c.status.OutputW = pi(get(9))
|
||||
c.status.SWRATU = pf(get(10))
|
||||
c.status.SWRAnt = pf(get(11))
|
||||
c.status.VoltPA = pf(get(12))
|
||||
c.status.CurrPA = pf(get(13))
|
||||
c.status.TempC = pi(get(14))
|
||||
if w := get(17); w != "" && w != "N" {
|
||||
c.status.Warnings = w
|
||||
} else {
|
||||
c.status.Warnings = ""
|
||||
}
|
||||
if a := get(18); a != "" && a != "N" {
|
||||
c.status.Alarms = a
|
||||
} else {
|
||||
c.status.Alarms = ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user