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

共1条 1/1 1 跳转至

【M5PAPERESP32EINKDEVKIT评测】用Arduino在M5Paper上复刻青萍温度计:M5Unified+DashIoT+BLE

助工
2026-04-13 00:34:34     打赏

前言

不久前我用 ESP-IDF v5.5 在 M5Paper 上把青萍蓝牙温湿度计的 V40 UI 磨到了像素级——本地显示、SHT30 读取、AXP192 电量图标,全部跑稳了。但我这人就是闲不住,总想着能不能再把数据推到手机上,搞个“墨水屏 + 手机端”双屏联动的玩法。

正好最近刷到了 Dash IoT App(iOS / Android)。这玩意儿有点意思:手机通过 BLE 直连 ESP32,然后可以在线拖拖拽拽自定义 Dashboard,TextBox、Time Graph、Dial 什么的随心所欲。对于咱这种既想折腾硬件又不想写 App 的嵌入式爱好者来说,简直是现成的上位机方案。

Dash IoT 在 Arduino 里有官方支持的 DashioESP 库,把 NimBLE 底层那一堆 GATT 初始化、广播包拼接、WHO 握手时序全包圆了。于是我心一横,把 ESP-IDF 那套 V40 绘制逻辑原样搬到 Arduino,再裹一层 Dash IoT BLE,搞出了这个能同时站在 M5Paper 桌面上和 Dash App 手机屏里的温湿度计。

项目源码https://github.com/HonestQiao/m5paper-weather/tree/main/arduino/m5paper_weather_dashio


一、硬件平台设备型号作用

主控板M5Paper (ESP32-D0WDQ6-V3)4.7" 960×540 EPD,16 级灰度
温湿度传感器SHT30 (板载)I2C 地址 0x44
电源管理AXP192电量百分比读取

M5Paper 没有给 SHT30 做高级封装,所以我直接通过 M5.In_I2C 发送原始 I2C 命令读取。


二、UI设计

界面还是延续之前ESP-IDF中的V40版本界面设计,继续用内置的 7 段数码管字体 fonts::Font7,通过 setTextSize(scale) 逼近 V40 设计的像素高度:

  • 温度整数:scale = 3.0f(≈144 px)

  • 湿度整数:scale = 2.0f(≈96 px)

  • 温度小数:scale ≈ 64/48(≈64 px)

  • 湿度小数:scale = 1.0f(≈48 px)

°C  % 依然是纯手绘简单图形,圆环、蓝牙 Material 图标、电池图标、上下云龙纹全都在。最关键的是,所有坐标我已经硬编码成了绝对整数值,从 ESP-IDF 搬过来的时候基本不用动,稳得很。

m5paper_weather_dashio_01-final.jpg


三、Dash IoT 了解官方文档:https://dashio.io/documents/3.1 体系入门

Dash IoT 并不是单纯的一个手机 App,而是一套完整的 IoT 设备 ↔ 手机屏 的协议体系。它由两部分组成:

  1. Dash IoT App(iOS / Android):负责扫描设备、加载 Dashboard、显示数据。

  2. DeviceView Builder(网页端):在线拖拽各种控件,设计好界面后导出 configC64Str,即发到你邮箱的一段 Base64 压缩字符串。

我看过 Dash IoT 的 Controls V2 文档,它支持的控件非常多。我们这个项目主要用到两类:

  • TextBox —— 单行数值显示,带标题、单位、阈值颜色。

  • Time Graph —— 时间序列折线图,可定义多条曲线、不同颜色、左右 Y 轴。

此外还有 Knob、Dial、Button、Status 等控件,扩展性很强,以后想加风扇控制、灯光亮度调节,只要在 DeviceView Builder 里多拖两个控件、改改 processIncomingMessage 就行。

Dash IoT 的所有通信都遵循同一种类 TSV 格式

  • 字段之间用 \t(Tab)分隔

  • 每条消息以 \n(换行)结束

这个 \n  Dashio 库里被定义为 END_DELIM。设备发给 App 的消息,以及 App 发给设备的命令,都靠这个格式解析。如果 \n 丢了,整条消息就会卡住不被处理。

3.2 协议深度解析

Dash App 识别一个设备时,并不是只看蓝牙 MAC,而是看三个信息的组合:

字段本项目中的值含义

