Skip to content

Commit e11b6ee

Browse files
authored
Merge pull request #89 from boschrexroth/bugfix/rawdata
fix: support for raw data
2 parents a710685 + 48f1c79 commit e11b6ee

File tree

9 files changed

+88
-43
lines changed

9 files changed

+88
-43
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ Any use of the source code and related documents of this repository in applicati
121121
- fix: add more tests to handle node address with invalid symbols
122122
* 2024-12-06: 1.9.7 - fix: update dependencies in package-lock
123123
- fix: further improve resilience on invalid data from server
124+
* 2025-01-27: 1.9.8 - fix: no support for raw binary data (types/datalayer/raw). E.g. for some Ethercat nodes, returned now as a buffer for further processing.
124125
```
125-
126126
## About
127127

128128
Copyright © 2020-2024 Bosch Rexroth AG. All rights reserved.

lib/CtrlxDatalayerSubscription.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ class CtrlxDatalayerSubscription extends EventEmitter {
176176
// All subscription settings are transmitted as payload
177177
const settings = {
178178
'properties': properties,
179-
'nodes': this._nodes,
179+
'nodes': this._nodes,
180180
};
181181

182182
let options = {
@@ -189,7 +189,7 @@ class CtrlxDatalayerSubscription extends EventEmitter {
189189
'Accept': 'application/json',
190190
'Authorization': this._authorization,
191191
'Connection': 'keep-alive',
192-
},
192+
},
193193
rejectUnauthorized: false // accept self-signed certificates
194194
};
195195

@@ -268,9 +268,9 @@ class CtrlxDatalayerSubscription extends EventEmitter {
268268
*
269269
* @memberof CtrlxCore
270270
*/
271-
get isEndByServer() {
272-
return this._isEndByServer;
273-
}
271+
get isEndByServer() {
272+
return this._isEndByServer;
273+
}
274274

275275
/**
276276
* Opens an event stream and starts the subscription.
@@ -320,8 +320,8 @@ class CtrlxDatalayerSubscription extends EventEmitter {
320320
}
321321
},
322322
agent: new https.Agent({ keepAlive: false }) // create a dedicated agent to have dedicated connection instance. Also disable the agent-keep-alive explicitly.
323-
// This is necessary because since node.js 19 the default behaviour was changed.
324-
// https://nodejs.org/en/blog/announcements/v19-release-announce#https11-keepalive-by-default
323+
// This is necessary because since node.js 19 the default behaviour was changed.
324+
// https://nodejs.org/en/blog/announcements/v19-release-announce#https11-keepalive-by-default
325325
};
326326

327327
if (this._keepaliveIntervalMs) {
@@ -432,14 +432,13 @@ class CtrlxDatalayerSubscription extends EventEmitter {
432432
let payload = CtrlxDatalayer._parseData(e.data);
433433
if (!this.emit('update', payload, e.lastEventId)) {
434434
// Listener seems not yet to be attached. Retry on next tick.
435-
setTimeout(()=>this.emit('update', payload, e.lastEventId), 0);
435+
setTimeout(() => this.emit('update', payload, e.lastEventId), 0);
436436
}
437-
} catch (err) {
437+
} catch(err) {
438438
if (this.listeners('error').length > 0) {
439439
this.emit('error', new Error(`Error parsing update event: ${err.message}`));
440440
}
441441
}
442-
443442
});
444443

445444
this._es.addEventListener('keepalive', (e) => {
@@ -451,9 +450,9 @@ class CtrlxDatalayerSubscription extends EventEmitter {
451450
let payload = CtrlxDatalayer._parseData(e.data);
452451
if (!this.emit('keepalive', payload, e.lastEventId)) {
453452
// Listener seems not yet to be attached. Retry on next tick.
454-
setTimeout(()=>this.emit('keepalive', payload, e.lastEventId), 0);
453+
setTimeout(() => this.emit('keepalive', payload, e.lastEventId), 0);
455454
}
456-
} catch (err) {
455+
} catch(err) {
457456
if (this.listeners('error').length > 0) {
458457
this.emit('error', new Error(`Error parsing keepalive event: ${err.message}`));
459458
}

lib/CtrlxDatalayerV2.js

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,20 @@ class CtrlxDatalayer {
6060
* Convert a Data Layer response body from string to a javascript object.
6161
*
6262
* This is basicly an extension to JSON.parse(), that supports also 64bit data types, which
63-
* will be converted to BigInt. Which is not supported by the standard JSON.parse().
64-
* With standard JSON.parse() a 64bit integer value might get rounded, because javascripts Number
63+
* will be converted to BigInt, which is not supported by the standard JSON.parse().
64+
* With standard JSON.parse() a 64bit integer value might get rounded, because JavaScripts Number
6565
* is based on a double precision floating point number.
66-
* E.g. values greater than Number.MAX_SAFE_INTEGER.
66+
* e.g. values greater than Number.MAX_SAFE_INTEGER.
6767
*
6868
* @static
69-
* @param {string} data - The content data as returend by a Data Layer request.
69+
* @param {string} data - The content data as returned by a Data Layer request.
7070
* @returns {object} - The parsed javascript object.
7171
* @memberof CtrlxDatalayer
7272
* @throws {SyntaxError} On invalid JSON objects.
7373
*/
7474
static _parseData(data) {
75+
76+
// We expect JSON.parse to fail here for invalid JSON
7577
let payload = JSON.parse(data);
7678

7779
if (payload.type === "int64" || payload.type === "uint64") {
@@ -81,7 +83,6 @@ class CtrlxDatalayer {
8183
let strBigArray = data.match(new RegExp(/(?:"value":)(?:.*\[)(.*?)(?:\])/))[1];
8284
payload.value = Array.from(strBigArray.split(","), (s) => BigInt(s))
8385
}
84-
8586
return payload;
8687
}
8788

@@ -301,27 +302,36 @@ class CtrlxDatalayer {
301302
}
302303

303304
const req = https.request(options, (res) => {
304-
let data = "";
305305

306-
res.setEncoding('utf8');
306+
// We have to read binary to support types/datalayer/raw
307+
let chunks = [];
307308
res.on('data', function(chunk) {
308-
data += chunk;
309+
chunks.push(Buffer.from(chunk));
309310
});
310311

311312
res.on('end', function() {
313+
const buffer = Buffer.concat(chunks);
312314

313315
// We expect 200 on success
314316
if (res.statusCode !== 200) {
315-
callback(CtrlxProblemError.fromHttpResponse(res, data));
317+
callback(CtrlxProblemError.fromHttpResponse(res, buffer.toString('utf8')));
316318
return;
317319
}
318320

319-
// Try to parse the data
321+
// If we don't receive JSON, we return the 'raw' binary buffer as-it-is.
320322
let payload;
321-
try {
322-
payload = CtrlxDatalayer._parseData(data);
323-
} catch (err) {
324-
callback(err, null);
323+
if (res.headers['content-type'].includes('application/json')){
324+
try {
325+
payload = CtrlxDatalayer._parseData(buffer.toString('utf8'));
326+
} catch (err) {
327+
callback(err, null);
328+
return;
329+
}
330+
} else {
331+
payload = {
332+
type: 'raw',
333+
value: new Uint8Array(buffer),
334+
}
325335
}
326336

327337
// No error, return payload data.
@@ -438,4 +448,3 @@ class CtrlxDatalayer {
438448
}
439449

440450
module.exports = CtrlxDatalayer;
441-

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-red-contrib-ctrlx-automation",
3-
"version": "1.9.7",
3+
"version": "1.9.8",
44
"description": "Node-RED nodes for ctrlX AUTOMATION",
55
"repository": {
66
"type": "git",
@@ -62,4 +62,4 @@
6262
"test_with_coverage": "nyc mocha --timeout 60000",
6363
"benchmark": "node ./test/helper/benchmark"
6464
}
65-
}
65+
}

test/CtrlxCore.nodes.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,21 @@ describe('CtrlxCoreDataLayerNodes', function() {
312312

313313
});
314314

315+
it('should return a raw buffer', function(done) {
316+
317+
let ctrlx = new CtrlxCore(getHostname(), getUsername(), getPassword());
318+
319+
ctrlx.logIn()
320+
.then(() => { return ctrlx.datalayerRead('encoding/raw/buffer'); })
321+
.then((data) => {
322+
assert.deepEqual(data.value, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]));
323+
done();
324+
})
325+
.catch((err) => done(err))
326+
.finally(() => ctrlx.logOut());
327+
328+
});
329+
315330
});
316331

317332

test/CtrlxCore.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ describe('CtrlxCore', function() {
110110
done();
111111
});
112112

113+
it('should throw exception on invalid JSON', function(done) {
114+
115+
const invalidJSON = 'test/invalid/json';
116+
expect(() => CtrlxDatalayer._parseData(invalidJSON)).to.throw(SyntaxError);
117+
118+
done();
119+
});
120+
113121
it('should parse BigInt', function(done) {
114122

115123
expect(CtrlxDatalayer._parseData(`{"type": "int64", "value": 9223372036854775807}`).value).to.equal(BigInt(9223372036854775807n))

test/ctrlx-datalayer-subscribe.test.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,8 @@ describe('ctrlx-datalayer-subscribe', function () {
272272
let flow = [
273273
{ "id": "h1", "type": "helper" },
274274
{ "id": "n1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": "test/options", "name": "subscribe", "wires": [["h1"]] },
275-
{ "id": "s1", "type": "ctrlx-config-subscription", "device": "c1",
275+
{
276+
"id": "s1", "type": "ctrlx-config-subscription", "device": "c1",
276277
"name": "sub1",
277278
"publishIntervalMs": "100",
278279
"publishIntervalUnits": "milliseconds",
@@ -468,11 +469,11 @@ describe('ctrlx-datalayer-subscribe', function () {
468469
let path = 'with/strange/symbols/abc=1;nichts-ist.wahr:("alles[ist]erlaubt")42/x.y.z';
469470

470471
let flow = [
471-
{ "id": "f1", "type": "tab", "label": "Test flow"},
472-
{ "id": "h1", "z":"f1", "type": "helper" },
473-
{ "id": "n1", "z":"f1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": path, "name": "subscribe", "wires": [["h1"]] },
474-
{ "id": "s1", "z":"f1", "type": "ctrlx-config-subscription", "device": "c1", "name": "sub1", "publishIntervalMs": "1000" },
475-
{ "id": "c1", "z":"f1", "type": "ctrlx-config", "name": "ctrlx", "hostname": getHostname(), "debug": true },
472+
{ "id": "f1", "type": "tab", "label": "Test flow" },
473+
{ "id": "h1", "z": "f1", "type": "helper" },
474+
{ "id": "n1", "z": "f1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": path, "name": "subscribe", "wires": [["h1"]] },
475+
{ "id": "s1", "z": "f1", "type": "ctrlx-config-subscription", "device": "c1", "name": "sub1", "publishIntervalMs": "1000" },
476+
{ "id": "c1", "z": "f1", "type": "ctrlx-config", "name": "ctrlx", "hostname": getHostname(), "debug": true },
476477
];
477478
let credentials = {
478479
c1: {
@@ -517,11 +518,11 @@ describe('ctrlx-datalayer-subscribe', function () {
517518
it('should handle invalid send json messages', function (done) {
518519

519520
let flow = [
520-
{ "id": "f1", "type": "tab", "label": "Test flow"},
521-
{ "id": "h1", "z":"f1", "type": "helper" },
522-
{ "id": "n1", "z":"f1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": "test/invalid/json", "name": "subscribe", "wires": [["h1"]] },
523-
{ "id": "s1", "z":"f1", "type": "ctrlx-config-subscription", "device": "c1", "name": "sub1", "publishIntervalMs": "1000" },
524-
{ "id": "c1", "z":"f1", "type": "ctrlx-config", "name": "ctrlx", "hostname": getHostname(), "debug": true },
521+
{ "id": "f1", "type": "tab", "label": "Test flow" },
522+
{ "id": "h1", "z": "f1", "type": "helper" },
523+
{ "id": "n1", "z": "f1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": "test/invalid/json", "name": "subscribe", "wires": [["h1"]] },
524+
{ "id": "s1", "z": "f1", "type": "ctrlx-config-subscription", "device": "c1", "name": "sub1", "publishIntervalMs": "1000" },
525+
{ "id": "c1", "z": "f1", "type": "ctrlx-config", "name": "ctrlx", "hostname": getHostname(), "debug": true },
525526
];
526527
let credentials = {
527528
c1: {

test/helper/CtrlxMockupV2.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class CtrlxMockupV2 {
121121
type: 'bool'
122122
});
123123
});
124+
124125
this.app.get('/automation/api/v2/nodes/framework/metrics/system/cpu-utilisation-percent', authenticateJWT, (req, res) => {
125126
switch (req.query.type) {
126127
case undefined:
@@ -166,6 +167,7 @@ class CtrlxMockupV2 {
166167
break;
167168
}
168169
});
170+
169171
this.app.get('/automation/api/v2/nodes/framework/metrics/system', authenticateJWT, (req, res) => {
170172
if (req.query.type === 'browse') {
171173
res.statusCode = 200;
@@ -193,6 +195,7 @@ class CtrlxMockupV2 {
193195
type: 'int16'
194196
});
195197
});
198+
196199
this.app.get('/automation/api/v2/nodes/plc/app/Application/sym/PLC_PRG/i', authenticateJWT, (req, res) => {
197200
res.statusCode = 200;
198201
res.json({
@@ -210,10 +213,13 @@ class CtrlxMockupV2 {
210213
}
211214
this.var_i64 = req.body.value;
212215
res.statusCode = 200;
216+
res.setHeader('content-type', 'application/json');
213217
res.send(`{"type": "int64", "value":${this.var_i64.toString()}}`);
214218
});
219+
215220
this.app.get('/automation/api/v2/nodes/plc/app/Application/sym/PLC_PRG/i64', authenticateJWT, (req, res) => {
216221
res.statusCode = 200;
222+
res.setHeader('content-type', 'application/json');
217223
res.send(`{"type": "int64", "value":${this.var_i64.toString()}}`);
218224
});
219225

@@ -231,6 +237,7 @@ class CtrlxMockupV2 {
231237
type: 'string'
232238
});
233239
});
240+
234241
this.app.get('/automation/api/v2/nodes/plc/app/Application/sym/PLC_PRG/str', authenticateJWT, (req, res) => {
235242
res.statusCode = 200;
236243
res.json({
@@ -282,6 +289,12 @@ class CtrlxMockupV2 {
282289
});
283290
});
284291

292+
this.app.get('/automation/api/v2/nodes/encoding/raw/buffer', authenticateJWT, (req, res) => {
293+
res.statusCode = 200;
294+
res.setHeader('content-type', 'application/octet-stream');
295+
res.send(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]));
296+
});
297+
285298

286299
//
287300
// Builtin Data Mockups - Create/Delete

0 commit comments

Comments
 (0)