news 2026/8/26 7:28:38

基于RFID的Key Fob刷卡答题游戏设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于RFID的Key Fob刷卡答题游戏设计与实现

把问答游戏做成“刷卡答题”,这件事我从第一次想到就觉得很值得写下来。所谓 Quiz Game Based on Key Fob,就是用车上那种遥控钥匙扣样式的 RFID 卡片,代替按钮和触屏,成为答题游戏的输入设备。玩家把钥匙扣往读卡器上一贴,上位机识别出这是哪张卡,再对应到某个选项,从而完成一次答题。

这个玩法最早是为了解决一个很现实的问题:年会和课堂上的问答互动,要么是按键抢答器,要么是手机扫码,前者容易误触,后者需要每个人都带手机,现场总有几个没电的。而 Key Fob 本身无源、无需电池、成本低,分发给玩家之后,它就是一个“握在手里的实体选项”。刷卡这个动作天然带一种仪式感,比敲键盘更有互动性,也更容易让观众理解“谁正在答题”。

这篇内容适合想尝试软硬件结合项目的开发者、做创客教育的老师、给公司搞团建活动的运营,以及所有想让“答题”这件事变得更有实感的玩家。我会从整体架构、硬件选型、上位机逻辑、通信协议到现场落地,一步步把整条实现路径讲清楚,给你一套可以复现、也能自由扩展的方案。

1. 整体设计:这是一套“刷卡事件驱动”的问答系统

1.1 玩法设定与场景适配

先想清楚一件事:这套系统到底怎么玩?最简单的版本是这样的——投影上显示一道选择题,两个选项分别标成 A 和 B,每个玩家手里拿一个 Key Fob,红色钥匙扣代表选 A,蓝色钥匙扣代表选 B。玩家把钥匙扣贴到读卡器上,系统读取 UID,根据预置的“UID → A/B”映射表,自动判定这道题选的是哪个选项,然后当场给出对错反馈,再进入下一题。

这个玩法只保留了“作答”这个核心动作,把检票、判定、计分全部交给后台,互动过程非常干净。它不依赖按键矩阵,不需要触屏校准,也不会出现几个人同时抢一个键盘的混乱局面。它真正适用的场景是:让参与者逐个、轮流、有仪式感地做选择,而不是追求高频次的抢答速度。

我做这套东西的时候,第一反应是它像个“刷卡开关”,但后来发现它更像一个“反向的门禁系统”。门禁是“卡对了,门开”,这里是“卡对了,题对”。二者本质上都是把一张卡的身份,转换成系统里的一次授权或一次选择。理解了这一层,后面扩展各种玩法就顺了。

1.2 为什么用 Key Fob,而不是按钮或触屏

对比下来,Key Fob 方案有几个优势非常明显。第一,物理隔离。玩家手里只有一张卡,没有其他任何按键,不存在“误按旁边按钮”的情况。第二,成本低。一颗 13.56MHz 的 RC522 读卡模块不过十来块钱,配套的钥匙扣卡一片就几块钱,整套硬件的成本甚至比一个体面点的游戏手柄还便宜。第三,可追踪。每一次刷卡都会生成一条带 UID、带时间戳的事件记录,这天然就是答题日志,活动结束直接导出 CSV 就能复盘。

当然它也有劣势:读卡时间大约在几十到几百毫秒,不适合需要毫秒级响应的动作游戏;RC522 的读卡距离只有 2~5 厘米,玩家需要“贴一下”而不是“扫一下”。但正是这个“贴一下”的距离,让问答过程的节奏感变得可控——一个人答完,下一个人再上,不会出现隔空抢答的混乱。

相比之下,触屏方案最容易被现场反光、误触和多人同时点击干扰;机械按键则要考虑键体老化、防误触逻辑和扩展布线。如果你想做一个稳定、低成本、带有实物交互感的答题系统,Key Fob 就是目前最优解之一。

1.3 系统分层与数据流转

整个系统从数据流上看是单向的、清晰的,我习惯分成三层。

感知层就是读卡器加主控:Key Fob 靠近 RC522,主控(比如 Arduino)通过 SPI 总线读取出卡片的 UID,然后把一次“刷卡事件”包装成一条结构化消息。

逻辑控制层是核心:主控把事件通过串口或 Wi-Fi 发给上位机,上位机解析事件、查映射表、和当前题目比对,得出对错,更新分数,决定下一题。

