node-red-contrib-ax25 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.
Files changed (55) hide show
  1. package/.eslintignore +5 -0
  2. package/.prettierignore +7 -0
  3. package/ARCHITECTURE.md +174 -0
  4. package/CONTEXT.md +90 -0
  5. package/MESSAGES.md +314 -0
  6. package/README.md +317 -0
  7. package/examples/beacons.json +130 -0
  8. package/examples/beacons.png +0 -0
  9. package/examples/bye_subflow.json +107 -0
  10. package/examples/bye_subflow.png +0 -0
  11. package/examples/delete_all_my_messages.json +491 -0
  12. package/examples/delete_all_my_messages.png +0 -0
  13. package/examples/get_message_list_subflow.json +129 -0
  14. package/examples/get_message_list_subflow.png +0 -0
  15. package/examples/send_message_subflow.json +367 -0
  16. package/examples/send_message_subflow.png +0 -0
  17. package/examples/send_test_message.json +643 -0
  18. package/examples/send_test_message.png +0 -0
  19. package/jsconfig.json +37 -0
  20. package/lib/agwpe-client-transport.js +99 -0
  21. package/lib/agwpe-frame-builder.js +176 -0
  22. package/lib/agwpe-frame-pretty.js +107 -0
  23. package/lib/ax25-codec.js +382 -0
  24. package/lib/frame-router.js +95 -0
  25. package/lib/frame-segmentation.js +53 -0
  26. package/lib/message-utils.js +59 -0
  27. package/lib/runtime-store.js +94 -0
  28. package/lib/session-registry.js +142 -0
  29. package/local/buffer_compare.json +135 -0
  30. package/local/debug-d-frame.js +84 -0
  31. package/local/raw-out-test.json +128 -0
  32. package/nodes/agwpe-client.html +70 -0
  33. package/nodes/agwpe-client.js +771 -0
  34. package/nodes/agwpe-client.js.bak +871 -0
  35. package/nodes/connect.html +128 -0
  36. package/nodes/connect.js +450 -0
  37. package/nodes/decode.html +83 -0
  38. package/nodes/decode.js +56 -0
  39. package/nodes/disconnect.html +55 -0
  40. package/nodes/disconnect.js +47 -0
  41. package/nodes/encode.html +117 -0
  42. package/nodes/encode.js +164 -0
  43. package/nodes/monitor-in.html +48 -0
  44. package/nodes/monitor-in.js +42 -0
  45. package/nodes/raw-in.html +50 -0
  46. package/nodes/raw-in.js +72 -0
  47. package/nodes/raw-out.html +76 -0
  48. package/nodes/raw-out.js +144 -0
  49. package/nodes/send.html +91 -0
  50. package/nodes/send.js +373 -0
  51. package/nodes/ui-in.html +64 -0
  52. package/nodes/ui-in.js +68 -0
  53. package/nodes/ui-out.html +80 -0
  54. package/nodes/ui-out.js +133 -0
  55. package/package.json +47 -0
