这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【M5PAPERESP32EINKDEVKIT评测】M5Paper电子工牌实作

共1条 1/1 1 跳转至

【M5PAPERESP32EINKDEVKIT评测】M5Paper电子工牌实作

助工
2026-04-20 15:19:34     打赏
1. 项目概述

本项目是一套基于 M5Paper 客户端 + ESP32-P4 服务端的智能电子工牌系统。M5Paper 作为 BLE 外围设备和墨水屏显示终端,通过低功耗蓝牙与 P4 服务端通信,实现工牌信息展示、自动打卡、无线配置等功能。

这篇文章,给大家重点分享 M5Paper 客户端的实现细节,包括墨水屏 UI 设计、BLE 通信协议、本地数据存储、以及与 P4 服务端的交互逻辑。

项目的源码,可以直接访问:https://github.com/HonestQiao/e-badge


2. M5Paper 硬件平台

M5Paper 是本系统的显示终端,核心硬件参数如下:

参数规格
主控ESP32-D0WD
显示屏4.7 寸电子墨水屏,分辨率 540×960
颜色黑白灰度(16 级灰阶)
触控支持多点电容触控
连接WiFi、蓝牙 5.0
存储Flash + Preferences(掉电不丢失)
2.1 电子墨水屏特性

电子墨水屏(EPD)与传统 LCD/OLED 有本质区别,开发时必须特别注意:

  • 断电画面保持:屏幕内容在断电后仍然保持,这是墨水屏的核心优势

  • 刷新有延迟:全屏刷新需要约 500ms~1s,期间屏幕会闪烁

  • 禁止连续快速刷新:连续刷新会导致残影、闪烁,严重时会永久损坏屏幕

  • 高对比度适合静态显示:黑白分明的文字显示效果极佳,但不适合动画

因此,M5Paper 端的设计原则是:以静态展示为主,刷新间隔至少 2 秒以上


3. 系统架构

system_architecture.png

M5Paper 在系统中承担两个角色:

  1. BLE 外围设备(Peripheral):持续广播设备名称,供 P4 扫描发现

  2. 墨水屏显示终端:展示工牌信息、打卡状态、等待配置等界面


4. 核心功能模块4.1 工牌信息展示

工牌展示是系统的核心界面,采用竖屏居中布局,整体风格简洁正式,符合企业工牌视觉规范。

m5paper_show.jpg

4.1.1 屏幕布局参数

屏幕尺寸为 540×960 像素,各元素位置通过代码精确计算:

static constexpr int HEADER_H     = 80;   // 顶部标题栏高度
static constexpr int AVATAR_Y     = 140;  // 头像圆顶部 Y 坐标
static constexpr int AVATAR_R     = 70;   // 头像圆半径
static constexpr int NAME_Y       = 340;  // 姓名 Y 坐标
static constexpr int SEP_Y        = 400;  // 分隔线 Y 坐标
static constexpr int DEPT_Y       = 465;  // 部门 Y 坐标
static constexpr int TITLE_Y      = 540;  // 职位 Y 坐标
static constexpr int ID_Y         = 615;  // 工号 Y 坐标
static constexpr int STATUS_BAR_Y = 880;  // 底部状态栏顶部 Y 坐标
static constexpr int STATUS_BAR_H = 80;   // 底部状态栏高度
4.1.2 视觉层次设计

从上到下分为五个视觉层次:

  1. 顶部标题栏(黑色背景,白色文字)

    • 显示"电子工牌"四个字

    • 使用 efontCN_24 中文字体,正常字号

  2. 头像区域(居中圆形)

    • 绘制加粗圆框(3 层同心圆模拟边框粗细)

    • 圆内显示姓名首字(UTF-8 前 3 字节,即一个汉字)

    • 姓名首字使用 2 倍放大字体,作为视觉焦点

  3. 个人信息区(居中文字)

    • 姓名:2 倍放大字体,最醒目

    • 分隔线:双像素加粗横线,视觉分割

    • 部门:1.5 倍字体

    • 职位:1.5 倍字体

    • 工号:1.2 倍字体,灰色显示,弱化处理

  4. 底部状态栏(黑色背景,白色文字)

    • 显示打卡状态或提示信息

    • 与顶部标题栏形成视觉呼应

