node-red-contrib-ntrip 0.2.10 → 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 +12 -0
- package/README.md +5 -3
- package/examples/watchdog.json +1 -1
- package/ntrip/lib/ntrip-client.js +70 -72
- 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
|
@@ -1,104 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
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
|
-
});
|