以下是main.cpp文件:
cpp
#include <Arduino.h>
#include <ArduinoJson.h>
#include <WiFiManager.h>
#include "OneButton.h"
#include "led.h"
#include "_sntp.h"
#include "weather.h"
#include "screen_ink.h"
#include "_preference.h"
#include "version.h"
#include <esp_attr.h>
#define PIN_BUTTON GPIO_NUM_14 // 注意:由于此按键负责唤醒,因此需要选择支持RTC唤醒的PIN脚。
OneButton button;
RTC_DATA_ATTR volatile int _display_mode =0;
void IRAM_ATTR checkTicks() {
button.tick();
}
WiFiManager wm;
WiFiManagerParameter para_qweather_host("qweather_host", "和风天气Host", "", 64); // 和风天气key
WiFiManagerParameter para_qweather_key("qweather_key", "和风天气API Key", "", 32); // 和风天气key
// const char* test_html = "<br/><label for='test'>天气模式</label><br/><input type='radio' name='test' value='0' checked> 每日天气test </input><input type='radio' name='test' value='1'> 实时天气test</input>";
// WiFiManagerParameter para_test(test_html);
WiFiManagerParameter para_qweather_type("qweather_type", "天气类型(0:每日天气,1:实时天气)", "0", 2, "pattern='\\[0-1]{1}'"); // 城市code
WiFiManagerParameter para_qweather_location("qweather_loc", "位置ID", "", 64); // 城市code
WiFiManagerParameter para_cd_day_label("cd_day_label", "倒数日(4字以内)", "", 10); // 倒数日
WiFiManagerParameter para_cd_day_date("cd_day_date", "日期(yyyyMMdd)", "", 8, "pattern='\\d{8}'"); // 城市code
WiFiManagerParameter para_tag_days("tag_days", "日期Tag(yyyyMMddx,详见README)", "", 30); // 日期Tag
WiFiManagerParameter para_si_week_1st("si_week_1st", "每周起始(0:周日,1:周一)", "0", 2, "pattern='\\[0-1]{1}'"); // 每周第一天
void print_wakeup_reason() {
esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
switch (wakeup_reason) {
case ESP_SLEEP_WAKEUP_EXT0:
Serial.println(F("Wakeup caused by external signal using RTC_IO"));
_display_mode++;
_display_mode %= 3;
Serial.printf("display mode: %d\r\n", _display_mode);
break;
case ESP_SLEEP_WAKEUP_EXT1:
Serial.println(F("Wakeup caused by external signal using RTC_CNTL"));
break;
case ESP_SLEEP_WAKEUP_TIMER:
Serial.println(F("Wakeup caused by timer"));
break;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
Serial.println(F("Wakeup caused by touchpad"));
break;
case ESP_SLEEP_WAKEUP_ULP:
Serial.println(F("Wakeup caused by ULP program"));
break;
default:
Serial.printf( "Wakeup was not caused by deep sleep.\n");
}
}
void buttonClick(void* oneButton);
void buttonDoubleClick(void* oneButton);
void buttonLongPressStop(void* oneButton);
void go_sleep();
unsigned long _idle_millis;
unsigned long TIME_TO_SLEEP = 180 * 1000;
bool _wifi_flag = false;
unsigned long _wifi_failed_millis;
void setup() {
delay(10);
Serial.begin(115200);
Serial.println(".");
print_wakeup_reason();
Serial.println("\r\n\r\n\r\n");
delay(10);
button.setup(PIN_BUTTON,INPUT_PULLUP,true);
button.setClickMs(300);
button.setPressMs(3000); // 设置长按的时长
button.attachClick(buttonClick, &button);
button.attachDoubleClick(buttonDoubleClick, &button);
// button.attachMultiClick()
button.attachLongPressStop(buttonLongPressStop, &button);
attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), checkTicks, CHANGE);
Serial.printf("***********************\r\n");
Serial.printf(" J-Calendar\r\n");
Serial.printf(" version: %s\r\n", J_VERSION);
Serial.printf("***********************\r\n\r\n");
Serial.printf("Copyright © 2022-2025 JADE Software Co., Ltd. All Rights Reserved.\r\n\r\n");
led_init();
led_fast();
Serial.println(F("Wm begin..."));
wm.setHostname(F("J-Calendar"));
wm.setEnableConfigPortal(false);
wm.setConnectTimeout(10);
if (wm.autoConnect()) {
Serial.println(F("Connect OK."));
led_on();
_wifi_flag = true;
} else {
Serial.println(F("Connect failed."));
_wifi_flag = false;
_wifi_failed_millis = millis();
led_slow();
_sntp_exec(2);
weather_exec(2);
WiFi.mode(WIFI_OFF); // 提前关闭WIFI,省电
Serial.println(F("Wifi closed."));
}
}
/**
* 处理各个任务
* 1. sntp同步
* 前置条件:Wifi已连接
* 2. 刷新日历
* 前置条件:sntp同步完成(无论成功或失败)
* 3. 刷新天气信息
* 前置条件:wifi已连接
* 4. 系统配置
* 前置条件:无
* 5. 休眠
* 前置条件:所有任务都完成或失败,
*/
void loop() {
button.tick(); // 单击,刷新页面;双击,打开配置;长按,重启
wm.process();
// 前置任务:wifi已连接
// sntp同步
if(_display_mode == 0 ){
if (_sntp_status() == -1) {
_sntp_exec();
}
// 如果是定时器唤醒,并且接近午夜(23:50之后),则直接休眠
if (_sntp_status() == SYNC_STATUS_TOO_LATE) {
go_sleep();
}
// 前置任务:wifi已连接
// 获取Weather信息
if (weather_status() == -1) {
weather_exec();
}
// 刷新日历
// 前置任务:sntp、weather
// 执行条件:屏幕状态为待处理
if (_sntp_status() > 0 && weather_status() > 0 && si_screen_status() == -1) {
// 数据获取完毕后,关闭Wifi,省电
if (!wm.getConfigPortalActive()) {
WiFi.mode(WIFI_OFF);
}
Serial.println(F("Wifi closed after data fetch."));
Serial.printf("display mode: %d\r\n", _display_mode);
int mode = _display_mode;
si_screen(mode);
}
} else {
// 刷新天气信息
// 前置任务:wifi已连接
if(si_screen_status() == -1){
if (!wm.getConfigPortalActive()) {
WiFi.mode(WIFI_OFF);
}
int mode = _display_mode;
si_screen(mode);
}
}
// 休眠
// 前置条件:屏幕刷新完成(或成功)
// 未在配置状态,且屏幕刷新完成,进入休眠
if (!wm.getConfigPortalActive() && si_screen_status() > 0) {
if(_wifi_flag) {
go_sleep();
}
if(!_wifi_flag && millis() - _wifi_failed_millis > 10 * 1000) { // 如果wifi连接不成功,等待10秒休眠
go_sleep();
}
}
// 配置状态下,
if (wm.getConfigPortalActive() && millis() - _idle_millis > TIME_TO_SLEEP) {
go_sleep();
}
delay(10);
}
// 刷新页面
void buttonClick(void* oneButton) {
Serial.println(F("Button click."));
if (wm.getConfigPortalActive()) {
Serial.println(F("In config status."));
} else {
Serial.println(F("Refresh screen manually."));
// _display_mode++;
// _display_mode %= 2;
int mode = _display_mode;
si_screen(mode);
}
}
void saveParamsCallback() {
Preferences pref;
pref.begin(PREF_NAMESPACE);
pref.putString(PREF_QWEATHER_HOST, para_qweather_host.getValue());
pref.putString(PREF_QWEATHER_KEY, para_qweather_key.getValue());
pref.putString(PREF_QWEATHER_TYPE, strcmp(para_qweather_type.getValue(), "1") == 0 ? "1" : "0");
pref.putString(PREF_QWEATHER_LOC, para_qweather_location.getValue());
pref.putString(PREF_CD_DAY_LABLE, para_cd_day_label.getValue());
pref.putString(PREF_CD_DAY_DATE, para_cd_day_date.getValue());
pref.putString(PREF_TAG_DAYS, para_tag_days.getValue());
pref.putString(PREF_SI_WEEK_1ST, strcmp(para_si_week_1st.getValue(), "1") == 0 ? "1" : "0");
pref.end();
Serial.println(F("Params saved."));
_idle_millis = millis(); // 刷新无操作时间点
ESP.restart();
}
void preSaveParamsCallback() {
}
// 双击打开配置页面
void buttonDoubleClick(void* oneButton) {
Serial.println(F("Button double click."));
if (wm.getConfigPortalActive()) {
ESP.restart();
return;
}
if (weather_status == 0) {
weather_stop();
}
// 设置配置页面
// 根据配置信息设置默认值
Preferences pref;
pref.begin(PREF_NAMESPACE);
String qHost = pref.getString(PREF_QWEATHER_HOST);
String qToken = pref.getString(PREF_QWEATHER_KEY);
String qType = pref.getString(PREF_QWEATHER_TYPE, "0");
String qLoc = pref.getString(PREF_QWEATHER_LOC);
String cddLabel = pref.getString(PREF_CD_DAY_LABLE);
String cddDate = pref.getString(PREF_CD_DAY_DATE);
String tagDays = pref.getString(PREF_TAG_DAYS);
String week1st = pref.getString(PREF_SI_WEEK_1ST, "0");
pref.end();
para_qweather_host.setValue(qHost.c_str(), 64);
para_qweather_key.setValue(qToken.c_str(), 32);
para_qweather_location.setValue(qLoc.c_str(), 64);
para_qweather_type.setValue(qType.c_str(), 1);
para_cd_day_label.setValue(cddLabel.c_str(), 16);
para_cd_day_date.setValue(cddDate.c_str(), 8);
para_tag_days.setValue(tagDays.c_str(), 30);
para_si_week_1st.setValue(week1st.c_str(), 1);
wm.setTitle("J-Calendar");
wm.addParameter(¶_si_week_1st);
wm.addParameter(¶_qweather_host);
wm.addParameter(¶_qweather_key);
wm.addParameter(¶_qweather_type);
wm.addParameter(¶_qweather_location);
wm.addParameter(¶_cd_day_label);
wm.addParameter(¶_cd_day_date);
wm.addParameter(¶_tag_days);
// std::vector<const char *> menu = {"wifi","wifinoscan","info","param","custom","close","sep","erase","update","restart","exit"};
std::vector<const char*> menu = { "wifi","param","update","sep","info","restart","exit" };
wm.setMenu(menu); // custom menu, pass vector
wm.setConfigPortalBlocking(false);
wm.setBreakAfterConfig(true);
wm.setPreSaveParamsCallback(preSaveParamsCallback);
wm.setSaveParamsCallback(saveParamsCallback);
wm.setSaveConnect(false); // 保存完wifi信息后是否自动连接,设置为否,以便于用户继续配置param。
wm.startConfigPortal("J-Calendar", "password");
led_config(); // LED 进入三快闪状态
// 控制配置超时180秒后休眠
_idle_millis = millis();
}
// 重置系统,并重启
void buttonLongPressStop(void* oneButton) {
Serial.println("Button long press.");
// 删除Preferences,namespace下所有健值对。
Preferences pref;
pref.begin(PREF_NAMESPACE);
pref.clear();
pref.end();
ESP.restart();
}
#define uS_TO_S_FACTOR 1000000
#define TIMEOUT_TO_SLEEP 10 // seconds
time_t blankTime = 0;
void go_sleep() {
// 设置唤醒时间为下个偶数整点。
time_t now = time(NULL);
struct tm tmNow = { 0 };
// Serial.printf("Now: %ld -- %s\n", now, ctime(&now));
localtime_r(&now, &tmNow); // 时间戳转化为本地时间结构
uint64_t p;
// 根据配置情况来刷新,如果未配置qweather信息,则24小时刷新,否则每2小时刷新
Preferences pref;
pref.begin(PREF_NAMESPACE);
String _qweather_key = pref.getString(PREF_QWEATHER_KEY, "");
pref.end();
if(_display_mode !=2){
if (_qweather_key.length() == 0 || weather_type() == 0) { // 没有配置天气或者使用按日天气,则第二天刷新。
Serial.println(F("Sleep to one hours..."));
now += 3600 * 1;
localtime_r(&now, &tmNow); // 将新时间转成tm
// Serial.printf("Set1: %ld -- %s\n", now, ctime(&now));
struct tm tmNew = { 0 };
tmNew.tm_year = tmNow.tm_year;
tmNew.tm_mon = tmNow.tm_mon; // 月份从0开始
tmNew.tm_mday = tmNow.tm_mday; // 日期
tmNew.tm_hour = tmNow.tm_hour; // 小时
tmNew.tm_min = 0; // 分钟
tmNew.tm_sec = 10; // 秒, 防止离线时出现时间误差,所以,延后10s
time_t set = mktime(&tmNew);
p = (uint64_t)(set - time(NULL));
Serial.printf("Sleep time: %ld seconds\n", p);
} else {
if (tmNow.tm_hour % 2 == 0) { // 将时间推后两个小时,偶整点刷新。
now += 7200;
} else {
now += 3600;
}
localtime_r(&now, &tmNow); // 将新时间转成tm
// Serial.printf("Set1: %ld -- %s\n", now, ctime(&now));
struct tm tmNew = { 0 };
tmNew.tm_year = tmNow.tm_year;
tmNew.tm_mon = tmNow.tm_mon; // 月份从0开始
tmNew.tm_mday = tmNow.tm_mday; // 日期
tmNew.tm_hour = tmNow.tm_hour; // 小时
tmNew.tm_min = 0; // 分钟
tmNew.tm_sec = 10; // 秒, 防止离线时出现时间误差,所以,延后10s
time_t set = mktime(&tmNew);
p = (uint64_t)(set - time(NULL));
Serial.printf("Sleep time: %ld seconds\n", p);
}
esp_sleep_enable_timer_wakeup(p * (uint64_t)uS_TO_S_FACTOR);
}
esp_sleep_enable_ext0_wakeup(PIN_BUTTON, 0);
// 省电考虑,关闭RTC外设和存储器
// esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF); // RTC IO, sensors and ULP, 注意:由于需要按键唤醒,所以不能关闭,否则会导致RTC_IO唤醒失败
//esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF); //
//esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
// 省电考虑,重置gpio,平均每针脚能省8ua。
gpio_reset_pin(PIN_LED); // 减小deep-sleep电流
gpio_reset_pin(GPIO_NUM_5); // 减小deep-sleep电流
gpio_reset_pin(GPIO_NUM_17); // 减小deep-sleep电流
gpio_reset_pin(GPIO_NUM_16); // 减小deep-sleep电流
gpio_reset_pin(GPIO_NUM_4); // 减小deep-sleep电流
delay(10);
Serial.println(F("Deep sleep..."));
Serial.flush();
esp_deep_sleep_start();
}
一、整体架构与功能概述
代码库主要实现了一个带网络功能的智能设备(推测为电子日历 / 天气显示器),核心功能包括:
- WiFi 连接与配置管理
- 时间同步(SNTP)
- 天气数据获取(实时、 hourly、daily 预报)
- 农历与节气计算
- 屏幕显示控制
- 按键交互与设备休眠管理
- LED 状态指示
二、核心文件解析
1. main.cpp(主程序入口)
核心功能:统筹设备整体流程,包括初始化、任务调度、交互处理和休眠管理。
-
初始化流程:
- 初始化串口、按键(
OneButton
库)、LED、WiFiManager(网络配置工具)。 - 打印唤醒原因(判断是按键唤醒还是定时唤醒),并更新显示模式(
_display_mode
)。
- 初始化串口、按键(
-
WiFi 管理:
- 使用
WiFiManager
自动连接 WiFi,失败则进入低功耗模式。 - 配置页面支持用户设置和风天气 API 密钥、位置 ID、显示模式等参数(通过
WiFiManagerParameter
定义配置项)。
- 使用
-
任务调度:
- 主循环(
loop
)中按条件执行子任务:- SNTP 时间同步(
_sntp_exec
)。 - 天气数据获取(
weather_exec
)。 - 屏幕刷新(
si_screen
),根据_display_mode
切换显示内容。
- SNTP 时间同步(
- 主循环(
-
交互处理:
- 按键事件:单击刷新屏幕、双击进入配置页面、长按重启设备。
- 配置参数保存后自动重启生效(
saveParamsCallback
)。
-
休眠管理:
- 所有任务完成或配置超时后,调用
go_sleep
进入深度睡眠,节省功耗。
- 所有任务完成或配置超时后,调用
2. weather.cpp(天气数据模块)
核心功能:通过和风天气 API 获取天气数据,并管理数据状态。
-
数据结构:
Weather
:实时天气数据(温度、湿度、风力、图标等)。DailyWeather
:每日预报(最高 / 低温、日出日落、昼夜天气等)。HourlyForecast
/DailyForecast
:封装小时 / 每日预报的数组及元信息(长度、间隔)。Hitokoto
:一言(随机句子)数据。
-
任务与状态:
- 独立 FreeRTOS 任务(
task_weather
)负责网络请求,避免阻塞主程序。 - 状态变量(
_weather_status
、_hourly_weather_status
等)标识数据获取结果(-1:未获取,1:成功,2:失败)。
- 独立 FreeRTOS 任务(
-
API 调用逻辑:
- 从偏好设置(
Preferences
)读取和风天气的主机、密钥、位置 ID。 - 调用
API
类的方法(getWeatherNow
、getForecastHourly
等)获取数据,支持实时 / 每日模式切换(_weather_type
控制)。
- 从偏好设置(
3. API.hpp(网络请求模块)
核心功能:封装 HTTP 请求逻辑,解析和风天气 API 响应,支持 gzip 解压和重试机制。
-
模板类
API
:- 模板参数
MAX_RETRY
控制请求重试次数(默认 3 次)。 - 私有方法
getRestfulAPI
:通用 HTTP 请求框架,处理 URL 请求、gzip 解压、JSON 解析,并通过回调函数处理业务逻辑。
- 模板参数
-
天气 API 实现:
getWeatherNow
:获取实时天气,解析/v7/weather/now
接口响应。getForecastHourly
:获取 24 小时逐小时预报,解析/v7/weather/24h
接口。getForecastDaily
:获取 3 天每日预报,解析/v7/weather/3d
接口。
-
数据解析:
- 使用
ArduinoJson
库解析 JSON 响应,将数据映射到Weather
、DailyWeather
等结构体。
- 使用
4. nongli.cpp(农历与节气模块)
核心功能:提供农历日期、节气、天干地支及生肖的计算。
-
农历计算:
- 基于内置的
_LunarCalDic
字典(1901-2099 年数据),通过nl_month_days
计算指定公历年月的每日农历(返回值格式:高两位为农历月,低两位为农历日,负数表示闰月)。
- 基于内置的
-
节气计算:
nl_year_jq
计算指定年份的 24 节气日期(返回一年中第几天),基于天文公式近似计算。
-
天干地支与生肖:
nl_tg
/nl_dz
分别返回年份对应的天干 / 地支索引,nl_sx_text
通过地支索引映射生肖。
5. led.cpp(LED 指示灯模块)
核心功能:控制 LED 的闪烁模式,用于指示设备状态(连接中、配置中、正常运行等)。
-
闪烁模式:
- 定义 5 种模式(
BLINK_TYPE
):常灭(0)、常亮(1)、慢闪(2)、快闪(3)、配置模式(4,三次快闪 + 暂停)。
- 定义 5 种模式(
-
任务管理:
- 独立 FreeRTOS 任务(
task_led
)循环执行当前模式的闪烁逻辑,通过led_fast
/led_slow
等函数切换模式(删除旧任务并创建新任务)。
- 独立 FreeRTOS 任务(
6. web_server.txt(Web 配置示例)
功能说明 :提供一个简易 Web 服务器配置页面,支持用户设置 WiFi、时区等参数(实际项目中可能被main.cpp
的WiFiManager
替代,作为备用配置方式)。
三、模块间交互关系
- 主程序与天气模块 :
main.cpp
调用weather_exec
启动天气任务,通过weather_status
等接口判断数据状态,再调用si_screen
刷新屏幕。 - 天气模块与 API 模块 :
weather.cpp
的task_weather
使用API
类的方法发起网络请求,获取并解析天气数据。 - 主程序与配置模块 :
main.cpp
通过WiFiManager
创建配置页面,用户输入的参数通过saveParamsCallback
保存到Preferences
,供天气模块等读取。 - 主程序与 LED 模块 :
main.cpp
在 WiFi 连接成功 / 失败时调用led_on
/led_slow
等函数,通过 LED 状态反馈设备状态。
四、关键技术点
- 多任务管理 :使用 FreeRTOS 的
xTaskCreate
/vTaskDelete
创建和销毁任务(天气任务、LED 任务),避免单线程阻塞。 - 低功耗设计 :完成任务后关闭 WiFi,通过
esp_sleep
进入深度睡眠,仅通过按键或定时器唤醒。 - 配置持久化 :使用
Preferences
库保存用户配置(天气 API 密钥、位置等),掉电不丢失。 - 网络容错:API 请求带重试机制,处理网络波动导致的失败。
通过以上模块的协同,设备实现了 "联网获取数据 - 处理显示 - 低功耗休眠" 的完整流程,适合作为离线优先的智能显示设备。
OneButton button;
oneButton 是一个类,此类中包括按键的内容。
RTC_DATA_ATTR volatile int _display_mode =0;
#define RTC_DATA_ATTR _SECTION_ATTR_IMPL(".rtc.data", COUNTER)
main.cpp这个宏定义 #define RTC_DATA_ATTR _SECTION_ATTR_IMPL(".rtc.data", __COUNTER__)
是 ESP32 开发中用于标记变量存储位置的特殊属性定义,主要用于深度睡眠(Deep Sleep)时保留变量值。
各部分含义解析:
-
RTC_DATA_ATTR
:这是用户层面使用的宏,用于修饰变量,表明该变量需要存储在 RTC 数据区域(RTC 即实时时钟,其供电独立于主 CPU,深度睡眠时仍保持供电)。 -
_SECTION_ATTR_IMPL
:这是 ESP-IDF(ESP32 官方开发框架)内部的辅助宏,用于将变量 "放置" 到指定的内存段(section)中。其作用是通过编译器属性(__attribute__
)指定变量的存储区域。 -
".rtc.data"
:这是 ESP32 链接脚本(linker script)中预定义的内存段名称,专门用于存储需要在深度睡眠中保留的数据。- 普通变量在深度睡眠时会因主电源关闭而丢失值,而
".rtc.data"
段的变量由 RTC 电源供电,值会被保留。
- 普通变量在深度睡眠时会因主电源关闭而丢失值,而
-
__COUNTER__
:这是 GCC 预处理器的内置宏,每次使用时会自动递增(从 0 开始),用于生成唯一的标识符。- 作用:避免多个变量在同一内存段中因命名冲突导致的编译或运行时错误。
实际用途:
在 ESP32 深度睡眠场景中,使用 RTC_DATA_ATTR
修饰的变量可以在设备唤醒后保留其值,无需重新初始化。
例如,在你提供的 main.cpp
中:
RTC_DATA_ATTR volatile int _display_mode = 0;
_display_mode
用于记录设备的显示模式,由于被 RTC_DATA_ATTR
修饰,即使设备进入深度睡眠,其值也不会丢失,唤醒后可直接使用之前的模式状态。
总结:
RTC_DATA_ATTR
是 ESP32 中用于标记 "深度睡眠保留变量" 的宏,通过将变量存储在 RTC 供电的内存段(.rtc.data
)中,确保其值在深度睡眠后不丢失,常用于保存设备状态、计数器等需要跨睡眠周期保留的信息。
void IRAM_ATTR checkTicks() {
button.tick();
}
这段代码是 ESP32 中用于按键中断服务程序(ISR) 的函数,主要功能是在按键状态变化时快速处理按键事件,结合 OneButton
库实现单击、双击、长按等复杂按键逻辑的检测。
各部分解析:
-
IRAM_ATTR
修饰符- 这是 ESP-IDF 提供的宏,用于将函数存储在 IRAM(指令 RAM) 中,而非默认的 Flash 存储器。
- 原因:中断服务程序(ISR)需要极快的响应速度,IRAM 的访问速度远高于 Flash,且避免了 Flash 可能处于忙碌状态(如写入时)导致的读取阻塞。
- 必须用于中断中调用的函数,否则可能导致程序崩溃。
-
函数功能:
checkTicks()
- 函数体内仅调用
button.tick()
,其中button
是OneButton
库的实例(在代码中定义为OneButton button;
)。 OneButton
库是一个轻量级按键处理库,tick()
方法是其核心函数,用于实时更新按键状态(检测电平变化、计算按下 / 释放时间等),为后续识别单击、双击、长按等事件提供原始数据。
- 函数体内仅调用
-
与中断的关联 在
setup()
中,该函数被注册为按键引脚的中断服务程序:attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), checkTicks, CHANGE);
- 当按键引脚(
PIN_BUTTON = GPIO_NUM_14
)的电平发生上升沿或下降沿变化 (CHANGE
触发条件)时,ESP32 会立即暂停当前任务,跳转到checkTicks()
执行。 - 通过中断触发
button.tick()
,确保按键状态的变化能被实时捕捉,避免因主循环延迟导致的按键事件漏检。
- 当按键引脚(
设计意义:
- 高效响应:利用中断机制,按键状态变化能被立即处理,解决了主循环中轮询可能导致的响应延迟问题。
- 兼容性 :
OneButton
库的tick()
方法设计为轻量级操作,适合在中断中调用(ISR 需尽量简短,避免阻塞系统)。 - 低功耗适配 :结合代码中深度睡眠的逻辑(按键可唤醒设备),中断触发的
tick()
能在设备唤醒后快速恢复按键状态检测。
简言之,这段代码是按键事件处理的 "前端接收器",通过中断 + IRAM 加速,确保按键操作能被快速、准确地识别,为后续的单击刷新、双击配置等功能提供基础。
WiFiManager wm;
WiFiManagerParameter para_qweather_host("qweather_host", "和风天气Host", "", 64); // 和风天气key
WiFiManagerParameter para_qweather_key("qweather_key", "和风天气API Key", "", 32); // 和风天气key
// const char* test_html = "<br/><label for='test'>天气模式</label><br/><input type='radio' name='test' value='0' checked> 每日天气test </input><input type='radio' name='test' value='1'> 实时天气test</input>";
// WiFiManagerParameter para_test(test_html);
WiFiManagerParameter para_qweather_type("qweather_type", "天气类型(0:每日天气,1:实时天气)", "0", 2, "pattern='\\[0-1]{1}'"); // 城市code
WiFiManagerParameter para_qweather_location("qweather_loc", "位置ID", "", 64); // 城市code
WiFiManagerParameter para_cd_day_label("cd_day_label", "倒数日(4字以内)", "", 10); // 倒数日
WiFiManagerParameter para_cd_day_date("cd_day_date", "日期(yyyyMMdd)", "", 8, "pattern='\\d{8}'"); // 城市code
WiFiManagerParameter para_tag_days("tag_days", "日期Tag(yyyyMMddx,详见README)", "", 30); // 日期Tag
WiFiManagerParameter para_si_week_1st("si_week_1st", "每周起始(0:周日,1:周一)", "0", 2, "pattern='\\[0-1]{1}'"); // 每周第一天
这段代码是基于 WiFiManager
库定义的设备配置参数项,用于在设备的 Web 配置页面中提供用户可输入的配置项(如 API 密钥、位置信息等),并将这些参数持久化存储以控制设备功能。以下是详细解析:
核心组件说明
-
WiFiManager wm
实例化WiFiManager
对象,用于管理 WiFi 连接和 Web 配置页面(提供一个临时 AP 和网页界面,让用户配置参数)。 -
WiFiManagerParameter
配置项 每个WiFiManagerParameter
对象对应 Web 配置页面上的一个输入项,包含参数名、显示名称、默认值、长度限制、验证规则等信息。用户在网页中输入的值会被这些对象捕获,之后保存到设备存储中。
各配置项详解
变量名 | 参数名(name ) |
显示名称(网页上的标签) | 作用与细节 |
---|---|---|---|
para_qweather_host |
qweather_host |
和风天气 Host | 存储和风天气 API 的主机地址(如默认的 api.qweather.com ),允许用户自定义,最大长度 64 字符。 |
para_qweather_key |
qweather_key |
和风天气 API Key | 存储访问和风天气 API 的密钥(用户需从和风天气开发者平台申请),最大长度 32 字符。 |
para_qweather_type |
qweather_type |
天气类型(0: 每日天气,1: 实时天气) | 控制设备显示的天气类型:0 为每日预报,1 为实时天气;默认值 0,通过正则 pattern='\\[0-1]{1}' 限制输入只能是 0 或 1。 |
para_qweather_location |
qweather_loc |
位置 ID | 存储和风天气 API 所需的地理位置 ID(如城市 ID),用于指定获取哪个地区的天气,最大长度 64 字符。 |
para_cd_day_label |
cd_day_label |
倒数日(4 字以内) | 存储倒数日的标签(如 "高考""生日"),限制 4 字以内(最大长度 10 字符,预留扩展空间)。 |
para_cd_day_date |
cd_day_date |
日期(yyyyMMdd) | 存储倒数日的目标日期,格式为 yyyyMMdd (如 20240607),通过正则 pattern='\\d{8}' 限制为 8 位数字。 |
para_tag_days |
tag_days |
日期 Tag(yyyyMMddx,详见 README) | 存储需要标记的特殊日期(如节日、纪念日),格式为 yyyyMMddx (x 为标记符号),最大长度 30 字符。 |
para_si_week_1st |
si_week_1st |
每周起始(0: 周日,1: 周一) | 控制日历中每周的起始日:0 为周日,1 为周一;默认值 0,正则限制输入只能是 0 或 1。 |
配置项的使用流程
-
网页展示 :当用户通过双击按键进入配置模式时(
buttonDoubleClick
函数),这些参数会被添加到WiFiManager
的配置页面(wm.addParameter(...)
),用户可在网页中输入或修改值。 -
参数保存 :用户提交配置后,
saveParamsCallback
函数会被调用,将各参数的值通过Preferences
库保存到设备的非易失性存储中(如pref.putString(PREF_QWEATHER_KEY, para_qweather_key.getValue())
)。 -
功能生效:其他模块(如天气模块、屏幕显示模块)会从存储中读取这些参数,控制具体功能:
- 天气模块(
weather.cpp
)使用qweather_host
、qweather_key
、qweather_loc
调用 API 获取天气数据; - 屏幕显示模块(
screen_ink.cpp
)使用si_week_1st
调整日历的周起始日显示; - 倒数日和日期 Tag 会在屏幕上标注对应的特殊日期。
- 天气模块(
总结
这段代码通过 WiFiManager
定义了设备的核心配置项,实现了用户友好的参数配置界面,并为设备的天气获取、日历显示等功能提供了可自定义的参数来源。这些配置项通过持久化存储确保设备重启或休眠后仍能保持用户设置,是设备灵活性和可定制性的关键实现。