
教师讲解:子女端更简单,只需要一个环形LED灯,通过颜色显示老人状态。
边写边测步骤1:基础框架
/* * 吃药提醒器 - 子女办公室端 * 硬件:ESP32-S3 + WS2812环形LED灯(GPIO4) * * 状态说明: * 微弱蓝光 = 空闲(系统正常) * 红灯闪烁 = 告警(老人未按时吃药,频率随时间加快) * 绿灯常亮 = 确认(老人已吃药) * 蓝灯闪烁 = 解除(告警已解除) */#include <WiFi.h>#include <Adafruit_NeoPixel.h>#include <PubSubClient.h>// ========== 硬件配置 ==========#define LED_PIN 4 // 环形灯数据脚#define NUM_LEDS 16 // 16颗灯(根据实际修改12/16/24)// ========== 网络配置(办公室WiFi) ==========const char* ssid = "办公室WiFi"; // <-- 学生修改const char* password = "办公室密码"; // <-- 学生修改// ========== 巴法云配置(必须与老人端一致!) ==========const char* mqtt_server = "bemfa.com";const int mqtt_port = 9501;// 关键:Client ID必须与老人端不同!但主题是相同的const char* mqtt_client_id = "你的巴法云私钥-office"; // <-- 修改:加后缀区分const char* mqtt_topic = "medication001"; // 与老人端相同// 创建对象WiFiClient espClient;PubSubClient mqttClient(espClient);Adafruit_NeoPixel ring(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);// LED辅助函数void setRingColor(uint32_t color) { for(int i=0; i<NUM_LEDS; i++) ring.setPixelColor(i, color); ring.show();}void setRingOff() { setRingColor(ring.Color(0, 0, 0)); }void setup() { Serial.begin(115200); delay(1000); Serial.println("=== 子女办公室端启动 ==="); // 初始化LED ring.begin(); ring.setBrightness(255); setRingOff(); // 自检动画:彩虹效果 Serial.println("LED自检..."); for(int j=0; j<256; j+=5) { for(int i=0; i<NUM_LEDS; i++) { int hue = (i * 256 / NUM_LEDS) + j; ring.setPixelColor(i, ring.gamma32(ring.ColorHSV(hue))); } ring.show(); delay(20); } setRingOff(); // WiFi连接(同老人端,略) WiFi.begin(ssid, password); Serial.print("连接WiFi"); while(WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(" 成功"); // MQTT设置 mqttClient.setServer(mqtt_server, mqtt_port); // 注意:子女端需要设置回调函数接收消息 mqttClient.setCallback(mqttCallback); Serial.println("系统就绪");}void loop() { // 保持连接 if(!mqttClient.connected()) reconnectMQTT(); mqttClient.loop(); delay(10);}// 占位函数(后续填充)void mqttCallback(char* topic, byte* payload, unsigned int length) {}void reconnectMQTT() {}学生操作:连接环形灯(VCC→5V, GND→GND, DIN→GPIO4),上传测试彩虹效果。
教师讲解:子女端是"订阅者",需要解析老人端发来的三种消息:ALERT(告警)、CONFIRM(确认)、RESOLVED(解除)。
边写边测步骤2:消息解析
// 在步骤1代码基础上添加状态机和消息解析// ========== 状态定义 ==========enum OfficeState { STATE_IDLE, // 空闲:微弱蓝光 STATE_ALERT, // 告警:红灯闪烁(频率递增) STATE_CONFIRMED, // 确认:绿灯常亮3秒 STATE_RESOLVED // 解除:蓝灯闪烁3秒};OfficeState currentState = STATE_IDLE;unsigned long stateStartTime = 0; // 状态开始时间unsigned long lastBlinkTime = 0; // 闪烁计时bool ledOn = false; // 闪烁状态int currentAlertId = -1; // 当前告警ID// 告警效果配置const unsigned long BASE_BLINK_INTERVAL = 1000; // 基础闪烁1秒const unsigned long MIN_BLINK_INTERVAL = 200; // 最快闪烁200msconst unsigned long ALERT_ESCALATION_TIME = 5*60*1000; // 5分钟后最快const unsigned long CONFIRM_DURATION = 3000; // 确认显示3秒// MQTT连接(教师讲解:与老人端类似,但不需要订阅自己发布的主题)void reconnectMQTT() { while(!mqttClient.connected()) { Serial.print("连接巴法云..."); // 生成唯一Client ID(避免冲突) String clientId = String(mqtt_client_id) + "-" + String(random(0xffff), HEX); if(mqttClient.connect(clientId.c_str())) { Serial.println("成功!"); mqttClient.subscribe(mqtt_topic); // 订阅老人端发布的主题 Serial.print("已订阅主题: "); Serial.println(mqtt_topic); } else { Serial.print("失败,rc="); Serial.println(mqttClient.state()); delay(5000); } }}// MQTT回调函数(核心!处理收到的消息)void mqttCallback(char* topic, byte* payload, unsigned int length) { // 将payload转换为字符串 String msg = ""; for(int i=0; i<length; i++) msg += (char)payload[i]; Serial.print("\n[MQTT接收] "); Serial.println(msg); // 解析消息格式:TYPE|ID|TIME|DESC // 例如:ALERT|1|12:30|超时未吃药 parseMessage(msg);}// 消息解析函数(教师重点讲解字符串分割)void parseMessage(String msg) { // 找分隔符位置 int firstSep = msg.indexOf('|'); // 第一个| int secondSep = msg.indexOf('|', firstSep+1); // 第二个| int thirdSep = msg.indexOf('|', secondSep+1); // 第三个| if(firstSep == -1) { Serial.println("消息格式错误"); return; } // 提取字段 String msgType = msg.substring(0, firstSep); // ALERT/CONFIRM/RESOLVED String idStr = msg.substring(firstSep+1, secondSep); // 提醒ID String timeStr = msg.substring(secondSep+1, thirdSep); // 时间 String desc = msg.substring(thirdSep+1); // 描述 int alertId = idStr.toInt(); Serial.printf("解析: 类型=%s, ID=%d, 时间=%s, 描述=%s\n", msgType.c_str(), alertId, timeStr.c_str(), desc.c_str()); // 根据类型切换状态 if(msgType == "ALERT") { currentState = STATE_ALERT; currentAlertId = alertId; stateStartTime = millis(); lastBlinkTime = millis(); ledOn = false; // 串口打印告警框(吸引注意力) Serial.println("\n╔══════════════════════════════════════╗"); Serial.println("║ ⚠️ 紧急告警:老人未按时吃药! ║"); Serial.printf("║ 提醒编号: %d 时间: %s ║\n", alertId, timeStr.c_str()); Serial.println("║ 请立即电话联系老人确认情况 ║"); Serial.println("╚══════════════════════════════════════╝\n"); } else if(msgType == "CONFIRM") { currentState = STATE_CONFIRMED; stateStartTime = millis(); Serial.println("\n╔══════════════════════════════════════╗"); Serial.println("║ ✅ 老人已确认吃药 ║"); Serial.printf("║ 提醒编号: %d 时间: %s ║\n", alertId, timeStr.c_str()); Serial.println("╚══════════════════════════════════════╝\n"); } else if(msgType == "RESOLVED") { currentState = STATE_RESOLVED; stateStartTime = millis(); Serial.println("\n╔══════════════════════════════════════╗"); Serial.println("║ ✓ 告警解除:状况已恢复正常 ║"); Serial.println("╚══════════════════════════════════════╝\n"); }}// 在loop()中添加状态处理void loop() { if(!mqttClient.connected()) reconnectMQTT(); mqttClient.loop(); handleState(); // 处理LED状态 delay(10);}测试方法:
medication001主题:教师讲解:告警需要紧迫感,我们让红灯闪烁越来越快,亮度也越来越高。
边写边测步骤3:动态效果
// 计算当前闪烁间隔(教师讲解:线性插值算法)unsigned long getCurrentBlinkInterval() { if(currentState != STATE_ALERT) return BASE_BLINK_INTERVAL; unsigned long alertDuration = millis() - stateStartTime; // 超过5分钟后,保持最快频率 if(alertDuration >= ALERT_ESCALATION_TIME) { return MIN_BLINK_INTERVAL; } // 线性加速:随着时间推移,间隔从1000ms降到200ms float progress = (float)alertDuration / ALERT_ESCALATION_TIME; unsigned long interval = BASE_BLINK_INTERVAL - (unsigned long)((BASE_BLINK_INTERVAL - MIN_BLINK_INTERVAL) * progress); return interval;}// 状态处理函数(教师边写边解释每个状态的效果)void handleState() { unsigned long now = millis(); switch(currentState) { case STATE_IDLE: // 空闲:微弱蓝光(表示系统在线) setRingColor(ring.Color(0, 0, 10)); break; case STATE_ALERT: { // 告警:红灯闪烁,频率和亮度都递增 unsigned long interval = getCurrentBlinkInterval(); if(now - lastBlinkTime >= interval) { lastBlinkTime = now; ledOn = !ledOn; if(ledOn) { // 亮度也随时间增加:100-255 unsigned long duration = now - stateStartTime; int brightness = 100 + min((int)(duration/1000), 155); setRingColor(ring.Color(brightness, 0, 0)); // 每10秒串口提示一次 static unsigned long lastPrint = 0; if(now - lastPrint >= 10000) { lastPrint = now; Serial.printf("[告警中] 持续%ld秒,闪烁间隔%ldms\n", duration/1000, interval); } } else { setRingOff(); } } break; } case STATE_CONFIRMED: // 确认:绿色常亮3秒后恢复空闲 setRingColor(ring.Color(0, 255, 0)); if(now - stateStartTime >= CONFIRM_DURATION) { currentState = STATE_IDLE; Serial.println("确认状态结束,恢复空闲"); } break; case STATE_RESOLVED: // 解除:蓝色闪烁后恢复 if(now - lastBlinkTime >= 200) { lastBlinkTime = now; ledOn = !ledOn; setRingColor(ledOn ? ring.Color(0, 100, 255) : ring.Color(0, 0, 0)); } if(now - stateStartTime >= CONFIRM_DURATION) { currentState = STATE_IDLE; Serial.println("解除状态结束,恢复空闲"); } break; }}测试方法:
教师组织:两组配对,A组老人端,B组子女端,进行联调测试。
测试场景:
场景 | 操作 | 预期结果 |
场景1:正常吃药 | 老人端提醒→立即按键 | 老人端绿灯2秒;子女端无告警 |
场景2:超时告警 | 老人端提醒→等待2分钟不按 | 老人端发送ALERT;子女端红灯闪烁 |
场景3:远程确认 | 在场景2后,老人按键 | 老人端绿灯;子女端收到CONFIRM变绿灯 |
场景4:日期切换 | 等待0点或手动改系统时间 | 两端都重置状态,准备次日提醒 |
完整代码(两端的最终版):
已在附件中提供,包含详细注释。
评价维度 | 优秀(90-100) | 良好(75-89) | 合格(60-74) |
功能实现 | 四场景全部通过 | 通过3个场景 | 通过2个场景 |
代码规范 | 注释清晰,命名规范 | 有基本注释 | 能运行但注释少 |
硬件连接 | 整洁,无松动 | 基本正确 | 需教师协助 |
团队协作 | 主动配对测试 | 能配合测试 | 独立完成 |
创新拓展 | 增加功能(如语音、多老人) | 有改进想法 | 按教案完成 |
medication/老人ID)CST-8本文标签:#少儿编程 #科创 #电子爱好者 #物联网 #arduino #ESP32
![]() | ![]() | ![]() |
关注我们,方便学习和答疑