node-red-contrib-power-genius 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,135 +1,135 @@
1
- // Client for the PowerGenius XL line based API (TCP port 9008).
2
- // banner: V<a.b.c>[ AUTH]\r\n (sent once on connect)
3
- // request: C<seq>|<command>\n (seq 1-255; the documentation says CR, but a bare LF is what the device works with)
4
- // reply: R<seq>|<hex result>|<message>
5
- // status: S<n>|<message> (asynchronous, for example "S|state=REBOOT")
6
- // The documentation is incomplete, so everything here is tolerant: unknown lines are passed on, never dropped.
7
- class GeniusPGProtocol {
8
- constructor(socket) {
9
- this.socket = socket;
10
- this.seq = 0;
11
- this.pending = new Map();
12
- this.lineBuffer = "";
13
- this.statusHandler = null;
14
- this.bannerHandler = null;
15
- this.lineHandler = null;
16
- }
17
-
18
- nextSeq() {
19
- this.seq = (this.seq % 255) + 1;
20
- return this.seq;
21
- }
22
-
23
- handleData(chunk) {
24
- this.lineBuffer += chunk.toString("utf8");
25
- const lines = this.lineBuffer.split(/\r\n|\r|\n/);
26
- this.lineBuffer = lines.pop();
27
- for (const line of lines) {
28
- this._handleLine(line);
29
- }
30
- }
31
-
32
- _handleLine(line) {
33
- if (!line) return;
34
- if (this.lineHandler) this.lineHandler(line);
35
-
36
- if (line.startsWith("V")) {
37
- if (this.bannerHandler) this.bannerHandler(line);
38
- return;
39
- }
40
-
41
- if (line.startsWith("S")) {
42
- const pipeIdx = line.indexOf("|");
43
- const msg = pipeIdx !== -1 ? line.substring(pipeIdx + 1) : "";
44
- if (this.statusHandler) this.statusHandler(msg);
45
- return;
46
- }
47
-
48
- if (line.startsWith("R")) {
49
- const pipeIdx1 = line.indexOf("|");
50
- if (pipeIdx1 === -1) return;
51
- const pipeIdx2 = line.indexOf("|", pipeIdx1 + 1);
52
- const seq = parseInt(line.substring(1, pipeIdx1), 10);
53
- const pending = this.pending.get(seq);
54
- if (!pending) return;
55
- this.pending.delete(seq);
56
-
57
- // The documentation's examples show replies without the hex result: "R0|state=STANDBY ...".
58
- // A real result field contains only hex digits, so anything else is the message itself.
59
- const first = line.substring(pipeIdx1 + 1, pipeIdx2 !== -1 ? pipeIdx2 : undefined);
60
- const hasResult = pipeIdx2 !== -1 && /^[0-9a-fA-F]+$/.test(first);
61
- const hexResult = hasResult ? first : "0";
62
- const message = hasResult ? line.substring(pipeIdx2 + 1) : line.substring(pipeIdx1 + 1);
63
-
64
- if (parseInt(hexResult, 16) !== 0) {
65
- const error = new Error(`Command error: 0x${hexResult} - ${message}`);
66
- error.code = hexResult;
67
- pending.reject(error);
68
- } else {
69
- pending.resolve(message);
70
- }
71
- }
72
- }
73
-
74
- sendCommand(cmd, timeoutMs) {
75
- const seq = this.nextSeq();
76
- return new Promise((resolve, reject) => {
77
- const entry = { resolve, reject };
78
- if (timeoutMs) {
79
- const timer = setTimeout(() => {
80
- this.pending.delete(seq);
81
- const error = new Error(`Timeout waiting for reply to '${cmd}'`);
82
- error.code = "ETIMEDOUT";
83
- reject(error);
84
- }, timeoutMs);
85
- entry.resolve = (v) => { clearTimeout(timer); resolve(v); };
86
- entry.reject = (e) => { clearTimeout(timer); reject(e); };
87
- }
88
- this.pending.set(seq, entry);
89
- this.socket.write(`C${seq}|${cmd}\n`);
90
- });
91
- }
92
-
93
- onStatus(handler) {
94
- this.statusHandler = handler;
95
- }
96
-
97
- onBanner(handler) {
98
- this.bannerHandler = handler;
99
- }
100
-
101
- // Called for every received line, before it is interpreted. Used to learn undocumented traffic.
102
- onLine(handler) {
103
- this.lineHandler = handler;
104
- }
105
- }
106
-
107
- const toValue = (value) => {
108
- if (value === "") return value;
109
- const n = Number(value);
110
- return Number.isNaN(n) ? value : n;
111
- };
112
-
113
- // Parses "key=value key=value ..." (optionally prefixed by a word such as "status").
114
- const parseKeyValues = (msg) => {
115
- const kv = {};
116
- const re = /([\w-]+)=(\S*)/g;
117
- let m;
118
- while ((m = re.exec(msg)) !== null) {
119
- kv[m[1]] = toValue(m[2]);
120
- }
121
- return kv;
122
- };
123
-
124
- // "V3.8.8 AUTH"
125
- const parseBanner = (line) => {
126
- const tokens = line.trim().split(/\s+/);
127
- return {
128
- firmware: tokens[0].replace(/^V/, ""),
129
- authRequired: tokens.includes("AUTH"),
130
- };
131
- };
132
-
133
- exports.GeniusPGProtocol = GeniusPGProtocol;
134
- exports.parseKeyValues = parseKeyValues;
135
- exports.parseBanner = parseBanner;
1
+ // Client for the PowerGenius XL line based API (TCP port 9008).
2
+ // banner: V<a.b.c>[ AUTH]\r\n (sent once on connect)
3
+ // request: C<seq>|<command>\n (seq 1-255; the documentation says CR, but a bare LF is what the device works with)
4
+ // reply: R<seq>|<hex result>|<message>
5
+ // status: S<n>|<message> (asynchronous, for example "S|state=REBOOT")
6
+ // The documentation is incomplete, so everything here is tolerant: unknown lines are passed on, never dropped.
7
+ class GeniusPGProtocol {
8
+ constructor(socket) {
9
+ this.socket = socket;
10
+ this.seq = 0;
11
+ this.pending = new Map();
12
+ this.lineBuffer = "";
13
+ this.statusHandler = null;
14
+ this.bannerHandler = null;
15
+ this.lineHandler = null;
16
+ }
17
+
18
+ nextSeq() {
19
+ this.seq = (this.seq % 255) + 1;
20
+ return this.seq;
21
+ }
22
+
23
+ handleData(chunk) {
24
+ this.lineBuffer += chunk.toString("utf8");
25
+ const lines = this.lineBuffer.split(/\r\n|\r|\n/);
26
+ this.lineBuffer = lines.pop();
27
+ for (const line of lines) {
28
+ this._handleLine(line);
29
+ }
30
+ }
31
+
32
+ _handleLine(line) {
33
+ if (!line) return;
34
+ if (this.lineHandler) this.lineHandler(line);
35
+
36
+ if (line.startsWith("V")) {
37
+ if (this.bannerHandler) this.bannerHandler(line);
38
+ return;
39
+ }
40
+
41
+ if (line.startsWith("S")) {
42
+ const pipeIdx = line.indexOf("|");
43
+ const msg = pipeIdx !== -1 ? line.substring(pipeIdx + 1) : "";
44
+ if (this.statusHandler) this.statusHandler(msg);
45
+ return;
46
+ }
47
+
48
+ if (line.startsWith("R")) {
49
+ const pipeIdx1 = line.indexOf("|");
50
+ if (pipeIdx1 === -1) return;
51
+ const pipeIdx2 = line.indexOf("|", pipeIdx1 + 1);
52
+ const seq = parseInt(line.substring(1, pipeIdx1), 10);
53
+ const pending = this.pending.get(seq);
54
+ if (!pending) return;
55
+ this.pending.delete(seq);
56
+
57
+ // The documentation's examples show replies without the hex result: "R0|state=STANDBY ...".
58
+ // A real result field contains only hex digits, so anything else is the message itself.
59
+ const first = line.substring(pipeIdx1 + 1, pipeIdx2 !== -1 ? pipeIdx2 : undefined);
60
+ const hasResult = pipeIdx2 !== -1 && /^[0-9a-fA-F]+$/.test(first);
61
+ const hexResult = hasResult ? first : "0";
62
+ const message = hasResult ? line.substring(pipeIdx2 + 1) : line.substring(pipeIdx1 + 1);
63
+
64
+ if (parseInt(hexResult, 16) !== 0) {
65
+ const error = new Error(`Command error: 0x${hexResult} - ${message}`);
66
+ error.code = hexResult;
67
+ pending.reject(error);
68
+ } else {
69
+ pending.resolve(message);
70
+ }
71
+ }
72
+ }
73
+
74
+ sendCommand(cmd, timeoutMs) {
75
+ const seq = this.nextSeq();
76
+ return new Promise((resolve, reject) => {
77
+ const entry = { resolve, reject };
78
+ if (timeoutMs) {
79
+ const timer = setTimeout(() => {
80
+ this.pending.delete(seq);
81
+ const error = new Error(`Timeout waiting for reply to '${cmd}'`);
82
+ error.code = "ETIMEDOUT";
83
+ reject(error);
84
+ }, timeoutMs);
85
+ entry.resolve = (v) => { clearTimeout(timer); resolve(v); };
86
+ entry.reject = (e) => { clearTimeout(timer); reject(e); };
87
+ }
88
+ this.pending.set(seq, entry);
89
+ this.socket.write(`C${seq}|${cmd}\n`);
90
+ });
91
+ }
92
+
93
+ onStatus(handler) {
94
+ this.statusHandler = handler;
95
+ }
96
+
97
+ onBanner(handler) {
98
+ this.bannerHandler = handler;
99
+ }
100
+
101
+ // Called for every received line, before it is interpreted. Used to learn undocumented traffic.
102
+ onLine(handler) {
103
+ this.lineHandler = handler;
104
+ }
105
+ }
106
+
107
+ const toValue = (value) => {
108
+ if (value === "") return value;
109
+ const n = Number(value);
110
+ return Number.isNaN(n) ? value : n;
111
+ };
112
+
113
+ // Parses "key=value key=value ..." (optionally prefixed by a word such as "status").
114
+ const parseKeyValues = (msg) => {
115
+ const kv = {};
116
+ const re = /([\w-]+)=(\S*)/g;
117
+ let m;
118
+ while ((m = re.exec(msg)) !== null) {
119
+ kv[m[1]] = toValue(m[2]);
120
+ }
121
+ return kv;
122
+ };
123
+
124
+ // "V3.8.8 AUTH"
125
+ const parseBanner = (line) => {
126
+ const tokens = line.trim().split(/\s+/);
127
+ return {
128
+ firmware: tokens[0].replace(/^V/, ""),
129
+ authRequired: tokens.includes("AUTH"),
130
+ };
131
+ };
132
+
133
+ exports.GeniusPGProtocol = GeniusPGProtocol;
134
+ exports.parseKeyValues = parseKeyValues;
135
+ exports.parseBanner = parseBanner;
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Jorgen Dahl
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jorgen Dahl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,107 +1,106 @@
1
- # node-red-contrib-power-genius
2
-
3
- Node-RED nodes for monitoring a FlexRadio / 4O3A PowerGenius XL (PGXL) amplifier over its TCP API (port 9008).
4
- Framing: `C<seq>|<command>` request, `R<seq>|<hex>|<message>` reply, `S<n>|<message>` asynchronous status.
5
-
6
- Requires Node.js 18 or newer and Node-RED 3.1.0 or newer.
7
-
8
- [![Node.js CI](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/node.js.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/node.js.yml) [![Node.js Package](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/npm-publish.yml) [![CodeQL](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/codeql-analysis.yml) [![njsscan sarif](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/njsscan-analysis.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/njsscan-analysis.yml)
9
- > **Work in progress.** The API documentation ([API Docs](API%20Docs/PGXL-Amplifier-to-Radio-API-Documentation.pdf),
10
- > firmware 3.8.8) only covers a few commands and says little about the replies, so the nodes keep whatever the
11
- > amplifier sends. The property tables below come from a real amplifier (firmware 3.9.9); units not given in the
12
- > documentation are inferred.
13
-
14
- ## Nodes
15
-
16
- - **power-genius-server** (config): host, port (default 9008), auto connect and status poll interval (default 1000 ms, 0 = off).
17
- - **power-genius-status**: emits one status property per node, or *all* as an object. The property is picked from a list, or typed (*other*) so that properties missing from the documentation can be used. Emits `msg.payload` (the value), `msg.topic` (the property name) and `msg.previous`. With `*`, `msg.payload` is the whole status object and `msg.topic` is `status`. By default it only emits when the value changes; an optional deadband suppresses small numeric changes. A start delay (default 1000 ms) holds back the first output after deploy so that downstream nodes, such as dashboard widgets, can initialize. Any input emits the last known value and refreshes from the device.
18
- - **power-genius-info**: emits the device info once after connecting, either as an object or as a single property. Any input re-reads and emits it.
19
- - **power-genius-control**: switches `operate` (standby / operate), selects the `antenna` (1-3, amplifiers with the antenna switch only), sets the `fanmode` (`STANDARD`, `CONTEST` or `BROADCAST`), or sends any command (`command`, the payload is the command) and outputs the reply as `msg.payload`. The action is fixed in the node, or taken from `msg.topic` when the node is set to `msg.topic`; a fixed action always wins. For `operate` the payload is `true`/`false`, `1`/`0` or `"on"`/`"off"`; anything else is rejected and nothing is sent.
20
-
21
- ### Status properties
22
-
23
- The `status` reply is a list of `key=value` pairs; numbers are emitted as numbers, everything else as text.
24
-
25
- | Property | Meaning | Values / unit |
26
- |---|---|---|
27
- | `state` | Amplifier state | `STANDBY`, `IDLE` (operate, not transmitting), `TRANSMIT_A`, `TRANSMIT_B`; probably `FAULT` |
28
- | `bandA`, `bandB` | Band of slice A / B | metres, e.g. `20` |
29
- | `bsrcA`, `bsrcB` | Source of the band data | e.g. `FLEX` |
30
- | `bsrcAutoA`, `bsrcAutoB` | Band source selected automatically | 1 = yes, 0 = no |
31
- | `flexA`, `flexB` | FlexRadio nickname (FLEX mode) | text |
32
- | `vac` | Mains voltage | V |
33
- | `vdd` | Supply voltage | V; 0 when not transmitting |
34
- | `id`, `peakid` | Drain current, and its peak | A; 0 when not transmitting |
35
- | `fwd`, `peakfwd` | Forward power, and its peak | dBm; `fwd` is 30 (the meter minimum) when there is no RF, `peakfwd` keeps the last peak |
36
- | `swr` | Return loss | dB (negative, e.g. -18; the FLEX `RL` meter); -60 when there is no RF |
37
- | `temp` | PA temperature | °C |
38
- | `hltemp` | Harmonic load temperature | °C |
39
- | `cntfreq` | Frequency counted from the RF | kHz, e.g. 14098; 0 when there is no RF |
40
- | `cat1freq`, `cat2freq` | Frequency from CAT port 1 / 2 (legacy radios with CAT or CI-V) | kHz; 0 in FLEX mode |
41
- | `biasA`, `biasB` | Bias setting of input A / B, as `SOURCE_MODE` | e.g. `RADIO_AB`; others seen elsewhere: `AUTO_AAB`, `MANUAL_A` |
42
- | `fanmode` | Fan mode | `STANDARD`, `CONTEST`, `BROADCAST` |
43
- | `meffa` | MeFFA state | `OFF`, `ACTIVE`; may name a fault such as SWR, TEMP or CURRENT |
44
- | `unkey` | Protection fault | 1 = active protection fault, 0 = normal operation |
45
- | `antA` | Antenna selected on the antenna switch | Only on the 3-port antenna switch version; 0 on the SO2R version, which has no switch |
46
- | `ants` | Antenna switch | 0 on the SO2R version (no antenna switch); values on the antenna switch version not seen yet |
47
-
48
- Many status properties are only meaningful for some radio setups. With a FlexRadio (FLEX mode) the amplifier gets the
49
- band and PTT from the radio over the network, and the properties for legacy radios using CAT or CI-V, such as
50
- `cat1freq` and `cat2freq`, stay at 0 (the `catradioA`/`catradioB` info objects likewise describe the legacy radio setup).
51
- It is not known which of the remaining properties, for example `bsrcAutoA/B`, belong to which mode, since the test amplifier is
52
- used with a FlexRadio.
53
-
54
- Units are taken from the FLEX meter definitions in the API documentation and from observed values. There are two
55
- hardware versions: SO2R, and one with a 3-port antenna switch. `antA` and `ants` were only seen on the SO2R version.
56
- Other properties may show up on other
57
- firmware or setups: use a status node
58
- with property `*` and a debug node to list them.
59
-
60
- ### Info properties
61
-
62
- Collected with `setup read`, `ifconf read`, `catradio read=A|B` and `flexradio read=A|B`; each reply is parsed as `key=value` pairs.
63
-
64
- | Property | Meaning |
65
- |---|---|
66
- | `name` | Device name (the `nickname`, with `_` replaced by a space) |
67
- | `nickname` | Device name as reported |
68
- | `firmware` | Firmware version, from the banner the device sends on connect |
69
- | `authRequired` | `true` when the device asked for authentication (not supported yet) |
70
- | `ledintens` | LED intensity |
71
- | `txdelay` | TX delay |
72
- | `inactivity-timeout` | Inactivity timeout, 0 = off |
73
- | `authcode` | Authentication code for WAN access. Treat it as a secret: it is part of the whole info object |
74
- | `ants`, `ant1name`..`ant3name` | Antenna switch setup: `ants` (0 on the SO2R version, which has no switch) and the names of antennas 1-3 (empty when not set) |
75
- | `ifconf` | Object with the network configuration: `address`, `netmask`, `gateway`, `dhcp`, `dhcpstatus` and the active `address-status`, `netmask-status`, `gateway-status` |
76
- | `catradioA`, `catradioB` | Objects with the CAT radio configuration of slice A / B, used with legacy radios (CAT or CI-V): `type`, `baud`, `parity`, `stopbits`, `civ`, `active` |
77
- | `flexradioA`, `flexradioB` | Objects with the FlexRadio configuration of slice A / B: `serial`, `txant`, `ptt`, `active` |
78
-
79
- ## Commands used
80
-
81
- Read-only: `status`, `setup read`, `ifconf read`, `catradio read={A|B}`, `flexradio read={A|B}`.
82
- Through the control node: `operate={0|1}` (0 = standby, 1 = operate), `antenna={n}` (select the antenna, only on the
83
- antenna switch version), `setup fanmode={STANDARD|CONTEST|BROADCAST}` and anything with the `command` action.
84
-
85
- `operate` comes from another open-source PGXL client ([Log4YM](https://github.com/brianbruff/Log4YM)) and works; it is not
86
- in the API documentation. `set operate=...` is rejected by the amplifier (error `0x50000015`, unknown command), so the commands
87
- are bare `name=value`. `antenna=1` is known to exist: the SO2R version answers `0x50000013 - No antenna switch on this hardware`.
88
- The antenna numbers (assumed 1-3) have not been tried on an amplifier with the switch.
89
-
90
- ## Learning what the amplifier sends
91
-
92
- - Set the Node-RED log level to `trace` to log every line received from the amplifier.
93
- - Use a **power-genius-control** node with action `command` to try commands and see the replies. An error with result code `0x50000015` means the amplifier does not know the command.
94
-
95
- Commands are sent as `C<seq>|<command>` terminated by a bare LF; the documentation says CR, but LF works.
96
-
97
- Not supported: authentication (`AUTH` in the banner, only outside the local network), UDP discovery, and the FLEX radio-side commands (`amplifier create`, `meter create`, `interlock create`, ...), which the amplifier sends to the radio, not to clients.
98
-
99
- ## Example
100
-
101
- An example flow is included. After installing, use **Import → Examples → node-red-contrib-power-genius** in the editor.
102
- Set the host of the **power genius** server node to your amplifier before deploying. The flow is in
103
- [examples/power-genius-example.json](examples/power-genius-example.json).
104
-
105
- ## Install
106
-
107
- npm install node-red-contrib-power-genius
1
+ # node-red-contrib-power-genius
2
+
3
+ Node-RED nodes for monitoring a FlexRadio / 4O3A PowerGenius XL (PGXL) amplifier over its TCP API (port 9008).
4
+ Framing: `C<seq>|<command>` request, `R<seq>|<hex>|<message>` reply, `S<n>|<message>` asynchronous status.
5
+
6
+ Requires Node.js 18 or newer and Node-RED 3.1.0 or newer.
7
+
8
+ [![Node.js CI](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/node.js.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/node.js.yml) [![Node.js Package](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/npm-publish.yml) [![CodeQL](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/codeql-analysis.yml) [![njsscan sarif](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/njsscan-analysis.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/njsscan-analysis.yml)
9
+ > **Work in progress.** The API documentation ([API Docs](API%20Docs/PGXL-Amplifier-to-Radio-API-Documentation.pdf),
10
+ > firmware 3.8.8) only covers a few commands and says little about the replies, so the nodes keep whatever the
11
+ > amplifier sends. The property tables below come from a real amplifier (firmware 3.9.9); units not given in the
12
+ > documentation are inferred.
13
+
14
+ ## Nodes
15
+
16
+ - **power-genius-server** (config): host, port (default 9008), auto connect and status poll interval (default 1000 ms, 0 = off).
17
+ - **power-genius-status**: emits one status property per node, or *all* as an object. The property is picked from a list, or typed (*other*) so that properties missing from the documentation can be used. Emits `msg.payload` (the value), `msg.topic` (the property name) and `msg.previous`. With `*`, `msg.payload` is the whole status object and `msg.topic` is `status`. By default it only emits when the value changes; an optional deadband suppresses small numeric changes. A start delay (default 1000 ms) holds back the first output after deploy so that downstream nodes, such as dashboard widgets, can initialize. Any input emits the last known value and refreshes from the device.
18
+ - **power-genius-info**: emits the device info once after connecting, either as an object or as a single property. Any input re-reads and emits it.
19
+ - **power-genius-control**: switches `operate` (standby / operate), selects the `antenna` (1-3, amplifiers with the antenna switch only), sets the `fanmode` (`STANDARD`, `CONTEST` or `BROADCAST`), or sends any command (`command`, the payload is the command) and outputs the reply as `msg.payload`. The action is fixed in the node, or taken from `msg.topic` when the node is set to `msg.topic`; a fixed action always wins. For `operate` the payload is `true`/`false`, `1`/`0` or `"on"`/`"off"`; anything else is rejected and nothing is sent.
20
+
21
+ ### Status properties
22
+
23
+ The `status` reply is a list of `key=value` pairs; numbers are emitted as numbers, everything else as text.
24
+
25
+ | Property | Meaning | Values / unit |
26
+ |---|---|---|
27
+ | `state` | Amplifier state | `STANDBY`, `IDLE` (operate, not transmitting), `TRANSMIT_A`, `TRANSMIT_B`; probably `FAULT` |
28
+ | `bandA`, `bandB` | Band of slice A / B | metres, e.g. `20` |
29
+ | `bsrcA`, `bsrcB` | Source of the band data | e.g. `FLEX` |
30
+ | `bsrcAutoA`, `bsrcAutoB` | Band source selected automatically | 1 = yes, 0 = no |
31
+ | `flexA`, `flexB` | FlexRadio nickname (FLEX mode) | text |
32
+ | `vac` | Mains voltage | V |
33
+ | `vdd` | Supply voltage | V; 0 when not transmitting |
34
+ | `id`, `peakid` | Drain current, and its peak | A; 0 when not transmitting |
35
+ | `fwd`, `peakfwd` | Forward power, and its peak | dBm; `fwd` is 30 (the meter minimum) when there is no RF, `peakfwd` keeps the last peak |
36
+ | `swr` | Return loss | dB (negative, e.g. -18; the FLEX `RL` meter); -60 when there is no RF |
37
+ | `temp` | PA temperature | °C |
38
+ | `hltemp` | Harmonic load temperature | °C |
39
+ | `cntfreq` | Frequency counted from the RF | kHz, e.g. 14098; 0 when there is no RF |
40
+ | `cat1freq`, `cat2freq` | Frequency from CAT port 1 / 2 (legacy radios with CAT or CI-V) | kHz; 0 in FLEX mode |
41
+ | `biasA`, `biasB` | Bias setting of input A / B, as `SOURCE_MODE` | e.g. `RADIO_AB`; others seen elsewhere: `AUTO_AAB`, `MANUAL_A` |
42
+ | `fanmode` | Fan mode | `STANDARD`, `CONTEST`, `BROADCAST` |
43
+ | `meffa` | MeFFA state | `OFF`, `ACTIVE`; may name a fault such as SWR, TEMP or CURRENT |
44
+ | `unkey` | Protection fault | 1 = active protection fault, 0 = normal operation |
45
+ | `antA` | Antenna selected on the antenna switch | Only on the 3-port antenna switch version; 0 on the SO2R version, which has no switch |
46
+ | `ants` | Antenna switch | 0 on the SO2R version (no antenna switch); values on the antenna switch version not seen yet |
47
+
48
+ Many status properties are only meaningful for some radio setups. With a FlexRadio (FLEX mode) the amplifier gets the
49
+ band and PTT from the radio over the network, and the properties for legacy radios using CAT or CI-V, such as
50
+ `cat1freq` and `cat2freq`, stay at 0 (the `catradioA`/`catradioB` info objects likewise describe the legacy radio setup).
51
+ It is not known which of the remaining properties, for example `bsrcAutoA/B`, belong to which mode, since the test amplifier is
52
+ used with a FlexRadio.
53
+
54
+ Units are taken from the FLEX meter definitions in the API documentation and from observed values. There are two
55
+ hardware versions: SO2R, and one with a 3-port antenna switch. `antA` and `ants` were only seen on the SO2R version.
56
+ Other properties may show up on other
57
+ firmware or setups: use a status node
58
+ with property `*` and a debug node to list them.
59
+
60
+ ### Info properties
61
+
62
+ Collected with `setup read`, `ifconf read`, `catradio read=A|B` and `flexradio read=A|B`; each reply is parsed as `key=value` pairs.
63
+
64
+ | Property | Meaning |
65
+ |---|---|
66
+ | `name` | Device name (the `nickname`, with `_` replaced by a space) |
67
+ | `nickname` | Device name as reported |
68
+ | `firmware` | Firmware version, from the banner the device sends on connect |
69
+ | `authRequired` | `true` when the device asked for authentication (not supported yet) |
70
+ | `ledintens` | LED intensity |
71
+ | `txdelay` | TX delay |
72
+ | `inactivity-timeout` | Inactivity timeout, 0 = off |
73
+ | `authcode` | Authentication code for WAN access. Treat it as a secret: it is part of the whole info object |
74
+ | `ants`, `ant1name`..`ant3name` | Antenna switch setup: `ants` (0 on the SO2R version, which has no switch) and the names of antennas 1-3 (empty when not set) |
75
+ | `ifconf` | Object with the network configuration: `address`, `netmask`, `gateway`, `dhcp`, `dhcpstatus` and the active `address-status`, `netmask-status`, `gateway-status` |
76
+ | `catradioA`, `catradioB` | Objects with the CAT radio configuration of slice A / B, used with legacy radios (CAT or CI-V): `type`, `baud`, `parity`, `stopbits`, `civ`, `active` |
77
+ | `flexradioA`, `flexradioB` | Objects with the FlexRadio configuration of slice A / B: `serial`, `txant`, `ptt`, `active` |
78
+
79
+ ## Commands used
80
+
81
+ Read-only: `status`, `setup read`, `ifconf read`, `catradio read={A|B}`, `flexradio read={A|B}`.
82
+ Through the control node: `operate={0|1}` (0 = standby, 1 = operate), `antenna={n}` (select the antenna, only on the
83
+ antenna switch version), `setup fanmode={STANDARD|CONTEST|BROADCAST}` and anything with the `command` action.
84
+
85
+ `operate` comes from another open-source PGXL client ([Log4YM](https://github.com/brianbruff/Log4YM)) and works; it is not
86
+ in the API documentation. `antenna=1` is known to exist: the SO2R version answers `0x50000013 - No antenna switch on this hardware`.
87
+ The antenna numbers (assumed 1-3) have not been tried on an amplifier with the switch.
88
+
89
+ ## Learning what the amplifier sends
90
+
91
+ - Set the Node-RED log level to `trace` to log every line received from the amplifier.
92
+ - Use a **power-genius-control** node with action `command` to try commands and see the replies. An error with result code `0x50000015` means the amplifier does not know the command.
93
+
94
+ Commands are sent as `C<seq>|<command>` terminated by a bare LF; the documentation says CR, but LF works.
95
+
96
+ Not supported: authentication (`AUTH` in the banner, only outside the local network), UDP discovery, and the FLEX radio-side commands (`amplifier create`, `meter create`, `interlock create`, ...), which the amplifier sends to the radio, not to clients.
97
+
98
+ ## Example
99
+
100
+ An example flow is included. After installing, use **Import → Examples → node-red-contrib-power-genius** in the editor.
101
+ Set the host of the **power genius** server node to your amplifier before deploying. The flow is in
102
+ [examples/power-genius-example.json](examples/power-genius-example.json).
103
+
104
+ ## Install
105
+
106
+ npm install node-red-contrib-power-genius