node-red-contrib-ntrip 0.2.7 → 0.2.8

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.
@@ -19,9 +19,9 @@ jobs:
19
19
  # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20
20
 
21
21
  steps:
22
- - uses: actions/checkout@v4
22
+ - uses: actions/checkout@v6
23
23
  - name: Use Node.js ${{ matrix.node-version }}
24
- uses: actions/setup-node@v4
24
+ uses: actions/setup-node@v6
25
25
  with:
26
26
  node-version: ${{ matrix.node-version }}
27
27
  cache: 'npm'
@@ -23,9 +23,9 @@ jobs:
23
23
  matrix:
24
24
  node-version: [18.x, 20.x, 22.x]
25
25
  steps:
26
- - uses: actions/checkout@v4
26
+ - uses: actions/checkout@v6
27
27
  - name: Use Node.js ${{ matrix.node-version }}
28
- uses: actions/setup-node@v4
28
+ uses: actions/setup-node@v6
29
29
  with:
30
30
  node-version: ${{ matrix.node-version }}
31
31
  cache: 'npm'
@@ -38,8 +38,8 @@ jobs:
38
38
  needs: test
39
39
  runs-on: ubuntu-latest
40
40
  steps:
41
- - uses: actions/checkout@v4
42
- - uses: actions/setup-node@v4
41
+ - uses: actions/checkout@v6
42
+ - uses: actions/setup-node@v6
43
43
  with:
44
44
  node-version: 20
45
45
  registry-url: 'https://registry.npmjs.org/'
@@ -54,11 +54,11 @@ jobs:
54
54
  permissions:
55
55
  contents: write
56
56
  steps:
57
- - uses: actions/checkout@v4
57
+ - uses: actions/checkout@v6
58
58
  with:
59
59
  fetch-depth: 0
60
60
  - name: Create GitHub Release
61
- uses: softprops/action-gh-release@v2
61
+ uses: softprops/action-gh-release@v3
62
62
  with:
63
63
  tag_name: ${{ github.ref_name }}
64
64
  name: ${{ github.ref_name }}
package/CHANGELOG.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # Changelog
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
+ ## [0.2.8]
5
+ ### Added RTCM Encoder node
6
+ - New `RtcmEncoder` node — inverse of `RtcmDecoder`. Accepts an `RtcmMessage` instance (or `msg.payload.message` from the decoder) and emits the encoded RTCM 3 frame as `msg.payload.rtcmMessage` (Buffer).
7
+ - Designed for round-trip flows: decode, optionally mutate, then re-encode for forwarding.
8
+ - Does not construct messages from plain fields (unlike the NMEA encoder) — there are 187 RTCM message types in `@gnss/rtcm` and per-type constructors are rarely needed for real flows.
9
+ - New example flow `examples/rtcm-encode.json` demonstrating the decode → encode round-trip.
10
+ - 6 new specs in `test/rtcm-encoder.spec.js` covering: round-trip from decoder instance, decoder-output-shape input, non-`RtcmMessage` rejected to error output, empty payload rejected, no `node.error` flood on bad input, fixture sanity.
11
+
4
12
  ## [0.2.7]
5
13
  ### Internal — tooling, tests, docs (no runtime changes)
6
14
  - Added a mocha test suite (14 specs across `NmeaDecoder`, `NmeaEncoder`, `RtcmDecoder`) using `node-red-node-test-helper`. Regression specs are tagged so they can be re-run with `mocha -g regression`.
package/README.md CHANGED
@@ -155,6 +155,46 @@ msg.payload =
155
155
  };
