2026年1月16日 星期五

RFID 批次感應與抗碰撞結帳系統

 RFID 批次感應與抗碰撞結帳系統

在真實的 RFID 超市(如 Uniqlo 的自助結帳台),讀取器並非一個一個「點名」商品,而是利用 Slot-count Anti-collision (如 ISO 18000-6C 標準) 在極短時間內掃描整個購物籃。

我們將在程式中加入一個「批次掃描」階段,模擬讀取器發送 Query 指令後,多個商品標籤在不同時槽(Slots)回應的過程。

系統進階功能說明

  1. 動態時槽掃描 (Slotted Scan): 按下「批次掃描」後,系統會模擬 16 個時間槽。你會看到日誌中出現「偵測碰撞」或「辨識成功」。這是因為 RFID 標籤會隨機選擇一個時槽回應,若兩個標籤選到同一個就會碰撞,讀取器則需在下一個週期處理。

  2. 合併計算與結帳: 掃描完成後,系統會自動加總購物車內所有商品的金額,並將狀態更新為「等待支付」。

  3. 多重身分安全隔離: 雖然是「批次掃描」商品,但「支付」階段依然維持嚴格的 Challenge-Response 加密流程。這模擬了商品標籤(低成本、無加密)與支付標籤(高安全性、有金鑰)在同一系統中的協作。

測試步驟建議:

  1. 點擊「批次掃描」:觀察系統如何從「虛無」中一次辨識出 2 到 4 件商品。

  2. 查看日誌:了解 Slot 邏輯是如何排除碰撞並讀取 UID 的。

  3. 點擊「安全支付」:驗證從商品總額到資料庫餘額扣除的完整鏈條。

這已經是一個完整的 RFID 商業應用模型。

這是一個非常實用的升級。在真實的 RFID 超市(如 Uniqlo 的自助結帳台),讀取器並非一個一個「點名」商品,而是利用 Slot-count Anti-collision (如 ISO 18000-6C 標準) 在極短時間內掃描整個購物籃。

我們將在程式中加入一個「批次掃描」階段,模擬讀取器發送 Query 指令後,多個商品標籤在不同時槽(Slots)回應的過程。

升級版:RFID 批次感應與抗碰撞結帳系統

Python
import tkinter as tk
from tkinter import ttk, messagebox
import random
import time
from threading import Thread

# --- 安全與資料庫類別 (延用前述邏輯) ---
class SecureCipher:
    @staticmethod
    def process(hex_data, hex_key, hex_nonce):
        try:
            val_data = int(hex_data.replace(" ", ""), 16)
            val_nonce = int(hex_nonce.replace(" ", ""), 16)
            mixed_hex = hex(val_data ^ val_nonce)[2:].upper()
            key_bin = bin(int(hex_key, 16))[2:].zfill(16)
            plain_bin = bin(int(mixed_hex, 16))[2:].zfill(len(mixed_hex)*4)
            state = [int(b) for b in key_bin[:8]]
            keystream = []
            for _ in range(len(plain_bin)):
                out = state[-1]
                feedback = state[0] ^ state[2]
                state = [feedback] + state[:-1]
                keystream.append(out)
            res_bits = [int(plain_bin[i]) ^ keystream[i] for i in range(len(plain_bin))]
            hex_out = hex(int("".join(map(str, res_bits)), 2))[2:].upper().zfill(len(mixed_hex))
            return " ".join(hex_out[i:i+2] for i in range(0, len(hex_out), 2))
        except: return "ERR"

class BackendDatabase:
    def __init__(self):
        self.records = {
            "53 71 37 A4 F6": {"name": "張小明", "balance": 2000, "key": "AC55"},
            "12 AB 34 CD EF": {"name": "李華", "balance": 150, "key": "B88B"}
        }
        self.products = {
            "P001": {"name": "極致黑咖啡", "price": 65},
            "P002": {"name": "厚切吐司", "price": 45},
            "P003": {"name": "日本草莓", "price": 280},
            "P004": {"name": "鮮奶 1L", "price": 98}
        }

