node-red-contrib-ntrip 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +17 -0
- package/examples/watchdog.json +1 -0
- package/ntrip/lib/ntrip-client.js +107 -74
- package/ntrip/nodes/nmea-decoder-node.js +36 -38
- package/ntrip/nodes/nmea-encoder-node.js +97 -97
- package/ntrip/nodes/ntrip-client-node.js +153 -148
- package/ntrip/nodes/rtcm-decoder-node.js +50 -47
- package/ntrip/nodes/rtcm-encoder-node.js +33 -32
- package/package.json +16 -6
- package/.github/dependabot.yml +0 -23
- package/.github/workflows/node.js.yml +0 -32
- package/.github/workflows/release.yml +0 -65
- package/.prettierignore +0 -20
- package/.prettierrc.json +0 -9
- package/CLAUDE.md +0 -73
- package/doc/architecture/README.md +0 -19
- package/doc/architecture/adr/0001-single-registration-file.md +0 -64
- package/doc/architecture/adr/0002-ntrip-uploader-extension.md +0 -71
- package/doc/architecture/adr/0003-two-output-decoder-design.md +0 -73
- package/doc/architecture/adr/0004-stateful-handshake-interception.md +0 -74
- package/doc/architecture/adr/0005-rtcm-partial-frame-buffer.md +0 -87
- package/doc/architecture/adr/0006-nmea-multi-sentence-split.md +0 -66
- package/doc/architecture/adr/0007-mocha-test-helper-stack.md +0 -67
- package/doc/architecture/adr/0008-coordinate-gating-sentinel.md +0 -76
- package/doc/architecture/adr/README.md +0 -18
- package/doc/architecture/architecture-decisions.md +0 -60
- package/doc/architecture/behavioural-design.md +0 -226
- package/doc/architecture/errors-and-weaknesses.md +0 -71
- package/doc/architecture/future-improvements.md +0 -130
- package/doc/architecture/overview.md +0 -77
- package/doc/architecture/refactoring-recommendations.md +0 -114
- package/doc/architecture/statistics.md +0 -118
- package/doc/architecture/structural-design.md +0 -141
- package/eslint.config.js +0 -36
- package/test/nmea-decoder.spec.js +0 -146
- package/test/nmea-encoder.spec.js +0 -104
- package/test/rtcm-decoder.spec.js +0 -116
- package/test/rtcm-encoder.spec.js +0 -144
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
All notable changes to this project will be documented in this file.
|
|
3
3
|
|
|
4
|
+
## [0.2.11]
|
|
5
|
+
### Documentation
|
|
6
|
+
- `examples/watchdog.json` now emits a `RECOVERED` message on the first data frame that arrives after a stall, in addition to the existing `STALLED` notification. Per-frame messages are still suppressed — you only see the two transitions.
|
|
7
|
+
|
|
8
|
+
### Internal — adopted `node-red-standards` (no runtime behaviour change)
|
|
9
|
+
- Migrated the test suite from `mocha` + `chai` to `node --test` + `node:assert` (still uses `node-red-node-test-helper`; specs renamed `*.spec.js` → `*.test.js`).
|
|
10
|
+
- Added portable `AGENTS.md` as the tool-neutral rule source of truth; `CLAUDE.md` slimmed to a thin adapter.
|
|
11
|
+
- Added `.github/workflows/standards-check.yml` — CI-side audit against `node-red-standards`.
|
|
12
|
+
- Consolidated the previous tag-driven `release.yml` into `npm-publish.yml` (same behaviour, standard-conforming filename).
|
|
13
|
+
- Bumped `engines.node` from `>=18.0.0` to `>=20.0.0`; CI/publish Node matrix trimmed to `20.x, 22.x` (Node 18 is EOL).
|
|
14
|
+
- Refactored all node files to single-exit style — every function has exactly one `return` at its final position. Zero behavioural change; 20/20 tests still pass.
|
|
15
|
+
|
|
16
|
+
## [0.2.10]
|
|
17
|
+
### NtripClient
|
|
18
|
+
- Reconnect attempts now use an exponential-ish backoff schedule (`1 s`, `2 s`, `5 s`, `10 s`, then capped at `10 s`) instead of the upstream library's fixed `2 s` interval. The schedule resets to `1 s` the moment the caster completes the `ICY 200 OK` handshake, so a brief outage restarts from the short end and a caster that stays down is no longer hammered.
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
- Added `examples/watchdog.json` — an example flow that alerts (via a built-in `trigger` node) when the NTRIP correction stream stops delivering data for more than 60 seconds. Closes [#4](https://github.com/windkh/node-red-contrib-ntrip/issues/4).
|
|
22
|
+
|
|
4
23
|
## [0.2.9]
|
|
5
24
|
### NtripClient
|
|
6
25
|
- 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.
|
package/README.md
CHANGED
|
@@ -299,6 +299,11 @@ A few things that are easy to get wrong:
|
|
|
299
299
|
- **NTRIP Client: CR/LF in credentials.** `mountpoint`, `username`, and
|
|
300
300
|
`password` are interpolated into the handshake string. CR/LF in these fields
|
|
301
301
|
is rejected at startup to prevent header injection.
|
|
302
|
+
- **NTRIP Client: reconnect backoff.** When the socket to the caster drops the
|
|
303
|
+
client waits `1 s`, then `2 s`, `5 s`, `10 s` before each successive retry
|
|
304
|
+
(capped at `10 s`). The schedule resets to `1 s` as soon as the caster
|
|
305
|
+
completes the `ICY 200 OK` handshake again — so a brief outage restarts at
|
|
306
|
+
the low end, and a caster that stays down doesn't get hammered.
|
|
302
307
|
- **NMEA Encoder input shape.** The encoder expects `msg.payload` to be an
|
|
303
308
|
object with `messageType` and `nmeaMessage` properties — not a NMEA string.
|
|
304
309
|
To re-encode the output of the NMEA Decoder you can pipe it straight in;
|
|
@@ -345,6 +350,18 @@ a local demo version of SNIP by creating a raw TCP input stream.
|
|
|
345
350
|
(SNIP can be found here https://www.use-snip.com)
|
|
346
351
|
See also example flow [**Upload flow**](examples/upload.json)
|
|
347
352
|
|
|
353
|
+
## Detect a stalled correction stream
|
|
354
|
+
NTRIP casters occasionally stop emitting data without closing the TCP socket
|
|
355
|
+
— the NtripClient node stays "connected" but no new frames arrive, and any
|
|
356
|
+
downstream rover eventually drops out of RTK-fixed mode without an obvious
|
|
357
|
+
cause. This example wires the NtripClient output into a built-in `trigger`
|
|
358
|
+
node configured to fire after 60 seconds of silence (configurable via the
|
|
359
|
+
trigger's Duration field). A small `function` node then acts as a state
|
|
360
|
+
tracker: it emits a **`STALLED`** notification when the trigger fires and a
|
|
361
|
+
**`RECOVERED`** notification on the first data frame that arrives after a
|
|
362
|
+
stall. Per-frame messages are suppressed — you only see the two transitions.
|
|
363
|
+
See also example flow [**Watchdog flow**](examples/watchdog.json) (closes [#4](https://github.com/windkh/node-red-contrib-ntrip/issues/4))
|
|
364
|
+
|
|
348
365
|
# License
|
|
349
366
|
|
|
350
367
|
Author: Karl-Heinz Wind
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"id":"47bd8463328d83d3","type":"comment","z":"a604108643639054","name":"NTRIP stream stall + recovery watchdog","info":"Emits a message when the NTRIP correction stream stops flowing\nAND when it starts flowing again — nothing else.\n\nHow it works:\n- Every message from the NTRIP Client resets the trigger node's timer.\n- If no message arrives within 60 seconds, the trigger fires; a change\n node tags it with topic=stall and the state tracker function emits a\n STALLED notification (once).\n- When data resumes, the state tracker sees the first data frame after\n the stall and emits a RECOVERED notification (once).\n- While the stream is flowing normally, the state tracker suppresses\n per-frame messages — you only see transitions.\n\nAdjust the timeout in the trigger node's Duration field.\n","x":260,"y":80,"wires":[]},{"id":"9f622b9ab6a72772","type":"NtripClient","z":"a604108643639054","description":"RTK2Go","passthrough":false,"port":"2101","host":"rtk2go.com","mountpoint":"Foo","interval":1000,"xcoordinate":"","ycoordinate":"","zcoordinate":"","x":140,"y":180,"wires":[["5107785e11b2425a","eb42f2cd60fcdc8f","1a2b3c4d5e6f7089"]]},{"id":"5107785e11b2425a","type":"debug","z":"a604108643639054","name":"data (each frame)","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":430,"y":140,"wires":[]},{"id":"eb42f2cd60fcdc8f","type":"trigger","z":"a604108643639054","name":"60s stall watchdog","op1":"","op2":"stall","op1type":"nul","op2type":"str","duration":"60","extend":true,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":440,"y":220,"wires":[["ab12cd34ef567890"]]},{"id":"ab12cd34ef567890","type":"change","z":"a604108643639054","name":"topic=stall","rules":[{"t":"set","p":"topic","pt":"msg","to":"stall","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":220,"wires":[["1a2b3c4d5e6f7089"]]},{"id":"1a2b3c4d5e6f7089","type":"function","z":"a604108643639054","name":"state tracker","func":"if (msg.topic === 'stall') {\n context.set('stalled', true);\n msg.payload = 'STALLED — no data for 60 s';\n return msg;\n}\n\n// Data frame from NtripClient — check whether we just recovered.\nif (context.get('stalled')) {\n context.set('stalled', false);\n msg.payload = 'RECOVERED — NTRIP stream is flowing again';\n return msg;\n}\n\n// Stream is fine; no transition to report.\nreturn null;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":840,"y":260,"wires":[["25f028d01cad6b57"]]},{"id":"25f028d01cad6b57","type":"debug","z":"a604108643639054","name":"STATUS","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1020,"y":260,"wires":[]}]
|
|
@@ -4,8 +4,43 @@
|
|
|
4
4
|
const { NtripClient } = require('ntrip-client');
|
|
5
5
|
const net = require('net');
|
|
6
6
|
|
|
7
|
+
// Backoff schedule for reconnect attempts, in milliseconds. The upstream
|
|
8
|
+
// client's default is a fixed 2000ms interval which flooded casters (and the
|
|
9
|
+
// Node-RED error log) when a server was down for a while. See issue on repeated
|
|
10
|
+
// error output during outages. The last entry acts as a cap for further retries.
|
|
11
|
+
const RECONNECT_BACKOFF_MS = [1000, 2000, 5000, 10000];
|
|
12
|
+
|
|
13
|
+
// Extends the upstream client with an exponential backoff on reconnect.
|
|
14
|
+
// The schedule advances on each failed reconnect and resets to the first
|
|
15
|
+
// entry as soon as the caster completes the handshake (`ICY 200 OK`).
|
|
16
|
+
class NtripClientWithBackoff extends NtripClient {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
super(options);
|
|
19
|
+
this._backoffIndex = 0;
|
|
20
|
+
this.reconnectInterval = RECONNECT_BACKOFF_MS[0];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_onData(data) {
|
|
24
|
+
const wasReady = this.isReady;
|
|
25
|
+
super._onData(data);
|
|
26
|
+
// Reset the backoff schedule the moment the handshake completes —
|
|
27
|
+
// subsequent connection failures start over at the first entry.
|
|
28
|
+
if (!wasReady && this.isReady) {
|
|
29
|
+
this._backoffIndex = 0;
|
|
30
|
+
this.reconnectInterval = RECONNECT_BACKOFF_MS[0];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async _reconnect() {
|
|
35
|
+
const step = Math.min(this._backoffIndex, RECONNECT_BACKOFF_MS.length - 1);
|
|
36
|
+
this.reconnectInterval = RECONNECT_BACKOFF_MS[step];
|
|
37
|
+
this._backoffIndex++;
|
|
38
|
+
return super._reconnect();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
7
42
|
// Orginal class is extended to be able to send data to the caster.
|
|
8
|
-
class NtripClientUploader extends
|
|
43
|
+
class NtripClientUploader extends NtripClientWithBackoff {
|
|
9
44
|
constructor(options) {
|
|
10
45
|
super(options);
|
|
11
46
|
this.authmode = options.authmode || 'legacy';
|
|
@@ -21,84 +56,82 @@ class NtripClientUploader extends NtripClient {
|
|
|
21
56
|
}
|
|
22
57
|
|
|
23
58
|
_connect() {
|
|
24
|
-
if (this.isClose) {
|
|
25
|
-
|
|
59
|
+
if (!this.isClose) {
|
|
60
|
+
// init connection of client
|
|
61
|
+
this.client = net.createConnection({
|
|
62
|
+
host: this.host,
|
|
63
|
+
port: this.port,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// on timeout event
|
|
67
|
+
this.client.on('timeout', () => {
|
|
68
|
+
this._onError('socket timeout');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// on connect event
|
|
72
|
+
this.client.on('connect', () => {
|
|
73
|
+
const mountpoint = this.mountpoint;
|
|
74
|
+
const userAgent = this.userAgent;
|
|
75
|
+
const username = this.username;
|
|
76
|
+
const password = this.password;
|
|
77
|
+
const host = this.host;
|
|
78
|
+
const port = this.port;
|
|
79
|
+
const authmode = this.authmode;
|
|
80
|
+
const authorization = Buffer.from(username + ':' + password, 'utf8').toString('base64');
|
|
81
|
+
|
|
82
|
+
let data;
|
|
83
|
+
switch (authmode) {
|
|
84
|
+
case 'legacy': // Legacy --> rtk2Go accepts this.
|
|
85
|
+
if (username === '' && password === '') {
|
|
86
|
+
data = `SOURCE ${mountpoint}\r\n\r\n`;
|
|
87
|
+
} else {
|
|
88
|
+
data = `SOURCE ${password} /${mountpoint}\r\nSource-Agent: NTRIP ${username}\r\n\r\n`;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case 'hybrid': // hybrid (legacy + http auth) --> SNIP accepts this.
|
|
92
|
+
data = `SOURCE ${mountpoint}\r\nAuthorization: Basic ${authorization}\r\n\r\n`;
|
|
93
|
+
break;
|
|
94
|
+
case 'ntripv1': // NTRIP V1 POST
|
|
95
|
+
data = `POST /${mountpoint} HTTP/1.0\r\nUser-Agent: NTRIP ${userAgent}\r\nAuthorization: Basic ${authorization}\r\nContent-Type: gnss/data\r\n\r\n`;
|
|
96
|
+
break;
|
|
97
|
+
case 'ntripv2': // NTRIP V2 POST
|
|
98
|
+
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`;
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
throw new Error('Auth mode is not supported ' + authmode);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.client.write(data);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// on data event
|
|
108
|
+
this.client.on('data', (data) => {
|
|
109
|
+
this._onData(data);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// on end event
|
|
113
|
+
this.client.on('end', () => {
|
|
114
|
+
this._onError('socket ended');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// on close event
|
|
118
|
+
this.client.on('close', () => {
|
|
119
|
+
this._onError('socket closed');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// on error event
|
|
123
|
+
this.client.on('error', (err) => {
|
|
124
|
+
// TODO: check for code = ECONNREFUSED
|
|
125
|
+
this._onError(err);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
this.client.setTimeout(this.timeout);
|
|
26
129
|
}
|
|
27
|
-
|
|
28
|
-
// init connection of client
|
|
29
|
-
this.client = net.createConnection({
|
|
30
|
-
host: this.host,
|
|
31
|
-
port: this.port,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// on timeout event
|
|
35
|
-
this.client.on('timeout', () => {
|
|
36
|
-
this._onError('socket timeout');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// on connect event
|
|
40
|
-
this.client.on('connect', () => {
|
|
41
|
-
const mountpoint = this.mountpoint;
|
|
42
|
-
const userAgent = this.userAgent;
|
|
43
|
-
const username = this.username;
|
|
44
|
-
const password = this.password;
|
|
45
|
-
const host = this.host;
|
|
46
|
-
const port = this.port;
|
|
47
|
-
const authmode = this.authmode;
|
|
48
|
-
const authorization = Buffer.from(username + ':' + password, 'utf8').toString('base64');
|
|
49
|
-
|
|
50
|
-
let data;
|
|
51
|
-
switch (authmode) {
|
|
52
|
-
case 'legacy': // Legacy --> rtk2Go accepts this.
|
|
53
|
-
if (username === '' && password === '') {
|
|
54
|
-
data = `SOURCE ${mountpoint}\r\n\r\n`;
|
|
55
|
-
} else {
|
|
56
|
-
data = `SOURCE ${password} /${mountpoint}\r\nSource-Agent: NTRIP ${username}\r\n\r\n`;
|
|
57
|
-
}
|
|
58
|
-
break;
|
|
59
|
-
case 'hybrid': // hybrid (legacy + http auth) --> SNIP accepts this.
|
|
60
|
-
data = `SOURCE ${mountpoint}\r\nAuthorization: Basic ${authorization}\r\n\r\n`;
|
|
61
|
-
break;
|
|
62
|
-
case 'ntripv1': // NTRIP V1 POST
|
|
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`;
|
|
64
|
-
break;
|
|
65
|
-
case 'ntripv2': // NTRIP V2 POST
|
|
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`;
|
|
67
|
-
break;
|
|
68
|
-
default:
|
|
69
|
-
throw new Error('Auth mode is not supported ' + authmode);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.client.write(data);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// on data event
|
|
76
|
-
this.client.on('data', (data) => {
|
|
77
|
-
this._onData(data);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// on end event
|
|
81
|
-
this.client.on('end', () => {
|
|
82
|
-
this._onError('socket ended');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// on close event
|
|
86
|
-
this.client.on('close', () => {
|
|
87
|
-
this._onError('socket closed');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// on error event
|
|
91
|
-
this.client.on('error', (err) => {
|
|
92
|
-
// TODO: check for code = ECONNREFUSED
|
|
93
|
-
this._onError(err);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
this.client.setTimeout(this.timeout);
|
|
97
130
|
}
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
function createDownloader(options) {
|
|
101
|
-
return new
|
|
134
|
+
return new NtripClientWithBackoff(options);
|
|
102
135
|
}
|
|
103
136
|
|
|
104
137
|
function createUploader(options) {
|
|
@@ -49,49 +49,47 @@ module.exports = function (RED) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
this.on('input', function (msg) {
|
|
52
|
-
if (msg.payload
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (msg.payload !== undefined && msg.payload !== null) {
|
|
53
|
+
// rawInput is the value the user provided (Buffer, string, ...).
|
|
54
|
+
// Keep it untouched so the error output can return it as-is.
|
|
55
|
+
let rawInput = typeof msg.payload === 'object' && msg.payload.nmeaMessage !== undefined ? msg.payload.nmeaMessage : msg.payload;
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
let rawString;
|
|
58
|
+
if (Buffer.isBuffer(rawInput)) {
|
|
59
|
+
rawString = rawInput.toString('utf8');
|
|
60
|
+
} else if (typeof rawInput === 'string') {
|
|
61
|
+
rawString = rawInput;
|
|
62
|
+
}
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
payload: {
|
|
70
|
-
error: 'Payload is neither string nor Buffer',
|
|
71
|
-
input: rawInput,
|
|
72
|
-
inputString: String(rawInput),
|
|
64
|
+
if (rawString === undefined) {
|
|
65
|
+
node.send([
|
|
66
|
+
null,
|
|
67
|
+
{
|
|
68
|
+
payload: {
|
|
69
|
+
error: 'Payload is neither string nor Buffer',
|
|
70
|
+
input: rawInput,
|
|
71
|
+
inputString: String(rawInput),
|
|
72
|
+
},
|
|
73
73
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
]);
|
|
75
|
+
node.invalidMessagesReceived++;
|
|
76
|
+
updateStatus();
|
|
77
|
+
} else {
|
|
78
|
+
// A single chunk may carry several sentences delimited by \r\n.
|
|
79
|
+
let sentences = rawString.split(/\r?\n/);
|
|
80
|
+
let any = false;
|
|
81
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
82
|
+
let s = sentences[i].trim();
|
|
83
|
+
if (s.length > 0) {
|
|
84
|
+
decodeOne(s, rawInput, rawString);
|
|
85
|
+
any = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < sentences.length; i++) {
|
|
85
|
-
let s = sentences[i].trim();
|
|
86
|
-
if (s.length === 0) {
|
|
87
|
-
continue;
|
|
89
|
+
if (any) {
|
|
90
|
+
updateStatus();
|
|
91
|
+
}
|
|
88
92
|
}
|
|
89
|
-
decodeOne(s, rawInput, rawString);
|
|
90
|
-
any = true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (any) {
|
|
94
|
-
updateStatus();
|
|
95
93
|
}
|
|
96
94
|
});
|
|
97
95
|
|
|
@@ -39,14 +39,15 @@ module.exports = function (RED) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function safeToString(value) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
let result = '';
|
|
43
|
+
if (value !== undefined && value !== null) {
|
|
44
|
+
try {
|
|
45
|
+
result = String(value);
|
|
46
|
+
} catch {
|
|
47
|
+
result = '';
|
|
48
|
+
}
|
|
49
49
|
}
|
|
50
|
+
return result;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
this.on('input', function (msg) {
|
|
@@ -63,102 +64,101 @@ module.exports = function (RED) {
|
|
|
63
64
|
node.send([null, errMsg]);
|
|
64
65
|
node.invalidMessagesReceived++;
|
|
65
66
|
updateStatus(false);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let input = payload.nmeaMessage;
|
|
67
|
+
} else {
|
|
68
|
+
let input = payload.nmeaMessage;
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
try {
|
|
71
|
+
let messageType = String(payload.messageType).toUpperCase();
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
73
|
+
let nmeaMessage;
|
|
74
|
+
if (input instanceof NmeaMessage) {
|
|
75
|
+
nmeaMessage = input;
|
|
76
|
+
} else {
|
|
77
|
+
switch (messageType) {
|
|
78
|
+
case 'OBJECT':
|
|
79
|
+
nmeaMessage = NmeaMessageUnknown.construct(input);
|
|
80
|
+
break;
|
|
81
|
+
case 'DTM':
|
|
82
|
+
nmeaMessage = NmeaMessageDtm.construct(input);
|
|
83
|
+
break;
|
|
84
|
+
case 'GBS':
|
|
85
|
+
nmeaMessage = NmeaMessageGbs.construct(input);
|
|
86
|
+
break;
|
|
87
|
+
case 'GGA':
|
|
88
|
+
nmeaMessage = NmeaMessageGga.construct(input);
|
|
89
|
+
break;
|
|
90
|
+
case 'GLL':
|
|
91
|
+
nmeaMessage = NmeaMessageGll.construct(input);
|
|
92
|
+
break;
|
|
93
|
+
case 'GNS':
|
|
94
|
+
nmeaMessage = NmeaMessageGns.construct(input);
|
|
95
|
+
break;
|
|
96
|
+
case 'GRS':
|
|
97
|
+
nmeaMessage = NmeaMessageGrs.construct(input);
|
|
98
|
+
break;
|
|
99
|
+
case 'GSA':
|
|
100
|
+
nmeaMessage = NmeaMessageGsa.construct(input);
|
|
101
|
+
break;
|
|
102
|
+
case 'GST':
|
|
103
|
+
nmeaMessage = NmeaMessageGst.construct(input);
|
|
104
|
+
break;
|
|
105
|
+
case 'GSV':
|
|
106
|
+
nmeaMessage = NmeaMessageGsv.construct(input);
|
|
107
|
+
break;
|
|
108
|
+
case 'RMC':
|
|
109
|
+
nmeaMessage = NmeaMessageRmc.construct(input);
|
|
110
|
+
break;
|
|
111
|
+
case 'THS':
|
|
112
|
+
nmeaMessage = NmeaMessageThs.construct(input);
|
|
113
|
+
break;
|
|
114
|
+
case 'TXT':
|
|
115
|
+
nmeaMessage = NmeaMessageTxt.construct(input);
|
|
116
|
+
break;
|
|
117
|
+
case 'VHW':
|
|
118
|
+
nmeaMessage = NmeaMessageVhw.construct(input);
|
|
119
|
+
break;
|
|
120
|
+
case 'VLW':
|
|
121
|
+
nmeaMessage = NmeaMessageVlw.construct(input);
|
|
122
|
+
break;
|
|
123
|
+
case 'VPW':
|
|
124
|
+
nmeaMessage = NmeaMessageVpw.construct(input);
|
|
125
|
+
break;
|
|
126
|
+
case 'VTG':
|
|
127
|
+
nmeaMessage = NmeaMessageVtg.construct(input);
|
|
128
|
+
break;
|
|
129
|
+
case 'ZDA':
|
|
130
|
+
nmeaMessage = NmeaMessageZda.construct(input);
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
throw new Error('Unsupported NMEA message type: ' + messageType);
|
|
134
|
+
}
|
|
135
135
|
}
|
|
136
|
-
}
|
|
137
136
|
|
|
138
|
-
|
|
137
|
+
let message = NmeaTransport.encode(nmeaMessage);
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
let outMsg = {
|
|
140
|
+
payload: {
|
|
141
|
+
nmeaMessage: message,
|
|
142
|
+
messageType: payload.messageType,
|
|
143
|
+
input: input,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
147
146
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
147
|
+
node.send([outMsg, null]);
|
|
148
|
+
node.nmeaMessagesReceived++;
|
|
149
|
+
updateStatus(true);
|
|
150
|
+
} catch (ex) {
|
|
151
|
+
let errMsg = {
|
|
152
|
+
payload: {
|
|
153
|
+
error: ex,
|
|
154
|
+
input: input,
|
|
155
|
+
inputString: safeToString(input),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
node.send([null, errMsg]);
|
|
159
|
+
node.invalidMessagesReceived++;
|
|
160
|
+
updateStatus(false);
|
|
161
|
+
}
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
164
|
|