表现层负责把逻辑结果呈现出去:pygame 窗口或浏览器页面显示题目、反馈和计分,喇叭给出提示音,甚至接一个 LED 灯条做视觉反馈。

三个层各干各的,调试起来非常舒服。硬件部分出问题,只需要看主控串口有没有数据;逻辑出问题,只需要盯住上位机的状态变量;表现层出问题,直接改界面代码,不影响判定规则。这套分层思路,和我后来做的很多项目一致,越早定好边界,后面扩展越从容。

1.4 硬件选型:Arduino 有线派还是 ESP32 无线派

选主控是第一个岔路口。保守派方案是 Arduino UNO + RC522,用 USB 线连电脑,串口通信。这个方案胜在稳:UNO 是 5V 逻辑电平,RC522 模块板上通常自带稳压和电平转换,杜邦线接好就能跑;USB 虚拟串口在电脑上几乎不会掉线,非常适合现场活动。

激进派方案是 ESP32 + RC522,用板载 Wi-Fi 把刷卡事件通过 WebSocket 推给浏览器。好处是读卡器可以脱离电脑摆放,放在场地中间也不怕绊线。代价是你要多处理一套网络重连、消息确认和延迟问题,现场调试复杂度明显上升。

我的个人建议是:第一版先用 Arduino + 有线串口跑通逻辑,等一切稳定了,再把主控换成 ESP32 做无线化。不要一上来就网线、Wi-Fi 全堆上,问题一多你都不知道该查哪一层。另外一个折中方案是直接用带 USB 的读卡器(比如 ACR122U),它可以绕过单片机,直接通过 PC/SC 协议把卡号送到电脑,但这套方案的驱动在 Windows 和 Linux 下差别很大,不如 RC522 原生态。

2. 硬件准备:材料、接线和第一行读卡代码

2.1 物料清单与选购注意点

需要的东西不多,一张表列清楚。

物料规格建议说明
主控板Arduino UNO 或 ESP32第一版强烈建议 UNO
RFID 读卡器RC522,13.56MHz兼容 ISO 14443A 卡片
Key Fob 钥匙扣卡50 张左右买固定 UID 的,别买 UID 可写卡
杜邦线母对母,长度 10~20cm越长越容易引入干扰
面包板或洞洞板一个用来做固定和走线
蜂鸣器有源蜂鸣器可选,做答对答错声音反馈
显示设备电脑屏幕或投影上位机展示用

买 RC522 时要留个心眼,市面上模块有两种常见丝印,一种写着 SDA,一种写着 NSS,其实指的都是 SPI 片选脚 CS/SS。模块上的 3.3V 引脚一定要接主控的 3.3V,不要想当然接 5V,虽然部分模块带稳压,但我见过不少模块在 5V 下发热严重、读卡距离骤降。

Key Fob 这里要特别注意,买卡时看清楚是“UID 固定卡”还是“UID 可写卡”。做游戏场景务必选固定 UID,一块钱一张的普通钥匙扣卡就行。可写卡虽然便宜且方便复制,但如果有玩家手里正好有写卡器,他可以随时复制一张别人的卡,答题逻辑瞬间就崩了。

2.2 RC522 接线:SPI 引脚别接错

RC522 和主控之间走的是 SPI 总线,UNO 上接线如下。

RC522 引脚Arduino UNO 引脚
SDA/NSS10
SCK13
MOSI11
MISO12
IRQ不接
RST9
GNDGND
3.3V3.3V

如果你用的是 ESP32,最常用的组法是 SDA/NSS 接 5,SCK 接 18,MOSI 接 23,MISO 接 19,RST 接 12。不同开发板引脚定义五花八门,务必先查板子的 SPI 引脚表再接线。

接线的顺序也有讲究,我习惯先把 GND 和 3.3V 接好,再接 SCK、MOSI、MISO,最后接 SDA 和 RST。原因无他,先供电再接信号能减少插拔时的打火概率。杜邦线尽量用短线,超过 20cm 后 SPI 时钟信号质量会下降,读卡会变得时好时坏。如果条件允许,直接焊到洞洞板上,那是最稳的。

2.3 安装 MFRC522 库并读取第一张 UID

打开 Arduino IDE,在库管理器里搜索 MFRC522,安装 Miguel Balboa 版本,然后用下面这段代码读取卡片 UID。

