node-red-contrib-ntrip 0.2.2 → 0.2.7
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/dependabot.yml +23 -0
- package/.github/workflows/node.js.yml +22 -21
- package/.github/workflows/release.yml +65 -0
- package/.prettierignore +20 -0
- package/.prettierrc.json +9 -0
- package/CHANGELOG.md +64 -0
- package/CLAUDE.md +73 -0
- package/README.md +145 -18
- package/doc/architecture/README.md +19 -0
- package/doc/architecture/adr/0001-single-registration-file.md +64 -0
- package/doc/architecture/adr/0002-ntrip-uploader-extension.md +71 -0
- package/doc/architecture/adr/0003-two-output-decoder-design.md +73 -0
- package/doc/architecture/adr/0004-stateful-handshake-interception.md +74 -0
- package/doc/architecture/adr/0005-rtcm-partial-frame-buffer.md +87 -0
- package/doc/architecture/adr/0006-nmea-multi-sentence-split.md +66 -0
- package/doc/architecture/adr/0007-mocha-test-helper-stack.md +67 -0
- package/doc/architecture/adr/0008-coordinate-gating-sentinel.md +76 -0
- package/doc/architecture/adr/README.md +18 -0
- package/doc/architecture/architecture-decisions.md +60 -0
- package/doc/architecture/behavioural-design.md +226 -0
- package/doc/architecture/errors-and-weaknesses.md +71 -0
- package/doc/architecture/future-improvements.md +130 -0
- package/doc/architecture/overview.md +77 -0
- package/doc/architecture/refactoring-recommendations.md +114 -0
- package/doc/architecture/statistics.md +118 -0
- package/doc/architecture/structural-design.md +141 -0
- package/eslint.config.js +36 -0
- package/examples/nmea-decode.json +1 -0
- package/examples/nmea-encode.json +1 -0
- package/ntrip/99-ntrip.html +248 -27
- package/ntrip/99-ntrip.js +14 -11
- package/ntrip/lib/ntrip-client.js +23 -18
- package/ntrip/nodes/nmea-decoder-node.js +80 -37
- package/ntrip/nodes/nmea-encoder-node.js +172 -0
- package/ntrip/nodes/ntrip-client-node.js +143 -123
- package/ntrip/nodes/rtcm-decoder-node.js +65 -47
- package/package.json +20 -2
- package/test/nmea-decoder.spec.js +146 -0
- package/test/nmea-encoder.spec.js +104 -0
- package/test/rtcm-decoder.spec.js +116 -0
- package/.github/workflows/npm-publish.yml +0 -33
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Behavioural Design
|
|
2
|
+
|
|
3
|
+
## Node-RED node lifecycle
|
|
4
|
+
|
|
5
|
+
Every node in this package follows Node-RED's standard lifecycle:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
deploy / restart
|
|
9
|
+
│
|
|
10
|
+
▼
|
|
11
|
+
constructor(config) ← create network resources, reset counters
|
|
12
|
+
│
|
|
13
|
+
▼
|
|
14
|
+
on('input', msg) ──── many ────► (transform / forward / write to socket)
|
|
15
|
+
│
|
|
16
|
+
▼
|
|
17
|
+
on('close', done) ← release resources, clear status, done()
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The constructor receives a `config` object built from the editor form. The
|
|
21
|
+
input handler is called for every message flowing into the node. The close
|
|
22
|
+
handler runs on flow redeploy or Node-RED shutdown.
|
|
23
|
+
|
|
24
|
+
## NtripClient — handshake state machine
|
|
25
|
+
|
|
26
|
+
The `NtripClient` node intercepts the caster's first reply line and uses it to
|
|
27
|
+
set its own status. Prior to v0.2.5 this check ran on **every** inbound TCP
|
|
28
|
+
packet, which both inflated the message counter and risked dropping RTCM bytes
|
|
29
|
+
that shared a TCP segment with the handshake reply. The current design is a
|
|
30
|
+
two-state machine:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
start
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
┌──────────────┐ data: "ICY 200 OK\r\n\r\n..." ┌─────────────┐
|
|
37
|
+
│ disconnected │ ───────────────────────────────► │ connected │
|
|
38
|
+
│ │ data: "ICY 406 ..." │ │
|
|
39
|
+
│ │ ──────► error, stay disconnected │ │
|
|
40
|
+
│ │ data: "SOURCETABLE 200 OK..." │ │
|
|
41
|
+
│ │ ──────► error, stay disconnected │ │
|
|
42
|
+
│ │ data: anything else │ │
|
|
43
|
+
│ │ ──────► forward, transition to ─►│ │
|
|
44
|
+
└──────────────┘ └──────┬──────┘
|
|
45
|
+
▲ │
|
|
46
|
+
│ socket close / error │
|
|
47
|
+
└──────────────────────────────────────────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Key behaviours:
|
|
51
|
+
|
|
52
|
+
- **Transitional bytes are not lost.** When the handshake reply and the first
|
|
53
|
+
RTCM frame share a TCP segment (common), the node splits at the `\r\n\r\n`
|
|
54
|
+
header terminator and forwards the trailing RTCM bytes on the output.
|
|
55
|
+
- **Reset on close.** The `connected` flag is set back to `false` when the
|
|
56
|
+
socket closes, so the next reconnect runs the handshake recognition again.
|
|
57
|
+
- **Counters track Rx and Tx separately.** `messagesReceived` is only bumped
|
|
58
|
+
for data forwarded after handshake (not control replies). `messagesSent` is
|
|
59
|
+
bumped per input message written to the caster.
|
|
60
|
+
|
|
61
|
+
See [ADR-0004](adr/0004-stateful-handshake-interception.md).
|
|
62
|
+
|
|
63
|
+
## NtripClient — input handling
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
msg.payload arrives
|
|
67
|
+
│
|
|
68
|
+
▼
|
|
69
|
+
payload nullish? ─yes─► drop (no-op)
|
|
70
|
+
│ no
|
|
71
|
+
▼
|
|
72
|
+
Array.isArray(payload)? ─yes─► client.setXYZ(payload) # update GGA seed
|
|
73
|
+
│ no
|
|
74
|
+
▼
|
|
75
|
+
client.write(payload)
|
|
76
|
+
│
|
|
77
|
+
▼
|
|
78
|
+
node.passthrough? ─yes─► node.send(msg) # echo on output
|
|
79
|
+
│ no
|
|
80
|
+
▼
|
|
81
|
+
updateStatus()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
An `Array` payload is interpreted as a coordinate triple `[x, y, z]` and routed
|
|
85
|
+
to the underlying client's `setXYZ` method — used by Sapos and other VRS
|
|
86
|
+
casters that gate the stream on a `GGA` from the client. A non-array payload
|
|
87
|
+
is written verbatim to the caster.
|
|
88
|
+
|
|
89
|
+
## NtripClient — outbound write error handling
|
|
90
|
+
|
|
91
|
+
`net.Socket.write` is asynchronous; runtime failures (ECONNRESET, EPIPE) come
|
|
92
|
+
through the socket's `error` event rather than as synchronous throws. The node
|
|
93
|
+
therefore has two error paths:
|
|
94
|
+
|
|
95
|
+
- **Synchronous** — the try/catch around `client.write` catches misuse
|
|
96
|
+
(`client` undefined, wrong-shape payload), reports via `node.error` and
|
|
97
|
+
flashes the status badge red.
|
|
98
|
+
- **Asynchronous** — the socket `error` listener calls `node.error` with the
|
|
99
|
+
same wording. `AggregateError` instances (e.g. from happy-eyeballs DNS
|
|
100
|
+
resolution failures) are expanded into one log line per inner error.
|
|
101
|
+
|
|
102
|
+
## RtcmDecoder — frame reassembly across input events
|
|
103
|
+
|
|
104
|
+
RTCM 3 frames are variable-length and routinely cross TCP packet boundaries.
|
|
105
|
+
The decoder keeps a `pendingBuffer` on the node instance and reassembles:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
on('input', msg):
|
|
109
|
+
chunk = msg.payload (Buffer or coerced)
|
|
110
|
+
buffer = concat(pendingBuffer, chunk)
|
|
111
|
+
|
|
112
|
+
loop while buffer not empty:
|
|
113
|
+
try: [message, length] = RtcmTransport.decode(buffer)
|
|
114
|
+
on throw:
|
|
115
|
+
if buffer.length > 64KB:
|
|
116
|
+
emit error on output 2
|
|
117
|
+
buffer = empty
|
|
118
|
+
break # keep remainder for next input event
|
|
119
|
+
if length not positive or > buffer.length:
|
|
120
|
+
break # defensive zero-length guard
|
|
121
|
+
emit decoded message on output 1
|
|
122
|
+
buffer = buffer.slice(length)
|
|
123
|
+
|
|
124
|
+
pendingBuffer = buffer
|
|
125
|
+
updateStatus()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- **Partial-frame tolerance.** A throw is interpreted as "frame may be incomplete";
|
|
129
|
+
bytes stay in `pendingBuffer` to be re-tried with the next chunk.
|
|
130
|
+
- **Garbage cap.** If `pendingBuffer` grows past 64 KB without ever decoding a
|
|
131
|
+
frame, the data is flushed to the error output and the buffer is cleared —
|
|
132
|
+
prevents runaway memory if the upstream feeds non-RTCM bytes indefinitely.
|
|
133
|
+
- **Forward-progress guarantee.** A non-positive or oversized `length` from the
|
|
134
|
+
decoder breaks the loop rather than spinning.
|
|
135
|
+
|
|
136
|
+
See [ADR-0005](adr/0005-rtcm-partial-frame-buffer.md).
|
|
137
|
+
|
|
138
|
+
## NmeaDecoder — multi-sentence splitting
|
|
139
|
+
|
|
140
|
+
A single input chunk may carry several `\r\n`-delimited sentences. The decoder
|
|
141
|
+
splits and decodes each independently:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
on('input', msg):
|
|
145
|
+
rawInput = msg.payload.nmeaMessage ?? msg.payload (preserved untouched)
|
|
146
|
+
rawString = Buffer? rawInput.toString('utf8') : rawInput
|
|
147
|
+
|
|
148
|
+
for each sentence in rawString.split(/\r?\n/):
|
|
149
|
+
sentence = sentence.trim()
|
|
150
|
+
skip if empty
|
|
151
|
+
try: NmeaTransport.decode(sentence) → emit on output 1
|
|
152
|
+
on throw: emit on output 2 with
|
|
153
|
+
input: rawInput (Buffer if Buffer was supplied)
|
|
154
|
+
inputString: rawString (utf8 form)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- **`input` preservation.** The original payload (`Buffer` if a `Buffer` was
|
|
158
|
+
supplied) is carried through to the error output unchanged — restored in
|
|
159
|
+
v0.2.6 after a regression in v0.2.5 forced everything through `toString`
|
|
160
|
+
before storing.
|
|
161
|
+
|
|
162
|
+
See [ADR-0006](adr/0006-nmea-multi-sentence-split.md).
|
|
163
|
+
|
|
164
|
+
## NmeaEncoder — instance passthrough vs construction
|
|
165
|
+
|
|
166
|
+
The encoder accepts two input shapes:
|
|
167
|
+
|
|
168
|
+
- A pre-constructed `NmeaMessage*` instance — passed straight to
|
|
169
|
+
`NmeaTransport.encode` without inspection.
|
|
170
|
+
- A plain field object plus a `messageType` discriminator — fed through a
|
|
171
|
+
`switch (messageType)` that maps to the right `NmeaMessage*.construct(input)`
|
|
172
|
+
factory.
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
on('input', msg):
|
|
176
|
+
require payload, payload.nmeaMessage, payload.messageType (else error output)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
messageType = String(payload.messageType).toUpperCase()
|
|
180
|
+
input = payload.nmeaMessage
|
|
181
|
+
|
|
182
|
+
if input instanceof NmeaMessage:
|
|
183
|
+
nmeaMessage = input
|
|
184
|
+
else:
|
|
185
|
+
nmeaMessage = NmeaMessage<Type>.construct(input)
|
|
186
|
+
unknown messageType → throw (caught and routed to error)
|
|
187
|
+
|
|
188
|
+
message = NmeaTransport.encode(nmeaMessage)
|
|
189
|
+
emit on output 1
|
|
190
|
+
|
|
191
|
+
on throw: emit on output 2 (no node.error)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Two-output error routing
|
|
195
|
+
|
|
196
|
+
Every decoder/encoder uses the `[ok, error]` two-output convention rather than
|
|
197
|
+
calling `node.error()` for per-message failures. The reason is that high-rate
|
|
198
|
+
streams routinely emit mismatched binary (the README explicitly suggests
|
|
199
|
+
wiring an NMEA decoder onto an RTK2Go stream as a demonstration) — calling
|
|
200
|
+
`node.error` for each failure floods the Node-RED `[error]` log. The error
|
|
201
|
+
output is the channel; wire it to a debug or downstream handler.
|
|
202
|
+
|
|
203
|
+
See [ADR-0003](adr/0003-two-output-decoder-design.md).
|
|
204
|
+
|
|
205
|
+
## Status badge convention
|
|
206
|
+
|
|
207
|
+
All four nodes follow the same rule for the badge under the node in the
|
|
208
|
+
Node-RED editor:
|
|
209
|
+
|
|
210
|
+
| State | Fill | Shape | Text |
|
|
211
|
+
|-------|------|-------|------|
|
|
212
|
+
| starting | yellow | ring | `connecting...` (NtripClient only) |
|
|
213
|
+
| running | green | ring | counters, e.g. `Rx 123 Tx 4` / `NMEA: 12 Invalid: 0` |
|
|
214
|
+
| connected (NtripClient) | green | ring | `NTRIP server connected.` |
|
|
215
|
+
| handshake error | red | ring | `NTRIP server rejected connection.` or `No mountpoint <name>` |
|
|
216
|
+
| write/decode error | red | ring | error message |
|
|
217
|
+
| closed | (cleared) | — | — |
|
|
218
|
+
|
|
219
|
+
## Close behaviour
|
|
220
|
+
|
|
221
|
+
| Node | On `close` |
|
|
222
|
+
|------|------------|
|
|
223
|
+
| NtripClient | `client.removeAllListeners()`, then `client.close()`, clear status. |
|
|
224
|
+
| RtcmDecoder | Discard `pendingBuffer`, clear status. |
|
|
225
|
+
| NmeaDecoder | Clear status. |
|
|
226
|
+
| NmeaEncoder | Clear status. |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Errors and Weaknesses
|
|
2
|
+
|
|
3
|
+
This chapter inventories known limitations. Items are grouped by severity and
|
|
4
|
+
labelled by status (Open / Fixed-in-vX.Y.Z / Accepted as design constraint).
|
|
5
|
+
|
|
6
|
+
## Critical
|
|
7
|
+
|
|
8
|
+
| ID | Description | Status |
|
|
9
|
+
|----|-------------|--------|
|
|
10
|
+
| C-1 | `NmeaEncoder` instance passthrough was broken (`input.prototype instanceof NmeaMessage` always false) | Fixed in 0.2.4 |
|
|
11
|
+
| C-2 | `NmeaEncoder` catch block referenced an out-of-scope `input`, crashing on every encoder failure | Fixed in 0.2.4 |
|
|
12
|
+
| C-3 | `NtripClient` crashed at construction when `mode` was neither `download` nor `upload` (`client.on('data',…)` on `undefined`) | Fixed in 0.2.5 |
|
|
13
|
+
| C-4 | `RtcmDecoder` dropped any partial frame at TCP packet boundaries (no carry-over) | Fixed in 0.2.5 |
|
|
14
|
+
| C-5 | `NmeaDecoder` only decoded the first sentence of a multi-sentence chunk despite documentation claiming otherwise | Fixed in 0.2.5 |
|
|
15
|
+
| C-6 | `NmeaDecoder` and `RtcmDecoder` called `node.error()` for every decode failure — flooded the Node-RED log when fed mismatched binary (the README's own demo wiring) | Fixed in 0.2.6 |
|
|
16
|
+
| C-7 | `NmeaDecoder` error output stored the `toString`-converted form on `input`, losing the original `Buffer` | Fixed in 0.2.6 |
|
|
17
|
+
|
|
18
|
+
## High
|
|
19
|
+
|
|
20
|
+
| ID | Description | Status |
|
|
21
|
+
|----|-------------|--------|
|
|
22
|
+
| H-1 | Always-on handshake check on every data packet — risked dropping RTCM bytes that shared a TCP segment with the `ICY 200 OK` reply | Fixed in 0.2.5 |
|
|
23
|
+
| H-2 | Coordinate guard `x && y && z` silently dropped axis-aligned positions (e.g. equator) | Fixed in 0.2.5 |
|
|
24
|
+
| H-3 | `NmeaEncoder` unknown `messageType` silently emitted `nmeaMessage: undefined` on the success output | Fixed in 0.2.4 |
|
|
25
|
+
| H-4 | `client.write` rejection errors were not visible (async — not caught by the surrounding `try`) | **Open.** Async write failures now surface via the socket `error` listener, but no per-input error feedback. |
|
|
26
|
+
| H-5 | RTCM/NMEA decoder loop had no zero-length-progress guard — theoretical infinite loop | Fixed in 0.2.5 |
|
|
27
|
+
|
|
28
|
+
## Medium
|
|
29
|
+
|
|
30
|
+
| ID | Description | Status |
|
|
31
|
+
|----|-------------|--------|
|
|
32
|
+
| M-1 | Password input rendered as `type="text"` in the editor form | Fixed in 0.2.5 |
|
|
33
|
+
| M-2 | CR/LF injection possible via `mountpoint`/`username`/`password` (interpolated into the handshake string) | Fixed in 0.2.5 — rejected at construct time |
|
|
34
|
+
| M-3 | `AggregateError` referenced unconditionally; ReferenceError on Node ≤ 14 | Fixed in 0.2.5 — `typeof` guard, and `engines.node` bumped to ≥ 18 |
|
|
35
|
+
| M-4 | `host` accepted as `""`/`null`; `interval` forwarded as a raw string | Fixed in 0.2.5 |
|
|
36
|
+
| M-5 | Counter `messagesReceived` mixed inbound, outbound, and handshake replies | Fixed in 0.2.5 — split into `Rx`/`Tx` |
|
|
37
|
+
| M-6 | No connection-state event on the `NtripClient` output — downstream flows can only react to the status badge text | **Open.** See [Future Improvements](future-improvements.md). |
|
|
38
|
+
| M-7 | Listener leaks on `client.close()` — accumulated across reconnects | Fixed in 0.2.5 — `removeAllListeners` on close |
|
|
39
|
+
|
|
40
|
+
## Low
|
|
41
|
+
|
|
42
|
+
| ID | Description | Status |
|
|
43
|
+
|----|-------------|--------|
|
|
44
|
+
| L-1 | `engines.node: ">=7.6.0"` was inconsistent with code that uses `AggregateError` and the CI matrix (18/20/22) | Fixed in 0.2.5 — bumped to `>=18.0.0` |
|
|
45
|
+
| L-2 | Editor help text was stubs (`<p>.</p>`) for three of four nodes | Fixed in 0.2.6 — full `<dl class="message-properties">` blocks |
|
|
46
|
+
| L-3 | Decoders/encoder kept the original input attached to every output message (memory pressure for high-rate streams) | **Open.** Cost is one Buffer reference per emission. |
|
|
47
|
+
| L-4 | `'socket timeouted'` typo in the upstream socket error message | Fixed in 0.2.5 |
|
|
48
|
+
| L-5 | NMEA case `'Object'` was unreachable because `messageType.toUpperCase()` ran first | Fixed in 0.2.4 |
|
|
49
|
+
|
|
50
|
+
## Open design-level constraints
|
|
51
|
+
|
|
52
|
+
These are *accepted* limitations — documented rather than fixed, because the
|
|
53
|
+
fix would be a significant architectural change.
|
|
54
|
+
|
|
55
|
+
- **NTRIP over plaintext TCP only.** No NTRIPS / TLS support. NTRIP V2 over TLS
|
|
56
|
+
is a real protocol but the upstream `ntrip-client` package does not implement
|
|
57
|
+
it, and this contrib does not bring its own transport layer. See
|
|
58
|
+
[Future Improvements](future-improvements.md).
|
|
59
|
+
- **`_connect()` full replacement** (not delegation) in
|
|
60
|
+
[ntrip/lib/ntrip-client.js](../../ntrip/lib/ntrip-client.js). Improvements to
|
|
61
|
+
the upstream client's connection logic (keep-alive, IPv6 handling, TLS
|
|
62
|
+
upgrade) won't propagate into the uploader path. See
|
|
63
|
+
[ADR-0002](adr/0002-ntrip-uploader-extension.md).
|
|
64
|
+
- **`credentials` block must be declared in two places** — both the JS
|
|
65
|
+
`registerType` and the HTML `RED.nodes.registerType`. Drift is detectable
|
|
66
|
+
only at runtime when persistence silently fails.
|
|
67
|
+
- **No per-node integration tests for `NtripClient`** — the unit test stack
|
|
68
|
+
covers the three pure transformer nodes only. The client requires a fake TCP
|
|
69
|
+
server for meaningful testing; see [Future Improvements](future-improvements.md).
|
|
70
|
+
- **Editor HTML help text duplicates much of the runtime documentation** — any
|
|
71
|
+
schema change to a `payload` shape needs two updates (code + help text).
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Future Improvements
|
|
2
|
+
|
|
3
|
+
These are *new capability* items rather than refactors. They would change
|
|
4
|
+
behaviour visible to the user, and most warrant a minor version bump
|
|
5
|
+
(0.3.0+) or a major one if they break the public message shape.
|
|
6
|
+
|
|
7
|
+
## Protocol / network
|
|
8
|
+
|
|
9
|
+
### NTRIPS (NTRIP over TLS)
|
|
10
|
+
|
|
11
|
+
NTRIP V2 may run over TLS (typically port 2102, sometimes 443). The package
|
|
12
|
+
currently relies on plaintext TCP via `net.createConnection`. Adding TLS would
|
|
13
|
+
need:
|
|
14
|
+
|
|
15
|
+
- A toggle in the editor form (`useTls: boolean`).
|
|
16
|
+
- A separate `tls.connect` path in [ntrip/lib/ntrip-client.js](../../ntrip/lib/ntrip-client.js).
|
|
17
|
+
- Optional CA bundle / SNI / certificate-pinning settings.
|
|
18
|
+
- Documentation that NTRIPS handshake is identical to NTRIP V2 once the TLS
|
|
19
|
+
tunnel is established.
|
|
20
|
+
|
|
21
|
+
Breaks no existing flow — pure additive.
|
|
22
|
+
|
|
23
|
+
### Connection-state event output
|
|
24
|
+
|
|
25
|
+
Today, downstream Node-RED flows cannot react to `NtripClient` reconnects,
|
|
26
|
+
rejections, or handshake completion except by scraping the status badge text.
|
|
27
|
+
Three implementation options, in order of disruption:
|
|
28
|
+
|
|
29
|
+
1. Emit a tagged message on the existing output:
|
|
30
|
+
`{ payload: null, state: 'connected' | 'rejected' | 'reconnecting' }`.
|
|
31
|
+
Slightly viral — every downstream node must filter `payload != null`.
|
|
32
|
+
2. Add a second output port for state events. Breaks layout of existing flows.
|
|
33
|
+
3. Surface state via Node-RED's runtime event bus (`RED.events.emit`).
|
|
34
|
+
Non-breaking but requires opt-in from consumers.
|
|
35
|
+
|
|
36
|
+
### NTRIP source-table fetch / mountpoint picker
|
|
37
|
+
|
|
38
|
+
The branch `prepared getMountpoints: postponed…` (commit `38a243b`) indicates a
|
|
39
|
+
prior attempt at fetching a caster's source table to populate a dropdown in
|
|
40
|
+
the editor. The hold-up was that credentials are stored server-side and the
|
|
41
|
+
editor JS can't access them. A workable approach: add a small admin endpoint
|
|
42
|
+
(`RED.httpAdmin.get('/ntrip/sourcetable')`) that performs the fetch on the
|
|
43
|
+
server using the stored credentials.
|
|
44
|
+
|
|
45
|
+
## Test / quality
|
|
46
|
+
|
|
47
|
+
### NtripClient integration tests
|
|
48
|
+
|
|
49
|
+
Use `net.createServer` to stand up a fake caster that the test harness fully
|
|
50
|
+
controls. Worth-testing scenarios:
|
|
51
|
+
|
|
52
|
+
- Each `authmode` writes the correct handshake string.
|
|
53
|
+
- `ICY 200 OK\r\n\r\n<rtcm>` reply: trailing bytes are forwarded.
|
|
54
|
+
- `ICY 406`: error logged, status red, no data forwarded.
|
|
55
|
+
- Mid-stream socket close: `connected` resets so the next handshake reply
|
|
56
|
+
re-enters the state machine.
|
|
57
|
+
- `msg.payload = [x, y, z]` routes to `setXYZ` (verify via the upstream
|
|
58
|
+
client's internal GGA emission).
|
|
59
|
+
- CR/LF in credentials: rejected at construct, no socket opened.
|
|
60
|
+
|
|
61
|
+
### Coverage reporting
|
|
62
|
+
|
|
63
|
+
Add `c8` (or `nyc`) as a dev dep and a `coverage` npm script. Currently
|
|
64
|
+
Statistics shows "not measured"; a single CI badge changes that. Suggest a
|
|
65
|
+
70 % statement coverage floor as a soft target — high enough to be useful, low
|
|
66
|
+
enough not to require gymnastics on the rare error paths.
|
|
67
|
+
|
|
68
|
+
### Linting
|
|
69
|
+
|
|
70
|
+
Adopt `eslint` with `eslint-config-node-red-contribs` or the maintainer's own
|
|
71
|
+
preference. Would have caught most of the regressions from 0.2.4 / 0.2.5
|
|
72
|
+
(unused vars, `let` vs `const`, missing returns).
|
|
73
|
+
|
|
74
|
+
### TypeScript types (`.d.ts`)
|
|
75
|
+
|
|
76
|
+
The four nodes have a public message-shape contract (the `payload` schemas
|
|
77
|
+
documented in the help text). Publishing JSDoc-derived `.d.ts` files would let
|
|
78
|
+
TypeScript users in Node-RED's `function` nodes get autocomplete for
|
|
79
|
+
`msg.payload.messageType`, `msg.payload.message.staX`, etc. Lightweight to
|
|
80
|
+
add via `// @ts-check` and JSDoc on the existing JS source.
|
|
81
|
+
|
|
82
|
+
## Editor UX
|
|
83
|
+
|
|
84
|
+
### Per-node info-sidebar fixtures
|
|
85
|
+
|
|
86
|
+
Add small example payloads to the help text so users can copy them straight
|
|
87
|
+
into an `inject` node:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// Example NmeaEncoder input
|
|
91
|
+
msg.payload = { messageType: 'GGA', nmeaMessage: { /* fields */ } };
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The HTML stubs added in 0.2.6 are documentation but no example flows are
|
|
95
|
+
linked from the editor — only from the README. A `<a href="…/examples/…">`
|
|
96
|
+
link inside each help block would close that gap.
|
|
97
|
+
|
|
98
|
+
### Hide credentials when not needed
|
|
99
|
+
|
|
100
|
+
When `mode === 'download'` and the caster doesn't require auth, the
|
|
101
|
+
Username/Password fields are still shown. A small `oneditprepare` hide rule
|
|
102
|
+
keyed on a "Requires authentication" checkbox would declutter the dialog.
|
|
103
|
+
|
|
104
|
+
## Codec coverage
|
|
105
|
+
|
|
106
|
+
### More NMEA sentence types
|
|
107
|
+
|
|
108
|
+
The current encoder supports 17 sentence types. The `@gnss/nmea` library has
|
|
109
|
+
more (e.g. `VBW`, `VDR`, `WCV`, …). Adding a case is one import + one
|
|
110
|
+
`switch` arm; the test suite already covers the through-path, so each new
|
|
111
|
+
type is ~5 LOC.
|
|
112
|
+
|
|
113
|
+
### RTCM message-type filter
|
|
114
|
+
|
|
115
|
+
A `messageTypeFilter: number[]` config (e.g. `[1005, 1077, 1087]`) on the
|
|
116
|
+
decoder would let users drop frame types they don't care about *before* they
|
|
117
|
+
reach downstream nodes. Reduces flow noise and downstream work.
|
|
118
|
+
|
|
119
|
+
## Distribution / packaging
|
|
120
|
+
|
|
121
|
+
### Publish `node-red` entries
|
|
122
|
+
|
|
123
|
+
[package.json](../../package.json) `keywords` includes `"mmea"` (typo — should
|
|
124
|
+
be `"nmea"`). Catalogue search misses are real downloads lost.
|
|
125
|
+
|
|
126
|
+
### CI: publish on tag
|
|
127
|
+
|
|
128
|
+
The `.github/workflows/npm-publish.yml` (out of scope here) already exists.
|
|
129
|
+
Worth pairing with semantic-release or a manual tag-driven publish so the
|
|
130
|
+
release cycle is auditable from GitHub.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
## What this package is
|
|
4
|
+
|
|
5
|
+
`node-red-contrib-ntrip` is a Node-RED contribution package that adds four
|
|
6
|
+
GNSS-related nodes to a Node-RED palette. The package is distributed via npm
|
|
7
|
+
and consumed by users through Node-RED's "Manage palette" UI — there is no
|
|
8
|
+
standalone application binary.
|
|
9
|
+
|
|
10
|
+
The four nodes:
|
|
11
|
+
|
|
12
|
+
| Node | Direction | Library | Purpose |
|
|
13
|
+
|------|-----------|---------|---------|
|
|
14
|
+
| `NtripClient` | network ↔ flow | [`ntrip-client`](https://github.com/dxhbiz/ntrip-client) (extended) | Connect to an NTRIP caster as either a *client* (download corrections) or a *server* (upload corrections to a mountpoint). |
|
|
15
|
+
| `RtcmDecoder` | binary → object | [`@gnss/rtcm`](https://github.com/node-ntrip/rtcm) | Decode RTCM 3 binary frames into JavaScript objects. |
|
|
16
|
+
| `NmeaDecoder` | text → object | [`@gnss/nmea`](https://github.com/node-ntrip/nmea) | Decode NMEA 0183 ASCII sentences into JavaScript objects. |
|
|
17
|
+
| `NmeaEncoder` | object → text | [`@gnss/nmea`](https://github.com/node-ntrip/nmea) | The inverse — emit a NMEA sentence string from an object. |
|
|
18
|
+
|
|
19
|
+
## Who uses it
|
|
20
|
+
|
|
21
|
+
Practitioners building RTK/PPP/DGNSS pipelines in Node-RED — typically:
|
|
22
|
+
|
|
23
|
+
- Receiving correction streams from a public NTRIP caster (RTK2Go, SAPOS,
|
|
24
|
+
Centipede, Onocoy) and forwarding them to a rover (e.g. an ESP32-XBee, an
|
|
25
|
+
RTKLib instance, a u-blox receiver) over TCP, Serial, MQTT.
|
|
26
|
+
- Operating a private base station (e.g. RTKBase) and pushing corrections to a
|
|
27
|
+
caster for others to consume.
|
|
28
|
+
- Logging or inspecting GNSS messages live for monitoring or debugging.
|
|
29
|
+
|
|
30
|
+
## How the nodes fit together
|
|
31
|
+
|
|
32
|
+
A typical flow looks like:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌────────────────┐
|
|
36
|
+
│ RTCM Decoder │──► (per-frame objects, output 1)
|
|
37
|
+
│ │──► (decode errors, output 2)
|
|
38
|
+
┌────────────────┐ RTCM bytes └────────────────┘
|
|
39
|
+
│ NTRIP Client │──────────────►
|
|
40
|
+
│ (download) │ ┌────────────────┐
|
|
41
|
+
└────────────────┘ │ NMEA Decoder │──► (per-sentence objects, output 1)
|
|
42
|
+
│ │──► (decode errors, output 2)
|
|
43
|
+
└────────────────┘
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
┌─────────────────┐ RTCM bytes ┌────────────────┐
|
|
48
|
+
│ TCP In / RTK │──────────────►│ NTRIP Client │──► (caster replies, optional pass-through)
|
|
49
|
+
│ Base / etc. │ │ (upload) │
|
|
50
|
+
└─────────────────┘ └────────────────┘
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For VRS / network-RTK casters that require a position fix from the client:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
┌─────────────┐ msg.payload=[x,y,z] ┌────────────────┐
|
|
57
|
+
│ Position │────────────────────►│ NTRIP Client │──(internal: synthesise NMEA GGA periodically)──► caster
|
|
58
|
+
│ source │ │ (download) │
|
|
59
|
+
└─────────────┘ └────────────────┘
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Runtime model
|
|
63
|
+
|
|
64
|
+
- **Pure JavaScript, no build step.** Files in [ntrip/](../../ntrip/) are loaded by Node-RED at startup.
|
|
65
|
+
- **Single registration entry point** — [ntrip/99-ntrip.js](../../ntrip/99-ntrip.js) `require`s the four node implementations and calls `RED.nodes.registerType` for each. See [ADR-0001](adr/0001-single-registration-file.md).
|
|
66
|
+
- **Editor HTML co-located** — all four node configuration dialogs and help text live in [ntrip/99-ntrip.html](../../ntrip/99-ntrip.html).
|
|
67
|
+
- **Node ≥ 18** — runtime requires `AggregateError` and modern syntax.
|
|
68
|
+
- **Stateless across restarts** — node configuration is persisted by Node-RED's flow file; the nodes themselves carry only in-memory state (counters, the NTRIP socket, the RTCM pending buffer).
|
|
69
|
+
|
|
70
|
+
## Reading order for new contributors
|
|
71
|
+
|
|
72
|
+
1. This file.
|
|
73
|
+
2. [Structural Design](structural-design.md) — how files and modules relate.
|
|
74
|
+
3. [Behavioural Design](behavioural-design.md) — runtime flow, state machines, error paths.
|
|
75
|
+
4. [Architecture Decisions](architecture-decisions.md) — links to each ADR.
|
|
76
|
+
5. [Errors and Weaknesses](errors-and-weaknesses.md) — current known limitations.
|
|
77
|
+
6. [Statistics](statistics.md) — LOC, test count, and a simple quality index.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Refactoring Recommendations
|
|
2
|
+
|
|
3
|
+
Items below are *recommendations*, not blockers. They are ordered by leverage —
|
|
4
|
+
the changes near the top would simplify the code or unlock further improvements;
|
|
5
|
+
the ones near the bottom are nice-to-have polish.
|
|
6
|
+
|
|
7
|
+
## 1. Split `NtripClient` node into mode-specific handlers
|
|
8
|
+
|
|
9
|
+
[ntrip/nodes/ntrip-client-node.js](../../ntrip/nodes/ntrip-client-node.js) is the
|
|
10
|
+
biggest file in the package (~180 LOC) and mixes:
|
|
11
|
+
|
|
12
|
+
- Configuration parsing and validation
|
|
13
|
+
- Coordinate gating
|
|
14
|
+
- Download vs upload branching
|
|
15
|
+
- Handshake state machine
|
|
16
|
+
- Status badge updates
|
|
17
|
+
- Input dispatch (Array → `setXYZ` vs Buffer → `write`)
|
|
18
|
+
- Error coercion (AggregateError vs plain Error)
|
|
19
|
+
- Close cleanup
|
|
20
|
+
|
|
21
|
+
Suggested decomposition:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
ntrip-client-node.js (90 LOC — wiring, lifecycle, status)
|
|
25
|
+
├── parse-config.js (50 LOC — host/port/coords validation, returns options)
|
|
26
|
+
├── handshake-state.js (40 LOC — the `connected` machine + buffer-tail splitter)
|
|
27
|
+
└── input-router.js (30 LOC — Array→setXYZ vs Buffer→write)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This unlocks per-piece unit tests (you can test `handshake-state.js` without a
|
|
31
|
+
socket) and makes future protocol additions (e.g. NTRIP V2 keep-alive
|
|
32
|
+
heartbeat) localisable.
|
|
33
|
+
|
|
34
|
+
## 2. Pull the handshake protocol bytes into a constants module
|
|
35
|
+
|
|
36
|
+
The four authentication-mode handshake strings live as template literals
|
|
37
|
+
inside [ntrip/lib/ntrip-client.js](../../ntrip/lib/ntrip-client.js) `_connect`.
|
|
38
|
+
Moving them to a `lib/handshake-protocols.js` table makes them:
|
|
39
|
+
|
|
40
|
+
- testable without standing up a TCP server
|
|
41
|
+
- comparable side-by-side
|
|
42
|
+
- replaceable per-protocol when a caster requires a quirk header
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// lib/handshake-protocols.js
|
|
46
|
+
module.exports = {
|
|
47
|
+
legacy: ({mp, user, pw}) => user || pw
|
|
48
|
+
? `SOURCE ${pw} /${mp}\r\nSource-Agent: NTRIP ${user}\r\n\r\n`
|
|
49
|
+
: `SOURCE ${mp}\r\n\r\n`,
|
|
50
|
+
hybrid: ({mp, auth}) => `SOURCE ${mp}\r\nAuthorization: Basic ${auth}\r\n\r\n`,
|
|
51
|
+
ntripv1: ({mp, ua, auth}) => `POST /${mp} HTTP/1.0\r\n...`,
|
|
52
|
+
ntripv2: ({mp, host, port, ua, auth}) => `POST /${mp} HTTP/1.1\r\nHost: ${host}:${port}\r\n...`,
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 3. Delegate `_connect` instead of replacing it
|
|
57
|
+
|
|
58
|
+
Today `NtripClientUploader._connect()` is a full re-implementation of the
|
|
59
|
+
upstream `NtripClient._connect()` — copy-paste of timeout/connect/data/end/
|
|
60
|
+
close/error wiring with only the on-connect handshake string differing. Any
|
|
61
|
+
fix to the upstream socket lifecycle won't reach the uploader.
|
|
62
|
+
|
|
63
|
+
A delegation refactor: override only `_buildHandshake()` (a new hook) and call
|
|
64
|
+
`super._connect()`. Requires either upstream cooperation or carefully scoping a
|
|
65
|
+
local fork. See [ADR-0002](adr/0002-ntrip-uploader-extension.md).
|
|
66
|
+
|
|
67
|
+
## 4. Generate the editor HTML from a schema
|
|
68
|
+
|
|
69
|
+
[ntrip/99-ntrip.html](../../ntrip/99-ntrip.html) is ~470 lines, most of which
|
|
70
|
+
is repeated `<dl class="message-properties">` markup for help text. A small
|
|
71
|
+
build step (`prebuild` npm script + JS that reads a JSON/JS schema) would
|
|
72
|
+
shrink the source and eliminate drift between code and help text. Trade-off:
|
|
73
|
+
introduces a build step the package currently doesn't have.
|
|
74
|
+
|
|
75
|
+
## 5. Stop attaching the raw `input` Buffer to every emitted message
|
|
76
|
+
|
|
77
|
+
Decoders today emit `payload.input = <slice of the consumed buffer>` on
|
|
78
|
+
success. For high-rate RTCM streams (~100 frames/s) this means each downstream
|
|
79
|
+
node holds a reference to that buffer until it discards the message — extra GC
|
|
80
|
+
pressure for no functional reason. An emit-on-error-only policy would halve
|
|
81
|
+
memory churn.
|
|
82
|
+
|
|
83
|
+
Caveat: callers may be using `input` for round-trip or replay tooling — would
|
|
84
|
+
need a deprecation cycle.
|
|
85
|
+
|
|
86
|
+
## 6. Pull the `updateStatus` helper into a shared mixin
|
|
87
|
+
|
|
88
|
+
All four node files have a near-identical `updateStatus()` function. Extracting
|
|
89
|
+
it to `lib/node-helpers.js` saves ~12 LOC × 4 files and provides one place to
|
|
90
|
+
adjust the badge format. Marginal savings, but it also unlocks a single place
|
|
91
|
+
to gate badge updates (e.g. throttle to 1 Hz for noisy streams).
|
|
92
|
+
|
|
93
|
+
## 7. Validate the editor form on save, not on connect
|
|
94
|
+
|
|
95
|
+
Currently the editor `validate:` callbacks only run for `host`/`mountpoint`/
|
|
96
|
+
`port` (and were missing on `host`/`mountpoint` before 0.2.5). Stricter
|
|
97
|
+
front-end validation (CR/LF rejection, mountpoint character class) avoids the
|
|
98
|
+
"saved fine, fails at runtime" failure mode and surfaces errors in the editor
|
|
99
|
+
ribbon.
|
|
100
|
+
|
|
101
|
+
## 8. Replace `node.passthrough = config.passthrough || false` with a tagged
|
|
102
|
+
pass-through message
|
|
103
|
+
|
|
104
|
+
When upload-mode pass-through is enabled, written bytes are re-emitted on the
|
|
105
|
+
same output as caster replies, with no tag distinguishing them. A
|
|
106
|
+
`msg._upload_echo = true` flag (or a second output port — breaking change) lets
|
|
107
|
+
downstream nodes tell the streams apart without sniffing the payload.
|
|
108
|
+
|
|
109
|
+
## 9. Extract common Mocha test helpers
|
|
110
|
+
|
|
111
|
+
The three spec files share boilerplate (`helper.startServer` / `helper.unload`,
|
|
112
|
+
flow factory, `helper.load(..., () => ... )` promise wrapping). A
|
|
113
|
+
`test/helpers/load-node.js` that returns `{n1, ok, err, done}` would cut the
|
|
114
|
+
spec files by ~30%.
|