node-red-contrib-ntrip 0.2.9 → 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 +19 -0
- package/README.md +17 -0
- package/examples/watchdog.json +1 -0
- package/ntrip/lib/ntrip-client.js +107 -74
- 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,77 +0,0 @@
|
|
|
1
|
-
# Overview
|
|
2
|
-
|
|
3
|
-
## What this package is
|
|
4
|
-
|
|
5
|
-
`node-red-contrib-ntrip` is a Node-RED contribution package that adds four
|
|
6
|
-
GNSS-related nodes to a Node-RED palette. The package is distributed via npm
|
|
7
|
-
and consumed by users through Node-RED's "Manage palette" UI — there is no
|
|
8
|
-
standalone application binary.
|
|
9
|
-
|
|
10
|
-
The four nodes:
|
|
11
|
-
|
|
12
|
-
| Node | Direction | Library | Purpose |
|
|
13
|
-
|------|-----------|---------|---------|
|
|
14
|
-
| `NtripClient` | network ↔ flow | [`ntrip-client`](https://github.com/dxhbiz/ntrip-client) (extended) | Connect to an NTRIP caster as either a *client* (download corrections) or a *server* (upload corrections to a mountpoint). |
|
|
15
|
-
| `RtcmDecoder` | binary → object | [`@gnss/rtcm`](https://github.com/node-ntrip/rtcm) | Decode RTCM 3 binary frames into JavaScript objects. |
|
|
16
|
-
| `NmeaDecoder` | text → object | [`@gnss/nmea`](https://github.com/node-ntrip/nmea) | Decode NMEA 0183 ASCII sentences into JavaScript objects. |
|
|
17
|
-
| `NmeaEncoder` | object → text | [`@gnss/nmea`](https://github.com/node-ntrip/nmea) | The inverse — emit a NMEA sentence string from an object. |
|
|
18
|
-
|
|
19
|
-
## Who uses it
|
|
20
|
-
|
|
21
|
-
Practitioners building RTK/PPP/DGNSS pipelines in Node-RED — typically:
|
|
22
|
-
|
|
23
|
-
- Receiving correction streams from a public NTRIP caster (RTK2Go, SAPOS,
|
|
24
|
-
Centipede, Onocoy) and forwarding them to a rover (e.g. an ESP32-XBee, an
|
|
25
|
-
RTKLib instance, a u-blox receiver) over TCP, Serial, MQTT.
|
|
26
|
-
- Operating a private base station (e.g. RTKBase) and pushing corrections to a
|
|
27
|
-
caster for others to consume.
|
|
28
|
-
- Logging or inspecting GNSS messages live for monitoring or debugging.
|
|
29
|
-
|
|
30
|
-
## How the nodes fit together
|
|
31
|
-
|
|
32
|
-
A typical flow looks like:
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
┌────────────────┐
|
|
36
|
-
│ RTCM Decoder │──► (per-frame objects, output 1)
|
|
37
|
-
│ │──► (decode errors, output 2)
|
|
38
|
-
┌────────────────┐ RTCM bytes └────────────────┘
|
|
39
|
-
│ NTRIP Client │──────────────►
|
|
40
|
-
│ (download) │ ┌────────────────┐
|
|
41
|
-
└────────────────┘ │ NMEA Decoder │──► (per-sentence objects, output 1)
|
|
42
|
-
│ │──► (decode errors, output 2)
|
|
43
|
-
└────────────────┘
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
┌─────────────────┐ RTCM bytes ┌────────────────┐
|
|
48
|
-
│ TCP In / RTK │──────────────►│ NTRIP Client │──► (caster replies, optional pass-through)
|
|
49
|
-
│ Base / etc. │ │ (upload) │
|
|
50
|
-
└─────────────────┘ └────────────────┘
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
For VRS / network-RTK casters that require a position fix from the client:
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
┌─────────────┐ msg.payload=[x,y,z] ┌────────────────┐
|
|
57
|
-
│ Position │────────────────────►│ NTRIP Client │──(internal: synthesise NMEA GGA periodically)──► caster
|
|
58
|
-
│ source │ │ (download) │
|
|
59
|
-
└─────────────┘ └────────────────┘
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Runtime model
|
|
63
|
-
|
|
64
|
-
- **Pure JavaScript, no build step.** Files in [ntrip/](../../ntrip/) are loaded by Node-RED at startup.
|
|
65
|
-
- **Single registration entry point** — [ntrip/99-ntrip.js](../../ntrip/99-ntrip.js) `require`s the four node implementations and calls `RED.nodes.registerType` for each. See [ADR-0001](adr/0001-single-registration-file.md).
|
|
66
|
-
- **Editor HTML co-located** — all four node configuration dialogs and help text live in [ntrip/99-ntrip.html](../../ntrip/99-ntrip.html).
|
|
67
|
-
- **Node ≥ 18** — runtime requires `AggregateError` and modern syntax.
|
|
68
|
-
- **Stateless across restarts** — node configuration is persisted by Node-RED's flow file; the nodes themselves carry only in-memory state (counters, the NTRIP socket, the RTCM pending buffer).
|
|
69
|
-
|
|
70
|
-
## Reading order for new contributors
|
|
71
|
-
|
|
72
|
-
1. This file.
|
|
73
|
-
2. [Structural Design](structural-design.md) — how files and modules relate.
|
|
74
|
-
3. [Behavioural Design](behavioural-design.md) — runtime flow, state machines, error paths.
|
|
75
|
-
4. [Architecture Decisions](architecture-decisions.md) — links to each ADR.
|
|
76
|
-
5. [Errors and Weaknesses](errors-and-weaknesses.md) — current known limitations.
|
|
77
|
-
6. [Statistics](statistics.md) — LOC, test count, and a simple quality index.
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# Refactoring Recommendations
|
|
2
|
-
|
|
3
|
-
Items below are *recommendations*, not blockers. They are ordered by leverage —
|
|
4
|
-
the changes near the top would simplify the code or unlock further improvements;
|
|
5
|
-
the ones near the bottom are nice-to-have polish.
|
|
6
|
-
|
|
7
|
-
## 1. Split `NtripClient` node into mode-specific handlers
|
|
8
|
-
|
|
9
|
-
[ntrip/nodes/ntrip-client-node.js](../../ntrip/nodes/ntrip-client-node.js) is the
|
|
10
|
-
biggest file in the package (~180 LOC) and mixes:
|
|
11
|
-
|
|
12
|
-
- Configuration parsing and validation
|
|
13
|
-
- Coordinate gating
|
|
14
|
-
- Download vs upload branching
|
|
15
|
-
- Handshake state machine
|
|
16
|
-
- Status badge updates
|
|
17
|
-
- Input dispatch (Array → `setXYZ` vs Buffer → `write`)
|
|
18
|
-
- Error coercion (AggregateError vs plain Error)
|
|
19
|
-
- Close cleanup
|
|
20
|
-
|
|
21
|
-
Suggested decomposition:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
ntrip-client-node.js (90 LOC — wiring, lifecycle, status)
|
|
25
|
-
├── parse-config.js (50 LOC — host/port/coords validation, returns options)
|
|
26
|
-
├── handshake-state.js (40 LOC — the `connected` machine + buffer-tail splitter)
|
|
27
|
-
└── input-router.js (30 LOC — Array→setXYZ vs Buffer→write)
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
This unlocks per-piece unit tests (you can test `handshake-state.js` without a
|
|
31
|
-
socket) and makes future protocol additions (e.g. NTRIP V2 keep-alive
|
|
32
|
-
heartbeat) localisable.
|
|
33
|
-
|
|
34
|
-
## 2. Pull the handshake protocol bytes into a constants module
|
|
35
|
-
|
|
36
|
-
The four authentication-mode handshake strings live as template literals
|
|
37
|
-
inside [ntrip/lib/ntrip-client.js](../../ntrip/lib/ntrip-client.js) `_connect`.
|
|
38
|
-
Moving them to a `lib/handshake-protocols.js` table makes them:
|
|
39
|
-
|
|
40
|
-
- testable without standing up a TCP server
|
|
41
|
-
- comparable side-by-side
|
|
42
|
-
- replaceable per-protocol when a caster requires a quirk header
|
|
43
|
-
|
|
44
|
-
```javascript
|
|
45
|
-
// lib/handshake-protocols.js
|
|
46
|
-
module.exports = {
|
|
47
|
-
legacy: ({mp, user, pw}) => user || pw
|
|
48
|
-
? `SOURCE ${pw} /${mp}\r\nSource-Agent: NTRIP ${user}\r\n\r\n`
|
|
49
|
-
: `SOURCE ${mp}\r\n\r\n`,
|
|
50
|
-
hybrid: ({mp, auth}) => `SOURCE ${mp}\r\nAuthorization: Basic ${auth}\r\n\r\n`,
|
|
51
|
-
ntripv1: ({mp, ua, auth}) => `POST /${mp} HTTP/1.0\r\n...`,
|
|
52
|
-
ntripv2: ({mp, host, port, ua, auth}) => `POST /${mp} HTTP/1.1\r\nHost: ${host}:${port}\r\n...`,
|
|
53
|
-
};
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## 3. Delegate `_connect` instead of replacing it
|
|
57
|
-
|
|
58
|
-
Today `NtripClientUploader._connect()` is a full re-implementation of the
|
|
59
|
-
upstream `NtripClient._connect()` — copy-paste of timeout/connect/data/end/
|
|
60
|
-
close/error wiring with only the on-connect handshake string differing. Any
|
|
61
|
-
fix to the upstream socket lifecycle won't reach the uploader.
|
|
62
|
-
|
|
63
|
-
A delegation refactor: override only `_buildHandshake()` (a new hook) and call
|
|
64
|
-
`super._connect()`. Requires either upstream cooperation or carefully scoping a
|
|
65
|
-
local fork. See [ADR-0002](adr/0002-ntrip-uploader-extension.md).
|
|
66
|
-
|
|
67
|
-
## 4. Generate the editor HTML from a schema
|
|
68
|
-
|
|
69
|
-
[ntrip/99-ntrip.html](../../ntrip/99-ntrip.html) is ~470 lines, most of which
|
|
70
|
-
is repeated `<dl class="message-properties">` markup for help text. A small
|
|
71
|
-
build step (`prebuild` npm script + JS that reads a JSON/JS schema) would
|
|
72
|
-
shrink the source and eliminate drift between code and help text. Trade-off:
|
|
73
|
-
introduces a build step the package currently doesn't have.
|
|
74
|
-
|
|
75
|
-
## 5. Stop attaching the raw `input` Buffer to every emitted message
|
|
76
|
-
|
|
77
|
-
Decoders today emit `payload.input = <slice of the consumed buffer>` on
|
|
78
|
-
success. For high-rate RTCM streams (~100 frames/s) this means each downstream
|
|
79
|
-
node holds a reference to that buffer until it discards the message — extra GC
|
|
80
|
-
pressure for no functional reason. An emit-on-error-only policy would halve
|
|
81
|
-
memory churn.
|
|
82
|
-
|
|
83
|
-
Caveat: callers may be using `input` for round-trip or replay tooling — would
|
|
84
|
-
need a deprecation cycle.
|
|
85
|
-
|
|
86
|
-
## 6. Pull the `updateStatus` helper into a shared mixin
|
|
87
|
-
|
|
88
|
-
All four node files have a near-identical `updateStatus()` function. Extracting
|
|
89
|
-
it to `lib/node-helpers.js` saves ~12 LOC × 4 files and provides one place to
|
|
90
|
-
adjust the badge format. Marginal savings, but it also unlocks a single place
|
|
91
|
-
to gate badge updates (e.g. throttle to 1 Hz for noisy streams).
|
|
92
|
-
|
|
93
|
-
## 7. Validate the editor form on save, not on connect
|
|
94
|
-
|
|
95
|
-
Currently the editor `validate:` callbacks only run for `host`/`mountpoint`/
|
|
96
|
-
`port` (and were missing on `host`/`mountpoint` before 0.2.5). Stricter
|
|
97
|
-
front-end validation (CR/LF rejection, mountpoint character class) avoids the
|
|
98
|
-
"saved fine, fails at runtime" failure mode and surfaces errors in the editor
|
|
99
|
-
ribbon.
|
|
100
|
-
|
|
101
|
-
## 8. Replace `node.passthrough = config.passthrough || false` with a tagged
|
|
102
|
-
pass-through message
|
|
103
|
-
|
|
104
|
-
When upload-mode pass-through is enabled, written bytes are re-emitted on the
|
|
105
|
-
same output as caster replies, with no tag distinguishing them. A
|
|
106
|
-
`msg._upload_echo = true` flag (or a second output port — breaking change) lets
|
|
107
|
-
downstream nodes tell the streams apart without sniffing the payload.
|
|
108
|
-
|
|
109
|
-
## 9. Extract common Mocha test helpers
|
|
110
|
-
|
|
111
|
-
The three spec files share boilerplate (`helper.startServer` / `helper.unload`,
|
|
112
|
-
flow factory, `helper.load(..., () => ... )` promise wrapping). A
|
|
113
|
-
`test/helpers/load-node.js` that returns `{n1, ok, err, done}` would cut the
|
|
114
|
-
spec files by ~30%.
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# Statistics
|
|
2
|
-
|
|
3
|
-
Snapshot taken at **v0.2.6** (commit `ecf02ba`).
|
|
4
|
-
|
|
5
|
-
## Lines of code
|
|
6
|
-
|
|
7
|
-
### Source
|
|
8
|
-
|
|
9
|
-
| File | LOC |
|
|
10
|
-
|------|----:|
|
|
11
|
-
| [ntrip/99-ntrip.html](../../ntrip/99-ntrip.html) | 467 |
|
|
12
|
-
| [ntrip/nodes/ntrip-client-node.js](../../ntrip/nodes/ntrip-client-node.js) | 182 |
|
|
13
|
-
| [ntrip/nodes/nmea-encoder-node.js](../../ntrip/nodes/nmea-encoder-node.js) | 177 |
|
|
14
|
-
| [ntrip/lib/ntrip-client.js](../../ntrip/lib/ntrip-client.js) | 114 |
|
|
15
|
-
| [ntrip/nodes/nmea-decoder-node.js](../../ntrip/nodes/nmea-decoder-node.js) | 98 |
|
|
16
|
-
| [ntrip/nodes/rtcm-decoder-node.js](../../ntrip/nodes/rtcm-decoder-node.js) | 91 |
|
|
17
|
-
| [ntrip/99-ntrip.js](../../ntrip/99-ntrip.js) | 26 |
|
|
18
|
-
| **Total** | **1155** |
|
|
19
|
-
|
|
20
|
-
- JavaScript-only source: **688 LOC** across 6 files.
|
|
21
|
-
- HTML editor UI: **467 LOC** in one file.
|
|
22
|
-
|
|
23
|
-
### Tests
|
|
24
|
-
|
|
25
|
-
| File | LOC | Tests |
|
|
26
|
-
|------|----:|------:|
|
|
27
|
-
| [test/nmea-decoder.spec.js](../../test/nmea-decoder.spec.js) | 134 | 6 |
|
|
28
|
-
| [test/rtcm-decoder.spec.js](../../test/rtcm-decoder.spec.js) | 108 | 4 |
|
|
29
|
-
| [test/nmea-encoder.spec.js](../../test/nmea-encoder.spec.js) | 96 | 4 |
|
|
30
|
-
| **Total** | **338** | **14** |
|
|
31
|
-
|
|
32
|
-
### Ratios
|
|
33
|
-
|
|
34
|
-
| Metric | Value |
|
|
35
|
-
|--------|------:|
|
|
36
|
-
| Test LOC / JS source LOC | 49 % |
|
|
37
|
-
| Tests per JS source file (4 of 6 covered by specs) | 3.5 avg |
|
|
38
|
-
| Comments + blank lines (rough estimate) | ~20 % of source |
|
|
39
|
-
|
|
40
|
-
## Test suite
|
|
41
|
-
|
|
42
|
-
Run with `npm test`. Latest local run: **14 passing, 0 failing, ~500 ms**.
|
|
43
|
-
|
|
44
|
-
Breakdown:
|
|
45
|
-
|
|
46
|
-
- **NmeaDecoder** (6 specs) — golden GGA decode, multi-sentence split, Buffer
|
|
47
|
-
input, `payload.nmeaMessage` shape, Buffer preservation in error output, no
|
|
48
|
-
`node.error` flood (regression).
|
|
49
|
-
- **NmeaEncoder** (4 specs) — `NmeaMessage` instance passthrough, unknown
|
|
50
|
-
`messageType` routed to error output, missing fields routed to error output,
|
|
51
|
-
empty payload handled.
|
|
52
|
-
- **RtcmDecoder** (4 specs) — golden RTCM 1005 decode, two concatenated frames
|
|
53
|
-
fan out, frame split across two input events reassembled, no `node.error`
|
|
54
|
-
flood under garbage (regression).
|
|
55
|
-
|
|
56
|
-
3 of the 14 specs are explicitly tagged `(regression: …)` and lock in
|
|
57
|
-
previously-shipped bug fixes. Run them in isolation with
|
|
58
|
-
`npx mocha test/**/*.spec.js -g regression`.
|
|
59
|
-
|
|
60
|
-
NtripClient is *not* covered by the suite — it requires a fake TCP server.
|
|
61
|
-
See [Future Improvements](future-improvements.md).
|
|
62
|
-
|
|
63
|
-
## Coverage
|
|
64
|
-
|
|
65
|
-
**Not measured.** No `c8` / `nyc` / Istanbul integration is configured. To
|
|
66
|
-
enable, add `c8` as a dev dep and a `coverage` script (`c8 --reporter=text
|
|
67
|
-
--reporter=html mocha test/**/*.spec.js`). Statement coverage of the three
|
|
68
|
-
spec'd nodes should be ≥ 85 % at the current spec count; the missing 15 % is
|
|
69
|
-
mostly edge-case branches inside `RtcmTransport.decode` failures.
|
|
70
|
-
|
|
71
|
-
## Repository history
|
|
72
|
-
|
|
73
|
-
Snapshot from `git log`:
|
|
74
|
-
|
|
75
|
-
| Metric | Value |
|
|
76
|
-
|--------|------:|
|
|
77
|
-
| Total commits on `main` | 28 |
|
|
78
|
-
| Commits matching `/fix/i` | 8 |
|
|
79
|
-
| Releases tagged in [CHANGELOG.md](../../CHANGELOG.md) | 9 (0.1.0 → 0.2.6) |
|
|
80
|
-
| Bug-fix-only releases (0.2.4 / 0.2.5 / 0.2.6) | 3 (last 14 days) |
|
|
81
|
-
|
|
82
|
-
## Dependencies
|
|
83
|
-
|
|
84
|
-
| Class | Count | Names |
|
|
85
|
-
|-------|------:|-------|
|
|
86
|
-
| Runtime | 3 | `@gnss/nmea`, `@gnss/rtcm`, `ntrip-client` |
|
|
87
|
-
| Dev | 4 | `mocha`, `chai`, `node-red`, `node-red-node-test-helper` |
|
|
88
|
-
| Transitive (dev tree) | ~600 | from `npm audit` |
|
|
89
|
-
|
|
90
|
-
After installing dev deps: `6 vulnerabilities (1 low, 2 moderate, 3 high)` —
|
|
91
|
-
all inside the `node-red` dev tree, none reachable from the published package
|
|
92
|
-
because the runtime dep set is just three packages.
|
|
93
|
-
|
|
94
|
-
## Quality Index
|
|
95
|
-
|
|
96
|
-
A composite, intentionally rough — a way to track movement over time, not an
|
|
97
|
-
absolute scale. Each criterion is scored 0–10; the index is the unweighted
|
|
98
|
-
mean.
|
|
99
|
-
|
|
100
|
-
| Criterion | Score | Reasoning |
|
|
101
|
-
|-----------|------:|-----------|
|
|
102
|
-
| Test coverage (proxy: spec count vs node count) | 7 | 14 specs over 3 of 4 nodes. `NtripClient` untested. |
|
|
103
|
-
| Bug-fix cadence (lower is better, scored inversely) | 5 | 3 patch releases in 2 weeks indicates churn from recent rewrite — settling now. |
|
|
104
|
-
| Documentation completeness | 8 | Per-node help text, README usage notes, this architecture doc. |
|
|
105
|
-
| Type safety | 2 | No TypeScript or JSDoc-with-`@ts-check`. |
|
|
106
|
-
| Lint / style enforcement | 1 | No `eslint` or `prettier`. Style is consistent by convention only. |
|
|
107
|
-
| Dependency hygiene | 7 | 3 runtime deps, 0 known runtime vulns. Dev tree carries `node-red` itself. |
|
|
108
|
-
| CI rigour | 6 | Builds + tests on three Node versions. No coverage gate, no lint gate. |
|
|
109
|
-
| Bus factor | 4 | Single primary maintainer (`Karl-Heinz Wind`). 1 active contributor. |
|
|
110
|
-
| **Overall** | **5.0** | Median: 5.5. Headroom in lint, types, and `NtripClient` coverage. |
|
|
111
|
-
|
|
112
|
-
### How to improve the index quickly
|
|
113
|
-
|
|
114
|
-
- **+1.0 each** — adopting `eslint` (covers Type safety bump via `@ts-check`
|
|
115
|
-
on JSDoc) and adding coverage reporting.
|
|
116
|
-
- **+0.5** — writing one round of `NtripClient` integration specs against a
|
|
117
|
-
fake caster.
|
|
118
|
-
- **+0.5** — pinning dev deps in `package.json` more strictly (`~` instead of `^`).
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# Structural Design
|
|
2
|
-
|
|
3
|
-
## Repository layout
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
node-red-contrib-ntrip/
|
|
7
|
-
├── ntrip/
|
|
8
|
-
│ ├── 99-ntrip.js # Node-RED registration entry point
|
|
9
|
-
│ ├── 99-ntrip.html # Editor UI for all four nodes (forms + help text)
|
|
10
|
-
│ ├── nodes/
|
|
11
|
-
│ │ ├── ntrip-client-node.js
|
|
12
|
-
│ │ ├── rtcm-decoder-node.js
|
|
13
|
-
│ │ ├── nmea-decoder-node.js
|
|
14
|
-
│ │ └── nmea-encoder-node.js
|
|
15
|
-
│ └── lib/
|
|
16
|
-
│ └── ntrip-client.js # Local extension of the upstream ntrip-client package
|
|
17
|
-
├── examples/ # Sample flows (ntripclient.json, sapos.json, upload.json, …)
|
|
18
|
-
├── test/ # Mocha specs (one per node, suffixed .spec.js)
|
|
19
|
-
├── images/ # Screenshots referenced by README.md
|
|
20
|
-
├── doc/architecture/ # This documentation tree
|
|
21
|
-
├── package.json
|
|
22
|
-
├── package-lock.json
|
|
23
|
-
├── CHANGELOG.md
|
|
24
|
-
├── CLAUDE.md
|
|
25
|
-
└── README.md
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Module dependency graph
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
┌────────────────────────┐
|
|
32
|
-
│ ntrip/99-ntrip.js │ (Node-RED entry, registerType x 4)
|
|
33
|
-
└─────────┬──────────────┘
|
|
34
|
-
│ require
|
|
35
|
-
┌─────────┴────────────────────────────────────────────────────────┐
|
|
36
|
-
▼ ▼ ▼ ▼ │
|
|
37
|
-
ntrip-client- rtcm-decoder- nmea-decoder- nmea-encoder- │
|
|
38
|
-
node.js node.js node.js node.js │
|
|
39
|
-
│ │ │ │ │
|
|
40
|
-
│ require │ require │ require │ require │
|
|
41
|
-
▼ ▼ ▼ ▼ │
|
|
42
|
-
lib/ntrip- @gnss/rtcm @gnss/nmea @gnss/nmea │
|
|
43
|
-
client.js │
|
|
44
|
-
│ │
|
|
45
|
-
│ require │
|
|
46
|
-
▼ │
|
|
47
|
-
ntrip-client (npm package, upstream) │
|
|
48
|
-
│
|
|
49
|
-
▼
|
|
50
|
-
package.json reads
|
|
51
|
-
version into log line
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## File responsibilities
|
|
55
|
-
|
|
56
|
-
### Entry layer
|
|
57
|
-
|
|
58
|
-
| File | Responsibility |
|
|
59
|
-
|------|----------------|
|
|
60
|
-
| [ntrip/99-ntrip.js](../../ntrip/99-ntrip.js) | Imports each node implementation. Registers the four types with `RED.nodes.registerType`. Declares the `credentials` block for `NtripClient`. Reads the package version from `package.json` and logs it. |
|
|
61
|
-
| [ntrip/99-ntrip.html](../../ntrip/99-ntrip.html) | Per-node `registerType` call (client side), the configuration `<template>`, and the help text shown in Node-RED's info sidebar. The credentials block is duplicated here — Node-RED requires both. |
|
|
62
|
-
|
|
63
|
-
### Node implementations
|
|
64
|
-
|
|
65
|
-
| File | Type | Inputs | Outputs |
|
|
66
|
-
|------|------|--------|---------|
|
|
67
|
-
| [ntrip/nodes/ntrip-client-node.js](../../ntrip/nodes/ntrip-client-node.js) | `NtripClient` | 1 | 1 (data from caster + optional pass-through of writes) |
|
|
68
|
-
| [ntrip/nodes/rtcm-decoder-node.js](../../ntrip/nodes/rtcm-decoder-node.js) | `RtcmDecoder` | 1 | 2 (decoded, error) |
|
|
69
|
-
| [ntrip/nodes/nmea-decoder-node.js](../../ntrip/nodes/nmea-decoder-node.js) | `NmeaDecoder` | 1 | 2 (decoded, error) |
|
|
70
|
-
| [ntrip/nodes/nmea-encoder-node.js](../../ntrip/nodes/nmea-encoder-node.js) | `NmeaEncoder` | 1 | 2 (encoded, error) |
|
|
71
|
-
|
|
72
|
-
All four follow the same skeleton:
|
|
73
|
-
|
|
74
|
-
```javascript
|
|
75
|
-
module.exports = function (RED) {
|
|
76
|
-
function Node(config) {
|
|
77
|
-
RED.nodes.createNode(this, config);
|
|
78
|
-
const node = this;
|
|
79
|
-
node.<counter>Received = 0;
|
|
80
|
-
node.invalidMessagesReceived = 0;
|
|
81
|
-
|
|
82
|
-
// ... wire up the on('input') handler, possibly an external resource
|
|
83
|
-
|
|
84
|
-
this.on('close', function (done) {
|
|
85
|
-
// cleanup
|
|
86
|
-
node.status({});
|
|
87
|
-
done();
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
return Node;
|
|
91
|
-
};
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Library layer
|
|
95
|
-
|
|
96
|
-
[ntrip/lib/ntrip-client.js](../../ntrip/lib/ntrip-client.js) — a thin local
|
|
97
|
-
adapter around the upstream `ntrip-client` npm package. Exposes two factories:
|
|
98
|
-
|
|
99
|
-
- `createDownloader(options)` — returns the upstream `NtripClient` unchanged.
|
|
100
|
-
- `createUploader(options)` — returns a subclass `NtripClientUploader` that
|
|
101
|
-
overrides `_connect()` to switch the handshake string by `authmode`
|
|
102
|
-
(`legacy`, `hybrid`, `ntripv1`, `ntripv2`). The constructor also rejects
|
|
103
|
-
CR/LF in `mountpoint`/`username`/`password` to prevent header injection.
|
|
104
|
-
|
|
105
|
-
The override pattern is a full replacement of the parent's `_connect`, not a
|
|
106
|
-
delegation — see [ADR-0002](adr/0002-ntrip-uploader-extension.md) for the
|
|
107
|
-
rationale and trade-offs.
|
|
108
|
-
|
|
109
|
-
## Class / type hierarchy
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
ntrip-client (npm)
|
|
113
|
-
└── NtripClient # download
|
|
114
|
-
└── NtripClientUploader (lib/ntrip-client.js) # upload
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
No other classes are defined in this package; every Node-RED node is a plain
|
|
118
|
-
function passed to `registerType`, and decoder/encoder state lives on the node
|
|
119
|
-
instance via simple property assignment (`node.pendingBuffer`, `node.messagesReceived`).
|
|
120
|
-
|
|
121
|
-
## Coupling
|
|
122
|
-
|
|
123
|
-
- **High cohesion within each node file.** A node's input handler, status
|
|
124
|
-
updates, and close handler are co-located. There is no shared mutable state
|
|
125
|
-
across node instances.
|
|
126
|
-
- **No cross-node imports.** The decoders do not know about `NtripClient` and
|
|
127
|
-
vice versa; they communicate only through Node-RED wires at flow runtime.
|
|
128
|
-
- **Two seams to the outside world:**
|
|
129
|
-
- The `ntrip-client` upstream package (via [lib/ntrip-client.js](../../ntrip/lib/ntrip-client.js)).
|
|
130
|
-
- `@gnss/rtcm` and `@gnss/nmea` for codec work.
|
|
131
|
-
- **Implicit coupling between [99-ntrip.js](../../ntrip/99-ntrip.js) and [99-ntrip.html](../../ntrip/99-ntrip.html)** — both must declare the `credentials` block, and the registered node-type names must match exactly. The HTML form field IDs (`node-input-host`, `node-input-port`, …) must align with the property names read in the JS (`config.host`, `config.port`, …).
|
|
132
|
-
|
|
133
|
-
## Distribution shape
|
|
134
|
-
|
|
135
|
-
- `node-red.nodes` in [package.json](../../package.json) points only at
|
|
136
|
-
`ntrip/99-ntrip.js` — Node-RED loads that file, which transitively pulls in
|
|
137
|
-
everything else.
|
|
138
|
-
- Runtime dependencies: `@gnss/nmea`, `@gnss/rtcm`, `ntrip-client`. No transient
|
|
139
|
-
runtime dependency on Node-RED itself.
|
|
140
|
-
- Dev-only dependencies: `mocha`, `chai` (CJS v4), `node-red`,
|
|
141
|
-
`node-red-node-test-helper`.
|
package/eslint.config.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const js = require('@eslint/js');
|
|
4
|
-
const globals = require('globals');
|
|
5
|
-
const prettierConfig = require('eslint-config-prettier');
|
|
6
|
-
|
|
7
|
-
module.exports = [
|
|
8
|
-
{
|
|
9
|
-
ignores: ['node_modules/**', 'examples/**', 'doc/**', 'ntrip/99-ntrip.html'],
|
|
10
|
-
},
|
|
11
|
-
js.configs.recommended,
|
|
12
|
-
prettierConfig,
|
|
13
|
-
{
|
|
14
|
-
languageOptions: {
|
|
15
|
-
ecmaVersion: 2022,
|
|
16
|
-
sourceType: 'commonjs',
|
|
17
|
-
globals: {
|
|
18
|
-
...globals.node,
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
rules: {
|
|
22
|
-
'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
|
23
|
-
'no-console': 'off',
|
|
24
|
-
'no-prototype-builtins': 'off',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
files: ['test/**/*.js'],
|
|
29
|
-
languageOptions: {
|
|
30
|
-
globals: {
|
|
31
|
-
...globals.node,
|
|
32
|
-
...globals.mocha,
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
];
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const helper = require('node-red-node-test-helper');
|
|
4
|
-
const { expect } = require('chai');
|
|
5
|
-
const ntripModule = require('../ntrip/99-ntrip.js');
|
|
6
|
-
|
|
7
|
-
helper.init(require.resolve('node-red'));
|
|
8
|
-
|
|
9
|
-
const GGA = '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47';
|
|
10
|
-
const GLL = '$GPGLL,4916.45,N,12311.12,W,225444,A,*1D';
|
|
11
|
-
|
|
12
|
-
function flow() {
|
|
13
|
-
return [
|
|
14
|
-
{ id: 'n1', type: 'NmeaDecoder', wires: [['ok'], ['err']] },
|
|
15
|
-
{ id: 'ok', type: 'helper' },
|
|
16
|
-
{ id: 'err', type: 'helper' },
|
|
17
|
-
];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('NmeaDecoder', function () {
|
|
21
|
-
before(() => helper.startServer());
|
|
22
|
-
after(() => helper.stopServer());
|
|
23
|
-
afterEach(() => helper.unload());
|
|
24
|
-
|
|
25
|
-
it('decodes a valid GGA sentence on the success output', function () {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
helper.load(ntripModule, flow(), () => {
|
|
28
|
-
const n1 = helper.getNode('n1');
|
|
29
|
-
const ok = helper.getNode('ok');
|
|
30
|
-
ok.on('input', (msg) => {
|
|
31
|
-
try {
|
|
32
|
-
expect(msg.payload.messageType).to.equal('GGA');
|
|
33
|
-
expect(msg.payload.nmeaMessage).to.have.property('latitude');
|
|
34
|
-
expect(msg.payload.input).to.equal(GGA);
|
|
35
|
-
resolve();
|
|
36
|
-
} catch (e) {
|
|
37
|
-
reject(e);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
n1.receive({ payload: GGA });
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('splits a multi-sentence chunk into separate output messages', function () {
|
|
46
|
-
return new Promise((resolve, reject) => {
|
|
47
|
-
helper.load(ntripModule, flow(), () => {
|
|
48
|
-
const n1 = helper.getNode('n1');
|
|
49
|
-
const ok = helper.getNode('ok');
|
|
50
|
-
const types = [];
|
|
51
|
-
ok.on('input', (msg) => {
|
|
52
|
-
types.push(msg.payload.messageType);
|
|
53
|
-
if (types.length === 2) {
|
|
54
|
-
try {
|
|
55
|
-
expect(types).to.deep.equal(['GGA', 'GLL']);
|
|
56
|
-
resolve();
|
|
57
|
-
} catch (e) {
|
|
58
|
-
reject(e);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
n1.receive({ payload: GGA + '\r\n' + GLL + '\r\n' });
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('accepts a Buffer payload and decodes it as UTF-8', function () {
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
helper.load(ntripModule, flow(), () => {
|
|
70
|
-
const n1 = helper.getNode('n1');
|
|
71
|
-
const ok = helper.getNode('ok');
|
|
72
|
-
ok.on('input', (msg) => {
|
|
73
|
-
try {
|
|
74
|
-
expect(msg.payload.messageType).to.equal('GGA');
|
|
75
|
-
resolve();
|
|
76
|
-
} catch (e) {
|
|
77
|
-
reject(e);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
n1.receive({ payload: Buffer.from(GGA, 'utf8') });
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('reads payload.nmeaMessage when payload is an object', function () {
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
helper.load(ntripModule, flow(), () => {
|
|
88
|
-
const n1 = helper.getNode('n1');
|
|
89
|
-
const ok = helper.getNode('ok');
|
|
90
|
-
ok.on('input', (msg) => {
|
|
91
|
-
try {
|
|
92
|
-
expect(msg.payload.messageType).to.equal('GGA');
|
|
93
|
-
resolve();
|
|
94
|
-
} catch (e) {
|
|
95
|
-
reject(e);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
n1.receive({ payload: { nmeaMessage: GGA } });
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('error output preserves Buffer input as `input`, utf8 as `inputString`', function () {
|
|
104
|
-
return new Promise((resolve, reject) => {
|
|
105
|
-
helper.load(ntripModule, flow(), () => {
|
|
106
|
-
const n1 = helper.getNode('n1');
|
|
107
|
-
const err = helper.getNode('err');
|
|
108
|
-
const garbage = Buffer.from([0xd3, 0x00, 0x13, 0x3e, 0xd0, 0x00]);
|
|
109
|
-
err.on('input', (msg) => {
|
|
110
|
-
try {
|
|
111
|
-
expect(Buffer.isBuffer(msg.payload.input)).to.equal(true);
|
|
112
|
-
expect(msg.payload.input).to.deep.equal(garbage);
|
|
113
|
-
expect(msg.payload.inputString).to.be.a('string');
|
|
114
|
-
resolve();
|
|
115
|
-
} catch (e) {
|
|
116
|
-
reject(e);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
n1.receive({ payload: garbage });
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('does not call node.error on decode failure (regression: log flood)', function () {
|
|
125
|
-
return new Promise((resolve, reject) => {
|
|
126
|
-
helper.load(ntripModule, flow(), () => {
|
|
127
|
-
const n1 = helper.getNode('n1');
|
|
128
|
-
let errorCalled = false;
|
|
129
|
-
const originalError = n1.error.bind(n1);
|
|
130
|
-
n1.error = function (...args) {
|
|
131
|
-
errorCalled = true;
|
|
132
|
-
return originalError(...args);
|
|
133
|
-
};
|
|
134
|
-
n1.receive({ payload: Buffer.from([0xd3, 0x00, 0x13, 0x3e]) });
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
try {
|
|
137
|
-
expect(errorCalled).to.equal(false);
|
|
138
|
-
resolve();
|
|
139
|
-
} catch (e) {
|
|
140
|
-
reject(e);
|
|
141
|
-
}
|
|
142
|
-
}, 50);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
});
|