node-red-contrib-3dm-space 2.1.0 → 2.2.1
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.js +15 -2
- package/3dm-config-store.js +5 -3
- package/CHANGELOG.md +8 -0
- package/README.md +247 -2
- package/package.json +43 -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.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");
|
|
@@ -234,7 +236,7 @@ module.exports = function (RED) {
|
|
|
234
236
|
|
|
235
237
|
return {
|
|
236
238
|
ts: ts,
|
|
237
|
-
values: payload
|
|
239
|
+
values: roundPayload(payload)
|
|
238
240
|
};
|
|
239
241
|
}
|
|
240
242
|
|
|
@@ -253,6 +255,8 @@ module.exports = function (RED) {
|
|
|
253
255
|
return;
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
packet = Object.assign({}, packet, { values: roundPayload(packet.values) });
|
|
259
|
+
|
|
256
260
|
node.server.publish(
|
|
257
261
|
TELEMETRY_TOPIC,
|
|
258
262
|
packet,
|
|
@@ -283,6 +287,15 @@ module.exports = function (RED) {
|
|
|
283
287
|
}
|
|
284
288
|
|
|
285
289
|
function sendResult(msg, send, done) {
|
|
290
|
+
if (typeof msg.payload === "string") {
|
|
291
|
+
try {
|
|
292
|
+
msg.payload = JSON.stringify(roundPayload(JSON.parse(msg.payload)));
|
|
293
|
+
} catch (err) {
|
|
294
|
+
// Preserve non-JSON text on rejected messages.
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
msg.payload = roundPayload(msg.payload);
|
|
298
|
+
}
|
|
286
299
|
send(msg);
|
|
287
300
|
|
|
288
301
|
if (done) {
|
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,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.2.1 — 2026-10-01
|
|
4
|
+
|
|
5
|
+
- Round numeric output payload values to a maximum of five decimal places, including nested objects and arrays.
|
|
6
|
+
- Apply consistent precision to cloud telemetry, queue replay, local telemetry results, incoming attributes, joined messages, and config snapshots.
|
|
7
|
+
- Preserve numeric types, integers, text, stored configuration precision, and internal control comparisons.
|
|
8
|
+
- Include the shared precision helper in the npm package and exclude old release archives.
|
package/README.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
2
8
|

|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
3
16
|
# node-red-contrib-3dm-space
|
|
4
17
|
|
|
5
18
|
Node-RED nodes for connecting Smithtek PassPort gateways to **3DM.space SCADA Cloud**.
|
|
@@ -10,6 +23,13 @@ The package is intended for Smithtek PassPort gateway projects using 3DM.space,
|
|
|
10
23
|
|
|
11
24
|
---
|
|
12
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
|
+
|
|
13
33
|
## Requirements
|
|
14
34
|
|
|
15
35
|
To use these nodes, you need:
|
|
@@ -40,13 +60,15 @@ The credentials are copied from 3DM.space into Node-RED in this order:
|
|
|
40
60
|
|
|
41
61
|
## Nodes Included
|
|
42
62
|
|
|
43
|
-
This package includes
|
|
63
|
+
This package includes five nodes:
|
|
44
64
|
|
|
45
65
|
- **3DM Cloud Login**
|
|
46
66
|
- **3DM Out**
|
|
47
67
|
- **3DM In**
|
|
48
68
|
- **3DM Config Store**
|
|
49
|
-
|
|
69
|
+
- **3DM Join**
|
|
70
|
+
|
|
71
|
+

|
|
50
72
|
|
|
51
73
|
---
|
|
52
74
|
|
|
@@ -1343,8 +1365,231 @@ output2 skipped - output disabled
|
|
|
1343
1365
|
|
|
1344
1366
|
Increase Active Outputs or change the item to `output1`.
|
|
1345
1367
|
|
|
1368
|
+
---
|
|
1369
|
+
---
|
|
1370
|
+
|
|
1371
|
+
# 3DM Join
|
|
1372
|
+

|
|
1373
|
+
The **3DM Join** node collects multiple JSON payloads, merges them into one payload, then sends one combined message after a configurable timeout.
|
|
1374
|
+
|
|
1375
|
+
It is useful when data arrives from multiple sources at slightly different times, but the user wants one clean telemetry update sent to 3DM.space.
|
|
1376
|
+
|
|
1377
|
+
Typical use:
|
|
1378
|
+
|
|
1379
|
+
```text
|
|
1380
|
+
Mako RF Station 1
|
|
1381
|
+
Mako RF Station 2
|
|
1382
|
+
Mako RF Station 3
|
|
1383
|
+
↓
|
|
1384
|
+
3DM Join
|
|
1385
|
+
↓
|
|
1386
|
+
3DM Out
|
|
1387
|
+
↓
|
|
1388
|
+
3DM.space
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
---
|
|
1392
|
+
|
|
1393
|
+
## Why Use 3DM Join
|
|
1394
|
+
|
|
1395
|
+
The 3DM Join node simplifies flows where telemetry is collected from more than one source.
|
|
1396
|
+
|
|
1397
|
+
Instead of sending each small payload separately to 3DM.space, the Join node can collect the values first and send them as one larger JSON object.
|
|
1398
|
+
|
|
1399
|
+
This is ideal for RF systems where each remote station may respond a few seconds apart, but the user is happy with one complete site update every minute.
|
|
1400
|
+
|
|
1401
|
+
Example:
|
|
1402
|
+
|
|
1403
|
+
```text
|
|
1404
|
+
Bore 1 data arrives
|
|
1405
|
+
Bore 2 data arrives
|
|
1406
|
+
Tank data arrives
|
|
1407
|
+
Weather data arrives
|
|
1408
|
+
↓
|
|
1409
|
+
3DM Join waits for the timeout
|
|
1410
|
+
↓
|
|
1411
|
+
One combined payload is sent to 3DM Out
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
---
|
|
1415
|
+
|
|
1416
|
+
## Input Data Type
|
|
1417
|
+
|
|
1418
|
+
The 3DM Join node expects `msg.payload` to be a JSON object.
|
|
1419
|
+
|
|
1420
|
+
Correct:
|
|
1421
|
+
|
|
1422
|
+
```json
|
|
1423
|
+
{
|
|
1424
|
+
"b1 dc": 24.3
|
|
1425
|
+
}
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
Correct:
|
|
1429
|
+
|
|
1430
|
+
```json
|
|
1431
|
+
{
|
|
1432
|
+
"b2 flow": 88.5
|
|
1433
|
+
}
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
Incorrect:
|
|
1437
|
+
|
|
1438
|
+
```json
|
|
1439
|
+
24.3
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
Incorrect:
|
|
1443
|
+
|
|
1444
|
+
```text
|
|
1445
|
+
b1 dc
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
Invalid payloads are ignored.
|
|
1449
|
+
|
|
1450
|
+
---
|
|
1451
|
+
|
|
1452
|
+
## Timeout
|
|
1453
|
+
|
|
1454
|
+
The timeout starts when the first valid message arrives.
|
|
1455
|
+
|
|
1456
|
+
The timer does not restart when more messages arrive.
|
|
1457
|
+
|
|
1458
|
+
Example:
|
|
1459
|
+
|
|
1460
|
+
```text
|
|
1461
|
+
Timeout: 10 seconds
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
If the first message arrives at 12:00:00, the joined output is sent at 12:00:10.
|
|
1465
|
+
|
|
1466
|
+
Any valid JSON payloads received during that 10 second window are merged together.
|
|
1467
|
+
|
|
1346
1468
|
---
|
|
1347
1469
|
|
|
1470
|
+
## Duplicate Keys
|
|
1471
|
+
|
|
1472
|
+
If the same key arrives more than once during the timeout window, the latest value is used.
|
|
1473
|
+
|
|
1474
|
+
Example:
|
|
1475
|
+
|
|
1476
|
+
```json
|
|
1477
|
+
{
|
|
1478
|
+
"b1 dc": 24.1
|
|
1479
|
+
}
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
then:
|
|
1483
|
+
|
|
1484
|
+
```json
|
|
1485
|
+
{
|
|
1486
|
+
"b1 dc": 24.4
|
|
1487
|
+
}
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
Final joined output:
|
|
1491
|
+
|
|
1492
|
+
```json
|
|
1493
|
+
{
|
|
1494
|
+
"b1 dc": 24.4
|
|
1495
|
+
}
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
---
|
|
1499
|
+
|
|
1500
|
+
## Example Joined Output
|
|
1501
|
+
|
|
1502
|
+
Incoming message 1:
|
|
1503
|
+
|
|
1504
|
+
```json
|
|
1505
|
+
{
|
|
1506
|
+
"b1 dc": 24.3,
|
|
1507
|
+
"b1 fb": 1
|
|
1508
|
+
}
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
Incoming message 2:
|
|
1512
|
+
|
|
1513
|
+
```json
|
|
1514
|
+
{
|
|
1515
|
+
"b2 dc": 24.1,
|
|
1516
|
+
"b2 fb": 0
|
|
1517
|
+
}
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
Incoming message 3:
|
|
1521
|
+
|
|
1522
|
+
```json
|
|
1523
|
+
{
|
|
1524
|
+
"tank level": 76.5
|
|
1525
|
+
}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
Output after timeout:
|
|
1529
|
+
|
|
1530
|
+
```json
|
|
1531
|
+
{
|
|
1532
|
+
"b1 dc": 24.3,
|
|
1533
|
+
"b1 fb": 1,
|
|
1534
|
+
"b2 dc": 24.1,
|
|
1535
|
+
"b2 fb": 0,
|
|
1536
|
+
"tank level": 76.5
|
|
1537
|
+
}
|
|
1538
|
+
```
|
|
1539
|
+
|
|
1540
|
+
---
|
|
1541
|
+
|
|
1542
|
+
## Recommended Use With 3DM Out
|
|
1543
|
+
|
|
1544
|
+
The 3DM Join node is normally placed before the **3DM Out** node.
|
|
1545
|
+
|
|
1546
|
+
```text
|
|
1547
|
+
RF / PLC / Sensor Inputs
|
|
1548
|
+
↓
|
|
1549
|
+
3DM Join
|
|
1550
|
+
↓
|
|
1551
|
+
3DM Out
|
|
1552
|
+
```
|
|
1553
|
+
|
|
1554
|
+
This keeps telemetry updates cleaner and helps reduce the number of separate cloud messages.
|
|
1555
|
+
|
|
1556
|
+
For example, an RF system may poll several stations over 30 to 45 seconds, then send one complete site update to 3DM.space every minute.
|
|
1557
|
+
|
|
1558
|
+
---
|
|
1559
|
+
|
|
1560
|
+
## 3DM Join Status Messages
|
|
1561
|
+
|
|
1562
|
+
### waiting
|
|
1563
|
+
|
|
1564
|
+
The node is waiting for the first valid JSON payload.
|
|
1565
|
+
|
|
1566
|
+
### collecting
|
|
1567
|
+
|
|
1568
|
+
The node has received at least one valid payload and is collecting keys until the timeout expires.
|
|
1569
|
+
|
|
1570
|
+
### sent
|
|
1571
|
+
|
|
1572
|
+
The timeout expired and the combined payload was sent.
|
|
1573
|
+
|
|
1574
|
+
### bad payload
|
|
1575
|
+
|
|
1576
|
+
The incoming `msg.payload` was not a JSON object.
|
|
1577
|
+
|
|
1578
|
+
Check the payload going into the 3DM Join node.
|
|
1579
|
+
|
|
1580
|
+
---
|
|
1581
|
+
|
|
1582
|
+
## Notes
|
|
1583
|
+
|
|
1584
|
+
- Use 3DM Join to combine multiple telemetry payloads before 3DM Out.
|
|
1585
|
+
- The timeout starts from the first valid message.
|
|
1586
|
+
- The timer does not reset when more messages arrive.
|
|
1587
|
+
- Payloads must be JSON objects.
|
|
1588
|
+
- Duplicate keys are overwritten by the latest value.
|
|
1589
|
+
- The output is one combined `msg.payload` object.
|
|
1590
|
+
- This is useful for RF polling systems, multi-device sites, PLC data blocks and grouped telemetry updates.
|
|
1591
|
+
|
|
1592
|
+
---
|
|
1348
1593
|
# 3DM SCADA Canvas Smart Control Integration
|
|
1349
1594
|
|
|
1350
1595
|

|
package/package.json
CHANGED
|
@@ -1,34 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "node-red-contrib-3dm-space",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"description": "Node-RED nodes for 3DM.space SCADA Cloud telemetry, attributes, and local config storage",
|
|
5
|
-
"main": "3dm-cloud-config.js",
|
|
6
|
-
"license": "GPL-3.0-or-later",
|
|
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.2.1",
|
|
4
|
+
"description": "Node-RED nodes for 3DM.space SCADA Cloud telemetry, attributes, and local config storage",
|
|
5
|
+
"main": "3dm-cloud-config.js",
|
|
6
|
+
"license": "GPL-3.0-or-later",
|
|
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
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -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
|