// ============================================================
// 智能语音药盒 - 干净仿真版 sketch.ino
// 平台:Wokwi + ESP32 + DS1307 RTC + LED + Button + Buzzer
//
// 目标:验证核心交互逻辑,而不是接入真实语音模块。
// - 串口命令模拟语音识别 / AI智能体输出
// - 按钮模拟药盒开盖
// - LED模拟药盒指示灯
// - 蜂鸣器 + 串口输出模拟语音播报
// ============================================================
#include <Arduino.h>
#include <Wire.h>
#include "RTClib.h"
// ==================== 引脚定义 ====================
// 功能按钮:模拟“设置药盒”和“知道了”
const int PIN_BTN_MODE = 5;
const int PIN_BTN_GOTIT = 18;
// 6个药盒按钮:按下 = 开盖,松开 = 关盖
const int BTN_PINS[6] = {32, 33, 4, 15, 19, 2};
// 6个LED:高电平点亮
const int LED_PINS[6] = {25, 26, 27, 14, 12, 13};
// 蜂鸣器:模拟语音播报提示音
const int PIN_BUZZER = 23;
// RTC
RTC_DS1307 rtc;
bool simulatedLidOpen[6] = {false, false, false, false, false, false};
// ==================== 数据结构 ====================
struct Slot {
bool enabled; // 是否已设置
uint8_t dose; // 1~10 表示几片,11 表示半片
uint8_t freq; // 1~4 表示一天几次
uint8_t hours[4]; // 服药小时
uint8_t minutes[4]; // 服药分钟
};
Slot slots[6];
enum MainState {
IDLE,
SET_MODE,
REMINDING
};
enum SetPhase {
SET_MATCH, // 等待“开盖 + 说编号”匹配
SET_DOSE, // 等待剂量
SET_FREQ, // 等待频次
SET_WAIT_CLOSE // 等待关盖保存
};
MainState currentState = IDLE;
SetPhase setPhase = SET_MATCH;
// ==================== 设置模式变量 ====================
int spokenSlot = -1; // 用户说出的格子编号,0~5
int openedSlot = -1; // 当前检测到打开的格子,0~5
int activeSetSlot = -1; // 正在设置的格子,0~5
uint8_t tempDose = 0;
uint8_t tempFreq = 0;
unsigned long setModeTimer = 0;
const unsigned long SET_TIMEOUT = 30000;
bool mismatchAnnounced = false;
// ==================== 提醒模式变量 ====================
struct ReminderState {
bool active;
bool pending[6]; // 当前时间段仍未完成的格子
uint8_t count; // 当前时间段剩余格子数
int openedDueSlot; // 当前打开的待服药格子
int playingSlot; // 当前正在播报剂量的格子
bool blinkOn;
unsigned long lastBlink;
};
ReminderState reminder;
bool lastLidState[6]; // 用于检测开盖/关盖边沿
long lastTriggeredMinute = -1;
// ==================== 函数声明 ====================
void processCommand(String cmd);
void checkSerialCommand();
void checkControlButtons();
void speakText(const String &text);
void stopSpeak();
bool isLidOpen(int idx);
int getFirstOpenSlot();
void setLED(int idx, bool on);
void allLEDOff();
void enterSetMode();
void exitSetMode();
void resetSetTransaction();
void handleSetMode();
void tryBeginSetting();
void saveCurrentSlot();
void calculateTimes(Slot &s);
void checkSchedule();
void triggerReminder(int dueSlots[], int count);
void handleReminder();
void finishReminder();
void setRtcByCommand(String cmd);
void printCurrentTime();
void printSlotList();
void clearAllSlots();
String doseToText(uint8_t dose);
String freqToText(uint8_t freq);
void printHelp();
// ==================== 初始化 ====================
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println(F("======================================"));
Serial.println(F("智能语音药盒仿真启动"));
Serial.println(F("输入 HELP 查看命令"));
Serial.println(F("======================================"));
Wire.begin(21, 22);
if (!rtc.begin()) {
Serial.println(F("[错误] 未找到 RTC 模块,请检查 Wokwi 接线。"));
while (1) delay(100);
}
if (!rtc.isrunning()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
pinMode(PIN_BTN_MODE, INPUT_PULLUP);
pinMode(PIN_BTN_GOTIT, INPUT_PULLUP);
for (int i = 0; i < 6; i++) {
pinMode(BTN_PINS[i], INPUT_PULLUP);
pinMode(LED_PINS[i], OUTPUT);
setLED(i, false);
lastLidState[i] = isLidOpen(i);
}
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
clearAllSlots();
memset(&reminder, 0, sizeof(reminder));
reminder.openedDueSlot = -1;
reminder.playingSlot = -1;
speakText("药盒已启动");
printHelp();
}
// ==================== 主循环 ====================
void loop() {
checkSerialCommand();
checkControlButtons();
switch (currentState) {
case IDLE:
checkSchedule();
break;
case SET_MODE:
handleSetMode();
break;
case REMINDING:
handleReminder();
break;
}
}
// ==================== 串口命令 ====================
void checkSerialCommand() {
static String buffer = "";
while (Serial.available()) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
buffer.trim();
if (buffer.length() > 0) {
processCommand(buffer);
buffer = "";
}
} else {
buffer += c;
}
}
}
void processCommand(String cmd) {
cmd.trim();
String upper = cmd;
upper.toUpperCase();
Serial.print(F("[命令] "));
Serial.println(upper);
if (upper == "HELP" || upper == "H") {
printHelp();
return;
}
if (upper == "TIME") {
printCurrentTime();
return;
}
if (upper.startsWith("SETRTC")) {
setRtcByCommand(upper);
return;
}
if (upper == "LIST") {
printSlotList();
return;
}
if (upper == "CLEAR") {
clearAllSlots();
speakText("所有药盒设置已清空");
return;
}
if (upper.startsWith("OPEN_")) {
int n = upper.charAt(5) - '1';
if (n >= 0 && n < 6) {
simulatedLidOpen[n] = true;
Serial.print(F("[模拟] "));
Serial.print(n + 1);
Serial.println(F("号格已打开"));
} else {
Serial.println(F("[错误] OPEN 命令编号无效"));
}
return;
}
if (upper.startsWith("CLOSE_")) {
int n = upper.charAt(6) - '1';
if (n >= 0 && n < 6) {
simulatedLidOpen[n] = false;
Serial.print(F("[模拟] "));
Serial.print(n + 1);
Serial.println(F("号格已关闭"));
} else {
Serial.println(F("[错误] CLOSE 命令编号无效"));
}
return;
}
// 通用打断命令:只停止播报,不代表已经服药
if (upper == "GOT_IT" || upper == "G" || upper == "STOP_SPEECH") {
if (currentState == REMINDING && reminder.playingSlot != -1) {
stopSpeak();
Serial.println(F("[提醒] 已停止当前剂量播报,但仍需关盖才算完成服药。"));
reminder.playingSlot = -1;
} else {
Serial.println(F("[提示] 当前没有正在播报的剂量。"));
}
return;
}
// 进入设置模式
if (upper == "MODE_SET" || upper == "M") {
if (currentState == REMINDING) {
speakText("当前正在提醒服药,暂不能设置");
} else {
enterSetMode();
}
return;
}
// 退出设置模式
if (upper == "EXIT_SET" || upper == "X") {
if (currentState == SET_MODE) {
exitSetMode();
} else {
Serial.println(F("[提示] 当前不在设置模式。"));
}
return;
}
// 设置模式下的命令
if (currentState == SET_MODE) {
if (upper.startsWith("SLOT_")) {
if (setPhase != SET_MATCH) {
speakText("当前格子尚未设置完成,请先完成或退出设置");
return;
}
int n = upper.charAt(5) - '1';
if (n < 0 || n >= 6) {
speakText("格子编号无效");
return;
}
spokenSlot = n;
mismatchAnnounced = false;
Serial.print(F("[设置] 已识别语音编号:"));
Serial.print(spokenSlot + 1);
Serial.println(F("号格"));
if (!isLidOpen(spokenSlot)) {
speakText("请打开对应格子");
}
tryBeginSetting();
setModeTimer = millis();
return;
}
if (upper.startsWith("DOSE_")) {
if (setPhase != SET_DOSE || activeSetSlot == -1) {
speakText("请先打开格子并说出对应编号");
return;
}
if (!isLidOpen(activeSetSlot)) {
speakText("格子已关闭,设置取消");
resetSetTransaction();
return;
}
String d = upper.substring(5);
d.trim();
if (d == "HALF") {
tempDose = 11;
} else {
int val = d.toInt();
if (val < 1 || val > 10) {
speakText("剂量无效");
return;
}
tempDose = val;
}
setPhase = SET_FREQ;
speakText("请说服用频次");
setModeTimer = millis();
return;
}
if (upper.startsWith("FREQ_")) {
if (setPhase != SET_FREQ || activeSetSlot == -1) {
speakText("请先设置剂量");
return;
}
if (!isLidOpen(activeSetSlot)) {
speakText("格子已关闭,设置取消");
resetSetTransaction();
return;
}
int val = upper.charAt(5) - '0';
if (val < 1 || val > 4) {
speakText("频次无效");
return;
}
tempFreq = val;
setPhase = SET_WAIT_CLOSE;
String msg = String(activeSetSlot + 1) + "号格," +
doseToText(tempDose) + "," +
freqToText(tempFreq) +
"。请关闭药盒保存";
speakText(msg);
setModeTimer = millis();
return;
}
}
Serial.println(F("[提示] 未识别命令,输入 HELP 查看可用命令。"));
}
// ==================== 功能按钮 ====================
void checkControlButtons() {
static bool lastModePressed = false;
static bool lastGotPressed = false;
bool modePressed = digitalRead(PIN_BTN_MODE) == LOW;
bool gotPressed = digitalRead(PIN_BTN_GOTIT) == LOW;
if (modePressed && !lastModePressed) {
processCommand("MODE_SET");
}
if (gotPressed && !lastGotPressed) {
processCommand("GOT_IT");
}
lastModePressed = modePressed;
lastGotPressed = gotPressed;
}
// ==================== 模拟语音输出 ====================
void speakText(const String &text) {
Serial.print(F("[语音] "));
Serial.println(text);
// 蜂鸣器短响,模拟语音播报开始
tone(PIN_BUZZER, 1000, 120);
}
void stopSpeak() {
noTone(PIN_BUZZER);
Serial.println(F("[语音] 播报已停止"));
}
// ==================== 基础硬件抽象 ====================
bool isLidOpen(int idx) {
if (idx < 0 || idx >= 6) return false;
// 串口命令模拟开盖优先
if (simulatedLidOpen[idx]) return true;
// 实体/仿真按钮:INPUT_PULLUP,按下接地 = LOW = 开盖
return digitalRead(BTN_PINS[idx]) == LOW;
}
int getFirstOpenSlot() {
for (int i = 0; i < 6; i++) {
if (isLidOpen(i)) return i;
}
return -1;
}
void setLED(int idx, bool on) {
if (idx < 0 || idx >= 6) return;
// 本仿真接法:GPIO -> LED正极,LED负极 -> GND
digitalWrite(LED_PINS[idx], on ? HIGH : LOW);
}
void allLEDOff() {
for (int i = 0; i < 6; i++) {
setLED(i, false);
}
}
// ==================== 设置模式 ====================
void enterSetMode() {
currentState = SET_MODE;
resetSetTransaction();
setModeTimer = millis();
speakText("进入设置模式,请打开格子并说出编号");
Serial.println(F("[状态] SET_MODE"));
}
void exitSetMode() {
currentState = IDLE;
resetSetTransaction();
allLEDOff();
speakText("已退出设置模式");
Serial.println(F("[状态] IDLE"));
}
void resetSetTransaction() {
setPhase = SET_MATCH;
spokenSlot = -1;
openedSlot = -1;
activeSetSlot = -1;
tempDose = 0;
tempFreq = 0;
mismatchAnnounced = false;
}
void handleSetMode() {
if (millis() - setModeTimer > SET_TIMEOUT) {
speakText("设置超时,已退出");
exitSetMode();
return;
}
// 如果已经进入剂量/频次/等待关盖阶段,目标格子提前关闭则取消或保存
if (activeSetSlot != -1) {
bool open = isLidOpen(activeSetSlot);
if (!open) {
if (setPhase == SET_WAIT_CLOSE) {
saveCurrentSlot();
} else if (setPhase == SET_DOSE || setPhase == SET_FREQ) {
speakText("设置未完成,已取消");
resetSetTransaction();
setModeTimer = millis();
}
}
return;
}
// SET_MATCH 阶段:持续观察当前打开的格子
int nowOpen = getFirstOpenSlot();
if (nowOpen != openedSlot) {
openedSlot = nowOpen;
mismatchAnnounced = false;
if (openedSlot != -1 && spokenSlot == -1) {
speakText("请说出格子编号");
setModeTimer = millis();
}
tryBeginSetting();
}
}
void tryBeginSetting() {
if (setPhase != SET_MATCH) return;
if (spokenSlot == -1 || openedSlot == -1) {
return;
}
if (spokenSlot == openedSlot) {
activeSetSlot = spokenSlot;
setPhase = SET_DOSE;
speakText("请说剂量");
Serial.print(F("[设置] 双重确认成功:"));
Serial.print(activeSetSlot + 1);
Serial.println(F("号格"));
setModeTimer = millis();
return;
}
if (!mismatchAnnounced) {
speakText("打开的格子和说出的编号不一致,请关闭后重新操作");
Serial.print(F("[设置] 不匹配:语音="));
Serial.print(spokenSlot + 1);
Serial.print(F("号格,打开="));
Serial.print(openedSlot + 1);
Serial.println(F("号格"));
mismatchAnnounced = true;
setModeTimer = millis();
}
}
void saveCurrentSlot() {
if (activeSetSlot < 0 || activeSetSlot >= 6) {
resetSetTransaction();
return;
}
Slot &s = slots[activeSetSlot];
s.enabled = true;
s.dose = tempDose;
s.freq = tempFreq;
calculateTimes(s);
String msg = String(activeSetSlot + 1) + "号格设置已保存";
speakText(msg);
Serial.print(F("[保存] "));
Serial.print(activeSetSlot + 1);
Serial.print(F("号格,剂量="));
Serial.print(doseToText(s.dose));
Serial.print(F(",频次="));
Serial.println(freqToText(s.freq));
Serial.print(F("[保存] 服药时间:"));
for (int i = 0; i < s.freq; i++) {
if (i > 0) Serial.print(F(", "));
if (s.hours[i] < 10) Serial.print('0');
Serial.print(s.hours[i]);
Serial.print(':');
if (s.minutes[i] < 10) Serial.print('0');
Serial.print(s.minutes[i]);
}
Serial.println();
resetSetTransaction();
setModeTimer = millis();
speakText("可继续设置其他格子,或说退出设置");
}
// ==================== 服药时间计算 ====================
void calculateTimes(Slot &s) {
// 固定白天服药时间表,符合当前需求:
// 1次:08:00
// 2次:08:00, 14:00
// 3次:08:00, 12:00, 16:00
// 4次:08:00, 11:00, 14:00, 17:00
for (int i = 0; i < 4; i++) {
s.hours[i] = 0;
s.minutes[i] = 0;
}
if (s.freq == 1) {
s.hours[0] = 8;
} else if (s.freq == 2) {
s.hours[0] = 8;
s.hours[1] = 14;
} else if (s.freq == 3) {
s.hours[0] = 8;
s.hours[1] = 12;
s.hours[2] = 16;
} else if (s.freq == 4) {
s.hours[0] = 8;
s.hours[1] = 11;
s.hours[2] = 14;
s.hours[3] = 17;
}
}
// ==================== 服药提醒 ====================
void checkSchedule() {
DateTime now = rtc.now();
long minuteKey = now.unixtime() / 60;
// 同一分钟只触发一次,避免 loop 重复触发
if (minuteKey == lastTriggeredMinute) return;
int dueSlots[6];
int dueCount = 0;
for (int i = 0; i < 6; i++) {
if (!slots[i].enabled) continue;
for (int j = 0; j < slots[i].freq; j++) {
if (slots[i].hours[j] == now.hour() &&
slots[i].minutes[j] == now.minute()) {
dueSlots[dueCount++] = i;
break;
}
}
}
if (dueCount > 0) {
lastTriggeredMinute = minuteKey;
triggerReminder(dueSlots, dueCount);
}
}
void triggerReminder(int dueSlots[], int count) {
currentState = REMINDING;
memset(&reminder, 0, sizeof(reminder));
reminder.active = true;
reminder.count = count;
reminder.openedDueSlot = -1;
reminder.playingSlot = -1;
reminder.blinkOn = false;
reminder.lastBlink = millis();
for (int i = 0; i < count; i++) {
int idx = dueSlots[i];
reminder.pending[idx] = true;
}
// 刷新盖子状态,避免刚进入提醒时误判边沿
for (int i = 0; i < 6; i++) {
lastLidState[i] = isLidOpen(i);
}
speakText("该吃药了");
Serial.print(F("[提醒] 当前时间段需服用格子:"));
for (int i = 0; i < 6; i++) {
if (reminder.pending[i]) {
Serial.print(i + 1);
Serial.print(F("号 "));
}
}
Serial.println();
Serial.println(F("[状态] REMINDING"));
}
void handleReminder() {
// 非阻塞 LED 闪烁
if (millis() - reminder.lastBlink >= 500) {
reminder.lastBlink = millis();
reminder.blinkOn = !reminder.blinkOn;
for (int i = 0; i < 6; i++) {
if (reminder.pending[i]) {
setLED(i, reminder.blinkOn);
} else {
setLED(i, false);
}
}
}
// 处理开盖/关盖边沿
for (int i = 0; i < 6; i++) {
bool nowOpen = isLidOpen(i);
// 开盖边沿:关闭 -> 打开
if (nowOpen && !lastLidState[i]) {
if (reminder.pending[i]) {
reminder.openedDueSlot = i;
reminder.playingSlot = i;
String msg = String(i + 1) + "号格," + doseToText(slots[i].dose);
speakText(msg);
Serial.print(F("[提醒] 打开 "));
Serial.print(i + 1);
Serial.println(F("号格,开始播报剂量。"));
} else {
Serial.print(F("[提醒] "));
Serial.print(i + 1);
Serial.println(F("号格当前时间段不需要服用。"));
}
}
// 关盖边沿:打开 -> 关闭
if (!nowOpen && lastLidState[i]) {
if (reminder.openedDueSlot == i && reminder.pending[i]) {
if (reminder.playingSlot == i) {
stopSpeak();
reminder.playingSlot = -1;
}
reminder.pending[i] = false;
if (reminder.count > 0) {
reminder.count--;
}
setLED(i, false);
Serial.print(F("[提醒] "));
Serial.print(i + 1);
Serial.println(F("号格已关闭,判定该格服药完成。"));
reminder.openedDueSlot = -1;
if (reminder.count == 0) {
finishReminder();
lastLidState[i] = nowOpen;
return;
}
}
}
lastLidState[i] = nowOpen;
}
}
void finishReminder() {
allLEDOff();
stopSpeak();
reminder.active = false;
reminder.openedDueSlot = -1;
reminder.playingSlot = -1;
speakText("本时间段药品已全部服用");
currentState = IDLE;
Serial.println(F("[状态] IDLE"));
}
// ==================== RTC 与调试命令 ====================
void setRtcByCommand(String cmd) {
// 格式:SETRTC 08:00
cmd.trim();
int spaceIdx = cmd.indexOf(' ');
if (spaceIdx < 0) {
Serial.println(F("[错误] 格式应为:SETRTC 08:00"));
return;
}
String timePart = cmd.substring(spaceIdx + 1);
timePart.trim();
int colonIdx = timePart.indexOf(':');
if (colonIdx < 0) {
Serial.println(F("[错误] 格式应为:SETRTC 08:00"));
return;
}
int hh = timePart.substring(0, colonIdx).toInt();
int mm = timePart.substring(colonIdx + 1).toInt();
if (hh < 0 || hh > 23 || mm < 0 || mm > 59) {
Serial.println(F("[错误] 时间范围无效。"));
return;
}
DateTime now = rtc.now();
rtc.adjust(DateTime(now.year(), now.month(), now.day(), hh, mm, 0));
// 方便重复测试:手动改时间后允许再次触发
lastTriggeredMinute = -1;
Serial.print(F("[RTC] 时间已设置为 "));
if (hh < 10) Serial.print('0');
Serial.print(hh);
Serial.print(':');
if (mm < 10) Serial.print('0');
Serial.println(mm);
speakText("时间已设置");
}
void printCurrentTime() {
DateTime now = rtc.now();
Serial.print(F("[TIME] "));
Serial.print(now.year());
Serial.print('-');
Serial.print(now.month());
Serial.print('-');
Serial.print(now.day());
Serial.print(' ');
if (now.hour() < 10) Serial.print('0');
Serial.print(now.hour());
Serial.print(':');
if (now.minute() < 10) Serial.print('0');
Serial.print(now.minute());
Serial.print(':');
if (now.second() < 10) Serial.print('0');
Serial.println(now.second());
}
void printSlotList() {
Serial.println(F("========== 药盒设置列表 =========="));
for (int i = 0; i < 6; i++) {
Serial.print(i + 1);
Serial.print(F("号格:"));
if (!slots[i].enabled) {
Serial.println(F("未设置"));
continue;
}
Serial.print(doseToText(slots[i].dose));
Serial.print(F(","));
Serial.print(freqToText(slots[i].freq));
Serial.print(F(",时间:"));
for (int j = 0; j < slots[i].freq; j++) {
if (j > 0) Serial.print(F(", "));
if (slots[i].hours[j] < 10) Serial.print('0');
Serial.print(slots[i].hours[j]);
Serial.print(':');
if (slots[i].minutes[j] < 10) Serial.print('0');
Serial.print(slots[i].minutes[j]);
}
Serial.println();
}
Serial.println(F("================================"));
}
void clearAllSlots() {
for (int i = 0; i < 6; i++) {
slots[i].enabled = false;
slots[i].dose = 0;
slots[i].freq = 0;
for (int j = 0; j < 4; j++) {
slots[i].hours[j] = 0;
slots[i].minutes[j] = 0;
}
setLED(i, false);
}
lastTriggeredMinute = -1;
}
// ==================== 文本工具 ====================
String doseToText(uint8_t dose) {
if (dose == 11) {
return "半片";
}
return String(dose) + "片";
}
String freqToText(uint8_t freq) {
if (freq == 1) return "一天一次";
if (freq == 2) return "一天两次";
if (freq == 3) return "一天三次";
if (freq == 4) return "一天四次";
return "频次无效";
}
void printHelp() {
Serial.println();
Serial.println(F("========== 可用命令 =========="));
Serial.println(F("HELP / H 查看帮助"));
Serial.println(F("MODE_SET / M 进入设置模式"));
Serial.println(F("EXIT_SET / X 退出设置模式"));
Serial.println(F("SLOT_1 ~ SLOT_6 说出几号格"));
Serial.println(F("DOSE_1 ~ DOSE_10 设置剂量"));
Serial.println(F("DOSE_HALF 设置半片"));
Serial.println(F("FREQ_1 ~ FREQ_4 设置一天几次"));
Serial.println(F("GOT_IT / G 模拟“知道了”,停止播报"));
Serial.println(F("SETRTC 08:00 设置仿真时间"));
Serial.println(F("TIME 查看当前时间"));
Serial.println(F("LIST 查看药盒设置"));
Serial.println(F("CLEAR 清空所有设置"));
Serial.println(F("OPEN_1 ~ OPEN_6 模拟打开药盒"));
Serial.println(F("CLOSE_1 ~ CLOSE_6 模拟关闭药盒"));
Serial.println(F("=============================="));
Serial.println();
}