#include <SPI.h> #include <MFRC522.h> #define RST_PIN 9 #define SS_PIN 10 MFRC522 mfrc522(SS_PIN, RST_PIN); void setup() { Serial.begin(115200); SPI.begin(); mfrc522.PCD_Init(); } void loop() { if (!mfrc522.PICC_IsNewCardPresent()) { return; } if (!mfrc522.PICC_ReadCardSerial()) { return; } Serial.print("UID:"); for (byte i = 0; i < mfrc522.uid.size; i++) { Serial.print(mfrc522.uid.uidByte[i], HEX); Serial.print(" "); } Serial.println(); mfrc522.PICC_HaltA(); }

这段代码会持续检测读卡器天线上有没有卡片。有卡片出现就读取 UID,然后通过串口打印出来。如果一切正常,打开串口监视器,波特率调成 115200,把钥匙扣贴上去,你会看到类似UID:a1 b2 c3 d4的输出。

注意 UID 打印出来可能是小写,也可能缺前导零,比如0a会被打印成a。这个细节后面做映射表时要统一处理,最简单的方法是上位机解析时统一upper()并补满 8 位十六进制。

2.4 给钥匙扣建立“身份映射表”

读到了 UID,下一步是给每张卡起一个“逻辑身份”。我做法很简单:把钥匙扣分组,红色标签的卡统一映射到选项 A,蓝色标签的卡统一映射到选项 B,再留一张特殊卡作为“重置/管理员卡”。

KEY_FOB_MAP = { "A1B2C3D4": "A", "E5F60718": "A", "9A0B1C2D": "B", "3C4D5E6F": "reset", }

这种“UID → 逻辑动作”的映射是整个系统里最重要的数据表。它的好处是,玩家手里的卡丢了、坏了,只需要在表里换一行 UID,不需要改任何硬件逻辑。活动开始前,把分发给玩家的所有钥匙扣都刷一遍,记录下 UID,再贴上对应的标签,这一步虽然琐碎,但能避免现场一半以上的纠纷。

3. 上位机问答引擎:状态机、题库和计分规则

3.1 游戏主循环用状态机来约束

问答游戏看起来简单,但实际跑起来很容易出幺蛾子。最常见的问题是:玩家刚答完一题,手还没离开读卡器,第二张卡又贴上来,系统就把下一题的答案也判了。这种问题靠 if 嵌套很难彻底防住,终极解法是引入状态机。

我把游戏拆成四个状态:QUESTION(等待答题)、LOCK(答题锁存)、FEEDBACK(反馈展示)、END(游戏结束)。只有处于 QUESTION 状态时,刷卡事件才有效;一旦收到有效刷卡,立刻切到 LOCK,处理完判定后进入 FEEDBACK,展示 2~3 秒,再切回 QUESTION。

state = "QUESTION" current_question = 0 score = 0 while True: event = read_serial_event() if state == "QUESTION" and event: card_name = KEY_FOB_MAP.get(event.uid, "unknown") if card_name in ("A", "B"): result = check_answer(card_name) show_feedback(result) state = "LOCK" elif state == "LOCK": time.sleep(2) current_question += 1 if current_question >= len(questions): state = "END" else: show_question(questions[current_question]) state = "QUESTION" elif state == "END": show_final_score(score)

这个状态流转看着简单,但它是整个游戏逻辑的心脏。LOCK 状态的价值不只是防止重复刷卡,它也给了系统一个“消化事件”的时间窗口,让题目切换不会出现在玩家还在贴卡的瞬间。

3.2 JSON 题库结构与编码问题

题库我建议直接用 JSON 文件管理,而不是写死在 Python 代码里。原因很现实:活动策划改题目往往很频繁,改 JSON 不需要重新跑代码,也不需要碰任何逻辑;而且 JSON 结构清晰,非程序员也能看懂。

[ { "id": 1, "question": "世界上最高的山峰是哪座?", "options": ["A. 珠穆朗玛峰", "B. 乔戈里峰"], "answer": "A" }, { "id": 2, "question": "太阳系中体积最大的行星是?", "options": ["A. 土星", "B. 木星"], "answer": "B" } ]

使用这个文件的时候,编码问题必须从一开始就处理好。Python 读文件一律用open("questions.json", encoding="utf-8")显式指定编码,别偷懒默认。Windows 下尤其容易踩坑,因为记事本保存 UTF-8 时可能带 BOM 头,导致 json.load 直接报错。我的做法是用 VS Code 打开,确认右下角编码是 UTF-8,保存后关闭“添加 BOM”选项。

