2026年1月15日 星期四

RFID reader-driven I-code 協議

RFID reader-driven I-code  協議 

I-Code 協議(特別是 NXP 的 I-Code SLI 系列)是典型的 Reader-driven 協議。它結合了 PollingSlotted ALOHA 的混合機制。讀取器發送 Inventory 指令,並指定 Time Slots(通常是 1、16 或 256 個槽位),標籤會根據其 UID 的一部分隨機選擇一個槽位回覆。

以下是一個基於 I-Code SLI (ISO 15693) 邏輯的 tkinter 模擬程式,模擬讀取器發送 Inventory 指令並逐一掃描 Slot 的過程。

I-Code (ISO 15693) 協議要點

  1. Inventory 指令與 Slots: 讀取器會發送一個 Inventory 指令來啟動流程。在 I-Code 協議中,標籤會根據其 UID 的最後 4 位元(若為 16 槽位模式)來決定自己在第幾個 Slot 回應。

  2. 碰撞機制: 在模擬器中,我故意將兩個標籤設定在 Slot 3。你會看到該槽位變成紅色並標示 Collision。在真實系統中,讀取器遇到碰撞後,會記錄下來,稍後再利用 Stay Quiet 或更長的前綴來解決衝突。

  3. 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) 的演算法說明:

  1. 偵測階段 (Detection): 讀取器發送 Inventory 指令。如果某個 Slot 同時有兩個標籤回覆,接收訊號會產生畸變,讀取器判定為 Collision

  2. 記憶階段 (Storage): 讀取器並不會在發現碰撞後立刻停下來,而是完成這一輪(16 個 Slot)的所有掃描,並記住哪些 Slot 發生了衝突。

  3. 解決階段 (Resolution): 讀取器會針對剛才發生衝突的 Slot 再次發送指令。這次它會帶入一個 Mask(遮罩)。例如:「請 UID 以 ...B 結尾且屬於 Slot 3 的標籤才回覆」。

    • 透過 Mask 的細分,原本在同一 Slot 的標籤會被分開到不同輪次處理。

  4. 終止條件: 直到所有的 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()

沒有留言:

張貼留言

RFID 系統的Stream Cipher(串流加密)

  RFID 系統的Stream Cipher(串流加密) 在 RFID 系統中,由於標籤(Tag)的運算資源有限,通常會採用 Stream Cipher(串流加密) 。其原理是利用一個「金鑰(Key)」與「初始向量(IV)」生成一段無限長的偽隨機位元流(Key Stream),...