定时调度
定时调度
本节介绍如何在 ESP32 音频播放器中实现基于时间的自动调度功能。学习完成后,您将能够:
- 在 ESP32 上实现精准的时间同步
- 配置定时播放和停止逻辑
- 实现工作日和节假日的差异化调度
- 通过 MQTT 远程修改调度计划
在开始本节之前,请确保:
- 已完成播放状态管理
- 了解 ESP32 的 NTP 时间同步功能
- 了解基本的 cron 表达式或定时触发概念
Time Sync with NTP
Section titled “Time Sync with NTP”NTP Time Synchronization
Section titled “NTP Time Synchronization”ESP32 通过 NTP(Network Time Protocol)从互联网获取精确时间:
#include <time.h>
// NTP 配置const char* ntpServer = "pool.ntp.org";const long gmtOffset_sec = 3600; // UTC+1 (中欧时间)const int daylightOffset_sec = 3600; // 夏令时偏移
void syncTime() { configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("正在同步 NTP 时间...");
time_t now; struct tm timeinfo;
int retry = 0; while (!getLocalTime(&timeinfo) && retry < 10) { Serial.print("."); delay(1000); retry++; }
if (retry < 10) { Serial.println("\n时间同步成功"); Serial.printf("当前时间: %04d-%02d-%02d %02d:%02d:%02d\n", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); } else { Serial.println("\n时间同步失败,使用上次保存的时间"); }}
// 每日重新同步(防止时间漂移)void dailyTimeResync() { static unsigned long lastSync = 0; if (millis() - lastSync > 86400000) { // 24 小时 syncTime(); lastSync = millis(); }}Schedule Configuration
Section titled “Schedule Configuration”Schedule Structure
Section titled “Schedule Structure”struct ScheduleEntry { int hour; // 0-23 int minute; // 0-59 int action; // 0=停止, 1=播放, 2=切换电台 int stationIndex; // 电台索引(当 action==2 时有效) int volume; // 目标音量 int weekdays; // 位掩码: 0x7F = 每天, 0x1F = 工作日 const char* description; // 描述文本};
// 位掩码定义#define MON (1 << 0)#define TUE (1 << 1)#define WED (1 << 2)#define THU (1 << 3)#define FRI (1 << 4)#define SAT (1 << 5)#define SUN (1 << 6)
#define WORKDAYS (MON | TUE | WED | THU | FRI)#define WEEKENDS (SAT | SUN)#define EVERYDAY (WORKDAYS | WEEKENDS)
// 默认调度计划ScheduleEntry schedule[] = { {6, 0, 1, 0, 40, WORKDAYS, "工作日:起床晨间新闻"}, {7, 30, 2, 1, 60, WORKDAYS, "工作日:背景音乐"}, {12, 0, 1, 2, 50, WORKDAYS, "午休:轻音乐"}, {13, 0, 2, 1, 60, WORKDAYS, "下午:背景音乐"}, {17, 0, 0, 0, 0, WORKDAYS, "工作日结束:关闭广播"}, {8, 0, 1, 1, 40, WEEKENDS, "周末:休闲音乐"}, {21, 0, 0, 0, 0, EVERYDAY, "夜间:关闭广播"},};
const int scheduleCount = sizeof(schedule) / sizeof(schedule[0]);Schedule Manager
Section titled “Schedule Manager”Schedule Execution
Section titled “Schedule Execution”class ScheduleManager {private: ScheduleEntry* entries; int entryCount; int lastExecuted; // 防止重复执行
public: ScheduleManager(ScheduleEntry* schedule, int count) : entries(schedule), entryCount(count), lastExecuted(-1) {}
// 检查并执行调度 void checkAndExecute() { time_t now; struct tm timeinfo; time(&now); localtime_r(&now, &timeinfo);
int currentHour = timeinfo.tm_hour; int currentMinute = timeinfo.tm_min; int currentWeekday = timeinfo.tm_wday; // 0=周日, 1=周一, ...
// 将 wday 转换为我们的位掩码 int weekdayBit = 1 << ((currentWeekday + 6) % 7);
for (int i = 0; i < entryCount; i++) { // 检查时间和日期的匹配 if (entries[i].hour == currentHour && entries[i].minute == currentMinute && (entries[i].weekdays & weekdayBit) && i != lastExecuted) {
executeEntry(entries[i]); lastExecuted = i; break; } }
// 分钟变化后重置 lastExecuted,允许下次执行 static int lastMinute = -1; if (currentMinute != lastMinute) { lastMinute = currentMinute; lastExecuted = -1; } }
// 执行调度条目 void executeEntry(const ScheduleEntry& entry) { Serial.printf("⏰ 执行调度: %s (%02d:%02d)\n", entry.description, entry.hour, entry.minute);
// 发布调度事件 String eventMsg = String("{\"event\":\"schedule_trigger\",") + "\"description\":\"" + entry.description + "\"," + "\"hour\":" + entry.hour + "," + "\"minute\":" + entry.minute + "}"; mqttClient.publish("factory/broadcast/schedule/event", eventMsg.c_str());
switch (entry.action) { case 0: // 停止 player.stop(); break;
case 1: // 播放 setVolume(entry.volume); player.play(stationURLs[entry.stationIndex]); break;
case 2: // 切换电台 setVolume(entry.volume); switchStation(entry.stationIndex); break; } }
// 获取当前生效的调度(用于状态上报) int getActiveScheduleIndex() { // 查找最近执行的调度 if (lastExecuted >= 0 && lastExecuted < entryCount) { return lastExecuted; } return -1; }};Remote Schedule Management
Section titled “Remote Schedule Management”MQTT Schedule Update
Section titled “MQTT Schedule Update”// MQTT Topic: factory/broadcast/schedule/update// JSON 格式:// [// {"hour":6,"minute":0,"action":1,"station":0,"volume":40,"days":"workdays","desc":"晨间新闻"},// ...// ]
void handleScheduleUpdate(const char* jsonPayload) { DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, jsonPayload);
if (error) { Serial.printf("调度配置 JSON 解析失败: %s\n", error.c_str()); return; }
JsonArray newSchedule = doc.as<JsonArray>(); int count = newSchedule.size();
if (count > 50) { Serial.println("调度条目过多(超过 50 条)"); return; }
// 更新调度计划 for (int i = 0; i < count; i++) { JsonObject entry = newSchedule[i]; // 解析并更新调度数组(需在 SPIFFS 或 Preferences 中持久化) }
Serial.printf("调度计划已更新: %d 条\n", count);
// 发布确认 mqttClient.publish("factory/broadcast/schedule/status", "{\"status\":\"updated\",\"entries\":" + String(count) + "}");}Holiday Calendar Integration
Section titled “Holiday Calendar Integration”Holiday Schedule Override
Section titled “Holiday Schedule Override”// 节假日配置(通过 MQTT 下发)struct HolidayEntry { int month; int day; const char* description;};
HolidayEntry holidays[] = { {1, 1, "元旦"}, {5, 1, "劳动节"}, {10, 1, "国庆节"}, {12, 25, "圣诞节"},};
bool isHoliday(struct tm* timeinfo) { for (const auto& h : holidays) { if (h.month == timeinfo->tm_mon + 1 && h.day == timeinfo->tm_mday) { return true; } } return false;}
// 节假日使用特殊调度ScheduleEntry holidaySchedule[] = { {8, 0, 1, 0, 30, MON, "节假日:低音量晨间广播"}, {21, 0, 0, 0, 0, MON, "节假日:关闭广播"},};Pre-sales Key Points
Section titled “Pre-sales Key Points”定时调度能力
Section titled “定时调度能力”| 功能 | 实现方式 | 买家价值 |
|---|---|---|
| 自动开关机 | 定时调度 | ”广播系统无需人工操作,自动运行” |
| 工作日/周末差异化 | 日期匹配 | ”工作日班前播放,节假日关闭” |
| 内容定时切换 | 电台切换 | ”不同时段播放不同内容” |
| 远程修改计划 | MQTT 下发 | ”可在办公室远程修改车间广播计划” |
| 定时音量调节 | 音量预设 | ”午休自动降低音量” |
Summary
Section titled “Summary”本节介绍了基于时间的调度功能:
- NTP 时间同步:ESP32 通过 NTP 协议从互联网获取精确时间
- 调度条目:配置时间、动作、电台、音量、日期掩码
- 调度管理器:分钟级精度检查和执行调度计划
- 远程管理:通过 MQTT 下发新的调度配置
- 节假日支持:可配置节假日特殊调度