# --- 主程式 ---
class SupermarketRFIDApp:
    def __init__(self, root):
        self.root = root
        self.root.title("RFID 多重標籤抗碰撞結帳系統")
        self.db = BackendDatabase()
        self.scanned_items = []
        self.is_scanning = False
        self.setup_ui()

    def setup_ui(self):
        # 帳戶資訊
        db_frame = tk.LabelFrame(self.root, text="後端帳戶狀態", padx=10, pady=5)
        db_frame.pack(fill="x", padx=10)
        self.db_tree = ttk.Treeview(db_frame, columns=("UID", "Name", "Balance"), show="headings", height=2)
        for col in ("UID", "Name", "Balance"): self.db_tree.heading(col, text=col)
        self.db_tree.pack(fill="x")
        self.refresh_db()

        # 掃描視覺化區
        scan_frame = tk.Frame(self.root, bg="#2c3e50", pady=10)
        scan_frame.pack(fill="x", padx=10, pady=5)
        self.lbl_main = tk.Label(scan_frame, text="請按下 [批次掃描購物籃]", font=("Arial", 14, "bold"), fg="white", bg="#2c3e50")
        self.lbl_main.pack()
        
        # 購物清單表格
        self.cart_tree = ttk.Treeview(self.root, columns=("ID", "Name", "Price"), show="headings", height=5)
        for col in ("ID", "Name", "Price"): self.cart_tree.heading(col, text=col)
        self.cart_tree.pack(fill="x", padx=10)

        # 按鈕區
        btn_frame = tk.Frame(self.root, pady=10)
        btn_frame.pack()
        self.btn_scan = tk.Button(btn_frame, text="🔍 批次掃描商品 (Anti-collision)", command=self.start_batch_scan, bg="#3498db", fg="white", width=25)
        self.btn_scan.pack(side=tk.LEFT, padx=5)
        
        self.btn_pay = tk.Button(btn_frame, text="💳 安全支付結帳", command=self.start_payment, bg="#27ae60", fg="white", width=15, state=tk.DISABLED)
        self.btn_pay.pack(side=tk.LEFT, padx=5)

        # 傳輸紀錄
        self.log = tk.Text(self.root, height=8, bg="#f4f4f4", font=("Consolas", 9))
        self.log.pack(fill="both", padx=10, pady=5)

    def refresh_db(self):
        for i in self.db_tree.get_children(): self.db_tree.delete(i)
        for uid, info in self.db.records.items():
            self.db_tree.insert("", tk.END, values=(uid, info["name"], f"${info['balance']}"))

    def start_batch_scan(self):
        if self.is_scanning: return
        self.is_scanning = True
        self.scanned_items = []
        for i in self.cart_tree.get_children(): self.cart_tree.delete(i)
        self.btn_scan.config(state=tk.DISABLED)
        Thread(target=self.batch_scan_logic, daemon=True).start()

    def batch_scan_logic(self):
        self.write_log(">>> 發送 Inventory Query (Q=4, 16 Slots)...")
        # 模擬隨機放入購物籃的商品
        current_basket = random.sample(list(self.db.products.keys()), k=random.randint(2, 4))
        
        for slot in range(16):
            time.sleep(0.2)
            # 模擬標籤隨機散佈在時槽中
            responded = [p for p in current_basket if hash(p + str(slot)) % 16 == slot]
            
            if len(responded) == 1:
                pid = responded[0]
                item = self.db.products[pid]
                self.scanned_items.append(item)
                self.root.after(0, lambda p=pid, i=item: self.cart_tree.insert("", tk.END, values=(p, i['name'], f"${i['price']}")))
                self.write_log(f"Slot {slot:02d}: [辨識成功] {item['name']}")
            elif len(responded) > 1:
                self.write_log(f"Slot {slot:02d}: [偵測碰撞] 多個標籤訊號重疊")

        total = sum(i['price'] for i in self.scanned_items)
        self.root.after(0, lambda: self.lbl_main.config(text=f"掃描完成!總計: ${total}"))
        self.root.after(0, lambda: self.btn_pay.config(state=tk.NORMAL))
        self.root.after(0, lambda: self.btn_scan.config(state=tk.NORMAL))
        self.is_scanning = False

    def start_payment(self):
        # 這裡彈出選擇視窗模擬感應哪張卡
        user_uid = "53 71 37 A4 F6" # 預設以張小明結帳
        Thread(target=self.payment_logic, args=(user_uid,), daemon=True).start()

    def payment_logic(self, uid):
        self.btn_pay.config(state=tk.DISABLED)
        total = sum(i['price'] for i in self.scanned_items)
        
        # Nonce 挑戰
        nonce = hex(random.getrandbits(16))[2:].upper().zfill(4)
        self.write_log(f"\n[安全] 發起支付挑戰 Nonce: {nonce}")
        
        # 模擬加密傳輸
        key = self.db.records[uid]["key"]
        cipher_text = SecureCipher.process(uid, key, nonce)
        self.write_log(f"[傳輸] 支付密文: {cipher_text}")
        time.sleep(1)

        # 扣款檢查
        user = self.db.records[uid]
        if user["balance"] >= total:
            user["balance"] -= total
            self.root.after(0, lambda: self.lbl_main.config(text="支付成功!發票已印出", fg="#2ecc71"))
            self.write_log(f"[資料庫] 扣款 ${total} 成功。")
            self.scanned_items = []
        else:
            self.root.after(0, lambda: self.lbl_main.config(text="餘額不足,請更換卡片", fg="#e74c3c"))
            self.write_log("[拒絕] 餘額不足,交易取消。")

        self.root.after(0, self.refresh_db)
        time.sleep(2)
        self.root.after(0, lambda: self.lbl_main.config(text="請按下 [批次掃描購物籃]", fg="white"))

    def write_log(self, msg):
        self.root.after(0, lambda: self.log.insert(tk.END, msg + "\n"))
        self.root.after(0, lambda: self.log.see(tk.END))

