基于ESP32-C3的智能药盒提醒器:从硬件选型到Web配置的物联网实践
1. 项目概述与设计初衷
在嵌入式开发和物联网应用逐渐普及的今天,我们身边有太多可以通过技术优化的小痛点。对我而言,其中一个就是用药管理。无论是工作繁忙的上班族,还是需要规律服药的家人,忘记吃药是再常见不过的事。市面上的智能药盒要么功能复杂、价格昂贵,要么依赖手机App,而手机本身又是一个巨大的干扰源。我的目标很明确:设计一个放在桌面上、足够简单、足够专注的设备,它只做一件事——在正确的时间,用最温和的方式提醒你该吃药了。
这就是“Half Pill”智能药盒提醒器的由来。它基于Seeed Studio的XIAO ESP32-C3核心板,搭配一块圆形的显示屏,通过Wi-Fi接入家庭网络。整个设备没有复杂的按钮,所有设置都通过一个优雅的手机网页完成。你只需要在浏览器里输入设备显示的IP地址,就能添加、删除或查看服药计划。设备会通过NTP自动同步网络时间,确保提醒准时,所有数据都安全地保存在板载的EEPROM里,即使断电也不会丢失。整个项目从3D建模、打印到代码编写、组装,是一个典型的端到端物联网硬件开发实践,非常适合想要入门嵌入式或智能硬件DIY的朋友。
2. 核心硬件选型与原理剖析
2.1 主控单元:为什么是XIAO ESP32-C3?
在项目启动时,主控芯片的选择是首要决策。我最终选择了Seeed Studio的XIAO ESP32-C3,主要基于以下几点考量:
集成度与尺寸的平衡:XIAO系列以其极致的紧凑尺寸著称,ESP32-C3版本在指甲盖大小的空间内集成了ESP32-C3芯片、天线、USB-C接口和必要的电路。对于桌面提醒器这类需要精致外观的设备,小巧的尺寸为外壳设计提供了巨大便利。相比之下,标准的NodeMCU或D1 Mini开发板虽然常见,但体积过大,会破坏整体的美观性。
性能与功耗的匹配:ESP32-C3是一款基于RISC-V架构的单核Wi-Fi & Bluetooth 5 (LE) SoC。对于本应用——驱动一块小屏幕、维护Wi-Fi连接、运行一个轻量级Web服务器、处理定时任务——它的性能绰绰有余。RISC-V架构在能效比上表现不错,这对于长期插电使用的设备虽非首要,但体现了技术选型的现代性。更重要的是,它原生支持Wi-Fi,这是实现网络时间同步和网页配置的基础。
开发生态与成本:ESP32系列拥有极其丰富的Arduino和ESP-IDF开发资源,社区支持强大,遇到问题容易找到解决方案。XIAO ESP32-C3完美兼容Arduino IDE,大大降低了开发门槛。从成本看,它是一款性价比极高的模块,在实现所需功能的同时,控制了项目的整体预算。
注意:XIAO ESP32-C3的IO口数量有限。在选择它时,必须提前规划好所有需要连接的外设(如屏幕、可能的蜂鸣器、传感器等),确保引脚资源足够。本项目仅使用屏幕,因此完全满足。
2.2 显示单元:圆形屏幕的交互哲学
我选择了Seeed Studio专为XIAO设计的圆形显示屏。这个选择并非偶然,而是交互设计的一部分。
形式服务于功能:药盒提醒的核心信息是“药名”和“时间”。圆形屏幕天然适合展示时钟、环形进度条等与时间相关的视觉元素。当提醒触发时,屏幕中央可以醒目地显示药名,周围用光晕或环形高亮作为视觉提示,这种形式比长方屏更具亲和力和专注度。
即插即用的便利性:这块屏幕通过专用的连接器与XIAO底板对接,无需焊接,真正实现了插拔即用。它集成了显示驱动和电容触摸控制器(虽然本项目未使用触摸功能),节省了大量连接和调试屏幕底层驱动的时间,让我能更专注于应用逻辑的开发。
硬件兼容性保障:使用同一品牌、专为彼此设计的核心板与屏幕,最大程度避免了电源、信号电平不匹配或引脚定义冲突等问题。对于DIY项目,这种“套件”式的兼容性能显著提高成功率,减少在硬件连接阶段踩坑的可能性。
2.3 数据持久化:EEPROM的可靠性与局限
设备需要存储用户设置的药名和服药时间,且断电后不能丢失。这里没有使用外置的SD卡或Flash芯片,而是利用了ESP32-C3内部自带的EEPROM模拟存储。
工作原理:ESP32的EEPROM实际上是在SPI Flash存储器中划出一块区域,通过EEPROM库进行模拟读写。当你调用EEPROM.write()或EEPROM.put()时,数据首先被写入微控制器的内存缓冲区,只有在调用EEPROM.commit()后,才会真正写入Flash。这个过程磨损的是Flash的特定扇区。
容量与寿命考量:ESP32-C3的EEPROM默认大小为4096字节(4KB)。对于存储20条记录,每条记录包含药名(假设最多20字符)和时间信息(如4字节的时间戳),总需求远低于4KB,完全足够。需要警惕的是Flash的擦写寿命,通常为10万次。频繁地、无意义地调用commit()会加速损耗。因此,在程序设计中,我采用了“批量保存”策略:用户在网页点击“配置”后,所有数据一次性写入,而非每次修改都保存,极大地延长了存储寿命。
实操心得:首次使用EEPROM前,最好在
setup()函数中执行一次EEPROM.begin(size)来初始化指定大小。读取数据后,建议进行简单的数据校验,例如在存储时额外写入一个固定的“魔数”(Magic Number),读取时校验该魔数是否正确,以此判断EEPROM中的数据是否有效或为初始乱码。
3. 系统设计与软件架构详解
3.1 整体工作流程与状态机设计
设备的软件核心是一个清晰的状态机,它定义了设备从启动到运行各个阶段的行为。理解这个状态机是理解整个项目逻辑的关键。
状态一:初始化与网络连接 (Boot & Connect)设备上电后,首先初始化串口(用于调试)、屏幕、并连接文件系统(如果需要)和EEPROM。随后,它尝试连接预设的Wi-Fi网络。此阶段,屏幕显示“Connecting to Wi-Fi…”之类的动态信息。连接成功后,设备通过SNTP(简单网络时间协议)从NTP服务器获取当前时间并设置本地时钟。成功后,屏幕会显示本机获取到的IP地址,这是后续网页配置的入口。
状态二:Web服务器与配置模式 (Configuration Mode)设备启动一个异步Web服务器(例如使用ESPAsyncWebServer库)。当用户在浏览器中输入设备IP地址时,服务器会返回一个HTML格式的配置页面。这个页面通过JavaScript与设备进行AJAX交互,实现无刷新添加、删除、查看日程。用户点击“保存配置”后,浏览器会将所有日程数据以JSON格式POST到设备的一个特定API端点。
状态三:运行与监控模式 (Running Mode)配置保存后,设备进入主循环运行状态。程序会持续将当前时间与EEPROM中存储的所有服药时间进行比较。当匹配到某个提醒时间点时,设备触发“提醒动作”——在当前设计中,即改变屏幕显示内容,以高亮方式展示药名。主循环会一直运行,直到下一次通过网页修改配置并保存,设备将重新加载日程数据。
关键设计:异步Web服务器我选择了异步Web服务器库而非传统的同步服务器(如WiFiServer)。原因是同步服务器在处理请求时会阻塞整个主循环,这意味着在服务器响应网页请求的几百毫秒内,设备无法检查时间、更新屏幕,可能导致提醒延迟或错过。异步服务器则将所有网络事件放入队列,在主循环中非阻塞地处理,确保了时间监控任务的实时性。
3.2 网页配置界面:前后端数据交互
配置界面是用户与硬件交互的桥梁,其设计原则是:简单、直观、无需安装额外App。
前端实现 (HTML/JS): 我编写了一个单一的index.html文件,包含一个表单用于输入药名和时间,一个表格用于展示现有日程,以及“添加”、“删除”、“保存”等按钮。这个文件被直接存储在ESP32的代码中(通过PROGMEM方式存入),当浏览器请求根路径时,服务器将其发送出去。
- 动态添加:点击“添加”按钮,JavaScript会在表格中动态插入一行新记录,但此时并未发送到设备。
- 数据暂存:所有增删改操作仅在浏览器内存(一个JavaScript数组)中进行,提供了良好的即时反馈。
- 批量提交:用户确认所有修改后,点击“保存配置”,JavaScript会将整个日程数组序列化为JSON字符串,通过
fetch()API的POST方法发送到设备的/configure端点。
后端处理 (ESP32 C++): 设备端的/configure端点处理POST请求。
- 解析收到的JSON数据。
- 进行基础校验(如时间格式、数量是否超限20个)。
- 将数据转换为更节省空间的格式(如将“14:30”字符串转换为一天中的分钟数)。
- 调用
EEPROM.put()将数据存入缓冲区,最后执行EEPROM.commit()写入Flash。 - 返回一个成功的JSON响应给浏览器,并自动重启或重载日程列表。
优势:这种设计将复杂的UI交互逻辑交给功能强大的浏览器处理,ESP32只负责提供API和存储,分工明确,极大地减轻了微控制器的负担,也使得界面可以做得更美观、响应更快。
3.3 时间同步与定时检查机制
精准的提醒依赖于精准的时间。
NTP时间同步:ESP32通过configTime()函数配置时区和NTP服务器地址(如pool.ntp.org)。连接Wi-Fi后,它会自动在后台同步时间。我建议在代码中设置一个每24小时自动重新同步一次的软定时器,以修正可能存在的微小时钟漂移。ESP32-C3的硬件RTC(实时时钟)在深度睡眠时精度尚可,但在常态运行下,依赖软件定时和NTP定期校准是更可靠的做法。
定时检查算法: 在主循环loop()中,我使用millis()或getLocalTime()来获取当前时间。一个高效的做法是,将EEPROM中存储的每个服药时间(例如“08:00”,“13:00”,“20:00”)转换为从午夜零点开始的分钟数(如480, 780, 1200)。同样,将当前时间也转换为分钟数。
int currentMinutes = hour * 60 + minute;然后,遍历所有日程,检查currentMinutes是否等于某个日程的分钟数。但这里有个细节:如果程序在08:00:00检查了一次,下一次循环可能在08:00:01,就会错过。因此,通常需要设置一个“提醒窗口期”,例如当前时间与预定时间差在±1分钟内,都视为触发提醒。触发后,可以设置一个“已提醒”标志,避免在同一分钟内重复触发屏幕刷新。
注意事项:避免在
loop()中使用delay()。如果使用delay(1000)来每秒检查一次,会阻塞整个程序,导致网页请求无法响应。正确的做法是使用非阻塞的时间间隔检查,例如记录上一次检查的时间戳,当millis() - lastCheckTime > 60000(一分钟)时,才执行一次时间比对,然后更新lastCheckTime。
4. 硬件组装与外壳制作实战
4.1 3D建模与结构设计要点
我使用Autodesk Fusion 360进行外壳设计,整个过程是典型的“由内而外”的逆向设计。
第一步:电子元件定位首先,我将从Seeed官网下载的XIAO ESP32-C3和圆形显示屏的3D模型(STEP或SLDPRT格式)导入Fusion 360。在虚拟空间中,我需要确定它们的相对位置:
- 主板固定:设计支撑柱和螺丝孔(M2规格)来固定XIAO板。
- 屏幕固定:屏幕通常通过其PCB上的安装孔固定。需要设计对应的支柱和孔位(M2或M3),确保屏幕显示面与外壳前面板开口完美对齐。
- 走线空间:必须留出足够的空间容纳连接屏幕与主板的排线,避免弯折过度或挤压。我通常在屏幕背面和主板之间预留5-10mm的空隙。
- 天线安置:ESP32-C3板载的PCB天线或外接天线需要远离金属部件和显示屏背板,以确保信号质量。设计中需要为天线预留一个开阔的区域。
第二步:外壳体设计围绕定位好的电子元件,绘制外壳的主体。分为前壳和后盖。
- 前壳:需要开一个精确的圆形视窗,让屏幕露出。视窗边缘最好有一个微小的唇边,从内部挡住屏幕边缘,使外观更整洁。
- 后盖:需要为USB-C接口开槽。这里强烈推荐为90度弯头USB线设计一个通道或凹槽,让线材可以优雅地引出,而不是直角弯折,影响美观和线材寿命。还需要设计散热孔(虽然功耗不大),以及用于固定到桌面支架的接口。
- 合盖方式:我采用了螺丝固定(M3螺丝),在四角设计螺丝柱。也可以考虑卡扣式,但对3D打印的精度和材料韧性要求更高。
第三步:桌面支架设计一个可分离的桌面支架能提升使用体验。我设计了一个带有一定倾角的支架,通过一个卡槽或单个螺丝与主机后盖连接。倾角(例如15-20度)需要经过测试,确保在桌面上视线舒适。
4.2 3D打印与后处理
打印参数建议:
- 材料:PLA+(增强PLA)是不错的选择,强度、精度和打印成功率都很好。PETG则更耐用、耐温。
- 层高:0.2mm层高可以在打印速度和表面光洁度之间取得良好平衡。对于显示窗口等关键部位,可以尝试0.16mm以获得更光滑的边缘。
- 填充率:15%-20%的填充率足以提供足够的结构强度,同时节省材料和时间。
- 支撑:对于外壳内部的螺丝柱、卡扣等悬空结构,需要生成支撑。务必仔细检查切片预览,确保支撑易于拆除,且不损坏关键表面。
后处理与组装:
- 去除支撑与打磨:小心地移除所有支撑材料。对于合模线或粗糙表面,可以使用砂纸(从粗到细)进行打磨。
- 试装配:在拧紧螺丝之前,先进行“干装配”,确保所有零件能顺利组合,螺丝孔对齐,屏幕能平整放入。
- 电子部件安装:先将天线(如果使用外接天线)安装到主板。然后将屏幕排线插入XIAO扩展板。务必在断电状态下操作!
- 整体组装:将屏幕总成放入前壳,用短螺丝固定。将主板放入后壳的固定柱上。连接屏幕与主板(如果它们是分离的)。最后合上前壳与后盖,用长螺丝锁紧。
- 功能测试:先不要完全封死外壳,连接USB线供电,测试屏幕是否点亮、Wi-Fi能否连接、网页能否访问。确认一切正常后,再完成最终组装。
实操心得:在打印外壳前,最好先用硬纸板或泡沫板制作一个1:1的模型,验证尺寸和人体工学。对于USB开槽,打印一个小的测试件来验证你的数据线插头是否能顺利插入,比打印完整个外壳才发现问题要节省大量时间和材料。
5. 软件代码深度解析与配置
5.1 开发环境搭建与库管理
本项目在Arduino IDE中进行开发。除了安装ESP32板支持包,两个图形库是关键。
安装ESP32开发板支持:
- 打开Arduino IDE,进入“文件”->“首选项”,在“附加开发板管理器网址”中添加:
https://espressif.github.io/arduino-esp32/package_esp32_index.json - 打开“工具”->“开发板”->“开发板管理器”,搜索“esp32”,安装“Espressif Systems”提供的ESP32平台。
安装必需的库:
- Seeed_Arduino_RoundDisplay:这是屏幕的底层驱动库,负责与屏幕的硬件通信。
- Seeed_GFX:这是一个图形库,提供了画线、画圆、显示文字、图像等高层API,依赖于前面的驱动库。
- ESPAsyncWebServer & AsyncTCP:用于实现异步Web服务器。这两个库通常不在Arduino库管理中,需要手动下载。可以从GitHub(分别为
me-no-dev/ESPAsyncWebServer和me-no-dev/AsyncTCP)下载ZIP文件,然后通过“项目”->“加载库”->“添加.ZIP库…”来安装。 - EEPROM:Arduino核心库自带,无需额外安装。
项目文件结构: 你的项目文件夹应包含:
half_pill.ino:主程序文件。boot_img.h:可能包含一个用于启动时显示的图像字节数组。driver.h:可能包含一些硬件驱动相关的定义或封装函数。index.h:存储了整个网页HTML/JS/CSS代码的字符串(通常以const char数组形式存储)。这是一个常见的技巧,将网页前端代码直接嵌入固件。
5.2 核心代码模块剖析
主程序 (half_pill.ino) 框架:
#include // 包含所有必要的头文件 #include “driver.h” #include “boot_img.h” // 网络配置 const char* ssid = “你的Wi-Fi名称”; const char* password = “你的Wi-Fi密码”; // 全局对象定义 RoundDisplay display(240, 240); // 假设屏幕分辨率240x240 AsyncWebServer server(80); // Web服务器端口80 struct PillSchedule { char name[20]; int timeMinute; }; // 日程结构体 PillSchedule schedules[20]; int scheduleCount = 0; void setup() { Serial.begin(115200); display.init(); // 初始化屏幕 EEPROM.begin(4096); // 初始化EEPROM loadSchedulesFromEEPROM(); // 从EEPROM加载已有日程 connectToWiFi(); // 连接Wi-Fi initWebServer(); // 初始化Web服务器路由 setupTimeSync(); // 设置NTP时间同步 } void loop() { checkAndTriggerReminders(); // 检查并触发提醒 server.handleClient(); // 处理网络客户端请求(非阻塞) // 可以在这里添加屏幕刷新或其他非阻塞任务 }关键函数详解:
connectToWiFi():除了基本的WiFi.begin(),还应加入重试机制和连接状态在屏幕上的反馈。initWebServer():在这里定义所有HTTP路由。server.on(“/“, HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, “text/html”, index_html); });用于发送网页。server.on(“/getSchedules”, HTTP_GET, [](AsyncWebServerRequest *request){ // 返回JSON格式的日程列表 });server.on(“/configure”, HTTP_POST, [](AsyncWebServerRequest *request){ // 接收并处理POST来的新日程数据,保存至EEPROM });
checkAndTriggerReminders():这是核心逻辑。获取当前时间,转换为分钟数,遍历schedules数组。如果发现匹配(在误差窗口内),并且该日程本次还未提醒,则调用displayReminder(schedule.name)函数高亮显示提醒,并标记为“已提醒”。每天零点需要重置所有“已提醒”标志。loadSchedulesFromEEPROM()和saveSchedulesToEEPROM():这两个函数负责数据结构与EEPROM字节流之间的转换。保存时,可以先写入一个版本号或魔数,再写入schedules数组和scheduleCount。读取时,先校验魔数,再读取数据。
5.3 网页界面嵌入与配置
将网页代码嵌入C++程序的标准做法是使用PROGMEM(程序存储区)来存储这个巨大的字符串常量,以节省宝贵的RAM。
创建index.h文件: 这个文件里定义了一个const char index_html[] PROGMEM = R”rawliteral( … )rawliteral”;变量,其中…部分就是你完整的HTML、CSS和JavaScript代码。现代Arduino IDE的编译工具链会自动将其放入Flash中。
网页功能要点:
- 响应式设计:使用CSS媒体查询,确保在手机和电脑上都能良好显示。
- 时间输入:使用HTML5的``控件,它能在移动端弹出原生时间选择器,体验很好。
- AJAX交互:
- 页面加载时,自动发送GET请求到
/getSchedules,获取并渲染现有日程。 - 点击“添加”,在本地表格新增一行。
- 点击“删除”,从本地数组移除对应行。
- 点击“保存”,将本地数组
JSON.stringify()后,POST到/configure。
- 页面加载时,自动发送GET请求到
- 用户反馈:在发送AJAX请求后,通过弹窗或动态文字提示“保存成功”或“保存失败”。
6. 调试、优化与功能扩展思路
6.1 常见问题排查指南
在开发过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 屏幕白屏或花屏 | 1. 电源不足 2. 屏幕初始化代码错误 3. 排线接触不良 | 1. 使用高质量的USB线和电源适配器(5V/1A以上)。 2. 检查 display.init()的参数和初始化序列是否正确。参考屏幕库的示例代码。3. 重新插拔屏幕排线,确保锁扣扣紧。 |
| 无法连接Wi-Fi | 1. SSID/密码错误 2. 路由器信号弱或加密方式不支持 3. 代码中网络配置错误 | 1. 仔细检查代码中的ssid和password,注意大小写和特殊字符。2. 将设备靠近路由器,或尝试连接手机热点以排除路由器问题。 3. 在 setup()中增加WiFi.mode(WIFI_STA)明确设置为站点模式。查看串口打印的连接状态信息。 |
| 网页无法访问 | 1. IP地址错误 2. 防火墙或网络隔离 3. Web服务器未启动 | 1. 确认屏幕显示的IP地址与手机/电脑在同一网段(如都是192.168.1.x)。 2. 有些路由器会开启“客户端隔离”功能,需关闭。尝试用电脑ping一下设备的IP。 3. 检查 initWebServer()是否被调用,以及server.begin()是否执行。查看串口是否有错误日志。 |
| 时间不同步 | 1. NTP服务器无法访问 2. 时区设置错误 | 1. 确保Wi-Fi连接成功。尝试更换NTP服务器地址,如cn.pool.ntp.org。2. 检查 configTime()函数中的时区偏移参数(如东八区为8*3600)。 |
| EEPROM数据丢失 | 1. 未调用EEPROM.commit()2. Flash扇区损坏 | 1. 确保在修改数据后执行了EEPROM.commit()。2. 在首次使用或数据异常时,在代码中加入初始化EEPROM默认值的逻辑。避免过于频繁的 commit。 |
6.2 性能与稳定性优化
- 电源管理:虽然本项目常插电,但稳定的电源是基础。可以在USB输入端口附近增加一个10-100µF的电解电容,以平滑可能存在的电压微小波动。
- 看门狗定时器:ESP32内置硬件看门狗。启用它可以在程序跑飞或陷入死循环时自动重启设备,增强可靠性。
esp_task_wdt_init(10, true); // 初始化看门狗,超时时间10秒 esp_task_wdt_add(NULL); // 将当前任务加入看门狗监控 // 在主循环中定期喂狗 esp_task_wdt_reset(); - 内存优化:使用异步Web服务器本身就是为了避免阻塞。此外,注意减少全局变量,优先使用局部变量;对于不变的字符串使用
PROGMEM;及时释放动态分配的内存(如果使用了的话)。 - 错误恢复:在连接Wi-Fi失败时,可以尝试进入一个“配置模式”(如开启一个AP热点,用手机连接后配网)。EEPROM数据读取失败时,应有恢复默认值或上次已知良好备份的机制。
6.3 潜在功能扩展方向
基础版本已经实用,但仍有丰富的扩展空间:
多提醒方式:
- 声音提醒:增加一个无源蜂鸣器或小型扬声器模块,连接到GPIO口。在触发提醒时,可以播放一段简单的旋律或滴滴声。代码上需要增加音频驱动(如使用
tone()函数或PWM模拟)。 - 灯光提醒:在屏幕周围增加一圈可编程RGB LED(如WS2812),提醒时发出柔和闪烁的光。
- 物理提醒:集成一个小型振动电机,适合放在口袋或需要静音的场合。
- 声音提醒:增加一个无源蜂鸣器或小型扬声器模块,连接到GPIO口。在触发提醒时,可以播放一段简单的旋律或滴滴声。代码上需要增加音频驱动(如使用
交互方式升级:
- 电容触摸:圆形屏幕本身支持触摸。可以开发触摸界面,实现滑动查看日程、点击确认服药等功能,减少对网页的依赖。
- 物理按钮:增加1-2个实体按钮,用于“延迟提醒”(Snooze)或“确认服药”(Dismiss),操作更直接。
云端连接与远程管理:
- 接入物联网平台(如阿里云、腾讯云IoT Hub),通过平台的小程序或App远程查看设备状态、修改日程,即使不在家也能为家人设置提醒。
- 实现用药记录上传,形成简单的服药日志。
低功耗与电池供电:
- 如果想做成便携式,需要改用电池供电。利用ESP32-C3的深度睡眠功能,在非提醒时间让芯片进入深度睡眠,仅靠RTC定时唤醒检查时间,可极大延长续航。此时屏幕需要选择低功耗型号或仅在唤醒时点亮。
药品库存管理:
- 这是一个更复杂的扩展。可以尝试在药盒底部增加重量传感器(如HX711模块+称重传感器),通过重量变化粗略估计剩余药片数量,并在UI中显示“低库存”预警。
这些扩展都会增加硬件复杂度和软件工作量,建议在完全掌握基础版本后,选择一两个最感兴趣的方向进行尝试。每个扩展都可以作为一个独立的子项目来研究和实现。