另一个更隐蔽的问题在 Arduino 端。如果你在 Serial.print 里直接输出中文字符,最终上位机收到的字节很可能是什么编码都说不清,因为 Arduino 源码文件本身的编码和串口监视器解码方式都不一定一致。我后来定了一条规矩:串口只允许出现 ASCII 字符,任何中文内容都在上位机端根据题目数据生成。这条规矩帮我避开了所有乱码坑。

3.3 计分与反馈:怎么判定一次“有效答题”

先定义清楚什么是一次“有效答题”:系统处于 QUESTION 状态,收到一张卡,这张卡的 UID 能在映射表里找到,并且映射结果就是当前题目需要的选项。这里有个容易忽视的细节:如果玩家刷了一张“重置卡”,系统不应该把它当作一次答题,而应该执行重置游戏、回到第一题的逻辑。

计分规则我给出两套可选。简单版是答对加 10 分,答错不扣分,适合儿童和娱乐场合。刺激版是基础分 10 分,每道题从展示到刷卡之间耗时越短,额外加成越高,让现场更有紧张感。如果做团体赛,可以事先把玩家分成两组,分别用红色和蓝色钥匙扣,后台统计两个颜色的正确次数,最后按组汇总。

反馈环节不要小看。答对时我用绿色全屏 + 上升音调,答错时用红色 + 下降音调,还要在旁边显示正确答案。反馈持续 2 秒,足够现场观众看清,又不会拖慢节奏。音效文件用 pygame 播放,背景音交给现场音响,注意别让喇叭声音盖过主持人。

3.4 选 pygame 还是 Web 做展示层

展示层有两种主流选择,我分别踩过一遍。pygame 方案的优势是本地运行稳定,不依赖浏览器,适合直接接投影;劣势是 UI 排版需要自己画,改样式比较费劲。Web 方案的优势是界面用 HTML/CSS 随便调,手机也能同步看排行榜;劣势是串口在浏览器里读取要靠 Web Serial API,兼容性参差不齐,而且现场一旦 Wi-Fi 不稳定,整个系统就悬了。

如果走 Python 路线,我建议 pygame 展示 + pyserial 读串口,两个线程配合。一个线程持续读串口事件,把事件放进队列;主线程负责渲染和状态机,每次循环从队列取事件。

import pygame import serial import queue import threading event_queue = queue.Queue() def read_serial(): ser = serial.Serial("COM3", 115200, timeout=1) while True: line = ser.readline().decode("ascii", errors="ignore").strip() if line: event_queue.put(line) threading.Thread(target=read_serial, daemon=True).start()

如果走 Web 方案,更稳的做法是主控把事件通过串口发给一个本地 Python 服务,再用 Flask 或 FastAPI 把它转成 WebSocket 推送。浏览器只负责展示,不直接碰串口。这套架构兼容性最好,也是我现在推荐给大多数人的方案,因为它把“硬件适配”和“界面迭代”解耦了。

4. 软硬件通信协议:让刷卡事件变成游戏事件

4.1 串口参数:统一波特率才能对话

主控和上位机之间的通信,所有参数必须一致。波特率、数据位、停止位、校验位,少一个不对,上位机收到的就是乱码。我用的是 115200、8N1(8 数据位、无校验、1 停止位),这是 Arduino 虚拟串口最稳的组合之一。

如果你的答题现场有多台电脑,最好把所有串口参数写进一个配置文件里,别散落在代码各处。别觉得小题大做,我见过因为某台电脑换了 COM 口号、波特率没同步,导致整套系统只有图像没有数据的翻车现场。

4.2 自定义帧协议:文本行加校验

关于协议,我推荐先用文本行,不用二进制帧。原因是文本行用串口监视器一眼能看出来问题,调试成本极低;二进制帧虽然紧凑,但出错时你连肉眼排查的能力都没有。二进制协议留给报文很长、吞吐量很大的场景,对于刷卡答题这种几十字节的事件,文本行完全够用。

我用的格式是分号分隔的四个字段:

EVT;R1;A1B2C3D4;F2

字段含义分别是:事件类型、读卡器编号、UID(8 位大写十六进制)、校验字节。校验字节计算方式是把前三个字段的字符串逐字符做 XOR,结果转两位大写十六进制拼在末尾。这样设计的好处是,上位机解析时先算一遍校验,对不上直接丢弃,能过滤掉大部分串口噪声。