4.1.3 颜色方案

墨水屏使用灰度值(0~255)表示颜色:

static constexpr uint8_t C_WHITE = 255;  // 纯白
static constexpr uint8_t C_BLACK = 0;    // 纯黑
static constexpr uint8_t C_GRAY  = 240;  // 浅灰(工号等次要信息)

所有文字和图形采用高对比度黑白配色,确保在各种光照条件下清晰可读。灰色仅用于工号等次要信息,降低视觉干扰。

4.1.4 关键绘制代码
void Display::drawBadgeContent(const BadgeInfo& info, bool isConfigured) {
  auto& dsp = M5.Display;
  int cx = SCREEN_WIDTH / 2;  // 水平中心点

  dsp.startWrite();

  // 白色背景
  dsp.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, C_WHITE);

  // 顶部黑色标题栏
  dsp.fillRect(0, 0, SCREEN_WIDTH, HEADER_H, C_BLACK);
  dsp.setTextColor(C_WHITE);
  drawCenteredText("电子工牌", cx, 30, &fonts::efontCN_24, 1.0f);

  // 头像圆框(加粗)
  int circleCenterY = AVATAR_Y + AVATAR_R;
  drawThickCircle(cx, circleCenterY, AVATAR_R, C_BLACK, 3);

  // 姓名首字(居中于圆心,大号字体)
  String firstChar = info.name.substring(0, 3);  // UTF-8 一个汉字 = 3 字节
  dsp.setTextColor(C_BLACK);
  drawCenteredText(firstChar.c_str(), cx, circleCenterY, &fonts::efontCN_24, 2.0f);

  // 姓名(大号字体)
  drawCenteredText(info.name.c_str(), cx, NAME_Y, &fonts::efontCN_24, 2.0f);

  // 分隔线(加粗:绘制两条相邻像素线)
  dsp.drawLine(60, SEP_Y, SCREEN_WIDTH - 60, SEP_Y, C_BLACK);
  dsp.drawLine(60, SEP_Y + 1, SCREEN_WIDTH - 60, SEP_Y + 1, C_BLACK);

  // 部门、职位、工号
  drawCenteredText(info.dept.c_str(), cx, DEPT_Y, &fonts::efontCN_24, 1.5f);
  drawCenteredText(info.title.c_str(), cx, TITLE_Y, &fonts::efontCN_24, 1.5f);

  // 工号(灰色,弱化显示)
  char idBuf[48];
  snprintf(idBuf, sizeof(idBuf), "工号:%s", info.badgeId.c_str());
  dsp.setTextColor(C_GRAY);
  drawCenteredText(idBuf, cx, ID_Y, &fonts::efontCN_24, 1.2f);

  // 底部状态栏
  dsp.fillRect(0, STATUS_BAR_Y, SCREEN_WIDTH, STATUS_BAR_H, C_BLACK);
  dsp.setTextColor(C_WHITE);
  // ... 状态文字

  dsp.endWrite();
}

居中绘制技巧:使用 MC_DATUM(Middle Center)文本对齐模式,使 x, y 坐标成为文字的几何中心,简化居中计算。

void Display::drawCenteredText(const char* text, int x, int y,
                               const lgfx::IFont* font, float scale) {
  auto& dsp = M5.Display;
  dsp.setFont(font);
  dsp.setTextSize(scale);
  dsp.setTextDatum(MC_DATUM);  // 以文字中心为基准点
  dsp.drawString(text, x, y);
}

4.2 打卡状态

打卡状态显示在屏幕底部黑色状态栏中,有三种状态:

