node-red-contrib-ta-cmi-coe 1.0.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 +23 -0
- package/LICENSE +201 -0
- package/README.md +277 -0
- package/__tests__/blockinfo.test.js +24 -0
- package/__tests__/conversion.test.js +22 -0
- package/__tests__/udp.test.js +46 -0
- package/coe/coe-input.html +61 -0
- package/coe/coe-input.js +144 -0
- package/coe/coe-monitor.html +60 -0
- package/coe/coe-monitor.js +138 -0
- package/coe/coe-output.html +99 -0
- package/coe/coe-output.js +68 -0
- package/coe/config.html +52 -0
- package/coe/config.js +101 -0
- package/coe/locales/de/coe-input.html +10 -0
- package/coe/locales/de/coe-input.json +19 -0
- package/coe/locales/de/coe-monitor.html +10 -0
- package/coe/locales/de/coe-monitor.json +20 -0
- package/coe/locales/de/coe-output.html +14 -0
- package/coe/locales/de/coe-output.json +18 -0
- package/coe/locales/de/config.html +13 -0
- package/coe/locales/de/config.json +10 -0
- package/coe/locales/en-US/coe-input.html +10 -0
- package/coe/locales/en-US/coe-input.json +19 -0
- package/coe/locales/en-US/coe-monitor.html +10 -0
- package/coe/locales/en-US/coe-monitor.json +20 -0
- package/coe/locales/en-US/coe-output.html +14 -0
- package/coe/locales/en-US/coe-output.json +18 -0
- package/coe/locales/en-US/config.html +13 -0
- package/coe/locales/en-US/config.json +10 -0
- package/coe/units-config.js +36 -0
- package/lib/coe-v2.js +189 -0
- package/lib/coe.js +109 -0
- package/lib/queueing.js +188 -0
- package/lib/units.js +78 -0
- package/lib/utils.js +163 -0
- package/package.json +39 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"coe-input": {
|
|
3
|
+
"label": {
|
|
4
|
+
"name": "Name",
|
|
5
|
+
"cmiconfig": "CMI Konfig",
|
|
6
|
+
"nodeNumber": "Knoten",
|
|
7
|
+
"outputNumber": "Eingang",
|
|
8
|
+
"dataType": "Datentyp",
|
|
9
|
+
"timeout": "Timeout (Min)"
|
|
10
|
+
},
|
|
11
|
+
"placeholder": {
|
|
12
|
+
"nodeNumber": "1-62, 0 = beliebig"
|
|
13
|
+
},
|
|
14
|
+
"status": {
|
|
15
|
+
"waiting": "wartet",
|
|
16
|
+
"waitingAny": "wartet (alle Knoten)"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="coe-monitor">
|
|
2
|
+
<p>Empfängt und überwacht CoE (CAN over Ethernet) Pakete von allen Quellen.</p>
|
|
3
|
+
<p>Ideal für Debugging, Logging und Überwachung des gesamten CoE-Verkehrs.</p>
|
|
4
|
+
<h3>Konfiguration</h3>
|
|
5
|
+
<ul>
|
|
6
|
+
<li>Optional: Filtere nur Pakete von einem bestimmten CAN-Knoten (0 = alle)</li>
|
|
7
|
+
<li>Filter Typ": "Filtere nach Datentyp: Alle, nur Analog oder nur Digital</li>
|
|
8
|
+
<li>Rohdaten einschließen": "Füge vollständige Rohdaten in msg.payload.raw hinzu</li>
|
|
9
|
+
</ul>
|
|
10
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"coe-monitor": {
|
|
3
|
+
"label": {
|
|
4
|
+
"name": "Name",
|
|
5
|
+
"cmiconfig": "CMI Konfig",
|
|
6
|
+
"filterNodeNumber": "Filter Knoten",
|
|
7
|
+
"filterDataType": "Filter Typ",
|
|
8
|
+
"includeRaw": "Rohdaten einschließen",
|
|
9
|
+
"all": "Alle"
|
|
10
|
+
},
|
|
11
|
+
"placeholder": {
|
|
12
|
+
"filterNodeNumber": "0 = alle"
|
|
13
|
+
},
|
|
14
|
+
"status": {
|
|
15
|
+
"monitoring": "Lauscht...",
|
|
16
|
+
"idle": "Inaktiv",
|
|
17
|
+
"node": "Knoten"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="coe-output">
|
|
2
|
+
<p>Sendet einzelne Werte an eine TA CMI via CoE.</p>
|
|
3
|
+
<h3>Konfiguration</h3>
|
|
4
|
+
<ul>
|
|
5
|
+
<li><b>Knoten:</b> CAN Knoten-Nummer (1-62)</li>
|
|
6
|
+
<li><b>Eingang:</b> Nummer des Netzwerkausgangs (Analog 1-32, Digital 1-16)</li>
|
|
7
|
+
<li><b>Datentyp:</b> Analog (numerisch) oder Digital (boolean)</li>
|
|
8
|
+
<li><b>Einheit:</b> Messeinheit (nur für analog)</li>
|
|
9
|
+
</ul>
|
|
10
|
+
<h3>Ausgänge</h3>
|
|
11
|
+
<p><b>2 Ausgänge:</b> [0] Durchgeleitete Nachricht, [1] Debug-Info (Hex, Block-State, Units)</p>
|
|
12
|
+
<h3>Hinweise</h3>
|
|
13
|
+
<p>Werte werden 50ms gepuffert um die Nachrichtenfrequenz zu reduzieren</p>
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"coe-output": {
|
|
3
|
+
"label": {
|
|
4
|
+
"name": "Name",
|
|
5
|
+
"cmiconfig": "CMI Konfig",
|
|
6
|
+
"nodeNumber": "Knoten",
|
|
7
|
+
"outputNumber": "Ausgang",
|
|
8
|
+
"dataType": "Datentyp",
|
|
9
|
+
"unit": "Einheit"
|
|
10
|
+
},
|
|
11
|
+
"status": {
|
|
12
|
+
"noconfig": "Nicht konfiguriert",
|
|
13
|
+
"queued": "wartet",
|
|
14
|
+
"ready": "bereit",
|
|
15
|
+
"merged": "gesendet (Paket)"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="cmiconfig">
|
|
2
|
+
<p><strong>Konfiguration:</strong></p>
|
|
3
|
+
<ul>
|
|
4
|
+
<li><b> Lokale IP:</b> IP Adressbereich in dem der UDP Port empfängt (Übliche Werte sind 0.0.0.0 oder 127.0.0.1)</li>
|
|
5
|
+
<li><b> Adresse:</b> Für das Senden von Nachrichten verwendete IP Adresse des TA C.M.I</li>
|
|
6
|
+
</ul>
|
|
7
|
+
<p><strong>CoE-Version:</strong></p>
|
|
8
|
+
<ul>
|
|
9
|
+
<li><b>V1:</b> Standard-Einstellung. Wertebereich: ±32.767</li>
|
|
10
|
+
<li><b>V2:</b> Erweitert, benötigt CMI FW ≥v1.43.1. Wertebereich: ±2.147.483.647</li>
|
|
11
|
+
</ul>
|
|
12
|
+
<p>Siehe Einstellungen > CAN im Menü des CMI</p>
|
|
13
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="coe-input">
|
|
2
|
+
<p>Receives values from a TA CMI via CoE (CAN over Ethernet).</p>
|
|
3
|
+
<h3>Configuration</h3>
|
|
4
|
+
<ul>
|
|
5
|
+
<li><b>Node:</b> CAN node number (1-62, 0 = receive from any node, not recommended for production)</li>
|
|
6
|
+
<li><b>Input:</b> Network input number (1-32)</li>
|
|
7
|
+
<li><b>Data Type:</b> Analog (1-32) or Digital (1-32)</li>
|
|
8
|
+
<li><b>Timeout:</b> CAN-Bus timeout value in Minutes</li>
|
|
9
|
+
</ul>
|
|
10
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"coe-input": {
|
|
3
|
+
"label": {
|
|
4
|
+
"name": "Name",
|
|
5
|
+
"cmiconfig": "CMI Config",
|
|
6
|
+
"nodeNumber": "Node",
|
|
7
|
+
"outputNumber": "Input",
|
|
8
|
+
"dataType": "Data Type",
|
|
9
|
+
"timeout": "Timeout (Min)"
|
|
10
|
+
},
|
|
11
|
+
"placeholder": {
|
|
12
|
+
"nodeNumber": "1-62, 0 = any"
|
|
13
|
+
},
|
|
14
|
+
"status": {
|
|
15
|
+
"waiting": "waiting",
|
|
16
|
+
"waitingAny": "waiting (any node)"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="coe-monitor">
|
|
2
|
+
<p>Receives and monitors CoE (CAN over Ethernet) packets from all sources.</p>
|
|
3
|
+
<p>Ideal for debugging, logging, and monitoring total CoE traffic.</p>
|
|
4
|
+
<h3>Configuration</h3>
|
|
5
|
+
<ul>
|
|
6
|
+
<li>Optional: Filter packets from a specific CAN node only (0 = all)</li>
|
|
7
|
+
<li>Filter Type: Filter by data type: All, Analog only, or Digital only</li>
|
|
8
|
+
<li>Include Raw: Include complete raw data in msg.payload.raw</li>
|
|
9
|
+
</ul>
|
|
10
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"coe-monitor": {
|
|
3
|
+
"label": {
|
|
4
|
+
"name": "Name",
|
|
5
|
+
"cmiconfig": "CMI Config",
|
|
6
|
+
"filterNodeNumber": "Filter Node",
|
|
7
|
+
"filterDataType": "Filter Type",
|
|
8
|
+
"includeRaw": "Include Raw Data",
|
|
9
|
+
"all": "All"
|
|
10
|
+
},
|
|
11
|
+
"placeholder": {
|
|
12
|
+
"filterNodeNumber": "0 = all"
|
|
13
|
+
},
|
|
14
|
+
"status": {
|
|
15
|
+
"monitoring": "Monitoring...",
|
|
16
|
+
"idle": "Idle",
|
|
17
|
+
"node": "Node"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="coe-output">
|
|
2
|
+
<p>Sends single values to a TA CMI via CoE.</p>
|
|
3
|
+
<h3>Configuration</h3>
|
|
4
|
+
<ul>
|
|
5
|
+
<li><b>Node:</b> CAN node number (1-62)</li>
|
|
6
|
+
<li><b>Input:</b> Network output number (Analog 1-32, Digital 1-16)</li>
|
|
7
|
+
<li><b>Data Type:</b> Analog (numeric) or Digital (boolean)</li>
|
|
8
|
+
<li><b>Unit:</b> Measurement unit (analog only)</li>
|
|
9
|
+
</ul>
|
|
10
|
+
<h3>Outputs</h3>
|
|
11
|
+
<p><b>2 outputs:</b> [0] Message passthrough, [1] Debug info (Hex, Block-State, Units)</p>
|
|
12
|
+
<h3>Notes</h3>
|
|
13
|
+
<p>Values are buffered for 50ms to reduce message frequency.</p>
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"coe-output": {
|
|
3
|
+
"label": {
|
|
4
|
+
"name": "Name",
|
|
5
|
+
"cmiconfig": "CMI Config",
|
|
6
|
+
"nodeNumber": "Node",
|
|
7
|
+
"outputNumber": "Output",
|
|
8
|
+
"dataType": "Data Type",
|
|
9
|
+
"unit": "Unit"
|
|
10
|
+
},
|
|
11
|
+
"status": {
|
|
12
|
+
"noconfig": "No config",
|
|
13
|
+
"queued": "queued",
|
|
14
|
+
"ready": "ready",
|
|
15
|
+
"merged": "sent (merged)"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="cmiconfig">
|
|
2
|
+
<p><strong>Configuration:</strong></p>
|
|
3
|
+
<ul>
|
|
4
|
+
<li><b> Local IP:</b> IP Address range the UDP Port is listening on (Common values are 0.0.0.0 or 127.0.0.1)</li>
|
|
5
|
+
<li><b> Address:</b> IP Address of TA C.M.I used for sending CoE messages</li>
|
|
6
|
+
</ul>
|
|
7
|
+
<p><strong>CoE Version:</strong></p>
|
|
8
|
+
<ul>
|
|
9
|
+
<li><b>V1:</b> Default setting. Value range: ±32,767</li>
|
|
10
|
+
<li><b>V2:</b> Extended, requires CMI FW ≥v1.43.1. Value range: ±2,147,483,647</li>
|
|
11
|
+
</ul>
|
|
12
|
+
<p>See Settings > CAN in the CMI menu.</p>
|
|
13
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Endpoint for Units
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2025 Florian Mayrhofer
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = function(RED) {
|
|
10
|
+
|
|
11
|
+
const UNITS = require ('../lib/units.js');
|
|
12
|
+
|
|
13
|
+
function UnitConfigNode(config) {
|
|
14
|
+
RED.nodes.createNode(this, config);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
RED.nodes.registerType("unit-config", UnitConfigNode);
|
|
18
|
+
|
|
19
|
+
RED.httpAdmin.get('/ta-cmi-coe/units/:lang?', function(req, res) {
|
|
20
|
+
const lang = req.params.lang && req.params.lang.toLowerCase().startsWith("de") ? "de" : "en";
|
|
21
|
+
|
|
22
|
+
const localizedUnits = {};
|
|
23
|
+
|
|
24
|
+
Object.keys(UNITS).forEach(key => {
|
|
25
|
+
const unit = UNITS[key];
|
|
26
|
+
localizedUnits[key] = {
|
|
27
|
+
name: lang === "de" ? unit.name_de : unit.name_en,
|
|
28
|
+
symb: lang === "de" ? unit.symb_de : unit.symb_en,
|
|
29
|
+
decimals: unit.decimals
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
res.json(localizedUnits);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
};
|
package/lib/coe-v2.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
const { convertCoEToValue, convertValueToCoE } = require('./utils.js');
|
|
10
|
+
|
|
11
|
+
// CoE V2 Parsing function
|
|
12
|
+
function parseCoEV2Packet(buffer) {
|
|
13
|
+
if (buffer.length < 4) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Parse header
|
|
18
|
+
const versionLow = buffer.readUInt8(0);
|
|
19
|
+
const versionHigh = buffer.readUInt8(1);
|
|
20
|
+
const messageLength = buffer.readUInt8(2);
|
|
21
|
+
const blockCount = buffer.readUInt8(3);
|
|
22
|
+
|
|
23
|
+
if (versionLow !== 0x02 || versionHigh !== 0x00) { // Validate version
|
|
24
|
+
console.warn(`V2: Ungültige Version: ${versionLow}.${versionHigh}`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const expectedLength = 4 + (blockCount * 8);
|
|
29
|
+
if (buffer.length < expectedLength) {
|
|
30
|
+
console.warn(`V2: Packet incomplete. Expected: ${expectedLength}, Received: ${buffer.length}`);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Parse value blocks
|
|
35
|
+
const blocks = [];
|
|
36
|
+
for (let i = 0; i < blockCount; i++) {
|
|
37
|
+
const offset = 4 + (i * 8);
|
|
38
|
+
const canNode = buffer.readUInt8(offset);
|
|
39
|
+
const outputNumber = buffer.readUInt16LE(offset + 1);
|
|
40
|
+
const unitId = buffer.readUInt8(offset + 3);
|
|
41
|
+
const value = buffer.readInt32LE(offset + 4);
|
|
42
|
+
|
|
43
|
+
blocks.push({
|
|
44
|
+
canNode: canNode,
|
|
45
|
+
outputNumber: outputNumber,
|
|
46
|
+
unitId: unitId,
|
|
47
|
+
value: value,
|
|
48
|
+
isDigital: outputNumber <= 254,
|
|
49
|
+
isAnalog: outputNumber > 254
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
version: 2,
|
|
55
|
+
messageLength: messageLength,
|
|
56
|
+
blockCount: blockCount,
|
|
57
|
+
blocks: blocks
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create CoE V2 Packet
|
|
62
|
+
function createCoEV2Packet(canNode, outputs) {
|
|
63
|
+
// Outputs: Array von {outputNumber, unitId, value}
|
|
64
|
+
// Max 16 value blocks
|
|
65
|
+
const blockCount = Math.min(outputs.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 = outputs[i];
|
|
80
|
+
|
|
81
|
+
buffer.writeUInt8(canNode, offset); // CAN Node
|
|
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
|
+
|
|
91
|
+
return buffer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Convert V2 Data into V1 (for compatibility)
|
|
95
|
+
function convertV2ToLegacyFormat(v2Data) {
|
|
96
|
+
// Group Outputs by block
|
|
97
|
+
const blockMap = {};
|
|
98
|
+
|
|
99
|
+
v2Data.blocks.forEach(block => {
|
|
100
|
+
const isDigital = block.outputNumber <= 254;
|
|
101
|
+
const actualOutput = isDigital ? block.outputNumber : (block.outputNumber - 255);
|
|
102
|
+
|
|
103
|
+
// Determine block number and position
|
|
104
|
+
let blockNumber, position;
|
|
105
|
+
|
|
106
|
+
if (isDigital) {
|
|
107
|
+
// Digital: Output 1-16 → Block 0, Output 17-32 → Block 9
|
|
108
|
+
if (actualOutput <= 16) {
|
|
109
|
+
blockNumber = 0;
|
|
110
|
+
position = actualOutput - 1;
|
|
111
|
+
} else {
|
|
112
|
+
blockNumber = 9;
|
|
113
|
+
position = actualOutput - 17;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
// Analog: Output 1-4 → Block 1, 5-8 → Block 2, etc.
|
|
117
|
+
blockNumber = Math.floor((actualOutput - 1) / 4) + 1;
|
|
118
|
+
position = (actualOutput - 1) % 4;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const key = `${block.canNode}-${blockNumber}`;
|
|
122
|
+
|
|
123
|
+
if (!blockMap[key]) {
|
|
124
|
+
blockMap[key] = {
|
|
125
|
+
nodeNumber: block.canNode,
|
|
126
|
+
blockNumber: blockNumber,
|
|
127
|
+
dataType: isDigital ? 'digital' : 'analog',
|
|
128
|
+
values: isDigital ? new Array(16).fill(undefined) : new Array(4).fill(undefined),
|
|
129
|
+
units: isDigital ? null : new Array(4).fill(undefined)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Convert value & insert (V2 uses other decimals)
|
|
134
|
+
const convertedValue = convertCoEToValue(block.value, block.unitId, 2);
|
|
135
|
+
blockMap[key].values[position] = isDigital ? (block.value ? 1 : 0) : convertedValue;
|
|
136
|
+
|
|
137
|
+
if (!isDigital && blockMap[key].units) {
|
|
138
|
+
blockMap[key].units[position] = block.unitId;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return Object.values(blockMap);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Convert V1 Format to V2 Outputs
|
|
146
|
+
function convertLegacyToV2Format(nodeNumber, blockNumber, values, units, dataType) {
|
|
147
|
+
const outputs = [];
|
|
148
|
+
|
|
149
|
+
if (dataType === 'digital') {
|
|
150
|
+
// Digital: 16 Bits
|
|
151
|
+
const baseOutput = blockNumber === 0 ? 1 : 17;
|
|
152
|
+
for (let i = 0; i < values.length; i++) {
|
|
153
|
+
if (values[i] !== undefined) {
|
|
154
|
+
outputs.push({
|
|
155
|
+
outputNumber: baseOutput + i,
|
|
156
|
+
unitId: 0,
|
|
157
|
+
value: values[i] ? 1 : 0
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
// Analog: 4 Values
|
|
163
|
+
const baseOutput = (blockNumber - 1) * 4 + 1;
|
|
164
|
+
for (let i = 0; i < 4; i++) {
|
|
165
|
+
if (values[i] !== undefined) {
|
|
166
|
+
const unitId = units ? units[i] : 0;
|
|
167
|
+
const rawValue = convertValueToCoE(values[i], unitId, 2); // V2 uses other decimals
|
|
168
|
+
|
|
169
|
+
// Output > 255 = analog
|
|
170
|
+
const outputNumber = baseOutput + i + 255;
|
|
171
|
+
|
|
172
|
+
outputs.push({
|
|
173
|
+
outputNumber: outputNumber,
|
|
174
|
+
unitId: unitId,
|
|
175
|
+
value: rawValue
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return outputs;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
parseCoEV2Packet,
|
|
186
|
+
createCoEV2Packet,
|
|
187
|
+
convertV2ToLegacyFormat,
|
|
188
|
+
convertLegacyToV2Format
|
|
189
|
+
};
|
package/lib/coe.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
const { parseCoEV2Packet, createCoEV2Packet, convertV2ToLegacyFormat, convertLegacyToV2Format } = require('./coe-v2');
|
|
10
|
+
const { convertCoEToValue, convertValueToCoE } = require('./utils');
|
|
11
|
+
|
|
12
|
+
// Parse CoE packet from buffer
|
|
13
|
+
function parseCoEPacket(buffer, version) {
|
|
14
|
+
|
|
15
|
+
// If V2 Protokoll is used
|
|
16
|
+
if (version === 2) {
|
|
17
|
+
const v2Data = parseCoEV2Packet(buffer);
|
|
18
|
+
if (!v2Data) {
|
|
19
|
+
console.warn('V2: Unable to parse packet.');
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Convert to legacy format for further processing
|
|
24
|
+
const legacyBlocks = convertV2ToLegacyFormat(v2Data);
|
|
25
|
+
|
|
26
|
+
// Return all blocks (can be multiple)
|
|
27
|
+
return legacyBlocks;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const nodeNumber = buffer.readUInt8(0);
|
|
31
|
+
const blockNumber = buffer.readUInt8(1);
|
|
32
|
+
|
|
33
|
+
let values = [];
|
|
34
|
+
let units = [];
|
|
35
|
+
|
|
36
|
+
if (blockNumber === 0 || blockNumber === 9) {
|
|
37
|
+
// digital
|
|
38
|
+
const bitField = buffer.readUInt16LE(2);
|
|
39
|
+
const unitId = buffer.readUInt8(11); // Block-wise unit id → byte 11
|
|
40
|
+
for (let i = 0; i < 16; i++) {
|
|
41
|
+
values.push((bitField >> i) & 1);
|
|
42
|
+
units.push(unitId);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
// analog V1
|
|
46
|
+
for (let i = 0; i < 4; i++) {
|
|
47
|
+
const value = buffer.readInt16LE(2 + i * 2);
|
|
48
|
+
const unitId = buffer.readUInt8(10 + i);
|
|
49
|
+
|
|
50
|
+
const convertedValue = convertCoEToValue(value, unitId, 1); // V1 decimals
|
|
51
|
+
values.push(convertedValue);
|
|
52
|
+
units.push(unitId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [{
|
|
57
|
+
nodeNumber: nodeNumber,
|
|
58
|
+
blockNumber: blockNumber,
|
|
59
|
+
values: values,
|
|
60
|
+
units: units
|
|
61
|
+
}];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create CoE Packet from values
|
|
65
|
+
function createCoEPacket(nodeNumber, blockNumber, values, units, dataType, version) {
|
|
66
|
+
// If V2 Protokoll is used
|
|
67
|
+
if (version === 2) {
|
|
68
|
+
const outputs = convertLegacyToV2Format(nodeNumber, blockNumber, values, units, dataType);
|
|
69
|
+
return createCoEV2Packet(nodeNumber, outputs);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let buffer;
|
|
73
|
+
|
|
74
|
+
if (dataType === 'digital') {
|
|
75
|
+
buffer = Buffer.alloc(14);
|
|
76
|
+
buffer.writeUInt8(nodeNumber, 0);
|
|
77
|
+
buffer.writeUInt8(blockNumber, 1);
|
|
78
|
+
|
|
79
|
+
let bitField = 0;
|
|
80
|
+
for (let i = 0; i < 16; i++) {
|
|
81
|
+
if (values[i]) {
|
|
82
|
+
bitField |= (1 << i);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
buffer.writeUInt16LE(bitField, 2);
|
|
86
|
+
buffer.fill(0, 4, buffer.length);
|
|
87
|
+
|
|
88
|
+
} else { // analog
|
|
89
|
+
buffer = Buffer.alloc(14);
|
|
90
|
+
buffer.writeUInt8(nodeNumber, 0);
|
|
91
|
+
buffer.writeUInt8(blockNumber, 1);
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < 4; i++) {
|
|
94
|
+
const unitId = units ? units[i] : 0;
|
|
95
|
+
const rawValue = convertValueToCoE(values[i], unitId, 1); // V1 Decimal places
|
|
96
|
+
|
|
97
|
+
if (rawValue > 32767 || rawValue < -32768) {
|
|
98
|
+
console.warn(`Value ${values[i]} exceeds V1 limits. Consider using V2.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
buffer.writeInt16LE(Math.max(-32768, Math.min(32767, rawValue)), 2 + i * 2);
|
|
102
|
+
buffer.writeUInt8(unitId, 10 + i);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return buffer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { parseCoEPacket, createCoEPacket};
|