node-red-contrib-ntrip 0.2.3 → 0.2.7
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 +61 -0
- package/CLAUDE.md +73 -0
- package/README.md +145 -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/ntrip/99-ntrip.html +207 -31
- package/ntrip/99-ntrip.js +12 -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/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/.github/workflows/npm-publish.yml +0 -33
|
@@ -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
|
+
};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const {
|
|
5
|
-
NmeaTransport,
|
|
4
|
+
const {
|
|
5
|
+
NmeaTransport,
|
|
6
6
|
NmeaMessageUnknown,
|
|
7
7
|
NmeaMessage,
|
|
8
8
|
NmeaMessageDtm,
|
|
9
9
|
NmeaMessageGbs,
|
|
10
10
|
NmeaMessageGga,
|
|
11
11
|
NmeaMessageGll,
|
|
12
|
-
NmeaMessageGns,
|
|
13
|
-
NmeaMessageGrs,
|
|
12
|
+
NmeaMessageGns,
|
|
13
|
+
NmeaMessageGrs,
|
|
14
14
|
NmeaMessageGsa,
|
|
15
15
|
NmeaMessageGst,
|
|
16
16
|
NmeaMessageGsv,
|
|
@@ -30,24 +30,53 @@ module.exports = function (RED) {
|
|
|
30
30
|
node.nmeaMessagesReceived = 0;
|
|
31
31
|
node.invalidMessagesReceived = 0;
|
|
32
32
|
|
|
33
|
+
function updateStatus(ok) {
|
|
34
|
+
node.status({
|
|
35
|
+
fill: ok ? 'green' : 'red',
|
|
36
|
+
shape: 'ring',
|
|
37
|
+
text: 'NMEA: ' + node.nmeaMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeToString(value) {
|
|
42
|
+
if (value === undefined || value === null) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return String(value);
|
|
47
|
+
} catch {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
this.on('input', function (msg) {
|
|
53
|
+
let payload = msg.payload;
|
|
54
|
+
|
|
55
|
+
if (payload == null || payload.nmeaMessage == null || payload.messageType == null) {
|
|
56
|
+
let errMsg = {
|
|
57
|
+
payload: {
|
|
58
|
+
error: 'Invalid input. Expected payload with nmeaMessage and messageType properties.',
|
|
59
|
+
input: payload,
|
|
60
|
+
inputString: safeToString(payload),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
node.send([null, errMsg]);
|
|
64
|
+
node.invalidMessagesReceived++;
|
|
65
|
+
updateStatus(false);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let input = payload.nmeaMessage;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
let messageType = String(payload.messageType).toUpperCase();
|
|
34
73
|
|
|
35
|
-
let input = msg.payload.nmeaMessage;
|
|
36
|
-
let isNmeaMessage = input.prototype instanceof NmeaMessage;
|
|
37
|
-
let messageType = msg.payload.messageType;
|
|
38
|
-
messageType = messageType.toUpperCase();
|
|
39
|
-
|
|
40
|
-
try
|
|
41
|
-
{
|
|
42
|
-
let message;
|
|
43
74
|
let nmeaMessage;
|
|
44
|
-
if(
|
|
45
|
-
nmeaMessage = input;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
|
|
75
|
+
if (input instanceof NmeaMessage) {
|
|
76
|
+
nmeaMessage = input;
|
|
77
|
+
} else {
|
|
49
78
|
switch (messageType) {
|
|
50
|
-
case '
|
|
79
|
+
case 'OBJECT':
|
|
51
80
|
nmeaMessage = NmeaMessageUnknown.construct(input);
|
|
52
81
|
break;
|
|
53
82
|
case 'DTM':
|
|
@@ -102,50 +131,42 @@ module.exports = function (RED) {
|
|
|
102
131
|
nmeaMessage = NmeaMessageZda.construct(input);
|
|
103
132
|
break;
|
|
104
133
|
default:
|
|
105
|
-
|
|
106
|
-
break;
|
|
134
|
+
throw new Error('Unsupported NMEA message type: ' + messageType);
|
|
107
135
|
}
|
|
108
136
|
}
|
|
109
137
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
input : input
|
|
138
|
+
let message = NmeaTransport.encode(nmeaMessage);
|
|
139
|
+
|
|
140
|
+
let outMsg = {
|
|
141
|
+
payload: {
|
|
142
|
+
nmeaMessage: message,
|
|
143
|
+
messageType: payload.messageType,
|
|
144
|
+
input: input,
|
|
118
145
|
},
|
|
119
146
|
};
|
|
120
147
|
|
|
121
|
-
node.send([
|
|
148
|
+
node.send([outMsg, null]);
|
|
122
149
|
node.nmeaMessagesReceived++;
|
|
123
|
-
|
|
124
|
-
catch(ex)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
150
|
+
updateStatus(true);
|
|
151
|
+
} catch (ex) {
|
|
152
|
+
let errMsg = {
|
|
153
|
+
payload: {
|
|
154
|
+
error: ex,
|
|
155
|
+
input: input,
|
|
156
|
+
inputString: safeToString(input),
|
|
157
|
+
},
|
|
132
158
|
};
|
|
133
|
-
node.send([null,
|
|
159
|
+
node.send([null, errMsg]);
|
|
134
160
|
node.invalidMessagesReceived++;
|
|
161
|
+
updateStatus(false);
|
|
135
162
|
}
|
|
136
|
-
|
|
137
|
-
node.status({
|
|
138
|
-
fill: 'green',
|
|
139
|
-
shape: 'ring',
|
|
140
|
-
text: 'NMEA: ' + node.nmeaMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
|
|
141
|
-
});
|
|
142
163
|
});
|
|
143
164
|
|
|
144
|
-
this.on('close', function(done) {
|
|
165
|
+
this.on('close', function (done) {
|
|
145
166
|
node.status({});
|
|
146
167
|
done();
|
|
147
168
|
});
|
|
148
169
|
}
|
|
149
170
|
|
|
150
171
|
return NmeaEncoderNode;
|
|
151
|
-
};
|
|
172
|
+
};
|