node-red-contrib-antenna-genius 1.1.0 → 2.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.
@@ -38,11 +38,11 @@ jobs:
38
38
 
39
39
  steps:
40
40
  - name: Checkout repository
41
- uses: actions/checkout@v2
41
+ uses: actions/checkout@v6
42
42
 
43
43
  # Initializes the CodeQL tools for scanning.
44
44
  - name: Initialize CodeQL
45
- uses: github/codeql-action/init@v1
45
+ uses: github/codeql-action/init@v4
46
46
  with:
47
47
  languages: ${{ matrix.language }}
48
48
  # If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,7 +53,7 @@ jobs:
53
53
  # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54
54
  # If this step fails, then you should remove it and run the build manually (see below)
55
55
  - name: Autobuild
56
- uses: github/codeql-action/autobuild@v1
56
+ uses: github/codeql-action/autobuild@v4
57
57
 
58
58
  # ℹ️ Command-line programs to run using the OS shell.
59
59
  # 📚 https://git.io/JvXDl
@@ -67,4 +67,4 @@ jobs:
67
67
  # make release
68
68
 
69
69
  - name: Perform CodeQL Analysis
70
- uses: github/codeql-action/analyze@v1
70
+ uses: github/codeql-action/analyze@v4
@@ -23,13 +23,13 @@ jobs:
23
23
  name: njsscan code scanning
24
24
  steps:
25
25
  - name: Checkout the code
26
- uses: actions/checkout@v2
26
+ uses: actions/checkout@v6
27
27
  - name: nodejsscan scan
28
28
  id: njsscan
29
29
  uses: ajinabraham/njsscan-action@7237412fdd36af517e2745077cedbf9d6900d711
30
30
  with:
31
31
  args: '. --sarif --output results.sarif || true'
32
32
  - name: Upload njsscan report
33
- uses: github/codeql-action/upload-sarif@v1
33
+ uses: github/codeql-action/upload-sarif@v4
34
34
  with:
35
35
  sarif_file: results.sarif
@@ -18,13 +18,13 @@ jobs:
18
18
 
19
19
  strategy:
20
20
  matrix:
21
- node-version: [14.x, 16.x, 18.x]
21
+ node-version: [22.x, 24.x, 26.x]
22
22
  # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v6
26
26
  - name: Use Node.js ${{ matrix.node-version }}
27
- uses: actions/setup-node@v2
27
+ uses: actions/setup-node@v6
28
28
  with:
29
29
  node-version: ${{ matrix.node-version }}
30
30
  cache: 'npm'
@@ -11,24 +11,28 @@ jobs:
11
11
  build:
12
12
  runs-on: ubuntu-latest
13
13
  steps:
14
- - uses: actions/checkout@v2
15
- - uses: actions/setup-node@v2
14
+ - uses: actions/checkout@v6
15
+ - uses: actions/setup-node@v6
16
16
  with:
17
- node-version: 16
17
+ node-version: 24
18
+ package-manager-cache: false
18
19
  - run: npm ci
19
20
  - run: npm test
20
21
 
21
22
  publish-npm:
22
23
  needs: build
23
24
  runs-on: ubuntu-latest
25
+ # Required permissions for OIDC provenance tracking and package deployment
26
+ permissions:
27
+ contents: read
28
+ id-token: write
24
29
  steps:
25
- - uses: actions/checkout@v2
26
- - uses: actions/setup-node@v2
30
+ - uses: actions/checkout@v6
31
+ - uses: actions/setup-node@v6
27
32
  with:
28
- node-version: 16
33
+ node-version: 24
29
34
  registry-url: https://registry.npmjs.org/
35
+ package-manager-cache: false # never use caching in release builds
30
36
  - run: npm ci
31
37
  - run: npx auto-dist-tag --write
32
38
  - run: npm publish
