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
|
@@ -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.8",
|
|
4
4
|
"description": "Node for ntrip and rtcm handling.",
|
|
5
5
|
"node-red": {
|
|
6
6
|
"version": ">=0.1.0",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
12
|
-
"node": ">=
|
|
12
|
+
"node": ">=18.0.0"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"node-red",
|
|
@@ -24,9 +24,27 @@
|
|
|
24
24
|
},
|
|
25
25
|
"author": "Karl-Heinz Wind",
|
|
26
26
|
"license": "MIT",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "mocha test/**/*.spec.js --timeout 5000",
|
|
29
|
+
"lint": "eslint .",
|
|
30
|
+
"lint:fix": "eslint . --fix",
|
|
31
|
+
"format": "prettier --write .",
|
|
32
|
+
"format:check": "prettier --check ."
|
|
33
|
+
},
|
|
27
34
|
"dependencies": {
|
|
28
35
|
"@gnss/nmea": "^0.1.2",
|
|
29
36
|
"@gnss/rtcm": "^0.1.5",
|
|
30
37
|
"ntrip-client": "^1.1.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "^10.0.1",
|
|
41
|
+
"chai": "^4.5.0",
|
|
42
|
+
"eslint": "^10.3.0",
|
|
43
|
+
"eslint-config-prettier": "^10.1.8",
|
|
44
|
+
"globals": "^17.6.0",
|
|
45
|
+
"mocha": "^11.7.5",
|
|
46
|
+
"node-red": "^4.1.10",
|
|
47
|
+
"node-red-node-test-helper": "^0.3.6",
|
|
48
|
+
"prettier": "^3.8.3"
|
|
31
49
|
}
|
|
32
50
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const helper = require('node-red-node-test-helper');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const ntripModule = require('../ntrip/99-ntrip.js');
|
|
6
|
+
|
|
7
|
+
helper.init(require.resolve('node-red'));
|
|
8
|
+
|
|
9
|
+
const GGA = '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47';
|
|
10
|
+
const GLL = '$GPGLL,4916.45,N,12311.12,W,225444,A,*1D';
|
|
11
|
+
|
|
12
|
+
function flow() {
|
|
13
|
+
return [
|
|
14
|
+
{ id: 'n1', type: 'NmeaDecoder', wires: [['ok'], ['err']] },
|
|
15
|
+
{ id: 'ok', type: 'helper' },
|
|
16
|
+
{ id: 'err', type: 'helper' },
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('NmeaDecoder', function () {
|
|
21
|
+
before(() => helper.startServer());
|
|
22
|
+
after(() => helper.stopServer());
|
|
23
|
+
afterEach(() => helper.unload());
|
|
24
|
+
|
|
25
|
+
it('decodes a valid GGA sentence on the success output', function () {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
helper.load(ntripModule, flow(), () => {
|
|
28
|
+
const n1 = helper.getNode('n1');
|
|
29
|
+
const ok = helper.getNode('ok');
|
|
30
|
+
ok.on('input', (msg) => {
|
|
31
|
+
try {
|
|
32
|
+
expect(msg.payload.messageType).to.equal('GGA');
|
|
33
|
+
expect(msg.payload.nmeaMessage).to.have.property('latitude');
|
|
34
|
+
expect(msg.payload.input).to.equal(GGA);
|
|
35
|
+
resolve();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
reject(e);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
n1.receive({ payload: GGA });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('splits a multi-sentence chunk into separate output messages', function () {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
helper.load(ntripModule, flow(), () => {
|
|
48
|
+
const n1 = helper.getNode('n1');
|
|
49
|
+
const ok = helper.getNode('ok');
|
|
50
|
+
const types = [];
|
|
51
|
+
ok.on('input', (msg) => {
|
|
52
|
+
types.push(msg.payload.messageType);
|
|
53
|
+
if (types.length === 2) {
|
|
54
|
+
try {
|
|
55
|
+
expect(types).to.deep.equal(['GGA', 'GLL']);
|
|
56
|
+
resolve();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
reject(e);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
n1.receive({ payload: GGA + '\r\n' + GLL + '\r\n' });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('accepts a Buffer payload and decodes it as UTF-8', function () {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
helper.load(ntripModule, flow(), () => {
|
|
70
|
+
const n1 = helper.getNode('n1');
|
|
71
|
+
const ok = helper.getNode('ok');
|
|
72
|
+
ok.on('input', (msg) => {
|
|
73
|
+
try {
|
|
74
|
+
expect(msg.payload.messageType).to.equal('GGA');
|
|
75
|
+
resolve();
|
|
76
|
+
} catch (e) {
|
|
77
|
+
reject(e);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
n1.receive({ payload: Buffer.from(GGA, 'utf8') });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('reads payload.nmeaMessage when payload is an object', function () {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
helper.load(ntripModule, flow(), () => {
|
|
88
|
+
const n1 = helper.getNode('n1');
|
|
89
|
+
const ok = helper.getNode('ok');
|
|
90
|
+
ok.on('input', (msg) => {
|
|
91
|
+
try {
|
|
92
|
+
expect(msg.payload.messageType).to.equal('GGA');
|
|
93
|
+
resolve();
|
|
94
|
+
} catch (e) {
|
|
95
|
+
reject(e);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
n1.receive({ payload: { nmeaMessage: GGA } });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('error output preserves Buffer input as `input`, utf8 as `inputString`', function () {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
helper.load(ntripModule, flow(), () => {
|
|
106
|
+
const n1 = helper.getNode('n1');
|
|
107
|
+
const err = helper.getNode('err');
|
|
108
|
+
const garbage = Buffer.from([0xd3, 0x00, 0x13, 0x3e, 0xd0, 0x00]);
|
|
109
|
+
err.on('input', (msg) => {
|
|
110
|
+
try {
|
|
111
|
+
expect(Buffer.isBuffer(msg.payload.input)).to.equal(true);
|
|
112
|
+
expect(msg.payload.input).to.deep.equal(garbage);
|
|
113
|
+
expect(msg.payload.inputString).to.be.a('string');
|
|
114
|
+
resolve();
|
|
115
|
+
} catch (e) {
|
|
116
|
+
reject(e);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
n1.receive({ payload: garbage });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not call node.error on decode failure (regression: log flood)', function () {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
helper.load(ntripModule, flow(), () => {
|
|
127
|
+
const n1 = helper.getNode('n1');
|
|
128
|
+
let errorCalled = false;
|
|
129
|
+
const originalError = n1.error.bind(n1);
|
|
130
|
+
n1.error = function (...args) {
|
|
131
|
+
errorCalled = true;
|
|
132
|
+
return originalError(...args);
|
|
133
|
+
};
|
|
134
|
+
n1.receive({ payload: Buffer.from([0xd3, 0x00, 0x13, 0x3e]) });
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
try {
|
|
137
|
+
expect(errorCalled).to.equal(false);
|
|
138
|
+
resolve();
|
|
139
|
+
} catch (e) {
|
|
140
|
+
reject(e);
|
|
141
|
+
}
|
|
142
|
+
}, 50);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const helper = require('node-red-node-test-helper');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const { NmeaTransport } = require('@gnss/nmea');
|
|
6
|
+
const ntripModule = require('../ntrip/99-ntrip.js');
|
|
7
|
+
|
|
8
|
+
helper.init(require.resolve('node-red'));
|
|
9
|
+
|
|
10
|
+
const GGA = '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47';
|
|
11
|
+
|
|
12
|
+
function flow() {
|
|
13
|
+
return [
|
|
14
|
+
{ id: 'n1', type: 'NmeaEncoder', wires: [['ok'], ['err']] },
|
|
15
|
+
{ id: 'ok', type: 'helper' },
|
|
16
|
+
{ id: 'err', type: 'helper' },
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('NmeaEncoder', function () {
|
|
21
|
+
before(() => helper.startServer());
|
|
22
|
+
after(() => helper.stopServer());
|
|
23
|
+
afterEach(() => helper.unload());
|
|
24
|
+
|
|
25
|
+
it('encodes a pre-constructed NmeaMessage instance (instanceof passthrough)', function () {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
helper.load(ntripModule, flow(), () => {
|
|
28
|
+
const n1 = helper.getNode('n1');
|
|
29
|
+
const ok = helper.getNode('ok');
|
|
30
|
+
const instance = NmeaTransport.decode(GGA);
|
|
31
|
+
ok.on('input', (msg) => {
|
|
32
|
+
try {
|
|
33
|
+
expect(msg.payload.nmeaMessage).to.be.a('string');
|
|
34
|
+
expect(msg.payload.nmeaMessage).to.match(/^\$.+\*[0-9A-Fa-f]{2}\r?\n?$/);
|
|
35
|
+
expect(msg.payload.nmeaMessage).to.include('GGA');
|
|
36
|
+
resolve();
|
|
37
|
+
} catch (e) {
|
|
38
|
+
reject(e);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// Even with a bogus messageType, an instance must take precedence and
|
|
42
|
+
// flow straight through to NmeaTransport.encode without re-construction.
|
|
43
|
+
n1.receive({ payload: { messageType: 'NONSENSE', nmeaMessage: instance } });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('routes unknown messageType to the error output (no silent success)', 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 err = helper.getNode('err');
|
|
54
|
+
ok.on('input', () => reject(new Error('unexpected success output')));
|
|
55
|
+
err.on('input', (msg) => {
|
|
56
|
+
try {
|
|
57
|
+
expect(msg.payload.error).to.exist;
|
|
58
|
+
expect(String(msg.payload.error).toLowerCase()).to.include('unsupported');
|
|
59
|
+
resolve();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
reject(e);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
n1.receive({ payload: { messageType: 'XYZ', nmeaMessage: { foo: 1 } } });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('routes missing nmeaMessage to the error output without crashing', function () {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
helper.load(ntripModule, flow(), () => {
|
|
72
|
+
const n1 = helper.getNode('n1');
|
|
73
|
+
const err = helper.getNode('err');
|
|
74
|
+
err.on('input', (msg) => {
|
|
75
|
+
try {
|
|
76
|
+
expect(msg.payload.error).to.exist;
|
|
77
|
+
resolve();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
reject(e);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
n1.receive({ payload: { messageType: 'GGA' } });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('routes empty payload to the error output without crashing', function () {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
helper.load(ntripModule, flow(), () => {
|
|
90
|
+
const n1 = helper.getNode('n1');
|
|
91
|
+
const err = helper.getNode('err');
|
|
92
|
+
err.on('input', (msg) => {
|
|
93
|
+
try {
|
|
94
|
+
expect(msg.payload.error).to.exist;
|
|
95
|
+
resolve();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
reject(e);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
n1.receive({ payload: {} });
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const helper = require('node-red-node-test-helper');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const ntripModule = require('../ntrip/99-ntrip.js');
|
|
6
|
+
|
|
7
|
+
helper.init(require.resolve('node-red'));
|
|
8
|
+
|
|
9
|
+
// A real RTCM 3 message type 1005 (Stationary RTK Reference Station ARP).
|
|
10
|
+
// 25 bytes total: 3-byte header + 19-byte payload + 3-byte CRC.
|
|
11
|
+
const RTCM_1005 = Buffer.from('D300133ED7D30202980EDEEF34B4BD62AC0941986F33360B98', 'hex');
|
|
12
|
+
|
|
13
|
+
function flow() {
|
|
14
|
+
return [
|
|
15
|
+
{ id: 'n1', type: 'RtcmDecoder', wires: [['ok'], ['err']] },
|
|
16
|
+
{ id: 'ok', type: 'helper' },
|
|
17
|
+
{ id: 'err', type: 'helper' },
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('RtcmDecoder', function () {
|
|
22
|
+
before(() => helper.startServer());
|
|
23
|
+
after(() => helper.stopServer());
|
|
24
|
+
afterEach(() => helper.unload());
|
|
25
|
+
|
|
26
|
+
it('decodes a valid RTCM 1005 frame on the success output', 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
|
+
ok.on('input', (msg) => {
|
|
32
|
+
try {
|
|
33
|
+
expect(msg.payload.rtcm).to.equal(1005);
|
|
34
|
+
expect(msg.payload.messageType).to.be.a('string');
|
|
35
|
+
expect(msg.payload.message).to.be.an('object');
|
|
36
|
+
expect(Buffer.isBuffer(msg.payload.input)).to.equal(true);
|
|
37
|
+
resolve();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
reject(e);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
n1.receive({ payload: RTCM_1005 });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('decodes two concatenated frames as two separate output messages', function () {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
helper.load(ntripModule, flow(), () => {
|
|
50
|
+
const n1 = helper.getNode('n1');
|
|
51
|
+
const ok = helper.getNode('ok');
|
|
52
|
+
let count = 0;
|
|
53
|
+
ok.on('input', () => {
|
|
54
|
+
count++;
|
|
55
|
+
if (count === 2) resolve();
|
|
56
|
+
});
|
|
57
|
+
n1.receive({ payload: Buffer.concat([RTCM_1005, RTCM_1005]) });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reassembles a frame split across two input events (carry-over buffer)', function () {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
helper.load(ntripModule, flow(), () => {
|
|
65
|
+
const n1 = helper.getNode('n1');
|
|
66
|
+
const ok = helper.getNode('ok');
|
|
67
|
+
let received = false;
|
|
68
|
+
ok.on('input', (msg) => {
|
|
69
|
+
if (received) return;
|
|
70
|
+
received = true;
|
|
71
|
+
try {
|
|
72
|
+
expect(msg.payload.rtcm).to.equal(1005);
|
|
73
|
+
resolve();
|
|
74
|
+
} catch (e) {
|
|
75
|
+
reject(e);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const half = Math.floor(RTCM_1005.length / 2);
|
|
79
|
+
n1.receive({ payload: RTCM_1005.slice(0, half) });
|
|
80
|
+
setImmediate(() => n1.receive({ payload: RTCM_1005.slice(half) }));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not call node.error on decode failure (regression: log flood)', function () {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
helper.load(ntripModule, flow(), () => {
|
|
88
|
+
const n1 = helper.getNode('n1');
|
|
89
|
+
const err = helper.getNode('err');
|
|
90
|
+
let errorCalled = false;
|
|
91
|
+
const originalError = n1.error.bind(n1);
|
|
92
|
+
n1.error = function (...args) {
|
|
93
|
+
errorCalled = true;
|
|
94
|
+
return originalError(...args);
|
|
95
|
+
};
|
|
96
|
+
// A buffer of garbage larger than the pending-buffer cap triggers the
|
|
97
|
+
// error-output path. node.error must NOT be called for that.
|
|
98
|
+
const big = Buffer.alloc(70000, 0xff);
|
|
99
|
+
let errEmitted = false;
|
|
100
|
+
err.on('input', () => {
|
|
101
|
+
errEmitted = true;
|
|
102
|
+
});
|
|
103
|
+
n1.receive({ payload: big });
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
try {
|
|
106
|
+
expect(errEmitted).to.equal(true);
|
|
107
|
+
expect(errorCalled).to.equal(false);
|
|
108
|
+
resolve();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
reject(e);
|
|
111
|
+
}
|
|
112
|
+
}, 200);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
-
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
-
|
|
4
|
-
name: Node.js Package
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
release:
|
|
8
|
-
types: [created]
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
build:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
- uses: actions/setup-node@v4
|
|
16
|
-
with:
|
|
17
|
-
node-version: 20
|
|
18
|
-
- run: npm ci
|
|
19
|
-
# - run: npm test
|
|
20
|
-
|
|
21
|
-
publish-npm:
|
|
22
|
-
needs: build
|
|
23
|
-
runs-on: ubuntu-latest
|
|
24
|
-
steps:
|
|
25
|
-
- uses: actions/checkout@v4
|
|
26
|
-
- uses: actions/setup-node@v4
|
|
27
|
-
with:
|
|
28
|
-
node-version: 20
|
|
29
|
-
registry-url: https://registry.npmjs.org/
|
|
30
|
-
- run: npm ci
|
|
31
|
-
- run: npm publish
|
|
32
|
-
env:
|
|
33
|
-
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|