Device TypeESP32_TypeDashboard 的模板钥匙,App 根据它来找对应的 layout
Device NameDashIO_M5Paper用户看到的设备名称
Device IDBLE MAC 地址唯一标识,格式如 3c:8a:1f:d7:a8:8a

这里有个容易踩的坑:广播名(peripheral local name)和 Device Name 不是一回事。DashioESP 的源码显示,广播名必须是 DashIO_ + Device Type = DashIO_ESP32_Type,而 Device Name 是在 WHO 握手回复里告诉 App 的。如果开发者把广播名直接设成了设备名称(比如 DashIO_M5Paper),Dash App 就会搜不到设备。

App 连接设备后,不是直接读取数据,而是先走一套握手三板斧

1) WHO

App 发送:

WHO

设备必须回复:

	3c:8a:1f:d7:a8:8a	WHO	ESP32_Type	DashIO_M5Paper	2

注意行首有一个 \t,后面依次是 Device ID、命令名、Device Type、Device Name、Layout Revision。

2) CONNECT

App 发送:

CONNECT

设备回复:

	3c:8a:1f:d7:a8:8a	CONNECT

同时我习惯在 CONNECT 回复后立刻主动推一次当前状态,这样用户连上后不用手动刷新就能看到温湿度。

3) STATUS

当用户在 App 里下拉刷新,或切换回前台时,App 会发送:

STATUS

设备回复当前的 textBox、timeGraphLine 等控件状态。

Dash IoT 的数据推送不是 JSON,而是纯 Tab 分隔字符串。核心格式如下:

TextBox 推送:

