node-red-contrib-ta-cmi-coe 1.0.0 → 1.1.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/CHANGELOG.md +31 -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/in-out.json +56 -0
- package/examples/monitor.json +42 -0
- package/lib/coe-v1.js +152 -0
- package/lib/coe-v2.js +80 -119
- 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 +17 -3
- 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/coe/coe-monitor.js
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
module.exports = function(RED) {
|
|
10
10
|
'use strict';
|
|
11
|
-
const { getUnitInfo } = require('../lib/utils')
|
|
11
|
+
const { getUnitInfo, getDigitalStateKey } = require('../lib/utils')
|
|
12
12
|
|
|
13
13
|
function CoEMonitorNode(config) {
|
|
14
14
|
RED.nodes.createNode(this, config);
|
|
15
15
|
const node = this;
|
|
16
|
+
node._ = RED._;
|
|
16
17
|
|
|
17
18
|
node.cmiConfig = RED.nodes.getNode(config.cmiconfig);
|
|
18
19
|
|
|
@@ -31,22 +32,22 @@ module.exports = function(RED) {
|
|
|
31
32
|
let packetCount = 0;
|
|
32
33
|
let lastUpdate = Date.now();
|
|
33
34
|
|
|
34
|
-
const listener = (
|
|
35
|
-
if (!
|
|
35
|
+
const listener = (received) => { // Listener for incoming data
|
|
36
|
+
if (!received || !received.data || !Array.isArray(received.data)) {
|
|
36
37
|
node.warn('Received invalid data format');
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
for (let
|
|
41
|
-
if (!
|
|
41
|
+
for (let canNode of received.data) {
|
|
42
|
+
if (!canNode) continue;
|
|
42
43
|
|
|
43
44
|
if (node.filterNodeNumber !== null && // Filter by Node Number
|
|
44
45
|
node.filterNodeNumber !== 0 &&
|
|
45
|
-
|
|
46
|
+
canNode.nodeNumber !== node.filterNodeNumber) {
|
|
46
47
|
continue;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
const isDigital = (
|
|
50
|
+
const isDigital = (canNode.dataType === 'digital'); // Filter by Data Type
|
|
50
51
|
const isAnalog = !isDigital;
|
|
51
52
|
|
|
52
53
|
if (node.filterDataType === 'analog' && !isAnalog) continue;
|
|
@@ -58,48 +59,38 @@ module.exports = function(RED) {
|
|
|
58
59
|
// Build message
|
|
59
60
|
const msg = {
|
|
60
61
|
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
62
|
timestamp: new Date().toISOString(),
|
|
69
|
-
|
|
63
|
+
sourceIP: received.sourceIP,
|
|
64
|
+
version: received.version,
|
|
65
|
+
nodeNumber: canNode.nodeNumber,
|
|
66
|
+
...(canNode.blockNumber && { blockNumber: canNode.blockNumber}),
|
|
67
|
+
dataType: canNode.dataType
|
|
70
68
|
},
|
|
71
|
-
topic: `coe/monitor/${
|
|
69
|
+
topic: `coe/monitor/${canNode.nodeNumber}/block/${canNode.dataType}`
|
|
72
70
|
};
|
|
73
71
|
|
|
74
|
-
// Additional Details for Analog Blocks
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
// Additional Details for Digital/Analog Blocks
|
|
73
|
+
const valuesDetailed = {};
|
|
74
|
+
|
|
75
|
+
if (canNode.outputs) {
|
|
76
|
+
Object.entries(canNode.outputs).forEach(([outputNumber, output]) => {
|
|
77
|
+
const unitInfo = getUnitInfo(output.unit, lang);
|
|
78
|
+
const translationKey = getDigitalStateKey(output.unit, output.value, "coe-monitor.status.");
|
|
79
|
+
|
|
80
|
+
valuesDetailed[outputNumber] = {
|
|
81
|
+
value: output.value,
|
|
82
|
+
...(isDigital && { state: node._(translationKey) }),
|
|
83
|
+
unit: output.unit,
|
|
84
84
|
unitName: unitInfo.name,
|
|
85
|
-
unitSymbol: unitInfo.symbol
|
|
85
|
+
...(isAnalog && { unitSymbol: unitInfo.symbol })
|
|
86
86
|
};
|
|
87
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
|
-
}));
|
|
88
|
+
msg.payload.values = valuesDetailed;
|
|
98
89
|
}
|
|
99
90
|
|
|
100
91
|
// Raw Data
|
|
101
92
|
if (node.includeRaw) {
|
|
102
|
-
msg.payload.raw =
|
|
93
|
+
msg.payload.raw = { raw: received.rawBuffer ? received.rawBuffer.toString('hex').toUpperCase() : null}
|
|
103
94
|
}
|
|
104
95
|
|
|
105
96
|
node.send(msg);
|
|
@@ -109,13 +100,13 @@ module.exports = function(RED) {
|
|
|
109
100
|
node.status({
|
|
110
101
|
fill: "green",
|
|
111
102
|
shape: "dot",
|
|
112
|
-
text:
|
|
103
|
+
text: node._("coe-monitor.status.node") + ` ${canNode.nodeNumber} ${dataTypeLabel} - ${packetCount} Pkts`
|
|
113
104
|
});
|
|
114
105
|
}
|
|
115
106
|
};
|
|
116
107
|
|
|
117
108
|
node.cmiConfig.registerListener(listener);
|
|
118
|
-
node.status({fill: "grey", shape: "ring", text: "coe-monitor.status.monitoring"});
|
|
109
|
+
node.status({fill: "grey", shape: "ring", text: node._("coe-monitor.status.monitoring") + "..."});
|
|
119
110
|
|
|
120
111
|
// Status Update Timer (shows last activity)
|
|
121
112
|
const statusTimer = setInterval(() => {
|
|
@@ -124,7 +115,7 @@ module.exports = function(RED) {
|
|
|
124
115
|
node.status({
|
|
125
116
|
fill: "yellow",
|
|
126
117
|
shape: "ring",
|
|
127
|
-
text:
|
|
118
|
+
text: node._("coe-monitor.status.monitoring") + ` ${secsSinceUpdate}s - ${packetCount} Pkt. [v${coeVersion}]`
|
|
128
119
|
});
|
|
129
120
|
}
|
|
130
121
|
}, 5000);
|
package/coe/coe-output.html
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
nodeNumber: { value: 1, validate: RED.validators.number() },
|
|
16
16
|
outputNumber: { value: 1, validate: RED.validators.number() },
|
|
17
17
|
dataType: { value: "analog" },
|
|
18
|
-
unit: { value: 0, validate: RED.validators.number() }
|
|
18
|
+
unit: { value: 0, validate: RED.validators.number() },
|
|
19
|
+
minChange: { value: 1, validate: RED.validators.number() },
|
|
20
|
+
blockingTime: { value: 10, validate: RED.validators.number() },
|
|
21
|
+
maxInterval: { value: 5, validate: RED.validators.number() }
|
|
19
22
|
},
|
|
20
23
|
inputs: 1,
|
|
21
24
|
outputs: 2,
|
|
@@ -33,37 +36,58 @@
|
|
|
33
36
|
const langString = RED.i18n.detectLanguage() || 'en';
|
|
34
37
|
const editorLanguage = (langString.toLowerCase().startsWith("de")) ? "de" : "en";
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.done(function(unitsData) {
|
|
39
|
-
const selectElement = $('#node-input-unit');
|
|
40
|
-
selectElement.empty();
|
|
39
|
+
function loadAndFilterUnits(dataType) {
|
|
40
|
+
const isDigital = dataType === 'analog' ? false : true;
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
selectElement
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
42
|
+
$.getJSON('ta-cmi-coe/units/' + editorLanguage)
|
|
43
|
+
.done(function(unitsData) {
|
|
44
|
+
const selectElement = $('#node-input-unit');
|
|
45
|
+
selectElement.empty();
|
|
46
|
+
let foundUnit = false;
|
|
47
|
+
|
|
48
|
+
Object.keys(unitsData).forEach(function(key) {
|
|
49
|
+
const unit = unitsData[key];
|
|
50
|
+
|
|
51
|
+
if (unit.digital === isDigital) {
|
|
52
|
+
selectElement.append(
|
|
53
|
+
$('<option></option>')
|
|
54
|
+
.attr('value', key)
|
|
55
|
+
.text(unit.name)
|
|
56
|
+
);
|
|
57
|
+
if (Number(key) === Number(node.unit)) {
|
|
58
|
+
foundUnit = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
if (!foundUnit) {
|
|
64
|
+
node.unit = selectElement.find('option:first').attr('value') || 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
selectElement.val(node.unit);
|
|
68
|
+
})
|
|
69
|
+
.fail(function(jqXHR, textStatus, errorThrown) {
|
|
70
|
+
console.error("Error loading unit list:", errorThrown);
|
|
71
|
+
RED.notify("Error loading unit list from unit-config.js.", "error");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
58
74
|
|
|
59
|
-
$("#node-input-dataType").change(function() {
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
$("#node-input-dataType").change(function() {
|
|
76
|
+
const selectedDataType = $(this).val();
|
|
77
|
+
loadAndFilterUnits(selectedDataType);
|
|
78
|
+
|
|
79
|
+
if (selectedDataType === "analog") {
|
|
80
|
+
$("#minchange-row").show();
|
|
62
81
|
} else {
|
|
63
|
-
$("#
|
|
82
|
+
$("#minchange-row").hide();
|
|
64
83
|
}
|
|
65
84
|
});
|
|
66
|
-
|
|
85
|
+
|
|
86
|
+
$("#node-input-dataType").change();
|
|
87
|
+
|
|
88
|
+
$("#node-input-minChange").val(
|
|
89
|
+
parseFloat($("#node-input-minChange").val() || 0).toFixed(5)
|
|
90
|
+
);
|
|
67
91
|
}
|
|
68
92
|
});
|
|
69
93
|
</script>
|
|
@@ -92,8 +116,23 @@
|
|
|
92
116
|
<option value="digital">Digital</span></option>
|
|
93
117
|
</select>
|
|
94
118
|
</div>
|
|
95
|
-
<div class="form-row"
|
|
96
|
-
<label for="node-input-unit"><i class="fa fa-
|
|
119
|
+
<div class="form-row">
|
|
120
|
+
<label for="node-input-unit"><i class="fa fa-thermometer-full"></i> <span data-i18n="coe-output.label.unit"></span></label>
|
|
97
121
|
<select id="node-input-unit"></select>
|
|
98
122
|
</div>
|
|
123
|
+
<div class="form-row">
|
|
124
|
+
<span data-i18n="coe-output.label.sendConditions"></span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="form-row" id="minchange-row" style="display:none;">
|
|
127
|
+
<label for="node-input-minChange"><i class="fa fa-line-chart"></i> <span data-i18n="coe-output.label.minChange"></span></label>
|
|
128
|
+
<input type="number" id="node-input-minChange" placeholder="1" min="0" max="1000000" step="0.001">
|
|
129
|
+
</div>
|
|
130
|
+
<div class="form-row">
|
|
131
|
+
<label for="node-input-blockingTime"><i class="fa fa-clock-o"></i> <span data-i18n="coe-output.label.blockingTime"></span></label>
|
|
132
|
+
<input type="number" id="node-input-blockingTime" placeholder="10" min="1" max="999">
|
|
133
|
+
</div>
|
|
134
|
+
<div class="form-row">
|
|
135
|
+
<label for="node-input-maxInterval"><i class="fa fa-repeat"></i> <span data-i18n="coe-output.label.maxInterval"></span></label>
|
|
136
|
+
<input type="number" id="node-input-maxInterval" placeholder="5" min="1" max="60">
|
|
137
|
+
</div>
|
|
99
138
|
</script>
|
package/coe/coe-output.js
CHANGED
|
@@ -14,6 +14,7 @@ module.exports = function(RED) {
|
|
|
14
14
|
function CoEOutputNode(config) {
|
|
15
15
|
RED.nodes.createNode(this, config);
|
|
16
16
|
const node = this;
|
|
17
|
+
node._ = RED._;
|
|
17
18
|
|
|
18
19
|
node.cmiConfig = RED.nodes.getNode(config.cmiconfig);
|
|
19
20
|
|
|
@@ -30,39 +31,173 @@ module.exports = function(RED) {
|
|
|
30
31
|
node.dataType = config.dataType || 'analog';
|
|
31
32
|
node.unit = parseInt(config.unit) || 0;
|
|
32
33
|
|
|
34
|
+
if (node.coeVersion === 2) {
|
|
35
|
+
node.queueKey = node.nodeNumber + '-' + node.dataType;
|
|
36
|
+
} else {
|
|
37
|
+
node.block = getBlockInfo(node.dataType, node.outputNumber);
|
|
38
|
+
node.queueKey = node.nodeNumber + '-' + node.block.number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Filter configuration parameters
|
|
42
|
+
node.minChange = parseFloat(config.minChange) || 1;
|
|
43
|
+
node.blockingTime = parseFloat(config.blockingTime) || 10; // Seconds
|
|
44
|
+
node.maxInterval = parseFloat(config.maxInterval) || 5; // Minutes
|
|
45
|
+
|
|
46
|
+
// State tracking per output (not global!)
|
|
47
|
+
node.lastReceivedMsg = null;
|
|
48
|
+
node.lastReceivedValue = undefined;
|
|
49
|
+
node.lastReceivedUnit = null;
|
|
50
|
+
node.lastSentValue = undefined;
|
|
51
|
+
node.lastSentTime = 0;
|
|
52
|
+
node.lastInputTime = 0;
|
|
53
|
+
|
|
54
|
+
node.intervalTimer = null;
|
|
55
|
+
|
|
33
56
|
node.on('input', function(msg) {
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
node.lastInputTime = Date.now();
|
|
58
|
+
const timeSinceLastSend = (node.lastInputTime - node.lastSentTime) / 1000; // Seconds
|
|
59
|
+
const isSupressed = checkSuppressCondition(node, timeSinceLastSend);
|
|
60
|
+
node.lastReceivedMsg = msg;
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
// Prepare last received values
|
|
63
|
+
const payloadValue = parseFloat(msg.payload);
|
|
64
|
+
node.lastReceivedValue = isNaN(payloadValue) ? 0 : payloadValue;
|
|
65
|
+
node.lastReceivedUnit = (msg.coe && msg.coe.unit !== undefined) ? parseInt(msg.coe.unit) : node.unit;
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Filter based on sending conditions
|
|
69
|
+
if (isSupressed > 0) {
|
|
70
|
+
if (isSupressed === 1) { // Suppressed due to blocking time
|
|
71
|
+
setblockingTimer(timeSinceLastSend);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (node.blockingTimer) {
|
|
77
|
+
clearTimeout(node.blockingTimer);
|
|
78
|
+
node.blockingTimer = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setIntervalTimer(); // Restart interval timer
|
|
82
|
+
|
|
83
|
+
queueAndSend(node);
|
|
84
|
+
node.lastSentValue = node.lastReceivedValue;
|
|
85
|
+
node.lastSentTime = Date.now();
|
|
86
|
+
|
|
87
|
+
node.status({ // Update to queued
|
|
88
|
+
fill: "yellow",
|
|
89
|
+
shape: "dot",
|
|
90
|
+
text: node._("coe-output.status.queued") + ` [v${node.coeVersion}]`
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
node.on('close', function() { // Clear timers on shutdown
|
|
95
|
+
if (node.intervalTimer) {
|
|
96
|
+
clearTimeout(node.intervalTimer);
|
|
97
|
+
node.intervalTimer = null;
|
|
98
|
+
}
|
|
99
|
+
if (node.blockingTimer) {
|
|
100
|
+
clearTimeout(node.blockingTimer);
|
|
101
|
+
node.blockingTimer = null;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
node.status({fill:"grey", shape:"ring", text:node._("coe-output.status.ready") + ` [v${node.coeVersion}]`});
|
|
106
|
+
|
|
107
|
+
// Start the sending interval timer
|
|
108
|
+
function setIntervalTimer() {
|
|
109
|
+
if (node.intervalTimer) {
|
|
110
|
+
clearTimeout(node.intervalTimer);
|
|
111
|
+
node.intervalTimer = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const checkInterval = node.maxInterval * 60 * 1000;
|
|
115
|
+
|
|
116
|
+
node.intervalTimer = setTimeout(() => { // Interval elapsed, retransmit last value
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const timeSinceLastSend = (now - node.lastSentTime) / 1000 / 60;
|
|
43
119
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
120
|
+
queueAndSend(node);
|
|
121
|
+
node.lastSentValue = node.lastReceivedValue;
|
|
122
|
+
node.lastSentTime = Date.now();
|
|
123
|
+
|
|
124
|
+
if (node.lastReceivedValue !== undefined) {
|
|
125
|
+
node.status({
|
|
126
|
+
fill: "yellow",
|
|
127
|
+
shape: "dot",
|
|
128
|
+
text: node._("coe-output.status.retransmit") + ` (${timeSinceLastSend.toFixed(0)} Min) [v${node.coeVersion}]`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
node.status({fill: "grey", shape: "ring", text: node._("coe-output.status.ready") + ` [v${node.coeVersion}]`});
|
|
133
|
+
}, 5000);
|
|
49
134
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
135
|
+
setIntervalTimer(); // Restart interval timer
|
|
136
|
+
}, checkInterval);
|
|
137
|
+
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function setblockingTimer(timeSinceLastSend) {
|
|
141
|
+
if (node.blockingTimer) {
|
|
142
|
+
clearTimeout(node.blockingTimer);
|
|
143
|
+
node.blockingTimer = null;
|
|
54
144
|
}
|
|
145
|
+
|
|
146
|
+
const blockingTimeMs = (node.blockingTime - timeSinceLastSend) * 1000;
|
|
147
|
+
|
|
148
|
+
node.blockingTimer = setTimeout(() => {
|
|
149
|
+
if (node.lastReceivedValue !== undefined) {
|
|
150
|
+
node.status({
|
|
151
|
+
fill: "yellow",
|
|
152
|
+
shape: "dot",
|
|
153
|
+
text: node._("coe-output.status.queued") + ` [v${node.coeVersion}]`
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
queueAndSend(node);
|
|
157
|
+
node.lastSentValue = node.lastReceivedValue;
|
|
158
|
+
node.lastSentTime = Date.now();
|
|
159
|
+
setIntervalTimer()
|
|
160
|
+
}
|
|
161
|
+
}, blockingTimeMs);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Checks if message should be suppressed based on sending conditions
|
|
166
|
+
function checkSuppressCondition(node, timeSinceLastSend) {
|
|
167
|
+
let isSuppressed = 0;
|
|
168
|
+
let blockReason = "";
|
|
169
|
+
|
|
170
|
+
if (node.lastSentValue === undefined) { // First input
|
|
171
|
+
isSuppressed = 0;
|
|
172
|
+
blockReason = "Initial";
|
|
173
|
+
} else {
|
|
55
174
|
|
|
175
|
+
// Check blocking time
|
|
176
|
+
if (node.blockingTime > 0 && timeSinceLastSend < node.blockingTime) {
|
|
177
|
+
isSuppressed = 1;
|
|
178
|
+
blockReason = `⏱ ${timeSinceLastSend.toFixed(0)}/${node.blockingTime}s`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check size of value change
|
|
182
|
+
if (node.dataType === 'analog') {
|
|
183
|
+
const delta = Math.abs(node.lastReceivedValue - node.lastSentValue);
|
|
184
|
+
if (node.minChange > 0 && delta < node.minChange) {
|
|
185
|
+
isSuppressed = 2;
|
|
186
|
+
blockReason = `Δ${delta.toFixed(2)} < ${node.minChange}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (isSuppressed > 0) { // Update status and state
|
|
56
192
|
node.status({
|
|
57
193
|
fill: "yellow",
|
|
58
|
-
shape: "
|
|
59
|
-
text:
|
|
194
|
+
shape: "ring",
|
|
195
|
+
text: node._("coe-output.status.blocked") + `: ${blockReason} [v${node.coeVersion}]`
|
|
60
196
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
node.status({fill:"grey", shape:"ring", text:RED._("coe-output.status.ready") + ` [v${node.coeVersion}]`});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return isSuppressed;
|
|
66
200
|
}
|
|
201
|
+
|
|
67
202
|
RED.nodes.registerType("coe-output", CoEOutputNode);
|
|
68
203
|
};
|
package/coe/config.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
module.exports = function(RED) {
|
|
10
10
|
"use strict";
|
|
11
11
|
const dgram = require('dgram');
|
|
12
|
-
const {
|
|
12
|
+
const { parsePacket } = require('../lib/protocol');
|
|
13
13
|
|
|
14
14
|
// CoE Protocol Ports
|
|
15
15
|
const COE_PORT1 = 5441; // CoE v1
|
|
@@ -19,7 +19,14 @@ module.exports = function(RED) {
|
|
|
19
19
|
RED.nodes.createNode(this, config);
|
|
20
20
|
const node = this;
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
let langSetting = (RED.settings.lang || "").toLowerCase();
|
|
23
|
+
switch (true) {
|
|
24
|
+
case langSetting.startsWith("de"):
|
|
25
|
+
node.lang = "de";
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
node.lang = "en";
|
|
29
|
+
}
|
|
23
30
|
|
|
24
31
|
node.address = config.address || '192.168.0.100';
|
|
25
32
|
node.coeVersion = parseInt(config.coeVersion) || 1;
|
|
@@ -36,11 +43,11 @@ module.exports = function(RED) {
|
|
|
36
43
|
});
|
|
37
44
|
|
|
38
45
|
node.socket.on('message', (msg, rinfo) => {
|
|
39
|
-
const
|
|
46
|
+
const data = parsePacket[node.coeVersion](msg);
|
|
40
47
|
|
|
41
|
-
if (
|
|
42
|
-
const
|
|
43
|
-
|
|
48
|
+
if (data && data.length > 0) {
|
|
49
|
+
const received = { // Wrapper object for meta info
|
|
50
|
+
data: data,
|
|
44
51
|
sourceIP: rinfo.address,
|
|
45
52
|
version: node.coeVersion,
|
|
46
53
|
timestamp: Date.now(),
|
|
@@ -49,7 +56,7 @@ module.exports = function(RED) {
|
|
|
49
56
|
|
|
50
57
|
node.listeners.forEach(listener => { // Notify all listeners
|
|
51
58
|
try {
|
|
52
|
-
listener(
|
|
59
|
+
listener(received);
|
|
53
60
|
} catch(err) {
|
|
54
61
|
node.error(`Listener error: ${err.message}`);
|
|
55
62
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
<p>Empfängt Werte von einer TA CMI via CoE (CAN over Ethernet).</p>
|
|
3
3
|
<h3>Konfiguration</h3>
|
|
4
4
|
<ul>
|
|
5
|
-
<li><b>Knoten:</b> CAN Knoten-Nummer (1-62, 0 = von beliebigem Knoten empfangen
|
|
5
|
+
<li><b>Knoten:</b> CAN Knoten-Nummer (1-62, 0 = von beliebigem Knoten empfangen; nicht empfohlen in Produktivbetrieb)</li>
|
|
6
6
|
<li><b>Eingang:</b> Nummer des Netzwerkeingangs (1-32)</li>
|
|
7
7
|
<li><b>Datentyp:</b> Analog (1-32) oder Digital (1-32)</li>
|
|
8
|
-
<li><b>Timeout:</b> CAN-Bus Timeout
|
|
8
|
+
<li><b>Timeout:</b> CAN-Bus Timeout (Wert in Minuten)</li>
|
|
9
9
|
</ul>
|
|
10
10
|
</script>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"coe-input": {
|
|
3
3
|
"label": {
|
|
4
4
|
"name": "Name",
|
|
5
|
-
"cmiconfig": "CMI
|
|
5
|
+
"cmiconfig": "CMI",
|
|
6
6
|
"nodeNumber": "Knoten",
|
|
7
7
|
"outputNumber": "Eingang",
|
|
8
8
|
"dataType": "Datentyp",
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
},
|
|
14
14
|
"status": {
|
|
15
15
|
"waiting": "wartet",
|
|
16
|
-
"waitingAny": "wartet (alle Knoten)"
|
|
16
|
+
"waitingAny": "wartet (alle Knoten)",
|
|
17
|
+
"open": "Offen",
|
|
18
|
+
"closed": "Geschlossen",
|
|
19
|
+
"on": "Ein",
|
|
20
|
+
"off": "Aus",
|
|
21
|
+
"yes": "Ja",
|
|
22
|
+
"no": "Nein"
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
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>
|
|
2
|
+
<p>Empfängt und überwacht CoE (CAN over Ethernet) Pakete von allen Quellen. Ideal für Debugging, Logging und Überwachung des gesamten CoE-Verkehrs.</p>
|
|
4
3
|
<h3>Konfiguration</h3>
|
|
5
4
|
<ul>
|
|
6
5
|
<li>Optional: Filtere nur Pakete von einem bestimmten CAN-Knoten (0 = alle)</li>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"coe-monitor": {
|
|
3
3
|
"label": {
|
|
4
4
|
"name": "Name",
|
|
5
|
-
"cmiconfig": "CMI
|
|
5
|
+
"cmiconfig": "CMI",
|
|
6
6
|
"filterNodeNumber": "Filter Knoten",
|
|
7
7
|
"filterDataType": "Filter Typ",
|
|
8
8
|
"includeRaw": "Rohdaten einschließen",
|
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
"filterNodeNumber": "0 = alle"
|
|
13
13
|
},
|
|
14
14
|
"status": {
|
|
15
|
-
"monitoring": "Lauscht
|
|
16
|
-
"
|
|
17
|
-
"
|
|
15
|
+
"monitoring": "Lauscht",
|
|
16
|
+
"node": "Knoten",
|
|
17
|
+
"open": "Offen",
|
|
18
|
+
"closed": "Geschlossen",
|
|
19
|
+
"on": "Ein",
|
|
20
|
+
"off": "Aus",
|
|
21
|
+
"yes": "Ja",
|
|
22
|
+
"no": "Nein"
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
}
|
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
<li><b>Datentyp:</b> Analog (numerisch) oder Digital (boolean)</li>
|
|
8
8
|
<li><b>Einheit:</b> Messeinheit (nur für analog)</li>
|
|
9
9
|
</ul>
|
|
10
|
+
<h3>Sendebedingungen</h3>
|
|
11
|
+
<ul>
|
|
12
|
+
<li><b>bei Änderung >:</b> Erneut senden, wenn sich der aktuelle Wert um mehr als dieser Wert ändert (analoge Werte)</li>
|
|
13
|
+
<li><b>Blockierzeit:</b> Frühestens nach dieser Zeit erneut senden (Sekunden)</li>
|
|
14
|
+
<li><b>Intervallzeit:</b> Falls sich der Wert nicht ändert, nach diesem Intervall erneut senden (Minuten)</li>
|
|
15
|
+
</ul>
|
|
10
16
|
<h3>Ausgänge</h3>
|
|
11
17
|
<p><b>2 Ausgänge:</b> [0] Durchgeleitete Nachricht, [1] Debug-Info (Hex, Block-State, Units)</p>
|
|
12
|
-
<h3>
|
|
13
|
-
<p>Werte werden
|
|
18
|
+
<h3>Hinweis</h3>
|
|
19
|
+
<p>Werte werden 50 ms gepuffert um die Nachrichtenfrequenz zu reduzieren.</p>
|
|
14
20
|
</script>
|
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
"coe-output": {
|
|
3
3
|
"label": {
|
|
4
4
|
"name": "Name",
|
|
5
|
-
"cmiconfig": "CMI
|
|
5
|
+
"cmiconfig": "CMI",
|
|
6
6
|
"nodeNumber": "Knoten",
|
|
7
7
|
"outputNumber": "Ausgang",
|
|
8
8
|
"dataType": "Datentyp",
|
|
9
|
-
"unit": "Einheit"
|
|
9
|
+
"unit": "Einheit",
|
|
10
|
+
"sendConditions": "Sendebedingungen:",
|
|
11
|
+
"minChange": "bei Änderung >",
|
|
12
|
+
"blockingTime": "Blockierzeit (Sek)",
|
|
13
|
+
"maxInterval": "Intervallzeit (Min)"
|
|
10
14
|
},
|
|
11
15
|
"status": {
|
|
12
16
|
"noconfig": "Nicht konfiguriert",
|
|
13
|
-
"queued": "
|
|
14
|
-
"ready": "
|
|
15
|
-
"merged": "
|
|
17
|
+
"queued": "Wartet",
|
|
18
|
+
"ready": "Bereit",
|
|
19
|
+
"merged": "Gesendet",
|
|
20
|
+
"retransmit": "Erneut gesendet",
|
|
21
|
+
"blocked": "Blockiert"
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script type="text/html" data-help-name="cmiconfig">
|
|
2
2
|
<p><strong>Konfiguration:</strong></p>
|
|
3
3
|
<ul>
|
|
4
|
-
<li><b>
|
|
4
|
+
<li><b> IP-Bereich:</b> IP Adressbereich in dem der UDP Port empfängt (Übliche Werte sind 0.0.0.0 oder 127.0.0.1)</li>
|
|
5
5
|
<li><b> Adresse:</b> Für das Senden von Nachrichten verwendete IP Adresse des TA C.M.I</li>
|
|
6
6
|
</ul>
|
|
7
7
|
<p><strong>CoE-Version:</strong></p>
|
|
8
8
|
<ul>
|
|
9
|
-
<li><b>
|
|
10
|
-
<li><b>
|
|
9
|
+
<li><b>Version 1:</b> Standard-Einstellung. Wertebereich: ± 32.767</li>
|
|
10
|
+
<li><b>Version 2:</b> Erweitert, benötigt CMI FW ≥ v1.43.1. Wertebereich: ± 2.147.483.647</li>
|
|
11
11
|
</ul>
|
|
12
12
|
<p>Siehe Einstellungen > CAN im Menü des CMI</p>
|
|
13
13
|
</script>
|