33
- env:
34
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
@@ -0,0 +1,162 @@
1
+ class GeniusV4Protocol {
2
+ constructor(socket) {
3
+ this.socket = socket;
4
+ this.seq = 0;
5
+ this.pending = new Map();
6
+ this.lineBuffer = "";
7
+ this.statusHandler = null;
8
+ }
9
+
10
+ nextSeq() {
11
+ this.seq = (this.seq % 255) + 1;
12
+ return this.seq;
13
+ }
14
+
15
+ handleData(chunk) {
16
+ this.lineBuffer += chunk.toString("utf8");
17
+ const lines = this.lineBuffer.split("\n");
18
+ this.lineBuffer = lines.pop();
19
+ for (const line of lines) {
20
+ this._handleLine(line.replace(/\r$/, ""));
21
+ }
22
+ }
23
+
24
+ _handleLine(line) {
25
+ if (!line) return;
26
+
27
+ if (line.startsWith("S")) {
28
+ const pipeIdx = line.indexOf("|");
29
+ const msg = pipeIdx !== -1 ? line.substring(pipeIdx + 1) : "";
30
+ if (this.statusHandler) {
31
+ this.statusHandler(msg);
32
+ }
33
+ return;
34
+ }
35
+
36
+ if (line.startsWith("R")) {
37
+ const pipeIdx1 = line.indexOf("|");
38
+ const pipeIdx2 = line.indexOf("|", pipeIdx1 + 1);
39
+ const seq = parseInt(line.substring(1, pipeIdx1));
40
+ const hexResult = line.substring(pipeIdx1 + 1, pipeIdx2 !== -1 ? pipeIdx2 : undefined);
41
+ const message = pipeIdx2 !== -1 ? line.substring(pipeIdx2 + 1) : "";
42
+
43
+ const pending = this.pending.get(seq);
44
+ if (!pending) return;
45
+
46
+ if (parseInt(hexResult, 16) !== 0) {
47
+ this.pending.delete(seq);
48
+ pending.reject(new Error(`Command error: 0x${hexResult} - ${message}`));
49
+ return;
50
+ }
51
+
52
+ if (pending.multi) {
53
+ if (message === "") {
54
+ this.pending.delete(seq);
55
+ pending.resolve(pending.lines);
56
+ } else {
57
+ pending.lines.push(message);
58
+ }
59
+ } else {
60
+ this.pending.delete(seq);
61
+ pending.resolve(message);
62
+ }
63
+ }
64
+ }
65
+
66
+ sendCommand(cmd) {
67
+ const seq = this.nextSeq();
68
+ return new Promise((resolve, reject) => {
69
+ this.pending.set(seq, { resolve, reject, lines: null, multi: false });
70
+ this.socket.write(`C${seq}|${cmd}\r\n`);
71
+ });
72
+ }
73
+
74
+ sendCommandMulti(cmd) {
75
+ const seq = this.nextSeq();
76
+ return new Promise((resolve, reject) => {
77
+ this.pending.set(seq, { resolve, reject, lines: [], multi: true });
78
+ this.socket.write(`C${seq}|${cmd}\r\n`);
79
+ });
80
+ }
81
+
82
+ onStatus(handler) {
83
+ this.statusHandler = handler;
84
+ }
85
+ }
86
+
87
+ const parseKeyValues = (tokens, startIndex) => {
88
+ const kv = {};
89
+ for (let i = startIndex; i < tokens.length; i++) {
90
+ const eq = tokens[i].indexOf("=");
91
+ if (eq !== -1) {
92
+ kv[tokens[i].substring(0, eq)] = tokens[i].substring(eq + 1);
93
+ }
94
+ }
95
+ return kv;
96
+ };
97
+
98
+ const parseInfo = (msg) => {
99
+ // "info v=4.0.22 date=2023-08-22 btl=1.6 hw=2.0 serial=9A-3A-DC name=Antenna_Genius ports=2 antennas=8 mode=master uptime=3600"
100
+ const tokens = msg.split(" ");
101
+ const kv = parseKeyValues(tokens, 1);
102
+ return {
103
+ name: (kv.name || "").replace(/_/g, " "),
104
+ ports: parseInt(kv.ports) || 2,
105
+ antennas: parseInt(kv.antennas) || 8,
106
+ firmware: kv.v || "",
107
+ serial: kv.serial || "",
108
+ };
109
+ };
110
+
111
+ const parsePort = (msg) => {
112
+ // "port 1 auto=1 source=AUTO band=0 rxant=0 txant=0 tx=0 inhibit=0"
113
+ const tokens = msg.split(" ");
114
+ const portNum = parseInt(tokens[1]);
115
+ const kv = parseKeyValues(tokens, 2);
116
+ return {
117
+ portNum,
118
+ band: parseInt(kv.band) || 0,
119
+ rxant: parseInt(kv.rxant) || 0,
120
+ txant: parseInt(kv.txant) || 0,
121
+ tx: parseInt(kv.tx) || 0,
122
+ inhibit: parseInt(kv.inhibit) || 0,
123
+ };
124
+ };
125
+
126
+ const parseAntennaList = (lines) => {
127
+ // "antenna 1 name=Antenna_1 tx=0000 rx=0001 inband=0000"
128
+ return lines.map((line) => {
129
+ const tokens = line.split(" ");
130
+ const index = parseInt(tokens[1]);
131
+ const kv = parseKeyValues(tokens, 2);
132
+ const rxMask = parseInt(kv.rx || "0", 16);
133
+ const bands = [];
134
+ for (let i = 0; i < 16; i++) {
135
+ bands.push((rxMask >> i) & 1);
136
+ }
137
+ return {
138
+ index,
139
+ name: (kv.name || "").replace(/_/g, " "),
140
+ bands,
141
+ };
142
+ });
143
+ };
144
+
145
+ const parseBandList = (lines) => {
146
+ // "band 0 name=None freq_start=0.000000 freq_stop=0.000000"
147
+ return lines.map((line) => {
148
+ const tokens = line.split(" ");
149
+ const index = parseInt(tokens[1]);
150
+ const kv = parseKeyValues(tokens, 2);
151
+ return {
152
+ index,
153
+ name: (kv.name || "").replace(/_/g, " "),
154
+ };
155
+ });
156
+ };
157
+
158
+ exports.GeniusV4Protocol = GeniusV4Protocol;
159
+ exports.parseInfo = parseInfo;
160
+ exports.parsePort = parsePort;
161
+ exports.parseAntennaList = parseAntennaList;
162
+ exports.parseBandList = parseBandList;
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # node-red-contrib-antenna-genius
2
2
  Antenna Genius Control and Monitoring Nodes