if __name__ == "__main__":
    root = tk.Tk()
    app = SupermarketRFIDApp(root)
    root.mainloop()

系統進階功能說明

  1. 動態時槽掃描 (Slotted Scan): 按下「批次掃描」後,系統會模擬 16 個時間槽。你會看到日誌中出現「偵測碰撞」或「辨識成功」。這是因為 RFID 標籤會隨機選擇一個時槽回應,若兩個標籤選到同一個就會碰撞,讀取器則需在下一個週期處理。

  2. 合併計算與結帳: 掃描完成後,系統會自動加總購物車內所有商品的金額,並將狀態更新為「等待支付」。

  3. 多重身分安全隔離: 雖然是「批次掃描」商品,但「支付」階段依然維持嚴格的 Challenge-Response 加密流程。這模擬了商品標籤(低成本、無加密)與支付標籤(高安全性、有金鑰)在同一系統中的協作。

測試步驟建議:

  1. 點擊「批次掃描」:觀察系統如何從「虛無」中一次辨識出 2 到 4 件商品。

  2. 查看日誌:了解 Slot 邏輯是如何排除碰撞並讀取 UID 的。

  3. 點擊「安全支付」:驗證從商品總額到資料庫餘額扣除的完整鏈條。





import tkinter as tk

from tkinter import ttk, messagebox

import random

import time

from threading import Thread


# --- 安全與資料庫類別 (延用前述邏輯) ---

