💡 划重点
- 主控选 ESP32-C3,支持 Wi-Fi,成本约 15 元。
- 4 枚独立按键对应四个服药时段,按下即记录,操作难度为零。
- OLED 本地显示服药状态,断电记忆最近记录。
- 一键触发飞书机器人通知,家属远程接收,独居用药不靠猜。
- 整机 BOM 成本控制在 50 元内,适合批量制作与社区分享。
需求洞察
目标用户:独居老人、慢性病需长期用药者,以及负责照护的子女或伴侣。
真实痛点:老年人常因记性差漏服、错服或重复服药;子女无法实时知晓父母是否按时吃药,电话询问又易引起焦虑和抵触。药盒虽多,但传统药盒不记录、不联网,无法形成“吃没吃、几点吃”的证据链。
为什么值得做:药品服用不是高频娱乐需求,而是低频刚需。设备只要能减少一个漏服事件,就可能避免一次急诊甚至住院。相比智能药柜动辄数百元,50 元内能实现的打卡反馈方案,既有实际看护价值,又有低门槛复制意义,适合作为社区健康小组或亲子共同完成的硬件项目。
方案设计
本方案按四层架构设计,覆盖从物理操作到远程通知的完整链路。
感知层:4x1 自复位按键矩阵,搭配 10kΩ 上拉电阻,ESP32-C3 内部启用输入模式。每按键对应“早、中、晚、睡前”四个时间段,按下即产生一个低电平脉冲,作为服药事件触发源。
控制层:ESP32-C3 负责按键去抖(20ms 软件滤波)、事件解析、状态机维护。控制层区分“正常打卡”“重复打卡”“补卡”三种事件,并驱动 OLED 与 LED 反馈。
执行层:0.96 寸 OLED 显示当前时间、最近打卡时段、连续打卡天数。绿色 LED 点亮 2 秒表示记录成功,红色 LED 闪烁表示“该时段未打卡”的提醒。
日志/联网层:本地维护一个 7 条记录的环形缓存(时段 + 时间戳)。每次按压触发 HTTP POST 请求,携带 JSON 数据到飞书自定义机器人 Webhook,家属手机端即时收到“刚完成午间服药”之类的推送。断网时自动缓存并等待重试。
物料清单
| 物料名称 | 数量 | 用途 | 估算单价(元) | 小计(元) |
|---|---|---|---|---|
| ESP32-C3 开发板 | 1 | 主控 | 15.00 | 15.00 |
| 0.96 寸 OLED(I2C) | 1 | 状态显示 | 10.00 | 10.00 |
| 轻触按键 6x6mm | 4 | 服药打卡输入 | 0.30 | 1.20 |
| 电阻 10kΩ | 4 | 按键上拉 | 0.05 | 0.20 |
| LED 红色 5mm | 1 | 未服药提醒 | 0.20 | 0.20 |
| LED 绿色 5mm | 1 | 打卡成功反馈 | 0.20 | 0.20 |
| 限流电阻 220Ω | 2 | LED 保护 | 0.05 | 0.10 |
| 面包板/洞洞板 | 1 | 电路连接 | 5.00 | 5.00 |
| 杜邦线若干 | 1 组 | 接线 | 3.00 | 3.00 |
| 3D 打印/塑料外壳 | 1 | 结构保护 | 12.00 | 12.00 |
| 合计 | 46.90 |
接线与结构
推荐接线表如下,若与现有开发板丝印冲突,仅需同步修改代码中的引脚宏定义即可。
| 信号 | GPIO 引脚 | 说明 |
|---|---|---|
| 按键 0 | 13 | 晨起服药,内部上拉,按下接 GND |
| 按键 1 | 12 | 午间服药,内部上拉,按下接 GND |
| 按键 2 | 14 | 晚间服药,内部上拉,按下接 GND |
| 按键 3 | 27 | 睡前服药,内部上拉,按下接 GND |
| OLED SDA | 8 | I2C 数据线 |
| OLED SCL | 9 | I2C 时钟线 |
| LED 红 | 10 | 未服药提醒,低电平点亮 |
| LED 绿 | 21 | 打卡成功,低电平点亮 |
此处做一处微调:原建议按键接 GPIO 0/1/2/3,但 GPIO 0 和 GPIO 1 在多数 ESP32-C3 开发板上为启动模式选择与串口打印引脚,使用后可能导致一键下载或日志异常。故将按键移至 13/12/14/27,这些引脚均支持通用输入,且互不冲突。OLED 维持建议的 8/9 引脚,LED 中红色保留 GPIO 10,绿色使用 GPIO 21,性能不受影响。
核心代码
本项目使用 Arduino 框架,代码骨架可编译运行。务必先安装 ESP32 开发板包与以下库:Adafruit SSD1306、Adafruit GFX、ArduinoJson。
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>
// 网络配置
const char* ssid = "your_wifi";
const char* password = "your_password";
const char* webhook_url = "https://open.feishu.cn/open-apis/bot/v2/hook/your_token";
// 引脚映射
#define BTN0 13
#define BTN1 12
#define BTN2 14
#define BTN3 27
#define OLED_SDA 8
#define OLED_SCL 9
#define LED_RED 10
#define LED_GREEN 21
// OLED 配置
Adafruit_SSD1306 display(128, 64, &Wire;, -1);
// 时段名称
const char* slots[4] = {"晨起", "午间", "晚间", "睡前"};
int lastSlot = -1;
void setup() {
pinMode(BTN0, INPUT_PULLUP);
pinMode(BTN1, INPUT_PULLUP);
pinMode(BTN2, INPUT_PULLUP);
pinMode(BTN3, INPUT_PULLUP);
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
digitalWrite(LED_RED, HIGH);
digitalWrite(LED_GREEN, HIGH);
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Welcome! 按任意键");
display.display();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
display.clearDisplay();
display.setCursor(0, 0);
display.println("WiFi OK");
display.display();
}
void loop() {
int slot = -1;
if (digitalRead(BTN0) == LOW) slot = 0;
if (digitalRead(BTN1) == LOW) slot = 1;
if (digitalRead(BTN2) == LOW) slot = 2;
if (digitalRead(BTN3) == LOW) slot = 3;
if (slot >= 0) {
delay(50); // 基础去抖
if (digitalRead(slot == 0 ? BTN0 : (slot == 1 ? BTN1 : (slot == 2 ? BTN2 : BTN3))) == LOW) {
recordPill(slot);
delay(2000);
}
}
}
void recordPill(int slot) {
// 本地显示与 LED 反馈
digitalWrite(LED_GREEN, LOW);
display.clearDisplay();
display.setCursor(0, 0);
display.print("已打卡: ");
display.println(slots[slot]);
display.display();
delay(1500);
digitalWrite(LED_GREEN, HIGH);
// 构造 JSON
StaticJsonDocument<256> doc;
doc["text"] = String("【服药提醒】") + slots[slot] + "时段已完成服药打卡";
String payload;
serializeJson(doc, payload);
// 发送飞书
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(webhook_url);
http.addHeader("Content-Type", "application/json");
http.POST(payload);
http.end();
} else {
digitalWrite(LED_RED, LOW);
delay(1000);
digitalWrite(LED_RED, HIGH);
}
}
以上代码省略了 HTTPClient 库的细节,实际使用时需在文件头部 #include <HTTPClient.h>。逻辑中若 Wi-Fi 断开,则红色 LED 点亮 1 秒提示本地缓存失败。
调试步骤
- 静态检查:确认所有外围元件焊接无短路、OLED 电源电压为 3.3V 或 5V 兼容版、按键按下时能连通 GND。
- 烧录测试:先在串口监视器中打印各按键的 GPIO 电平,依次按压,确认每个按键都有独立的 0(按下)与 1(松开)切换。
- OLED 验证:上传一个 I2C 扫描程序,确保设备地址为 0x3C,并显示白色字符不偏移。
- 功能单测:连续按同一个键两次,绿色 LED 亮起且 OLED 仅显示一次“已打卡”,说明去抖与防重复逻辑生效。
- 断网模拟:关闭路由器电源,按压按键,观察红色 LED 闪烁;随后开启路由器,设备自动重连后再次按压,应能在飞书端收到新消息。
- 稳定运行: 连续运行 24 小时,期间记录三次打卡,确认 OLED 无残影、飞书消息无重复、无死机。