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 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) {
@@ -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
  ![This is an alt text.](https://github.com/smithtekIOT/ArtWork/blob/master/3dm%20github.png?raw=true")
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 four nodes:
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
- ![This is an alt text.](https://github.com/smithtekIOT/ArtWork/blob/master/3dm%20nodes.jpg?raw=true")
69
+ - **3DM Join**
70
+
71
+ ![This is an alt text.](https://github.com/smithtekIOT/ArtWork/blob/master/3dm%204%20nodes.jpg?raw=true)
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
+ ![This is an alt text.](https://github.com/smithtekIOT/ArtWork/blob/master/3DM%20join%20nodes.jpg?raw=true)
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
  ![This is an alt text.](https://github.com/smithtekIOT/ArtWork/blob/master/Canvas%20SCADA%203dm.jpg?raw=true)
package/package.json CHANGED
@@ -1,34 +1,43 @@
1
- {
2
- "name": "node-red-contrib-3dm-space",
3
- "version": "2.1.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": "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;