3
3
 
4
+ Supports V3 and V4 of the Antenna Genius firmware.
5
+
4
6
  [![Node.js CI](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/node.js.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/node.js.yml) [![Node.js Package](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/npm-publish.yml) [![CodeQL](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/codeql-analysis.yml) [![njsscan sarif](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/njsscan-analysis.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/njsscan-analysis.yml)
5
7
 
6
8
  ![image](https://user-images.githubusercontent.com/7033002/148573983-00305f15-7acf-4d4d-bb1f-9c820c4b3a79.png) ![image](https://user-images.githubusercontent.com/7033002/148489672-9e2c03e0-a968-4c2f-9dc7-923c5423fad7.png)
@@ -94,3 +96,4 @@ Antenna Genius shared connection configuration.
94
96
  `Active Background (string)`: Background color to use for an antenna that is active (e.g. can be selected).
95
97
 
96
98
  `Selected Background (string)`: Background color to use for and antenna that is selected.
99
+
@@ -1,5 +1,3 @@
1
- const Utils = require("./GeniusUtils");
2
-
3
1
  module.exports = (RED) => {
4
2
  class AntennaGeniusActivateAntenna {
5
3
  constructor(config) {
@@ -47,9 +45,7 @@ module.exports = (RED) => {
47
45
  });
48
46
  }
49
47
 
50
- let command = Utils.encode(0, 0, 415, 0, msg.topic);
51
-
52
- this.server.client.write(command);
48
+ this.server.activate(msg.topic);
53
49
  } else {
54
50
  this.status({
55
51
  fill: "red",
@@ -2,6 +2,7 @@ const Net = require("net");
2
2
  const { PromiseSocket } = require("promise-socket");
3
3
  const EventEmitter = require("events");
4
4
  const Utils = require("./GeniusUtils");
5
+ const { GeniusV4Protocol, parseInfo, parsePort, parseAntennaList, parseBandList } = require("./GeniusV4Protocol");
5
6
 
6
7
  class UpdatesEventEmitter extends EventEmitter {}
7
8
 
@@ -23,6 +24,9 @@ module.exports = (RED) => {
23
24
  this.interval = null;
24
25
  this.timer = null;
25
26
  this.refresh = -1;
27
+ this.apiVersion = null;
28
+ this._v4 = null;
29
+ this._v4DataHandler = null;
26
30
 
27
31
  this.updatesEventEmitter = new UpdatesEventEmitter();
28
32
  this.updatesEventEmitter.setMaxListeners(0);
@@ -36,6 +40,12 @@ module.exports = (RED) => {
36
40
  clearTimeout(this.timer);
37
41
  this.connected = false;
38
42
 
43
+ if (this._v4DataHandler) {
44
+ this.client.removeListener("data", this._v4DataHandler);
45
+ this._v4DataHandler = null;
46
+ }
47
+ this._v4 = null;
48
+
39
49
  if (this.updatesEventEmitter.listenerCount("closed") == 0) {
40
50
  this.log(
41
51
  "Stop retrying. No nodes are refering this connection anymore."
@@ -59,60 +69,33 @@ module.exports = (RED) => {
59
69
  this.log("TCP connection established with the server.");
60
70
  this.connected = true;
61
71
 
62
- const promiseClient = new PromiseSocket(this.client);
63
-
64
- let command = Utils.encode(0, 0, 401, 0, "");
65
- await promiseClient.write(command);
66
- let packet = await promiseClient.read();
67
- this.status = Utils.decode(packet);
68
-
69
- command = Utils.encode(0, 0, 24, 0, "");
70
- await promiseClient.write(command);
71
- packet = await promiseClient.read();
72
- this.info = Utils.decode(packet);
73
-
74
- let numAntennas = 8 * this.status.stackReach;
75
- for (let index = 1; index <= numAntennas; ++index) {
76
- let command = Utils.encode(0, 0, 412, 0, index.toString());
77
- await promiseClient.write(command);
78
- let packet = await promiseClient.read();
79
- let data = Utils.decode(packet);
80
- this.antennas.push(data);
81
- }
82
- this.updatesEventEmitter.emit("antennas");
83
-
84
- for (let index = 0; index < 16; ++index) {
85
- let command = Utils.encode(0, 0, 82, 0, index.toString());
86
- await promiseClient.write(command);
87
- let packet = await promiseClient.read();
88
- let data = Utils.decode(packet);
89
- this.bands.push(data);
90
- }
91
- this.updatesEventEmitter.emit("bands");
92
-
93
- this.client.on("data", (packet) => {
94
- let decoded = Utils.decode(packet);
95
- if (decoded.command == 401) {
96
- this.status = {
97
- ...this.status,
98
- ...Utils.decode(packet),
99
- };
100
- this.refresh = (this.refresh + 1) % 1500;
101
- this.updatesEventEmitter.emit(
102
- "status",
103
- this.refresh == 0
104
- );
105
- }
72
+ // Detect API version from prologue banner.
73
+ // V4 devices immediately send "V4.x.x AG\r\n" upon connection.
74
+ // V3 devices send nothing and wait for commands.
75
+ const banner = await new Promise((resolve) => {
76
+ let timer;
77
+ const handler = (data) => {
78
+ clearTimeout(timer);
79
+ resolve(data.toString());
80
+ };
81
+ timer = setTimeout(() => {
82
+ this.client.removeListener("data", handler);
83
+ resolve(null);
84
+ }, 300);
85
+ this.client.once("data", handler);
106
86
  });
107
87
 
108
- // Poll status
109
- this.interval = setInterval(() => {
110
- let command = Utils.encode(0, 0, 401, 0, "");
111
- this.client.write(command);
112
- }, 400);
113
-
114
- this.forceUpdate();
115
- this.updatesEventEmitter.emit("connected");
88
+ try {
89
+ if (banner && /^V4/.test(banner)) {
90
+ this.apiVersion = 4;
91
+ await this._connectV4();
92
+ } else {
93
+ this.apiVersion = 3;
94
+ await this._connectV3();
95
+ }
96
+ } catch (err) {
97
+ this.warn("Connection setup failed: " + err.message);
98
+ }
116
99
  });
117
100
 
118
101
  this.client.on("error", (err) => {
@@ -124,12 +107,162 @@ module.exports = (RED) => {
124
107
  clearTimeout(this.timer);
125
108
  this.autoConnect = false;
126
109
  this.connected = false;
110
+ if (this._v4DataHandler) {
111
+ this.client.removeListener("data", this._v4DataHandler);
112
+ this._v4DataHandler = null;
113
+ }
114
+ this._v4 = null;
127
115
  this.client.end();
128
116
  this.forceUpdate();
129
117
  done();
130
118
  });
131
119
  }
132
120
 
121
+ async _connectV3() {
122
+ const promiseClient = new PromiseSocket(this.client);
123
+
124
+ let command = Utils.encode(0, 0, 401, 0, "");
125
+ await promiseClient.write(command);
126
+ let packet = await promiseClient.read();
127
+ this.status = Utils.decode(packet);
128
+
129
+ command = Utils.encode(0, 0, 24, 0, "");
130
+ await promiseClient.write(command);
131
+ packet = await promiseClient.read();
132
+ this.info = Utils.decode(packet);
133
+
134
+ let numAntennas = 8 * this.status.stackReach;
135
+ for (let index = 1; index <= numAntennas; ++index) {
136
+ let command = Utils.encode(0, 0, 412, 0, index.toString());
137
+ await promiseClient.write(command);
138
+ let packet = await promiseClient.read();
139
+ let data = Utils.decode(packet);
140
+ this.antennas.push(data);
141
+ }
142
+ this.updatesEventEmitter.emit("antennas");
143
+
144
+ for (let index = 0; index < 16; ++index) {
145
+ let command = Utils.encode(0, 0, 82, 0, index.toString());
146
+ await promiseClient.write(command);
147
+ let packet = await promiseClient.read();
148
+ let data = Utils.decode(packet);
149
+ this.bands.push(data);
150
+ }
151
+ this.updatesEventEmitter.emit("bands");
152
+
153
+ this.client.on("data", (packet) => {
154
+ let decoded = Utils.decode(packet);
155
+ if (decoded.command == 401) {
156
+ this.status = {
157
+ ...this.status,
158
+ ...Utils.decode(packet),
159
+ };
160
+ this.refresh = (this.refresh + 1) % 1500;
161
+ this.updatesEventEmitter.emit(
162
+ "status",
163
+ this.refresh == 0
164
+ );
165
+ }
166
+ });
167
+
168
+ this.interval = setInterval(() => {
169
+ let command = Utils.encode(0, 0, 401, 0, "");
170
+ this.client.write(command);
171
+ }, 400);
172
+
173
+ this.forceUpdate();
174
+ this.updatesEventEmitter.emit("connected");
175
+ }
176
+
177
+ async _connectV4() {
178
+ const v4 = new GeniusV4Protocol(this.client);
179
+ this._v4 = v4;
180
+
181
+ const dataHandler = (chunk) => v4.handleData(chunk);
182
+ this._v4DataHandler = dataHandler;
183
+ this.client.on("data", dataHandler);
184
+
185
+ const infoMsg = await v4.sendCommand("info get");
186
+ this.info = parseInfo(infoMsg);
187
+
188
+ const port1Msg = await v4.sendCommand("port get 1");
189
+ const port2Msg = await v4.sendCommand("port get 2");
190
+ const port1 = parsePort(port1Msg);
191
+ const port2 = parsePort(port2Msg);
192
+ this.status = {
193
+ portA_band: port1.band,
194
+ portA_antenna: port1.rxant,
195
+ portA_tx: port1.tx,
196
+ portA_inhibit: port1.inhibit,
197
+ portB_band: port2.band,
198
+ portB_antenna: port2.rxant,
199
+ portB_tx: port2.tx,
200
+ portB_inhibit: port2.inhibit,
201
+ stackReach: Math.ceil(this.info.antennas / 8),
202
+ };
203
+
204
+ const antennaLines = await v4.sendCommandMulti("antenna list");
205
+ this.antennas = parseAntennaList(antennaLines);
206
+ this.updatesEventEmitter.emit("antennas");
207
+
208
+ const bandLines = await v4.sendCommandMulti("band list");
209
+ this.bands = parseBandList(bandLines);
210
+ this.updatesEventEmitter.emit("bands");
211
+
212
+ await v4.sendCommand("sub port all");
213
+
214
+ v4.onStatus((msg) => {
215
+ if (msg.startsWith("port ")) {
216
+ const portData = parsePort(msg);
217
+ if (portData.portNum === 1) {
218
+ this.status.portA_band = portData.band;
219
+ this.status.portA_antenna = portData.rxant;
220
+ this.status.portA_tx = portData.tx;
221
+ this.status.portA_inhibit = portData.inhibit;
222
+ } else if (portData.portNum === 2) {
223
+ this.status.portB_band = portData.band;
224
+ this.status.portB_antenna = portData.rxant;
225
+ this.status.portB_tx = portData.tx;
226
+ this.status.portB_inhibit = portData.inhibit;
227
+ }
228
+ this.refresh = (this.refresh + 1) % 1500;
229
+ this.updatesEventEmitter.emit("status", this.refresh === 0);
230
+ } else if (msg === "antenna reload") {
231
+ this._reloadV4Antennas();
232
+ }
233
+ });
234
+
235
+ this.updatesEventEmitter.emit("connected");
236
+ // Emit initial status so consumer nodes produce output immediately.
237
+ // In v3 the poll interval does this; in v4 we only get push updates
238
+ // when something changes, so we trigger it manually here.
239
+ this.refresh = 0;
240
+ this.updatesEventEmitter.emit("status", true);
241
+ }
242
+
243
+ async _reloadV4Antennas() {
244
+ if (!this._v4) return;
245
+ try {
246
+ const antennaLines = await this._v4.sendCommandMulti("antenna list");
247
+ this.antennas = parseAntennaList(antennaLines);
248
+ this.updatesEventEmitter.emit("antennas");
249
+ } catch (err) {
250
+ this.warn("Failed to reload antennas: " + err.message);
251
+ }
252
+ }
253
+
254
+ activate(topic) {
255
+ if (this.apiVersion === 4 && this._v4) {
256
+ const [port, antenna] = topic.split(";");
257
+ this._v4
258
+ .sendCommand(`port set ${port} rxant=${antenna} txant=${antenna}`)
259
+ .catch(() => {});
260
+ } else {
261
+ const command = Utils.encode(0, 0, 415, 0, topic);
262
+ this.client.write(command);
263
+ }
264
+ }
265
+
133
266
  connect() {
134
267
  if (this.autoConnect && !this.connected & !this.client.connecting) {
135
268
  this.client.connect(this.port, this.host);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-antenna-genius",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Antenna Genius Control and Monitoring Nodes",
5
5
  "keywords": [
6
6
  "node-red"
@@ -36,8 +36,8 @@
36
36
  "devDependencies": {
37
37
  "auto-dist-tag": "^2.1.1",
38
38
  "jest": "^28.1.3",
39
- "node-red": "^3.0.2",
40
- "node-red-node-test-helper": "^0.3.0"
39
+ "node-red": "^5.0.0",
40
+ "node-red-node-test-helper": "^0.3.6"
41
41
  },
42
42
  "jest": {
43
43
  "verbose": true
@@ -0,0 +1,102 @@
1
+ const net = require("net");
2
+
3
+ var server;
4
+
5
+ const bands = [
6
+ "band 0 name=None freq_start=0.000000 freq_stop=0.000000",
7
+ "band 1 name=160m freq_start=1.600000 freq_stop=2.200000",
8
+ "band 2 name=80m freq_start=3.300000 freq_stop=4.000000",
9
+ "band 3 name=40m freq_start=6.800000 freq_stop=7.400000",
10
+ "band 4 name=30m freq_start=9.900000 freq_stop=10.350000",
11
+ "band 5 name=20m freq_start=13.800000 freq_stop=14.550000",
12
+ "band 6 name=17m freq_start=17.868000 freq_stop=18.368000",
13
+ "band 7 name=15m freq_start=20.800000 freq_stop=21.650000",
14
+ "band 8 name=12m freq_start=24.690000 freq_stop=25.190000",
15
+ "band 9 name=10m freq_start=27.800000 freq_stop=29.900000",
16
+ "band 10 name=6m freq_start=49.800000 freq_stop=54.200000",
17
+ "band 11 name=60m freq_start=5.000000 freq_stop=6.000000",
18
+ "band 12 name=Custom_1 freq_start=0.000000 freq_stop=0.000000",
19
+ "band 13 name=Custom_2 freq_start=0.000000 freq_stop=0.000000",
20
+ "band 14 name=Custom_3 freq_start=0.000000 freq_stop=0.000000",
21
+ "band 15 name=Custom_4 freq_start=0.000000 freq_stop=0.000000",
22
+ ];
23
+
24
+ const antennas = [
25
+ "antenna 1 name=Antenna_1 tx=ffff rx=ffff inband=0000",
26
+ "antenna 2 name=Antenna_2 tx=ffff rx=ffff inband=0000",
27
+ "antenna 3 name=Antenna_3 tx=ffff rx=ffff inband=0000",
28
+ "antenna 4 name=Antenna_4 tx=ffff rx=ffff inband=0000",
29
+ "antenna 5 name=Antenna_5 tx=ffff rx=ffff inband=0000",
30
+ "antenna 6 name=Antenna_6 tx=ffff rx=ffff inband=0000",
31
+ "antenna 7 name=Antenna_7 tx=ffff rx=ffff inband=0000",
32
+ "antenna 8 name=Antenna_8 tx=ffff rx=ffff inband=0000",
33
+ ];
34
+
35
+ const start = () => {
36
+ server = net.createServer();
37
+ server.on("connection", (conn) => {
38
+ let lineBuffer = "";
39
+ let portSubscribed = false;
40
+
41
+ // Send v4 banner immediately on connect
42
+ conn.write("V4.0.22 AG\r\n");
43
+
44
+ const sendResponse = (seq, message) => {
45
+ conn.write(`R${seq}|0|${message}\r\n`);
46
+ };
47
+
48
+ const sendMultiResponse = (seq, lines) => {
49
+ for (const line of lines) {
50
+ conn.write(`R${seq}|0|${line}\r\n`);
51
+ }
52
+ conn.write(`R${seq}|0|\r\n`);
53
+ };
54
+
55
+ conn.on("data", (data) => {
56
+ lineBuffer += data.toString();
57
+ const lines = lineBuffer.split("\n");
58
+ lineBuffer = lines.pop();
59
+
60
+ for (const line of lines) {
61
+ const trimmed = line.replace(/\r$/, "").trim();
62
+ if (!trimmed.startsWith("C")) continue;
63
+
64
+ const pipeIdx = trimmed.indexOf("|");
65
+ const seq = parseInt(trimmed.substring(1, pipeIdx));
66
+ const cmd = trimmed.substring(pipeIdx + 1);
67
+
68
+ if (cmd === "info get") {
69
+ sendResponse(seq, "info v=4.0.22 date=2023-08-22 btl=1.6 hw=2.0 serial=9A-3A-DC name=TestGenius ports=2 antennas=8 mode=master uptime=3600");
70
+ } else if (cmd === "port get 1") {
71
+ sendResponse(seq, "port 1 auto=1 source=AUTO band=1 rxant=2 txant=2 tx=0 inhibit=0");
72
+ } else if (cmd === "port get 2") {
73
+ sendResponse(seq, "port 2 auto=1 source=AUTO band=2 rxant=4 txant=4 tx=0 inhibit=0");
74
+ } else if (cmd === "antenna list") {
75
+ sendMultiResponse(seq, antennas);
76
+ } else if (cmd === "band list") {
77
+ sendMultiResponse(seq, bands);
78
+ } else if (cmd === "sub port all") {
79
+ portSubscribed = true;
80
+ sendResponse(seq, "");
81
+ } else if (cmd.startsWith("port set ")) {
82
+ sendResponse(seq, "");
83
+ } else {
84
+ sendResponse(seq, "");
85
+ }
86
+ }
87
+ });
88
+ });
89
+
90
+ return new Promise((resolve) => {
91
+ server.listen(0, "localhost", () => {
92
+ return resolve(server.address().port);
93
+ });
94
+ });
95
+ };
96
+
97
+ const stop = () => {
98
+ server.unref();
99
+ };
100
+
101
+ exports.start = start;
102
+ exports.stop = stop;
@@ -0,0 +1,172 @@
1
+ const helper = require("node-red-node-test-helper");
2
+ const serverNode = require("../antenna-genius-server.js");
3
+ const testServer = require("./GeniusTestServerV4.js");
4
+
5
+ describe("antenna-genius-server Node (v4)", () => {
6
+ var port;
7
+
8
+ beforeEach(async () => {
9
+ port = await testServer.start();
10
+ });
11
+
12
+ afterEach(async () => {
13
+ testServer.stop();
14
+ helper.unload();
15
+ });
16
+
17
+ it("should be loaded", (done) => {
18
+ let flow = [
19
+ {
20
+ id: "n1",
21
+ type: "antenna-genius-server",
22
+ name: "test name",
23
+ host: "localhost",
24
+ port: port,
25
+ disabledColor: "Black",
26
+ activeColor: "Green",
27
+ selectedColor: "Blue",
28
+ autoConnect: true,
29
+ },
30
+ ];
31
+ helper.load(serverNode, flow, () => {
32
+ let n1 = helper.getNode("n1");
33
+ expect(n1).toBeDefined();
34
+ done();
35
+ });
36
+ });
37
+
38
+ it("can connect and detect v4", (done) => {
39
+ let flow = [
40
+ {
41
+ id: "n1",
42
+ type: "antenna-genius-server",
43
+ name: "test name",
44
+ host: "localhost",
45
+ port: port,
46
+ disabledColor: "Black",
47
+ activeColor: "Green",
48
+ selectedColor: "Blue",
49
+ autoConnect: true,
50
+ },
51
+ ];
52
+ helper.load(serverNode, flow, () => {
53
+ let n1 = helper.getNode("n1");
54
+ n1.updatesEventEmitter.on("connected", () => {
55
+ expect(n1.connected).toBeTruthy();
56
+ expect(n1.apiVersion).toBe(4);
57
+ done();
58
+ });
59
+ n1.connect();
60
+ });
61
+ });
62
+
63
+ it("parses info correctly", (done) => {
64
+ let flow = [
65
+ {
66
+ id: "n1",
67
+ type: "antenna-genius-server",
68
+ name: "test name",
69
+ host: "localhost",
70
+ port: port,
71
+ disabledColor: "Black",
72
+ activeColor: "Green",
73
+ selectedColor: "Blue",
74
+ autoConnect: true,
75
+ },
76
+ ];
77
+ helper.load(serverNode, flow, () => {
78
+ let n1 = helper.getNode("n1");
79
+ n1.updatesEventEmitter.on("connected", () => {
80
+ expect(n1.info.name).toBe("TestGenius");
81
+ expect(n1.info.ports).toBe(2);
82
+ expect(n1.info.antennas).toBe(8);
83
+ done();
84
+ });
85
+ n1.connect();
86
+ });
87
+ });
88
+
89
+ it("parses port status correctly", (done) => {
90
+ let flow = [
91
+ {
92
+ id: "n1",
93
+ type: "antenna-genius-server",
94
+ name: "test name",
95
+ host: "localhost",
96
+ port: port,
97
+ disabledColor: "Black",
98
+ activeColor: "Green",
99
+ selectedColor: "Blue",
100
+ autoConnect: true,
101
+ },
102
+ ];
103
+ helper.load(serverNode, flow, () => {
104
+ let n1 = helper.getNode("n1");
105
+ n1.updatesEventEmitter.on("connected", () => {
106
+ expect(n1.status.portA_band).toBe(1);
107
+ expect(n1.status.portA_antenna).toBe(2);
108
+ expect(n1.status.portB_band).toBe(2);
109
+ expect(n1.status.portB_antenna).toBe(4);
110
+ expect(n1.status.stackReach).toBe(1);
111
+ done();
112
+ });
113
+ n1.connect();
114
+ });
115
+ });
116
+
117
+ it("parses antenna list correctly", (done) => {
118
+ let flow = [
119
+ {
120
+ id: "n1",
121
+ type: "antenna-genius-server",
122
+ name: "test name",
123
+ host: "localhost",
124
+ port: port,
125
+ disabledColor: "Black",
126
+ activeColor: "Green",
127
+ selectedColor: "Blue",
128
+ autoConnect: true,
129
+ },
130
+ ];
131
+ helper.load(serverNode, flow, () => {
132
+ let n1 = helper.getNode("n1");
133
+ n1.updatesEventEmitter.on("connected", () => {
134
+ expect(n1.antennas.length).toBe(8);
135
+ expect(n1.antennas[0].index).toBe(1);
136
+ expect(n1.antennas[0].name).toBe("Antenna 1");
137
+ expect(n1.antennas[0].bands).toStrictEqual(
138
+ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
139
+ );
140
+ done();
141
+ });
142
+ n1.connect();
143
+ });
144
+ });
145
+
146
+ it("parses band list correctly", (done) => {
147
+ let flow = [
148
+ {
149
+ id: "n1",
150
+ type: "antenna-genius-server",
151
+ name: "test name",
152
+ host: "localhost",
153
+ port: port,
154
+ disabledColor: "Black",
155
+ activeColor: "Green",
156
+ selectedColor: "Blue",
157
+ autoConnect: true,
158
+ },
159
+ ];
160
+ helper.load(serverNode, flow, () => {
161
+ let n1 = helper.getNode("n1");
162
+ n1.updatesEventEmitter.on("connected", () => {
163
+ expect(n1.bands.length).toBe(16);
164
+ expect(n1.bands[0].name).toBe("None");
165
+ expect(n1.bands[1].name).toBe("160m");
166
+ expect(n1.bands[5].name).toBe("20m");
167
+ done();
168
+ });
169
+ n1.connect();
170
+ });
171
+ });
172
+ });