node-red-contrib-ntrip 0.2.7 → 0.2.9
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/.github/workflows/node.js.yml +2 -2
- package/.github/workflows/release.yml +6 -6
- package/CHANGELOG.md +20 -0
- package/README.md +44 -2
- package/examples/rtcm-encode.json +1 -0
- package/ntrip/99-ntrip.html +87 -2
- package/ntrip/99-ntrip.js +3 -0
- package/ntrip/nodes/ntrip-client-node.js +24 -1
- package/ntrip/nodes/rtcm-encoder-node.js +90 -0
- package/package.json +2 -2
- package/test/rtcm-encoder.spec.js +144 -0
|
@@ -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@
|
|
22
|
+
- uses: actions/checkout@v7
|
|
23
23
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
24
|
-
uses: actions/setup-node@
|
|
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@
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
27
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
28
|
-
uses: actions/setup-node@
|
|
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@
|
|
42
|
-
- uses: actions/setup-node@
|
|
41
|
+
- uses: actions/checkout@v7
|
|
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@
|
|
57
|
+
- uses: actions/checkout@v7
|
|
58
58
|
with:
|
|
59
59
|
fetch-depth: 0
|
|
60
60
|
- name: Create GitHub Release
|
|
61
|
-
uses: softprops/action-gh-release@
|
|
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,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
All notable changes to this project will be documented in this file.
|
|
3
3
|
|
|
4
|
+
## [0.2.9]
|
|
5
|
+
### NtripClient
|
|
6
|
+
- Status badge now shows the inbound data rate alongside the Rx/Tx message counters (e.g. `2.4 kbps Rx 123 Tx 4`). The rate is sampled once per second and formatted as `bps`, `kbps`, or `Mbps` depending on magnitude.
|
|
7
|
+
|
|
8
|
+
### Internal — dependency bumps (dev tree, no runtime changes)
|
|
9
|
+
- `node-red` 4.1.11 → 5.0.1 (via the sigstore/node-red bundle).
|
|
10
|
+
- `eslint` 10.4.1 → 10.6.0.
|
|
11
|
+
- `prettier` 3.8.3 → 3.9.3.
|
|
12
|
+
- `mocha` 11.7.5 → 11.7.6.
|
|
13
|
+
- `globals` 17.6.0 → 17.7.0.
|
|
14
|
+
- `actions/checkout` v6 → v7 in the CI + release workflows.
|
|
15
|
+
|
|
16
|
+
## [0.2.8]
|
|
17
|
+
### Added RTCM Encoder node
|
|
18
|
+
- 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).
|
|
19
|
+
- Designed for round-trip flows: decode, optionally mutate, then re-encode for forwarding.
|
|
20
|
+
- 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.
|
|
21
|
+
- New example flow `examples/rtcm-encode.json` demonstrating the decode → encode round-trip.
|
|
22
|
+
- 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.
|
|
23
|
+
|
|
4
24
|
## [0.2.7]
|
|
5
25
|
### Internal — tooling, tests, docs (no runtime changes)
|
|
6
26
|
- 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
|
|
@@ -252,8 +292,10 @@ A few things that are easy to get wrong:
|
|
|
252
292
|
interleaved with bytes received from the caster. Downstream nodes will see
|
|
253
293
|
both streams on the same wire — distinguish them with a tag in a function
|
|
254
294
|
node if you need to.
|
|
255
|
-
- **NTRIP Client: status badge.**
|
|
256
|
-
|
|
295
|
+
- **NTRIP Client: status badge.** `<rate> Rx N Tx M` shows the inbound data
|
|
296
|
+
rate (sampled over a 1-second window; formatted as `bps`, `kbps`, or `Mbps`)
|
|
297
|
+
followed by the inbound and outbound message counts. Handshake replies do
|
|
298
|
+
not bump the inbound count.
|
|
257
299
|
- **NTRIP Client: CR/LF in credentials.** `mountpoint`, `username`, and
|
|
258
300
|
`password` are interpolated into the handshake string. CR/LF in these fields
|
|
259
301
|
is rejected at startup to prevent header injection.
|
|
@@ -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":[]}]
|
package/ntrip/99-ntrip.html
CHANGED
|
@@ -209,8 +209,11 @@
|
|
|
209
209
|
<h3>Status</h3>
|
|
210
210
|
<p>The node shows <code>connecting…</code> until the caster acknowledges the
|
|
211
211
|
handshake with <code>ICY 200 OK</code>. After that it shows the running
|
|
212
|
-
<code>Rx</code> / <code>Tx</code> counters
|
|
213
|
-
|
|
212
|
+
<code>Rx</code> / <code>Tx</code> message counters plus the inbound data
|
|
213
|
+
rate in <code>bps</code> / <code>kbps</code> / <code>Mbps</code>, sampled
|
|
214
|
+
once per second over a 1-second window
|
|
215
|
+
(e.g. <code>2.4 kbps Rx 123 Tx 4</code>). A red badge indicates a
|
|
216
|
+
rejected connection, a missing mountpoint, or a write failure.</p>
|
|
214
217
|
|
|
215
218
|
<h3>Details</h3>
|
|
216
219
|
<p>The first <code>ICY 200 OK</code>, <code>ICY 406</code>, or
|
|
@@ -301,6 +304,88 @@
|
|
|
301
304
|
|
|
302
305
|
<!-- ------------------------------------------------------------------------------------------ -->
|
|
303
306
|
|
|
307
|
+
<script type="text/javascript">
|
|
308
|
+
RED.nodes.registerType('RtcmEncoder', {
|
|
309
|
+
category: 'GNSS',
|
|
310
|
+
color: '#C0DEED',
|
|
311
|
+
defaults: {
|
|
312
|
+
description: { value:"" },
|
|
313
|
+
},
|
|
314
|
+
inputs: 1,
|
|
315
|
+
outputs: 2,
|
|
316
|
+
icon: "function.png",
|
|
317
|
+
paletteLabel: "RTCM Encoder",
|
|
318
|
+
label: function () {
|
|
319
|
+
return this.description || "RTCM Encoder";
|
|
320
|
+
},
|
|
321
|
+
labelStyle: function() {
|
|
322
|
+
return this.description?"node_label_italic":"";
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
</script>
|
|
327
|
+
|
|
328
|
+
<script type="text/x-red" data-template-name="RtcmEncoder">
|
|
329
|
+
<div class="form-row">
|
|
330
|
+
<label for="node-input-description"><i class="fa fa-comment"></i> Description</label>
|
|
331
|
+
<input type="text" id="node-input-description" placeholder="The description of the node (optional)">
|
|
332
|
+
</div>
|
|
333
|
+
</script>
|
|
334
|
+
|
|
335
|
+
<script type="text/x-red" data-help-name="RtcmEncoder">
|
|
336
|
+
<p>Encodes an <code>RtcmMessage</code> instance back into a binary RTCM 3
|
|
337
|
+
frame using the <code>@gnss/rtcm</code> library. The inverse of the
|
|
338
|
+
<i>RTCM Decoder</i> — useful for re-emitting a decoded frame after
|
|
339
|
+
modification (e.g. rewriting a station ID before forwarding upstream).</p>
|
|
340
|
+
<p>This encoder does not construct an <code>RtcmMessage</code> from plain
|
|
341
|
+
fields. The input must already be an instance — typically the
|
|
342
|
+
<code>msg.payload.message</code> emitted by the decoder.</p>
|
|
343
|
+
|
|
344
|
+
<h3>Configuration</h3>
|
|
345
|
+
<dl class="message-properties">
|
|
346
|
+
<dt>Description <span class="property-type">string</span></dt>
|
|
347
|
+
<dd>Optional label shown in the flow editor.</dd>
|
|
348
|
+
</dl>
|
|
349
|
+
|
|
350
|
+
<h3>Inputs</h3>
|
|
351
|
+
<dl class="message-properties">
|
|
352
|
+
<dt>payload <span class="property-type">RtcmMessage</span></dt>
|
|
353
|
+
<dd>An <code>RtcmMessage</code> instance, encoded directly.</dd>
|
|
354
|
+
<dt>payload.message <span class="property-type">RtcmMessage</span></dt>
|
|
355
|
+
<dd>Round-trip shape from the <i>RTCM Decoder</i> — the encoder picks
|
|
356
|
+
up <code>msg.payload.message</code> and encodes that.</dd>
|
|
357
|
+
</dl>
|
|
358
|
+
|
|
359
|
+
<h3>Outputs</h3>
|
|
360
|
+
<p>Two outputs: successfully encoded frames go to the first; failures go to the second.</p>
|
|
361
|
+
<ol class="node-ports">
|
|
362
|
+
<li>Encoded
|
|
363
|
+
<dl class="message-properties">
|
|
364
|
+
<dt>payload.rtcmMessage <span class="property-type">Buffer</span></dt>
|
|
365
|
+
<dd>The encoded RTCM 3 frame, ready to forward to an NTRIP
|
|
366
|
+
uploader or a TCP sink.</dd>
|
|
367
|
+
<dt>payload.rtcm <span class="property-type">number</span></dt>
|
|
368
|
+
<dd>The RTCM message type identifier (e.g. <code>1005</code>).</dd>
|
|
369
|
+
<dt>payload.messageType <span class="property-type">string</span></dt>
|
|
370
|
+
<dd>The constructor name (e.g. <code>StationArp</code>).</dd>
|
|
371
|
+
<dt>payload.input <span class="property-type">any</span></dt>
|
|
372
|
+
<dd>The original input as supplied.</dd>
|
|
373
|
+
</dl>
|
|
374
|
+
</li>
|
|
375
|
+
<li>Error
|
|
376
|
+
<dl class="message-properties">
|
|
377
|
+
<dt>payload.error <span class="property-type">Error | string</span></dt>
|
|
378
|
+
<dd>The exception thrown by the encoder, or a string describing
|
|
379
|
+
invalid input.</dd>
|
|
380
|
+
<dt>payload.input <span class="property-type">any</span></dt>
|
|
381
|
+
<dd>The offending input.</dd>
|
|
382
|
+
</dl>
|
|
383
|
+
</li>
|
|
384
|
+
</ol>
|
|
385
|
+
</script>
|
|
386
|
+
|
|
387
|
+
<!-- ------------------------------------------------------------------------------------------ -->
|
|
388
|
+
|
|
304
389
|
<script type="text/javascript">
|
|
305
390
|
RED.nodes.registerType('NmeaDecoder', {
|
|
306
391
|
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
|
|
|
@@ -7,11 +7,23 @@ module.exports = function (RED) {
|
|
|
7
7
|
const NtripServerMissingMountpoint = 'SOURCETABLE 200 OK';
|
|
8
8
|
const HandshakePrefixBytes = 64;
|
|
9
9
|
|
|
10
|
+
function formatBps(bps) {
|
|
11
|
+
if (bps < 1000) {
|
|
12
|
+
return Math.round(bps) + ' bps';
|
|
13
|
+
}
|
|
14
|
+
if (bps < 1000000) {
|
|
15
|
+
return (bps / 1000).toFixed(1) + ' kbps';
|
|
16
|
+
}
|
|
17
|
+
return (bps / 1000000).toFixed(2) + ' Mbps';
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
function NtripClientNode(config) {
|
|
11
21
|
RED.nodes.createNode(this, config);
|
|
12
22
|
let node = this;
|
|
13
23
|
node.messagesReceived = 0;
|
|
14
24
|
node.messagesSent = 0;
|
|
25
|
+
node.bytesAccum = 0;
|
|
26
|
+
node.currentBps = 0;
|
|
15
27
|
node.passthrough = config.passthrough || false;
|
|
16
28
|
|
|
17
29
|
let host = typeof config.host === 'string' ? config.host.trim() : '';
|
|
@@ -70,11 +82,21 @@ module.exports = function (RED) {
|
|
|
70
82
|
node.status({
|
|
71
83
|
fill: 'green',
|
|
72
84
|
shape: 'ring',
|
|
73
|
-
text: 'Rx ' + node.messagesReceived + ' Tx ' + node.messagesSent,
|
|
85
|
+
text: formatBps(node.currentBps) + ' Rx ' + node.messagesReceived + ' Tx ' + node.messagesSent,
|
|
74
86
|
});
|
|
75
87
|
}
|
|
76
88
|
|
|
89
|
+
// Sample inbound byte counter once per second to compute the bps shown in the
|
|
90
|
+
// status badge. Counter is reset each tick — currentBps reflects the most
|
|
91
|
+
// recent 1-second window.
|
|
92
|
+
const bpsSampler = setInterval(() => {
|
|
93
|
+
node.currentBps = node.bytesAccum * 8;
|
|
94
|
+
node.bytesAccum = 0;
|
|
95
|
+
updateStatus();
|
|
96
|
+
}, 1000);
|
|
97
|
+
|
|
77
98
|
client.on('data', (data) => {
|
|
99
|
+
node.bytesAccum += data.length;
|
|
78
100
|
if (!connected) {
|
|
79
101
|
let prefix = data.toString('utf8', 0, Math.min(data.length, HandshakePrefixBytes));
|
|
80
102
|
if (prefix.startsWith(NtripServerOkReply)) {
|
|
@@ -163,6 +185,7 @@ module.exports = function (RED) {
|
|
|
163
185
|
});
|
|
164
186
|
|
|
165
187
|
this.on('close', function (done) {
|
|
188
|
+
clearInterval(bpsSampler);
|
|
166
189
|
try {
|
|
167
190
|
client.removeAllListeners();
|
|
168
191
|
client.close();
|
|
@@ -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.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Node for ntrip and rtcm handling.",
|
|
5
5
|
"node-red": {
|
|
6
6
|
"version": ">=0.1.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"eslint-config-prettier": "^10.1.8",
|
|
44
44
|
"globals": "^17.6.0",
|
|
45
45
|
"mocha": "^11.7.5",
|
|
46
|
-
"node-red": "^
|
|
46
|
+
"node-red": "^5.0.1",
|
|
47
47
|
"node-red-node-test-helper": "^0.3.6",
|
|
48
48
|
"prettier": "^3.8.3"
|
|
49
49
|
}
|
|
@@ -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
|
+
});
|