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
+51
View File
@@ -2733,6 +2733,24 @@ export namespace main {
}
}
export class PSUSettings {
enabled: boolean;
com_port: string;
baud: number;
address: number;
static createFrom(source: any = {}) {
return new PSUSettings(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.enabled = source["enabled"];
this.com_port = source["com_port"];
this.baud = source["baud"];
this.address = source["address"];
}
}
export class QSLBulkUpdate {
sent_status: string;
rcvd_status: string;
@@ -4063,6 +4081,39 @@ export namespace pskr {
}
export namespace psu {
export class Status {
connected: boolean;
on: boolean;
volts: number;
amps: number;
watts: number;
set_volts: number;
set_amps: number;
protected: number;
error?: 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.on = source["on"];
this.volts = source["volts"];
this.amps = source["amps"];
this.watts = source["watts"];
this.set_volts = source["set_volts"];
this.set_amps = source["set_amps"];
this.protected = source["protected"];
this.error = source["error"];
}
}
}
export namespace qslcard {
export class Bevel {