node-red-contrib-ntrip 0.2.10 → 0.2.11
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/CHANGELOG.md +12 -0
- package/README.md +5 -3
- package/examples/watchdog.json +1 -1
- package/ntrip/lib/ntrip-client.js +70 -72
- package/ntrip/nodes/nmea-decoder-node.js +36 -38
- package/ntrip/nodes/nmea-encoder-node.js +97 -97
- package/ntrip/nodes/ntrip-client-node.js +153 -148
- package/ntrip/nodes/rtcm-decoder-node.js +50 -47
- package/ntrip/nodes/rtcm-encoder-node.js +33 -32
- package/package.json +16 -6
- package/.github/dependabot.yml +0 -23
- package/.github/workflows/node.js.yml +0 -32
- package/.github/workflows/release.yml +0 -65
- package/.prettierignore +0 -20
- package/.prettierrc.json +0 -9
- package/CLAUDE.md +0 -73
- package/doc/architecture/README.md +0 -19
- package/doc/architecture/adr/0001-single-registration-file.md +0 -64
- package/doc/architecture/adr/0002-ntrip-uploader-extension.md +0 -71
- package/doc/architecture/adr/0003-two-output-decoder-design.md +0 -73
- package/doc/architecture/adr/0004-stateful-handshake-interception.md +0 -74
- package/doc/architecture/adr/0005-rtcm-partial-frame-buffer.md +0 -87
- package/doc/architecture/adr/0006-nmea-multi-sentence-split.md +0 -66
- package/doc/architecture/adr/0007-mocha-test-helper-stack.md +0 -67
- package/doc/architecture/adr/0008-coordinate-gating-sentinel.md +0 -76
- package/doc/architecture/adr/README.md +0 -18
- package/doc/architecture/architecture-decisions.md +0 -60
- package/doc/architecture/behavioural-design.md +0 -226
- package/doc/architecture/errors-and-weaknesses.md +0 -71
- package/doc/architecture/future-improvements.md +0 -130
- package/doc/architecture/overview.md +0 -77
- package/doc/architecture/refactoring-recommendations.md +0 -114
- package/doc/architecture/statistics.md +0 -118
- package/doc/architecture/structural-design.md +0 -141
- package/eslint.config.js +0 -36
- package/test/nmea-decoder.spec.js +0 -146
- package/test/nmea-encoder.spec.js +0 -104
- package/test/rtcm-decoder.spec.js +0 -116
- package/test/rtcm-encoder.spec.js +0 -144
|
@@ -1,67 +0,0 @@
|
|
|
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
|
|
@@ -1,76 +0,0 @@
|
|
|
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`"
|
|
@@ -1,18 +0,0 @@
|
|
|
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.
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
```
|
|
@@ -1,226 +0,0 @@
|
|
|
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. |
|
|
@@ -1,71 +0,0 @@
|
|
|
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).
|
|
@@ -1,130 +0,0 @@
|
|
|
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.
|