node-red-contrib-antenna-genius 1.0.1 → 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.
- package/.github/workflows/codeql-analysis.yml +4 -4
- package/.github/workflows/njsscan-analysis.yml +2 -2
- package/.github/workflows/node.js.yml +3 -3
- package/.github/workflows/npm-publish.yml +12 -8
- package/GeniusV4Protocol.js +162 -0
- package/README.md +3 -0
- package/SECURITY.md +8 -0
- package/antenna-genius-activate-antenna.js +1 -5
- package/antenna-genius-server.js +186 -52
- package/package.json +4 -4
- package/test/GeniusTestServerV4.js +102 -0
- package/test/antenna-genius-server-v4.spec.js +172 -0
|
@@ -38,11 +38,11 @@ jobs:
|
|
|
38
38
|
|
|
39
39
|
steps:
|
|
40
40
|
- name: Checkout repository
|
|
41
|
-
uses: actions/checkout@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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: [
|
|
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@
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
26
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
27
|
-
uses: actions/setup-node@
|
|
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@
|
|
15
|
-
- uses: actions/setup-node@
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
16
|
with:
|
|
17
|
-
node-version:
|
|
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@
|
|
26
|
-
- uses: actions/setup-node@
|
|
30
|
+
- uses: actions/checkout@v6
|
|
31
|
+
- uses: actions/setup-node@v6
|
|
27
32
|
with:
|
|
28
|
-
node-version:
|
|
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
|
[](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/node.js.yml) [](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/npm-publish.yml) [](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/codeql-analysis.yml) [](https://github.com/sm6srw/node-red-contrib-antenna-genius/actions/workflows/njsscan-analysis.yml)
|
|
5
7
|
|
|
6
8
|
 
|
|
@@ -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
|
+
|
package/SECURITY.md
ADDED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
this.server.client.write(command);
|
|
48
|
+
this.server.activate(msg.topic);
|
|
53
49
|
} else {
|
|
54
50
|
this.status({
|
|
55
51
|
fill: "red",
|
package/antenna-genius-server.js
CHANGED
|
@@ -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);
|
|
@@ -34,6 +38,13 @@ module.exports = (RED) => {
|
|
|
34
38
|
this.log("TCP connection disconnected with the server.");
|
|
35
39
|
clearInterval(this.interval);
|
|
36
40
|
clearTimeout(this.timer);
|
|
41
|
+
this.connected = false;
|
|
42
|
+
|
|
43
|
+
if (this._v4DataHandler) {
|
|
44
|
+
this.client.removeListener("data", this._v4DataHandler);
|
|
45
|
+
this._v4DataHandler = null;
|
|
46
|
+
}
|
|
47
|
+
this._v4 = null;
|
|
37
48
|
|
|
38
49
|
if (this.updatesEventEmitter.listenerCount("closed") == 0) {
|
|
39
50
|
this.log(
|
|
@@ -58,60 +69,33 @@ module.exports = (RED) => {
|
|
|
58
69
|
this.log("TCP connection established with the server.");
|
|
59
70
|
this.connected = true;
|
|
60
71
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let command = Utils.encode(0, 0, 412, 0, index.toString());
|
|
76
|
-
await promiseClient.write(command);
|
|
77
|
-
let packet = await promiseClient.read();
|
|
78
|
-
let data = Utils.decode(packet);
|
|
79
|
-
this.antennas.push(data);
|
|
80
|
-
}
|
|
81
|
-
this.updatesEventEmitter.emit("antennas");
|
|
82
|
-
|
|
83
|
-
for (let index = 0; index < 16; ++index) {
|
|
84
|
-
let command = Utils.encode(0, 0, 82, 0, index.toString());
|
|
85
|
-
await promiseClient.write(command);
|
|
86
|
-
let packet = await promiseClient.read();
|
|
87
|
-
let data = Utils.decode(packet);
|
|
88
|
-
this.bands.push(data);
|
|
89
|
-
}
|
|
90
|
-
this.updatesEventEmitter.emit("bands");
|
|
91
|
-
|
|
92
|
-
this.client.on("data", (packet) => {
|
|
93
|
-
let decoded = Utils.decode(packet);
|
|
94
|
-
if (decoded.command == 401) {
|
|
95
|
-
this.status = {
|
|
96
|
-
...this.status,
|
|
97
|
-
...Utils.decode(packet),
|
|
98
|
-
};
|
|
99
|
-
this.refresh = (this.refresh + 1) % 1500;
|
|
100
|
-
this.updatesEventEmitter.emit(
|
|
101
|
-
"status",
|
|
102
|
-
this.refresh == 0
|
|
103
|
-
);
|
|
104
|
-
}
|
|
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);
|
|
105
86
|
});
|
|
106
87
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}
|
|
115
99
|
});
|
|
116
100
|
|
|
117
101
|
this.client.on("error", (err) => {
|
|
@@ -123,12 +107,162 @@ module.exports = (RED) => {
|
|
|
123
107
|
clearTimeout(this.timer);
|
|
124
108
|
this.autoConnect = false;
|
|
125
109
|
this.connected = false;
|
|
110
|
+
if (this._v4DataHandler) {
|
|
111
|
+
this.client.removeListener("data", this._v4DataHandler);
|
|
112
|
+
this._v4DataHandler = null;
|
|
113
|
+
}
|
|
114
|
+
this._v4 = null;
|
|
126
115
|
this.client.end();
|
|
127
116
|
this.forceUpdate();
|
|
128
117
|
done();
|
|
129
118
|
});
|
|
130
119
|
}
|
|
131
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
|
+
|
|
132
266
|
connect() {
|
|
133
267
|
if (this.autoConnect && !this.connected & !this.client.connecting) {
|
|
134
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": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Antenna Genius Control and Monitoring Nodes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red"
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"auto-dist-tag": "^2.1.1",
|
|
38
|
-
"jest": "^
|
|
39
|
-
"node-red": "^
|
|
40
|
-
"node-red-node-test-helper": "^0.
|
|
38
|
+
"jest": "^28.1.3",
|
|
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
|
+
});
|