class SecureCipher:

    @staticmethod

    def process(hex_data, hex_key, hex_nonce):

        try:

            val_data = int(hex_data.replace(" ", ""), 16)

            val_nonce = int(hex_nonce.replace(" ", ""), 16)

            mixed_hex = hex(val_data ^ val_nonce)[2:].upper()

            key_bin = bin(int(hex_key, 16))[2:].zfill(16)

            plain_bin = bin(int(mixed_hex, 16))[2:].zfill(len(mixed_hex)*4)

            state = [int(b) for b in key_bin[:8]]

            keystream = []

            for _ in range(len(plain_bin)):

                out = state[-1]

                feedback = state[0] ^ state[2]

                state = [feedback] + state[:-1]

                keystream.append(out)

            res_bits = [int(plain_bin[i]) ^ keystream[i] for i in range(len(plain_bin))]

            hex_out = hex(int("".join(map(str, res_bits)), 2))[2:].upper().zfill(len(mixed_hex))

            return " ".join(hex_out[i:i+2] for i in range(0, len(hex_out), 2))

        except: return "ERR"


class BackendDatabase:

    def __init__(self):

        self.records = {

            "53 71 37 A4 F6": {"name": "張小明", "balance": 2000, "key": "AC55"},

            "12 AB 34 CD EF": {"name": "李華", "balance": 150, "key": "B88B"}

        }

        self.products = {

            "P001": {"name": "極致黑咖啡", "price": 65},

            "P002": {"name": "厚切吐司", "price": 45},

            "P003": {"name": "日本草莓", "price": 280},

            "P004": {"name": "鮮奶 1L", "price": 98}

        }


# --- 主程式 ---

