node-red-contrib-ntrip 0.2.3 → 0.2.8

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 (42) hide show
  1. package/.github/dependabot.yml +23 -0
  2. package/.github/workflows/node.js.yml +22 -21
  3. package/.github/workflows/release.yml +65 -0
  4. package/.prettierignore +20 -0
  5. package/.prettierrc.json +9 -0
  6. package/CHANGELOG.md +69 -0
  7. package/CLAUDE.md +73 -0
  8. package/README.md +185 -18
  9. package/doc/architecture/README.md +19 -0
  10. package/doc/architecture/adr/0001-single-registration-file.md +64 -0
  11. package/doc/architecture/adr/0002-ntrip-uploader-extension.md +71 -0
  12. package/doc/architecture/adr/0003-two-output-decoder-design.md +73 -0
  13. package/doc/architecture/adr/0004-stateful-handshake-interception.md +74 -0
  14. package/doc/architecture/adr/0005-rtcm-partial-frame-buffer.md +87 -0
  15. package/doc/architecture/adr/0006-nmea-multi-sentence-split.md +66 -0
  16. package/doc/architecture/adr/0007-mocha-test-helper-stack.md +67 -0
  17. package/doc/architecture/adr/0008-coordinate-gating-sentinel.md +76 -0
  18. package/doc/architecture/adr/README.md +18 -0
  19. package/doc/architecture/architecture-decisions.md +60 -0
  20. package/doc/architecture/behavioural-design.md +226 -0
  21. package/doc/architecture/errors-and-weaknesses.md +71 -0
  22. package/doc/architecture/future-improvements.md +130 -0
  23. package/doc/architecture/overview.md +77 -0
  24. package/doc/architecture/refactoring-recommendations.md +114 -0
  25. package/doc/architecture/statistics.md +118 -0
  26. package/doc/architecture/structural-design.md +141 -0
  27. package/eslint.config.js +36 -0
  28. package/examples/rtcm-encode.json +1 -0
  29. package/ntrip/99-ntrip.html +289 -31
  30. package/ntrip/99-ntrip.js +15 -12
  31. package/ntrip/lib/ntrip-client.js +23 -18
  32. package/ntrip/nodes/nmea-decoder-node.js +77 -34
  33. package/ntrip/nodes/nmea-encoder-node.js +68 -47
  34. package/ntrip/nodes/ntrip-client-node.js +143 -123
  35. package/ntrip/nodes/rtcm-decoder-node.js +65 -47
  36. package/ntrip/nodes/rtcm-encoder-node.js +90 -0
  37. package/package.json +20 -2
  38. package/test/nmea-decoder.spec.js +146 -0
  39. package/test/nmea-encoder.spec.js +104 -0
  40. package/test/rtcm-decoder.spec.js +116 -0
  41. package/test/rtcm-encoder.spec.js +144 -0
  42. 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%.