156
156
  ```
157
157
 
158
+ # RTCM Encoder Node
159
+ Encodes an `RtcmMessage` instance back into a binary RTCM 3 frame using the
160
+ `@gnss/rtcm` library. The inverse of the RTCM Decoder — useful for re-emitting
161
+ a decoded frame after modification (e.g. rewriting a station ID before
162
+ forwarding) or for synthesising test fixtures.
163
+
164
+ The node has **two outputs**: encoded frames go to the first, failures to the
165
+ second.
166
+
167
+ This encoder does **not** construct an `RtcmMessage` from plain fields. The
168
+ input must already be an instance — typically `msg.payload.message` from the
169
+ RTCM Decoder.
170
+
171
+ Input shape:
172
+ ```javascript
173
+ // Direct: pass an RtcmMessage instance
174
+ msg.payload = <RtcmMessage instance>;
175
+
176
+ // Or pipe the decoder's success output straight through:
177
+ msg.payload = {
178
+ rtcm, // number — echoed back
179
+ messageType, // string — echoed back
180
+ message, // RtcmMessage instance — what gets encoded
181
+ input // ignored
182
+ };
183
+ ```
184
+
185
+ Successful output:
186
+ ```javascript
187
+ msg.payload =
188
+ {
189
+ rtcmMessage, // Buffer — the encoded RTCM 3 frame, ready to forward
190
+ rtcm, // number — the RTCM message type identifier
191
+ messageType, // string — the constructor name
192
+ input, // the original input as supplied
193
+ };
194
+ ```
195
+
196
+ See also example flow [**RTCM encode flow**](examples/rtcm-encode.json).
197
+
158
198
  # NMEA Decoder Node
159
199
  This node accepts NMEA 0183 sentences as a string or `Buffer`. Multiple
160
200
  sentences in a single input (delimited by `\r\n`) are split and decoded
@@ -0,0 +1 @@
1
+ [{"id":"rtcm-encode-tab","type":"tab","label":"RTCM encode round-trip","disabled":false,"info":"Decode a known RTCM 1005 frame, then re-encode it via the RTCM Encoder node.\nWatch the two debug nodes — the encoded output should match the original bytes.\n"},{"id":"inj-1","type":"inject","z":"rtcm-encode-tab","name":"Trigger","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":140,"wires":[["fn-load-rtcm"]]},{"id":"fn-load-rtcm","type":"function","z":"rtcm-encode-tab","name":"Load RTCM 1005 sample","func":"// A real RTCM 3 type 1005 (Stationary RTK Reference Station ARP) frame.\nmsg.payload = Buffer.from('D300133ED7D30202980EDEEF34B4BD62AC0941986F33360B98', 'hex');\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":140,"wires":[["decoder","dbg-raw"]]},{"id":"dbg-raw","type":"debug","z":"rtcm-encode-tab","name":"Raw RTCM bytes","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":540,"y":80,"wires":[]},{"id":"decoder","type":"RtcmDecoder","z":"rtcm-encode-tab","description":"","x":540,"y":140,"wires":[["encoder","dbg-decoded"],["dbg-err"]]},{"id":"dbg-decoded","type":"debug","z":"rtcm-encode-tab","name":"Decoded frame","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":740,"y":80,"wires":[]},{"id":"encoder","type":"RtcmEncoder","z":"rtcm-encode-tab","description":"","x":740,"y":140,"wires":[["dbg-encoded"],["dbg-err"]]},{"id":"dbg-encoded","type":"debug","z":"rtcm-encode-tab","name":"Re-encoded RTCM","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":940,"y":140,"wires":[]},{"id":"dbg-err","type":"debug","z":"rtcm-encode-tab","name":"decode/encode error","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":740,"y":220,"wires":[]}]
@@ -301,6 +301,88 @@
301
301
 
302
302
  <!-- ------------------------------------------------------------------------------------------ -->
303
303
 
304
+ <script type="text/javascript">
305
+ RED.nodes.registerType('RtcmEncoder', {
306
+ category: 'GNSS',
307
+ color: '#C0DEED',
308
+ defaults: {
309
+ description: { value:"" },
310
+ },
311
+ inputs: 1,
312
+ outputs: 2,
313
+ icon: "function.png",
314
+ paletteLabel: "RTCM Encoder",
315
+ label: function () {
316
+ return this.description || "RTCM Encoder";
317
+ },
318
+ labelStyle: function() {
319
+ return this.description?"node_label_italic":"";
320
+ }
321
+ });
322
+
323
+ </script>
324
+
325
+ <script type="text/x-red" data-template-name="RtcmEncoder">
326
+ <div class="form-row">
327
+ <label for="node-input-description"><i class="fa fa-comment"></i> Description</label>
328
+ <input type="text" id="node-input-description" placeholder="The description of the node (optional)">
329
+ </div>
330
+ </script>
331
+
332
+ <script type="text/x-red" data-help-name="RtcmEncoder">
333
+ <p>Encodes an <code>RtcmMessage</code> instance back into a binary RTCM 3
334
+ frame using the <code>@gnss/rtcm</code> library. The inverse of the
335
+ <i>RTCM Decoder</i> — useful for re-emitting a decoded frame after
336
+ modification (e.g. rewriting a station ID before forwarding upstream).</p>
337
+ <p>This encoder does not construct an <code>RtcmMessage</code> from plain
338
+ fields. The input must already be an instance — typically the
339
+ <code>msg.payload.message</code> emitted by the decoder.</p>
340
+
341
+ <h3>Configuration</h3>
342
+ <dl class="message-properties">
343
+ <dt>Description <span class="property-type">string</span></dt>
344
+ <dd>Optional label shown in the flow editor.</dd>
345
+ </dl>
346
+
347
+ <h3>Inputs</h3>
348
+ <dl class="message-properties">
349
+ <dt>payload <span class="property-type">RtcmMessage</span></dt>
350
+ <dd>An <code>RtcmMessage</code> instance, encoded directly.</dd>
351
+ <dt>payload.message <span class="property-type">RtcmMessage</span></dt>
352
+ <dd>Round-trip shape from the <i>RTCM Decoder</i> — the encoder picks
353
+ up <code>msg.payload.message</code> and encodes that.</dd>
354
+ </dl>
355
+
356
+ <h3>Outputs</h3>
357
+ <p>Two outputs: successfully encoded frames go to the first; failures go to the second.</p>
358
+ <ol class="node-ports">
359
+ <li>Encoded
360
+ <dl class="message-properties">
361
+ <dt>payload.rtcmMessage <span class="property-type">Buffer</span></dt>
362
+ <dd>The encoded RTCM 3 frame, ready to forward to an NTRIP
363
+ uploader or a TCP sink.</dd>
364
+ <dt>payload.rtcm <span class="property-type">number</span></dt>
365
+ <dd>The RTCM message type identifier (e.g. <code>1005</code>).</dd>
366
+ <dt>payload.messageType <span class="property-type">string</span></dt>
367
+ <dd>The constructor name (e.g. <code>StationArp</code>).</dd>
368
+ <dt>payload.input <span class="property-type">any</span></dt>
369
+ <dd>The original input as supplied.</dd>
370
+ </dl>
371
+ </li>
372
+ <li>Error
373
+ <dl class="message-properties">
374
+ <dt>payload.error <span class="property-type">Error | string</span></dt>
375
+ <dd>The exception thrown by the encoder, or a string describing
376
+ invalid input.</dd>
377
+ <dt>payload.input <span class="property-type">any</span></dt>
378
+ <dd>The offending input.</dd>
379
+ </dl>
380
+ </li>
381
+ </ol>
382
+ </script>
383
+
384
+ <!-- ------------------------------------------------------------------------------------------ -->
385
+
304
386
  <script type="text/javascript">
305
387
  RED.nodes.registerType('NmeaDecoder', {
306
388
  category: 'GNSS',
package/ntrip/99-ntrip.js CHANGED
@@ -19,6 +19,9 @@ module.exports = function (RED) {
19
19
  const RtcmDecoderNode = require('./nodes/rtcm-decoder-node.js')(RED);
20
20
  RED.nodes.registerType('RtcmDecoder', RtcmDecoderNode);
21
21
 
22
+ const RtcmEncoderNode = require('./nodes/rtcm-encoder-node.js')(RED);
23
+ RED.nodes.registerType('RtcmEncoder', RtcmEncoderNode);
24
+
22
25
  const NmeaDecoderNode = require('./nodes/nmea-decoder-node.js')(RED);
23
26
  RED.nodes.registerType('NmeaDecoder', NmeaDecoderNode);
24
27
 
@@ -0,0 +1,90 @@
1
+ module.exports = function (RED) {
2
+ 'use strict';
3
+
4
+ const { RtcmTransport, RtcmMessage } = require('@gnss/rtcm');
5
+
6
+ // Maximum RTCM 3 frame size — 3-byte header + 1023-byte payload + 3-byte CRC.
7
+ const MaxFrameBytes = 1029;
8
+
9
+ function RtcmEncoderNode(config) {
10
+ RED.nodes.createNode(this, config);
11
+ let node = this;
12
+ node.rtcmMessagesReceived = 0;
13
+ node.invalidMessagesReceived = 0;
14
+
15
+ function updateStatus(ok) {
16
+ node.status({
17
+ fill: ok ? 'green' : 'red',
18
+ shape: 'ring',
19
+ text: 'RTCM: ' + node.rtcmMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
20
+ });
21
+ }
22
+
23
+ this.on('input', function (msg) {
24
+ let payload = msg.payload;
25
+
26
+ // Accept either an RtcmMessage instance directly as msg.payload, or the
27
+ // shape emitted by the RtcmDecoder (msg.payload.message). Anything else
28
+ // routes to the error output.
29
+ let message;
30
+ if (payload instanceof RtcmMessage) {
31
+ message = payload;
32
+ } else if (payload != null && payload.message instanceof RtcmMessage) {
33
+ message = payload.message;
34
+ } else {
35
+ node.send([
36
+ null,
37
+ {
38
+ payload: {
39
+ error: 'Invalid input. Expected an RtcmMessage instance as msg.payload, or msg.payload.message.',
40
+ input: payload,
41
+ },
42
+ },
43
+ ]);
44
+ node.invalidMessagesReceived++;
45
+ updateStatus(false);
46
+ return;
47
+ }
48
+
49
+ try {
50
+ let buffer = Buffer.alloc(MaxFrameBytes);
51
+ let length = RtcmTransport.encode(message, buffer);
52
+ let encoded = buffer.slice(0, length);
53
+ let messageType = message.constructor.name.replace('RtcmMessage', '');
54
+
55
+ node.send([
56
+ {
57
+ payload: {
58
+ rtcmMessage: encoded,
59
+ rtcm: message.messageType,
60
+ messageType: messageType,
61
+ input: payload,
62
+ },
63
+ },
64
+ null,
65
+ ]);
66
+ node.rtcmMessagesReceived++;
67
+ updateStatus(true);
68
+ } catch (ex) {
69
+ node.send([
70
+ null,
71
+ {
72
+ payload: {
73
+ error: ex,
74
+ input: payload,
75
+ },
76
+ },
77
+ ]);
78
+ node.invalidMessagesReceived++;
79
+ updateStatus(false);
80
+ }
81
+ });
82
+
83
+ this.on('close', function (done) {
84
+ node.status({});
85
+ done();
86
+ });
87
+ }
88
+
89
+ return RtcmEncoderNode;
90
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ntrip",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Node for ntrip and rtcm handling.",
5
5
  "node-red": {
6
6
  "version": ">=0.1.0",
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ const helper = require('node-red-node-test-helper');
4
+ const { expect } = require('chai');
5
+ const { RtcmTransport, RtcmMessage } = require('@gnss/rtcm');
6
+ const ntripModule = require('../ntrip/99-ntrip.js');
7
+
8
+ helper.init(require.resolve('node-red'));
9
+
10
+ // A real RTCM 3 message type 1005 (Stationary RTK Reference Station ARP).
11
+ const RTCM_1005 = Buffer.from('D300133ED7D30202980EDEEF34B4BD62AC0941986F33360B98', 'hex');
12
+
13
+ function flow() {
14
+ return [
15
+ { id: 'n1', type: 'RtcmEncoder', wires: [['ok'], ['err']] },
16
+ { id: 'ok', type: 'helper' },
17
+ { id: 'err', type: 'helper' },
18
+ ];
19
+ }
20
+
21
+ describe('RtcmEncoder', function () {
22
+ before(() => helper.startServer());
23
+ after(() => helper.stopServer());
24
+ afterEach(() => helper.unload());
25
+
26
+ it('encodes an RtcmMessage instance round-tripped from the decoder', function () {
27
+ return new Promise((resolve, reject) => {
28
+ helper.load(ntripModule, flow(), () => {
29
+ const n1 = helper.getNode('n1');
30
+ const ok = helper.getNode('ok');
31
+ const [instance] = RtcmTransport.decode(RTCM_1005);
32
+ ok.on('input', (msg) => {
33
+ try {
34
+ expect(msg.payload.rtcm).to.equal(1005);
35
+ expect(msg.payload.messageType).to.be.a('string');
36
+ expect(Buffer.isBuffer(msg.payload.rtcmMessage)).to.equal(true);
37
+ expect(msg.payload.rtcmMessage).to.deep.equal(RTCM_1005);
38
+ resolve();
39
+ } catch (e) {
40
+ reject(e);
41
+ }
42
+ });
43
+ n1.receive({ payload: instance });
44
+ });
45
+ });
46
+ });
47
+
48
+ it('accepts the decoder output shape (msg.payload.message)', function () {
49
+ return new Promise((resolve, reject) => {
50
+ helper.load(ntripModule, flow(), () => {
51
+ const n1 = helper.getNode('n1');
52
+ const ok = helper.getNode('ok');
53
+ const [instance, length] = RtcmTransport.decode(RTCM_1005);
54
+ ok.on('input', (msg) => {
55
+ try {
56
+ expect(msg.payload.rtcm).to.equal(1005);
57
+ expect(Buffer.isBuffer(msg.payload.rtcmMessage)).to.equal(true);
58
+ expect(msg.payload.rtcmMessage.length).to.equal(length);
59
+ resolve();
60
+ } catch (e) {
61
+ reject(e);
62
+ }
63
+ });
64
+ // Mimic the RtcmDecoder's success payload shape.
65
+ n1.receive({
66
+ payload: {
67
+ rtcm: instance.messageType,
68
+ messageType: 'StationArp',
69
+ message: instance,
70
+ input: RTCM_1005,
71
+ },
72
+ });
73
+ });
74
+ });
75
+ });
76
+
77
+ it('routes a non-RtcmMessage payload to the error output', function () {
78
+ return new Promise((resolve, reject) => {
79
+ helper.load(ntripModule, flow(), () => {
80
+ const n1 = helper.getNode('n1');
81
+ const ok = helper.getNode('ok');
82
+ const err = helper.getNode('err');
83
+ ok.on('input', () => reject(new Error('unexpected success output')));
84
+ err.on('input', (msg) => {
85
+ try {
86
+ expect(msg.payload.error).to.be.a('string');
87
+ expect(msg.payload.error.toLowerCase()).to.include('invalid');
88
+ resolve();
89
+ } catch (e) {
90
+ reject(e);
91
+ }
92
+ });
93
+ n1.receive({ payload: { foo: 'bar' } });
94
+ });
95
+ });
96
+ });
97
+
98
+ it('routes empty payload to the error output', function () {
99
+ return new Promise((resolve, reject) => {
100
+ helper.load(ntripModule, flow(), () => {
101
+ const n1 = helper.getNode('n1');
102
+ const err = helper.getNode('err');
103
+ err.on('input', (msg) => {
104
+ try {
105
+ expect(msg.payload.error).to.exist;
106
+ resolve();
107
+ } catch (e) {
108
+ reject(e);
109
+ }
110
+ });
111
+ n1.receive({ payload: null });
112
+ });
113
+ });
114
+ });
115
+
116
+ it('does not call node.error on decode-shape failure (regression: log flood)', function () {
117
+ return new Promise((resolve, reject) => {
118
+ helper.load(ntripModule, flow(), () => {
119
+ const n1 = helper.getNode('n1');
120
+ let errorCalled = false;
121
+ const originalError = n1.error.bind(n1);
122
+ n1.error = function (...args) {
123
+ errorCalled = true;
124
+ return originalError(...args);
125
+ };
126
+ n1.receive({ payload: 'not-an-rtcm-message' });
127
+ setTimeout(() => {
128
+ try {
129
+ expect(errorCalled).to.equal(false);
130
+ resolve();
131
+ } catch (e) {
132
+ reject(e);
133
+ }
134
+ }, 50);
135
+ });
136
+ });
137
+ });
138
+
139
+ // Sanity check: RtcmMessage is exported and the test fixture decodes to an instance.
140
+ it('test fixture sanity: decoded 1005 frame is an RtcmMessage instance', function () {
141
+ const [instance] = RtcmTransport.decode(RTCM_1005);
142
+ expect(instance).to.be.instanceOf(RtcmMessage);
143
+ });
144
+ });