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: 'mytools',
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
- if (triggerMode === "auto") {
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
- if (enableDep === "true") {
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">自動 (持續偵測 Tag)</option>
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 讀寫控制模組 V9 (絕對穩定版)。</p>
148
- <h3>模式說明:</h3>
150
+ <p>這是一個全自動的 RFID 讀寫控制模組 V13 (半自動產線模式版)。</p>
151
+ <h3>讀取模式說明:</h3>
149
152
  <ul>
150
- <li><strong>讀取 (Read):</strong> 為確保通訊絕對穩定,單次讀取極限強制鎖定為 16 個 Word。</li>
151
- <li><strong>寫入 (Write) 自動分批:</strong> 你只需給定字串內容,大腦將自動計算長度,並在背景以「高延遲防撞機制」接力分批寫入,直到超長資料完美寫入完畢!</li>
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(); // 沒掃到 Tag,提早釋放總線讓別人用
207
+ releaseLock();
217
208
 
218
- if (node.enableDeparture && node.lastSeenTag && !node.departureTimer) {
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
- if (!(node.mode === "READ" && node.readTriggerMode === "auto")) {
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
- // 嘗試拿鎖,若被自動讀取佔用,就等它 0.2 秒,最多重試 5 次 (1秒)
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.0.0",
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" : "node-red-ui_media_viewer/ui_media_viewer.js"
9
+ "node-red-ui_media_viewer": "node-red-ui_media_viewer/ui_media_viewer.js"
10
10
  }
11
- }
12
- }
11
+ }
12
+ }