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,56 @@
1
+ "use strict";
2
+
3
+ const codec = require("../lib/ax25-codec");
4
+ const { okEnvelope, errorEnvelope } = require("../lib/message-utils");
5
+
6
+ module.exports = function (RED) {
7
+ function DecodeNode(config) {
8
+ RED.nodes.createNode(this, config);
9
+ const node = this;
10
+ node.payloadOutput = config.payloadOutput === "buffer" ? "buffer" : "string";
11
+
12
+ function normalizeAgwpePort(value) {
13
+ if (Buffer.isBuffer(value)) {
14
+ return value.length > 0 ? value.readUInt8(0) : 0;
15
+ }
16
+ const numeric = Number(value);
17
+ if (Number.isInteger(numeric) && numeric >= 0 && numeric <= 255) {
18
+ return numeric;
19
+ }
20
+ return 0;
21
+ }
22
+
23
+ node.on("input", function (msg, send, done) {
24
+ const localSend = send || function (m) {
25
+ node.send(m);
26
+ };
27
+ const localDone = done || function () {};
28
+
29
+ if (!Buffer.isBuffer(msg.payload)) {
30
+ localSend(errorEnvelope("DECODE_INPUT_INVALID", "payload must be Buffer"));
31
+ localDone();
32
+ return;
33
+ }
34
+
35
+ try {
36
+ const parsed = codec.decode(msg.payload);
37
+ const rawPort = msg.agwpePort !== undefined
38
+ ? msg.agwpePort
39
+ : (msg.agwpePrefix !== undefined ? msg.agwpePrefix : 0);
40
+ const out = Object.assign(
41
+ { agwpePort: normalizeAgwpePort(rawPort) },
42
+ parsed
43
+ );
44
+ if (node.payloadOutput === "string" && Buffer.isBuffer(out.payload)) {
45
+ out.payload = out.payload.toString("utf8");
46
+ }
47
+ localSend(okEnvelope(out));
48
+ } catch (error) {
49
+ localSend(errorEnvelope("DECODE_FAILED", error.message));
50
+ }
51
+ localDone();
52
+ });
53
+ }
54
+
55
+ RED.nodes.registerType("decode", DecodeNode);
56
+ };
@@ -0,0 +1,55 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("disconnect", {
3
+ category: "agwpe",
4
+ color: "#d4edda",
5
+ defaults: {
6
+ name: { value: "" }
7
+ },
8
+ inputs: 1,
9
+ outputs: 1,
10
+ outputLabels: ["events"],
11
+ icon: "font-awesome/fa-sign-out",
12
+ label: function () {
13
+ return this.name || "disconnect";
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="disconnect">
19
+ <div class="form-row">
20
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
21
+ <input type="text" id="node-input-name" />
22
+ </div>
23
+ </script>
24
+
25
+ <script type="text/html" data-help-name="disconnect">
26
+ <p>Initiates a graceful disconnect for an AX.25 connected session.</p>
27
+
28
+ <h3>Input</h3>
29
+ <dl class="message-properties">
30
+ <dt>sessionId <span class="property-type">string</span></dt>
31
+ <dd>Session to disconnect.</dd>
32
+ </dl>
33
+
34
+ <h3>Outputs</h3>
35
+ <ol class="node-ports">
36
+ <li>Events
37
+ <dl class="message-properties">
38
+ <dt>status <span class="property-type">string</span></dt>
39
+ <dd><code>"ok"</code> on success; <code>"error"</code> on error.</dd>
40
+ <dt>event <span class="property-type">string</span></dt>
41
+ <dd>When <code>status</code> is <code>"ok"</code>: <code>disconnecting</code>.</dd>
42
+ <dt>sessionId <span class="property-type">string</span></dt>
43
+ <dd>Identifies the session.</dd>
44
+ <dt class="optional">errorCode <span class="property-type">string</span></dt>
45
+ <dd>When <code>status</code> is <code>"error"</code>: <code>SESSION_NOT_FOUND</code>.</dd>
46
+ <dt class="optional">errorText <span class="property-type">string</span></dt>
47
+ <dd>When <code>status</code> is <code>"error"</code>: human-readable description of the error.</dd>
48
+ </dl>
49
+ </li>
50
+ </ol>
51
+
52
+ <h3>Details</h3>
53
+ <p>Emits <code>disconnecting</code> immediately. The <code>disconnected</code> event fires on the
54
+ originating <b>Connect</b> node's event output once the TNC confirms with a <code>d</code> frame.</p>
55
+ </script>
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ const { okEnvelope, errorEnvelope } = require("../lib/message-utils");
4
+ const store = require("../lib/runtime-store");
5
+
6
+ module.exports = function (RED) {
7
+ function DisconnectNode(config) {
8
+ RED.nodes.createNode(this, config);
9
+ const node = this;
10
+
11
+ node.status({});
12
+
13
+ node.on("input", function (msg, send, done) {
14
+ const localSend = send || function (m) { node.send(m); };
15
+ const localDone = done || function () {};
16
+
17
+ const iid = store.instanceIdForSession(msg.sessionId);
18
+ const ctx = iid ? store.getInstance(iid) : null;
19
+ const session = ctx ? ctx.registry.get(ctx.instanceId, msg.sessionId) : null;
20
+ if (!session) {
21
+ localSend(errorEnvelope("SESSION_NOT_FOUND", "Session not found", { sessionId: msg.sessionId }));
22
+ localDone();
23
+ return;
24
+ }
25
+
26
+ ctx.registry.update(ctx.instanceId, msg.sessionId, { state: "disconnecting" });
27
+ ctx.bus.emit("conn-data", {
28
+ event: "disconnect",
29
+ direction: "tx",
30
+ instanceId: ctx.instanceId,
31
+ sessionId: msg.sessionId,
32
+ source: session.source || session.sourceCallsign,
33
+ destination: session.destination || session.destinationCallsign
34
+ });
35
+ ctx.bus.emit("conn-lifecycle", {
36
+ event: "disconnecting",
37
+ instanceId: ctx.instanceId,
38
+ sessionId: msg.sessionId
39
+ });
40
+ // Session removal and "disconnected" event fire when the TNC confirms with a d frame.
41
+ localSend(okEnvelope({ instanceId: ctx.instanceId, event: "disconnecting", sessionId: msg.sessionId }));
42
+ localDone();
43
+ });
44
+ }
45
+
46
+ RED.nodes.registerType("disconnect", DisconnectNode);
47
+ };
@@ -0,0 +1,117 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("encode", {
3
+ category: "agwpe",
4
+ color: "#d4edda",
5
+ defaults: {
6
+ name: { value: "" },
7
+ source: { value: "" },
8
+ destination: { value: "" },
9
+ via: { value: "" },
10
+ control: { value: 3 },
11
+ pid: { value: 240 },
12
+ frameType: { value: "U" },
13
+ payload: { value: "" }
14
+ },
15
+ inputs: 1,
16
+ outputs: 1,
17
+ icon: "font-awesome/fa-compress",
18
+ label: function () {
19
+ return this.name || "encode";
20
+ }
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="encode">
25
+ <div class="form-row">
26
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
+ <input type="text" id="node-input-name" />
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-input-source"><i class="fa fa-sign-in"></i> Source</label>
31
+ <input type="text" id="node-input-source" />
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-destination"><i class="fa fa-sign-out"></i> Destination</label>
35
+ <input type="text" id="node-input-destination" />
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-via"><i class="fa fa-random"></i> Via</label>
39
+ <input type="text" id="node-input-via" placeholder="WIDE1-1, WIDE2-1" />
40
+ </div>
41
+ <div class="form-row">
42
+ <label for="node-input-control"><i class="fa fa-sliders"></i> Control</label>
43
+ <input type="number" id="node-input-control" min="0" max="255" />
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-pid"><i class="fa fa-hashtag"></i> PID</label>
47
+ <input type="number" id="node-input-pid" min="0" max="255" />
48
+ </div>
49
+ <div class="form-row">
50
+ <label for="node-input-frameType"><i class="fa fa-filter"></i> Frame Type</label>
51
+ <select id="node-input-frameType">
52
+ <option value="I">I</option>
53
+ <option value="S">S</option>
54
+ <option value="U">U</option>
55
+ </select>
56
+ </div>
57
+ <div class="form-row">
58
+ <label for="node-input-payload"><i class="fa fa-file-text-o"></i> Payload</label>
59
+ <input type="text" id="node-input-payload" />
60
+ </div>
61
+ </script>
62
+
63
+ <script type="text/html" data-help-name="encode">
64
+ <p>Encodes structured AX.25 frame fields into raw bytes. Editor values are defaults;
65
+ incoming message properties override them when present.</p>
66
+
67
+ <h3>Input</h3>
68
+ <dl class="message-properties">
69
+ <dt>source <span class="property-type">string</span></dt>
70
+ <dd>Sending callsign (e.g. <code>"N0CALL-1"</code>). Overrides editor value.</dd>
71
+ <dt>destination <span class="property-type">string</span></dt>
72
+ <dd>Destination callsign (e.g. <code>"CQ"</code>). Overrides editor value.</dd>
73
+ <dt class="optional">via <span class="property-type">string | array</span></dt>
74
+ <dd>Digipeater path. A space- or comma-separated string (e.g. <code>"WIDE1-1 WIDE2-1"</code>)
75
+ or an array of callsign strings or <code>{callsign, hasBeenRepeated}</code> objects.
76
+ Overrides editor value. Omit or leave empty for no digipeaters.</dd>
77
+ <dt class="optional">control <span class="property-type">number</span></dt>
78
+ <dd>Raw control field byte (0–255). Takes precedence over <code>frameType</code> when both
79
+ are provided. Overrides editor value.</dd>
80
+ <dt class="optional">frameType <span class="property-type">string</span></dt>
81
+ <dd>Convenience shorthand for <code>control</code> when an exact byte is not needed.
82
+ <code>"I"</code> → <code>0x00</code>, <code>"S"</code> → <code>0x01</code>,
83
+ <code>"U"</code> → <code>0x03</code>. Ignored when <code>control</code> is also set.
84
+ Overrides editor value.</dd>
85
+ <dt class="optional">pid <span class="property-type">number</span></dt>
86
+ <dd>Protocol identifier byte (0–255). Defaults to <code>240</code> (<code>0xF0</code> — no layer 3).
87
+ Overrides editor value.</dd>
88
+ <dt class="optional">payload <span class="property-type">string | Buffer</span></dt>
89
+ <dd>Frame information field content. A string is encoded as UTF-8 bytes; a Buffer is used
90
+ as-is. Overrides editor value.</dd>
91
+ <dt class="optional">agwpePort <span class="property-type">number</span></dt>
92
+ <dd>AGWPE port byte. Passed through unchanged to the output message so the result can be
93
+ piped directly into <b>raw-out</b> without modification.</dd>
94
+ </dl>
95
+
96
+ <h3>Output</h3>
97
+ <dl class="message-properties">
98
+ <dt>status <span class="property-type">string</span></dt>
99
+ <dd><code>"ok"</code> on success; <code>"error"</code> if encoding fails. Check this field first.</dd>
100
+ <dt>event <span class="property-type">string</span></dt>
101
+ <dd>When <code>status</code> is <code>"ok"</code>: always <code>"encoded"</code>.</dd>
102
+ <dt>payload <span class="property-type">Buffer</span></dt>
103
+ <dd>When <code>status</code> is <code>"ok"</code>: the complete encoded AX.25 frame as raw bytes.
104
+ Can be passed directly to <b>raw-out</b> — it recognises the encode envelope and extracts
105
+ the nested payload automatically.</dd>
106
+ <dt class="optional">agwpePort <span class="property-type">number | null</span></dt>
107
+ <dd>When <code>status</code> is <code>"ok"</code>: AGWPE port byte passed through from the input,
108
+ or <code>null</code> if neither <code>msg.agwpePort</code> nor <code>msg.agwpePrefix</code>
109
+ was present.</dd>
110
+ <dt class="optional">errorCode <span class="property-type">string</span></dt>
111
+ <dd>When <code>status</code> is <code>"error"</code>: <code>ENCODE_INPUT_INVALID</code>
112
+ (source, destination, or control/frameType missing) or <code>ENCODE_FAILED</code>
113
+ (codec error during encoding).</dd>
114
+ <dt class="optional">errorText <span class="property-type">string</span></dt>
115
+ <dd>When <code>status</code> is <code>"error"</code>: human-readable description of the error.</dd>
116
+ </dl>
117
+ </script>
@@ -0,0 +1,164 @@
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
+ function parseByte(value) {
58
+ if (value === undefined || value === null || value === "") {
59
+ return undefined;
60
+ }
61
+
62
+ const numeric = typeof value === "number" ? value : Number(value);
63
+ if (!Number.isInteger(numeric) || numeric < 0 || numeric > 255) {
64
+ throw new Error("ENCODE_INVALID_BYTE");
65
+ }
66
+ return numeric;
67
+ }
68
+
69
+ function controlFromFrameType(frameType) {
70
+ const type = String(frameType || "").trim().toUpperCase();
71
+ if (!type) {
72
+ return undefined;
73
+ }
74
+ if (type === "I") {
75
+ return 0x00;
76
+ }
77
+ if (type === "S") {
78
+ return 0x01;
79
+ }
80
+ if (type === "U") {
81
+ return 0x03;
82
+ }
83
+ throw new Error("ENCODE_INVALID_FRAME_TYPE");
84
+ }
85
+
86
+ module.exports = function (RED) {
87
+ function EncodeNode(config) {
88
+ RED.nodes.createNode(this, config);
89
+ const node = this;
90
+
91
+ node.defaults = {
92
+ source: config.source !== undefined ? config.source : "N0CALL",
93
+ destination: config.destination !== undefined ? config.destination : "CQ",
94
+ via: config.via !== undefined ? config.via : "",
95
+ control: config.control !== undefined ? config.control : 3,
96
+ pid: config.pid !== undefined ? config.pid : 240,
97
+ frameType: config.frameType !== undefined ? config.frameType : "U",
98
+ payload: config.payload !== undefined ? config.payload : ""
99
+ };
100
+
101
+ node.on("input", function (msg, send, done) {
102
+ const localSend = send || function (m) {
103
+ node.send(m);
104
+ };
105
+ const localDone = done || function () {};
106
+
107
+ try {
108
+ const source = pickValue(msg.source, node.defaults.source);
109
+ const destination = pickValue(msg.destination, node.defaults.destination);
110
+ const via = normalizeViaInput(
111
+ pickValue(msg.via, node.defaults.via)
112
+ );
113
+ const frameType = pickValue(msg.frameType, node.defaults.frameType);
114
+ const control = parseByte(pickValue(msg.control, node.defaults.control));
115
+ const pid = parseByte(pickValue(msg.pid, node.defaults.pid));
116
+ const payloadInput = pickValue(msg.payload, node.defaults.payload);
117
+ const sourceHasBeenRepeated = pickValue(
118
+ msg.sourceHasBeenRepeated,
119
+ node.defaults.sourceHasBeenRepeated
120
+ );
121
+ const destinationHasBeenRepeated = pickValue(
122
+ msg.destinationHasBeenRepeated,
123
+ node.defaults.destinationHasBeenRepeated
124
+ );
125
+
126
+ const resolvedControl = control !== undefined ? control : controlFromFrameType(frameType);
127
+
128
+ if (!source || !destination || resolvedControl === undefined) {
129
+ localSend(
130
+ errorEnvelope(
131
+ "ENCODE_INPUT_INVALID",
132
+ "source, destination, and control/frameType are required"
133
+ )
134
+ );
135
+ localDone();
136
+ return;
137
+ }
138
+
139
+ const payload = codec.encode({
140
+ source,
141
+ destination,
142
+ sourceHasBeenRepeated,
143
+ destinationHasBeenRepeated,
144
+ via,
145
+ control: resolvedControl,
146
+ pid,
147
+ payload: payloadInput
148
+ });
149
+ localSend(okEnvelope({
150
+ event: "encoded",
151
+ agwpePort: msg.agwpePort !== undefined
152
+ ? msg.agwpePort
153
+ : (msg.agwpePrefix !== undefined ? msg.agwpePrefix : null),
154
+ payload
155
+ }));
156
+ } catch (error) {
157
+ localSend(errorEnvelope("ENCODE_FAILED", error.message));
158
+ }
159
+ localDone();
160
+ });
161
+ }
162
+
163
+ RED.nodes.registerType("encode", EncodeNode);
164
+ };
@@ -0,0 +1,48 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("monitor-in", {
3
+ category: "agwpe",
4
+ color: "#d4edda",
5
+ defaults: {
6
+ client: { value: "", type: "agwpe-client", required: true },
7
+ name: { value: "" }
8
+ },
9
+ inputs: 0,
10
+ outputs: 1,
11
+ icon: "font-awesome/fa-eye",
12
+ label: function () {
13
+ return this.name || "monitor-in";
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="monitor-in">
19
+ <div class="form-row">
20
+ <label for="node-input-client"><i class="fa fa-server"></i> Client</label>
21
+ <input type="text" id="node-input-client" />
22
+ </div>
23
+ <div class="form-row">
24
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
25
+ <input type="text" id="node-input-name" />
26
+ </div>
27
+ </script>
28
+
29
+ <script type="text/html" data-help-name="monitor-in">
30
+ <p>Receives passive monitor traffic when monitor mode is enabled on agwpe-client.</p>
31
+ <p>Automatically binds to the AGWPE client node in the flow.</p>
32
+
33
+ <h3>Output</h3>
34
+ <dl class="message-properties">
35
+ <dt>status <span class="property-type">string</span></dt>
36
+ <dd>Always <code>"ok"</code>.</dd>
37
+ <dt>event <span class="property-type">string</span></dt>
38
+ <dd>Always <code>"monitor"</code>.</dd>
39
+ <dt>source <span class="property-type">string</span></dt>
40
+ <dd>Source callsign from the AX.25 header.</dd>
41
+ <dt>destination <span class="property-type">string</span></dt>
42
+ <dd>Destination callsign.</dd>
43
+ <dt>via <span class="property-type">array</span></dt>
44
+ <dd>Decoded digipeater objects (empty array if none).</dd>
45
+ <dt>payload <span class="property-type">Buffer</span></dt>
46
+ <dd>Frame information field bytes.</dd>
47
+ </dl>
48
+ </script>
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ const { okEnvelope } = require("../lib/message-utils");
4
+
5
+ module.exports = function (RED) {
6
+ function MonitorInNode(config) {
7
+ RED.nodes.createNode(this, config);
8
+ const node = this;
9
+
10
+ const cfg = RED.nodes.getNode(config.client);
11
+ const context = cfg ? cfg.instance : null;
12
+ if (!context) {
13
+ node.status({ fill: "red", shape: "ring", text: "client missing" });
14
+ return;
15
+ }
16
+
17
+ const onMonitorData = function (frame) {
18
+ if (!context.monitorEnabled) {
19
+ return;
20
+ }
21
+ node.send(
22
+ okEnvelope({
23
+ instanceId: context.instanceId,
24
+ event: "monitor",
25
+ payload: frame.payload,
26
+ source: frame.source,
27
+ destination: frame.destination,
28
+ via: frame.via || []
29
+ })
30
+ );
31
+ };
32
+
33
+ context.bus.on("monitor-data", onMonitorData);
34
+
35
+ node.on("close", function (removed, done) {
36
+ context.bus.off("monitor-data", onMonitorData);
37
+ done();
38
+ });
39
+ }
40
+
41
+ RED.nodes.registerType("monitor-in", MonitorInNode);
42
+ };
@@ -0,0 +1,50 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("raw-in", {
3
+ category: "agwpe",
4
+ color: "#d4edda",
5
+ defaults: {
6
+ client: { value: "", type: "agwpe-client", required: true },
7
+ name: { value: "" }
8
+ },
9
+ inputs: 0,
10
+ outputs: 1,
11
+ icon: "font-awesome/fa-download",
12
+ label: function () {
13
+ return this.name || "raw-in";
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="raw-in">
19
+ <div class="form-row">
20
+ <label for="node-input-client"><i class="fa fa-server"></i> Client</label>
21
+ <input type="text" id="node-input-client" />
22
+ </div>
23
+ <div class="form-row">
24
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
25
+ <input type="text" id="node-input-name" />
26
+ </div>
27
+ </script>
28
+
29
+ <script type="text/html" data-help-name="raw-in">
30
+ <p>Receives raw AX.25 bytes when raw mode is enabled.</p>
31
+ <p>Automatically binds to the AGWPE client node in the flow.</p>
32
+
33
+ <h3>Output</h3>
34
+ <dl class="message-properties">
35
+ <dt>status <span class="property-type">string</span></dt>
36
+ <dd>Always <code>"ok"</code>.</dd>
37
+ <dt>event <span class="property-type">string</span></dt>
38
+ <dd>Always <code>"raw"</code>.</dd>
39
+ <dt>source <span class="property-type">string</span></dt>
40
+ <dd>Source callsign decoded from the AX.25 header.</dd>
41
+ <dt>destination <span class="property-type">string</span></dt>
42
+ <dd>Destination callsign.</dd>
43
+ <dt>via <span class="property-type">array</span></dt>
44
+ <dd>Decoded digipeater objects from the AX.25 header (empty array if none).</dd>
45
+ <dt>agwpePort <span class="property-type">number</span></dt>
46
+ <dd>AGWPE port byte (0–255).</dd>
47
+ <dt>payload <span class="property-type">Buffer</span></dt>
48
+ <dd>Raw AX.25 frame bytes.</dd>
49
+ </dl>
50
+ </script>
@@ -0,0 +1,72 @@
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 RawInNode(config) {
8
+ RED.nodes.createNode(this, config);
9
+ const node = this;
10
+
11
+ const cfg = RED.nodes.getNode(config.client);
12
+ const context = cfg ? cfg.instance : null;
13
+ if (!context) {
14
+ node.status({ fill: "red", shape: "ring", text: "client missing" });
15
+ return;
16
+ }
17
+
18
+ const onRawData = function (frame) {
19
+ if (!context.rawEnabled) {
20
+ return;
21
+ }
22
+
23
+ function normalizeAgwpePort(value) {
24
+ if (Buffer.isBuffer(value)) {
25
+ return value.length > 0 ? value.readUInt8(0) : 0;
26
+ }
27
+ const numeric = Number(value);
28
+ if (Number.isInteger(numeric) && numeric >= 0 && numeric <= 255) {
29
+ return numeric;
30
+ }
31
+ return 0;
32
+ }
33
+
34
+ // Strip leading AGWPE K-frame pad byte (0x00) that precedes the AX.25
35
+ // address chain in some AGWPE implementations. Payload emitted to
36
+ // downstream nodes is the raw AX.25 wire frame only.
37
+ let rawPayload = frame.payload;
38
+ let agwpePort = normalizeAgwpePort(frame.agwpePort);
39
+ if (rawPayload.length > 1 && rawPayload[0] === 0x00 && rawPayload[1] >= 0x60) {
40
+ agwpePort = rawPayload.readUInt8(0);
41
+ rawPayload = rawPayload.subarray(1);
42
+ }
43
+
44
+ node.send(
45
+ okEnvelope({
46
+ instanceId: context.instanceId,
47
+ event: "raw",
48
+ payload: rawPayload,
49
+ agwpePort,
50
+ source: frame.source,
51
+ destination: frame.destination,
52
+ via: (function () {
53
+ try {
54
+ return codec.decodeWireAx25(rawPayload).via || [];
55
+ } catch (_) {
56
+ return [];
57
+ }
58
+ }())
59
+ })
60
+ );
61
+ };
62
+
63
+ context.bus.on("raw-data", onRawData);
64
+
65
+ node.on("close", function (removed, done) {
66
+ context.bus.off("raw-data", onRawData);
67
+ done();
68
+ });
69
+ }
70
+
71
+ RED.nodes.registerType("raw-in", RawInNode);
72
+ };