@@ -0,0 +1,64 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("ui-in", {
3
+ category: "agwpe",
4
+ color: "#d4edda",
5
+ defaults: {
6
+ client: { value: "", type: "agwpe-client", required: true },
7
+ name: { value: "" },
8
+ payloadOutput: { value: "string" }
9
+ },
10
+ inputs: 0,
11
+ outputs: 1,
12
+ icon: "font-awesome/fa-rss",
13
+ oneditprepare: function () {
14
+ const mode = this.payloadOutput === "string" ? "string" : "buffer";
15
+ $("#node-input-payloadOutput").val(mode);
16
+ },
17
+ oneditsave: function () {
18
+ const mode = $("#node-input-payloadOutput").val();
19
+ this.payloadOutput = mode === "string" ? "string" : "buffer";
20
+ },
21
+ label: function () {
22
+ return this.name || "ui-in";
23
+ }
24
+ });
25
+ </script>
26
+
27
+ <script type="text/html" data-template-name="ui-in">
28
+ <div class="form-row">
29
+ <label for="node-input-client"><i class="fa fa-server"></i> Client</label>
30
+ <input type="text" id="node-input-client" />
31
+ </div>
32
+ <div class="form-row">
33
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
34
+ <input type="text" id="node-input-name" />
35
+ </div>
36
+ <div class="form-row">
37
+ <label for="node-input-payloadOutput"><i class="fa fa-file-text-o"></i> Payload Output</label>
38
+ <select id="node-input-payloadOutput">
39
+ <option value="buffer">Buffer</option>
40
+ <option value="string">String (UTF-8)</option>
41
+ </select>
42
+ </div>
43
+ </script>
44
+
45
+ <script type="text/html" data-help-name="ui-in">
46
+ <p>Receives AGWPE K-frame traffic and decodes AX.25 UI frames only.</p>
47
+ <p>Requires raw mode enabled on the AGWPE client node.</p>
48
+
49
+ <h3>Output</h3>
50
+ <dl class="message-properties">
51
+ <dt>status <span class="property-type">string</span></dt>
52
+ <dd>Always <code>"ok"</code>. Non-UI frames are silently dropped.</dd>
53
+ <dt>event <span class="property-type">string</span></dt>
54
+ <dd>Always <code>"ui"</code>.</dd>
55
+ <dt>source <span class="property-type">string</span></dt>
56
+ <dd>Source callsign.</dd>
57
+ <dt>destination <span class="property-type">string</span></dt>
58
+ <dd>Destination callsign.</dd>
59
+ <dt class="optional">via <span class="property-type">array</span></dt>
60
+ <dd>Decoded digipeater objects, if present.</dd>
61
+ <dt>payload <span class="property-type">Buffer | string</span></dt>
62
+ <dd>Frame information field. Buffer or UTF-8 string depending on Payload Output setting.</dd>
63
+ </dl>
64
+ </script>
package/nodes/ui-in.js ADDED
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+
3
+ const codec = require("../lib/ax25-codec");
4
+ const { okEnvelope } = require("../lib/message-utils");
5
+
6
+ module.exports = function (RED) {
7
+ function UiInNode(config) {
8
+ RED.nodes.createNode(this, config);
9
+ const node = this;
10
+ node.payloadOutput = config.payloadOutput === "buffer" ? "buffer" : "string";
11
+
12
+ const cfg = RED.nodes.getNode(config.client);
13
+ const context = cfg ? cfg.instance : null;
14
+ if (!context) {
15
+ node.status({ fill: "red", shape: "ring", text: "client missing" });
16
+ return;
17
+ }
18
+
19
+ const onRawData = function (frame) {
20
+ if (!context.rawEnabled || !frame || !Buffer.isBuffer(frame.payload)) {
21
+ return;
22
+ }
23
+
24
+ // Some AGWPE implementations prepend a one-byte port prefix to K payloads.
25
+ const rawPayload =
26
+ frame.payload.length > 1 && frame.payload[0] === 0x00 && frame.payload[1] >= 0x60
27
+ ? frame.payload.subarray(1)
28
+ : frame.payload;
29
+
30
+ let decoded;
31
+ try {
32
+ decoded = codec.decodeWireAx25(rawPayload);
33
+ } catch (_error) {
34
+ return;
35
+ }
36
+
37
+ // UI frame control field: 0x03 with optional P/F bit (0x13).
38
+ if ((decoded.control & 0xef) !== 0x03) {
39
+ return;
40
+ }
41
+
42
+ const payload =
43
+ node.payloadOutput === "string" && Buffer.isBuffer(decoded.payload)
44
+ ? decoded.payload.toString("utf8")
45
+ : decoded.payload;
46
+
47
+ node.send(
48
+ okEnvelope({
49
+ instanceId: context.instanceId,
50
+ event: "ui",
51
+ source: decoded.source,
52
+ destination: decoded.destination,
53
+ via: decoded.via,
54
+ payload
55
+ })
56
+ );
57
+ };
58
+
59
+ context.bus.on("raw-data", onRawData);
60
+
61
+ node.on("close", function (removed, done) {
62
+ context.bus.off("raw-data", onRawData);
63
+ done();
64
+ });
65
+ }
66
+
67
+ RED.nodes.registerType("ui-in", UiInNode);
68
+ };
@@ -0,0 +1,80 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("ui-out", {
3
+ category: "agwpe",
4
+ color: "#d4edda",
5
+ defaults: {
6
+ client: { value: "", type: "agwpe-client", required: true },
7
+ name: { value: "" },
8
+ source: { value: "" },
9
+ destination: { value: "" },
10
+ via: { value: "" },
11
+ payload: { value: "" }
12
+ },
13
+ inputs: 1,
14
+ outputs: 1,
15
+ icon: "font-awesome/fa-broadcast-tower",
16
+ label: function () {
17
+ return this.name || "ui-out";
18
+ }
19
+ });
20
+ </script>
21
+
22
+ <script type="text/html" data-template-name="ui-out">
23
+ <div class="form-row">
24
+ <label for="node-input-client"><i class="fa fa-server"></i> Client</label>
25
+ <input type="text" id="node-input-client" />
26
+ </div>
27
+ <div class="form-row">
28
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
29
+ <input type="text" id="node-input-name" />
30
+ </div>
31
+ <div class="form-row">
32
+ <label for="node-input-source"><i class="fa fa-sign-in"></i> Source <span style="color:#d00">*</span></label>
33
+ <input type="text" id="node-input-source" />
34
+ </div>
35
+ <div class="form-row">
36
+ <label for="node-input-destination"><i class="fa fa-sign-out"></i> Destination <span style="color:#d00">*</span></label>
37
+ <input type="text" id="node-input-destination" />
38
+ </div>
39
+ <div class="form-row">
40
+ <label for="node-input-via"><i class="fa fa-random"></i> Via</label>
41
+ <input type="text" id="node-input-via" placeholder="WIDE1-1, WIDE2-1" />
42
+ </div>
43
+ <div class="form-row">
44
+ <label for="node-input-payload"><i class="fa fa-file-text-o"></i> Payload <span style="color:#d00">*</span></label>
45
+ <input type="text" id="node-input-payload" />
46
+ </div>
47
+ </script>
48
+
49
+ <script type="text/html" data-help-name="ui-out">
50
+ <p>Encodes AX.25 UI frames (control=0x03, pid=0xF0) and sends them via AGWPE K frames.</p>
51
+ <p>Requires raw mode enabled on the AGWPE client node.</p>
52
+
53
+ <h3>Input</h3>
54
+ <dl class="message-properties">
55
+ <dt>source <span class="property-type">string</span></dt>
56
+ <dd>Sending callsign (e.g. <code>"N0CALL-1"</code>). Overrides editor value.</dd>
57
+ <dt>destination <span class="property-type">string</span></dt>
58
+ <dd>Destination callsign (e.g. <code>"APRS"</code>). Overrides editor value.</dd>
59
+ <dt class="optional">via <span class="property-type">string | array</span></dt>
60
+ <dd>Digipeater path. Overrides editor value. A space- or comma-separated string
61
+ (e.g. <code>"WIDE1-1 WIDE2-1"</code>) or an array of callsign strings
62
+ (e.g. <code>["WIDE1-1", "WIDE2-1"]</code>). Omit or leave empty for no digipeating.</dd>
63
+ <dt>payload <span class="property-type">string | Buffer</span></dt>
64
+ <dd>Frame information field to transmit. A string is encoded as UTF-8 bytes before
65
+ being placed in the AX.25 information field. A Buffer is used as-is. Overrides editor value.</dd>
66
+ </dl>
67
+ <p><strong>Required:</strong> source, destination, and payload must each be set in the editor or provided in the input message.</p>
68
+
69
+ <h3>Output</h3>
70
+ <dl class="message-properties">
71
+ <dt>status <span class="property-type">string</span></dt>
72
+ <dd><code>"ok"</code> on success; <code>"error"</code> if the send fails. Check this field first.</dd>
73
+ <dt>event <span class="property-type">string</span></dt>
74
+ <dd>When <code>status</code> is <code>"ok"</code>: always <code>"ui-sent"</code>.</dd>
75
+ <dt class="optional">errorCode <span class="property-type">string</span></dt>
76
+ <dd>When <code>status</code> is <code>"error"</code>: <code>CLIENT_NOT_FOUND</code>, <code>RAW_MODE_DISABLED</code>, or <code>UI_SEND_INVALID</code>.</dd>
77
+ <dt class="optional">errorText <span class="property-type">string</span></dt>
78
+ <dd>When <code>status</code> is <code>"error"</code>: human-readable description of the error.</dd>
79
+ </dl>
80
+ </script>
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+
3
+ const codec = require("../lib/ax25-codec");
4
+ const { okEnvelope, errorEnvelope } = require("../lib/message-utils");
5
+
6
+ function splitCallsignList(value) {
7
+ return String(value || "")
8
+ .split(/[\s,]+/)
9
+ .map(function (item) {
10
+ return item.trim();
11
+ })
12
+ .filter(Boolean);
13
+ }
14
+
15
+ function normalizeViaInput(value) {
16
+ if (value === undefined || value === null || value === "") {
17
+ return undefined;
18
+ }
19
+
20
+ if (typeof value === "string") {
21
+ return splitCallsignList(value);
22
+ }
23
+
24
+ if (!Array.isArray(value)) {
25
+ return value;
26
+ }
27
+
28
+ const expanded = [];
29
+ value.forEach(function (entry) {
30
+ if (typeof entry === "string") {
31
+ splitCallsignList(entry).forEach(function (callsign) {
32
+ expanded.push(callsign);
33
+ });
34
+ return;
35
+ }
36
+
37
+ if (entry && typeof entry === "object" && typeof entry.callsign === "string") {
38
+ splitCallsignList(entry.callsign).forEach(function (callsign) {
39
+ expanded.push({
40
+ callsign,
41
+ hasBeenRepeated: Boolean(entry.hasBeenRepeated)
42
+ });
43
+ });
44
+ return;
45
+ }
46
+
47
+ expanded.push(entry);
48
+ });
49
+
50
+ return expanded;
51
+ }
52
+
53
+ function pickValue(msgValue, configValue) {
54
+ return msgValue !== undefined ? msgValue : configValue;
55
+ }
56
+
57
+ module.exports = function (RED) {
58
+ function UiOutNode(config) {
59
+ RED.nodes.createNode(this, config);
60
+ const node = this;
61
+
62
+ const cfg = RED.nodes.getNode(config.client);
63
+ const context0 = cfg ? cfg.instance : null;
64
+
65
+ node.defaults = {
66
+ source: config.source !== undefined ? config.source : "",
67
+ destination: config.destination !== undefined ? config.destination : "",
68
+ via: config.via !== undefined ? config.via : "",
69
+ payload: config.payload !== undefined ? config.payload : ""
70
+ };
71
+
72
+ node.on("input", function (msg, send, done) {
73
+ const localSend = send || function (m) {
74
+ node.send(m);
75
+ };
76
+ const localDone = done || function () {};
77
+
78
+ const context = context0;
79
+ if (!context) {
80
+ localSend(errorEnvelope("CLIENT_NOT_FOUND", "AGWPE Client instance not found"));
81
+ localDone();
82
+ return;
83
+ }
84
+
85
+ if (!context.rawEnabled) {
86
+ localSend(errorEnvelope("RAW_MODE_DISABLED", "Raw mode is disabled"));
87
+ localDone();
88
+ return;
89
+ }
90
+
91
+ const source = pickValue(msg.source, node.defaults.source);
92
+ const destination = pickValue(msg.destination, node.defaults.destination);
93
+ const via = normalizeViaInput(pickValue(msg.via, node.defaults.via));
94
+ const payloadInput = pickValue(msg.payload, node.defaults.payload);
95
+
96
+ if (!source || !destination || payloadInput === undefined || payloadInput === null || source === "" || destination === "" || payloadInput === "") {
97
+ localSend(errorEnvelope("UI_SEND_INVALID", "ui-out requires source, destination, and payload (set in editor or input message)"));
98
+ localDone();
99
+ return;
100
+ }
101
+
102
+ let payload;
103
+ try {
104
+ payload = codec.encode({
105
+ source,
106
+ destination,
107
+ via,
108
+ control: 0x03,
109
+ pid: 0xf0,
110
+ payload: payloadInput
111
+ });
112
+ } catch (error) {
113
+ localSend(errorEnvelope("UI_SEND_INVALID", error.message));
114
+ localDone();
115
+ return;
116
+ }
117
+
118
+ context.bus.emit("raw-data", {
119
+ instanceId: context.instanceId,
120
+ direction: "tx",
121
+ source,
122
+ destination,
123
+ payload,
124
+ agwpePort: msg.agwpePort
125
+ });
126
+
127
+ localSend(okEnvelope({ instanceId: context.instanceId, event: "ui-sent" }));
128
+ localDone();
129
+ });
130
+ }
131
+
132
+ RED.nodes.registerType("ui-out", UiOutNode);
133
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "node-red-contrib-ax25",
3
+ "version": "1.0.0",
4
+ "description": "Node-RED nodes for AX.25 connectivity and tooling",
5
+ "main": "nodes/agwpe-client.js",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "node-red",
9
+ "agwpe",
10
+ "ax25",
11
+ "aprs",
12
+ "packet-radio"
13
+ ],
14
+ "author": {
15
+ "name": "Robert Ambrose",
16
+ "email": "npm@muttsoft.com",
17
+ "url": "https://github.com/n7get/node-red-contrib-ax25"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "scripts": {
23
+ "test": "mocha --require test/setup.js 'test/**/*.spec.js'",
24
+ "lint": "node -e \"console.log('lint placeholder')\""
25
+ },
26
+ "dependencies": {},
27
+ "devDependencies": {
28
+ "mocha": "^10.7.3",
29
+ "node-red": "^3.1.9",
30
+ "node-red-node-test-helper": "^0.3.3"
31
+ },
32
+ "node-red": {
33
+ "nodes": {
34
+ "agwpe-client": "nodes/agwpe-client.js",
35
+ "connect": "nodes/connect.js",
36
+ "send": "nodes/send.js",
37
+ "disconnect": "nodes/disconnect.js",
38
+ "ui-in": "nodes/ui-in.js",
39
+ "ui-out": "nodes/ui-out.js",
40
+ "monitor-in": "nodes/monitor-in.js",
41
+ "raw-in": "nodes/raw-in.js",
42
+ "raw-out": "nodes/raw-out.js",
43
+ "decode": "nodes/decode.js",
44
+ "encode": "nodes/encode.js"
45
+ }
46
+ }
47
+ }