状态显示文字触发条件
未配置"未配置"本地无工牌信息
已配置未打卡"请尽快打卡"有工牌信息,但当日未打卡
已打卡"已打卡 HH:MM:SS"P4 发送打卡通知,显示打卡时间
4.2.1 打卡通知处理

P4 在检测到 M5Paper 靠近(RSSI ≥ -70dBm)且当日未打卡时,通过 BLE 发送打卡通知:

// P4 发送: "CHECKIN:09:15:32"
if (json.startsWith("CHECKIN:")) {
  String time = json.substring(8);
  display.showCheckInStatus(time);   // 更新屏幕显示
  storage.saveCheckInTime(time);     // 保存到本地
}

showCheckInStatus() 仅刷新底部状态栏区域,避免全屏重绘:

void Display::showCheckInStatus(const String& time) {
  checkInTime = time;
  auto& dsp = M5.Display;
  int cx = SCREEN_WIDTH / 2;

  // 仅重绘状态栏区域
  dsp.fillRect(0, STATUS_BAR_Y, SCREEN_WIDTH, STATUS_BAR_H, C_GRAY);
  dsp.setTextColor(C_WHITE);
  char buf[48];
  snprintf(buf, sizeof(buf), "已打卡 %s", time.c_str());
  drawCenteredText(buf, cx, STATUS_BAR_Y + STATUS_BAR_H / 2,
                   &fonts::efontCN_24, 1.0f);
  dsp.display();
}
4.2.2 跨天自动清除

P4 每日会同步当前日期到 M5Paper。当检测到日期变化时,M5Paper 自动清除打卡状态:

if (json.startsWith("TIME:")) {
  String date = json.substring(5);
  String savedDate = storage.loadCurrentDate();
  if (!savedDate.isEmpty() && savedDate != date) {
    // 跨天了,清除打卡状态
    storage.clearCheckInTime();
    display.clearCheckInStatus();
    // 恢复工牌显示,提示重新打卡
    BadgeInfo info = storage.load();
    if (info.configured) {
      display.showBadge(info, true);
      display.showPromptCheckIn();
    }
  }
  storage.saveCurrentDate(date);
}

4.3 等待配置界面

M5Paper 首次启动(或配置被清空后)会显示等待配置界面,提示用户在 P4 Web 管理页面进行配置。

m5paper_configure.jpg

4.3.1 界面元素
  • 顶部灰色标题栏:与已配置状态的黑色标题栏区分,表示未就绪

  • 问号圆圈:视觉提示设备尚未配置

  • "等待配置...":主提示文字

  • "请在P4管理页面配置":操作指引

  • 设备 ID:如 设备ID: D71F8A3C,供 Web 端识别选择

  • 底部状态栏:显示"未配置"

4.3.2 设备 ID 生成

设备 ID 基于 ESP32 的 eFuse MAC 地址生成,确保全局唯一:

String generateDeviceId() {
  uint64_t mac = ESP.getEfuseMac();
  char buf[13];
  sprintf(buf, "%02X%02X%02X%02X",
          (uint8_t)(mac >> 24), (uint8_t)(mac >> 16),
          (uint8_t)(mac >> 8),  (uint8_t)mac);
  return String(buf);}

取 MAC 地址后 4 字节(32 位),格式化为 8 位十六进制字符串,如 D71F8A3C。

4.3.3 配置清空触发条件

以下情况会触发配置清空,进入等待配置状态:

  1. 新设备首次被 P4 检测到:P4 数据库中无该设备配置,发送 CLEAR_CONFIG 指令

  2. Web 管理页面手动删除配置:P4 发送 CLEAR_CONFIG 指令

  3. M5Paper 本地恢复出厂设置:手动清除 Preferences

if (json == "CLEAR_CONFIG") {
  storage.clear();              // 清除工牌信息
  storage.clearCheckInTime();   // 清除打卡时间
  storage.clearCurrentDate();   // 清除日期
  display.clearCheckInStatus(); // 清除屏幕状态

  // ⚠️ 关键:延迟 2 秒再全屏刷新
  delay(2000);
  display.showWaitingConfig(deviceId);
}

