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
package/.eslintignore ADDED
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ coverage/
5
+ *.min.js
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ coverage/
5
+ package-lock.json
6
+ yarn.lock
7
+ pnpm-lock.yaml
@@ -0,0 +1,174 @@
1
+ # Architecture: node-red-contrib-ax25
2
+
3
+ ## Layers
4
+
5
+ ```
6
+ ┌─────────────────────────────────────────────────────────────┐
7
+ │ Node-RED flow │
8
+ │ (inject, function, debug, etc.) │
9
+ └──────────────────────┬──────────────────────────────────────┘
10
+ │ msg
11
+ ┌──────────────────────▼──────────────────────────────────────┐
12
+ │ Node layer (nodes/) │
13
+ │ agwpe-client · connect · send · disconnect │
14
+ │ ui-out · ui-in · monitor-in · raw-out · raw-in │
15
+ │ decode · encode │
16
+ └──────┬───────────────────────────┬──────────────────────────┘
17
+ │ transport calls │ routed frames
18
+ ┌──────▼──────────┐ ┌────────────▼──────────────────────────┐
19
+ │ AgwpeClient │ │ FrameRouter │
20
+ │ Transport │ │ (lib/frame-router.js) │
21
+ │ (lib/agwpe- │ │ routes parsed frames to per-instance │
22
+ │ client- │ │ handler callbacks by frame kind │
23
+ │ transport.js) │ └──────────────┬────────────────────────┘
24
+ │ │ │ handler callbacks
25
+ │ Node.js │ ┌──────────────▼────────────────────────┐
26
+ │ net.Socket │ │ SessionRegistry │
27
+ │ EventEmitter │ │ (lib/session-registry.js) │
28
+ └──────┬──────────┘ │ tracks Ax25Session lifecycle per │
29
+ │ raw bytes │ instance; maps serverSessionId ↔ │
30
+ │ │ sessionId │
31
+ │ └───────────────────────────────────────┘
32
+
33
+ ┌──────▼──────────────────────────────────────────────────────┐
34
+ │ AGWPE server (external) │
35
+ │ TCP port 8000 (default) │
36
+ └─────────────────────────────────────────────────────────────┘
37
+ ```
38
+
39
+ ## Shared lib modules
40
+
41
+ | Module | Role |
42
+ |--------|------|
43
+ | `agwpe-client-transport.js` | Wraps `net.Socket`; emits `connected`, `error`, `close`, and raw `frame` events; injectable `socketFactory` for testing |
44
+ | `agwpe-frame-builder.js` | Builds AGWPE binary frame headers for each frame kind (P, X, C, D, U, K, m, M) |
45
+ | `agwpe-frame-pretty.js` | Debug-friendly AGWPE frame formatting for transport logging |
46
+ | `ax25-codec.js` | AX.25 frame encode/decode (callsign bit-shifting, via path, control, PID, payload) |
47
+ | `frame-router.js` | Parses raw AGWPE bytes into typed frame objects; dispatches to per-instance handler maps |
48
+ | `frame-segmentation.js` | Splits outbound payloads into ≤255-byte chunks; attaches `messageId`/`chunkIndex`/`chunkCount` metadata |
49
+ | `message-utils.js` | `nowTimestamp()`, `makeMessageId()` — shared across nodes and lib |
50
+ | `runtime-store.js` | In-memory store for per-instance context objects; also hosts a module-level `globalBus` (EventEmitter forwarding `conn-data`, `conn-lifecycle`, `conn-timeout-set` from all instances) and a `sessionIndex` map (`sessionId → instanceId`) that lets `send` resolve the correct instance at runtime without a static client reference |
51
+ | `session-registry.js` | CRUD for `Ax25Session` records; indexes by `instanceId` + `sessionId` and by `serverSessionId` |
52
+
53
+ ## Node internals pattern
54
+
55
+ Most nodes follow this structural pattern:
56
+
57
+ ```
58
+ node constructor
59
+ ├── validate config
60
+ ├── resolve agwpe-client instance (RED.nodes.getNode) ← all nodes except send
61
+ ├── register handler callbacks on agwpe-client's FrameRouter
62
+ node.on("input", ...) ← triggers on any message
63
+ ├── connect (connect node — uses msg.destination)
64
+ ├── send (send, ui-out, raw-out — uses msg.payload)
65
+ └── disconnect (disconnect node — uses msg.sessionId)
66
+
67
+ node.on("close", ...)
68
+ └── deregister from FrameRouter; destroy transport if agwpe-client
69
+ ```
70
+
71
+ ## AGWPE frame kind to Node-RED node routing
72
+
73
+ | AGWPE frame kind | Emitted to node |
74
+ |------------------|-----------------|
75
+ | D (connected data) | `connect` or `send` — matched by session callsign pair |
76
+ | C (connect/disconnect lifecycle) | `connect` — lifecycle event |
77
+ | U / K (UI/raw) | `ui-in` or `raw-in` — gated by mode flag |
78
+ | M / m (monitor) | `monitor-in` — gated by `monitorEnabled` flag |
79
+ | X (callsign registration ACK) | consumed by `agwpe-client` internally |
80
+ | P (login/auth) | consumed by `agwpe-client` internally |
81
+
82
+ ## Session index and global bus
83
+
84
+ `runtime-store` maintains a module-level `sessionIndex` map (`sessionId → instanceId`). The `connect` node writes to this index when a session is created and removes the entry on disconnect. The `send` node uses `instanceIdForSession(sessionId)` at message-handling time to look up the correct instance — no static client reference is needed.
85
+
86
+ `runtime-store` also hosts a `globalBus` EventEmitter. Each instance `bus` forwards `conn-data`, `conn-lifecycle`, and `conn-timeout-set` events to `globalBus` so `send` nodes can subscribe once for all instances.
87
+
88
+ ## connect node transport status
89
+
90
+ The `connect` node mirrors the transport state of its `agwpe-client` as a Node-RED status badge:
91
+
92
+ | agwpe-client state | connect node status |
93
+ |--------------------|---------------------|
94
+ | connecting | yellow dot — `connecting` |
95
+ | connected | green dot — `ready` |
96
+ | reconnecting | yellow ring — `reconnecting...` |
97
+ | disconnected | grey ring — `disconnected` |
98
+ | failed | red dot — `failed` |
99
+
100
+ On startup, the connect node reads `context.state` to set the initial status immediately. Ongoing changes are driven by `transport-connecting`, `transport-connected`, `transport-reconnecting`, `transport-closed`, and `failed` events on the instance bus.
101
+
102
+ ## Session lifecycle state machine
103
+
104
+ ```
105
+ connecting → connected → disconnecting → disconnected
106
+ ├──────────────────────────────────────→ failed
107
+ └──────────────────────────────────────→ connect-failed (d-frame before C-frame confirmed)
108
+ ```
109
+
110
+ Transitions are driven by AGWPE C-frame events and transport errors. On any transport failure all sessions in the owning instance transition to `failed` immediately.
111
+
112
+ ## Message shape contracts
113
+
114
+ ### connect node input
115
+
116
+ ```json
117
+ { "source": "N0CALL", "destination": "REMOTE-1" }
118
+ ```
119
+
120
+ ### send node input
121
+
122
+ ```json
123
+ { "sessionId": "sess-abc", "payload": "hello" }
124
+ ```
125
+
126
+ ### disconnect node input
127
+
128
+ ```json
129
+ { "sessionId": "sess-abc" }
130
+ ```
131
+
132
+ ### Standard success output envelope
133
+
134
+ ```json
135
+ { "timestamp": "…", "status": "ok", "instanceId": "…",
136
+ "sessionId": "opt", "event": "connected" }
137
+ ```
138
+
139
+ ### Standard error output envelope
140
+
141
+ ```json
142
+ { "timestamp": "…", "status": "error",
143
+ "errorCode": "AUTH_FAILED", "errorText": "…",
144
+ "instanceId": "…", "sessionId": "opt" }
145
+ ```
146
+
147
+ ### Outbound chunk envelope (attached per segment by send, ui-out)
148
+
149
+ ```json
150
+ { "messageId": "msg-abc", "chunkIndex": 0, "chunkCount": 3, "payload": "…" }
151
+ ```
152
+
153
+ Payloads ≤255 bytes produce a single chunk (`chunkIndex: 0`, `chunkCount: 1`).
154
+
155
+ ### Inbound data envelope (connect, send, ui-in)
156
+
157
+ ```json
158
+ { "timestamp": "…", "instanceId": "…", "sessionId": "…",
159
+ "event": "data", "payload": "…",
160
+ "source": "REMOTE-1", "destination": "N0CALL", "via": [] }
161
+ ```
162
+
163
+ Each received AGWPE frame is emitted immediately. No grouping metadata is added — there is no way to know whether the remote intended several frames as one logical payload.
164
+
165
+ ## Testability design
166
+
167
+ - `AgwpeClientTransport` accepts an injectable `socketFactory` — tests pass a fake factory that returns a mock socket
168
+ - `FrameRouter` and `SessionRegistry` are stateless relative to the transport and can be instantiated and exercised independently
169
+ - `node-red-node-test-helper` is used for all integration-level tests; no live AGWPE server required
170
+
171
+ ## Extension points for future work
172
+
173
+ - **Inbound reassembly**: an optional reassembly buffer keyed on a session + sequence, with a configurable flush timeout
174
+ - **TLS transport**: replacing `net.createConnection` with `tls.connect` via the existing `socketFactory` injection point
package/CONTEXT.md ADDED
@@ -0,0 +1,90 @@
1
+ # Project Context: node-red-contrib-ax25
2
+
3
+ ## What this is
4
+
5
+ A Node-RED contrib package that enables AX.25 packet radio connectivity through an AGWPE (AGW Packet Engine) server. Radio amateurs use it to integrate packet radio into Node-RED flows — connecting to BBSs, digipeaters, and other AX.25 stations.
6
+
7
+ The package provides ten custom nodes covering connected sessions, unconnected UI frames, monitor mode, raw frames, and AX.25 encode/decode. It manages two distinct connection layers: one TCP connection to the AGWPE server, and multiple concurrent AX.25 protocol sessions to remote stations.
8
+
9
+ ## Target users
10
+
11
+ Radio amateurs and enthusiasts running Node-RED who want to interact with AX.25 packet radio networks without writing socket code.
12
+
13
+ ## Runtime environment
14
+
15
+ - Node.js 20 LTS, CommonJS modules
16
+ - Node-RED 3.x+
17
+ - No external runtime dependencies (only Node.js `net` and `events`)
18
+
19
+ ## Key design decisions
20
+
21
+ **Auto-reconnect is on by default.** When the AGWPE transport drops, all owned sessions are marked disconnected and the node automatically attempts to reconnect after a configurable delay (default 5000 ms). Auto-reconnect can be disabled via the "Auto-Reconnect" checkbox in the config editor. When disabled, all sessions fail and the flow must re-deploy or restart to reconnect.
22
+
23
+ **`agwpe-client` is a config node.** It does not receive messages. All configuration (host, port, callsigns, auth, monitor/raw mode, reconnect settings) is set in the Node-RED editor. The connection is established automatically when the flow deploys. There is no `open`/`close` command interface.
24
+
25
+ **Per-instance connection ownership.** Each `agwpe-client` node manages one AGWPE TCP connection. All downstream nodes (`conn-out`, `conn-in`, `ui-in`, `ui-out`, `monitor-in`, `raw-in`, `raw-out`) bind to a specific `agwpe-client` instance by `instanceId`. Cross-instance routing is not allowed.
26
+
27
+ **Inbound data follows the last active node.** Received data for a connected session is routed to whichever `connect` or `send` node most recently processed a command for that session. When a `send` node handles a command, it takes over the data output for that session; when the session is first established by `connect`, data goes to `connect`'s output. This lets flows chain request/response steps naturally: each node in the chain receives the reply to its own send.
28
+
29
+ **Binary and line modes.** The `connect` node's `mode` setting (default: `line`) controls how inbound data is delivered on output port 2:
30
+
31
+ - **binary** — each received frame emitted immediately; `payload` is a Buffer.
32
+ - **line** — fragments are buffered and split on CR or CR+LF; each complete line is emitted with `payload` as a string. If `waitFor` (a regex) is set, lines accumulate until one matches; the output message then contains `payload` (array of preceding lines) and `match` (the matching line). If the timeout fires before a match, a `timeout` event is emitted on port 1 along with whatever has buffered so far.
33
+
34
+ `mode` and `waitFor` can be set on the node and overridden per-message via `msg.mode` and `msg.waitFor`.
35
+
36
+ **Payload output type.** Several nodes have a `payloadOutput` editor option (`"string"` or `"buffer"`, default `"string"`) that controls whether the AX.25 data payload is delivered as a UTF-8 string or a raw Buffer:
37
+
38
+ - `ui-in` — decoded AX.25 UI frame payload
39
+ - `decode` — decoded AX.25 frame payload
40
+
41
+ `raw-in` always emits payload as a Buffer (raw AX.25 wire bytes, leading AGWPE port byte stripped). `connect` and `send` payload type is governed by `mode` as described above.
42
+
43
+ **Outbound segmentation at 255 bytes.** Payloads larger than 255 bytes are segmented into multiple AGWPE frames before transmission.
44
+
45
+ **Optional auth.** `username` and `password` fields in the config node editor pass AGWPE-level credentials. Many LAN deployments omit them.
46
+
47
+ ## Message contract summary
48
+
49
+ All output messages include `timestamp` (ISO-8601). Errors always include `errorCode` and `errorText`. Connected session events include `instanceId`, `sessionId`, and `event` (`connected`, `disconnecting`, `disconnected`, `failed`, `timeout`). Inbound data messages include `instanceId`, `sessionId`, `event: "data"`, `payload`, `source`, `destination`, and `via`. Outbound segmented sends attach `messageId`, `chunkIndex`, and `chunkCount` to each chunk.
50
+
51
+ Full contract details are in [README.md](README.md).
52
+
53
+ ## Node list
54
+
55
+ | Node | Direction | Role |
56
+ |------|-----------|------|
57
+ | `agwpe-client` | config | Owns the AGWPE TCP connection; configured in the editor; connects on deploy |
58
+ | `connect` | in + 2 out | Establish an AX.25 session; output 1: lifecycle events; output 2: received data |
59
+ | `send` | in + 2 out | Send data or disconnect on an established session; output 1: events; output 2: received data |
60
+ | `ui-out` | data in | Encode and send AX.25 UI frames via AGWPE K raw transport |
61
+ | `ui-in` | data out | Decode incoming AGWPE K raw traffic to AX.25 UI frame fields |
62
+ | `monitor-in` | data out | Passive monitor stream (monitor mode must be enabled) |
63
+ | `raw-out` | data in | Send raw AGWPE frames (raw mode must be enabled) |
64
+ | `raw-in` | data out | Receive raw AGWPE frames (raw mode must be enabled) |
65
+ | `decode` | transform | AX.25 frame bytes → JSON (source, destination, via, control, PID, payload) |
66
+ | `encode` | transform | Structured JSON → raw AX.25 frame bytes |
67
+
68
+ ## Test strategy
69
+
70
+ Three test layers under `test/`:
71
+
72
+ - **contract/** — message shape and field validation, independent of Node-RED wiring
73
+ - **integration/** — full node loading + wiring with `node-red-node-test-helper`; covers open/close flows, session lifecycle, routing, segmentation, mode toggling
74
+ - **unit/** — isolated lib module behavior (codec, segmentation, router, registry)
75
+
76
+ Transport boundaries are mocked; no live AGWPE server is required for any test.
77
+
78
+ ## Project layout
79
+
80
+ ```text
81
+ nodes/ runtime JS + editor HTML for all ten node types
82
+ lib/ shared internals (transport, router, registry, codec, segmentation)
83
+ test/ contract / integration / unit test suites
84
+ examples/ importable Node-RED example flows
85
+ docs/ development guidance (AI working agreement, Node-RED patterns)
86
+ ```
87
+
88
+ ## Known limitations and deferred work
89
+
90
+ - No TLS support for the AGWPE TCP transport
package/MESSAGES.md ADDED
@@ -0,0 +1,314 @@
1
+ # Node Messages Reference
2
+
3
+ All messages include a `timestamp` (ISO 8601 string).
4
+ Messages with `status: "ok"` are built by `okEnvelope(fields)`.
5
+ Messages with `status: "error"` are built by `errorEnvelope(errorCode, errorText, fields)` and always include `errorCode` and `errorText`.
6
+
7
+ ---
8
+
9
+ ## agwpe-client
10
+
11
+ Configuration node. No inputs or outputs.
12
+
13
+ ---
14
+
15
+ ## connect
16
+
17
+ Initiates an AX.25 connected session. Two outputs: **output 1** (events), **output 2** (data).
18
+
19
+ ### Input
20
+
21
+ | Field | Type | Required | Description |
22
+ |---|---|---|---|
23
+ | `source` | string | no¹ | Calling callsign. Falls back to node config, then first registered callsign. |
24
+ | `destination` | string | no¹ | Called callsign. Falls back to node config. |
25
+ | `via` | string \| string[] | no | Digipeater path. Falls back to node config. |
26
+ | `sessionId` | string | no | Override the auto-generated session ID. |
27
+ | `mode` | `"line"` \| `"binary"` | no | Data framing mode. Falls back to node config (default: `"line"`). |
28
+ | `timeout` | number | no | Inactivity timeout in milliseconds. Falls back to node config. |
29
+ | `waitFor` | string | no | Regex pattern. Buffers inbound lines until a match; then emits all at once. Falls back to node config. |
30
+
31
+ ¹ Required unless provided by node config or registered callsign.
32
+
33
+ ### Output 1 — events
34
+
35
+ #### `status: "ok"` messages
36
+
37
+ | `event` | Additional fields | When |
38
+ |---|---|---|
39
+ | `connecting` | `instanceId`, `sessionId`, `source`, `destination`, `via` | C frame sent to TNC |
40
+ | `connected` | `instanceId`, `sessionId`, `source`, `destination`, `called` | TNC confirmed connection |
41
+ | `disconnecting` | `instanceId`, `sessionId`, `source`, `destination` | Disconnect initiated |
42
+ | `disconnected` | `instanceId`, `sessionId`, `source`, `destination` | TNC confirmed disconnect |
43
+
44
+ #### `status: "error"` messages
45
+
46
+ | `errorCode` | `errorText` | Additional fields | When |
47
+ |---|---|---|---|
48
+ | `CLIENT_NOT_CONNECTED` | `AGWPE Client is not open` | `instanceId` | Input received while transport is not connected |
49
+ | `CONNECT_INVALID` | `connect requires source and destination` | `instanceId` | Source or destination could not be resolved |
50
+ | `SESSION_ID_CONFLICT` | `Session already exists` | `instanceId` | Supplied `sessionId` is already in use |
51
+ | `SESSION_ID_REUSED` | `Server session ID collision detected` | `instanceId`, `sessionId`, `serverSessionId` | TNC reused a session ID for a different session |
52
+ | `CONNECT_FAILED` | `Connection attempt failed` | `instanceId`, `sessionId`, `source`, `destination` | TNC sent a `d` frame before ever confirming with a `C` frame |
53
+ | `TIMEOUT` | `Inactivity timeout` | `instanceId`, `sessionId`, `event: "timeout"` | Inactivity timer expired (binary mode) |
54
+ | `TIMEOUT` | `Inactivity timeout` | `instanceId`, `sessionId`, `event: "timeout"`, `waitFor`, `lineBuffer`, `waitForBuffer` | waitFor timer expired before the regex matched (line mode) |
55
+
56
+ > **Note:** Timeout messages may be emitted by a **send** node instead of the connect node if a send node currently holds the output claim for the session.
57
+
58
+ ### Output 2 — data
59
+
60
+ All data messages have `status: "ok"`, `event: "data"`, `instanceId`, `sessionId`, `source`, `destination`, `via`.
61
+
62
+ | Mode | `payload` type | `match` field | When |
63
+ |---|---|---|---|
64
+ | `binary` | `Buffer` | — | Inbound D frame received |
65
+ | `line` (no waitFor) | `string` | — | One complete line received (CR or CR+LF delimited) |
66
+ | `line` (waitFor active) | `string[]` — lines before the match | `string` — the matching line or fragment | First line matching the waitFor regex arrives |
67
+ | `line` (disconnect flush) | `string[]` — buffered lines | — | Session disconnects while lines are buffered |
68
+
69
+ ---
70
+
71
+ ## send
72
+
73
+ Sends data over an existing AX.25 connected session. Two outputs: **output 1** (events), **output 2** (data).
74
+
75
+ ### Input
76
+
77
+ | Field | Type | Required | Description |
78
+ |---|---|---|---|
79
+ | `sessionId` | string | yes | ID of the target session |
80
+ | `payload` | string \| Buffer \| Array<string\|Buffer> | yes | Data to send. Arrays are sent as separate D frames. |
81
+ | `waitFor` | string | no | Regex pattern. Claims data output and buffers until a match. Falls back to node config. |
82
+ | `timeout` | number | no | Override inactivity timeout in milliseconds. Falls back to node config. |
83
+
84
+ ### Output 1 — events
85
+
86
+ #### `status: "ok"` messages
87
+
88
+ | `event` | Additional fields | When |
89
+ |---|---|---|
90
+ | `sent` | `instanceId`, `sessionId`, `messageId`, `chunkCount` | All payload items delivered to TNC |
91
+
92
+ #### `status: "error"` messages
93
+
94
+ | `errorCode` | `errorText` | Additional fields | When |
95
+ |---|---|---|---|
96
+ | `SESSION_NOT_FOUND` | `Session not found` | `sessionId` | `msg.sessionId` not in the session registry |
97
+ | `SESSION_NOT_CONNECTED` | `Session is not connected` | `sessionId` | Session exists but state is not `"connected"` |
98
+ | `PAYLOAD_INVALID` | `payload items must be string or Buffer` | `sessionId` | A payload item is not a string or Buffer |
99
+ | `TIMEOUT` | `Inactivity timeout` | (same as connect timeout) | Timer fired while this node holds the output claim |
100
+
101
+ ### Output 2 — data
102
+
103
+ Same shape as connect output 2. The send node takes the data output claim when it processes an input; data arrives on this output until the session disconnects or a different send node claims it.
104
+
105
+ ---
106
+
107
+ ## disconnect
108
+
109
+ Initiates graceful disconnect of an existing session. One output (events).
110
+
111
+ ### Input
112
+
113
+ | Field | Type | Required | Description |
114
+ |---|---|---|---|
115
+ | `sessionId` | string | yes | ID of the session to disconnect |
116
+
117
+ ### Output — events
118
+
119
+ #### `status: "ok"` messages
120
+
121
+ | `event` | Additional fields | When |
122
+ |---|---|---|
123
+ | `disconnecting` | `instanceId`, `sessionId` | Disconnect frame sent to TNC |
124
+
125
+ #### `status: "error"` messages
126
+
127
+ | `errorCode` | `errorText` | Additional fields | When |
128
+ |---|---|---|---|
129
+ | `SESSION_NOT_FOUND` | `Session not found` | `sessionId` | `msg.sessionId` not in the session registry |
130
+
131
+ > The corresponding `disconnected` event is emitted by the **connect** node when the TNC confirms with a `d` frame.
132
+
133
+ ---
134
+
135
+ ## raw-in
136
+
137
+ Receives raw AX.25 frames (K frames) from the TNC. No input. One output.
138
+
139
+ Raw mode must be enabled on the agwpe-client node.
140
+
141
+ ### Output
142
+
143
+ | `status` | `event` | Fields | Description |
144
+ |---|---|---|---|
145
+ | `ok` | `raw` | `instanceId`, `payload` (Buffer), `agwpePort`, `source`, `destination`, `via` | Raw AX.25 wire frame received from TNC |
146
+
147
+ ---
148
+
149
+ ## raw-out
150
+
151
+ Transmits a raw AX.25 frame (K frame) via the TNC. One output.
152
+
153
+ Raw mode must be enabled on the agwpe-client node.
154
+
155
+ ### Input
156
+
157
+ | Field | Type | Required | Description |
158
+ |---|---|---|---|
159
+ | `payload` | Buffer \| Uint8Array \| byte array \| hex string \| encode envelope | yes | AX.25 wire frame to transmit |
160
+ | `agwpePort` | number | no | AGWPE port (0–255). Falls back to `msg.flag`, then node config (default: `0`). |
161
+
162
+ ### Output
163
+
164
+ #### `status: "ok"` messages
165
+
166
+ | `event` | Additional fields | When |
167
+ |---|---|---|
168
+ | `raw-sent` | `instanceId` | Frame handed to transport |
169
+
170
+ #### `status: "error"` messages
171
+
172
+ | `errorCode` | `errorText` | When |
173
+ |---|---|---|
174
+ | `CLIENT_NOT_FOUND` | `AGWPE Client instance not found` | agwpe-client config node is missing |
175
+ | `RAW_MODE_DISABLED` | `Raw mode is disabled` | Raw mode not enabled on agwpe-client |
176
+ | `RAW_FRAME_INVALID` | `Raw frame payload/agwpePort is invalid; payload must be Buffer, byte array, Uint8Array, hex string, or encode envelope` | Payload could not be parsed or `agwpePort` is out of range |
177
+
178
+ ---
179
+
180
+ ## monitor-in
181
+
182
+ Receives monitored AX.25 frames from the TNC (all traffic, not just connected sessions). No input. One output.
183
+
184
+ Monitor mode must be enabled on the agwpe-client node.
185
+
186
+ ### Output
187
+
188
+ | `status` | `event` | Fields | Description |
189
+ |---|---|---|---|
190
+ | `ok` | `monitor` | `instanceId`, `payload` (Buffer), `source`, `destination`, `via` | Monitored frame received |
191
+
192
+ ---
193
+
194
+ ## encode
195
+
196
+ Encodes an AX.25 frame from field inputs into a wire-format Buffer. One output.
197
+
198
+ ### Input
199
+
200
+ | Field | Type | Required | Description |
201
+ |---|---|---|---|
202
+ | `source` | string | no¹ | Source callsign. Falls back to node config (default: `"N0CALL"`). |
203
+ | `destination` | string | no¹ | Destination callsign. Falls back to node config (default: `"CQ"`). |
204
+ | `via` | string \| string[] \| object[] | no | Digipeater path. Falls back to node config. |
205
+ | `frameType` | `"I"` \| `"S"` \| `"U"` | no | Determines control byte if `control` is not set. Falls back to node config (default: `"U"`). |
206
+ | `control` | number (0–255) | no | Explicit control byte. Overrides `frameType`. Falls back to node config (default: `3`). |
207
+ | `pid` | number (0–255) | no | Protocol identifier byte. Falls back to node config (default: `240` / `0xF0`). |
208
+ | `payload` | string \| Buffer | no | Frame payload. Falls back to node config. |
209
+ | `agwpePort` | number | no | Passed through to output for use by raw-out. |
210
+
211
+ ¹ Required unless provided by node config.
212
+
213
+ ### Output
214
+
215
+ #### `status: "ok"` messages
216
+
217
+ | `event` | Additional fields | When |
218
+ |---|---|---|
219
+ | `encoded` | `payload` (Buffer), `agwpePort` | Frame successfully encoded |
220
+
221
+ #### `status: "error"` messages
222
+
223
+ | `errorCode` | `errorText` | When |
224
+ |---|---|---|
225
+ | `ENCODE_INPUT_INVALID` | `source, destination, and control/frameType are required` | Required fields missing |
226
+ | `ENCODE_FAILED` | codec error message | Codec rejected the input |
227
+
228
+ ---
229
+
230
+ ## decode
231
+
232
+ Decodes a wire-format AX.25 frame Buffer into structured fields. One output.
233
+
234
+ ### Input
235
+
236
+ | Field | Type | Required | Description |
237
+ |---|---|---|---|
238
+ | `payload` | Buffer | yes | AX.25 wire-format frame |
239
+ | `agwpePort` | number | no | Passed through to output. Falls back to `msg.agwpePrefix`, then `0`. |
240
+
241
+ ### Output
242
+
243
+ #### `status: "ok"` messages
244
+
245
+ Fields from the decoded frame, plus:
246
+
247
+ | Field | Type | Description |
248
+ |---|---|---|
249
+ | `status` | `"ok"` | |
250
+ | `agwpePort` | number | Normalized AGWPE port |
251
+ | `source` | string | Source callsign |
252
+ | `destination` | string | Destination callsign |
253
+ | `via` | object[] | Digipeater path |
254
+ | `control` | number | Control byte |
255
+ | `pid` | number | PID byte |
256
+ | `payload` | string \| Buffer | Frame payload (string if node config `payloadOutput` is `"string"`) |
257
+
258
+ #### `status: "error"` messages
259
+
260
+ | `errorCode` | `errorText` | When |
261
+ |---|---|---|
262
+ | `DECODE_INPUT_INVALID` | `payload must be Buffer` | Input payload is not a Buffer |
263
+ | `DECODE_FAILED` | codec error message | Codec rejected the frame |
264
+
265
+ ---
266
+
267
+ ## ui-in
268
+
269
+ Receives UI (unproto) AX.25 frames from raw traffic. No input. One output.
270
+
271
+ Raw mode must be enabled on the agwpe-client node.
272
+
273
+ ### Output
274
+
275
+ | `status` | `event` | Fields | Description |
276
+ |---|---|---|---|
277
+ | `ok` | `ui` | `instanceId`, `source`, `destination`, `via`, `payload` (string or Buffer) | UI frame received (`payload` is string if node config `payloadOutput` is `"string"`) |
278
+
279
+ ---
280
+
281
+ ## ui-out
282
+
283
+ Transmits a UI (unproto) AX.25 frame via the TNC. One output.
284
+
285
+ Raw mode must be enabled on the agwpe-client node.
286
+
287
+ ### Input
288
+
289
+ | Field | Type | Required | Description |
290
+ |---|---|---|---|
291
+ | `source` | string | no¹ | Source callsign. Falls back to node config. |
292
+ | `destination` | string | no¹ | Destination callsign. Falls back to node config. |
293
+ | `via` | string \| string[] \| object[] | no | Digipeater path. Falls back to node config. |
294
+ | `payload` | string \| Buffer | no¹ | Frame payload. Falls back to node config. |
295
+ | `agwpePort` | number | no | AGWPE port. Falls back to `0`. |
296
+
297
+ ¹ Required unless provided by node config.
298
+
299
+ ### Output
300
+
301
+ #### `status: "ok"` messages
302
+
303
+ | `event` | Additional fields | When |
304
+ |---|---|---|
305
+ | `ui-sent` | `instanceId` | UI frame handed to transport |
306
+
307
+ #### `status: "error"` messages
308
+
309
+ | `errorCode` | `errorText` | When |
310
+ |---|---|---|
311
+ | `CLIENT_NOT_FOUND` | `AGWPE Client instance not found` | agwpe-client config node is missing |
312
+ | `RAW_MODE_DISABLED` | `Raw mode is disabled` | Raw mode not enabled on agwpe-client |
313
+ | `UI_SEND_INVALID` | `ui-out requires source, destination, and payload (set in editor or input message)` | Required fields missing or empty |
314
+ | `UI_SEND_INVALID` | codec error message | Frame encoding failed |