跳转到内容

定时调度

定时调度

本节介绍如何在 ESP32 音频播放器中实现基于时间的自动调度功能。学习完成后,您将能够:

  • 在 ESP32 上实现精准的时间同步
  • 配置定时播放和停止逻辑
  • 实现工作日和节假日的差异化调度
  • 通过 MQTT 远程修改调度计划

在开始本节之前,请确保:

  • 已完成播放状态管理
  • 了解 ESP32 的 NTP 时间同步功能
  • 了解基本的 cron 表达式或定时触发概念

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();
}
}
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]);
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;
}
};
// 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) + "}");
}
// 节假日配置(通过 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, "节假日:关闭广播"},
};
功能实现方式买家价值
自动开关机定时调度”广播系统无需人工操作,自动运行”
工作日/周末差异化日期匹配”工作日班前播放,节假日关闭”
内容定时切换电台切换”不同时段播放不同内容”
远程修改计划MQTT 下发”可在办公室远程修改车间广播计划”
定时音量调节音量预设”午休自动降低音量”

本节介绍了基于时间的调度功能:

  1. NTP 时间同步:ESP32 通过 NTP 协议从互联网获取精确时间
  2. 调度条目:配置时间、动作、电台、音量、日期掩码
  3. 调度管理器:分钟级精度检查和执行调度计划
  4. 远程管理:通过 MQTT 下发新的调度配置
  5. 节假日支持:可配置节假日特殊调度