Arduino 端发送前,把 UID 统一转成大写十六进制字符串,并控制在 8 位,不足补前导零。

char uidStr[9]; snprintf(uidStr, sizeof(uidStr), "%02X%02X%02X%02X", mfrc522.uid.uidByte[0], mfrc522.uid.uidByte[1], mfrc522.uid.uidByte[2], mfrc522.uid.uidByte[3]); Serial.print("EVT;R1;"); Serial.print(uidStr); Serial.print(";"); Serial.println(calcChecksum("EVT;R1;")); // 这里只做示意

上位机解析我通常写成独立函数,方便单元测试:

def parse_event(line: str): parts = line.strip().split(";") if len(parts) != 4: return None event_type, reader_id, uid, checksum = parts if checksum != calc_xor_checksum(parts[:-1]): return None if event_type != "EVT": return None return {"reader": reader_id, "uid": uid}

这个协议从设计到落地只有几十行代码,但是把“刷卡”和“答题”两件事彻底分开了。以后哪怕要加温度传感器、加距离传感器,只需要扩一个事件类型字段,底层架构完全不用改。

4.3 防抖与去重:双端加锁

RC522 有一个非常典型的特性:卡片靠近天线时,如果主控循环足够快,同一张卡会在短时间内被PICC_IsNewCardPresent判定为“新卡”多次。如果不做防抖,一次贴卡可能触发三条 EVT,游戏直接连续判三次,分数混乱。

解决思路是“双端加锁”。主控端定义一个lastSeenTime,只有距离上次事件超过 1 秒的刷卡才向串口发送。

