node-red-contrib-ta-cmi-coe 1.0.0 → 1.1.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/CHANGELOG.md +28 -17
- package/README.de.md +225 -0
- package/README.md +134 -187
- package/coe/coe-input.js +49 -36
- package/coe/coe-monitor.html +1 -1
- package/coe/coe-monitor.js +32 -41
- package/coe/coe-output.html +67 -28
- package/coe/coe-output.js +159 -24
- package/coe/config.js +14 -7
- package/coe/locales/de/coe-input.html +2 -2
- package/coe/locales/de/coe-input.json +8 -2
- package/coe/locales/de/coe-monitor.html +1 -2
- package/coe/locales/de/coe-monitor.json +9 -4
- package/coe/locales/de/coe-output.html +8 -2
- package/coe/locales/de/coe-output.json +11 -5
- package/coe/locales/de/config.html +3 -3
- package/coe/locales/de/config.json +1 -1
- package/coe/locales/en-US/coe-input.html +2 -2
- package/coe/locales/en-US/coe-input.json +8 -2
- package/coe/locales/en-US/coe-monitor.html +1 -2
- package/coe/locales/en-US/coe-monitor.json +9 -4
- package/coe/locales/en-US/coe-output.html +8 -2
- package/coe/locales/en-US/coe-output.json +10 -4
- package/coe/locales/en-US/config.html +3 -3
- package/coe/locales/en-US/config.json +1 -1
- package/coe/units-config.js +2 -1
- package/examples/Example Flow.json +56 -0
- package/lib/coe-v1.js +152 -0
- package/lib/coe-v2.js +78 -116
- package/lib/old.js +133 -0
- package/lib/protocol.js +19 -0
- package/lib/queueing.js +92 -137
- package/lib/units.js +7 -7
- package/lib/utils.js +71 -66
- package/package.json +14 -2
- package/__tests__/blockinfo.test.js +0 -24
- package/__tests__/conversion.test.js +0 -22
- package/__tests__/udp.test.js +0 -46
- package/lib/coe.js +0 -109
package/lib/old.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoE V2 Protocol Support Module
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2025 Florian Mayrhofer
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Convert V2 Data into Uniform format
|
|
10
|
+
function convertV2ToUniformFormat(v2Data) {
|
|
11
|
+
// Group Outputs by block
|
|
12
|
+
const blockMap = {};
|
|
13
|
+
|
|
14
|
+
v2Data.blocks.forEach(block => {
|
|
15
|
+
const isDigital = block.outputNumber <= 254;
|
|
16
|
+
const actualOutput = isDigital ? block.outputNumber : (block.outputNumber - 255);
|
|
17
|
+
|
|
18
|
+
// Determine block number and position
|
|
19
|
+
let blockNumber, position;
|
|
20
|
+
|
|
21
|
+
if (isDigital) {
|
|
22
|
+
// Digital: Output 1-16 → Block 0, Output 17-32 → Block 9
|
|
23
|
+
if (actualOutput <= 16) {
|
|
24
|
+
blockNumber = 0;
|
|
25
|
+
position = actualOutput - 1;
|
|
26
|
+
} else {
|
|
27
|
+
blockNumber = 9;
|
|
28
|
+
position = actualOutput - 17;
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
// Analog: Output 1-4 → Block 1, 5-8 → Block 2, etc.
|
|
32
|
+
blockNumber = Math.floor((actualOutput - 1) / 4) + 1;
|
|
33
|
+
position = (actualOutput - 1) % 4;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const key = `${block.nodeNumber}-${blockNumber}`;
|
|
37
|
+
|
|
38
|
+
if (!blockMap[key]) {
|
|
39
|
+
blockMap[key] = {
|
|
40
|
+
nodeNumber: block.nodeNumber,
|
|
41
|
+
blockNumber: blockNumber,
|
|
42
|
+
dataType: isDigital ? 'digital' : 'analog',
|
|
43
|
+
values: isDigital ? new Array(16).fill(undefined) : new Array(4).fill(undefined),
|
|
44
|
+
units: isDigital ? new Array(16).fill(undefined) : new Array(4).fill(undefined)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Convert value & insert (V2 uses other decimals)
|
|
49
|
+
const convertedValue = convertRawToValue(block.value, block.unitId, 2);
|
|
50
|
+
blockMap[key].values[position] = isDigital ? (block.value ? 1 : 0) : convertedValue;
|
|
51
|
+
|
|
52
|
+
if (blockMap[key].units) {
|
|
53
|
+
blockMap[key].units[position] = block.unitId;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return Object.values(blockMap);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create CoE V2 Packet
|
|
60
|
+
function createPacket(nodeNumber, dataType, outputs) {
|
|
61
|
+
// outputState: Array von {outputNumber, unit, value}
|
|
62
|
+
// Max 16 value blocks
|
|
63
|
+
const outputState = convertUniformToV2Format(dataType, outputs);
|
|
64
|
+
|
|
65
|
+
const blockCount = Math.min(outputState.length, 16);
|
|
66
|
+
const messageLength = 4 + (blockCount * 8);
|
|
67
|
+
|
|
68
|
+
const buffer = Buffer.alloc(messageLength);
|
|
69
|
+
|
|
70
|
+
// Write header
|
|
71
|
+
buffer.writeUInt8(0x02, 0); // Version Low
|
|
72
|
+
buffer.writeUInt8(0x00, 1); // Version High
|
|
73
|
+
buffer.writeUInt8(messageLength, 2); // Message Length
|
|
74
|
+
buffer.writeUInt8(blockCount, 3); // Block Count
|
|
75
|
+
|
|
76
|
+
// Write value blocks
|
|
77
|
+
for (let i = 0; i < blockCount; i++) {
|
|
78
|
+
const offset = 4 + (i * 8);
|
|
79
|
+
const output = outputState[i];
|
|
80
|
+
|
|
81
|
+
buffer.writeUInt8(nodeNumber, offset);
|
|
82
|
+
|
|
83
|
+
// Output Number (Little Endian, 2 Bytes)
|
|
84
|
+
buffer.writeUInt8(output.outputNumber & 0xFF, offset + 1);
|
|
85
|
+
buffer.writeUInt8((output.outputNumber >> 8) & 0xFF, offset + 2);
|
|
86
|
+
|
|
87
|
+
buffer.writeUInt8(output.unitId || 0, offset + 3); // Unit ID
|
|
88
|
+
buffer.writeInt32LE(output.value, offset + 4); // Value (Int32 LE)
|
|
89
|
+
}
|
|
90
|
+
return buffer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Convert Uniform format to V2 outputs
|
|
94
|
+
// function convertUniformToV2Format(blockNumber, values, units, dataType) {
|
|
95
|
+
function convertUniformToV2Format(dataType, outputs) {
|
|
96
|
+
let outputState = [];
|
|
97
|
+
|
|
98
|
+
if (dataType === 'digital') {
|
|
99
|
+
// Digital: 16 Bits
|
|
100
|
+
const baseOutput = blockNumber === 0 ? 1 : 17;
|
|
101
|
+
for (let i = 0; i < values.length; i++) {
|
|
102
|
+
if (values[i] !== undefined) {
|
|
103
|
+
const unitId = units && units[i] !== undefined ? units[i] : 0;
|
|
104
|
+
|
|
105
|
+
outputState.push({
|
|
106
|
+
outputNumber: baseOutput + i,
|
|
107
|
+
unitId: unitId,
|
|
108
|
+
value: values[i] ? 1 : 0
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Analog: 4 Values
|
|
114
|
+
const baseOutput = (blockNumber - 1) * 4 + 1;
|
|
115
|
+
for (let i = 0; i < 4; i++) {
|
|
116
|
+
if (values[i] !== undefined) {
|
|
117
|
+
const unitId = units ? units[i] : 0;
|
|
118
|
+
const rawValue = convertValueToRaw(values[i], unitId, 2); // V2 uses other decimals
|
|
119
|
+
|
|
120
|
+
// Output > 255 = analog
|
|
121
|
+
const outputNumber = baseOutput + i + 255;
|
|
122
|
+
|
|
123
|
+
outputState.push({
|
|
124
|
+
outputNumber: outputNumber,
|
|
125
|
+
unitId: unitId,
|
|
126
|
+
value: rawValue
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return outputState;
|
|
133
|
+
}
|
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoE Protocol Parsing and Creation Module
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2025 Florian Mayrhofer
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
// Parsing functions for CoE V1 and V2
|
|
11
|
+
createPacket: {
|
|
12
|
+
1: require('./coe-v1.js').createPacket,
|
|
13
|
+
2: require('./coe-v2.js').createPacket
|
|
14
|
+
},
|
|
15
|
+
parsePacket: {
|
|
16
|
+
1: require('./coe-v1.js').parsePacket,
|
|
17
|
+
2: require('./coe-v2.js').parsePacket
|
|
18
|
+
}
|
|
19
|
+
};
|
package/lib/queueing.js
CHANGED
|
@@ -6,115 +6,69 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const { createPacket } = require('./protocol');
|
|
10
10
|
|
|
11
|
-
const blockStateCache = {};
|
|
12
|
-
const blockUnitsCache = {}; // Cache for units per block
|
|
13
|
-
const blockQueues = {};
|
|
14
11
|
const blockTimers = {};
|
|
15
12
|
const DEBOUNCE_DELAY = 50; // ms (Time slot for message collection)
|
|
13
|
+
const outputStateCache = {};
|
|
14
|
+
const queuedState = {};
|
|
16
15
|
|
|
17
|
-
//
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} else {
|
|
24
|
-
blockStateCache[key] = new Array(16).fill(0);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
// Return a copy to avoid accidental external mutation
|
|
28
|
-
return Array.isArray(blockStateCache[key]) ? [...blockStateCache[key]] : blockStateCache[key];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Sets the current block state (values) for the given block
|
|
32
|
-
function setBlockState(nodeNumber, blockNumber, dataType, values) {
|
|
33
|
-
const key = `${nodeNumber}-${blockNumber}-${dataType}`;
|
|
34
|
-
blockStateCache[key] = [...values];
|
|
16
|
+
// Persist output state for a specific block
|
|
17
|
+
function setOutputState(queueKey, output) {
|
|
18
|
+
outputStateCache[queueKey] = {
|
|
19
|
+
...outputStateCache[queueKey],
|
|
20
|
+
outputs: output
|
|
21
|
+
};
|
|
35
22
|
}
|
|
36
23
|
|
|
37
|
-
//
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} else {
|
|
45
|
-
blockUnitsCache[key] = null;
|
|
24
|
+
// Return a copy of the current message state
|
|
25
|
+
function getOutputState(node, queueKey) {
|
|
26
|
+
if (!outputStateCache[queueKey]) {
|
|
27
|
+
outputStateCache[queueKey] = {
|
|
28
|
+
nodeNumber: node.nodeNumber,
|
|
29
|
+
dataType: node.dataType,
|
|
30
|
+
outputs: {}
|
|
46
31
|
}
|
|
47
32
|
}
|
|
48
|
-
return
|
|
33
|
+
return { ...outputStateCache[queueKey] };
|
|
49
34
|
}
|
|
50
35
|
|
|
51
|
-
// Sets the current block units for the given block (analog only)
|
|
52
|
-
function setBlockUnits(nodeNumber, blockNumber, dataType, units) {
|
|
53
|
-
const key = `${nodeNumber}-${blockNumber}-${dataType}`;
|
|
54
|
-
if (dataType === 'analog') {
|
|
55
|
-
blockUnitsCache[key] = units ? [...units] : [0,0,0,0];
|
|
56
|
-
} else {
|
|
57
|
-
blockUnitsCache[key] = null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Generates a unique key for the queue based on node, block, and data type
|
|
62
|
-
function getQueueKey(nodeNumber, blockNumber, dataType) {
|
|
63
|
-
return `${nodeNumber}-${blockNumber}-${dataType}`;
|
|
64
|
-
}
|
|
65
36
|
|
|
66
37
|
// Queues and debounces messages for a specific block
|
|
67
|
-
function queueAndSend(node
|
|
68
|
-
const
|
|
38
|
+
function queueAndSend(node) {
|
|
39
|
+
const valueState = {output: node.outputNumber, value: node.lastReceivedValue, unit: node.lastReceivedUnit};
|
|
40
|
+
const origMsg = node.lastReceivedMsg;
|
|
41
|
+
const queueKey = node.queueKey;
|
|
42
|
+
const coeVersion = node.coeVersion;
|
|
43
|
+
const cmiConfig = node.cmiConfig;
|
|
44
|
+
const cmiAddress = node.cmiAddress;
|
|
69
45
|
|
|
70
46
|
// New queueing logic
|
|
71
|
-
let
|
|
72
|
-
let
|
|
47
|
+
let initialOutputState;
|
|
48
|
+
let participatingNodes = new Set();
|
|
73
49
|
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
baseUnits = blockQueues[queueKey].units ? [...blockQueues[queueKey].units] : null;
|
|
50
|
+
if (queuedState[queueKey]) {
|
|
51
|
+
participatingNodes = queuedState[queueKey].participating.add(node);
|
|
77
52
|
} else {
|
|
78
|
-
|
|
79
|
-
|
|
53
|
+
queuedState[queueKey] = {
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
participating: participatingNodes.add(node),
|
|
56
|
+
origMsg: origMsg || null
|
|
57
|
+
}
|
|
80
58
|
}
|
|
81
59
|
|
|
60
|
+
initialOutputState = getOutputState(node, queueKey).outputs;
|
|
61
|
+
|
|
82
62
|
// Merge incoming values/units with existing block state
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
if (units && units[i] !== undefined) {
|
|
92
|
-
mergedUnits[i] = units[i];
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
for (let i = 0; i < 16; i++) {
|
|
97
|
-
if (values[i] !== undefined) {
|
|
98
|
-
mergedValues[i] = values[i];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
63
|
+
let mergedOutputState = initialOutputState;
|
|
64
|
+
|
|
65
|
+
if (valueState.output !== undefined && valueState.output !== null) {
|
|
66
|
+
mergedOutputState[valueState.output] = {
|
|
67
|
+
value: valueState.value,
|
|
68
|
+
unit: valueState.unit
|
|
69
|
+
};
|
|
101
70
|
}
|
|
102
71
|
|
|
103
|
-
if (!blockQueues[queueKey]) { // Create queue, if none
|
|
104
|
-
blockQueues[queueKey] = {
|
|
105
|
-
values: mergedValues,
|
|
106
|
-
units: mergedUnits,
|
|
107
|
-
node: node,
|
|
108
|
-
timestamp: Date.now(),
|
|
109
|
-
origMsg: origMsg || null
|
|
110
|
-
};
|
|
111
|
-
} else { // Overwrite state, if queue exists
|
|
112
|
-
const q = blockQueues[queueKey];
|
|
113
|
-
q.values = mergedValues;
|
|
114
|
-
q.units = mergedUnits;
|
|
115
|
-
q.origMsg = origMsg || q.origMsg;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
72
|
// Delete existing timer if any
|
|
119
73
|
if (blockTimers[queueKey]) {
|
|
120
74
|
clearTimeout(blockTimers[queueKey]);
|
|
@@ -122,67 +76,68 @@ function queueAndSend(node, translate, nodeNumber, blockNumber, values, units, d
|
|
|
122
76
|
|
|
123
77
|
// Start a new timer to send the queued message after the debounce delay
|
|
124
78
|
blockTimers[queueKey] = setTimeout(() => {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
79
|
+
const nodeNumber = outputStateCache[queueKey].nodeNumber;
|
|
80
|
+
const dataType = outputStateCache[queueKey].dataType;
|
|
81
|
+
const queued = mergedOutputState;
|
|
82
|
+
if (outputStateCache[queueKey]) {
|
|
83
|
+
const packet = createPacket[coeVersion](
|
|
128
84
|
nodeNumber,
|
|
129
|
-
blockNumber,
|
|
130
|
-
queued.values,
|
|
131
|
-
queued.units,
|
|
132
85
|
dataType,
|
|
133
|
-
|
|
86
|
+
queued
|
|
134
87
|
);
|
|
135
88
|
|
|
136
|
-
// Persist
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
89
|
+
// Persist the merged output state for the queue
|
|
90
|
+
setOutputState(queueKey, queued);
|
|
91
|
+
|
|
92
|
+
cmiConfig.send(cmiAddress, packet);
|
|
93
|
+
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const mergedText = node._("coe-output.status.merged");
|
|
96
|
+
const readyText = node._("coe-output.status.ready");
|
|
97
|
+
|
|
98
|
+
queuedState[queueKey].participating.forEach(participatingNode => {
|
|
99
|
+
const outputNumber = participatingNode.outputNumber;
|
|
100
|
+
|
|
101
|
+
// Guard against missing sparse entries
|
|
102
|
+
participatingNode.lastSentValue = (queued[outputNumber] && queued[outputNumber].value !== undefined) ? queued[outputNumber].value : undefined;
|
|
103
|
+
participatingNode.lastSentTime = now;
|
|
141
104
|
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
const debugPayload = {
|
|
145
|
-
debug: {
|
|
146
|
-
hex: packet.toString('hex').toUpperCase(),
|
|
147
|
-
node: nodeNumber,
|
|
148
|
-
block: blockNumber,
|
|
149
|
-
dataType: dataType,
|
|
150
|
-
version: version,
|
|
151
|
-
blockState: queued.values,
|
|
152
|
-
units: queued.units
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
// If node has outputs, send original msg on first output and debug on second
|
|
156
|
-
queued.node.send([queued.origMsg || null, { payload: debugPayload }]);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
// Do not break sending on debug failure
|
|
159
|
-
queued.node.warn(`Failed to send debug msg: ${err.message}`);
|
|
160
|
-
}
|
|
161
|
-
const mergedText = translate("coe-output.status.merged");
|
|
162
|
-
const readyText = translate("coe-output.status.ready");
|
|
163
|
-
|
|
164
|
-
cmiConfig.send(cmiAddress, packet);
|
|
165
|
-
|
|
166
|
-
queued.node.status({
|
|
105
|
+
participatingNode.status({
|
|
167
106
|
fill: "green",
|
|
168
107
|
shape: "dot",
|
|
169
|
-
text: `${mergedText} [${
|
|
108
|
+
text: `${mergedText} [v${coeVersion}]`
|
|
170
109
|
});
|
|
171
|
-
|
|
172
110
|
setTimeout(() => {
|
|
173
|
-
|
|
174
|
-
},
|
|
111
|
+
participatingNode.status({fill: "grey", shape: "ring", text: `${readyText} [v${coeVersion}]`});
|
|
112
|
+
}, 5000);
|
|
113
|
+
|
|
114
|
+
// Send debug output on the node outputs: [original msg, debug info]
|
|
115
|
+
try {
|
|
116
|
+
const debugPayload = {
|
|
117
|
+
debug: {
|
|
118
|
+
node: nodeNumber,
|
|
119
|
+
dataType: dataType,
|
|
120
|
+
output: outputNumber,
|
|
121
|
+
value: queued[outputNumber] ? queued[outputNumber].value : undefined,
|
|
122
|
+
unit: queued[outputNumber] ? queued[outputNumber].unit : undefined
|
|
123
|
+
// raw: packet.toString('hex').toUpperCase()
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// If node has outputs, send original msg on first output and debug on second
|
|
127
|
+
participatingNode.send([queuedState[queueKey].origMsg || null, { payload: debugPayload }]);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Do not break sending on debug failure
|
|
130
|
+
participatingNode.warn(`Failed to send debug msg: ${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
});
|
|
175
134
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
135
|
+
delete queuedState[queueKey];
|
|
136
|
+
delete blockTimers[queueKey];
|
|
137
|
+
}
|
|
138
|
+
}, DEBOUNCE_DELAY);
|
|
180
139
|
}
|
|
181
140
|
|
|
182
141
|
module.exports = {
|
|
183
|
-
getBlockState,
|
|
184
|
-
setBlockState,
|
|
185
|
-
getBlockUnits,
|
|
186
|
-
setBlockUnits,
|
|
187
142
|
queueAndSend
|
|
188
143
|
};
|
package/lib/units.js
CHANGED
|
@@ -33,7 +33,7 @@ const UNITS = {
|
|
|
33
33
|
22: { name_de: 'Durchfluss l/min', symb_de: 'l/min',name_en: 'Flow rate l/min', symb_en: 'l/min', decimals: 0 },
|
|
34
34
|
23: { name_de: 'Druck bar', symb_de: 'bar',name_en: 'Pressure bar', symb_en: 'bar', decimals: 2 },
|
|
35
35
|
24: { name_de: 'Arbeitszahl', symb_de: '',name_en: 'COP', symb_en: '', decimals: 2 },
|
|
36
|
-
25: { name_de: 'Länge km', symb_de: 'km',name_en: 'Length km', symb_en: 'km', decimals: 0
|
|
36
|
+
25: { name_de: 'Länge km', symb_de: 'km',name_en: 'Length km', symb_en: 'km', decimals: 0},
|
|
37
37
|
26: { name_de: 'Länge m', symb_de: 'm',name_en: 'Length m', symb_en: 'm', decimals: 1 },
|
|
38
38
|
27: { name_de: 'Länge mm', symb_de: 'mm',name_en: 'Length mm', symb_en: 'mm', decimals: 1 },
|
|
39
39
|
28: { name_de: 'Kubikmeter', symb_de: 'm³',name_en: 'Cubic meters', symb_en: 'm³', decimals: 0 },
|
|
@@ -45,8 +45,8 @@ const UNITS = {
|
|
|
45
45
|
40: { name_de: 'Geschwindigkeit mm/min', symb_de: 'mm/min',name_en: 'Speed mm/min', symb_en: 'mm/min', decimals: 0 },
|
|
46
46
|
41: { name_de: 'Geschwindigkeit mm/h', symb_de: 'mm/h',name_en: 'Speed mm/h', symb_en: 'mm/h', decimals: 0 },
|
|
47
47
|
42: { name_de: 'Geschwindigkeit mm/d', symb_de: 'mm/d',name_en: 'Speed mm/d', symb_en: 'mm/d', decimals: 0 },
|
|
48
|
-
43: { name_de: '
|
|
49
|
-
44: { name_de: '
|
|
48
|
+
43: { name_de: 'Ein/Aus', symb_de: 'Aus/Ein',name_en: 'On/Off', symb_en: 'Off/On', decimals: 0, digital: true },
|
|
49
|
+
44: { name_de: 'Ja/Nein', symb_de: 'Nein/Ja',name_en: 'Yes/No', symb_en: 'No/Yes', decimals: 0, digital: true },
|
|
50
50
|
46: { name_de: 'RAS', symb_de: '°C',name_en: 'RAS', symb_en: '°C', decimals: 1 },
|
|
51
51
|
50: { name_de: 'Euro', symb_de: '€',name_en: 'Euro', symb_en: '€', decimals: 2 },
|
|
52
52
|
51: { name_de: 'Dollar', symb_de: '$',name_en: 'Dollar', symb_en: '$', decimals: 2 },
|
|
@@ -62,17 +62,17 @@ const UNITS = {
|
|
|
62
62
|
65: { name_de: 'Druck mbar', symb_de: 'mbar',name_en: 'Pressure mbar', symb_en: 'mbar', decimals: 1 },
|
|
63
63
|
66: { name_de: 'Druck Pa', symb_de: 'Pa',name_en: 'Pressure Pa', symb_en: 'Pa', decimals: 0 },
|
|
64
64
|
67: { name_de: 'CO2-Gehalt ppm', symb_de: 'ppm',name_en: 'CO2 content ppm', symb_en: 'ppm', decimals: 0 },
|
|
65
|
-
68: { name_de: '', symb_de: '',name_en: '', symb_en: '', decimals: 0 },
|
|
66
65
|
69: { name_de: 'Leistung W', symb_de: 'W',name_en: 'Power W', symb_en: 'W', decimals: 0 },
|
|
67
66
|
70: { name_de: 'Gewicht t', symb_de: 't',name_en: 'Weight t', symb_en: 't', decimals: 2 },
|
|
68
|
-
71: { name_de: 'Gewicht kg', symb_de: 'kg',name_en: 'Weight kg', symb_en: 'kg', decimals: 1
|
|
67
|
+
71: { name_de: 'Gewicht kg', symb_de: 'kg',name_en: 'Weight kg', symb_en: 'kg', decimals: 1},
|
|
69
68
|
72: { name_de: 'Gewicht g', symb_de: 'g',name_en: 'Weight g', symb_en: 'g', decimals: 1 },
|
|
70
69
|
73: { name_de: 'Länge cm', symb_de: 'cm',name_en: 'Length cm', symb_en: 'cm', decimals: 1 },
|
|
71
70
|
74: { name_de: 'Temperatur K', symb_de: 'K',name_en: 'Temperature K', symb_en: 'K', decimals: 0 },
|
|
72
71
|
75: { name_de: 'Lichtstärke', symb_de: 'lx',name_en: 'Light intensity', symb_en: 'lx', decimals: 1 },
|
|
73
72
|
76: { name_de: 'Radonkonzentration', symb_de: 'Bq/m³',name_en: 'Radon concentration', symb_en: 'Bq/m³', decimals: 0 },
|
|
74
73
|
77: { name_de: 'Preis ct/kWh', symb_de: 'ct/kWh',name_en: 'Price ct/kWh', symb_en: 'ct/kWh', decimals: 3 },
|
|
75
|
-
78: { name_de: '
|
|
74
|
+
78: { name_de: 'Offen/Geschlossen', symb_de: 'Geschlossen/Offen',name_en: 'Open/Closed', symb_en: 'Closed/Open', decimals: 0, digital: true },
|
|
75
|
+
79: { name_de: 'Konzentration ppb', symb_de: 'ppb',name_en: 'Concentration ppb', symb_en: 'ppb', decimals: 0 }
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
module.exports = UNITS;
|
|
78
|
+
module.exports = UNITS ;
|
package/lib/utils.js
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
const UNITS = require ('./units.js');
|
|
10
10
|
|
|
11
11
|
// Utilities for unit conversion
|
|
12
|
-
function
|
|
13
|
-
const unitDecimals = getUnitDecimals(unitId,
|
|
12
|
+
function convertRawToValue(rawValue, unitId, coeVersion) {
|
|
13
|
+
const unitDecimals = getUnitDecimals(unitId, coeVersion);
|
|
14
14
|
return rawValue / Math.pow(10, unitDecimals);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function
|
|
18
|
-
const unitDecimals = getUnitDecimals(unitId,
|
|
17
|
+
function convertValueToRaw(value, unitId, coeVersion) {
|
|
18
|
+
const unitDecimals = getUnitDecimals(unitId, coeVersion);
|
|
19
19
|
return Math.round(value * Math.pow(10, unitDecimals));
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -25,11 +25,11 @@ function getUnitInfo(unitId, langCode) {
|
|
|
25
25
|
const idKey = String(unitId);
|
|
26
26
|
|
|
27
27
|
// Use UNITS from units.js as base
|
|
28
|
-
if (UNITS && UNITS[idKey]) {
|
|
28
|
+
if (UNITS && UNITS[idKey]) {
|
|
29
29
|
const unit = UNITS[idKey];
|
|
30
30
|
return {
|
|
31
31
|
name: useGerman ? unit.name_de : unit.name_en,
|
|
32
|
-
symbol: useGerman ? unit.symb_de : unit.symb_en
|
|
32
|
+
symbol: unit.digital ? '' : (useGerman ? unit.symb_de : unit.symb_en) // No symbol for digital types
|
|
33
33
|
};
|
|
34
34
|
} else {
|
|
35
35
|
return {
|
|
@@ -39,6 +39,19 @@ function getUnitInfo(unitId, langCode) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Generate digital state key based on unit and value
|
|
43
|
+
function getDigitalStateKey(unit, value, prefix = "coe.status.") {
|
|
44
|
+
const states = {
|
|
45
|
+
43: value ? "on" : "off",
|
|
46
|
+
44: value ? "yes" : "no",
|
|
47
|
+
78: value ? "open" : "closed",
|
|
48
|
+
default: value ? "on" : "off"
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const state = states[unit] || states.default;
|
|
52
|
+
return `${prefix}${state}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
// Retrieve unit decimals from unit list
|
|
43
56
|
function getUnitDecimals(unitId, protocolVersion) {
|
|
44
57
|
let unitDecimals;
|
|
@@ -74,90 +87,82 @@ function getUnitLanguage(lang) {
|
|
|
74
87
|
|
|
75
88
|
// Translate output number to block position (CoE V1)
|
|
76
89
|
function getBlockInfo(dataType, outputNumber) {
|
|
90
|
+
let blockNumber, position, dType;
|
|
77
91
|
outputNumber = parseInt(outputNumber);
|
|
78
|
-
if (isNaN(outputNumber) || outputNumber < 1) {
|
|
79
|
-
// Default to block 1 position 0
|
|
80
|
-
return { block: 1, position: 0 };
|
|
81
|
-
}
|
|
82
92
|
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return { block: block, position: position };
|
|
93
|
+
if (isNaN(outputNumber) || outputNumber < 1) { // Default to block 1 position 0
|
|
94
|
+
blockNumber = 1;
|
|
95
|
+
position = 0;
|
|
96
|
+
dType = 'a';
|
|
88
97
|
} else {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
if (dataType === 'analog') {
|
|
99
|
+
// Analog: Outputs 1..32 → Blocks 1..8 (4 Outputs each)
|
|
100
|
+
blockNumber = Math.floor((outputNumber - 1) / 4) + 1; // 1..8
|
|
101
|
+
position = (outputNumber - 1) % 4; // 0..3
|
|
102
|
+
dType = 'a';
|
|
92
103
|
} else {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const numValues = isDigital ? 16 : 4;
|
|
102
|
-
|
|
103
|
-
const updatedBlock = { // Initialize the updated block (copy)
|
|
104
|
-
...currentState,
|
|
105
|
-
values: new Array(numValues).fill(undefined),
|
|
106
|
-
units: isDigital ? null : new Array(numValues).fill(undefined)
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const oldValues = currentState.values || [];
|
|
110
|
-
const oldUnits = currentState.units || [];
|
|
111
|
-
|
|
112
|
-
for (let i = 0; i < numValues; i++) { // Copy the old LKGV (Last Known Good Values)
|
|
113
|
-
if (oldValues[i] !== undefined) {
|
|
114
|
-
updatedBlock.values[i] = oldValues[i];
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (!isDigital && updatedBlock.units && oldUnits) { // Copy old units
|
|
119
|
-
for (let i = 0; i < 4; i++) {
|
|
120
|
-
if (oldUnits[i] !== undefined) {
|
|
121
|
-
updatedBlock.units[i] = oldUnits[i];
|
|
104
|
+
dType = 'd';
|
|
105
|
+
// Digital: Outputs 1..16 → Block 0, 17..32 → Block 9
|
|
106
|
+
if (outputNumber <= 16) {
|
|
107
|
+
blockNumber = 0;
|
|
108
|
+
position = outputNumber - 1; // 0..1
|
|
109
|
+
} else {
|
|
110
|
+
blockNumber = 9;
|
|
111
|
+
position = outputNumber - 17; // 0..15
|
|
122
112
|
}
|
|
123
113
|
}
|
|
124
114
|
}
|
|
115
|
+
return { number: blockNumber, position: position};
|
|
116
|
+
}
|
|
125
117
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
118
|
+
// Translate block number to output arrays (CoE V1)
|
|
119
|
+
function getOutputsfromBlock(blockNumber, dataType) {
|
|
120
|
+
let start, length;
|
|
121
|
+
if (dataType === 'analog') {
|
|
122
|
+
length = 4;
|
|
123
|
+
start = ((blockNumber - 1) * 4) + 1;
|
|
130
124
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
else { // digital
|
|
126
|
+
length = 16;
|
|
127
|
+
if (blockNumber === 0) {
|
|
128
|
+
start = 1;
|
|
129
|
+
} else { // block 9
|
|
130
|
+
start = 17;
|
|
137
131
|
}
|
|
138
132
|
}
|
|
133
|
+
return Array.from({ length }, (_, i) => start + i);
|
|
134
|
+
}
|
|
139
135
|
|
|
140
|
-
|
|
136
|
+
// Merge incoming (V2) node data with LKGV (Last Known Good Values)
|
|
137
|
+
function mergeNodeData(currentState, newNode) {
|
|
138
|
+
return {
|
|
139
|
+
...currentState,
|
|
140
|
+
...newNode,
|
|
141
|
+
outputs: {
|
|
142
|
+
...currentState.outputs,
|
|
143
|
+
...newNode.outputs
|
|
144
|
+
}
|
|
145
|
+
};
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
// Create empty block state (incoming block)
|
|
144
149
|
function createEmptyState(incomingBlock) {
|
|
145
|
-
const isDigital = incomingBlock.dataType === 'digital'
|
|
146
|
-
|
|
150
|
+
const isDigital = incomingBlock.dataType === 'digital';
|
|
151
|
+
|
|
147
152
|
return {
|
|
148
153
|
nodeNumber: incomingBlock.nodeNumber,
|
|
149
|
-
blockNumber: incomingBlock.blockNumber,
|
|
150
154
|
dataType: isDigital ? 'digital' : 'analog',
|
|
151
|
-
|
|
152
|
-
units: isDigital ? null : new Array(numValues).fill(undefined),
|
|
155
|
+
outputs: {}
|
|
153
156
|
};
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
module.exports = {
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
convertRawToValue,
|
|
161
|
+
convertValueToRaw,
|
|
159
162
|
getUnitInfo,
|
|
163
|
+
getDigitalStateKey,
|
|
160
164
|
getBlockInfo,
|
|
161
|
-
|
|
165
|
+
getOutputsfromBlock,
|
|
166
|
+
mergeNodeData,
|
|
162
167
|
createEmptyState
|
|
163
168
|
};
|