node-red-contrib-my-tools 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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType('rfid-lazy',{
|
|
3
|
-
category: '
|
|
3
|
+
category: 'RFID',
|
|
4
4
|
color: '#87A980',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value:""},
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
$(".write-options").hide();
|
|
51
51
|
$(".read-options").show();
|
|
52
52
|
$(".length-row").show();
|
|
53
|
-
|
|
53
|
+
// 半自動與自動模式都需要顯示輪詢頻率
|
|
54
|
+
if (triggerMode === "auto" || triggerMode === "semi-auto") {
|
|
54
55
|
$(".auto-read-options").show();
|
|
55
56
|
} else {
|
|
56
57
|
$(".auto-read-options").hide();
|
|
@@ -63,7 +64,8 @@
|
|
|
63
64
|
$(".length-row").show();
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
// 半自動模式強制需要設定延遲時間來停止動作
|
|
68
|
+
if (enableDep === "true" || (mode === "READ" && triggerMode === "semi-auto")) {
|
|
67
69
|
$(".departure-timeout-row").show();
|
|
68
70
|
} else {
|
|
69
71
|
$(".departure-timeout-row").hide();
|
|
@@ -97,7 +99,8 @@
|
|
|
97
99
|
<label for="node-input-readTriggerMode"><i class="fa fa-refresh"></i> 讀取觸發方式</label>
|
|
98
100
|
<select id="node-input-readTriggerMode">
|
|
99
101
|
<option value="manual">手動 (需外部 Inject 觸發)</option>
|
|
100
|
-
<option value="auto">自動 (
|
|
102
|
+
<option value="auto">自動 (永遠在背景持續掃描)</option>
|
|
103
|
+
<option value="semi-auto">半自動 (Inject 啟動掃描,離開後停止)</option>
|
|
101
104
|
</select>
|
|
102
105
|
</div>
|
|
103
106
|
<div class="form-row auto-read-options">
|
|
@@ -144,10 +147,11 @@
|
|
|
144
147
|
</script>
|
|
145
148
|
|
|
146
149
|
<script type="text/html" data-help-name="rfid-lazy">
|
|
147
|
-
<p>這是一個全自動的 RFID 讀寫控制模組
|
|
148
|
-
<h3
|
|
150
|
+
<p>這是一個全自動的 RFID 讀寫控制模組 V13 (半自動產線模式版)。</p>
|
|
151
|
+
<h3>讀取模式說明:</h3>
|
|
149
152
|
<ul>
|
|
150
|
-
<li><strong
|
|
151
|
-
<li><strong
|
|
153
|
+
<li><strong>手動 (Manual):</strong> 收到 Inject 訊號才讀取一次。</li>
|
|
154
|
+
<li><strong>自動 (Auto):</strong> 永遠在背景連續掃描。</li>
|
|
155
|
+
<li><strong>半自動 (Semi-Auto):</strong> 平時待命,收到 Inject 觸發後開始連續掃描,直到 Tag 離開天線且超過「延遲時間」後,自動停止掃描並釋放資源,等待下一次 Inject。</li>
|
|
152
156
|
</ul>
|
|
153
157
|
</script>
|
|
@@ -3,7 +3,6 @@ module.exports = function(RED) {
|
|
|
3
3
|
RED.nodes.createNode(this, config);
|
|
4
4
|
var node = this;
|
|
5
5
|
|
|
6
|
-
// 讀取 UI 設定值
|
|
7
6
|
node.mode = config.mode;
|
|
8
7
|
node.writeStrategy = config.writeStrategy || "manual";
|
|
9
8
|
node.offset = parseInt(config.offset) || 0;
|
|
@@ -16,7 +15,6 @@ module.exports = function(RED) {
|
|
|
16
15
|
node.enableDeparture = (config.enableDeparture === "true" || config.enableDeparture === true);
|
|
17
16
|
node.departureTimeout = (parseFloat(config.departureTimeout) || 3) * 1000;
|
|
18
17
|
|
|
19
|
-
// 內部狀態變數
|
|
20
18
|
node.rfidState = "IDLE";
|
|
21
19
|
node.readTime = 0;
|
|
22
20
|
node.targetTag = [];
|
|
@@ -24,7 +22,6 @@ module.exports = function(RED) {
|
|
|
24
22
|
node.currentWriteText = "";
|
|
25
23
|
node.autoTimer = null;
|
|
26
24
|
|
|
27
|
-
// WRITE/CLEAR 專用分批處理變數
|
|
28
25
|
node.currentProcessOffset = 0;
|
|
29
26
|
node.processEndOffset = 0;
|
|
30
27
|
node.currentChunkLength = 0;
|
|
@@ -33,12 +30,9 @@ module.exports = function(RED) {
|
|
|
33
30
|
node.departureTimer = null;
|
|
34
31
|
node.watchdogTimer = null;
|
|
35
32
|
node.hardwareDelayTimer = null;
|
|
36
|
-
node.manualRetryTimer = null;
|
|
33
|
+
node.manualRetryTimer = null;
|
|
34
|
+
node.isSemiAutoRunning = false; // 【新增】半自動運行狀態標記
|
|
37
35
|
|
|
38
|
-
// ==========================================
|
|
39
|
-
// 【核心 1】全域互斥鎖 (Global Mutex Lock)
|
|
40
|
-
// 確保整個 Node-RED 同一時間只有一個節點能存取硬體
|
|
41
|
-
// ==========================================
|
|
42
36
|
function acquireLock() {
|
|
43
37
|
let lock = node.context().global.get("rfid_hardware_lock");
|
|
44
38
|
if (!lock) {
|
|
@@ -68,12 +62,12 @@ module.exports = function(RED) {
|
|
|
68
62
|
if (node.rfidState !== "IDLE") {
|
|
69
63
|
if (node.rfidState !== "CHECK_TAG") {
|
|
70
64
|
node.warn("看門狗警報:Modbus 掉線或無回應,強制解除忙碌並釋放總線鎖!");
|
|
71
|
-
if (!(node.mode === "READ" && node.readTriggerMode === "auto")) {
|
|
65
|
+
if (!(node.mode === "READ" && (node.readTriggerMode === "auto" || node.readTriggerMode === "semi-auto"))) {
|
|
72
66
|
node.send([null, null, { payload: "錯誤: 底層通訊掉線,已自動重置", timeout: true }]);
|
|
73
67
|
}
|
|
74
68
|
}
|
|
75
69
|
node.rfidState = "IDLE";
|
|
76
|
-
releaseLock();
|
|
70
|
+
releaseLock();
|
|
77
71
|
}
|
|
78
72
|
}, 6000);
|
|
79
73
|
}
|
|
@@ -86,13 +80,11 @@ module.exports = function(RED) {
|
|
|
86
80
|
resetWatchdog();
|
|
87
81
|
}
|
|
88
82
|
|
|
89
|
-
//
|
|
90
|
-
// 【核心 2】自動讀取靜音迴避機制
|
|
91
|
-
// ==========================================
|
|
83
|
+
// 自動讀取(永遠在背景執行)
|
|
92
84
|
if (node.mode === "READ" && node.readTriggerMode === "auto") {
|
|
93
85
|
node.autoTimer = setInterval(() => {
|
|
94
86
|
if (node.rfidState === "IDLE") {
|
|
95
|
-
if (acquireLock()) {
|
|
87
|
+
if (acquireLock()) {
|
|
96
88
|
prepareAndStartProcess(null);
|
|
97
89
|
}
|
|
98
90
|
}
|
|
@@ -107,7 +99,6 @@ module.exports = function(RED) {
|
|
|
107
99
|
releaseLock();
|
|
108
100
|
});
|
|
109
101
|
|
|
110
|
-
// 整理前置準備邏輯
|
|
111
102
|
function prepareAndStartProcess(msg) {
|
|
112
103
|
if (node.mode === "WRITE") {
|
|
113
104
|
let val = "";
|
|
@@ -116,11 +107,10 @@ module.exports = function(RED) {
|
|
|
116
107
|
else if (node.writeDataType === "global") val = node.context().global.get(node.writeDataConfig);
|
|
117
108
|
else val = node.writeDataConfig;
|
|
118
109
|
|
|
119
|
-
// 【終極字串淨化器】:杜絕 undefined 幽靈字串
|
|
120
110
|
if (val === undefined || val === null) val = "";
|
|
121
111
|
else if (typeof val === "object") { try { val = JSON.stringify(val); } catch(e) { val = String(val); } }
|
|
122
112
|
else val = String(val);
|
|
123
|
-
val = val.replace(/undefined/g, "");
|
|
113
|
+
val = val.replace(/undefined/g, "");
|
|
124
114
|
|
|
125
115
|
node.currentWriteText = val;
|
|
126
116
|
|
|
@@ -167,6 +157,11 @@ module.exports = function(RED) {
|
|
|
167
157
|
if (msg.payload === "reset") {
|
|
168
158
|
node.rfidState = "IDLE";
|
|
169
159
|
node.lastSeenTag = false;
|
|
160
|
+
node.isSemiAutoRunning = false;
|
|
161
|
+
if (node.autoTimer && node.readTriggerMode === "semi-auto") {
|
|
162
|
+
clearInterval(node.autoTimer);
|
|
163
|
+
node.autoTimer = null;
|
|
164
|
+
}
|
|
170
165
|
if (node.departureTimer) { clearTimeout(node.departureTimer); node.departureTimer = null; }
|
|
171
166
|
if (node.manualRetryTimer) { clearTimeout(node.manualRetryTimer); node.manualRetryTimer = null; }
|
|
172
167
|
clearAllTimers();
|
|
@@ -175,28 +170,24 @@ module.exports = function(RED) {
|
|
|
175
170
|
if (done) done(); return;
|
|
176
171
|
}
|
|
177
172
|
|
|
178
|
-
// ==========================================
|
|
179
|
-
// 【核心 3】嚴格路由:區分 Modbus 回傳 vs 手動 Inject
|
|
180
|
-
// ==========================================
|
|
181
173
|
let isModbusMsg = Array.isArray(msg.payload) ||
|
|
182
174
|
(msg.payload !== null && typeof msg.payload === 'object' &&
|
|
183
175
|
!Array.isArray(msg.payload) &&
|
|
184
176
|
('fc' in msg.payload || 'unitid' in msg.payload || 'value' in msg.payload || 'data' in msg.payload));
|
|
185
177
|
|
|
186
178
|
if (isModbusMsg) {
|
|
187
|
-
// 如果是 Modbus 廣播,但我沒拿鎖,或者是多餘的雜訊,直接無情丟棄!
|
|
188
179
|
let lock = node.context().global.get("rfid_hardware_lock");
|
|
189
180
|
if (lock !== node.id || node.rfidState === "IDLE") {
|
|
190
181
|
if (done) done(); return;
|
|
191
182
|
}
|
|
192
183
|
|
|
193
|
-
// --- 以下為拿著鎖的節點,專心處理狀態機 ---
|
|
194
184
|
clearAllTimers();
|
|
195
185
|
let data = msg.payload;
|
|
196
186
|
|
|
197
187
|
if (node.rfidState === "CHECK_TAG") {
|
|
198
188
|
if (data[0] > 0) {
|
|
199
189
|
node.lastSeenTag = true;
|
|
190
|
+
// 取消移開倒數(防抖動:如果 Tag 在離開時間內回來了,繼續掃描不中斷)
|
|
200
191
|
if (node.departureTimer) {
|
|
201
192
|
clearTimeout(node.departureTimer);
|
|
202
193
|
node.departureTimer = null;
|
|
@@ -213,17 +204,36 @@ module.exports = function(RED) {
|
|
|
213
204
|
|
|
214
205
|
} else {
|
|
215
206
|
node.rfidState = "IDLE";
|
|
216
|
-
releaseLock();
|
|
207
|
+
releaseLock();
|
|
217
208
|
|
|
218
|
-
|
|
209
|
+
// 【核心】半自動的智能停止邏輯
|
|
210
|
+
let shouldStartTimer = node.lastSeenTag && !node.departureTimer &&
|
|
211
|
+
(node.enableDeparture || (node.mode === "READ" && node.readTriggerMode === "semi-auto"));
|
|
212
|
+
|
|
213
|
+
if (shouldStartTimer) {
|
|
219
214
|
node.departureTimer = setTimeout(() => {
|
|
220
|
-
let warnMsg = { payload: "提醒: Tag 已移開", event: "tag_departed", timeout_seconds: (node.departureTimeout / 1000) };
|
|
221
|
-
node.send([null, null, warnMsg]);
|
|
222
215
|
node.lastSeenTag = false;
|
|
223
216
|
node.departureTimer = null;
|
|
217
|
+
|
|
218
|
+
if (node.enableDeparture) {
|
|
219
|
+
let warnMsg = { payload: "提醒: Tag 已移開", event: "tag_departed", timeout_seconds: (node.departureTimeout / 1000) };
|
|
220
|
+
node.send([null, null, warnMsg]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 延遲時間到,徹底關閉半自動掃描
|
|
224
|
+
if (node.mode === "READ" && node.readTriggerMode === "semi-auto") {
|
|
225
|
+
if (node.autoTimer) {
|
|
226
|
+
clearInterval(node.autoTimer);
|
|
227
|
+
node.autoTimer = null;
|
|
228
|
+
}
|
|
229
|
+
node.isSemiAutoRunning = false;
|
|
230
|
+
node.status({ fill: 'grey', shape: 'dot', text: '閒置 (半自動結束)' });
|
|
231
|
+
node.send([null, null, { payload: "半自動掃描結束,等待下次觸發", event: "semi_auto_stopped" }]);
|
|
232
|
+
}
|
|
224
233
|
}, node.departureTimeout);
|
|
225
234
|
}
|
|
226
|
-
|
|
235
|
+
|
|
236
|
+
if (!(node.mode === "READ" && (node.readTriggerMode === "auto" || node.readTriggerMode === "semi-auto"))) {
|
|
227
237
|
send([null, null, { payload: "錯誤: 範圍內無 Tag", error: true }]);
|
|
228
238
|
}
|
|
229
239
|
}
|
|
@@ -270,7 +280,7 @@ module.exports = function(RED) {
|
|
|
270
280
|
releaseLock();
|
|
271
281
|
let addr = (node.mode === "READ") ? 0x0300 : 0x0400;
|
|
272
282
|
let resetMsg = { payload: { value: [0], fc: 16, unitid: 1, address: addr, quantity: 1 } };
|
|
273
|
-
if (node.mode === "READ" && node.readTriggerMode === "auto") {
|
|
283
|
+
if (node.mode === "READ" && (node.readTriggerMode === "auto" || node.readTriggerMode === "semi-auto")) {
|
|
274
284
|
send([null, resetMsg, null]);
|
|
275
285
|
} else {
|
|
276
286
|
send([null, resetMsg, { payload: "錯誤: 執行逾時", timeout: true }]);
|
|
@@ -293,9 +303,8 @@ module.exports = function(RED) {
|
|
|
293
303
|
}
|
|
294
304
|
}
|
|
295
305
|
|
|
296
|
-
// ==== 全部處理完成,打包最終輸出 ====
|
|
297
306
|
node.rfidState = "IDLE";
|
|
298
|
-
releaseLock();
|
|
307
|
+
releaseLock();
|
|
299
308
|
|
|
300
309
|
let hexString = node.targetTag.map(b => ("0" + b.toString(16)).slice(-2).toUpperCase()).join("");
|
|
301
310
|
let outbuf = { tagID: hexString, mode: node.mode, offset: node.offset, length: node.length, userData: "" };
|
|
@@ -326,22 +335,37 @@ module.exports = function(RED) {
|
|
|
326
335
|
}
|
|
327
336
|
}
|
|
328
337
|
} else {
|
|
329
|
-
// ==========================================
|
|
330
|
-
// 【核心 4】外部手動觸發 (Inject) 的智能排隊系統
|
|
331
|
-
// ==========================================
|
|
332
338
|
if (node.rfidState !== "IDLE" || node.context().global.get("rfid_hardware_lock") === node.id) {
|
|
333
339
|
node.warn("忽略重複觸發:系統正在處理您上一次的指令。");
|
|
334
340
|
if (done) done(); return;
|
|
335
341
|
}
|
|
336
342
|
|
|
337
|
-
//
|
|
343
|
+
// 【核心】半自動觸發啟動:當接收到 Inject 訊號時
|
|
344
|
+
if (node.mode === "READ" && node.readTriggerMode === "semi-auto") {
|
|
345
|
+
if (node.isSemiAutoRunning) {
|
|
346
|
+
node.warn("忽略:半自動掃描已在進行中。");
|
|
347
|
+
if (done) done(); return;
|
|
348
|
+
}
|
|
349
|
+
node.isSemiAutoRunning = true;
|
|
350
|
+
node.status({ fill: 'blue', shape: 'dot', text: '半自動啟動,等待 Tag...' });
|
|
351
|
+
|
|
352
|
+
// 啟動定時輪詢
|
|
353
|
+
node.autoTimer = setInterval(() => {
|
|
354
|
+
if (node.rfidState === "IDLE") {
|
|
355
|
+
if (acquireLock()) {
|
|
356
|
+
prepareAndStartProcess(null);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}, node.autoReadInterval);
|
|
360
|
+
}
|
|
361
|
+
|
|
338
362
|
function tryManualTrigger(retryCount) {
|
|
339
363
|
let lock = node.context().global.get("rfid_hardware_lock");
|
|
340
364
|
if (lock && lock !== node.id) {
|
|
341
365
|
if (retryCount < 5) {
|
|
342
366
|
node.manualRetryTimer = setTimeout(() => tryManualTrigger(retryCount + 1), 200);
|
|
343
367
|
} else {
|
|
344
|
-
if (!(node.mode === "READ" && node.readTriggerMode === "auto")) {
|
|
368
|
+
if (!(node.mode === "READ" && (node.readTriggerMode === "auto" || node.readTriggerMode === "semi-auto"))) {
|
|
345
369
|
send([null, null, { payload: "錯誤: 總線持續忙碌中,請稍後再試", error: true }]);
|
|
346
370
|
}
|
|
347
371
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-my-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"node-red": {
|
|
5
5
|
"nodes": {
|
|
6
6
|
"my-mysql-extended": "node-red-contrib-mysql-extended-test/mysql-extended.js",
|
|
7
7
|
"rfid-helper": "node-red-contrib-rfid-helper/rfid-lazy.js",
|
|
8
8
|
"node-red-flowgobal-change": "node-red-flowgobal-change/variable-change.js",
|
|
9
|
-
"node-red-ui_media_viewer"
|
|
9
|
+
"node-red-ui_media_viewer": "node-red-ui_media_viewer/ui_media_viewer.js"
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
}
|
|
11
|
+
}
|
|
12
|
+
}
|