RFID reader-driven I-code 協議
I-Code 協議(特別是 NXP 的 I-Code SLI 系列)是典型的 Reader-driven 協議。它結合了 Polling 與 Slotted ALOHA 的混合機制。讀取器發送 Inventory 指令,並指定 Time Slots(通常是 1、16 或 256 個槽位),標籤會根據其 UID 的一部分隨機選擇一個槽位回覆。
以下是一個基於 I-Code SLI (ISO 15693) 邏輯的 tkinter 模擬程式,模擬讀取器發送 Inventory 指令並逐一掃描 Slot 的過程。
I-Code (ISO 15693) 協議要點
Inventory 指令與 Slots:
讀取器會發送一個 Inventory 指令來啟動流程。在 I-Code 協議中,標籤會根據其 UID 的最後 4 位元(若為 16 槽位模式)來決定自己在第幾個 Slot 回應。
碰撞機制:
在模擬器中,我故意將兩個標籤設定在 Slot 3。你會看到該槽位變成紅色並標示 Collision。在真實系統中,讀取器遇到碰撞後,會記錄下來,稍後再利用 Stay Quiet 或更長的前綴來解決衝突。
EOF (End of Frame) 脈衝:
在自動模式下,每個 Slot 之間的轉換代表讀取器發送了一個 EOF 訊號,告訴所有標籤:「現在進入下一個時間槽」。
import tkinter as tk
from tkinter import ttk
import random
import time
from threading import Thread
class ICodeSimulator:
def __init__(self, root):
self.root = root
self.root.title("RFID I-Code SLI (ISO 15693) 模擬器")
# 標籤資料 (模擬 UID)
self.available_tags = [
{"uid": "E00401001", "slot": 0},
{"uid": "E00401002", "slot": 3},
{"uid": "E00401003", "slot": 3}, # 故意製造碰撞
{"uid": "E00401004", "slot": 7},
{"uid": "E00401005", "slot": 12}
]
self.num_slots = 16
self.current_slot = -1
self.is_running = False
self.step_mode = tk.BooleanVar(value=True)
self.step_trigger = False
self.setup_ui()
def setup_ui(self):
# 狀態面板
stat_frame = tk.Frame(self.root, bg="#f0f0f0", pady=10)
stat_frame.pack(fill="x")
self.lbl_command = tk.Label(stat_frame, text="指令: 等待中", font=("Consolas", 12, "bold"), fg="#d32f2f")
self.lbl_command.pack()
# 槽位視覺化區域 (16 個 Slot)
self.canvas = tk.Canvas(self.root, width=800, height=200, bg="white")
self.canvas.pack(pady=10)
self.slot_rects = []
for i in range(self.num_slots):
x0 = 20 + (i * 48)
rect = self.canvas.create_rectangle(x0, 50, x0+40, 100, fill="#eeeeee", outline="#999")
self.canvas.create_text(x0+20, 115, text=f"S{i}")
self.slot_rects.append(rect)
# 控制列
ctrl_frame = tk.Frame(self.root)
ctrl_frame.pack(pady=10)
self.btn_inventory = tk.Button(ctrl_frame, text="發送 Inventory 指令", command=self.start_inventory, bg="#4caf50", fg="white")
self.btn_inventory.pack(side=tk.LEFT, padx=5)
tk.Checkbutton(ctrl_frame, text="手動單步 (Next Slot)", variable=self.step_mode).pack(side=tk.LEFT, padx=10)
self.btn_next = tk.Button(ctrl_frame, text="下一步 >>", command=self.trigger_next, state=tk.DISABLED)
self.btn_next.pack(side=tk.LEFT, padx=5)
# 結果列表
self.tree = ttk.Treeview(self.root, columns=("Slot", "Result", "UID"), show="headings", height=8)
self.tree.heading("Slot", text="Slot #")
self.tree.heading("Result", text="通訊結果")
self.tree.heading("UID", text="接收到的 UID")
self.tree.column("Slot", width=80, anchor="center")
self.tree.pack(fill="both", padx=10, pady=10)
def trigger_next(self):
self.step_trigger = True
def start_inventory(self):
if self.is_running: return
self.is_running = True
self.current_slot = -1
self.btn_inventory.config(state=tk.DISABLED)
self.btn_next.config(state=tk.NORMAL if self.step_mode.get() else tk.DISABLED)
# 清除 UI
for rect in self.slot_rects:
self.canvas.itemconfig(rect, fill="#eeeeee")
for item in self.tree.get_children():
self.tree.delete(item)
Thread(target=self.inventory_process, daemon=True).start()
def inventory_process(self):
self.update_cmd("SENT: Inventory (16 Slots)")
for s in range(self.num_slots):
if not self.is_running: break
self.current_slot = s
# 視覺化當前 Slot
self.root.after(0, lambda: self.canvas.itemconfig(self.slot_rects[s], fill="#fff176"))
# 等待觸發
if self.step_mode.get():
self.step_trigger = False
while not self.step_trigger and self.is_running:
time.sleep(0.1)
else:
time.sleep(0.6)
# 檢查該 Slot 有多少標籤回應
responded_tags = [t for t in self.available_tags if t["slot"] == s]
if len(responded_tags) == 0:
self.log_result(s, "Empty (Idle)", "---", "#f5f5f5")
elif len(responded_tags) == 1:
uid = responded_tags[0]["uid"]
self.log_result(s, "Success", uid, "#c8e6c9")
else:
self.log_result(s, "COLLISION!", "Multiple Tags", "#ffcdd2")
self.update_cmd("COMPLETED: Inventory Done")
self.is_running = False
self.root.after(0, lambda: self.btn_inventory.config(state=tk.NORMAL))
def update_cmd(self, msg):
self.root.after(0, lambda: self.lbl_command.config(text=msg))
def log_result(self, slot, res, uid, color):
def update():
self.tree.insert("", tk.END, values=(f"Slot {slot}", res, uid))
self.canvas.itemconfig(self.slot_rects[slot], fill=color)
self.tree.yview_moveto(1)
self.root.after(0, update)
if __name__ == "__main__":
root = tk.Tk()
app = ICodeSimulator(root)
root.mainloop()
在 I-Code (ISO 15693) 協議中,當 Inventory 過程中發現碰撞(例如多個標籤落在同一個 Slot)時,讀取器會啟動抗碰撞循環 (Anti-collision Loop)。
主要的作法是:讀取器會發送帶有 Mask(遮罩) 的指令,要求「只有 UID 前幾位符合特定值的標籤才准回應」,藉此將原本發生衝突的標籤強行拆分到不同的掃描輪次中。
I-Code 抗碰撞循環模擬器
這個版本增加了「待辨識隊列」與「自動重試機制」,當偵測到碰撞後,會自動發起下一輪查詢。
抗碰撞循環 (Anti-collision Loop) 的演算法說明:
偵測階段 (Detection):
讀取器發送 Inventory 指令。如果某個 Slot 同時有兩個標籤回覆,接收訊號會產生畸變,讀取器判定為 Collision。
記憶階段 (Storage):
讀取器並不會在發現碰撞後立刻停下來,而是完成這一輪(16 個 Slot)的所有掃描,並記住哪些 Slot 發生了衝突。
解決階段 (Resolution):
讀取器會針對剛才發生衝突的 Slot 再次發送指令。這次它會帶入一個 Mask(遮罩)。例如:「請 UID 以 ...B 結尾且屬於 Slot 3 的標籤才回覆」。
終止條件:
直到所有的 Slot 要麼是「空閒(Empty)」,要麼是「辨識成功(Success)」,循環才會停止。
import tkinter as tk
from tkinter import ttk
import random
import time
from threading import Thread, Event
class ICodeProSimulator:
def __init__(self, root):
self.root = root
self.root.title("RFID I-Code SLI 專業控制面板")
# 標籤資料
self.all_tags = [
{"uid": "E00401...A1", "slot_id": 1},
{"uid": "E00401...B3", "slot_id": 3},
{"uid": "E00401...C3", "slot_id": 3},
{"uid": "E00401...D3", "slot_id": 3},
{"uid": "E00401...E7", "slot_id": 7}
]
# 狀態控制變數
self.is_running = False
self.is_paused = False
self.stop_requested = False
self.pause_event = Event()
self.pause_event.set() # 預設為非暫停狀態
self.round_count = 0
self.identified_uids = []
self.setup_ui()
def setup_ui(self):
# 狀態顯示欄
info_frame = tk.Frame(self.root, bg="#2c3e50", pady=10)
info_frame.pack(fill="x")
self.lbl_round = tk.Label(info_frame, text="等待操作...", font=("微軟正黑體", 14, "bold"), fg="#ecf0f1", bg="#2c3e50")
self.lbl_round.pack()
self.lbl_status = tk.Label(info_frame, text="請點擊 [啟動] 開始抗碰撞程序", font=("微軟正黑體", 10), fg="#bdc3c7", bg="#2c3e50")
self.lbl_status.pack()
# Slot 視覺化 (16個槽位)
self.canvas = tk.Canvas(self.root, width=800, height=120, bg="white")
self.canvas.pack(pady=10)
self.slot_rects = []
for i in range(16):
x0 = 20 + (i * 48)
rect = self.canvas.create_rectangle(x0, 30, x0+40, 70, fill="#ecf0f1", outline="#bdc3c7")
self.canvas.create_text(x0+20, 85, text=f"{i}", font=("Arial", 8))
self.slot_rects.append(rect)
# 核心控制按鈕區
ctrl_frame = tk.Frame(self.root)
ctrl_frame.pack(pady=10)
self.btn_run = tk.Button(ctrl_frame, text="▶ 啟動", command=self.start_process, bg="#27ae60", fg="white", width=10)
self.btn_run.pack(side=tk.LEFT, padx=5)
self.btn_pause = tk.Button(ctrl_frame, text="⏸ 暫停", command=self.toggle_pause, state=tk.DISABLED, width=10)
self.btn_pause.pack(side=tk.LEFT, padx=5)
self.btn_stop = tk.Button(ctrl_frame, text="⏹ 結束", command=self.terminate_process, state=tk.DISABLED, bg="#c0392b", fg="white", width=10)
self.btn_stop.pack(side=tk.LEFT, padx=5)
# 資料表格
self.tree = ttk.Treeview(self.root, columns=("Round", "Slot", "Event", "UID"), show="headings", height=8)
self.tree.heading("Round", text="輪次")
self.tree.heading("Slot", text="槽位")
self.tree.heading("Event", text="事件")
self.tree.heading("UID", text="辨識結果 / 狀態說明")
self.tree.column("Round", width=60, anchor="center")
self.tree.column("Slot", width=60, anchor="center")
self.tree.pack(fill="both", padx=10, pady=10)
def toggle_pause(self):
if not self.is_running: return
if self.is_paused:
self.is_paused = False
self.pause_event.set() # 恢復執行
self.btn_pause.config(text="⏸ 暫停", bg="SystemButtonFace")
self.update_info("執行中...", "程序已恢復")
else:
self.is_paused = True
self.pause_event.clear() # 阻塞執行
self.btn_pause.config(text="▶ 恢復", bg="#f1c40f")
self.update_info("已暫停", "點擊 [恢復] 繼續掃描")
def terminate_process(self):
self.stop_requested = True
self.is_paused = False
self.pause_event.set() # 如果暫停中,先解除阻塞才能結束
self.update_info("正在結束...", "正在停止所有程序")
def start_process(self):
# 初始化狀態
self.is_running = True
self.is_paused = False
self.stop_requested = False
self.pause_event.set()
self.round_count = 0
self.identified_uids = []
# UI 切換
self.btn_run.config(state=tk.DISABLED)
self.btn_pause.config(state=tk.NORMAL, text="⏸ 暫停", bg="SystemButtonFace")
self.btn_stop.config(state=tk.NORMAL)
for item in self.tree.get_children(): self.tree.delete(item)
Thread(target=self.main_loop, daemon=True).start()
def main_loop(self):
pending_work = [{"mask": "", "target_slot": None}]
while pending_work and not self.stop_requested:
current_task = pending_work.pop(0)
self.round_count += 1
mask = current_task["mask"]
self.update_info(f"第 {self.round_count} 輪掃描", f"目前掩碼: {mask if mask else 'None'}")
self.root.after(0, self.reset_slots)
current_round_collisions = []
for s in range(16):
# 檢查是否暫停
self.pause_event.wait()
# 檢查是否強制結束
if self.stop_requested: break
# 視覺化當前 Slot
self.root.after(0, lambda idx=s: self.canvas.itemconfig(self.slot_rects[idx], fill="#3498db"))
active_tags = [t for t in self.all_tags if t["slot_id"] == s and t["uid"] not in self.identified_uids]
time.sleep(0.3)
if len(active_tags) == 0:
self.log_step(self.round_count, s, "Empty", "無回應")
self.root.after(0, lambda idx=s: self.canvas.itemconfig(self.slot_rects[idx], fill="#ecf0f1"))
elif len(active_tags) == 1:
tag = active_tags[0]
self.identified_uids.append(tag["uid"])
self.log_step(self.round_count, s, "Success", tag["uid"])
self.root.after(0, lambda idx=s: self.canvas.itemconfig(self.slot_rects[idx], fill="#2ecc71"))
else:
self.log_step(self.round_count, s, "Collision", f"偵測到 {len(active_tags)} 衝突")
self.root.after(0, lambda idx=s: self.canvas.itemconfig(self.slot_rects[idx], fill="#e74c3c"))
current_round_collisions.append(s)
if self.stop_requested: break
if current_round_collisions:
for col_slot in current_round_collisions:
pending_work.append({"mask": f"S{col_slot}", "target_slot": col_slot})
time.sleep(1)
# 程序結束後的清理
status_msg = "程序已遭使用者終止" if self.stop_requested else "辨識完成"
self.update_info(status_msg, f"共運行 {self.round_count} 輪次")
self.reset_ui_after_run()
def reset_ui_after_run(self):
self.is_running = False
self.btn_run.config(state=tk.NORMAL)
self.btn_pause.config(state=tk.DISABLED, text="⏸ 暫停", bg="SystemButtonFace")
self.btn_stop.config(state=tk.DISABLED)
def reset_slots(self):
for r in self.slot_rects: self.canvas.itemconfig(r, fill="#ecf0f1")
def update_info(self, r_text, s_text):
self.root.after(0, lambda: self.lbl_round.config(text=r_text))
self.root.after(0, lambda: self.lbl_status.config(text=s_text))
def log_step(self, r, s, event, info):
self.root.after(0, lambda: self.tree.insert("", tk.END, values=(r, s, event, info)))
self.root.after(0, lambda: self.tree.yview_moveto(1))
if __name__ == "__main__":
root = tk.Tk()
app = ICodeProSimulator(root)
root.mainloop()
沒有留言:
張貼留言