textBox	TB01	25.6
  • textBox = 控件类型

  • TB01 = 我在 DeviceView Builder 里给这个 TextBox 设的 Control ID

  • 25.6 = 数值(如果 TextBox 是 Number format,这里绝对不能带单位后缀

Time Graph 线条定义:

timeGraphLine	IDTG	L1	Temp	line	red	yLeft

定义了一条名为 L1、标签 Temp、红色、实线、左 Y 轴的曲线。

Time Graph 数据点:

timeGraphPoint	IDTG	L1	2025-04-12T10:00:00Z	25.6

Dashio 库在内部会自动帮我们把 getTimeGraphPoint 生成成带时间戳的完整格式,不需要我手写 ISO 时间。

3.3 Layout 与 configC64Str

configC64Str 是一个 Base64 编码的 Dashboard 布局压缩包,里面包含了:

  • 页面尺寸和背景色

  • 每个控件的类型、位置、大小、Control ID

  • 颜色主题、字体大小、阈值规则等

把它硬编码进 Sketch 后,用户在 App 里点击 Get Layout,设备就会通过 Notify 把这段 Base64 字符串分段发给 App,App 解码后立刻渲染出对应的控制面板。

这是我在调试时卡了半小时的地方。DashDevice 的构造函数第三个参数是 revision:

DashDevice dashDevice("ESP32_Type", configC64Str, 2);

如果 revision 写成 0,或者你不传第三个参数(默认也是 0),App 点 Get Layout 时会弹出一个红色提示:

Attention: Layout Revision number must be greater than zero to download the layout.

这是因为 App 用 revision 来做本地缓存校验,只有大于 0 才认为这是一个合法的 layout。

如何获取/替换 Layout?

  • 方法一:快速上手(本项目当前做法)
    直接复制 DashIO_ESP32_temperature 示例自带的 configC64Str,它自带 TB01、TB02、TB03  IDTG,能立刻跑通。

  • 方法二:自定义高级玩法
    打开 DashIO DeviceView Builder,在线拖拽 TextBox 和 Time Graph,把 Control ID 设成 TB01、TB02、IDTG,调好颜色和位置后点击 Export Layout。过一会儿你的邮箱会收到一封邮件,正文里就是新的 configC64Str,把它替换到代码里,revision 加 1,重新烧录即可。


四、Arduino 库依赖

 Arduino IDE  PlatformIO 中安装以下库:

  • M5Unified — M5Stack 新一代统一硬件抽象层

  • M5GFX — LovyanGFX 的 M5 官方封装,负责墨水屏绘制

  • Dashio — Dash IoT 协议基础库(消息格式、Control ID 定义)

  • DashioESP — ESP32 专用传输层(BLE / TCP / MQTT)

这里要说明一下:DashioESP 并不是自己重写了一套 BLE 协议栈,它只是在你看不见的底层调用了 ESP-IDF NimBLE。所有 GAP 广播、GATT Service/Characteristic 注册、Notify 发送、连接状态机,本质上还是 NimBLE。但 DashioESP 把这些极其琐碎的 C 结构体初始化、UUID 字节序处理、Scan Response 拼接都包好了,你只需要写两行代码就能开始广播。

在 Arduino IDE 中,Board 选择:M5Stack / M5Paper


五、核心代码解析5.1 屏幕驱动:M5Unified 是真的香

在 Arduino 框架下,M5Paper 的 IT8951 电子纸屏幕通过 M5Unified 库驱动。初始化两行代码就完事:

auto cfg = M5.config();M5.begin(cfg);

M5.begin(cfg) 内部把 EPD 控制器、AXP192 电源管理、I2C 总线、SD 卡槽全初始化好了。真正绘图时还是标准三板斧:

auto& dsp = M5.Display;dsp.startWrite();/* fillEllipse、drawString、fillRect 等绘图调用 */dsp.endWrite();dsp.display();  // 触发 EPD 全局刷新

5.2 温湿度读取:SHT30 极简驱动

M5Paper 板载 SHT30,I2C 地址 0x44。Arduino 下 M5Unified 提供 M5.In_I2C,用法和 ESP-IDF 的 m5::In_I2C 几乎一样,就是 vTaskDelay 换成了 delay()。

class SHT30 {
    uint8_t _addr = 0x44;public:
    bool read(float &temp, float &humi) {
        uint8_t cmd[2] = { 0x2C, 0x06 };
        if (!M5.In_I2C.start(_addr, false, 100000)) return false;
        if (!M5.In_I2C.write(cmd, 2)) { M5.In_I2C.stop(); return false; }
        M5.In_I2C.stop();
        delay(15);

        uint8_t buf[6];
        if (!M5.In_I2C.start(_addr, true, 100000)) return false;
        if (!M5.In_I2C.read(buf, 6, false)) { M5.In_I2C.stop(); return false; }
        M5.In_I2C.stop();

        uint16_t t_raw = ((uint16_t)buf[0] << 8) | buf[1];
        uint16_t h_raw = ((uint16_t)buf[3] << 8) | buf[4];
        temp = -45.0f + 175.0f * ((float)t_raw / 65535.0f);
        humi = 100.0f * ((float)h_raw / 65535.0f);
        return true;
    }};

5.3 电量读取:AXP192 一行搞定

M5Paper 的电源管理 IC 是 AXP192。M5Unified 封装为 M5.Power,读电量一行代码:

g_batt = M5.Power.getBatteryLevel();  // 返回 0~100

直接拿来驱动电池图标的填充条即可。

5.4 DashDevice / DashBLE 初始化顺序

DashDevice dashDevice("ESP32_Type", configC64Str, 2);DashBLE    ble_con(&dashDevice, true);ble_con.setCallback(&processIncomingMessage);ble_con.begin();dashDevice.setup(ble_con.macAddress(), "DashIO_M5Paper");

关键点:dashDevice.setup() 必须放在 ble_con.begin() 之后。因为 setup() 内部要读取 NimBLE 的 MAC 地址作为 deviceID,如果 NimBLE 还没初始化完毕,读到的 MAC 是空的,App 在 WHO 阶段就会判定设备非法并断开。

5.5 BLE 消息回调处理

static void processIncomingMessage(MessageData *messageData) {
    switch (messageData->control) {
    case status:
    case connect:
        processStatus(messageData->connectionType);
        break;
    default:
        break;
    }}

DashioESP 库已经帮我们把原始 Tab 分隔字符串解析成了 MessageData 结构体,我只需要根据 control 字段分发即可。who 的处理库内部已经自动完成,不需要开发者手写回复。

5.6 TextBox 纯数字陷阱

Dash App 的 TextBox 如果设成了 Number format,但它其实并不会做智能单位剥离,而是直接用 strtof 或类似函数尝试解析整行字符串。如果我在 payload 里加了单位后缀:

message += dashDevice.getTextBoxMessage("TB01", String(g_temp, 1) + " C");

App 里这个 TextBox 就会一片空白,没有任何报错提示,搞得我以为 BLE 断连了。改成纯数字才正常:

message += dashDevice.getTextBoxMessage("TB01", String(g_temp, 1));message += dashDevice.getTextBoxMessage("TB02", String(g_humi, 1));

单位的正确做法是在 DeviceView Builder 里设置控件的 Unit 属性,而不是在设备端硬编码。

5.7 Time Graph 双曲线配置

Dash IoT 的 Time Graph 不是被动读取的,而是需要设备主动推送两样东西:

  1. 线条定义(getTimeGraphLine)—— 在 status / connect 时发送一次

  2. 数据点(getTimeGraphPoint)—— 定时发送,App 自动追加到曲线尾部

为了让温度(0~50℃)和湿度(0~100%RH)不挤在同一侧 Y 轴,我把温度放左轴(yLeft,红色),湿度放右轴(yRight,蓝色):

static void processStatus(ConnectionType connectionType) {
    String message((char *)0);
    message.reserve(256);

    message += dashDevice.getTextBoxMessage("TB01", String(g_temp, 1));
    message += dashDevice.getTextBoxMessage("TB02", String(g_humi, 1));
    // 定义两条曲线
    message += dashDevice.getTimeGraphLine("IDTG", "L1", "Temp", line, "red", yLeft);
    message += dashDevice.getTimeGraphLine("IDTG", "L2", "Humi", line, "blue", yRight);

    ble_con.sendMessage(message);}

然后每分钟更新传感器时追加数据点:

static void update_sensors_and_screen() {
    // ... 读取 SHT30 和电量 ...
    update_screen();

    if (g_ble_connected) {
        String graphMsg = dashDevice.getTimeGraphPoint("IDTG", "L1", g_temp);
        graphMsg += dashDevice.getTimeGraphPoint("IDTG", "L2", g_humi);
        ble_con.sendMessage(graphMsg);
    }}

首次连接后大概要等 1~2 分钟,Graph 上才会出现第一个折线段。长期运行后会累积成完整的温湿度趋势图。


六、运行效果6.1 串口输出

编译上传后,打开 Arduino 串口监视器(115200),连接 Dash App 时应看到如下日志:

m5paper_weather_dashio_02-log.jpg

6.2 M5Paper界面

m5paper_weather_dashio_03_m5paper.jpg

6.3 Dash App 界面

  1. 搜索设备
    m5paper_weather_dashio_04-app-search.jpg    

  2. 界面设计
    m5paper_weather_dashio_05-app-temp.jpg

    m5paper_weather_dashio_06-app-humi.jpg

  3. 数据显示
    m5paper_weather_dashio_07-app-data.jpg


七、工程文件说明

arduino/m5paper_weather_dashio/
└── m5paper_weather_dashio.ino   // 单一文件完整 Sketch

核心功能全部收敛在一个 .ino 文件中,约 475 行,包含:

  • V40 UI 精确绘制

  • SHT30 I2C 驱动

  • AXP192 电量读取

  • Dash IoT BLE 连接 + config layout 推送

  • 1 分钟自动刷新 + Time Graph 双曲线数据推送


八、结语

用 Arduino + DashioESP 来做 Dash IoT 设备,投入产出比非常高。库把 NimBLE 的脏活累活全包了,让我能把精力放回产品实现本身——也就是怎么把墨水屏上的温湿度显示做得更漂亮、更省电。

V40 的 UI 坐标和绘制逻辑全部保留在 setup() / loop() 的结构里。如果你是 Arduino 老玩家,这套代码拿来改刷新周期、加新控件、或者做 RTC 休眠唤醒,都会非常顺手。

后续可探索方向

  1. 低功耗休眠:利用 M5Paper 的 EPD 特性,每小时唤醒一次刷新,续航能拉长到天级别;

  2. 本地数据缓存:把 24h 温湿度存到 SPIFFS,断电后重启也能恢复曲线;

  3. 自定义 Dashboard:在 Dash App 的 Edit Mode 里重新排列控件,然后 Export Layout,得到专属 configC64Str。

如果你也想复刻一个,直接把代码拖进 Arduino IDE,安装上面四个库,编译上传,几分钟就能在 M5Paper 上看到自己的青萍风温度计,并且手机上也能同步查看实时数据了!





关键词: M5Paper          Arduino     蓝牙温湿度计         

共1条 1/1 1 跳转至

回复

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