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,144 @@
1
+ /**
2
+ * CoE Input Node
3
+ *
4
+ * Copyright 2025 Florian Mayrhofer
5
+ * Licensed under the Apache License, Version 2.0
6
+ *
7
+ */
8
+
9
+ module.exports = function(RED) {
10
+ 'use strict';
11
+ const { getBlockInfo, getUnitInfo, mergeBlockData, createEmptyState } = require('../lib/utils');
12
+
13
+ // CoE Input Node (receiving values)
14
+ function CoEInputNode(config) {
15
+ RED.nodes.createNode(this, config);
16
+ const node = this;
17
+
18
+ node.cmiConfig = RED.nodes.getNode(config.cmiconfig);
19
+
20
+ if (!node.cmiConfig) {
21
+ node.error("CMI Configuration missing");
22
+ return;
23
+ }
24
+
25
+ node.cmiAddress = node.cmiConfig.address;
26
+ node.coeVersion = node.cmiConfig.coeVersion || 1;
27
+ node.lang = node.cmiConfig.lang;
28
+ node.nodeNumber = parseInt(config.nodeNumber) || 0;
29
+ node.outputNumber = parseInt(config.outputNumber) || 1;
30
+ node.dataType = config.dataType || 'analog';
31
+
32
+ // State management for LKGVs (Last Known Good Values) per block
33
+ node.blockState = {};
34
+
35
+ // Calculate block & position
36
+ const blockInfo = getBlockInfo(node.dataType, node.outputNumber);
37
+
38
+ // Set Timer for CoE timeout
39
+ const timeoutMs = (config.timeout || 20) * 60 * 1000; // Timeout in milliseconds
40
+ let timeoutTimer = null;
41
+ let currentNodeText = "";
42
+
43
+ // Listener for incoming data
44
+ const listener = (data) => {
45
+ // Data is now: { blocks, sourceIP, version, timestamp }
46
+ if (!data || !data.blocks || !Array.isArray(data.blocks)) {
47
+ node.warn('Received invalid data format');
48
+ return;
49
+ }
50
+
51
+ for (let incomingBlock of data.blocks) {
52
+ if (!incomingBlock) continue;
53
+
54
+ const blockKey = `${incomingBlock.nodeNumber}-${incomingBlock.blockNumber}`;
55
+
56
+ // Filter Node number (if > 0)
57
+ if (node.nodeNumber > 0 && incomingBlock.nodeNumber !== node.nodeNumber) {
58
+ continue;
59
+ }
60
+
61
+ // Filter Block number
62
+ if (incomingBlock.blockNumber !== blockInfo.block) {
63
+ continue;
64
+ }
65
+
66
+ // Merge blocks
67
+ let currentState = node.blockState[blockKey];
68
+ if (!currentState) {
69
+ currentState = createEmptyState(incomingBlock);
70
+ }
71
+
72
+ const mergedBlock = mergeBlockData(currentState, incomingBlock);
73
+ node.blockState[blockKey] = mergedBlock;
74
+
75
+ // Extract Values from merged block
76
+ let value, unit;
77
+ if (node.dataType === 'analog') {
78
+ value = mergedBlock.values[blockInfo.position];
79
+ unit = mergedBlock.units ? mergedBlock.units[blockInfo.position] : null;
80
+ } else {
81
+ value = mergedBlock.values[blockInfo.position] ? true : false;
82
+ unit = null;
83
+ }
84
+
85
+ // Build message
86
+ const unitInfo = getUnitInfo(unit, node.lang);
87
+ const msg = {
88
+ payload: value,
89
+ topic: `coe/${node.nodeNumber || mergedBlock.nodeNumber}/${node.dataType}/${node.outputNumber}`,
90
+ coe: {
91
+ nodeNumber: mergedBlock.nodeNumber,
92
+ blockNumber: mergedBlock.blockNumber,
93
+ outputNumber: node.outputNumber,
94
+ dataType: node.dataType,
95
+ version: data.version,
96
+ unit: unit,
97
+ unitName: unitInfo.name,
98
+ unitSymbol: unitInfo.symbol,
99
+ sourceIP: data.sourceIP,
100
+ timestamp: data.timestamp,
101
+ raw: mergedBlock
102
+ }
103
+ };
104
+
105
+ if (value !== undefined) { // Send only, if value is defined in block
106
+ node.send(msg);
107
+ }
108
+
109
+ currentNodeText = `${value} ${unitInfo.symbol || ''} [v${node.coeVersion}]` // Caching last Node text
110
+
111
+ node.status({
112
+ fill: "green",
113
+ shape: "dot",
114
+ text: currentNodeText
115
+ });
116
+
117
+ resetTimeout();
118
+ }
119
+ };
120
+
121
+ node.cmiConfig.registerListener(listener);
122
+
123
+ // Reset CoE Timeout
124
+ function resetTimeout() {
125
+ if (timeoutTimer) clearTimeout(timeoutTimer);
126
+ timeoutTimer = setTimeout(() => {
127
+ node.status({ fill: "red", shape: "dot", text: `${currentNodeText} (Timeout)` });
128
+ }, timeoutMs);
129
+ }
130
+
131
+ // Status information, including if filtered
132
+ if (node.nodeNumber === 0) {
133
+ node.status({fill: "yellow", shape: "ring", text: "coe-input.status.waitingAny"});
134
+ } else {
135
+ node.status({fill: "grey", shape: "ring", text: "coe-input.status.waiting"});
136
+ }
137
+
138
+ node.on('close', function() {
139
+ node.cmiConfig.unregisterListener(listener);
140
+ });
141
+ }
142
+
143
+ RED.nodes.registerType("coe-input", CoEInputNode);
144
+ };
@@ -0,0 +1,60 @@
1
+ <!--
2
+ CoE Monitor Node
3
+
4
+ Copyright 2025 Florian Mayrhofer
5
+ Licensed under the Apache License, Version 2.0
6
+ -->
7
+
8
+ <script type="text/javascript">
9
+ RED.nodes.registerType('coe-monitor', {
10
+ category: 'TA CMI',
11
+ color: '#FFCC66',
12
+ defaults: {
13
+ name: { value: "" },
14
+ cmiconfig: { value: "", type: "cmiconfig", required: true },
15
+ filterNodeNumber: { value: 0, validate: RED.validators.number() },
16
+ filterDataType: { value: "all" },
17
+ includeRaw: { value: false }
18
+ },
19
+ inputs: 0,
20
+ outputs: 1,
21
+ icon: "feed.svg",
22
+ label: function() {
23
+ if (this.filterNodeNumber && this.filterNodeNumber > 0) {
24
+ return this.name || `CoE Monitor (Node ${this.filterNodeNumber})`;
25
+ }
26
+ return this.name || "CoE Monitor (All)";
27
+ },
28
+ labelStyle: function() {
29
+ return this.name ? "node_label_italic" : "";
30
+ }
31
+ });
32
+ </script>
33
+
34
+ <script type="text/html" data-template-name="coe-monitor">
35
+ <div class="form-row">
36
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="coe-monitor.label.name"></span></label>
37
+ <input type="text" id="node-input-name">
38
+ </div>
39
+ <div class="form-row">
40
+ <label for="node-input-cmiconfig"><i class="fa fa-cog"></i> <span data-i18n="coe-monitor.label.cmiconfig"></span></label>
41
+ <input type="text" id="node-input-cmiconfig">
42
+ </div>
43
+ <div class="form-row">
44
+ <label for="node-input-filterNodeNumber"><i class="fa fa-filter"></i> <span data-i18n="coe-monitor.label.filterNodeNumber"></span></label>
45
+ <input type="number" id="node-input-filterNodeNumber" placeholder="0" min="0" max="62">
46
+ <span id="monitor-filter-help" style="margin-left: 110px; font-size: 0.9em; color: #666;"></span>
47
+ </div>
48
+ <div class="form-row">
49
+ <label for="node-input-filterDataType"><i class="fa fa-filter"></i> <span data-i18n="coe-monitor.label.filterDataType"></span></label>
50
+ <select id="node-input-filterDataType">
51
+ <option value="" data-i18n="coe-monitor.label.all"></option>
52
+ <option value="analog">Analog</span></option>
53
+ <option value="digital">Digital</span></option>
54
+ </select>
55
+ </div>
56
+ <div class="form-row">
57
+ <label for="node-input-includeRaw"><i class="fa fa-database"></i> <span data-i18n="coe-monitor.label.includeRaw"></span></label>
58
+ <input type="checkbox" id="node-input-includeRaw" style="display: inline-block; width: auto;">
59
+ </div>
60
+ </script>
@@ -0,0 +1,138 @@
1
+ /**
2
+ * CoE Monitor Node (Receives all CoE packets)
3
+ *
4
+ * Copyright 2025 Florian Mayrhofer
5
+ * Licensed under the Apache License, Version 2.0
6
+ *
7
+ */
8
+
9
+ module.exports = function(RED) {
10
+ 'use strict';
11
+ const { getUnitInfo } = require('../lib/utils')
12
+
13
+ function CoEMonitorNode(config) {
14
+ RED.nodes.createNode(this, config);
15
+ const node = this;
16
+
17
+ node.cmiConfig = RED.nodes.getNode(config.cmiconfig);
18
+
19
+ const lang = node.cmiConfig.lang;
20
+ const coeVersion = node.cmiConfig.coeVersion || 1;
21
+
22
+ if (!node.cmiConfig) {
23
+ node.error("CMI Configuration missing");
24
+ return;
25
+ }
26
+
27
+ node.filterNodeNumber = config.filterNodeNumber ? parseInt(config.filterNodeNumber) : null;
28
+ node.filterDataType = config.filterDataType || 'all';
29
+ node.includeRaw = config.includeRaw || false;
30
+
31
+ let packetCount = 0;
32
+ let lastUpdate = Date.now();
33
+
34
+ const listener = (data) => { // Listener for incoming data
35
+ if (!data || !data.blocks || !Array.isArray(data.blocks)) {
36
+ node.warn('Received invalid data format');
37
+ return;
38
+ }
39
+
40
+ for (let block of data.blocks) {
41
+ if (!block) continue;
42
+
43
+ if (node.filterNodeNumber !== null && // Filter by Node Number
44
+ node.filterNodeNumber !== 0 &&
45
+ block.nodeNumber !== node.filterNodeNumber) {
46
+ continue;
47
+ }
48
+
49
+ const isDigital = (block.blockNumber === 0 || block.blockNumber === 9); // Filter by Data Type
50
+ const isAnalog = !isDigital;
51
+
52
+ if (node.filterDataType === 'analog' && !isAnalog) continue;
53
+ if (node.filterDataType === 'digital' && !isDigital) continue;
54
+
55
+ packetCount++;
56
+ lastUpdate = Date.now();
57
+
58
+ // Build message
59
+ const msg = {
60
+ payload: {
61
+ nodeNumber: block.nodeNumber,
62
+ blockNumber: block.blockNumber,
63
+ dataType: isDigital ? 'digital' : 'analog',
64
+ values: block.values,
65
+ units: block.units,
66
+ sourceIP: data.sourceIP,
67
+ version: data.version,
68
+ timestamp: new Date().toISOString(),
69
+ rawBuffer: data.rawBuffer ? data.rawBuffer.toString('hex').toUpperCase() : null
70
+ },
71
+ topic: `coe/monitor/${block.nodeNumber}/block/${block.blockNumber}`
72
+ };
73
+
74
+ // Additional Details for Analog Blocks
75
+ if (isAnalog && block.units) {
76
+ msg.payload.valuesDetailed = block.values.map((value, idx) => {
77
+ const unitInfo = getUnitInfo(block.units[idx], lang);
78
+ RED.log.debug(`Unit Info for unit ${block.units[idx]}: ${JSON.stringify(unitInfo)} + version: ${coeVersion} + key: ${unitInfo.key} + symbol: ${unitInfo.symbol}`);
79
+ const outputNumber = (block.blockNumber - 1) * 4 + idx + 1;
80
+ return {
81
+ outputNumber: outputNumber,
82
+ value: value,
83
+ unit: block.units[idx],
84
+ unitName: unitInfo.name,
85
+ unitSymbol: unitInfo.symbol
86
+ };
87
+ });
88
+ }
89
+
90
+ // Additional Details for Digital Blocks
91
+ if (isDigital) {
92
+ const baseOutput = block.blockNumber === 0 ? 1 : 17;
93
+ msg.payload.valuesDetailed = block.values.map((value, idx) => ({
94
+ outputNumber: baseOutput + idx,
95
+ value: value === 1,
96
+ state: value === 1 ? 'ON' : 'OFF'
97
+ }));
98
+ }
99
+
100
+ // Raw Data
101
+ if (node.includeRaw) {
102
+ msg.payload.raw = block;
103
+ }
104
+
105
+ node.send(msg);
106
+
107
+ // Status Update
108
+ const dataTypeLabel = isDigital ? 'D' : 'A';
109
+ node.status({
110
+ fill: "green",
111
+ shape: "dot",
112
+ text: RED._("coe-monitor.status.node") + ` ${block.nodeNumber} B${block.blockNumber}[${dataTypeLabel}] - ${packetCount} Pkts`
113
+ });
114
+ }
115
+ };
116
+
117
+ node.cmiConfig.registerListener(listener);
118
+ node.status({fill: "grey", shape: "ring", text: "coe-monitor.status.monitoring"});
119
+
120
+ // Status Update Timer (shows last activity)
121
+ const statusTimer = setInterval(() => {
122
+ const secsSinceUpdate = Math.floor((Date.now() - lastUpdate) / 1000);
123
+ if (secsSinceUpdate > 10) {
124
+ node.status({
125
+ fill: "yellow",
126
+ shape: "ring",
127
+ text: RED._("coe-monitor.status.idle") + ` ${secsSinceUpdate}s - ${packetCount} Pkts [v${coeVersion}]`
128
+ });
129
+ }
130
+ }, 5000);
131
+
132
+ node.on('close', function() {
133
+ clearInterval(statusTimer);
134
+ node.cmiConfig.unregisterListener(listener);
135
+ });
136
+ }
137
+ RED.nodes.registerType("coe-monitor", CoEMonitorNode);
138
+ };
@@ -0,0 +1,99 @@
1
+ <!--
2
+ CoE Output Node
3
+
4
+ Copyright 2025 Florian Mayrhofer
5
+ Licensed under the Apache License, Version 2.0
6
+ -->
7
+
8
+ <script type="text/javascript">
9
+ RED.nodes.registerType('coe-output', {
10
+ category: 'TA CMI',
11
+ color: '#C0DEED',
12
+ defaults: {
13
+ name: { value: "" },
14
+ cmiconfig: { value: "", type: "cmiconfig", required: true },
15
+ nodeNumber: { value: 1, validate: RED.validators.number() },
16
+ outputNumber: { value: 1, validate: RED.validators.number() },
17
+ dataType: { value: "analog" },
18
+ unit: { value: 0, validate: RED.validators.number() }
19
+ },
20
+ inputs: 1,
21
+ outputs: 2,
22
+ icon: "serial.svg",
23
+ outputLabels: ["message", "debug"],
24
+ label: function() {
25
+ const type = this.dataType === 'analog' ? 'A' : 'D';
26
+ return this.name || `CoE Out (${type}${this.outputNumber})`;
27
+ },
28
+ labelStyle: function() {
29
+ return this.name ? "node_label_italic" : "";
30
+ },
31
+ oneditprepare: function() {
32
+ const node = this;
33
+ const langString = RED.i18n.detectLanguage() || 'en';
34
+ const editorLanguage = (langString.toLowerCase().startsWith("de")) ? "de" : "en";
35
+
36
+ // Populate unit dropdown
37
+ $.getJSON('ta-cmi-coe/units/' + editorLanguage)
38
+ .done(function(unitsData) {
39
+ const selectElement = $('#node-input-unit');
40
+ selectElement.empty();
41
+
42
+ Object.keys(unitsData).forEach(function(key) {
43
+ const unit = unitsData[key];
44
+ selectElement.append(
45
+ $('<option></option>')
46
+ .attr('value', key)
47
+ .text(unit.name)
48
+ );
49
+ });
50
+
51
+ // Set existing value
52
+ selectElement.val(node.unit);
53
+ })
54
+ .fail(function(jqXHR, textStatus, errorThrown) {
55
+ console.error("Error loading unit list:", errorThrown);
56
+ RED.notify("Error loading unit list from unit-config.js.", "error");
57
+ });
58
+
59
+ $("#node-input-dataType").change(function() {
60
+ if ($(this).val() === "analog") {
61
+ $("#unit-row").show();
62
+ } else {
63
+ $("#unit-row").hide();
64
+ }
65
+ });
66
+ $("#node-input-dataType").change();
67
+ }
68
+ });
69
+ </script>
70
+
71
+ <script type="text/html" data-template-name="coe-output">
72
+ <div class="form-row">
73
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="coe-output.label.name"></span></label>
74
+ <input type="text" id="node-input-name">
75
+ </div>
76
+ <div class="form-row">
77
+ <label for="node-input-cmiconfig"><i class="fa fa-cog"></i> <span data-i18n="coe-output.label.cmiconfig"></span></label>
78
+ <input type="text" id="node-input-cmiconfig">
79
+ </div>
80
+ <div class="form-row">
81
+ <label for="node-input-nodeNumber"><i class="fa fa-sitemap"></i> <span data-i18n="coe-output.label.nodeNumber"></span></label>
82
+ <input type="number" id="node-input-nodeNumber" placeholder="1" min="1" max="62">
83
+ </div>
84
+ <div class="form-row">
85
+ <label for="node-input-outputNumber"><i class="fa fa-arrow-up"></i> <span data-i18n="coe-output.label.outputNumber"></span></label>
86
+ <input type="number" id="node-input-outputNumber" placeholder="1" min="1" max="32">
87
+ </div>
88
+ <div class="form-row">
89
+ <label for="node-input-dataType"><i class="fa fa-list"></i> <span data-i18n="coe-output.label.dataType"></span></label>
90
+ <select id="node-input-dataType">
91
+ <option value="analog">Analog</span></option>
92
+ <option value="digital">Digital</span></option>
93
+ </select>
94
+ </div>
95
+ <div class="form-row" id="unit-row" style="display:none;">
96
+ <label for="node-input-unit"><i class="fa fa-ruler"></i> <span data-i18n="coe-output.label.unit"></span></label>
97
+ <select id="node-input-unit"></select>
98
+ </div>
99
+ </script>
@@ -0,0 +1,68 @@
1
+ /**
2
+ * CoE Output Node (Sending of values)
3
+ *
4
+ * Copyright 2025 Florian Mayrhofer
5
+ * Licensed under the Apache License, Version 2.0
6
+ *
7
+ */
8
+
9
+ module.exports = function(RED) {
10
+ 'use strict';
11
+ const { getBlockInfo } = require('../lib/utils')
12
+ const { queueAndSend } = require('../lib/queueing');
13
+
14
+ function CoEOutputNode(config) {
15
+ RED.nodes.createNode(this, config);
16
+ const node = this;
17
+
18
+ node.cmiConfig = RED.nodes.getNode(config.cmiconfig);
19
+
20
+ if (!node.cmiConfig) {
21
+ node.error("CoE Configuration missing or invalid.");
22
+ node.status({fill:"red", shape:"ring", text:"coe-output.status.noconfig"});
23
+ return;
24
+ }
25
+
26
+ node.cmiAddress = node.cmiConfig.address;
27
+ node.coeVersion = node.cmiConfig.coeVersion || 1;
28
+ node.nodeNumber = parseInt(config.nodeNumber) || 1;
29
+ node.outputNumber = parseInt(config.outputNumber) || 1;
30
+ node.dataType = config.dataType || 'analog';
31
+ node.unit = parseInt(config.unit) || 0;
32
+
33
+ node.on('input', function(msg) {
34
+ const blockInfo = getBlockInfo(node.dataType, node.outputNumber);
35
+ let values, units;
36
+
37
+ if (node.dataType === 'analog') {
38
+ values = [undefined, undefined, undefined, undefined];
39
+ units = [undefined, undefined, undefined, undefined];
40
+
41
+ const payloadValue = parseFloat(msg.payload);
42
+ values[blockInfo.position] = isNaN(payloadValue) ? 0 : payloadValue;
43
+
44
+ // Set unit for the specific output
45
+ units[blockInfo.position] =
46
+ (msg.coe && msg.coe.unit !== undefined)
47
+ ? parseInt(msg.coe.unit)
48
+ : node.unit;
49
+
50
+ } else { // digital
51
+ values = new Array(16).fill(undefined);
52
+ values[blockInfo.position] = msg.payload ? 1 : 0;
53
+ units = null;
54
+ }
55
+
56
+ node.status({
57
+ fill: "yellow",
58
+ shape: "dot",
59
+ text: RED._("coe-output.status.queued") + `[v${node.coeVersion}]`
60
+ });
61
+
62
+ queueAndSend(node, RED._, node.nodeNumber, blockInfo.block, values, units, node.dataType, node.coeVersion, node.cmiConfig, node.cmiAddress, msg);
63
+ });
64
+
65
+ node.status({fill:"grey", shape:"ring", text:RED._("coe-output.status.ready") + ` [v${node.coeVersion}]`});
66
+ }
67
+ RED.nodes.registerType("coe-output", CoEOutputNode);
68
+ };
@@ -0,0 +1,52 @@
1
+ <!--
2
+ CMI Configuration Node
3
+
4
+ Copyright 2025 Florian Mayrhofer
5
+ Licensed under the Apache License, Version 2.0
6
+ -->
7
+
8
+ <script type="text/javascript">
9
+ RED.nodes.registerType('cmiconfig', {
10
+ category: 'config',
11
+ defaults: {
12
+ name: { value: "" },
13
+ localip: { value: "0.0.0.0" },
14
+ address: { value: "192.168.0.100" },
15
+ coeVersion:{ value: 1 }
16
+ },
17
+ label: function() {
18
+ const version = this.coeVersion || 1;
19
+ return this.name || `Address (${this.address || "192.168.0.100"}), CMI Config (${version})`;
20
+ }
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="cmiconfig">
25
+ <div class="form-row">
26
+ <label for="node-config-input-name"><i class="fa fa-tag"></i>
27
+ <span data-i18n="config.label.name"></span>
28
+ </label>
29
+ <input type="text" id="node-config-input-name">
30
+ </div>
31
+ <div class="form-row">
32
+ <label for="node-config-input-localip"><i class="fa fa-wifi"></i>
33
+ <span data-i18n="config.label.localip"></span>
34
+ </label>
35
+ <input type="text" id="node-config-input-localip" placeholder="0.0.0.0">
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-config-input-address"><i class="fa fa-globe"></i>
39
+ <span data-i18n="config.label.address"></span>
40
+ </label>
41
+ <input type="text" id="node-config-input-address" placeholder="192.168.1.100">
42
+ </div>
43
+ <div class="form-row">
44
+ <label for="node-config-input-coeVersion"><i class="fa fa-cog"></i>
45
+ <span data-i18n="config.label.coeVersion"></span>
46
+ </label>
47
+ <select id="node-config-input-coeVersion">
48
+ <option value="1">V1</option>
49
+ <option value="2">V2</option>
50
+ </select>
51
+ </div>
52
+ </script>
package/coe/config.js ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * CMI Configuration Node & Shared UDP socket
3
+ *
4
+ * Copyright 2025 Florian Mayrhofer
5
+ * Licensed under the Apache License, Version 2.0
6
+ *
7
+ */
8
+
9
+ module.exports = function(RED) {
10
+ "use strict";
11
+ const dgram = require('dgram');
12
+ const { parseCoEPacket } = require('../lib/coe');
13
+
14
+ // CoE Protocol Ports
15
+ const COE_PORT1 = 5441; // CoE v1
16
+ const COE_PORT2 = 5442; // CoE v2
17
+
18
+ function CMIConfigNode(config) {
19
+ RED.nodes.createNode(this, config);
20
+ const node = this;
21
+
22
+ node.lang = (RED.settings.lang.toLowerCase().startsWith("de")) ? "de" : "en";
23
+
24
+ node.address = config.address || '192.168.0.100';
25
+ node.coeVersion = parseInt(config.coeVersion) || 1;
26
+ node.port = (node.coeVersion === 2) ? COE_PORT2 : COE_PORT1;
27
+ node.localAddress = config.localip || '0.0.0.0';
28
+ node.socket = null;
29
+ node.listeners = [];
30
+
31
+ // Add UDP Socket
32
+ try {
33
+ node.socket = dgram.createSocket({
34
+ type: 'udp4',
35
+ reuseAddr: true // Enable Socket Reuse
36
+ });
37
+
38
+ node.socket.on('message', (msg, rinfo) => {
39
+ const blocks = parseCoEPacket(msg, node.coeVersion);
40
+
41
+ if (blocks && blocks.length > 0) {
42
+ const data = { // Wrapper object for meta info
43
+ blocks: blocks,
44
+ sourceIP: rinfo.address,
45
+ version: node.coeVersion,
46
+ timestamp: Date.now(),
47
+ rawBuffer: msg
48
+ };
49
+
50
+ node.listeners.forEach(listener => { // Notify all listeners
51
+ try {
52
+ listener(data);
53
+ } catch(err) {
54
+ node.error(`Listener error: ${err.message}`);
55
+ }
56
+ });
57
+ }
58
+ });
59
+
60
+ node.socket.on('error', (err) => {
61
+ node.error(`UDP Socket Error: ${err.message}`);
62
+ });
63
+
64
+ node.socket.bind(node.port, node.localAddress, () => {
65
+ node.log(`CoE UDP Socket is listening on ${node.localAddress}:${node.port} (CoE V${node.coeVersion})`);
66
+ });
67
+
68
+ } catch(err) {
69
+ node.error(`Failed to create UDP socket: ${err.message}`);
70
+ }
71
+
72
+ node.registerListener = function(callback) { // Add listener
73
+ node.listeners.push(callback);
74
+ };
75
+
76
+ node.unregisterListener = function(callback) { // Remove listener
77
+ const index = node.listeners.indexOf(callback);
78
+ if (index > -1) {
79
+ node.listeners.splice(index, 1);
80
+ }
81
+ };
82
+
83
+ node.send = function(host, packet) { // Send data
84
+ if (node.socket) {
85
+ node.socket.send(packet, 0, packet.length, node.port, host, (err) => {
86
+ if (err) {
87
+ node.error(`Failed to send: ${err.message}`);
88
+ }
89
+ });
90
+ }
91
+ };
92
+
93
+ node.on('close', function() { // Cleanup on node shutdown
94
+ if (node.socket) {
95
+ node.socket.close();
96
+ }
97
+ });
98
+ }
99
+ RED.nodes.registerType("cmiconfig", CMIConfigNode);
100
+
101
+ };
@@ -0,0 +1,10 @@
1
+ <script type="text/html" data-help-name="coe-input">
2
+ <p>Empfängt Werte von einer TA CMI via CoE (CAN over Ethernet).</p>
3
+ <h3>Konfiguration</h3>
4
+ <ul>
5
+ <li><b>Knoten:</b> CAN Knoten-Nummer (1-62, 0 = von beliebigem Knoten empfangen, nicht empfohlen in Produktivbetrieb)</li>
6
+ <li><b>Eingang:</b> Nummer des Netzwerkeingangs (1-32)</li>
7
+ <li><b>Datentyp:</b> Analog (1-32) oder Digital (1-32)</li>
8
+ <li><b>Timeout:</b> CAN-Bus Timeout-Wert in Minuten</li>
9
+ </ul>
10
+ </script>