4.4 配置解析与自动重启

当用户在 P4 Web 管理页面提交配置后,P4 通过 BLE 将 JSON 配置发送到 M5Paper。

4.4.1 配置 JSON 格式
{
  "name": "张三",
  "dept": "技术部",
  "title": "高级工程师",
  "id": "T10086"
}
4.4.2 配置解析流程
void parseConfig(const String& json) {
  JsonDocument doc;
  DeserializationError error = deserializeJson(doc, json);
  if (error) {
    Serial.print("[Config] JSON parse failed: ");
    Serial.println(error.c_str());
    return;
  }

  BadgeInfo info;
  info.name = doc["name"] | "";
  info.dept = doc["dept"] | "";
  info.title = doc["title"] | "";
  info.badgeId = doc["id"] | "";
  info.configured = true;

  if (info.name.isEmpty()) {
    Serial.println("[Config] Name is empty, ignoring");
    return;
  }

  // 保存到 Preferences
  storage.save(info);
  Serial.println("[Config] Saved to storage");

  // 刷新屏幕
  display.showBadge(info, true);
  Serial.println("[Config] Display updated");

  // 新配置下发后,如果没有当天打卡记录,提示打卡
  String savedDate = storage.loadCurrentDate();
  String savedTime = storage.loadCheckInTime();
  if (savedDate.isEmpty() || savedTime.isEmpty()) {
    display.showPromptCheckIn();
  }

  // 配置完成后自动重启,让 P4 重新检测并自动打卡
  Serial.println("[Config] Auto-reboot in 2s...");
  delay(2000);
  ESP.restart();
}
4.4.3 自动重启的设计意图

配置完成后自动重启有两个目的:

  1. 让 P4 重新检测:重启后 BLE 服务重新初始化,P4 的扫描循环会再次发现该设备,触发自动打卡流程

  2. 确保状态一致:重启后从 Preferences 重新加载配置,避免内存与存储不一致


5. 本地数据存储(Preferences)

M5Paper 使用 ESP32 的 Preferences 库实现掉电不丢失的本地存储。所有数据存储在 badge 命名空间下。

5.1 存储数据结构键名类型说明
nameString姓名
deptString部门
titleString职位
idString工号
configuredbool是否已配置
citimeString当日打卡时间(HH:MM:SS)
curdateString当前日期(YYYY-MM-DD,用于跨天判断)
5.2 Storage 类封装
class Storage {public:
  void init();
  BadgeInfo load();                    // 加载工牌信息
  void save(const BadgeInfo& info);    // 保存工牌信息
  void clear();                        // 清除工牌信息

  String loadCheckInTime();            // 加载打卡时间
  void saveCheckInTime(const String& time);
  void clearCheckInTime();

  String loadCurrentDate();            // 加载当前日期
  void saveCurrentDate(const String& date);
  void clearCurrentDate();
};
5.3 读写实现

Preferences 以键值对形式存储,读写时自动处理 Flash 擦写:

BadgeInfo Storage::load() {
  Preferences prefs;
  BadgeInfo info;

  if (!prefs.begin("badge", true)) {  // true = 只读模式
    info.configured = false;
    return info;
  }

  info.name = prefs.getString("name", "");
  info.dept = prefs.getString("dept", "");
  info.title = prefs.getString("title", "");
  info.badgeId = prefs.getString("id", "");
  info.configured = prefs.getBool("configured", false);

  prefs.end();
  return info;
}

设计要点

  • 每次读写都独立打开/关闭 Preferences,避免长时间占用

  • 只读模式(true)不会触发 Flash 写入,延长寿命

  • 所有数据使用同一个命名空间,简化管理


6. BLE 通信协议

M5Paper 作为 BLE 外围设备(Peripheral),P4 作为中心设备(Central)。通信基于 GATT 协议,使用自定义 UUID 的服务和特征值。

