
本教案将带领您制作一个基于ESP32的网络闹钟。它通过WiFi连接互联网,使用NTP协议获取准确时间,并通过巴法云MQTT接收远程指令来设定闹钟。当到达设定时间时,蜂鸣器鸣响,同时点阵屏显示提醒信息。点阵屏平时显示当前时间(时:分),收到其他文字消息时也可短暂显示。
功能特点:
HH:MM:SS)。Alarm!。组件 | 数量 | 说明 |
ESP32开发板 | 1块 | 核心控制 |
4合1点阵屏 | 1个 | MAX7219驱动,8×32像素 |
有源蜂鸣器 | 1个 | 通电即响,3.3V驱动 |
杜邦线 | 若干 | 母对母 |
点阵屏引脚 | ESP32引脚 |
VCC | 5V |
GND | GND |
DIN | GPIO23 |
CLK | GPIO18 |
CS | GPIO15 |
蜂鸣器引脚 | ESP32引脚 |
VCC | 3.3V |
GND | GND |
S(信号) | GPIO13 |
注意:点阵屏务必接5V,否则亮度不足或无法点亮。蜂鸣器为有源型,直接高低电平控制。
在Arduino IDE中点击“项目” → “加载库” → “管理库”,搜索并安装以下库:
安装完成后,重启IDE。
我们将代码分为若干模块,逐一解释。您可以边阅读边在IDE中编写,最后合成完整程序。
#include <WiFi.h>
#include <PubSubClient.h>
#include <U8g2lib.h>
#include <NTPClient.h>
#include <WiFiUdp.h>WiFi.h:ESP32的WiFi功能。PubSubClient.h:MQTT通信。U8g2lib.h:点阵屏驱动。NTPClient.h 和 WiFiUdp.h:用于网络时间获取。const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";
const char* mqtt_server = "bemfa.com";
const int mqtt_port = 9501;
const char* mqtt_topic = "你的主题名"; // 巴法云主题,需在官网创建
const char* mqtt_client_id = "自定义客户端ID"; // 可任意,建议唯一
WiFiClient espClient;
PubSubClient client(espClient);ssid 和 password 替换为您自己的WiFi凭据。bemfa.com,端口 9501。esp32_alarm。WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "cn.pool.ntp.org", 8 * 3600, 60000);cn.pool.ntp.org。8*3600。#define MATRIX_CLK 18
#define MATRIX_DIN 23
#define MATRIX_CS 15
U8G2_MAX7219_32X8_F_4W_SW_SPI u8g2(U8G2_R0, MATRIX_CLK, MATRIX_DIN, MATRIX_CS, U8X8_PIN_NONE, U8X8_PIN_NONE);U8G2_R0 为正常方向),后面依次是CLK、DIN、CS,最后两个是DC和复位引脚,MAX7219不需要,填 U8X8_PIN_NONE。#define BUZZER_PIN 13struct AlarmTime {
uint8_t hour;
uint8_t minute;
uint8_t second;
bool enabled;
} alarmTime = {0, 0, 0, false};
int lastTriggerDay = -1; // 用于记录上次触发日期(简单版,未使用)
bool alarmActive = false;
unsigned long alarmStartTime = 0;
const unsigned long alarmDuration = 5000; // 鸣响5秒AlarmTime 结构体存储闹钟的小时、分钟、秒和启用标志。lastTriggerDay 本意是用于每天只触发一次,当前版本暂未使用(可后续扩展)。alarmActive 表示闹钟正在响铃,alarmStartTime 记录开始时间,用于控制响铃时长。void setup_wifi();
void reconnect();
void callback(char* topic, byte* payload, unsigned int length);
void checkAlarm();
void triggerAlarm();
void displayTime(int hour, int minute);
void displayMessage(const char* msg);void setup() {
Serial.begin(115200);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 8, "Init");
u8g2.sendBuffer();
setup_wifi();
timeClient.begin();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
}setup_wifi()void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}reconnect()void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
if (client.connect(mqtt_client_id)) {
Serial.println("connected");
client.subscribe(mqtt_topic);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}callback()void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.println(message);
if (length == 8) { // 格式 "HH:MM:SS"
int h, m, s;
if (sscanf(message.c_str(), "%d:%d:%d", &h, &m, &s) == 3) {
if (h >= 0 && h < 24 && m >= 0 && m < 60 && s >= 0 && s < 60) {
alarmTime.hour = h;
alarmTime.minute = m;
alarmTime.second = s;
alarmTime.enabled = true;
lastTriggerDay = -1; // 重置触发记录
Serial.println("Alarm set successfully");
char timeStr[6];
sprintf(timeStr, "%02d:%02d", h, m);
displayMessage(timeStr);
}
}
} else {
displayMessage(message.c_str());
}
}14:30:00),则解析时分秒,并更新闹钟结构体,显示设定时间(只显示时:分,避免超宽)。checkAlarm()void checkAlarm() {
if (!alarmTime.enabled) return;
int nowHour = timeClient.getHours();
int nowMinute = timeClient.getMinutes();
int nowSecond = timeClient.getSeconds();
// 当前版本每分钟的每一秒都可能触发(只要时分秒完全匹配)
if (nowHour == alarmTime.hour && nowMinute == alarmTime.minute && nowSecond == alarmTime.second) {
triggerAlarm();
// 如果需要每天只触发一次,可在此增加日期判断(见扩展)
}
}triggerAlarm()void triggerAlarm() {
Serial.println("ALARM TRIGGERED!");
alarmActive = true;
alarmStartTime = millis();
digitalWrite(BUZZER_PIN, HIGH);
displayMessage("Alarm!");
}Alarm!。void displayTime(int hour, int minute) {
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_ncenB08_tr);
char timeStr[6];
sprintf(timeStr, "%02d:%02d", hour, minute);
u8g2.drawStr(0, 8, timeStr);
} while (u8g2.nextPage());
}
void displayMessage(const char* msg) {
char shortMsg[7];
strncpy(shortMsg, msg, 6);
shortMsg[6] = '\0';
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 8, shortMsg);
} while (u8g2.nextPage());
}firstPage(),然后在循环中绘制,最后 nextPage() 结束。displayTime 显示格式化的 HH:MM。displayMessage 将消息截断为最多6字符(屏幕宽度32像素,每个字符约6像素,6字符约36像素,可能略微超出,但通常可显示),保证不溢出。loop()void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
static unsigned long lastTimeUpdate = 0;
if (millis() - lastTimeUpdate >= 1000) {
lastTimeUpdate = millis();
timeClient.update(); // 内部会按60秒间隔请求NTP
}
checkAlarm();
if (alarmActive) {
if (millis() - alarmStartTime >= alarmDuration) {
alarmActive = false;
digitalWrite(BUZZER_PIN, LOW);
}
} else {
static unsigned long lastDisplay = 0;
if (millis() - lastDisplay > 1000) {
lastDisplay = millis();
int h = timeClient.getHours();
int m = timeClient.getMinutes();
displayTime(h, m);
}
}
delay(10);
}打开串口监视器(115200),看到如下信息表示成功:
Connecting to Chinanet-2.4G-6E80...
WiFi connected
IP address: 192.168.x.x
Attempting MQTT connection...connected14:30:00 设置闹钟(注意英文冒号)。Hello)测试显示。12:34:20,则发送 12:34:30。Alarm!,5秒后自动停止。12:34。digitalWrite(13, HIGH); 看是否发声。setup() 中添加 u8g2.setContrast(150);。当前版本每次匹配时分秒都会触发(例如每分钟的同一秒都会触发)。若要改为每天一次,需加入日期判断。修改 checkAlarm() 如下:
#include <time.h> // 文件开头添加
void checkAlarm() {
if (!alarmTime.enabled) return;
int nowHour = timeClient.getHours();
int nowMinute = timeClient.getMinutes();
int nowSecond = timeClient.getSeconds();
time_t epoch = timeClient.getEpochTime();
struct tm *ptm = localtime(&epoch);
int currentDate = (ptm->tm_year + 1900) * 10000 + (ptm->tm_mon + 1) * 100 + ptm->tm_mday;
if (nowHour == alarmTime.hour && nowMinute == alarmTime.minute && nowSecond == alarmTime.second) {
if (currentDate != lastTriggerDay) {
triggerAlarm();
lastTriggerDay = currentDate;
}
}
}同时,在 callback 中重置 lastTriggerDay = -1;。
可以将 alarmTime 改为数组,例如 AlarmTime alarms[5];,然后循环检查。
如果WiFi断开,时间将停止更新。可考虑加入DS1302作为备份,或增加错误提示。
// 此处粘贴用户提供的最终成功代码,注意提醒用户修改WiFi信息
#include <WiFi.h>
#include <PubSubClient.h>
#include <U8g2lib.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
// ========== WiFi 配置 ==========
const char* ssid = "xx-2.4G-6E80";
const char* password = "xxx";
// ========== 巴法云 MQTT 配置 ==========
const char* mqtt_server = "bemfa.com";
const int mqtt_port = 9501;
const char* mqtt_topic = "xxxx"; // 巴法云主题
const char* mqtt_client_id = "xxxx";
WiFiClient espClient;
PubSubClient client(espClient);
// ========== NTP 时间配置 ==========
WiFiUDP ntpUDP;
// 使用国内 NTP 服务器(pool.ntp.org 也可),时区为东八区(UTC+8)
NTPClient timeClient(ntpUDP, "cn.pool.ntp.org", 8 * 3600, 60000); // 更新间隔60秒
// ========== MAX7219 点阵屏 (8x32) ==========
#define MATRIX_CLK 18
#define MATRIX_DIN 23
#define MATRIX_CS 15
U8G2_MAX7219_32X8_F_4W_SW_SPI u8g2(U8G2_R0, MATRIX_CLK, MATRIX_DIN, MATRIX_CS, U8X8_PIN_NONE, U8X8_PIN_NONE);
// ========== 有源蜂鸣器 ==========
#define BUZZER_PIN 13
// ========== 闹钟数据结构 ==========
struct AlarmTime {
uint8_t hour;
uint8_t minute;
uint8_t second;
bool enabled;
} alarmTime = {0, 0, 0, false};
// 上次触发闹钟的日期(用于避免一天内重复触发)
int lastTriggerDay = -1; // -1 表示从未触发
// 闹钟激活状态
bool alarmActive = false;
unsigned long alarmStartTime = 0;
const unsigned long alarmDuration = 5000; // 鸣响5秒
// ========== 函数声明 ==========
void setup_wifi();
void reconnect();
void callback(char* topic, byte* payload, unsigned int length);
void checkAlarm();
void triggerAlarm();
void displayTime(int hour, int minute);
void displayMessage(const char* msg);
// ========== 初始化 ==========
void setup() {
Serial.begin(115200);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// 初始化点阵屏
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 8, "Init");
u8g2.sendBuffer();
// 连接 WiFi
setup_wifi();
// 初始化 NTP 客户端
timeClient.begin();
// 设置 MQTT
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
}
// ========== 主循环 ==========
void loop() {
// 保持 MQTT 连接
if (!client.connected()) {
reconnect();
}
client.loop();
// 更新时间(每秒检查一次)
static unsigned long lastTimeUpdate = 0;
if (millis() - lastTimeUpdate >= 1000) {
lastTimeUpdate = millis();
timeClient.update(); // 从 NTP 服务器获取最新时间(实际内部会按设定间隔请求,不会每秒都发请求)
}
// 检查闹钟
checkAlarm();
// 闹钟激活时持续显示报警信息
if (alarmActive) {
if (millis() - alarmStartTime >= alarmDuration) {
alarmActive = false;
digitalWrite(BUZZER_PIN, LOW);
}
} else {
// 非闹钟状态,每秒刷新一次时间显示
static unsigned long lastDisplay = 0;
if (millis() - lastDisplay > 1000) {
lastDisplay = millis();
int h = timeClient.getHours();
int m = timeClient.getMinutes();
displayTime(h, m);
}
}
delay(10);
}
// ========== WiFi 连接 ==========
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
// ========== MQTT 重连 ==========
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
if (client.connect(mqtt_client_id)) {
Serial.println("connected");
client.subscribe(mqtt_topic);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
// ========== MQTT 消息回调 ==========
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.println(message);
// 解析时间设置 HH:MM:SS
if (length == 8) { // 如 "14:30:00"
int h, m, s;
if (sscanf(message.c_str(), "%d:%d:%d", &h, &m, &s) == 3) {
if (h >= 0 && h < 24 && m >= 0 && m < 60 && s >= 0 && s < 60) {
alarmTime.hour = h;
alarmTime.minute = m;
alarmTime.second = s;
alarmTime.enabled = true;
// 重置触发日期,使新闹钟当天有效
lastTriggerDay = -1;
Serial.println("Alarm set successfully");
// 显示设定时间(只显示小时:分钟,避免超宽)
char timeStr[6];
sprintf(timeStr, "%02d:%02d", h, m);
displayMessage(timeStr);
}
}
} else {
// 其他消息直接显示
displayMessage(message.c_str());
}
}
// ========== 闹钟检查 ==========
void checkAlarm() {
if (!alarmTime.enabled) return;
int nowHour = timeClient.getHours();
int nowMinute = timeClient.getMinutes();
int nowSecond = timeClient.getSeconds();
int nowDay = timeClient.getDay(); // 注意:getDay() 返回星期几(0-6),不适合作为日期判断
// 改用日期判断需要更复杂,这里简单用“当天”的概念:我们记录触发时的“日”为 lastTriggerDay,
// 但 NTPClient 不直接提供年月日,需要自己从 epoch 解析。为简化,我们可以使用一个布尔标志
// 表示当天是否已经触发过,并每天重置。这里采用简单方法:每次触发后设置一个标志,并在每天午夜重置。
// 但为了快速演示,我们暂时取消一天一次的限制,改为每次匹配都触发(测试用)。
// 如果您希望每天只触发一次,需要获取当前日期(年/月/日),可以修改 NTPClient 获取完整时间。
// 为简化,我们先让每次匹配都触发(方便测试),正式使用时可改进。
// 简单起见,这里每次匹配都触发(如果不想重复,可以加上日期判断)
if (nowHour == alarmTime.hour && nowMinute == alarmTime.minute && nowSecond == alarmTime.second) {
// 取消一天一次的限制(注释掉)
// if (lastTriggerDay != timeClient.getDay()) { // 用星期几判断不准确
triggerAlarm();
// lastTriggerDay = timeClient.getDay(); // 不准确,仅作示例
// }
}
}
// ========== 触发闹钟 ==========
void triggerAlarm() {
Serial.println("ALARM TRIGGERED!");
alarmActive = true;
alarmStartTime = millis();
digitalWrite(BUZZER_PIN, HIGH);
displayMessage("Alarm!");
}
// ========== 显示时间(时:分) ==========
void displayTime(int hour, int minute) {
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_ncenB08_tr);
char timeStr[6];
sprintf(timeStr, "%02d:%02d", hour, minute);
u8g2.drawStr(0, 8, timeStr);
} while (u8g2.nextPage());
}
// ========== 显示任意消息(截断为最多6字符) ==========
void displayMessage(const char* msg) {
char shortMsg[7]; // 6字符 + 结束符
strncpy(shortMsg, msg, 6);
shortMsg[6] = '\0';
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 8, shortMsg);
} while (u8g2.nextPage());
}提醒:请将代码中的WiFi名称、密码和MQTT主题替换为您自己的信息。
通过本教案,您不仅实现了一个实用的网络闹钟,还学习了ESP32的WiFi、MQTT、NTP以及点阵屏的使用。您可以根据需要自由扩展功能,享受创造的乐趣!