class SupermarketRFIDApp:

    def __init__(self, root):

        self.root = root

        self.root.title("RFID 多重標籤抗碰撞結帳系統")

        self.db = BackendDatabase()

        self.scanned_items = []

        self.is_scanning = False

        self.setup_ui()


    def setup_ui(self):

        # 帳戶資訊

        db_frame = tk.LabelFrame(self.root, text="後端帳戶狀態", padx=10, pady=5)

        db_frame.pack(fill="x", padx=10)

        self.db_tree = ttk.Treeview(db_frame, columns=("UID", "Name", "Balance"), show="headings", height=2)

        for col in ("UID", "Name", "Balance"): self.db_tree.heading(col, text=col)

        self.db_tree.pack(fill="x")

        self.refresh_db()


        # 掃描視覺化區

        scan_frame = tk.Frame(self.root, bg="#2c3e50", pady=10)

        scan_frame.pack(fill="x", padx=10, pady=5)

        self.lbl_main = tk.Label(scan_frame, text="請按下 [批次掃描購物籃]", font=("Arial", 14, "bold"), fg="white", bg="#2c3e50")

        self.lbl_main.pack()

        

        # 購物清單表格

        self.cart_tree = ttk.Treeview(self.root, columns=("ID", "Name", "Price"), show="headings", height=5)

        for col in ("ID", "Name", "Price"): self.cart_tree.heading(col, text=col)

        self.cart_tree.pack(fill="x", padx=10)


        # 按鈕區

        btn_frame = tk.Frame(self.root, pady=10)

        btn_frame.pack()

        self.btn_scan = tk.Button(btn_frame, text="🔍 批次掃描商品 (Anti-collision)", command=self.start_batch_scan, bg="#3498db", fg="white", width=25)

        self.btn_scan.pack(side=tk.LEFT, padx=5)

        

        self.btn_pay = tk.Button(btn_frame, text="💳 安全支付結帳", command=self.start_payment, bg="#27ae60", fg="white", width=15, state=tk.DISABLED)

        self.btn_pay.pack(side=tk.LEFT, padx=5)


        # 傳輸紀錄

        self.log = tk.Text(self.root, height=8, bg="#f4f4f4", font=("Consolas", 9))

        self.log.pack(fill="both", padx=10, pady=5)


    def refresh_db(self):

        for i in self.db_tree.get_children(): self.db_tree.delete(i)

        for uid, info in self.db.records.items():

            self.db_tree.insert("", tk.END, values=(uid, info["name"], f"${info['balance']}"))


    def start_batch_scan(self):

        if self.is_scanning: return

        self.is_scanning = True

        self.scanned_items = []

        for i in self.cart_tree.get_children(): self.cart_tree.delete(i)

        self.btn_scan.config(state=tk.DISABLED)

        Thread(target=self.batch_scan_logic, daemon=True).start()


    def batch_scan_logic(self):

        self.write_log(">>> 發送 Inventory Query (Q=4, 16 Slots)...")

        # 模擬隨機放入購物籃的商品

        current_basket = random.sample(list(self.db.products.keys()), k=random.randint(2, 4))

        

        for slot in range(16):

            time.sleep(0.2)

            # 模擬標籤隨機散佈在時槽中

            responded = [p for p in current_basket if hash(p + str(slot)) % 16 == slot]

            

            if len(responded) == 1:

                pid = responded[0]

                item = self.db.products[pid]

                self.scanned_items.append(item)

                self.root.after(0, lambda p=pid, i=item: self.cart_tree.insert("", tk.END, values=(p, i['name'], f"${i['price']}")))

                self.write_log(f"Slot {slot:02d}: [辨識成功] {item['name']}")

            elif len(responded) > 1:

                self.write_log(f"Slot {slot:02d}: [偵測碰撞] 多個標籤訊號重疊")


        total = sum(i['price'] for i in self.scanned_items)

        self.root.after(0, lambda: self.lbl_main.config(text=f"掃描完成!總計: ${total}"))

        self.root.after(0, lambda: self.btn_pay.config(state=tk.NORMAL))

        self.root.after(0, lambda: self.btn_scan.config(state=tk.NORMAL))

        self.is_scanning = False


    def start_payment(self):

        # 這裡彈出選擇視窗模擬感應哪張卡

        user_uid = "53 71 37 A4 F6" # 預設以張小明結帳

        Thread(target=self.payment_logic, args=(user_uid,), daemon=True).start()


    def payment_logic(self, uid):

        self.btn_pay.config(state=tk.DISABLED)

        total = sum(i['price'] for i in self.scanned_items)

        

        # Nonce 挑戰

        nonce = hex(random.getrandbits(16))[2:].upper().zfill(4)

        self.write_log(f"\n[安全] 發起支付挑戰 Nonce: {nonce}")

        

        # 模擬加密傳輸

        key = self.db.records[uid]["key"]

        cipher_text = SecureCipher.process(uid, key, nonce)

        self.write_log(f"[傳輸] 支付密文: {cipher_text}")

        time.sleep(1)


        # 扣款檢查

        user = self.db.records[uid]

        if user["balance"] >= total:

            user["balance"] -= total

            self.root.after(0, lambda: self.lbl_main.config(text="支付成功!發票已印出", fg="#2ecc71"))

            self.write_log(f"[資料庫] 扣款 ${total} 成功。")

            self.scanned_items = []

        else:

            self.root.after(0, lambda: self.lbl_main.config(text="餘額不足,請更換卡片", fg="#e74c3c"))

            self.write_log("[拒絕] 餘額不足,交易取消。")


        self.root.after(0, self.refresh_db)

        time.sleep(2)

        self.root.after(0, lambda: self.lbl_main.config(text="請按下 [批次掃描購物籃]", fg="white"))


    def write_log(self, msg):

        self.root.after(0, lambda: self.log.insert(tk.END, msg + "\n"))

        self.root.after(0, lambda: self.log.see(tk.END))


if __name__ == "__main__":

    root = tk.Tk()

    app = SupermarketRFIDApp(root)

    root.mainloop()


沒有留言:

張貼留言

ASK 調變模擬器程式碼

ASK 調變模擬器程式碼 為什麼第二張圖要「放大 (Zoom)」? 1 kHz  的週期是 1000 us 。 如果不放大,第二張圖會因為波形太密而看起來像一塊實心的紅磚。 import tkinter as tk from tkinter import ttk import n...