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.
- package/.eslintignore +5 -0
- package/.prettierignore +7 -0
- package/ARCHITECTURE.md +174 -0
- package/CONTEXT.md +90 -0
- package/MESSAGES.md +314 -0
- package/README.md +317 -0
- package/examples/beacons.json +130 -0
- package/examples/beacons.png +0 -0
- package/examples/bye_subflow.json +107 -0
- package/examples/bye_subflow.png +0 -0
- package/examples/delete_all_my_messages.json +491 -0
- package/examples/delete_all_my_messages.png +0 -0
- package/examples/get_message_list_subflow.json +129 -0
- package/examples/get_message_list_subflow.png +0 -0
- package/examples/send_message_subflow.json +367 -0
- package/examples/send_message_subflow.png +0 -0
- package/examples/send_test_message.json +643 -0
- package/examples/send_test_message.png +0 -0
- package/jsconfig.json +37 -0
- package/lib/agwpe-client-transport.js +99 -0
- package/lib/agwpe-frame-builder.js +176 -0
- package/lib/agwpe-frame-pretty.js +107 -0
- package/lib/ax25-codec.js +382 -0
- package/lib/frame-router.js +95 -0
- package/lib/frame-segmentation.js +53 -0
- package/lib/message-utils.js +59 -0
- package/lib/runtime-store.js +94 -0
- package/lib/session-registry.js +142 -0
- package/local/buffer_compare.json +135 -0
- package/local/debug-d-frame.js +84 -0
- package/local/raw-out-test.json +128 -0
- package/nodes/agwpe-client.html +70 -0
- package/nodes/agwpe-client.js +771 -0
- package/nodes/agwpe-client.js.bak +871 -0
- package/nodes/connect.html +128 -0
- package/nodes/connect.js +450 -0
- package/nodes/decode.html +83 -0
- package/nodes/decode.js +56 -0
- package/nodes/disconnect.html +55 -0
- package/nodes/disconnect.js +47 -0
- package/nodes/encode.html +117 -0
- package/nodes/encode.js +164 -0
- package/nodes/monitor-in.html +48 -0
- package/nodes/monitor-in.js +42 -0
- package/nodes/raw-in.html +50 -0
- package/nodes/raw-in.js +72 -0
- package/nodes/raw-out.html +76 -0
- package/nodes/raw-out.js +144 -0
- package/nodes/send.html +91 -0
- package/nodes/send.js +373 -0
- package/nodes/ui-in.html +64 -0
- package/nodes/ui-in.js +68 -0
- package/nodes/ui-out.html +80 -0
- package/nodes/ui-out.js +133 -0
- package/package.json +47 -0
package/nodes/ui-in.html
ADDED
|
@@ -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>
|
package/nodes/ui-out.js
ADDED
|
@@ -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
|
+
}
|