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,65 +0,0 @@
|
|
|
1
|
-
# Release pipeline.
|
|
2
|
-
#
|
|
3
|
-
# Trigger: push of a version tag matching v* or V* (e.g. v0.2.7, V1.0.0).
|
|
4
|
-
# Stages:
|
|
5
|
-
# 1. test — runs the full lint/format/test matrix on the tagged commit.
|
|
6
|
-
# 2. publish-npm — publishes the package to the npm registry.
|
|
7
|
-
# 3. github-release — creates a GitHub Release with auto-generated notes.
|
|
8
|
-
#
|
|
9
|
-
# Requires the `NPM_TOKEN` secret to be configured in the repository.
|
|
10
|
-
|
|
11
|
-
name: Release
|
|
12
|
-
|
|
13
|
-
on:
|
|
14
|
-
push:
|
|
15
|
-
tags:
|
|
16
|
-
- 'v*'
|
|
17
|
-
- 'V*'
|
|
18
|
-
|
|
19
|
-
jobs:
|
|
20
|
-
test:
|
|
21
|
-
runs-on: ubuntu-latest
|
|
22
|
-
strategy:
|
|
23
|
-
matrix:
|
|
24
|
-
node-version: [18.x, 20.x, 22.x]
|
|
25
|
-
steps:
|
|
26
|
-
- uses: actions/checkout@v7
|
|
27
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
28
|
-
uses: actions/setup-node@v6
|
|
29
|
-
with:
|
|
30
|
-
node-version: ${{ matrix.node-version }}
|
|
31
|
-
cache: 'npm'
|
|
32
|
-
- run: npm ci
|
|
33
|
-
- run: npm run lint
|
|
34
|
-
- run: npm run format:check
|
|
35
|
-
- run: npm test
|
|
36
|
-
|
|
37
|
-
publish-npm:
|
|
38
|
-
needs: test
|
|
39
|
-
runs-on: ubuntu-latest
|
|
40
|
-
steps:
|
|
41
|
-
- uses: actions/checkout@v7
|
|
42
|
-
- uses: actions/setup-node@v6
|
|
43
|
-
with:
|
|
44
|
-
node-version: 20
|
|
45
|
-
registry-url: 'https://registry.npmjs.org/'
|
|
46
|
-
- run: npm ci
|
|
47
|
-
- run: npm publish
|
|
48
|
-
env:
|
|
49
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
50
|
-
|
|
51
|
-
github-release:
|
|
52
|
-
needs: publish-npm
|
|
53
|
-
runs-on: ubuntu-latest
|
|
54
|
-
permissions:
|
|
55
|
-
contents: write
|
|
56
|
-
steps:
|
|
57
|
-
- uses: actions/checkout@v7
|
|
58
|
-
with:
|
|
59
|
-
fetch-depth: 0
|
|
60
|
-
- name: Create GitHub Release
|
|
61
|
-
uses: softprops/action-gh-release@v3
|
|
62
|
-
with:
|
|
63
|
-
tag_name: ${{ github.ref_name }}
|
|
64
|
-
name: ${{ github.ref_name }}
|
|
65
|
-
generate_release_notes: true
|
package/.prettierignore
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Generated / vendored
|
|
2
|
-
node_modules/
|
|
3
|
-
package-lock.json
|
|
4
|
-
|
|
5
|
-
# Node-RED flow files — re-formatting would change diff noise without value
|
|
6
|
-
examples/
|
|
7
|
-
|
|
8
|
-
# Editor UI HTML has Node-RED-specific structural conventions
|
|
9
|
-
# (data-template-name script blocks, x-red script blocks) that Prettier
|
|
10
|
-
# can mangle. Touch only with care.
|
|
11
|
-
ntrip/99-ntrip.html
|
|
12
|
-
|
|
13
|
-
# Markdown is kept hand-formatted (line-wrapped prose, ASCII diagrams).
|
|
14
|
-
# Re-flowing it would produce a large noisy diff.
|
|
15
|
-
*.md
|
|
16
|
-
doc/
|
|
17
|
-
|
|
18
|
-
# Editor / agent local state — not checked in
|
|
19
|
-
.claude/
|
|
20
|
-
.vscode/
|
package/.prettierrc.json
DELETED
package/CLAUDE.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project overview
|
|
6
|
-
|
|
7
|
-
`node-red-contrib-ntrip` is a Node-RED contribution package providing four GNSS-related nodes: an NTRIP client (download/upload), an RTCM decoder, an NMEA decoder, and an NMEA encoder. It is published to npm and consumed by users via Node-RED's palette manager — there is no application entry point of its own. Runtime target is Node.js >= 7.6 (CI matrix: 18.x, 20.x, 22.x).
|
|
8
|
-
|
|
9
|
-
## Commands
|
|
10
|
-
|
|
11
|
-
This package has no build step. Linting (ESLint flat config) and formatting (Prettier) are wired in:
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npm test # run all specs (mocha + node-red-node-test-helper)
|
|
15
|
-
npm run lint # eslint, treats warnings as warnings
|
|
16
|
-
npm run lint:fix # eslint --fix
|
|
17
|
-
npm run format # prettier --write .
|
|
18
|
-
npm run format:check # prettier --check . (used in CI)
|
|
19
|
-
|
|
20
|
-
npx mocha test/nmea-decoder.spec.js # run one spec file
|
|
21
|
-
npx mocha test/**/*.spec.js -g 'regression' # run only regression specs
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
CI runs `npm ci`, `npm run build --if-present`, `npm run lint`, `npm run format:check`, then `npm test` across Node 18/20/22 (see [.github/workflows/node.js.yml](.github/workflows/node.js.yml)).
|
|
25
|
-
|
|
26
|
-
Prettier settings live in [.prettierrc.json](.prettierrc.json) (4-space indent, single quotes, trailing-commas-es5, printWidth 160). [.prettierignore](.prettierignore) skips `examples/` (Node-RED flow files), `ntrip/99-ntrip.html` (Node-RED-specific structural conventions), markdown, and `doc/`. ESLint config is [eslint.config.js](eslint.config.js) (flat config, `@eslint/js` recommended + `eslint-config-prettier`).
|
|
27
|
-
|
|
28
|
-
To exercise changes manually, install this directory into a local Node-RED:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
cd <node-red userDir, e.g. ~/.node-red>
|
|
32
|
-
npm install <path-to-this-repo>
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Then start Node-RED and import a flow from [examples/](examples/) (e.g. `ntripclient.json`, `sapos.json`, `upload.json`, `tcp.json`, `nmea-decode.json`, `nmea-encode.json`) to drive the nodes against a real NTRIP caster.
|
|
36
|
-
|
|
37
|
-
## Architecture
|
|
38
|
-
|
|
39
|
-
Node-RED contribution packages follow a strict structure that this repo conforms to:
|
|
40
|
-
|
|
41
|
-
- [package.json](package.json) `node-red.nodes` points at a single registration file [ntrip/99-ntrip.js](ntrip/99-ntrip.js).
|
|
42
|
-
- That registration file is the only entry Node-RED loads. It `require`s each node implementation from [ntrip/nodes/](ntrip/nodes/) and calls `RED.nodes.registerType(...)` for each.
|
|
43
|
-
- Each Node-RED node is a pair: a `.js` runtime file in [ntrip/nodes/](ntrip/nodes/), plus the editor UI (HTML + inline `<script>`/`<template>`) co-located in [ntrip/99-ntrip.html](ntrip/99-ntrip.html). All four nodes' editor definitions share this single HTML file.
|
|
44
|
-
- Credentials (NTRIP username/password) are declared in both the registration call ([ntrip/99-ntrip.js:13-16](ntrip/99-ntrip.js#L13-L16)) and the HTML `credentials` block — they must stay in sync, otherwise Node-RED will not persist them.
|
|
45
|
-
|
|
46
|
-
Node responsibilities:
|
|
47
|
-
|
|
48
|
-
- **NtripClient** ([ntrip/nodes/ntrip-client-node.js](ntrip/nodes/ntrip-client-node.js)) — wraps [ntrip/lib/ntrip-client.js](ntrip/lib/ntrip-client.js), which extends the upstream `ntrip-client` package to add an *uploader* variant. The uploader's `_connect()` switches the handshake string by `authmode` (`legacy`, `hybrid`, `ntripv1`, `ntripv2`) — see [ntrip/lib/ntrip-client.js:45-66](ntrip/lib/ntrip-client.js#L45-L66). Special inbound bytes are intercepted and turned into status/error rather than forwarded: `ICY 200 OK` (connected), `ICY 406` (rejected), `SOURCETABLE 200 OK` (mountpoint missing). When `msg.payload` is an `Array`, it is treated as an `[x, y, z]` coordinate triple and forwarded via `client.setXYZ(...)` (used by the Sapos flow to seed a GGA sentence); any other payload is written to the caster. `passthrough` controls whether written data is also re-emitted on the output.
|
|
49
|
-
- **RtcmDecoder** ([ntrip/nodes/rtcm-decoder-node.js](ntrip/nodes/rtcm-decoder-node.js)) — loops `RtcmTransport.decode(buffer)` (from `@gnss/rtcm`), slicing the buffer by the returned `length` until empty, emitting one message per decoded RTCM frame. Two outputs: `[ok, error]`.
|
|
50
|
-
- **NmeaDecoder** ([ntrip/nodes/nmea-decoder-node.js](ntrip/nodes/nmea-decoder-node.js)) — decodes via `NmeaTransport.decode` from `@gnss/nmea`. Accepts either a Buffer or a string, and either `msg.payload` directly or `msg.payload.nmeaMessage`. Two outputs: `[ok, error]`.
|
|
51
|
-
- **NmeaEncoder** ([ntrip/nodes/nmea-encoder-node.js](ntrip/nodes/nmea-encoder-node.js)) — symmetric inverse, with a large `switch (messageType)` that maps NMEA sentence types (`GGA`, `RMC`, `GSV`, ...) to the corresponding `NmeaMessage*.construct(...)` factory before calling `NmeaTransport.encode`. When adding support for a new NMEA sentence type, add both the import at the top and the matching `case` in the switch.
|
|
52
|
-
|
|
53
|
-
## Tests
|
|
54
|
-
|
|
55
|
-
Specs live in [test/](test/) — one file per node. Each uses `node-red-node-test-helper` to load the registration file [ntrip/99-ntrip.js](ntrip/99-ntrip.js), wires the node under test to two `helper` sink nodes (one per output port), and asserts on `msg.payload` shape.
|
|
56
|
-
|
|
57
|
-
Two patterns to reuse when adding tests:
|
|
58
|
-
|
|
59
|
-
- **`regression:` tag in the test name** — for any spec that locks in a previously-shipped bug. Future maintainers can run `mocha -g regression` to verify the fix is still in place.
|
|
60
|
-
- **`node.error` spy** — replace `n1.error` with a wrapper that flips a boolean, then assert the boolean stayed false. Used to prevent regressing the "decoder floods the Node-RED error log when fed mismatched binary" bug.
|
|
61
|
-
|
|
62
|
-
Known-good test fixtures embedded in the specs:
|
|
63
|
-
|
|
64
|
-
- NMEA: `$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47`, `$GPGLL,4916.45,N,12311.12,W,225444,A,*1D`
|
|
65
|
-
- RTCM: a real 25-byte type 1005 frame as hex `D300133ED7D30202980EDEEF34B4BD62AC0941986F33360B98`
|
|
66
|
-
|
|
67
|
-
NTRIP client integration tests are not yet written; the recommended approach is a fake TCP server via `net.createServer` that returns `"ICY 200 OK\r\n\r\n" + rtcmBytes` to exercise the handshake-state machine.
|
|
68
|
-
|
|
69
|
-
## Conventions worth knowing
|
|
70
|
-
|
|
71
|
-
- All four nodes follow the same pattern: increment a `messagesReceived`/`invalidMessagesReceived` counter and call `node.status({...})` after each input, set `node.status({})` on `close`. Keep this consistent when adding nodes.
|
|
72
|
-
- The package version printed at load time is read from `package.json` at [ntrip/99-ntrip.js:8-9](ntrip/99-ntrip.js#L8-L9) — bumping `version` in package.json is the only source of truth.
|
|
73
|
-
- Releases are tracked in [CHANGELOG.md](CHANGELOG.md) using "Keep a Changelog" headings; each entry typically references the GitHub issue number that motivated it.
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# Architecture documentation
|
|
2
|
-
|
|
3
|
-
Architectural analysis of `node-red-contrib-ntrip`. Start with
|
|
4
|
-
[overview.md](overview.md) for a high-level orientation.
|
|
5
|
-
|
|
6
|
-
| Chapter | Read when |
|
|
7
|
-
|---------|-----------|
|
|
8
|
-
| [Overview](overview.md) | First contact — what this package is and how the pieces fit. |
|
|
9
|
-
| [Structural Design](structural-design.md) | You need to find or change a specific file / module. |
|
|
10
|
-
| [Behavioural Design](behavioural-design.md) | You're changing runtime behaviour — state machines, error paths, lifecycle hooks. |
|
|
11
|
-
| [Architecture Decisions](architecture-decisions.md) | You're about to revisit a load-bearing decision. Individual ADRs live in [adr/](adr/). |
|
|
12
|
-
| [Errors and Weaknesses](errors-and-weaknesses.md) | You're triaging a bug report — check whether it's already known. |
|
|
13
|
-
| [Refactoring Recommendations](refactoring-recommendations.md) | You have spare cycles and want to invest them in code health. |
|
|
14
|
-
| [Future Improvements](future-improvements.md) | You're planning the next minor release. |
|
|
15
|
-
| [Statistics](statistics.md) | You want a quantitative read on size, test coverage, and quality. |
|
|
16
|
-
|
|
17
|
-
This documentation lives alongside the code and is versioned with it. When
|
|
18
|
-
you change behaviour that contradicts a statement here, update the doc in the
|
|
19
|
-
same commit.
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# ADR-0001: Single registration file for all nodes
|
|
2
|
-
|
|
3
|
-
## Status
|
|
4
|
-
|
|
5
|
-
Accepted (original design).
|
|
6
|
-
|
|
7
|
-
## Context
|
|
8
|
-
|
|
9
|
-
Node-RED loads a contribution package via the `node-red.nodes` map in
|
|
10
|
-
`package.json`. That map can point at one or many entry files: each entry file
|
|
11
|
-
is run once at startup and is expected to call `RED.nodes.registerType(...)`
|
|
12
|
-
for the node types it owns.
|
|
13
|
-
|
|
14
|
-
Two viable patterns exist:
|
|
15
|
-
|
|
16
|
-
- **One entry per node.** `package.json` lists four entries; each file
|
|
17
|
-
registers exactly one type. Pro: a node's entire definition is in one place.
|
|
18
|
-
Con: four entries to keep in sync, four loader invocations.
|
|
19
|
-
- **One entry for the whole package.** `package.json` lists one entry; that
|
|
20
|
-
file `require`s each node implementation and registers them all. Pro:
|
|
21
|
-
centralised; package version logging happens once.
|
|
22
|
-
|
|
23
|
-
## Decision
|
|
24
|
-
|
|
25
|
-
Use a single registration entry file: [ntrip/99-ntrip.js](../../../ntrip/99-ntrip.js).
|
|
26
|
-
The `99-` prefix is a convention seen across published `node-red-contrib-*`
|
|
27
|
-
packages — it sorts late alphabetically, making the file easy to find.
|
|
28
|
-
|
|
29
|
-
Each node implementation lives under [ntrip/nodes/](../../../ntrip/nodes/) and
|
|
30
|
-
exports a factory function that the registration file imports.
|
|
31
|
-
|
|
32
|
-
The four node types' editor UIs share one HTML file
|
|
33
|
-
([ntrip/99-ntrip.html](../../../ntrip/99-ntrip.html)) — Node-RED's loader picks
|
|
34
|
-
up the `.html` next to the registered `.js`.
|
|
35
|
-
|
|
36
|
-
## Consequences
|
|
37
|
-
|
|
38
|
-
**Positive**
|
|
39
|
-
|
|
40
|
-
- One place to read the full inventory of nodes.
|
|
41
|
-
- Package version logging happens exactly once at load time (read from
|
|
42
|
-
`package.json`).
|
|
43
|
-
- New nodes are added by a one-line `require` + one-line `registerType` in the
|
|
44
|
-
entry file, plus a new file under `nodes/` and a new `<script>` block in the
|
|
45
|
-
HTML.
|
|
46
|
-
|
|
47
|
-
**Negative**
|
|
48
|
-
|
|
49
|
-
- The HTML file is large (~470 lines) because it must hold form templates and
|
|
50
|
-
help text for all four nodes. Adding a fifth node means appending another
|
|
51
|
-
~100 lines to the same file.
|
|
52
|
-
- A typo in `registerType` names (e.g. `NtripClinet` instead of `NtripClient`)
|
|
53
|
-
is silently fatal — the JS side and HTML side must agree exactly.
|
|
54
|
-
|
|
55
|
-
**Trade-offs**
|
|
56
|
-
|
|
57
|
-
- Choosing the single-entry pattern prioritises *package coherence* over
|
|
58
|
-
*per-node isolation*. If the package ever grows to a dozen+ nodes, splitting
|
|
59
|
-
would become attractive.
|
|
60
|
-
|
|
61
|
-
## Related
|
|
62
|
-
|
|
63
|
-
- Code: [ntrip/99-ntrip.js](../../../ntrip/99-ntrip.js), [ntrip/99-ntrip.html](../../../ntrip/99-ntrip.html)
|
|
64
|
-
- Configured via: [package.json](../../../package.json) `node-red.nodes`
|
|
@@ -1,71 +0,0 @@
|
|
|
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)
|
|
@@ -1,73 +0,0 @@
|
|
|
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"
|
|
@@ -1,74 +0,0 @@
|
|
|
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)
|
|
@@ -1,87 +0,0 @@
|
|
|
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"
|
|
@@ -1,66 +0,0 @@
|
|
|
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"
|