node-red-contrib-ntrip 0.2.3 → 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 +61 -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/ntrip/99-ntrip.html +207 -31
- package/ntrip/99-ntrip.js +12 -12
- package/ntrip/lib/ntrip-client.js +23 -18
- package/ntrip/nodes/nmea-decoder-node.js +77 -34
- package/ntrip/nodes/nmea-encoder-node.js +68 -47
- 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,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
|
+
```
|