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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -3
  3. package/examples/watchdog.json +1 -1
  4. package/ntrip/lib/ntrip-client.js +70 -72
  5. package/ntrip/nodes/nmea-decoder-node.js +36 -38
  6. package/ntrip/nodes/nmea-encoder-node.js +97 -97
  7. package/ntrip/nodes/ntrip-client-node.js +153 -148
  8. package/ntrip/nodes/rtcm-decoder-node.js +50 -47
  9. package/ntrip/nodes/rtcm-encoder-node.js +33 -32
  10. package/package.json +16 -6
  11. package/.github/dependabot.yml +0 -23
  12. package/.github/workflows/node.js.yml +0 -32
  13. package/.github/workflows/release.yml +0 -65
  14. package/.prettierignore +0 -20
  15. package/.prettierrc.json +0 -9
  16. package/CLAUDE.md +0 -73
  17. package/doc/architecture/README.md +0 -19
  18. package/doc/architecture/adr/0001-single-registration-file.md +0 -64
  19. package/doc/architecture/adr/0002-ntrip-uploader-extension.md +0 -71
  20. package/doc/architecture/adr/0003-two-output-decoder-design.md +0 -73
  21. package/doc/architecture/adr/0004-stateful-handshake-interception.md +0 -74
  22. package/doc/architecture/adr/0005-rtcm-partial-frame-buffer.md +0 -87
  23. package/doc/architecture/adr/0006-nmea-multi-sentence-split.md +0 -66
  24. package/doc/architecture/adr/0007-mocha-test-helper-stack.md +0 -67
  25. package/doc/architecture/adr/0008-coordinate-gating-sentinel.md +0 -76
  26. package/doc/architecture/adr/README.md +0 -18
  27. package/doc/architecture/architecture-decisions.md +0 -60
  28. package/doc/architecture/behavioural-design.md +0 -226
  29. package/doc/architecture/errors-and-weaknesses.md +0 -71
  30. package/doc/architecture/future-improvements.md +0 -130
  31. package/doc/architecture/overview.md +0 -77
  32. package/doc/architecture/refactoring-recommendations.md +0 -114
  33. package/doc/architecture/statistics.md +0 -118
  34. package/doc/architecture/structural-design.md +0 -141
  35. package/eslint.config.js +0 -36
  36. package/test/nmea-decoder.spec.js +0 -146
  37. package/test/nmea-encoder.spec.js +0 -104
  38. package/test/rtcm-decoder.spec.js +0 -116
  39. 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
@@ -1,9 +0,0 @@
1
- {
2
- "trailingComma": "es5",
3
- "tabWidth": 4,
4
- "semi": true,
5
- "singleQuote": true,
6
- "bracketSpacing": true,
7
- "endOfLine": "auto",
8
- "printWidth": 160
9
- }
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"