node-red-contrib-ntrip 0.2.3 → 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.
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/node.js.yml +22 -21
- package/.github/workflows/release.yml +65 -0
- package/.prettierignore +20 -0
- package/.prettierrc.json +9 -0
- package/CHANGELOG.md +69 -0
- package/CLAUDE.md +73 -0
- package/README.md +185 -18
- package/doc/architecture/README.md +19 -0
- package/doc/architecture/adr/0001-single-registration-file.md +64 -0
- package/doc/architecture/adr/0002-ntrip-uploader-extension.md +71 -0
- package/doc/architecture/adr/0003-two-output-decoder-design.md +73 -0
- package/doc/architecture/adr/0004-stateful-handshake-interception.md +74 -0
- package/doc/architecture/adr/0005-rtcm-partial-frame-buffer.md +87 -0
- package/doc/architecture/adr/0006-nmea-multi-sentence-split.md +66 -0
- package/doc/architecture/adr/0007-mocha-test-helper-stack.md +67 -0
- package/doc/architecture/adr/0008-coordinate-gating-sentinel.md +76 -0
- package/doc/architecture/adr/README.md +18 -0
- package/doc/architecture/architecture-decisions.md +60 -0
- package/doc/architecture/behavioural-design.md +226 -0
- package/doc/architecture/errors-and-weaknesses.md +71 -0
- package/doc/architecture/future-improvements.md +130 -0
- package/doc/architecture/overview.md +77 -0
- package/doc/architecture/refactoring-recommendations.md +114 -0
- package/doc/architecture/statistics.md +118 -0
- package/doc/architecture/structural-design.md +141 -0
- package/eslint.config.js +36 -0
- package/examples/rtcm-encode.json +1 -0
- package/ntrip/99-ntrip.html +289 -31
- package/ntrip/99-ntrip.js +15 -12
- package/ntrip/lib/ntrip-client.js +23 -18
- package/ntrip/nodes/nmea-decoder-node.js +77 -34
- package/ntrip/nodes/nmea-encoder-node.js +68 -47
- package/ntrip/nodes/ntrip-client-node.js +143 -123
- package/ntrip/nodes/rtcm-decoder-node.js +65 -47
- package/ntrip/nodes/rtcm-encoder-node.js +90 -0
- package/package.json +20 -2
- package/test/nmea-decoder.spec.js +146 -0
- package/test/nmea-encoder.spec.js +104 -0
- package/test/rtcm-decoder.spec.js +116 -0
- package/test/rtcm-encoder.spec.js +144 -0
- package/.github/workflows/npm-publish.yml +0 -33
package/ntrip/99-ntrip.html
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
passthrough: { value: false, required: false },
|
|
14
14
|
|
|
15
15
|
port: { value: 2101, required: false, validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 65535))) }},
|
|
16
|
-
host: { value:"" },
|
|
17
|
-
mountpoint: { value:"" },
|
|
16
|
+
host: { value:"", required: true, validate: function(v) { return typeof v === "string" && v.trim().length > 0; } },
|
|
17
|
+
mountpoint: { value:"", required: true, validate: function(v) { return typeof v === "string" && v.trim().length > 0; } },
|
|
18
18
|
interval: { value: 1000, required: false, validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 100000))) }},
|
|
19
19
|
xcoordinate: { value: 0, required: false, validate:function(v) { return ((v === "") || (RED.validators.number(v) )) }},
|
|
20
20
|
ycoordinate: { value: 0, required: false, validate:function(v) { return ((v === "") || (RED.validators.number(v) )) }},
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
</div>
|
|
109
109
|
<div class="form-row">
|
|
110
110
|
<label for="node-input-password"><i class="fa fa-key"></i> Password</label>
|
|
111
|
-
<input type="
|
|
111
|
+
<input type="password" id="node-input-password" placeholder="Enter the password here">
|
|
112
112
|
</div>
|
|
113
113
|
|
|
114
114
|
<hr align="middle"/>
|
|
@@ -140,18 +140,83 @@
|
|
|
140
140
|
</script>
|
|
141
141
|
|
|
142
142
|
<script type="text/x-red" data-help-name="NtripClient">
|
|
143
|
-
<p>
|
|
143
|
+
<p>Connects to an NTRIP caster as either a client (download) or a server (upload).
|
|
144
|
+
In download mode the node emits the bytes received from the caster (typically
|
|
145
|
+
RTCM correction data). In upload mode the node forwards bytes from its input
|
|
146
|
+
to the caster on the configured mountpoint.</p>
|
|
144
147
|
|
|
145
148
|
<h3>Configuration</h3>
|
|
149
|
+
<dl class="message-properties">
|
|
150
|
+
<dt>Host <span class="property-type">string</span></dt>
|
|
151
|
+
<dd>Hostname or IP address of the NTRIP caster (e.g. <code>rtk2go.com</code>).</dd>
|
|
152
|
+
|
|
153
|
+
<dt>Port <span class="property-type">number</span></dt>
|
|
154
|
+
<dd>TCP port of the caster. Defaults to <code>2101</code>.</dd>
|
|
155
|
+
|
|
156
|
+
<dt>Mountpoint <span class="property-type">string</span></dt>
|
|
157
|
+
<dd>The stream name on the caster. Enter the name without a leading slash.</dd>
|
|
158
|
+
|
|
159
|
+
<dt>Mode <span class="property-type">"download" | "upload"</span></dt>
|
|
160
|
+
<dd><b>Download</b>: receive correction data from the caster.<br>
|
|
161
|
+
<b>Upload</b>: push correction data to a mountpoint on the caster.</dd>
|
|
162
|
+
|
|
163
|
+
<dt>Authentication Mode <span class="property-type">string</span></dt>
|
|
164
|
+
<dd>Upload only. Selects the handshake the caster expects:
|
|
165
|
+
<ul>
|
|
166
|
+
<li><b>Legacy (Plain Text)</b> — <code>SOURCE</code> with no auth header, accepted by rtk2go.</li>
|
|
167
|
+
<li><b>Legacy (Basic Auth)</b> — <code>SOURCE</code> plus an <code>Authorization: Basic</code> header, accepted by SNIP.</li>
|
|
168
|
+
<li><b>NTRIP V1</b> — <code>POST</code> over HTTP/1.0 with Basic Auth.</li>
|
|
169
|
+
<li><b>NTRIP V2</b> — <code>POST</code> over HTTP/1.1 with Basic Auth and <code>Ntrip-Version: Ntrip/2.0</code>.</li>
|
|
170
|
+
</ul>
|
|
171
|
+
</dd>
|
|
172
|
+
|
|
173
|
+
<dt>Pass through data <span class="property-type">boolean</span></dt>
|
|
174
|
+
<dd>Upload only. If enabled, bytes written to the caster are also re-emitted
|
|
175
|
+
on the node's output for downstream processing.</dd>
|
|
176
|
+
|
|
177
|
+
<dt>Username / Password <span class="property-type">credential</span></dt>
|
|
178
|
+
<dd>Caster credentials. Stored encrypted by Node-RED.</dd>
|
|
179
|
+
|
|
180
|
+
<dt>X / Y / Z <span class="property-type">number</span></dt>
|
|
181
|
+
<dd>Optional ECEF coordinate triple. If set (i.e. not all zero) the underlying
|
|
182
|
+
client generates and periodically sends a NMEA <code>GGA</code> sentence to
|
|
183
|
+
the caster — required by some VRS / network-RTK services.</dd>
|
|
184
|
+
|
|
185
|
+
<dt>Interval <span class="property-type">number (ms)</span></dt>
|
|
186
|
+
<dd>Period between <code>GGA</code> emissions when X/Y/Z are configured.</dd>
|
|
187
|
+
</dl>
|
|
146
188
|
|
|
147
|
-
<p>Description of the device.</p>
|
|
148
|
-
|
|
149
189
|
<h3>Inputs</h3>
|
|
150
|
-
<
|
|
151
|
-
|
|
190
|
+
<dl class="message-properties">
|
|
191
|
+
<dt>payload <span class="property-type">Buffer | string</span></dt>
|
|
192
|
+
<dd>Written verbatim to the caster (upload mode). In download mode the input
|
|
193
|
+
is typically only used to refresh the position via the form below.</dd>
|
|
194
|
+
|
|
195
|
+
<dt>payload <span class="property-type">number[]</span></dt>
|
|
196
|
+
<dd>An array <code>[x, y, z]</code> is interpreted as a coordinate update and
|
|
197
|
+
forwarded to <code>client.setXYZ()</code> rather than being written to the
|
|
198
|
+
caster. Useful for streaming position fixes into the GGA emitter.</dd>
|
|
199
|
+
</dl>
|
|
200
|
+
|
|
152
201
|
<h3>Outputs</h3>
|
|
153
|
-
<
|
|
154
|
-
|
|
202
|
+
<dl class="message-properties">
|
|
203
|
+
<dt>payload <span class="property-type">Buffer</span></dt>
|
|
204
|
+
<dd>Bytes received from the caster. In download mode this is the correction
|
|
205
|
+
stream (RTCM). In upload mode with <i>Pass through data</i> enabled, written
|
|
206
|
+
bytes are also re-emitted here.</dd>
|
|
207
|
+
</dl>
|
|
208
|
+
|
|
209
|
+
<h3>Status</h3>
|
|
210
|
+
<p>The node shows <code>connecting…</code> until the caster acknowledges the
|
|
211
|
+
handshake with <code>ICY 200 OK</code>. After that it shows the running
|
|
212
|
+
<code>Rx</code> / <code>Tx</code> counters. A red badge indicates a rejected
|
|
213
|
+
connection, a missing mountpoint, or a write failure.</p>
|
|
214
|
+
|
|
215
|
+
<h3>Details</h3>
|
|
216
|
+
<p>The first <code>ICY 200 OK</code>, <code>ICY 406</code>, or
|
|
217
|
+
<code>SOURCETABLE 200 OK</code> reply is consumed by the node to set its
|
|
218
|
+
status; any RTCM bytes that arrive in the same TCP segment as the
|
|
219
|
+
<code>ICY 200 OK</code> reply are forwarded on the output.</p>
|
|
155
220
|
</script>
|
|
156
221
|
|
|
157
222
|
|
|
@@ -186,18 +251,134 @@
|
|
|
186
251
|
</script>
|
|
187
252
|
|
|
188
253
|
<script type="text/x-red" data-help-name="RtcmDecoder">
|
|
189
|
-
<p>
|
|
254
|
+
<p>Decodes binary RTCM (Radio Technical Commission for Maritime Services)
|
|
255
|
+
correction frames using the <code>@gnss/rtcm</code> library. One input
|
|
256
|
+
message may contain a single frame or several concatenated frames; each
|
|
257
|
+
decoded frame is emitted as its own output message.</p>
|
|
258
|
+
<p>The node keeps an internal buffer so frames that straddle TCP packet
|
|
259
|
+
boundaries are reassembled across consecutive input events.</p>
|
|
190
260
|
|
|
191
261
|
<h3>Configuration</h3>
|
|
262
|
+
<dl class="message-properties">
|
|
263
|
+
<dt>Description <span class="property-type">string</span></dt>
|
|
264
|
+
<dd>Optional label shown in the flow editor.</dd>
|
|
265
|
+
</dl>
|
|
192
266
|
|
|
193
|
-
<p>Description of the device.</p>
|
|
194
|
-
|
|
195
267
|
<h3>Inputs</h3>
|
|
196
|
-
<
|
|
197
|
-
|
|
268
|
+
<dl class="message-properties">
|
|
269
|
+
<dt>payload <span class="property-type">Buffer</span></dt>
|
|
270
|
+
<dd>Raw RTCM bytes, typically produced by the upstream <i>NTRIP Client</i>
|
|
271
|
+
node. Strings are accepted but converted via <code>Buffer.from</code>.</dd>
|
|
272
|
+
</dl>
|
|
273
|
+
|
|
198
274
|
<h3>Outputs</h3>
|
|
199
|
-
<p
|
|
200
|
-
<
|
|
275
|
+
<p>Two outputs: successfully decoded frames go to the first; decode failures go to the second.</p>
|
|
276
|
+
<ol class="node-ports">
|
|
277
|
+
<li>Decoded
|
|
278
|
+
<dl class="message-properties">
|
|
279
|
+
<dt>payload.rtcm <span class="property-type">number</span></dt>
|
|
280
|
+
<dd>The RTCM message type identifier (e.g. <code>1005</code>, <code>1077</code>).</dd>
|
|
281
|
+
<dt>payload.messageType <span class="property-type">string</span></dt>
|
|
282
|
+
<dd>The constructor name of the decoded message (e.g. <code>1005</code> → <code>Stationary</code>).</dd>
|
|
283
|
+
<dt>payload.message <span class="property-type">object</span></dt>
|
|
284
|
+
<dd>The decoded message object with all fields.</dd>
|
|
285
|
+
<dt>payload.input <span class="property-type">Buffer</span></dt>
|
|
286
|
+
<dd>The bytes consumed for this frame.</dd>
|
|
287
|
+
</dl>
|
|
288
|
+
</li>
|
|
289
|
+
<li>Error
|
|
290
|
+
<dl class="message-properties">
|
|
291
|
+
<dt>payload.error <span class="property-type">Error</span></dt>
|
|
292
|
+
<dd>The exception thrown by the decoder.</dd>
|
|
293
|
+
<dt>payload.input <span class="property-type">Buffer</span></dt>
|
|
294
|
+
<dd>The remaining buffer at the point of failure.</dd>
|
|
295
|
+
<dt>payload.inputString <span class="property-type">string</span></dt>
|
|
296
|
+
<dd>A debug string representation of the buffer.</dd>
|
|
297
|
+
</dl>
|
|
298
|
+
</li>
|
|
299
|
+
</ol>
|
|
300
|
+
</script>
|
|
301
|
+
|
|
302
|
+
<!-- ------------------------------------------------------------------------------------------ -->
|
|
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>
|
|
201
382
|
</script>
|
|
202
383
|
|
|
203
384
|
<!-- ------------------------------------------------------------------------------------------ -->
|
|
@@ -231,18 +412,52 @@
|
|
|
231
412
|
</script>
|
|
232
413
|
|
|
233
414
|
<script type="text/x-red" data-help-name="NmeaDecoder">
|
|
234
|
-
<p>
|
|
415
|
+
<p>Decodes NMEA 0183 sentences (<code>GGA</code>, <code>RMC</code>,
|
|
416
|
+
<code>GSV</code>, …) using the <code>@gnss/nmea</code> library. A single
|
|
417
|
+
input message may contain several <code>\r\n</code>-delimited sentences;
|
|
418
|
+
each is decoded individually and emitted as its own output message.</p>
|
|
235
419
|
|
|
236
420
|
<h3>Configuration</h3>
|
|
421
|
+
<dl class="message-properties">
|
|
422
|
+
<dt>Description <span class="property-type">string</span></dt>
|
|
423
|
+
<dd>Optional label shown in the flow editor.</dd>
|
|
424
|
+
</dl>
|
|
237
425
|
|
|
238
|
-
<p>Description of the device.</p>
|
|
239
|
-
|
|
240
426
|
<h3>Inputs</h3>
|
|
241
|
-
<
|
|
242
|
-
|
|
427
|
+
<dl class="message-properties">
|
|
428
|
+
<dt>payload <span class="property-type">string | Buffer</span></dt>
|
|
429
|
+
<dd>A NMEA sentence or a chunk containing several sentences. Buffers are
|
|
430
|
+
decoded as UTF-8.</dd>
|
|
431
|
+
<dt>payload.nmeaMessage <span class="property-type">string | Buffer</span></dt>
|
|
432
|
+
<dd>Alternative location for the input. If <code>msg.payload</code> is an
|
|
433
|
+
object with an <code>nmeaMessage</code> property, that value is used
|
|
434
|
+
instead of <code>msg.payload</code> itself.</dd>
|
|
435
|
+
</dl>
|
|
436
|
+
|
|
243
437
|
<h3>Outputs</h3>
|
|
244
|
-
<p
|
|
245
|
-
<
|
|
438
|
+
<p>Two outputs: successfully decoded sentences go to the first; decode failures go to the second.</p>
|
|
439
|
+
<ol class="node-ports">
|
|
440
|
+
<li>Decoded
|
|
441
|
+
<dl class="message-properties">
|
|
442
|
+
<dt>payload.messageType <span class="property-type">string</span></dt>
|
|
443
|
+
<dd>Sentence type in upper case, e.g. <code>GGA</code>, <code>RMC</code>, <code>GSV</code>.</dd>
|
|
444
|
+
<dt>payload.nmeaMessage <span class="property-type">object</span></dt>
|
|
445
|
+
<dd>The decoded sentence object with all fields.</dd>
|
|
446
|
+
<dt>payload.input <span class="property-type">string</span></dt>
|
|
447
|
+
<dd>The raw sentence as it was parsed.</dd>
|
|
448
|
+
</dl>
|
|
449
|
+
</li>
|
|
450
|
+
<li>Error
|
|
451
|
+
<dl class="message-properties">
|
|
452
|
+
<dt>payload.error <span class="property-type">Error | string</span></dt>
|
|
453
|
+
<dd>The exception or reason for the decode failure.</dd>
|
|
454
|
+
<dt>payload.input <span class="property-type">string</span></dt>
|
|
455
|
+
<dd>The sentence that failed to decode.</dd>
|
|
456
|
+
<dt>payload.inputString <span class="property-type">string</span></dt>
|
|
457
|
+
<dd>String representation of the input for logging.</dd>
|
|
458
|
+
</dl>
|
|
459
|
+
</li>
|
|
460
|
+
</ol>
|
|
246
461
|
</script>
|
|
247
462
|
|
|
248
463
|
<!-- ------------------------------------------------------------------------------------------ -->
|
|
@@ -276,16 +491,59 @@
|
|
|
276
491
|
</script>
|
|
277
492
|
|
|
278
493
|
<script type="text/x-red" data-help-name="NmeaEncoder">
|
|
279
|
-
<p>
|
|
494
|
+
<p>Encodes a NMEA sentence object back to its textual form using the
|
|
495
|
+
<code>@gnss/nmea</code> library. The inverse of the <i>NMEA Decoder</i>
|
|
496
|
+
node — useful for round-tripping or for emitting handcrafted sentences
|
|
497
|
+
(e.g. a <code>GGA</code> fix into an NTRIP uploader).</p>
|
|
280
498
|
|
|
281
499
|
<h3>Configuration</h3>
|
|
500
|
+
<dl class="message-properties">
|
|
501
|
+
<dt>Description <span class="property-type">string</span></dt>
|
|
502
|
+
<dd>Optional label shown in the flow editor.</dd>
|
|
503
|
+
</dl>
|
|
282
504
|
|
|
283
|
-
<p>Description of the device.</p>
|
|
284
|
-
|
|
285
505
|
<h3>Inputs</h3>
|
|
286
|
-
<
|
|
287
|
-
|
|
506
|
+
<dl class="message-properties">
|
|
507
|
+
<dt>payload.messageType <span class="property-type">string</span></dt>
|
|
508
|
+
<dd>Sentence type, e.g. <code>GGA</code>, <code>RMC</code>, <code>VTG</code>,
|
|
509
|
+
<code>GSV</code>, <code>ZDA</code>. Case-insensitive. Supported types:
|
|
510
|
+
<code>DTM</code>, <code>GBS</code>, <code>GGA</code>, <code>GLL</code>,
|
|
511
|
+
<code>GNS</code>, <code>GRS</code>, <code>GSA</code>, <code>GST</code>,
|
|
512
|
+
<code>GSV</code>, <code>RMC</code>, <code>THS</code>, <code>TXT</code>,
|
|
513
|
+
<code>VHW</code>, <code>VLW</code>, <code>VPW</code>, <code>VTG</code>,
|
|
514
|
+
<code>ZDA</code>, plus <code>OBJECT</code> for the generic
|
|
515
|
+
<code>NmeaMessageUnknown</code> form.</dd>
|
|
516
|
+
<dt>payload.nmeaMessage <span class="property-type">object | NmeaMessage</span></dt>
|
|
517
|
+
<dd>The sentence content. Either a plain field object that matches the
|
|
518
|
+
target sentence schema, or an already-constructed <code>NmeaMessage</code>
|
|
519
|
+
instance (which is forwarded as-is to <code>NmeaTransport.encode</code>).</dd>
|
|
520
|
+
</dl>
|
|
521
|
+
|
|
288
522
|
<h3>Outputs</h3>
|
|
289
|
-
<p
|
|
290
|
-
<
|
|
523
|
+
<p>Two outputs: successfully encoded sentences go to the first; failures go to the second.</p>
|
|
524
|
+
<ol class="node-ports">
|
|
525
|
+
<li>Encoded
|
|
526
|
+
<dl class="message-properties">
|
|
527
|
+
<dt>payload.nmeaMessage <span class="property-type">string</span></dt>
|
|
528
|
+
<dd>The encoded NMEA sentence (including <code>$</code> prefix and
|
|
529
|
+
checksum).</dd>
|
|
530
|
+
<dt>payload.messageType <span class="property-type">string</span></dt>
|
|
531
|
+
<dd>The sentence type, echoed back as it was supplied.</dd>
|
|
532
|
+
<dt>payload.input <span class="property-type">object</span></dt>
|
|
533
|
+
<dd>The original input object.</dd>
|
|
534
|
+
</dl>
|
|
535
|
+
</li>
|
|
536
|
+
<li>Error
|
|
537
|
+
<dl class="message-properties">
|
|
538
|
+
<dt>payload.error <span class="property-type">Error | string</span></dt>
|
|
539
|
+
<dd>The exception or reason for the failure. Unknown
|
|
540
|
+
<code>messageType</code> values, missing input fields, and
|
|
541
|
+
encoder errors all surface here.</dd>
|
|
542
|
+
<dt>payload.input <span class="property-type">any</span></dt>
|
|
543
|
+
<dd>The offending input.</dd>
|
|
544
|
+
<dt>payload.inputString <span class="property-type">string</span></dt>
|
|
545
|
+
<dd>String representation of the input for logging.</dd>
|
|
546
|
+
</dl>
|
|
547
|
+
</li>
|
|
548
|
+
</ol>
|
|
291
549
|
</script>
|
package/ntrip/99-ntrip.js
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Created by Karl-Heinz Wind
|
|
3
|
-
**/
|
|
2
|
+
* Created by Karl-Heinz Wind
|
|
3
|
+
**/
|
|
4
4
|
|
|
5
5
|
module.exports = function (RED) {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
8
|
const pkg = require('./../package.json');
|
|
9
9
|
RED.log.info('node-red-contrib-ntrip version: v' + pkg.version);
|
|
10
10
|
|
|
11
11
|
const NtripClientNode = require('./nodes/ntrip-client-node.js')(RED);
|
|
12
|
-
RED.nodes.registerType(
|
|
12
|
+
RED.nodes.registerType('NtripClient', NtripClientNode, {
|
|
13
13
|
credentials: {
|
|
14
|
-
username: { type:
|
|
15
|
-
password: { type:
|
|
16
|
-
}
|
|
14
|
+
username: { type: 'text' },
|
|
15
|
+
password: { type: 'password' },
|
|
16
|
+
},
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
const RtcmDecoderNode = require('./nodes/rtcm-decoder-node.js')(RED);
|
|
20
|
-
RED.nodes.registerType(
|
|
20
|
+
RED.nodes.registerType('RtcmDecoder', RtcmDecoderNode);
|
|
21
|
+
|
|
22
|
+
const RtcmEncoderNode = require('./nodes/rtcm-encoder-node.js')(RED);
|
|
23
|
+
RED.nodes.registerType('RtcmEncoder', RtcmEncoderNode);
|
|
21
24
|
|
|
22
25
|
const NmeaDecoderNode = require('./nodes/nmea-decoder-node.js')(RED);
|
|
23
|
-
RED.nodes.registerType(
|
|
26
|
+
RED.nodes.registerType('NmeaDecoder', NmeaDecoderNode);
|
|
24
27
|
|
|
25
28
|
const NmeaEncoderNode = require('./nodes/nmea-encoder-node.js')(RED);
|
|
26
|
-
RED.nodes.registerType(
|
|
27
|
-
}
|
|
29
|
+
RED.nodes.registerType('NmeaEncoder', NmeaEncoderNode);
|
|
30
|
+
};
|
|
@@ -6,9 +6,18 @@ const net = require('net');
|
|
|
6
6
|
|
|
7
7
|
// Orginal class is extended to be able to send data to the caster.
|
|
8
8
|
class NtripClientUploader extends NtripClient {
|
|
9
|
-
constructor(options) {
|
|
9
|
+
constructor(options) {
|
|
10
10
|
super(options);
|
|
11
11
|
this.authmode = options.authmode || 'legacy';
|
|
12
|
+
|
|
13
|
+
// Reject CR/LF in user-supplied fields that are interpolated into the
|
|
14
|
+
// handshake string, to prevent header injection.
|
|
15
|
+
for (const field of ['mountpoint', 'username', 'password']) {
|
|
16
|
+
const value = this[field];
|
|
17
|
+
if (typeof value === 'string' && /[\r\n]/.test(value)) {
|
|
18
|
+
throw new Error('Invalid character in ' + field + ': CR/LF not allowed');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
_connect() {
|
|
@@ -19,12 +28,12 @@ class NtripClientUploader extends NtripClient {
|
|
|
19
28
|
// init connection of client
|
|
20
29
|
this.client = net.createConnection({
|
|
21
30
|
host: this.host,
|
|
22
|
-
port: this.port
|
|
31
|
+
port: this.port,
|
|
23
32
|
});
|
|
24
33
|
|
|
25
34
|
// on timeout event
|
|
26
35
|
this.client.on('timeout', () => {
|
|
27
|
-
this._onError('socket
|
|
36
|
+
this._onError('socket timeout');
|
|
28
37
|
});
|
|
29
38
|
|
|
30
39
|
// on connect event
|
|
@@ -36,23 +45,19 @@ class NtripClientUploader extends NtripClient {
|
|
|
36
45
|
const host = this.host;
|
|
37
46
|
const port = this.port;
|
|
38
47
|
const authmode = this.authmode;
|
|
39
|
-
const authorization = Buffer.from(
|
|
40
|
-
|
|
41
|
-
'utf8'
|
|
42
|
-
).toString('base64');
|
|
43
|
-
|
|
48
|
+
const authorization = Buffer.from(username + ':' + password, 'utf8').toString('base64');
|
|
49
|
+
|
|
44
50
|
let data;
|
|
45
51
|
switch (authmode) {
|
|
46
|
-
case 'legacy': // Legacy --> rtk2Go accepts this.
|
|
52
|
+
case 'legacy': // Legacy --> rtk2Go accepts this.
|
|
47
53
|
if (username === '' && password === '') {
|
|
48
54
|
data = `SOURCE ${mountpoint}\r\n\r\n`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
data = `SOURCE ${password} /${mountpoint}\r\nSource-Agent: NTRIP ${username}\r\n\r\n`;
|
|
55
|
+
} else {
|
|
56
|
+
data = `SOURCE ${password} /${mountpoint}\r\nSource-Agent: NTRIP ${username}\r\n\r\n`;
|
|
52
57
|
}
|
|
53
58
|
break;
|
|
54
59
|
case 'hybrid': // hybrid (legacy + http auth) --> SNIP accepts this.
|
|
55
|
-
data = `SOURCE ${mountpoint}\r\nAuthorization: Basic ${authorization}\r\n\r\n`;
|
|
60
|
+
data = `SOURCE ${mountpoint}\r\nAuthorization: Basic ${authorization}\r\n\r\n`;
|
|
56
61
|
break;
|
|
57
62
|
case 'ntripv1': // NTRIP V1 POST
|
|
58
63
|
data = `POST /${mountpoint} HTTP/1.0\r\nUser-Agent: NTRIP ${userAgent}\r\nAuthorization: Basic ${authorization}\r\nContent-Type: gnss/data\r\n\r\n`;
|
|
@@ -61,7 +66,7 @@ class NtripClientUploader extends NtripClient {
|
|
|
61
66
|
data = `POST /${mountpoint} HTTP/1.1\r\nHost: ${host}:${port}\r\nUser-Agent: NTRIP ${userAgent}\r\nAuthorization: Basic ${authorization}\r\nNtrip-Version: Ntrip/2.0\r\nContent-Type: gnss/data\r\nConnection: keep-alive\r\n\r\n`;
|
|
62
67
|
break;
|
|
63
68
|
default:
|
|
64
|
-
throw new Error(
|
|
69
|
+
throw new Error('Auth mode is not supported ' + authmode);
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
this.client.write(data);
|
|
@@ -93,14 +98,14 @@ class NtripClientUploader extends NtripClient {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
function createDownloader(options) {
|
|
96
|
-
return new NtripClient(options)
|
|
101
|
+
return new NtripClient(options);
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
function createUploader(options) {
|
|
100
|
-
return new NtripClientUploader(options)
|
|
105
|
+
return new NtripClientUploader(options);
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
module.exports = {
|
|
104
109
|
createDownloader,
|
|
105
|
-
createUploader
|
|
106
|
-
};
|
|
110
|
+
createUploader,
|
|
111
|
+
};
|
|
@@ -9,54 +9,97 @@ module.exports = function (RED) {
|
|
|
9
9
|
node.nmeaMessagesReceived = 0;
|
|
10
10
|
node.invalidMessagesReceived = 0;
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
function updateStatus() {
|
|
13
|
+
node.status({
|
|
14
|
+
fill: 'green',
|
|
15
|
+
shape: 'ring',
|
|
16
|
+
text: 'NMEA: ' + node.nmeaMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try
|
|
20
|
-
{
|
|
21
|
-
let nmeaMessage = NmeaTransport.decode(buffer);
|
|
20
|
+
function decodeOne(sentence, rawInput, rawString) {
|
|
21
|
+
try {
|
|
22
|
+
let nmeaMessage = NmeaTransport.decode(sentence);
|
|
22
23
|
let messageType = nmeaMessage.constructor.name.replace('NmeaMessage', '').toUpperCase();
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
node.send([
|
|
26
|
+
{
|
|
27
|
+
payload: {
|
|
28
|
+
messageType: messageType,
|
|
29
|
+
nmeaMessage: nmeaMessage,
|
|
30
|
+
input: sentence,
|
|
31
|
+
},
|
|
29
32
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
node.send([msg, null]);
|
|
33
|
+
null,
|
|
34
|
+
]);
|
|
33
35
|
node.nmeaMessagesReceived++;
|
|
36
|
+
} catch (ex) {
|
|
37
|
+
node.send([
|
|
38
|
+
null,
|
|
39
|
+
{
|
|
40
|
+
payload: {
|
|
41
|
+
error: ex,
|
|
42
|
+
input: rawInput,
|
|
43
|
+
inputString: rawString,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
node.invalidMessagesReceived++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.on('input', function (msg) {
|
|
52
|
+
if (msg.payload === undefined || msg.payload === null) {
|
|
53
|
+
return;
|
|
34
54
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
|
|
56
|
+
// rawInput is the value the user provided (Buffer, string, ...).
|
|
57
|
+
// Keep it untouched so the error output can return it as-is.
|
|
58
|
+
let rawInput = typeof msg.payload === 'object' && msg.payload.nmeaMessage !== undefined ? msg.payload.nmeaMessage : msg.payload;
|
|
59
|
+
|
|
60
|
+
let rawString;
|
|
61
|
+
if (Buffer.isBuffer(rawInput)) {
|
|
62
|
+
rawString = rawInput.toString('utf8');
|
|
63
|
+
} else if (typeof rawInput === 'string') {
|
|
64
|
+
rawString = rawInput;
|
|
65
|
+
} else {
|
|
66
|
+
node.send([
|
|
67
|
+
null,
|
|
68
|
+
{
|
|
69
|
+
payload: {
|
|
70
|
+
error: 'Payload is neither string nor Buffer',
|
|
71
|
+
input: rawInput,
|
|
72
|
+
inputString: String(rawInput),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
45
76
|
node.invalidMessagesReceived++;
|
|
77
|
+
updateStatus();
|
|
78
|
+
return;
|
|
46
79
|
}
|
|
47
80
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
// A single chunk may carry several sentences delimited by \r\n.
|
|
82
|
+
let sentences = rawString.split(/\r?\n/);
|
|
83
|
+
let any = false;
|
|
84
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
85
|
+
let s = sentences[i].trim();
|
|
86
|
+
if (s.length === 0) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
decodeOne(s, rawInput, rawString);
|
|
90
|
+
any = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (any) {
|
|
94
|
+
updateStatus();
|
|
95
|
+
}
|
|
53
96
|
});
|
|
54
97
|
|
|
55
|
-
this.on('close', function(done) {
|
|
98
|
+
this.on('close', function (done) {
|
|
56
99
|
node.status({});
|
|
57
100
|
done();
|
|
58
101
|
});
|
|
59
102
|
}
|
|
60
103
|
|
|
61
104
|
return NmeaDecoderNode;
|
|
62
|
-
};
|
|
105
|
+
};
|