NFC 打卡自习馆签到
💡 划重点
- 用 PN532 读卡,ESP32 联网,OLED 显示状态,按键用于手动确认或切换模式,LED 做打卡成功/失败提示。
- 目标成本 80 元内,适合自习馆、合作社、小型共享空间。
- 打卡数据经 Wi‑Fi 发送到飞书 webhook,管理员在飞书群实时看到签到记录。
- 核心链路:刷卡 → 本地校验/显示 → 联网上报 → 飞书通知。
需求洞察
自习馆和合作社的日常运营里,签到往往靠纸质登记、微信群接龙或前台人工确认。用户到了座位,先找笔、翻本子,或者掏手机在群里发“已到”,操作繁琐;管理员事后还得把散落的信息整理成表格,费时且容易漏记。对于按小时计费或需要统计自习时长的场景,签到不准会直接影响结算和座位调度。
目标用户是两类人:一是自习馆会员和合作社成员,他们希望“刷一下就走”,不想被签到流程打断学习状态;二是空间管理员,他们需要低成本、可追溯、能自动汇总的签到数据,而不是每天手工抄写。真实痛点是:签到动作要快、身份要准、记录要自动同步、设备要便宜且不依赖专人维护。
为什么值得做?ESP32 加 PN532 的方案成熟、开源资料多,80 元内能做出稳定原型。NFC 卡可以贴在会员卡、手机壳或钥匙扣上,用户无需打开 App。飞书 webhook 让数据直接进入群机器人,管理员不用登录后台。对小型自习馆来说,这套设备可以放在门口或每张桌子旁,即装即用,后期还能扩展占座检测、灯光联动和时长统计。
方案设计
感知层由 PN532 负责。它通过 I2C 与 ESP32 通信,读取 Mifare Classic 或 NTAG 卡片的 UID。OLED 同挂在 I2C 总线上,显示“请刷卡”“打卡成功”“卡未注册”等状态。按键用于两种场景:一是管理员短按进入注册模式,把新卡 UID 写入白名单;二是用户刷卡异常时,长按触发重新上报。
控制层是 ESP32。它维护一个本地卡号白名单,可以用数组或 Preferences 存储。读到 UID 后,先比对白名单,再决定是否上报。控制层还负责去重,比如同一张卡 30 秒内只记录一次,避免反复刷。
执行层包括 OLED、LED 和蜂鸣器(可选)。OLED 显示卡号后四位和打卡结果,LED 用绿色常亮 1 秒表示成功,红色快闪表示失败。如果加蜂鸣器,成功时短鸣一声,失败时长鸣一声,适合嘈杂环境。
日志/联网层由 ESP32 的 Wi‑Fi 完成。打卡成功后,把卡号、时间戳、设备编号组装成 JSON,POST 到飞书 webhook。飞书群机器人会以卡片消息形式展示“张三 已签到 09:32”。如果网络失败,ESP32 先把记录写入 SPIFFS 或 Preferences,恢复后再补传。设备编号用于多台设备区分,比如“前台机”“A 区机”。
物料清单
| 物料 | 数量 | 用途 | 估算单价 |
|---|---|---|---|
| ESP32 开发板 | 1 | 主控,Wi‑Fi 联网 | 22 元 |
| PN532 NFC 模块 | 1 | 读卡 | 28 元 |
| 0.96 寸 OLED | 1 | 显示状态 | 9 元 |
| 轻触按键 | 1 | 注册/重传 | 0.5 元 |
| LED 与电阻 | 2 | 状态指示 | 0.5 元 |
| 杜邦线/面包板 | 1 套 | 接线 | 5 元 |
| 外壳/卡贴 | 若干 | 固定与会员卡 | 8 元 |
| 合计 | 73 元 |
接线与结构
PN532 使用 I2C 模式:SDA 接 GPIO21,SCL 接 GPIO22,VCC 接 3.3V,GND 共地。OLED 也接同一组 SDA/SCL,地址不冲突:PN532 通常为 0x24,OLED 为 0x3C。LED 正极串 220Ω 电阻接 GPIO2,负极接地;按键一端接 GPIO4,另一端接地,使用内部上拉。若 PN532 的 I2C 地址被改成 0x3C,需要改回 0x24,否则与 OLED 冲突。结构上建议把 PN532 线圈朝外,OLED 朝上,按键隐藏在外壳侧面,避免用户误触。
核心代码
以下为 Arduino 骨架,可编译上传。需要安装 Adafruit PN532、Adafruit SSD1306、ArduinoJson。
#include <Wire.h>
#include <Adafruit_PN532.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#define SDA 21
#define SCL 22
#define LED 2
#define KEY 4
Adafruit_PN532 nfc(-1, -1);
Adafruit_SSD1306 oled(128, 64, &Wire;, -1);
const char* ssid = "YOUR_WIFI";
const char* pass = "YOUR_PASS";
const char* webhook = "https://open.feishu.cn/open-apis/bot/v2/hook/xxx";
uint8_t whitelist[][4] = {{0x12,0x34,0x56,0x78}};
unsigned long lastRead = 0;
bool checkCard(uint8_t* uid) {
for (auto &c; : whitelist) {
if (memcmp(c, uid, 4) == 0) return true;
}
return false;
}
void sendFeishu(String uidStr) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
http.begin(webhook);
http.addHeader("Content-Type", "application/json");
StaticJsonDocument<256> doc;
doc["msg_type"] = "text";
doc["content"]["text"] = "NFC签到:" + uidStr;
String body;
serializeJson(doc, body);
http.POST(body);
http.end();
}
void setup() {
pinMode(LED, OUTPUT);
pinMode(KEY, INPUT_PULLUP);
Wire.begin(SDA, SCL);
oled.clearDisplay();
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.println("NFC Check-in");
oled.display();
nfc.begin();
nfc.SAMConfig();
WiFi.begin(ssid, pass);
}
void loop() {
uint8_t uid[7], uidLen;
if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen;, 100)) {
if (millis() - lastRead < 30000) return;
lastRead = millis();
String uidStr = "";
for (int i = 0; i < uidLen; i++) {
if (uid[i] < 0x10) uidStr += "0";
uidStr += String(uid[i], HEX);
}
oled.clearDisplay();
oled.setCursor(0, 0);
if (checkCard(uid)) {
digitalWrite(LED, HIGH);
oled.println("OK " + uidStr);
sendFeishu(uidStr);
delay(1000);
digitalWrite(LED, LOW);
} else {
oled.println("UNKNOWN");
for (int i = 0; i < 3; i++) {
}
}
oled.display();
delay(1500);
}
if (digitalRead(KEY) == LOW) {
delay(200);
oled.println("REG MODE");
oled.display();
}
}
调试步骤
- 先只接 OLED,运行 I2C 扫描,确认地址 0x3C。
- 接入 PN532,扫描地址应为 0x24。若找不到,检查 PN532 的 DIP 开关是否在 I2C 模式。
- 烧录代码,串口查看 Wi‑Fi 是否连接成功。
- 用手机 NFC 或 Mifare 卡刷卡,观察 OLED 是否显示 UID。
- 把 UID 加入白名单,重新刷卡,确认 LED 亮起且飞书收到消息。
- 测试断网场景:拔掉路由器,刷卡后记录应暂存,恢复网络后补传。
- 最后固定外壳,调整 PN532 天线方向,确保刷卡距离 2 至 4 厘米。