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,71 @@
1
+ # ADR-0002: NTRIP uploader as full-replacement extension of the upstream client
2
+
3
+ ## Status
4
+
5
+ Accepted in 0.2.0.
6
+
7
+ ## Context
8
+
9
+ The upstream `ntrip-client` npm package implements the *download* side of
10
+ NTRIP only — it connects to a caster and emits received bytes. We needed the
11
+ *upload* side too: open a socket, send a SOURCE / POST handshake, then forward
12
+ locally-produced bytes to the caster (e.g. RTCM from an RTKBase station).
13
+
14
+ Four authentication-mode handshakes are commonly seen in the wild:
15
+
16
+ - `legacy` — `SOURCE <mp>` over plain TCP, rtk2go accepts this.
17
+ - `hybrid` — `SOURCE <mp>` plus a Basic Auth header. SNIP accepts this.
18
+ - `ntripv1` — `POST` over HTTP/1.0.
19
+ - `ntripv2` — `POST` over HTTP/1.1 with `Ntrip-Version: Ntrip/2.0`.
20
+
21
+ The upstream client's `_connect()` method handles only the download handshake.
22
+ Three options to add upload:
23
+
24
+ 1. **Fork the upstream package.** Maintain a copy in this repo. Heavy
25
+ maintenance burden.
26
+ 2. **Submit a PR upstream.** Slow turn-around; upstream may not accept the
27
+ shape we need.
28
+ 3. **Subclass the upstream client locally** and override the connection method
29
+ to write a different handshake. Light footprint.
30
+
31
+ ## Decision
32
+
33
+ Subclass locally. [ntrip/lib/ntrip-client.js](../../../ntrip/lib/ntrip-client.js)
34
+ defines `NtripClientUploader extends NtripClient` and overrides `_connect()`
35
+ to emit the right handshake string per `authmode`.
36
+
37
+ The constructor also validates CR/LF in `mountpoint`/`username`/`password` to
38
+ prevent header injection — the rejection happens at instance construction so
39
+ malicious config never reaches the wire.
40
+
41
+ ## Consequences
42
+
43
+ **Positive**
44
+
45
+ - No fork to maintain. Upstream upgrades flow through (`npm update`).
46
+ - All upload-specific logic is in one file, easy to audit.
47
+ - The CR/LF guard is co-located with the only code that interpolates those
48
+ fields, minimising the chance of a future caller bypassing it.
49
+
50
+ **Negative**
51
+
52
+ - `_connect()` is a *full replacement*, not a delegation. Any improvement
53
+ upstream makes to the connection lifecycle (timeout handling, IPv6, keep-
54
+ alive, TLS upgrade) will not propagate into our uploader path.
55
+ - The subclass depends on internal-ish state of the parent (`this.client`,
56
+ `this.timeout`, `_onError`, `_onData`). A breaking refactor upstream would
57
+ surface as a runtime error in this package.
58
+ - The fork-by-extension makes a future TLS migration harder than it would be
59
+ in a from-scratch implementation.
60
+
61
+ **Trade-offs**
62
+
63
+ - Picking this over a full fork chose *easy upstream upgrades* over *insulation
64
+ from upstream API churn*. The upstream package has been stable for years, so
65
+ the bet has paid off so far.
66
+
67
+ ## Related
68
+
69
+ - Code: [ntrip/lib/ntrip-client.js:1-100](../../../ntrip/lib/ntrip-client.js#L1-L100)
70
+ - Referenced in: [Refactoring Recommendations §3](../refactoring-recommendations.md)
71
+ - Related ADRs: [ADR-0004](0004-stateful-handshake-interception.md) (handshake state machine sits on top of this subclass)
@@ -0,0 +1,73 @@
1
+ # ADR-0003: Two-output `[ok, error]` convention for decoders / encoder
2
+
3
+ ## Status
4
+
5
+ Accepted in the original design, **reaffirmed in 0.2.6** after a brief
6
+ deviation in 0.2.5 that added `node.error()` calls inside the catch blocks.
7
+
8
+ ## Context
9
+
10
+ Each of `RtcmDecoder`, `NmeaDecoder`, `NmeaEncoder` can fail per message —
11
+ unexpected payload type, malformed bytes, unknown sentence/message ID. There
12
+ are two idiomatic Node-RED patterns to report such failures:
13
+
14
+ - **`node.error(err, msg)`** — logs to the Node-RED `[error]` log and, with the
15
+ `msg` second argument, hands the message off to any wired Catch node.
16
+ - **A second output port** — `node.send([okMsg, errMsg])`. Flows wire the
17
+ second port to a debug node, MQTT publish, in-memory queue, etc.
18
+
19
+ Both are legitimate. The choice matters when the decoder is fed high-rate
20
+ mismatched binary — for example, the README's own demo flow wires an
21
+ `NmeaDecoder` onto an `NtripClient` download output as an illustration of
22
+ "the stream can be split", even though RTK2Go only emits RTCM. Every byte that
23
+ arrives produces a NMEA decode failure.
24
+
25
+ In 0.2.5 the catch blocks called both `node.send` *and* `node.error`. Users
26
+ running the demo immediately saw a flood of:
27
+
28
+ ```
29
+ 12 May 19:20:21 - [error] [NmeaDecoder:fa8…] Error: NMEA decode exception: Invalid delimiter (expected $ or !, got �)
30
+ ```
31
+
32
+ — one per chunk, several per second. The log became unusable.
33
+
34
+ ## Decision
35
+
36
+ Per-message decode failures are reported on the **second output only**, never
37
+ via `node.error`. `node.error` is reserved for unhandled-state conditions
38
+ (node construction failure, unsupported configuration) where the node can no
39
+ longer make forward progress.
40
+
41
+ If a downstream flow wants centralised error handling, it should wire the
42
+ second output to its own Catch-equivalent — typically a Function node that
43
+ logs and forwards.
44
+
45
+ ## Consequences
46
+
47
+ **Positive**
48
+
49
+ - Decoders fed mismatched binary stop polluting the Node-RED error log.
50
+ - The error output port is opt-in: ignore it and errors are silently dropped
51
+ (the decoder still bumps `invalidMessagesReceived` and updates the status
52
+ badge, so the user can tell from the editor that something is wrong).
53
+ - Behaviour is consistent across all three transformer nodes.
54
+
55
+ **Negative**
56
+
57
+ - Users coming from packages that use `node.error` for everything need to wire
58
+ the second output explicitly — a small learning-curve cost.
59
+ - The Catch-node integration that `node.error` provides is lost. A workaround
60
+ is to chain a Function node that calls `node.error()` if the user wants
61
+ Catch firing.
62
+
63
+ **Trade-offs**
64
+
65
+ - This favours **operability over discoverability** — a quiet error log is
66
+ more important than out-of-the-box Catch integration, given the documented
67
+ demo wiring.
68
+
69
+ ## Related
70
+
71
+ - Code: [ntrip/nodes/nmea-decoder-node.js:33-42](../../../ntrip/nodes/nmea-decoder-node.js#L33-L42), [ntrip/nodes/rtcm-decoder-node.js:35-55](../../../ntrip/nodes/rtcm-decoder-node.js#L35-L55), [ntrip/nodes/nmea-encoder-node.js:155-166](../../../ntrip/nodes/nmea-encoder-node.js#L155-L166)
72
+ - Regression spec: [test/nmea-decoder.spec.js](../../../test/nmea-decoder.spec.js) `does not call node.error on decode failure (regression: log flood)`
73
+ - CHANGELOG: 0.2.6 "Fixed regressions introduced in 0.2.5"
@@ -0,0 +1,74 @@
1
+ # ADR-0004: Stateful handshake interception in the NtripClient
2
+
3
+ ## Status
4
+
5
+ Accepted in 0.2.5.
6
+
7
+ ## Context
8
+
9
+ The NTRIP caster replies to a successful connection with one of:
10
+
11
+ - `ICY 200 OK\r\n\r\n` followed immediately by the correction stream.
12
+ - `ICY 406 ...\r\n\r\n` (rejected — bad credentials, etc.).
13
+ - `SOURCETABLE 200 OK\r\n\r\n` followed by the source table (returned when the
14
+ client did not specify a mountpoint, or specified an invalid one).
15
+
16
+ These first-byte replies need to drive the node's status badge: green
17
+ "connected", red "rejected", red "no mountpoint". After the handshake is
18
+ processed, all subsequent bytes are application data (RTCM in download mode,
19
+ mountpoint advertisements in upload reverse channel).
20
+
21
+ Up to 0.2.4 the `'data'` listener ran `data.toString().startsWith('ICY 200 OK')`
22
+ on every incoming chunk. This was wrong for two reasons:
23
+
24
+ 1. **The check inflated the message counter.** Each control reply bumped
25
+ `messagesReceived`, polluting the status badge.
26
+ 2. **It could drop RTCM bytes.** TCP is byte-stream — when the caster sent
27
+ `"ICY 200 OK\r\n\r\n" + firstRtcmFrame` in one segment, the prefix match
28
+ succeeded and the whole chunk (including the RTCM tail) was discarded
29
+ instead of forwarded.
30
+
31
+ ## Decision
32
+
33
+ Maintain a `connected: boolean` flag on the node instance. The `'data'`
34
+ listener checks the flag:
35
+
36
+ - **`!connected`** — look at the first ~64 bytes of the chunk for an `ICY`
37
+ or `SOURCETABLE` prefix. If found, update status accordingly and consume the
38
+ handshake. If `ICY 200 OK` was found, also split at `\r\n\r\n` and forward
39
+ the trailing bytes as the first real data frame. Either way, set
40
+ `connected = true` (or, for rejection paths, leave `connected = false` and
41
+ log the error).
42
+ - **`connected`** — every byte is application data; just forward it and bump
43
+ the counter.
44
+
45
+ The flag is reset to `false` on the `'close'` event so the next reconnect
46
+ re-enters the handshake state.
47
+
48
+ ## Consequences
49
+
50
+ **Positive**
51
+
52
+ - No byte loss at the handshake boundary.
53
+ - Counter reflects application data only.
54
+ - Behaviour after reconnect matches behaviour on first connect — no special
55
+ case for reconnects.
56
+
57
+ **Negative**
58
+
59
+ - Two paths in the data listener that future maintainers must keep aligned.
60
+ - A spec for the trailing-bytes split would need a fake caster — currently
61
+ covered by manual testing only. See
62
+ [Future Improvements](../future-improvements.md).
63
+
64
+ **Trade-offs**
65
+
66
+ - Choosing a state-machine over a "stateless prefix check every time" approach
67
+ trades minimal code complexity (one extra boolean) for correct behaviour on
68
+ the boundary case.
69
+
70
+ ## Related
71
+
72
+ - Code: [ntrip/nodes/ntrip-client-node.js:75-115](../../../ntrip/nodes/ntrip-client-node.js#L75-L115)
73
+ - CHANGELOG: 0.2.5 "Fixed NTRIP client node — Handshake detection is now stateful"
74
+ - Related ADRs: [ADR-0002](0002-ntrip-uploader-extension.md) (the underlying client this layer sits on)
@@ -0,0 +1,87 @@
1
+ # ADR-0005: Carry-over buffer for partial RTCM frames
2
+
3
+ ## Status
4
+
5
+ Accepted in 0.2.5.
6
+
7
+ ## Context
8
+
9
+ RTCM 3 frames are variable-length and routinely cross TCP packet boundaries.
10
+ A typical RTCM 1077 (MSM7 GPS) frame is ~200 bytes; an MTU is ~1460 bytes
11
+ payload — perfectly possible for a frame to span two TCP segments, and very
12
+ common when many frames are sent back-to-back.
13
+
14
+ The upstream `@gnss/rtcm` library's `RtcmTransport.decode(buffer)`:
15
+
16
+ - Returns `[message, length]` if `buffer` starts with a complete frame.
17
+ - Throws if the buffer starts with what looks like an RTCM frame but ends
18
+ before the frame does.
19
+
20
+ Up to 0.2.4 the decoder consumed one chunk at a time without any
21
+ buffer-carry-over: every throw inside the chunk caused the loop to break and
22
+ any trailing bytes were discarded. With ~5 frames per packet boundary and
23
+ ~100 frames/s, this lost ~5 % of frames on a busy stream.
24
+
25
+ ## Decision
26
+
27
+ Carry partial-frame bytes across input events:
28
+
29
+ ```javascript
30
+ node.pendingBuffer = Buffer.alloc(0);
31
+
32
+ on('input', msg) {
33
+ let buffer = pendingBuffer.length > 0
34
+ ? Buffer.concat([pendingBuffer, msg.payload])
35
+ : msg.payload;
36
+
37
+ while (buffer.length > 0) {
38
+ try {
39
+ [message, length] = RtcmTransport.decode(buffer);
40
+ } catch (ex) {
41
+ // Likely partial frame — keep buffer for next chunk.
42
+ if (buffer.length > 64KB) {
43
+ // Garbage cap — emit error and clear.
44
+ emit_error(); buffer = empty;
45
+ }
46
+ break;
47
+ }
48
+ if (length <= 0 || length > buffer.length) break; // forward-progress guard
49
+ emit_decoded(); buffer = buffer.slice(length);
50
+ }
51
+ pendingBuffer = buffer;
52
+ }
53
+ ```
54
+
55
+ The 64 KB cap stops the buffer growing unboundedly when the upstream feeds
56
+ non-RTCM bytes. The forward-progress guard stops a hypothetical infinite loop
57
+ on a zero-length decode.
58
+
59
+ ## Consequences
60
+
61
+ **Positive**
62
+
63
+ - Frames that straddle TCP boundaries are decoded correctly — no frame loss
64
+ in normal operation.
65
+ - The cap means a misconfigured flow (e.g. RTCM decoder fed NMEA) eventually
66
+ produces an error rather than running out of memory.
67
+ - The forward-progress guard makes the loop terminate under any decoder
68
+ behaviour.
69
+
70
+ **Negative**
71
+
72
+ - The node now holds a per-instance buffer up to 64 KB. For 100 instances in
73
+ one flow that's 6.4 MB worst-case — not significant, but worth noting.
74
+ - Decode latency at the chunk boundary is slightly higher: the decoder has to
75
+ retry as more bytes arrive. In practice this is one extra round of
76
+ `decode()` per chunk.
77
+
78
+ **Trade-offs**
79
+
80
+ - Chose **correctness on stream boundaries** over **per-call statelessness**.
81
+ A stateless decoder would be easier to reason about, but would lose frames.
82
+
83
+ ## Related
84
+
85
+ - Code: [ntrip/nodes/rtcm-decoder-node.js:14-78](../../../ntrip/nodes/rtcm-decoder-node.js#L14-L78)
86
+ - Regression spec: [test/rtcm-decoder.spec.js](../../../test/rtcm-decoder.spec.js) `reassembles a frame split across two input events (carry-over buffer)`
87
+ - CHANGELOG: 0.2.5 "Fixed RTCM decoder node — Frames that straddle TCP packet boundaries are now buffered"
@@ -0,0 +1,66 @@
1
+ # ADR-0006: Multi-sentence splitting in the NMEA decoder
2
+
3
+ ## Status
4
+
5
+ Accepted in 0.2.5.
6
+
7
+ ## Context
8
+
9
+ A serial GNSS receiver typically emits several NMEA sentences as a batch each
10
+ cycle (e.g. `$GPGGA…\r\n$GPRMC…\r\n$GPGSA…\r\n$GPGSV…\r\n`). Likewise, a TCP
11
+ producer of NMEA data may pack multiple sentences into one segment.
12
+
13
+ Pre-0.2.5 the decoder called `NmeaTransport.decode(buffer)` exactly once on
14
+ whatever it received. If the input contained more than one sentence, only the
15
+ first decoded successfully — the trailing sentences were either lost
16
+ (`NmeaTransport` silently took the prefix) or caused the whole call to throw
17
+ (if the parser was strict). The README claimed the decoder accepted "either a
18
+ single or multiple NMEA messages" — that was untrue.
19
+
20
+ Two approaches to fix:
21
+
22
+ 1. **Buffer-and-split.** Treat the input as a string, split on `\r\n` (or
23
+ `\n`), trim each piece, decode each non-empty piece.
24
+ 2. **Push-down to the library.** Ask `@gnss/nmea` to handle multi-sentence
25
+ input. Out of scope — would require upstream changes.
26
+
27
+ ## Decision
28
+
29
+ Split-and-decode locally. The decoder splits on `/\r?\n/`, trims each chunk,
30
+ skips empty ones, and decodes each remaining sentence individually. Each
31
+ successful decode produces its own output message — so a batch of four
32
+ sentences emits four output messages, not one.
33
+
34
+ Errors are reported per-sentence. The error output uses the original
35
+ `msg.payload` (or `msg.payload.nmeaMessage`) as `input` so a downstream
36
+ debugger sees the raw bytes that arrived, not the per-sentence string that
37
+ failed (see [Behavioural Design](../behavioural-design.md)).
38
+
39
+ ## Consequences
40
+
41
+ **Positive**
42
+
43
+ - Matches the documented behaviour ("accepts single or multiple sentences").
44
+ - Downstream wiring is uniform: one decoded sentence = one output message,
45
+ regardless of how the producer chose to chunk.
46
+ - Naturally handles trailing/leading whitespace and stray empty lines.
47
+
48
+ **Negative**
49
+
50
+ - A chunk that contains both valid and invalid sentences fans out a mix of ok
51
+ and error messages — downstream nodes that assumed "one input ⇒ one output"
52
+ need adjustment.
53
+ - Per-sentence trimming adds a small amount of work compared to a single
54
+ parser invocation. For typical NMEA volumes this is negligible.
55
+
56
+ **Trade-offs**
57
+
58
+ - Picked **per-sentence semantics** (one in, N out) over **chunk semantics**
59
+ (one in, one out) because the value of getting structured per-sentence
60
+ output far outweighs the slight asymmetry.
61
+
62
+ ## Related
63
+
64
+ - Code: [ntrip/nodes/nmea-decoder-node.js:50-89](../../../ntrip/nodes/nmea-decoder-node.js#L50-L89)
65
+ - Regression spec: [test/nmea-decoder.spec.js](../../../test/nmea-decoder.spec.js) `splits a multi-sentence chunk into separate output messages`
66
+ - CHANGELOG: 0.2.5 "Fixed NMEA decoder node — A single chunk containing multiple `\r\n`-delimited NMEA sentences is now decoded sentence by sentence"
@@ -0,0 +1,67 @@
1
+ # ADR-0007: Mocha + node-red-node-test-helper for the test suite
2
+
3
+ ## Status
4
+
5
+ Accepted in 0.2.6.
6
+
7
+ ## Context
8
+
9
+ Up to 0.2.5 the package had no automated tests — three patch releases in two
10
+ weeks shipped regressions that an even minimal suite would have caught.
11
+ Adding tests was overdue. Choices:
12
+
13
+ - **Mocha** vs **Jest** as the runner. Mocha is the dominant choice for
14
+ published `node-red-contrib-*` packages; Jest's auto-mocking and snapshotting
15
+ add nothing this package needs.
16
+ - **`node-red-node-test-helper`** vs **direct invocation of node factories
17
+ with a hand-rolled `RED` stub**. The helper spins up a real Node-RED runtime
18
+ in-process, loads a flow, and exposes wired nodes. A hand-rolled stub would
19
+ let tests run faster but would not exercise the real lifecycle hooks
20
+ (`registerType`, `createNode`, `node.receive`, `node.send`) — which is
21
+ exactly where the recent regressions lived.
22
+ - **`chai` v4 vs v5.** v5 is ESM-only. The package is CommonJS and there is
23
+ no reason to move it; pin v4.
24
+
25
+ ## Decision
26
+
27
+ - Mocha for the test runner.
28
+ - `node-red-node-test-helper` for the in-process Node-RED runtime.
29
+ - `chai` v4 for the assertion DSL.
30
+ - `node-red` itself as a dev dep (required by the helper from v0.3 onwards).
31
+
32
+ Tests live under `test/` with one `*.spec.js` per node. Each previously-fixed
33
+ regression has a dedicated test tagged `(regression: …)` so they can be run
34
+ in isolation with `mocha -g regression`.
35
+
36
+ CI runs `npm test` across the existing matrix (Node 18 / 20 / 22).
37
+
38
+ ## Consequences
39
+
40
+ **Positive**
41
+
42
+ - Real Node-RED runtime — tests verify the actual `registerType` + `createNode`
43
+ + `node.send` path, not a stub.
44
+ - Each spec file is small (~100 LOC) and self-contained.
45
+ - Regression tests are explicit — visible in test names, locked in for the
46
+ long term.
47
+ - CI gate prevents accidental regressions on every push.
48
+
49
+ **Negative**
50
+
51
+ - `node-red` is a ~400-package dev dependency tree, with some known
52
+ vulnerabilities (all dev-only, not reachable in the published artefact).
53
+ - First test run is slower (~80 ms helper startup) than a unit test on plain
54
+ functions would be.
55
+ - Spec authors have to learn the `helper.load(...)` + `helper.getNode(...)`
56
+ +`on('input', …)` pattern — slight learning curve.
57
+
58
+ **Trade-offs**
59
+
60
+ - Chose **realism** over **speed and small dep tree**. For four nodes with
61
+ tight Node-RED integration, integrating the real runtime is worth the
62
+ weight.
63
+
64
+ ## Related
65
+
66
+ - Code: [test/](../../../test/) and [.github/workflows/node.js.yml](../../../.github/workflows/node.js.yml)
67
+ - Statistics: [statistics.md](../statistics.md) "Test suite" section
@@ -0,0 +1,76 @@
1
+ # ADR-0008: `(0, 0, 0)` as the "no location" sentinel for GGA emission
2
+
3
+ ## Status
4
+
5
+ Accepted in 0.2.5.
6
+
7
+ ## Context
8
+
9
+ Many NTRIP casters require a position fix from the client before they will
10
+ stream corrections — they need to know which network-RTK base station to
11
+ deliver corrections from. The standard way to convey the fix is a periodic
12
+ NMEA `GGA` sentence from the client to the caster. The upstream `ntrip-client`
13
+ package's `xyz` option turns on this behaviour: if `xyz` is set, a `GGA` is
14
+ synthesised from those coordinates and sent every `interval` ms.
15
+
16
+ In the editor form the user supplies three numeric fields (X / Y / Z) plus an
17
+ interval. There needs to be a way to say "I have no location, do not send
18
+ GGA" — for casters that *don't* require GGA, sending a synthetic one is
19
+ unnecessary noise.
20
+
21
+ Two options:
22
+
23
+ 1. **A dedicated checkbox / radio** (`Send periodic GGA: yes/no`).
24
+ 2. **A sentinel value** — treat `(0, 0, 0)` as "no location".
25
+
26
+ Pre-0.2.5 the code used `x && y && z` as the gate, which had the wrong
27
+ semantics — *any* zero component (e.g. someone on the equator with X≈6378km,
28
+ Y=0, Z=0) disabled the GGA.
29
+
30
+ ## Decision
31
+
32
+ Use `(0, 0, 0)` as the sentinel. The check is:
33
+
34
+ ```javascript
35
+ if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)
36
+ && !(x === 0 && y === 0 && z === 0)) {
37
+ options.xyz = [x, y, z];
38
+ }
39
+ ```
40
+
41
+ - All three must be finite — `NaN` (the result of `parseFloat('')` on an
42
+ empty form) does not enable GGA.
43
+ - The all-zero triple is the only excluded value.
44
+ - Coordinates with one or two zero components are fine — they fall through to
45
+ the caster.
46
+
47
+ ## Consequences
48
+
49
+ **Positive**
50
+
51
+ - One-field-fewer in the editor form.
52
+ - Equator / axis-aligned positions work correctly.
53
+ - Matches the convention used by other NTRIP tooling (rtklib, str2str): if
54
+ you don't want GGA, you set the coordinates to all zeros.
55
+
56
+ **Negative**
57
+
58
+ - Discoverability — a user looking at the form has no in-UI hint that
59
+ `(0, 0, 0)` is special. The README and the editor help text document this,
60
+ but a fresh user might not read either.
61
+ - The Earth's center of mass at `(0, 0, 0)` ECEF is technically a *real*
62
+ position. Not a meaningful place for a GNSS receiver to be, but the sentinel
63
+ collides with one mathematically valid input.
64
+
65
+ **Trade-offs**
66
+
67
+ - Picked **minimum form surface** over **explicit toggle**. If user feedback
68
+ reports confusion, swap to an explicit checkbox in a future minor release
69
+ and keep the sentinel for backward compatibility.
70
+
71
+ ## Related
72
+
73
+ - Code: [ntrip/nodes/ntrip-client-node.js:43-46](../../../ntrip/nodes/ntrip-client-node.js#L43-L46)
74
+ - Editor help text: [ntrip/99-ntrip.html](../../../ntrip/99-ntrip.html) "Optional ECEF coordinate triple" block
75
+ - README: "Notes on usage — NTRIP Client: coordinates"
76
+ - CHANGELOG: 0.2.5 "Coordinate guard changed from `x && y && z` to `not all zero`"
@@ -0,0 +1,18 @@
1
+ # ADR Index
2
+
3
+ This folder contains Architecture Decision Records. One file per ADR, numbered
4
+ in the order they were accepted.
5
+
6
+ | # | Title | Status | Date accepted |
7
+ |---|-------|--------|---------------|
8
+ | [0001](0001-single-registration-file.md) | Single registration file for all nodes | Accepted | original design |
9
+ | [0002](0002-ntrip-uploader-extension.md) | NTRIP uploader as full-replacement extension of the upstream client | Accepted | 0.2.0 |
10
+ | [0003](0003-two-output-decoder-design.md) | Two-output `[ok, error]` convention for decoders / encoder | Accepted | original design (reaffirmed in 0.2.6) |
11
+ | [0004](0004-stateful-handshake-interception.md) | Stateful handshake interception in the NtripClient | Accepted | 0.2.5 |
12
+ | [0005](0005-rtcm-partial-frame-buffer.md) | Carry-over buffer for partial RTCM frames | Accepted | 0.2.5 |
13
+ | [0006](0006-nmea-multi-sentence-split.md) | Multi-sentence splitting in the NMEA decoder | Accepted | 0.2.5 |
14
+ | [0007](0007-mocha-test-helper-stack.md) | Mocha + node-red-node-test-helper for the test suite | Accepted | 0.2.6 |
15
+ | [0008](0008-coordinate-gating-sentinel.md) | `(0, 0, 0)` as the "no location" sentinel for GGA emission | Accepted | 0.2.5 |
16
+
17
+ See [../architecture-decisions.md](../architecture-decisions.md) for the
18
+ template and the criteria for when to add a new ADR.
@@ -0,0 +1,60 @@
1
+ # Architecture Decisions
2
+
3
+ This chapter is the index of Architecture Decision Records (ADRs). Each
4
+ individual ADR is stored as its own markdown file under [adr/](adr/) and
5
+ follows a compact template (Status / Context / Decision / Consequences /
6
+ Related).
7
+
8
+ ## Index
9
+
10
+ | # | Title | Status |
11
+ |---|-------|--------|
12
+ | [0001](adr/0001-single-registration-file.md) | Single registration file for all nodes | Accepted |
13
+ | [0002](adr/0002-ntrip-uploader-extension.md) | NTRIP uploader as full-replacement extension of the upstream client | Accepted |
14
+ | [0003](adr/0003-two-output-decoder-design.md) | Two-output `[ok, error]` convention for decoders / encoder | Accepted |
15
+ | [0004](adr/0004-stateful-handshake-interception.md) | Stateful handshake interception in the NtripClient | Accepted |
16
+ | [0005](adr/0005-rtcm-partial-frame-buffer.md) | Carry-over buffer for partial RTCM frames | Accepted |
17
+ | [0006](adr/0006-nmea-multi-sentence-split.md) | Multi-sentence splitting in the NMEA decoder | Accepted |
18
+ | [0007](adr/0007-mocha-test-helper-stack.md) | Mocha + node-red-node-test-helper for the test suite | Accepted |
19
+ | [0008](adr/0008-coordinate-gating-sentinel.md) | `(0, 0, 0)` as the "no location" sentinel for GGA emission | Accepted |
20
+
21
+ ## When to add a new ADR
22
+
23
+ Add one when a decision is non-obvious and would surprise a contributor reading
24
+ the code six months from now. Typical signals:
25
+
26
+ - The code does something differently from how upstream / community examples
27
+ do it.
28
+ - The choice was load-bearing — reverting it would break a documented contract.
29
+ - There is a clear trade-off (e.g. simpler code vs. larger memory footprint)
30
+ that future maintainers should understand before changing direction.
31
+
32
+ ## When *not* to add an ADR
33
+
34
+ Skip the formality for routine bug fixes, refactors that don't change external
35
+ behaviour, or decisions that are already obvious from idiomatic style. The
36
+ goal is a small, stable archive — not a write-up of every commit.
37
+
38
+ ## Template
39
+
40
+ ```markdown
41
+ # ADR-NNNN: Short imperative title
42
+
43
+ ## Status
44
+ Proposed | Accepted | Superseded by ADR-MMMM | Deprecated
45
+
46
+ ## Context
47
+ What problem are we solving? What constraints / forces are at play?
48
+
49
+ ## Decision
50
+ What we are going to do, in one or two sentences.
51
+
52
+ ## Consequences
53
+ - Positive: …
54
+ - Negative: …
55
+ - Trade-offs: …
56
+
57
+ ## Related
58
+ - Code: [path/to/file.js:LL-LL](../../path/to/file.js#LLL-LLL)
59
+ - ADRs: [[ADR-XXXX](XXXX-title.md)]
60
+ ```