node-red-contrib-3dm-space 2.2.0 → 2.3.0
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/3dm-cloud-in.js +4 -2
- package/3dm-cloud-join.js +3 -1
- package/3dm-cloud-out.html +4 -1
- package/3dm-cloud-out.js +43 -3
- package/3dm-config-store.js +5 -3
- package/CHANGELOG.md +16 -0
- package/LICENSE +25 -0
- package/README.md +20 -2
- package/package.json +44 -34
- package/payload-precision.js +38 -0
- package/node-red-contrib-3dm-space-2.0.0.tgz +0 -0
package/3dm-cloud-in.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
|
-
"use strict";
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var roundPayload = require("./payload-precision");
|
|
3
5
|
|
|
4
6
|
/*
|
|
5
7
|
* Hidden cloud subscribe settings.
|
|
@@ -53,7 +55,7 @@ module.exports = function (RED) {
|
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
var data = parsePayload(payload);
|
|
58
|
+
var data = roundPayload(parsePayload(payload));
|
|
57
59
|
|
|
58
60
|
if (node.keyFilter) {
|
|
59
61
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
package/3dm-cloud-join.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
+
var roundPayload = require("./payload-precision");
|
|
5
|
+
|
|
4
6
|
function isPlainObject(value) {
|
|
5
7
|
return value &&
|
|
6
8
|
typeof value === "object" &&
|
|
@@ -79,7 +81,7 @@ module.exports = function (RED) {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
var outMsg = node.firstMsg || {};
|
|
82
|
-
outMsg.payload = clonePayloadObject(node.buffer);
|
|
84
|
+
outMsg.payload = roundPayload(clonePayloadObject(node.buffer));
|
|
83
85
|
|
|
84
86
|
node.send(outMsg);
|
|
85
87
|
|
package/3dm-cloud-out.html
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
server: { value: "", type: "3dm-cloud-config", required: true }
|
|
8
8
|
},
|
|
9
9
|
inputs: 1,
|
|
10
|
-
outputs:
|
|
10
|
+
outputs: 2,
|
|
11
|
+
outputLabels: ["telemetry result", "cloud online"],
|
|
11
12
|
icon: "font-awesome/fa-cloud-upload",
|
|
12
13
|
label: function () {
|
|
13
14
|
return this.name || "3DM telemetry out";
|
|
@@ -36,6 +37,8 @@
|
|
|
36
37
|
<p>Sends telemetry to 3DM.space SCADA Cloud.</p>
|
|
37
38
|
|
|
38
39
|
<p>Input must be a JSON object.</p>
|
|
40
|
+
<p>Output 1: the existing telemetry result message.</p>
|
|
41
|
+
<p>Output 2: connection status with <code>msg.topic = "3dm.connection"</code> and a boolean <code>msg.payload</code>: <code>true</code> for online, <code>false</code> for offline. Emitted once after deployment and on each connection state change, without requiring telemetry input. A missing login configuration reports false.</p>
|
|
39
42
|
|
|
40
43
|
<p>Example:</p>
|
|
41
44
|
|
package/3dm-cloud-out.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
|
-
"use strict";
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var roundPayload = require("./payload-precision");
|
|
3
5
|
|
|
4
6
|
var fs = require("fs");
|
|
5
7
|
var path = require("path");
|
|
@@ -85,6 +87,32 @@ module.exports = function (RED) {
|
|
|
85
87
|
node.name = config.name;
|
|
86
88
|
node.server = RED.nodes.getNode(config.server);
|
|
87
89
|
|
|
90
|
+
var lastConnectionState;
|
|
91
|
+
var connectionOutputClosed = false;
|
|
92
|
+
|
|
93
|
+
function sendConnectionState(connected) {
|
|
94
|
+
connected = Boolean(connected);
|
|
95
|
+
if (connectionOutputClosed || connected === lastConnectionState) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
lastConnectionState = connected;
|
|
99
|
+
node.send([null, { topic: "3dm.connection", payload: connected }]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Defer the initial message until Node-RED has connected the flow wires.
|
|
103
|
+
var connectionStartupTimer = setTimeout(function () {
|
|
104
|
+
connectionStartupTimer = null;
|
|
105
|
+
sendConnectionState(node.server && node.server.connected);
|
|
106
|
+
}, 0);
|
|
107
|
+
|
|
108
|
+
node.on("close", function () {
|
|
109
|
+
connectionOutputClosed = true;
|
|
110
|
+
if (connectionStartupTimer) {
|
|
111
|
+
clearTimeout(connectionStartupTimer);
|
|
112
|
+
connectionStartupTimer = null;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
88
116
|
var queueFile = path.join(
|
|
89
117
|
QUEUE_DIR,
|
|
90
118
|
QUEUE_FILE_PREFIX + "-" + node.id + ".sqlite"
|
|
@@ -234,7 +262,7 @@ module.exports = function (RED) {
|
|
|
234
262
|
|
|
235
263
|
return {
|
|
236
264
|
ts: ts,
|
|
237
|
-
values: payload
|
|
265
|
+
values: roundPayload(payload)
|
|
238
266
|
};
|
|
239
267
|
}
|
|
240
268
|
|
|
@@ -253,6 +281,8 @@ module.exports = function (RED) {
|
|
|
253
281
|
return;
|
|
254
282
|
}
|
|
255
283
|
|
|
284
|
+
packet = Object.assign({}, packet, { values: roundPayload(packet.values) });
|
|
285
|
+
|
|
256
286
|
node.server.publish(
|
|
257
287
|
TELEMETRY_TOPIC,
|
|
258
288
|
packet,
|
|
@@ -283,7 +313,16 @@ module.exports = function (RED) {
|
|
|
283
313
|
}
|
|
284
314
|
|
|
285
315
|
function sendResult(msg, send, done) {
|
|
286
|
-
|
|
316
|
+
if (typeof msg.payload === "string") {
|
|
317
|
+
try {
|
|
318
|
+
msg.payload = JSON.stringify(roundPayload(JSON.parse(msg.payload)));
|
|
319
|
+
} catch (err) {
|
|
320
|
+
// Preserve non-JSON text on rejected messages.
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
msg.payload = roundPayload(msg.payload);
|
|
324
|
+
}
|
|
325
|
+
send([msg, null]);
|
|
287
326
|
|
|
288
327
|
if (done) {
|
|
289
328
|
done();
|
|
@@ -592,6 +631,7 @@ module.exports = function (RED) {
|
|
|
592
631
|
}
|
|
593
632
|
|
|
594
633
|
var stateHandler = function (connected) {
|
|
634
|
+
sendConnectionState(connected);
|
|
595
635
|
if (connected) {
|
|
596
636
|
updateStatus("connected");
|
|
597
637
|
maybeStartBackhaul();
|
package/3dm-config-store.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
|
-
"use strict";
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var roundPayload = require("./payload-precision");
|
|
3
5
|
|
|
4
6
|
var fs = require("fs");
|
|
5
7
|
var path = require("path");
|
|
@@ -694,9 +696,9 @@ module.exports = function (RED) {
|
|
|
694
696
|
topic: "configStore",
|
|
695
697
|
event: eventName || "configStore",
|
|
696
698
|
payload: {
|
|
697
|
-
configStore: latestStore
|
|
699
|
+
configStore: roundPayload(latestStore)
|
|
698
700
|
},
|
|
699
|
-
configStore: latestStore
|
|
701
|
+
configStore: roundPayload(latestStore)
|
|
700
702
|
};
|
|
701
703
|
|
|
702
704
|
node.send(outputs);
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.3.0 — 2026-10-18
|
|
4
|
+
|
|
5
|
+
- Add a second 3DM Out output for boolean cloud connection status: true online, false offline, on deployment and state changes.
|
|
6
|
+
- Preserve existing telemetry results on output 1, queue behavior, rate limiting, and five-decimal payload precision.
|
|
7
|
+
- Replace GPL declarations with Smithtek proprietary licence terms and include LICENSE in the release.
|
|
8
|
+
- Preserve all 2.2.1 documentation and assets; extend the npm file list to include all Markdown documents.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## 2.2.1 — 2026-10-01
|
|
12
|
+
|
|
13
|
+
- Round numeric output payload values to a maximum of five decimal places, including nested objects and arrays.
|
|
14
|
+
- Apply consistent precision to cloud telemetry, queue replay, local telemetry results, incoming attributes, joined messages, and config snapshots.
|
|
15
|
+
- Preserve numeric types, integers, text, stored configuration precision, and internal control comparisons.
|
|
16
|
+
- Include the shared precision helper in the npm package and exclude old release archives.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Smithtek Proprietary Software Licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Smithtek. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software, including its source code, documentation, and accompanying
|
|
6
|
+
Smithtek materials, is proprietary to Smithtek. It is not licensed under the
|
|
7
|
+
GPL or any other open-source licence.
|
|
8
|
+
|
|
9
|
+
Possession, download, installation, or public availability of this package does
|
|
10
|
+
not grant a licence. Use is permitted only under a separate written agreement
|
|
11
|
+
or express written permission from Smithtek, and only within its scope.
|
|
12
|
+
|
|
13
|
+
Without that permission, no person may use, copy, modify, distribute,
|
|
14
|
+
sublicense, sell, or create derivative works of this software, except to the
|
|
15
|
+
extent applicable law requires otherwise. Copyright and proprietary notices
|
|
16
|
+
must be retained in any authorised copies.
|
|
17
|
+
|
|
18
|
+
Third-party dependencies and materials remain subject to their respective
|
|
19
|
+
licences. These terms do not replace or restrict rights granted by those
|
|
20
|
+
third-party licences.
|
|
21
|
+
|
|
22
|
+
To the extent permitted by law, this software is provided AS IS, without
|
|
23
|
+
warranties of any kind. Smithtek shall not be liable for loss or damage arising
|
|
24
|
+
from use of the software. A separate written agreement with Smithtek takes
|
|
25
|
+
precedence over these terms where they conflict.
|
package/README.md
CHANGED
|
@@ -23,6 +23,13 @@ The package is intended for Smithtek PassPort gateway projects using 3DM.space,
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
+
|
|
27
|
+
## Numeric output precision
|
|
28
|
+
|
|
29
|
+
From version 2.2.1, numeric output payload values are rounded to a maximum of five decimal places. This applies recursively to nested objects and arrays in telemetry, incoming attributes, joined payloads, and config snapshots. For example, `12.12345678` becomes `12.12346`.
|
|
30
|
+
|
|
31
|
+
Values remain numbers; trailing zeros are not added. Integers, strings (including numeric strings), booleans, and null are unchanged. JSON telemetry strings are parsed and their numeric values are rounded before publishing; the local result payload remains a JSON string. Live and queued telemetry use the same precision, including older packets replayed from the queue. Stored configuration and internal control comparisons retain their original precision.
|
|
32
|
+
|
|
26
33
|
## Requirements
|
|
27
34
|
|
|
28
35
|
To use these nodes, you need:
|
|
@@ -53,7 +60,7 @@ The credentials are copied from 3DM.space into Node-RED in this order:
|
|
|
53
60
|
|
|
54
61
|
## Nodes Included
|
|
55
62
|
|
|
56
|
-
This package includes
|
|
63
|
+
This package includes five nodes:
|
|
57
64
|
|
|
58
65
|
- **3DM Cloud Login**
|
|
59
66
|
- **3DM Out**
|
|
@@ -178,6 +185,15 @@ If the cloud connection is unavailable, valid data is stored locally and sent la
|
|
|
178
185
|
|
|
179
186
|
---
|
|
180
187
|
|
|
188
|
+
## 3DM Out Outputs
|
|
189
|
+
|
|
190
|
+
From version 2.3.0, 3DM Out has two output pins:
|
|
191
|
+
|
|
192
|
+
1. **Telemetry result**: the existing result message, including sent, queued, rate-limit, and error fields.
|
|
193
|
+
2. **Cloud online**: a message with boolean `msg.payload`: `true` when the shared MQTT connection to 3DM.space is online, or `false` when offline. The topic is `3dm.connection`.
|
|
194
|
+
|
|
195
|
+
The second output emits once after deployment and whenever the connection state changes. It works without incoming telemetry. Repeated notifications of the same state do not produce duplicate messages. A missing cloud login configuration reports `false`. This reports MQTT connection status; it does not confirm that an individual telemetry packet was delivered or stored. Existing wires on output 1 keep their telemetry behavior. Connect output 2 to a Switch node or other flow logic to react to online/offline changes.
|
|
196
|
+
|
|
181
197
|
## 3DM Out Input Data Type
|
|
182
198
|
|
|
183
199
|
The 3DM Out node expects `msg.payload` to be a JSON object.
|
|
@@ -2606,4 +2622,6 @@ Smithtek
|
|
|
2606
2622
|
|
|
2607
2623
|
# License
|
|
2608
2624
|
|
|
2609
|
-
|
|
2625
|
+
Smithtek proprietary software. Copyright (c) 2026 Smithtek. All rights reserved.
|
|
2626
|
+
|
|
2627
|
+
This package is not offered under an open-source licence. Use requires permission from Smithtek. See [LICENSE](LICENSE) for the proprietary terms. Third-party dependencies retain their own licences.
|
package/package.json
CHANGED
|
@@ -1,34 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "node-red-contrib-3dm-space",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Node-RED nodes for 3DM.space SCADA Cloud telemetry, attributes, and local config storage",
|
|
5
|
-
"main": "3dm-cloud-config.js",
|
|
6
|
-
"license": "
|
|
7
|
-
"keywords": [
|
|
8
|
-
"node-red",
|
|
9
|
-
"node-red-contrib",
|
|
10
|
-
"3dm",
|
|
11
|
-
"3dm.space",
|
|
12
|
-
"smithtek",
|
|
13
|
-
"scada",
|
|
14
|
-
"telemetry",
|
|
15
|
-
"store-and-forward",
|
|
16
|
-
"config-store",
|
|
17
|
-
"scheduler"
|
|
18
|
-
],
|
|
19
|
-
"node-red": {
|
|
20
|
-
"nodes": {
|
|
21
|
-
"3dm-cloud-config": "3dm-cloud-config.js",
|
|
22
|
-
"3dm-cloud-out": "3dm-cloud-out.js",
|
|
23
|
-
"3dm-cloud-in": "3dm-cloud-in.js",
|
|
24
|
-
"3dm-config-store": "3dm-config-store.js",
|
|
25
|
-
"3dm-cloud-join": "3dm-cloud-join.js"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"mqtt": "~4.3.7"
|
|
30
|
-
},
|
|
31
|
-
"optionalDependencies": {
|
|
32
|
-
"node-persistent-queue": "^0.3.1"
|
|
33
|
-
}
|
|
34
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-3dm-space",
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Node-RED nodes for 3DM.space SCADA Cloud telemetry, attributes, and local config storage",
|
|
5
|
+
"main": "3dm-cloud-config.js",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"node-red",
|
|
9
|
+
"node-red-contrib",
|
|
10
|
+
"3dm",
|
|
11
|
+
"3dm.space",
|
|
12
|
+
"smithtek",
|
|
13
|
+
"scada",
|
|
14
|
+
"telemetry",
|
|
15
|
+
"store-and-forward",
|
|
16
|
+
"config-store",
|
|
17
|
+
"scheduler"
|
|
18
|
+
],
|
|
19
|
+
"node-red": {
|
|
20
|
+
"nodes": {
|
|
21
|
+
"3dm-cloud-config": "3dm-cloud-config.js",
|
|
22
|
+
"3dm-cloud-out": "3dm-cloud-out.js",
|
|
23
|
+
"3dm-cloud-in": "3dm-cloud-in.js",
|
|
24
|
+
"3dm-config-store": "3dm-config-store.js",
|
|
25
|
+
"3dm-cloud-join": "3dm-cloud-join.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"mqtt": "~4.3.7"
|
|
30
|
+
},
|
|
31
|
+
"optionalDependencies": {
|
|
32
|
+
"node-persistent-queue": "^0.3.1"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"3dm-*.js",
|
|
36
|
+
"3dm-*.html",
|
|
37
|
+
"payload-precision.js",
|
|
38
|
+
"icons/",
|
|
39
|
+
"icon.png",
|
|
40
|
+
"*.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"author": "Smithtek"
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Round numeric payload values without changing strings or the source objects.
|
|
4
|
+
function roundPayload(value, seen) {
|
|
5
|
+
if (typeof value === "number") {
|
|
6
|
+
return Number.isFinite(value) && !Number.isInteger(value)
|
|
7
|
+
? Number(value.toFixed(5))
|
|
8
|
+
: value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!value || typeof value !== "object") {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var prototype = Object.getPrototypeOf(value);
|
|
16
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
seen = seen || new WeakMap();
|
|
21
|
+
if (seen.has(value)) {
|
|
22
|
+
return seen.get(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var output = Array.isArray(value) ? new Array(value.length) : Object.create(prototype);
|
|
26
|
+
seen.set(value, output);
|
|
27
|
+
Object.keys(value).forEach(function (key) {
|
|
28
|
+
Object.defineProperty(output, key, {
|
|
29
|
+
value: roundPayload(value[key], seen),
|
|
30
|
+
enumerable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
configurable: true
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = roundPayload;
|
|
Binary file
|