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.
@@ -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
+ {
2
+ "config": {
3
+ "label": {
4
+ "name": "Name",
5
+ "localip": "Lokale IP",
6
+ "address": "CMI Adresse",
7
+ "coeVersion": "CoE Version"
8
+ }
9
+ }
10
+ }
@@ -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,10 @@
1
+ {
2
+ "config": {
3
+ "label": {
4
+ "name": "Name",
5
+ "localip": "Local IP",
6
+ "address": "CMI Address",
7
+ "coeVersion": "CoE Version"
8
+ }
9
+ }
10
+ }
@@ -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};