unsigned long lastEventTime = 0; const unsigned long debounceMs = 1000; void loop() { // ... 读取卡片 ... unsigned long now = millis(); if (now - lastEventTime < debounceMs) { return; } lastEventTime = now; // ... 发送事件 ... }

上位机端再做一层防御:记录最近一次有效刷卡的时间戳,如果两次事件间隔小于 1.5 秒,直接忽略。双保险的意义在于,即使主控因为某种原因没有防抖成功,上位机也能兜住。

4.4 多读卡器与抢答模式

如果你要支持红蓝两队同时抢答,就得接入多个读卡器。这里有个坑:RC522 使用 SPI 协议,一根 SPI 总线上可以挂多个从设备,但每个从机的 SS(片选)引脚必须独占一个数字引脚。

UNO 上可以把第一个 RC522 的 SDA 接 10,第二个接 8,SCK、MOSI、MISO 共用。代码里需要为每个读卡器创建一个 MFRC522 实例,循环检测哪一张卡被刷到,发送事件时带上 reader_id:

EVT;R1;A1B2C3D4;xx EVT;R2;E5F60718;xx

上位机根据 reader_id 判断是红队还是蓝队在答题,再结合 UID 判断具体选项。这一套扩展下来,你的答题系统就从“单人轮答”升级成了“团体对战”。协议里的 reader_id 字段,就是为这一天准备的。

5. 联调与落地:从开发台到活动现场

5.1 完整联调流程演示

联调阶段不要一上来就全流程跑,我习惯分三步走。第一步,只验证硬件链路:把 Arduino 和电脑连好,打开串口监视器,确认每张钥匙扣都能刷出稳定的 UID 和校验值。第二步,跑一个“裸解析脚本”,把串口事件打印成结构化的 dict,确认协议解析没问题。第三步,再把 pygame 界面接进来,做一轮完整的“刷 A → 判对 → 下一题”的模拟。

完整联调的演示流程是这样:启动上位机,看到题库加载成功,进入标题界面。用手里的空卡刷一次,系统提示“未登记的卡”,这本身就说明链路是通的。接着刷红色钥匙扣,系统识别选项 A,第一题正确答案如果是 A,画面变绿,分数加 10,2 秒后自动进下一题;如果刷的是蓝色钥匙扣,画面变红,显示正确答案。

最后测特殊场景:同一张卡连续刷两次,确认第二次被防抖拦截;在答题过程中刷“重置卡”,确认游戏从头开始;拔掉 USB 线再插回去,确认上位机能自动重连串口。这一整套过完,基本可以放心上活动现场了。

5.2 现场布置的实测经验

现场布置最容易翻车的问题不是代码,而是物理环境。RC522 这类读卡模块对金属极其敏感,直接放在金属桌面或金属面板上,读卡距离会从正常的 3、4 厘米骤降到 1 厘米以内,甚至完全读不到。解决办法是给读卡器垫一层亚克力板或塑料垫片,让它和金属面隔离开。

另一个经验是读卡器不要多个并排靠在一起,两个读卡器的天线之间至少要留出 10 厘米以上距离,否则射频会互相干扰。如果同时启用两个读卡器,最好把它们分别放在答题台的两头。

玩家手里钥匙扣的标签也要提前做好,A 组统一贴红色标签,B 组统一贴蓝色标签,标签上写大号字。不要只靠主持人喊“红色是 A”,现场一紧张,观众根本分不清谁该刷哪张卡。

5.3 扩展玩法:计时赛、排行榜、多组对战

如果活动结束后还有余力,强烈建议加这些扩展。计时赛逻辑很简单,每道题加一个倒计时,超时自动判错并切下一题;排行榜可以在 SQLite 里存每个玩家/每支队伍的积分,页面上实时刷新。做多组对战就按 4.4 节的多读卡器方案,把两组玩家的成绩分别累加,最后显示冠亚军的名字。

还可以预留一张“复活卡”,某张特殊 Key Fob 刷一下可以让答错扣掉的分数恢复一次,这招在现场特别能活跃气氛。本质上这些扩展都只是在“刷卡事件 → 游戏状态”之间塞入更多业务规则,协议和架构完全不需要动。

6. 常见问题排查与避坑实录

6.1 读卡距离短、时灵时不灵

最典型的故障是:开发时读卡距离有 3 厘米,到了活动现场变成 1 厘米,甚至要把卡按在读卡器上才识别。优先检查三件事:第一,RC522 的 VCC 有没有误接 5V,接 5V 模块会发热,读卡灵敏度直线下降;第二,读卡器周围有没有金属物体,有就垫高隔离;第三,杜邦线是不是太长,超过 20 厘米就考虑缩短或焊接。

实在要增大读卡距离,可以换用天线面积更大的读卡模块,或者调整 RC522 天线匹配电路里的电容参数,但普通项目不建议碰后者,容易调崩。

6.2 中文乱码:源头在编码,不在显示

如果你把题目信息、玩家姓名等中文内容放进 JSON,读出来显示成乱码,绝大多数情况是文件编码不对。统一按 UTF-8 保存文件,Python 读取时显式传encoding="utf-8",pygame 渲染中文用的字体文件要提前确认支持中文字符,比如思源黑体或者微软雅黑。

串口端一旦出现中文字符,排查难度会成倍增长,因为 Arduino 的源码编码和串口输出编码不一定一致。所以我的原则是:所有通过串口传输的数据,必须纯净 ASCII,中文只存在于上位机的 JSON 和界面层。这几乎是避免乱码最有效的手段。

6.3 不安全的串口输入:别把刷卡内容当命令执行

刷卡数据本质上是不可信的外部输入,尤其是你买的如果是 UID 可写卡,卡里的数据可以被任意改写。如果你在上位机里直接eval()或者把串口内容拼接到系统命令里执行,就等于给现场留了一个后门。

我见过一个反面案例:有人为了省事,把卡名直接写进卡里,上位机收到后直接exec一段脚本,结果一张被改过 UID 的卡轻松让系统执行了任意命令。正确的做法是:串口数据只做白名单匹配,UID 必须匹配^[0-9A-F]{8}$这种正则,匹配不上的一律忽略。协议里的校验字段也不能省,它能过滤掉串口噪声和错误字节。

6.4 一题重复计分:防抖不生效的连环坑

前面说了双端防抖,但如果你发现防抖设置后还是重复计分,还有两个容易被忽略的原因。第一,上位机的状态机里有多个入口同时消费同一个事件,导致事件被处理两遍;第二,pygame 主循环跑得太快,同一帧里处理了队列里残留的同一条消息。

我的排查方法是:在事件解析入口打一行带时间戳的调试日志,把每条原始消息、处理后结果和当前状态全部记下来。复现一次重复计分,看日志就一目了然。很多时候不是防抖没生效,是事件队列没有在状态切换时清理干净,或者队列里堆了上一条题的残留事件。

6.5 避免“认卡不认人”的问题

RC522 频率是 13.56MHz,它能识别到的不仅是你的钥匙扣卡,NFC 手机、公交卡、门禁卡,只要频率匹配都可能被读到。活动现场如果观众拿手机靠近读卡器,系统可能误判成一次答题。解决办法是维护一个“已登记 UID 白名单”,解析到事件后先查白名单,不在名单里的一律忽略。这个逻辑在 3.2 节的映射表里已经天然实现了,但要注意不要为了省事把KEY_FOB_MAP.get(uid, "unknown")的默认值设置得太宽松。

我个人在实际操作中的体会是,这类软硬件结合的项目,真正的难点从来不在“写代码”,而在于把硬件环境的变量控制住。读卡距离、供电质量、金属干扰、现场电磁环境,每一个因素都能让你的代码毫无用武之地。所以我强烈建议:至少提前一天到现场做全链路测试,而且测试时就要用当天实际要用的读卡器、钥匙扣和电脑,不要拿开发环境的结果想当然。

另外有一个小技巧值得分享:给每张钥匙扣做标识的时候,除了贴标签,还可以用不同颜色的挂绳区分阵营。红绳刷左边读卡器,蓝绳刷右边读卡器,视觉上比任何文字提示都直观。这套“Key Fob + Quiz Game”的架构,往后还能自然延伸到签到墙、密室逃脱门禁和答题闯关玩法,底层那一套刷卡事件协议完全不用重写,改业务逻辑就行。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 7:27:19

AI编程助手OpenClaw与腾讯云CVD云桌面融合部署实战指南

1. 项目缘起&#xff1a;当AI助手遇上云桌面&#xff0c;我的效率革命最近在折腾一个挺有意思的组合&#xff1a;把OpenClaw这个AI编程助手&#xff0c;塞进腾讯云CVD&#xff08;Cloud Virtual Desktop&#xff09;云桌面里。听起来可能有点“缝合怪”的意思&#xff0c;但实际…

作者头像 李华
网站建设 2026/8/26 7:26:23

大厂面试必备:业务结合型技术问题解析与应对策略

1. 面试场景解析&#xff1a;为什么大厂偏爱业务结合型问题&#xff1f; 最近帮团队面试了几位Java工程师候选人&#xff0c;发现一个有趣现象&#xff1a;纯技术问题大家答得都不错&#xff0c;但一旦问到"你们系统里如何保证分布式事务一致性"或"订单超时未支…

作者头像 李华
网站建设 2026/8/26 7:26:19

Python+Django协同过滤电影推荐网站毕业设计实战指南

简介&#xff1a;推荐系统作为信息过滤的重要手段&#xff0c;在视频、电商等场景中广泛应用。协同过滤算法通过分析用户历史行为&#xff0c;计算相似度并预测偏好&#xff0c;是构建个性化推荐的核心方法之一。Python凭借丰富的数据处理库和成熟的Web框架Django&#xff0c;成…

作者头像 李华
网站建设 2026/8/26 7:26:01

C语言realloc函数深度解析:从内存管理原理到安全编程实践

1. 从一次内存泄漏排查说起&#xff1a;为什么realloc不是简单的“重新分配”那天下午&#xff0c;我被一个线上服务的诡异崩溃搞得焦头烂额。服务在连续运行几天后&#xff0c;内存使用量会缓慢但坚定地攀升&#xff0c;最终触发OOM&#xff08;内存耗尽&#xff09;被系统杀死…

作者头像 李华
网站建设 2026/8/26 7:25:02

蓝桥杯单片机国赛核心方案:定时器扫描+PCA超声波测距

1. 项目概述&#xff1a;为什么蓝桥杯国赛偏爱“定时器扫描PCA超声波”这个组合&#xff1f; 蓝桥杯单片机组国赛题&#xff0c;尤其是第八届那套题&#xff0c;表面看是考一个超声波测距功能&#xff0c;但真正卡住90%选手的&#xff0c;从来不是HC-SR04模块怎么接线&#xff…

作者头像 李华
网站建设 2026/8/26 7:24:56

OpenIM如何保障10万人大群消息一致性:分布式架构与Seq机制详解

1. 项目概述&#xff1a;当“大群”遇上“一致性”的挑战在即时通讯领域&#xff0c;支撑一个10万人的超大群组&#xff0c;远不止是把服务器配置调高那么简单。最核心、也最让开发者头疼的问题之一&#xff0c;就是如何保证海量客户端与服务器之间数据状态的强一致性。想象一下…

作者头像 李华