Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support for raw data #89

Merged
merged 22 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ Any use of the source code and related documents of this repository in applicati
- fix: add more tests to handle node address with invalid symbols
* 2024-12-06: 1.9.7 - fix: update dependencies in package-lock
- fix: further improve resilience on invalid data from server
* 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.
```

## About

Copyright © 2020-2024 Bosch Rexroth AG. All rights reserved.
Expand Down
23 changes: 11 additions & 12 deletions lib/CtrlxDatalayerSubscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class CtrlxDatalayerSubscription extends EventEmitter {
// All subscription settings are transmitted as payload
const settings = {
'properties': properties,
'nodes': this._nodes,
'nodes': this._nodes,
};

let options = {
Expand All @@ -189,7 +189,7 @@ class CtrlxDatalayerSubscription extends EventEmitter {
'Accept': 'application/json',
'Authorization': this._authorization,
'Connection': 'keep-alive',
},
},
rejectUnauthorized: false // accept self-signed certificates
};

Expand Down Expand Up @@ -268,9 +268,9 @@ class CtrlxDatalayerSubscription extends EventEmitter {
*
* @memberof CtrlxCore
*/
get isEndByServer() {
return this._isEndByServer;
}
get isEndByServer() {
return this._isEndByServer;
}

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

if (this._keepaliveIntervalMs) {
Expand Down Expand Up @@ -432,14 +432,13 @@ class CtrlxDatalayerSubscription extends EventEmitter {
let payload = CtrlxDatalayer._parseData(e.data);
if (!this.emit('update', payload, e.lastEventId)) {
// Listener seems not yet to be attached. Retry on next tick.
setTimeout(()=>this.emit('update', payload, e.lastEventId), 0);
setTimeout(() => this.emit('update', payload, e.lastEventId), 0);
}
} catch (err) {
} catch(err) {
if (this.listeners('error').length > 0) {
this.emit('error', new Error(`Error parsing update event: ${err.message}`));
}
}

});

this._es.addEventListener('keepalive', (e) => {
Expand All @@ -451,9 +450,9 @@ class CtrlxDatalayerSubscription extends EventEmitter {
let payload = CtrlxDatalayer._parseData(e.data);
if (!this.emit('keepalive', payload, e.lastEventId)) {
// Listener seems not yet to be attached. Retry on next tick.
setTimeout(()=>this.emit('keepalive', payload, e.lastEventId), 0);
setTimeout(() => this.emit('keepalive', payload, e.lastEventId), 0);
}
} catch (err) {
} catch(err) {
if (this.listeners('error').length > 0) {
this.emit('error', new Error(`Error parsing keepalive event: ${err.message}`));
}
Expand Down
39 changes: 24 additions & 15 deletions lib/CtrlxDatalayerV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,20 @@ class CtrlxDatalayer {
* Convert a Data Layer response body from string to a javascript object.
*
* This is basicly an extension to JSON.parse(), that supports also 64bit data types, which
* will be converted to BigInt. Which is not supported by the standard JSON.parse().
* With standard JSON.parse() a 64bit integer value might get rounded, because javascripts Number
* will be converted to BigInt, which is not supported by the standard JSON.parse().
* With standard JSON.parse() a 64bit integer value might get rounded, because JavaScripts Number
* is based on a double precision floating point number.
* E.g. values greater than Number.MAX_SAFE_INTEGER.
* e.g. values greater than Number.MAX_SAFE_INTEGER.
*
* @static
* @param {string} data - The content data as returend by a Data Layer request.
* @param {string} data - The content data as returned by a Data Layer request.
* @returns {object} - The parsed javascript object.
* @memberof CtrlxDatalayer
* @throws {SyntaxError} On invalid JSON objects.
*/
static _parseData(data) {

// We expect JSON.parse to fail here for invalid JSON
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// It may happen, that JSON.parse may fail here for invalid JSON. But we catch this in the calling function.

let payload = JSON.parse(data);

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

return payload;
}

Expand Down Expand Up @@ -301,27 +302,36 @@ class CtrlxDatalayer {
}

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

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

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

// We expect 200 on success
if (res.statusCode !== 200) {
callback(CtrlxProblemError.fromHttpResponse(res, data));
callback(CtrlxProblemError.fromHttpResponse(res, buffer.toString('utf8')));
return;
}

// Try to parse the data
// If we don't receive JSON, we return the 'raw' binary buffer as-it-is.
let payload;
try {
payload = CtrlxDatalayer._parseData(data);
} catch (err) {
callback(err, null);
if (res.headers['content-type'].includes('application/json')){
try {
payload = CtrlxDatalayer._parseData(buffer.toString('utf8'));
} catch (err) {
callback(err, null);
return;
}
} else {
payload = {
type: 'raw',
value: new Uint8Array(buffer),
}
}

// No error, return payload data.
Expand Down Expand Up @@ -438,4 +448,3 @@ class CtrlxDatalayer {
}

module.exports = CtrlxDatalayer;

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-red-contrib-ctrlx-automation",
"version": "1.9.7",
"version": "1.9.8",
"description": "Node-RED nodes for ctrlX AUTOMATION",
"repository": {
"type": "git",
Expand Down Expand Up @@ -62,4 +62,4 @@
"test_with_coverage": "nyc mocha --timeout 60000",
"benchmark": "node ./test/helper/benchmark"
}
}
}
15 changes: 15 additions & 0 deletions test/CtrlxCore.nodes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,21 @@ describe('CtrlxCoreDataLayerNodes', function() {

});

it('should return a raw buffer', function(done) {

let ctrlx = new CtrlxCore(getHostname(), getUsername(), getPassword());

ctrlx.logIn()
.then(() => { return ctrlx.datalayerRead('encoding/raw/buffer'); })
.then((data) => {
assert.deepEqual(data.value, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]));
done();
})
.catch((err) => done(err))
.finally(() => ctrlx.logOut());

});

});


Expand Down
8 changes: 8 additions & 0 deletions test/CtrlxCore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ describe('CtrlxCore', function() {
done();
});

it('should throw exception on invalid JSON', function(done) {

const invalidJSON = 'test/invalid/json';
expect(() => CtrlxDatalayer._parseData(invalidJSON)).to.throw(SyntaxError);

done();
});

it('should parse BigInt', function(done) {

expect(CtrlxDatalayer._parseData(`{"type": "int64", "value": 9223372036854775807}`).value).to.equal(BigInt(9223372036854775807n))
Expand Down
23 changes: 12 additions & 11 deletions test/ctrlx-datalayer-subscribe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ describe('ctrlx-datalayer-subscribe', function () {
let flow = [
{ "id": "h1", "type": "helper" },
{ "id": "n1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": "test/options", "name": "subscribe", "wires": [["h1"]] },
{ "id": "s1", "type": "ctrlx-config-subscription", "device": "c1",
{
"id": "s1", "type": "ctrlx-config-subscription", "device": "c1",
"name": "sub1",
"publishIntervalMs": "100",
"publishIntervalUnits": "milliseconds",
Expand Down Expand Up @@ -468,11 +469,11 @@ describe('ctrlx-datalayer-subscribe', function () {
let path = 'with/strange/symbols/abc=1;nichts-ist.wahr:("alles[ist]erlaubt")42/x.y.z';

let flow = [
{ "id": "f1", "type": "tab", "label": "Test flow"},
{ "id": "h1", "z":"f1", "type": "helper" },
{ "id": "n1", "z":"f1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": path, "name": "subscribe", "wires": [["h1"]] },
{ "id": "s1", "z":"f1", "type": "ctrlx-config-subscription", "device": "c1", "name": "sub1", "publishIntervalMs": "1000" },
{ "id": "c1", "z":"f1", "type": "ctrlx-config", "name": "ctrlx", "hostname": getHostname(), "debug": true },
{ "id": "f1", "type": "tab", "label": "Test flow" },
{ "id": "h1", "z": "f1", "type": "helper" },
{ "id": "n1", "z": "f1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": path, "name": "subscribe", "wires": [["h1"]] },
{ "id": "s1", "z": "f1", "type": "ctrlx-config-subscription", "device": "c1", "name": "sub1", "publishIntervalMs": "1000" },
{ "id": "c1", "z": "f1", "type": "ctrlx-config", "name": "ctrlx", "hostname": getHostname(), "debug": true },
];
let credentials = {
c1: {
Expand Down Expand Up @@ -517,11 +518,11 @@ describe('ctrlx-datalayer-subscribe', function () {
it('should handle invalid send json messages', function (done) {

let flow = [
{ "id": "f1", "type": "tab", "label": "Test flow"},
{ "id": "h1", "z":"f1", "type": "helper" },
{ "id": "n1", "z":"f1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": "test/invalid/json", "name": "subscribe", "wires": [["h1"]] },
{ "id": "s1", "z":"f1", "type": "ctrlx-config-subscription", "device": "c1", "name": "sub1", "publishIntervalMs": "1000" },
{ "id": "c1", "z":"f1", "type": "ctrlx-config", "name": "ctrlx", "hostname": getHostname(), "debug": true },
{ "id": "f1", "type": "tab", "label": "Test flow" },
{ "id": "h1", "z": "f1", "type": "helper" },
{ "id": "n1", "z": "f1", "type": "ctrlx-datalayer-subscribe", "subscription": "s1", "path": "test/invalid/json", "name": "subscribe", "wires": [["h1"]] },
{ "id": "s1", "z": "f1", "type": "ctrlx-config-subscription", "device": "c1", "name": "sub1", "publishIntervalMs": "1000" },
{ "id": "c1", "z": "f1", "type": "ctrlx-config", "name": "ctrlx", "hostname": getHostname(), "debug": true },
];
let credentials = {
c1: {
Expand Down
13 changes: 13 additions & 0 deletions test/helper/CtrlxMockupV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class CtrlxMockupV2 {
type: 'bool'
});
});

this.app.get('/automation/api/v2/nodes/framework/metrics/system/cpu-utilisation-percent', authenticateJWT, (req, res) => {
switch (req.query.type) {
case undefined:
Expand Down Expand Up @@ -166,6 +167,7 @@ class CtrlxMockupV2 {
break;
}
});

this.app.get('/automation/api/v2/nodes/framework/metrics/system', authenticateJWT, (req, res) => {
if (req.query.type === 'browse') {
res.statusCode = 200;
Expand Down Expand Up @@ -193,6 +195,7 @@ class CtrlxMockupV2 {
type: 'int16'
});
});

this.app.get('/automation/api/v2/nodes/plc/app/Application/sym/PLC_PRG/i', authenticateJWT, (req, res) => {
res.statusCode = 200;
res.json({
Expand All @@ -210,10 +213,13 @@ class CtrlxMockupV2 {
}
this.var_i64 = req.body.value;
res.statusCode = 200;
res.setHeader('content-type', 'application/json');
res.send(`{"type": "int64", "value":${this.var_i64.toString()}}`);
});

this.app.get('/automation/api/v2/nodes/plc/app/Application/sym/PLC_PRG/i64', authenticateJWT, (req, res) => {
res.statusCode = 200;
res.setHeader('content-type', 'application/json');
res.send(`{"type": "int64", "value":${this.var_i64.toString()}}`);
});

Expand All @@ -231,6 +237,7 @@ class CtrlxMockupV2 {
type: 'string'
});
});

this.app.get('/automation/api/v2/nodes/plc/app/Application/sym/PLC_PRG/str', authenticateJWT, (req, res) => {
res.statusCode = 200;
res.json({
Expand Down Expand Up @@ -282,6 +289,12 @@ class CtrlxMockupV2 {
});
});

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


//
// Builtin Data Mockups - Create/Delete
Expand Down