6.1 BLE 服务定义
#define SERVICE_UUID        "0000ABCD-0000-1000-8000-00805F9B34FB"
#define CHARACTERISTIC_UUID "00001234-0000-1000-8000-00805F9B34FB"
属性UUID权限
服务0000ABCD-0000-1000-8000-00805F9B34FB主服务
特征值00001234-0000-1000-8000-00805F9B34FB读 / 写
6.2 设备广播

M5Paper 启动后创建 BLE 服务并持续广播,设备名称格式为 M5B_ + 8 位设备 ID:

void BLEPeripheral::init(const String& deviceId) {
  deviceName = String("M5B_") + deviceId;  // 如: M5B_D71F8A3C

  BLEDevice::init(deviceName.c_str());
  pServer = BLEDevice::createServer();

  BLEService* pService = pServer->createService(SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE  );
  pCharacteristic->setCallbacks(new ConfigCallbacks());
  pService->start();

  BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  BLEDevice::startAdvertising();
}
6.3 消息协议

P4 与 M5Paper 之间通过特征值的 Write 操作传输消息:

消息方向格式说明
工牌配置P4 → M5Paper{"name":"...","dept":"...","title":"...","id":"..."}下发工牌信息
打卡通知P4 → M5PaperCHECKIN:HH:MM:SS当日首次打卡
时间同步P4 → M5PaperTIME:YYYY-MM-DD日期同步,用于跨天判断
清空配置P4 → M5PaperCLEAR_CONFIG清空本地配置
6.4 配置接收回调

当 P4 通过 BLE 写入配置时,触发 onWrite 回调:

class ConfigCallbacks : public BLECharacteristicCallbacks {public:
  void onWrite(BLECharacteristic* pCharacteristic) {
    String value = pCharacteristic->getValue();
    if (value.length() > 0 && gBLEPeripheral) {
      gBLEPeripheral->configJson = value;
      gBLEPeripheral->configReceived = true;
      Serial.print("[BLE] Config received: ");
      Serial.println(value);
    }
  }
};

主循环中检查配置标志并处理:

void loop() {
  blePeripheral.loop();

  if (blePeripheral.isConfigReceived()) {
    String json = blePeripheral.getConfigJson();
    parseConfig(json);           // 解析并处理配置
    blePeripheral.clearConfigFlag();
  }

  delay(100);
}

7. P4 Web 管理界面(简要)

P4 提供 Web 管理页面,用户通过浏览器访问 P4 的 IP 地址即可管理所有工牌设备。

device_checkin_list.png

device_check_list.png

device_info_list.png

device_config.png


7.1 页面结构

页面采用四标签设计:

标签功能
打卡记录显示所有设备的打卡历史,含姓名、部门、时间、RSSI
检测记录显示蓝牙扫描到的设备列表,实时更新
工牌信息显示已保存的所有工牌配置,支持删除
工牌配置选择在线设备,填写信息后下发到 M5Paper
7.2 配置流程
  1. 点击"工牌配置"标签,页面自动扫描并列出在线设备(广播名称前缀为 M5B_)

  2. 点击设备名称,自动从数据库加载已有配置填充表单(如无配置则留空)

  3. 填写姓名、部门、职位、工号

  4. 点击"发送配置到设备",P4 通过 BLE 将配置发送到 M5Paper

  5. M5Paper 收到配置后自动重启,P4 重新检测到后自动完成打卡


8. 墨水屏特殊处理

电子墨水屏的物理特性决定了必须采取特殊的刷新策略,否则可能损坏屏幕或导致设备异常。

8.1 刷新冷却控制

所有显示操作都经过刷新冷却检查,确保两次刷新间隔至少 2 秒:

#define REFRESH_COOLDOWN  2000  // 最小刷新间隔(ms)bool Display::canRefresh() {
  unsigned long now = millis();
  if (now - lastRefresh < REFRESH_COOLDOWN) return false;
  lastRefresh = now;
  return true;}
8.2 局部刷新优化

打卡状态更新时,仅重绘底部状态栏,避免全屏刷新:

