feat(psu): switch a Modbus RTU bench supply from OpsLog

The manufacturer's document arrived, so this is no longer guesswork: 9600 8N1,
function codes 03 and 06 only, and a register map with the output on/off at
0x0001, the measurements at 0x0010…0x0013 and the set points at 0x0030/0x0031.

ONE REGISTER IS WRITTEN — 0x0001, the output. The map also exposes the voltage
and current set points and the three protection trip levels as writable, and
none of them belong to a logbook: a wrong value there is 30 V where a radio
expected 13.8, or a trip level lifted on a supply feeding an amplifier. They are
read and displayed instead, next to the measured values, which is also how an
operator sees at a glance that the supply is on and the radio is drawing nothing.

The wire layer is tested where it can be. CRC-16/MODBUS is pinned against its
published check value — the CRC of "123456789" is 0x4B37 — which fixes the
polynomial, the initial value, the reflection and the absence of a final xor all
at once; the rest of the protocol is checked frame by frame against the manual,
including the byte order of the CRC, an exception reply told apart from a broken
line, and a reply from another slave on the bus refused. The write echo must
match the value sent: it is the only confirmation the output really switched,
and accepting the frame without it is how a radio ends up dark behind a green
light.

Framing follows the manual's own rules: 3.5 character times of silence between
frames (4 ms at 9600), and a frame is over when the line falls quiet — Modbus
RTU has no terminator, and a serial read that times out returns (0, nil) here,
so the reader is built on a deadline and a quiet-time rather than on an error
that never comes.

Untested against hardware — nobody here has the supply.
This commit is contained in:
2026-08-16 13:15:55 +02:00
parent 9f8e3c73d9
commit 8683a450a7
12 changed files with 1038 additions and 4 deletions
+9
View File
@@ -15,6 +15,7 @@ import {cluster} from '../models';
import {extsvc} from '../models';
import {powergenius} from '../models';
import {pskr} from '../models';
import {psu} from '../models';
import {spe} from '../models';
import {solar} from '../models';
import {tunergenius} from '../models';
@@ -488,6 +489,10 @@ export function GetPOTAToken():Promise<string>;
export function GetPSKReporterStatus():Promise<pskr.Status>;
export function GetPSUSettings():Promise<main.PSUSettings>;
export function GetPSUStatus():Promise<psu.Status>;
export function GetPendingQSOs():Promise<Array<qso.QSO>>;
export function GetQSLDefaults():Promise<main.QSLDefaults>;
@@ -944,6 +949,8 @@ export function SavePGXLSettings(arg1:main.PGXLSettings):Promise<void>;
export function SavePOTAToken(arg1:string):Promise<void>;
export function SavePSUSettings(arg1:main.PSUSettings):Promise<void>;
export function SaveProfile(arg1:profile.Profile):Promise<profile.Profile>;
export function SaveQSLDefaults(arg1:main.QSLDefaults):Promise<void>;
@@ -1024,6 +1031,8 @@ export function SetMotorFollow(arg1:boolean,arg2:number,arg3:string):Promise<voi
export function SetOpsLogQSLReceived(arg1:number,arg2:boolean):Promise<void>;
export function SetPSUOutput(arg1:boolean):Promise<void>;
export function SetPassphrase(arg1:string):Promise<void>;
export function SetScpEnabled(arg1:boolean):Promise<void>;