// 仅刷新状态栏区域(540×80 像素)
dsp.fillRect(0, STATUS_BAR_Y, SCREEN_WIDTH, STATUS_BAR_H, C_GRAY);
// ... 绘制状态文字
dsp.display();

9. 代码结构
m5paper/
├── m5paper.ino          # 主程序:初始化、配置解析、主循环
├── config.h             # 配置常量:BLE UUID、屏幕尺寸、刷新控制
├── storage.h/cpp        # Preferences 存储封装
├── display.h/cpp        # 墨水屏 UI 绘制
└── ble_peripheral.h/cpp # BLE 外围设备:广播、GATT 服务、配置接收
9.1 各文件职责文件职责
m5paper.ino系统入口,负责初始化显示、存储、BLE,主循环处理配置接收
config.h集中管理可配置常量,如 UUID、设备前缀、屏幕参数
storage.cpp封装 Preferences 读写,提供 BadgeInfo 结构体存取
display.cpp所有屏幕绘制逻辑,含工牌界面、等待界面、状态更新
ble_peripheral.cppBLE 协议栈初始化,广播配置,处理 Central 的连接和写入
9.2 主程序流程
void setup() {
  Serial.begin(115200);

  display.init();              // 初始化墨水屏
  storage.init();              // 初始化存储

  deviceId = generateDeviceId();  // 生成唯一设备 ID

  BadgeInfo info = storage.load();
  if (info.configured) {
    // 已配置:显示工牌,恢复打卡状态
    display.showBadge(info);
    String savedCheckIn = storage.loadCheckInTime();
    if (!savedCheckIn.isEmpty()) {
      display.showCheckInStatus(savedCheckIn);
    } else {
      display.showPromptCheckIn();
    }
  } else {
    // 未配置:显示等待配置页
    display.showWaitingConfig(deviceId);
  }

  blePeripheral.init(deviceId);  // 启动 BLE 广播}void loop() {
  blePeripheral.loop();

  if (blePeripheral.isConfigReceived()) {
    String json = blePeripheral.getConfigJson();
    parseConfig(json);          // 解析配置/打卡通知/时间同步
    blePeripheral.clearConfigFlag();
  }

  delay(100);
}

10. 完整使用流程

以下是从零开始使用 M5Paper 电子工牌的完整流程:

full_flow.png

步骤 1:首次启动

M5Paper 上电后:

  1. 初始化墨水屏、Preferences、BLE

  2. 检查本地是否有工牌配置

  3. 无配置:显示等待配置界面,展示设备 ID(如 M5B_D71F8A3C)

  4. 开始 BLE 广播

步骤 2:Web 端配置

用户在浏览器中访问 P4 的管理页面:

  1. 点击"工牌配置"标签

  2. 在设备列表中找到 M5B_D71F8A3C

  3. 点击设备名称,自动加载已有配置(如有)

  4. 填写姓名、部门、职位、工号

  5. 点击"发送配置到设备"

步骤 3:M5Paper 接收配置

M5Paper 通过 BLE 收到配置 JSON:

  1. 解析 JSON,提取工牌信息

  2. 保存到 Preferences

  3. 刷新屏幕显示工牌信息

  4. 延迟 2 秒后自动重启

步骤 4:自动打卡

M5Paper 重启后:

  1. 重新初始化 BLE 并广播

  2. P4 的扫描循环检测到 M5B_D71F8A3C

  3. P4 检查数据库:该设备今日未打卡

  4. P4 发送 CHECKIN:HH:MM:SS 通知

  5. M5Paper 显示"已打卡"状态,保存打卡时间

步骤 5:日常使用
  • M5Paper 持续显示工牌信息和打卡状态

  • 墨水屏断电后画面保持,无需持续供电

  • 跨天时 P4 发送新的 TIME:YYYY-MM-DD,M5Paper 自动清除打卡状态,提示重新打卡





关键词: M5PAPERESP32EINKDEVKIT     电子    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]