完成初代的雏形设计

This commit is contained in:
Wang Beihong
2026-03-11 20:14:14 +08:00
commit 2f56316c18
63 changed files with 10594 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "console_user_cmds.c"
INCLUDE_DIRS "include"
REQUIRES console_simple_init console i2c_master_messager io_device_control wifi-connect)

View File

@@ -0,0 +1,293 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_check.h"
#include "console_simple_init.h"
#include "console_user_cmds.h"
#include "i2c_master_messager.h"
#include "io_device_control.h"
#include "wifi-connect.h"
static const char *wifi_status_to_str(wifi_connect_status_t status)
{
switch (status) {
case WIFI_CONNECT_STATUS_IDLE:
return "idle";
case WIFI_CONNECT_STATUS_PROVISIONING:
return "provisioning";
case WIFI_CONNECT_STATUS_CONNECTING:
return "connecting";
case WIFI_CONNECT_STATUS_CONNECTED:
return "connected";
case WIFI_CONNECT_STATUS_FAILED:
return "failed";
case WIFI_CONNECT_STATUS_TIMEOUT:
return "timeout";
default:
return "unknown";
}
}
// hello: 最小可用命令,用于验证 console 链路是否正常。
static int cmd_hello(int argc, char **argv)
{
(void)argc;
(void)argv;
printf("hello from BotanicalBuddy\n");
return 0;
}
// sensor: 读取一次传感器缓存数据并打印,便于快速排查现场状态。
static int cmd_sensor(int argc, char **argv)
{
(void)argc;
(void)argv;
i2c_master_messager_data_t data = {0};
esp_err_t ret = i2c_master_messager_get_data(&data);
if (ret != ESP_OK) {
printf("sensor read failed: %s\n", esp_err_to_name(ret));
return 1;
}
if (data.bh1750.valid) {
printf("BH1750: lux=%.1f, ts=%lld ms\n",
data.bh1750.lux,
(long long)data.bh1750.last_update_ms);
} else {
printf("BH1750: invalid, err=%s\n", esp_err_to_name(data.bh1750.last_error));
}
if (data.aht30.valid) {
printf("AHT30: temp=%.1f C, hum=%.1f %%, ts=%lld ms\n",
data.aht30.temperature_c,
data.aht30.humidity_rh,
(long long)data.aht30.last_update_ms);
} else {
printf("AHT30: invalid, err=%s\n", esp_err_to_name(data.aht30.last_error));
}
return 0;
}
static bool parse_on_off_arg(const char *arg, bool *on)
{
if (strcmp(arg, "on") == 0 || strcmp(arg, "1") == 0) {
*on = true;
return true;
}
if (strcmp(arg, "off") == 0 || strcmp(arg, "0") == 0) {
*on = false;
return true;
}
return false;
}
// fan: 控制风扇开关,参数支持 on/off 或 1/0。
static int cmd_fan(int argc, char **argv)
{
if (argc < 2) {
printf("usage: fan <on|off>\n");
return 1;
}
bool on = false;
if (!parse_on_off_arg(argv[1], &on)) {
printf("invalid arg: %s\n", argv[1]);
printf("usage: fan <on|off>\n");
return 1;
}
esp_err_t ret = io_device_control_set_fan(on);
if (ret != ESP_OK) {
printf("set fan failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("fan: %s\n", on ? "on" : "off");
return 0;
}
// light: 控制补光灯开关,参数支持 on/off 或 1/0。
static int cmd_light(int argc, char **argv)
{
if (argc < 2) {
printf("usage: light <on|off>\n");
return 1;
}
bool on = false;
if (!parse_on_off_arg(argv[1], &on)) {
printf("invalid arg: %s\n", argv[1]);
printf("usage: light <on|off>\n");
return 1;
}
esp_err_t ret = io_device_control_set_light(on);
if (ret != ESP_OK) {
printf("set light failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("light: %s\n", on ? "on" : "off");
return 0;
}
// hot: 控制加热开关,参数支持 on/off 或 1/0。
static int cmd_hot(int argc, char **argv)
{
if (argc < 2) {
printf("usage: hot <on|off>\n");
return 1;
}
bool on = false;
if (!parse_on_off_arg(argv[1], &on)) {
printf("invalid arg: %s\n", argv[1]);
printf("usage: hot <on|off>\n");
return 1;
}
esp_err_t ret = io_device_control_set_hot(on);
if (ret != ESP_OK) {
printf("set hot failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("hot: %s\n", on ? "on" : "off");
return 0;
}
// cool: 控制制冷开关,参数支持 on/off 或 1/0。
static int cmd_cool(int argc, char **argv)
{
if (argc < 2) {
printf("usage: cool <on|off>\n");
return 1;
}
bool on = false;
if (!parse_on_off_arg(argv[1], &on)) {
printf("invalid arg: %s\n", argv[1]);
printf("usage: cool <on|off>\n");
return 1;
}
esp_err_t ret = io_device_control_set_cool(on);
if (ret != ESP_OK) {
printf("set cool failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("cool: %s\n", on ? "on" : "off");
return 0;
}
// wifi: 查询或控制配网状态,支持 status/start/stop/clear 子命令。
static int cmd_wifi(int argc, char **argv)
{
if (argc < 2 || strcmp(argv[1], "status") == 0) {
wifi_connect_config_t cfg = {0};
esp_err_t cfg_ret = wifi_connect_get_config(&cfg);
printf("wifi status: %s\n", wifi_status_to_str(wifi_connect_get_status()));
if (cfg_ret == ESP_OK && cfg.has_config) {
printf("saved ssid: %s\n", cfg.ssid);
} else {
printf("saved config: none\n");
}
return 0;
}
if (strcmp(argv[1], "start") == 0) {
esp_err_t ret = wifi_connect_start();
if (ret != ESP_OK) {
printf("wifi start failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("wifi start requested\n");
return 0;
}
if (strcmp(argv[1], "stop") == 0) {
esp_err_t ret = wifi_connect_stop();
if (ret != ESP_OK) {
printf("wifi stop failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("wifi stop requested\n");
return 0;
}
if (strcmp(argv[1], "clear") == 0) {
esp_err_t ret = wifi_connect_clear_config();
if (ret != ESP_OK) {
printf("wifi clear failed: %s\n", esp_err_to_name(ret));
return 1;
}
printf("wifi config cleared\n");
return 0;
}
printf("usage: wifi <status|start|stop|clear>\n");
return 1;
}
esp_err_t console_user_cmds_register(void)
{
const esp_console_cmd_t hello_cmd = {
.command = "hello",
.help = "打印欢迎信息。用法: hello",
.func = cmd_hello,
};
ESP_RETURN_ON_ERROR(esp_console_cmd_register(&hello_cmd), "console_user_cmds", "register hello failed");
const esp_console_cmd_t sensor_cmd = {
.command = "sensor",
.help = "打印当前传感器缓存数据。用法: sensor",
.func = cmd_sensor,
};
ESP_RETURN_ON_ERROR(esp_console_cmd_register(&sensor_cmd), "console_user_cmds", "register sensor failed");
const esp_console_cmd_t fan_cmd = {
.command = "fan",
.help = "控制风扇。用法: fan <on|off>",
.hint = "<on|off>",
.func = cmd_fan,
};
ESP_RETURN_ON_ERROR(esp_console_cmd_register(&fan_cmd), "console_user_cmds", "register fan failed");
const esp_console_cmd_t light_cmd = {
.command = "light",
.help = "控制补光灯。用法: light <on|off>",
.hint = "<on|off>",
.func = cmd_light,
};
ESP_RETURN_ON_ERROR(esp_console_cmd_register(&light_cmd), "console_user_cmds", "register light failed");
const esp_console_cmd_t hot_cmd = {
.command = "hot",
.help = "控制加热。用法: hot <on|off>",
.hint = "<on|off>",
.func = cmd_hot,
};
ESP_RETURN_ON_ERROR(esp_console_cmd_register(&hot_cmd), "console_user_cmds", "register hot failed");
const esp_console_cmd_t cool_cmd = {
.command = "cool",
.help = "控制制冷。用法: cool <on|off>",
.hint = "<on|off>",
.func = cmd_cool,
};
ESP_RETURN_ON_ERROR(esp_console_cmd_register(&cool_cmd), "console_user_cmds", "register cool failed");
const esp_console_cmd_t wifi_cmd = {
.command = "wifi",
.help = "Wi-Fi 状态与控制。用法: wifi <status|start|stop|clear>",
.hint = "<status|start|stop|clear>",
.func = cmd_wifi,
};
ESP_RETURN_ON_ERROR(esp_console_cmd_register(&wifi_cmd), "console_user_cmds", "register wifi failed");
return ESP_OK;
}

View File

@@ -0,0 +1,5 @@
#pragma once
#include "esp_err.h"
esp_err_t console_user_cmds_register(void);

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "i2c_master_messager.c"
INCLUDE_DIRS "include"
REQUIRES bh1750 k0i05__esp_ahtxx esp_driver_i2c esp_driver_gpio esp_timer
)

View File

@@ -0,0 +1,38 @@
menu "I2C 传感器管理"
config I2C_MASTER_MESSAGER_ENABLE_INTERNAL_PULLUP
bool "启用 I2C 内部上拉电阻"
default y
help
启用后SCL/SDA 会使用芯片内部上拉。
如果你的硬件已经有外部上拉电阻,通常也可以关闭该选项。
config I2C_MASTER_MESSAGER_BH1750_ENABLE
bool "启用 BH1750 光照传感器"
default y
help
关闭后将不会初始化与读取 BH1750。
config I2C_MASTER_MESSAGER_BH1750_READ_PERIOD_MS
int "BH1750 采样周期 (ms)"
range 100 10000
default 500
depends on I2C_MASTER_MESSAGER_BH1750_ENABLE
help
BH1750 的轮询间隔,单位毫秒。
config I2C_MASTER_MESSAGER_AHT30_ENABLE
bool "启用 AHT30 温湿度传感器"
default y
help
关闭后将不会初始化与读取 AHT30。
config I2C_MASTER_MESSAGER_AHT30_READ_PERIOD_MS
int "AHT30 采样周期 (ms)"
range 100 10000
default 2000
depends on I2C_MASTER_MESSAGER_AHT30_ENABLE
help
AHT30 的轮询间隔,单位毫秒。
endmenu

View File

@@ -0,0 +1,109 @@
# i2c_master_messager
`i2c_master_messager` 用于统一管理工程中的 I2C 传感器。
当前已接入:
- BH1750 光照传感器(使用驱动默认地址)
- AHT30 温湿度传感器(使用驱动默认地址)
设计目标:
- 提供统一的数据结构,方便其他模块读取
- 提供独立采集任务,周期性更新数据
- 为后续新增其他 I2C 传感器预留扩展位置
- 各传感器驱动相互独立,管理层只做调度与数据汇总
- 支持每个传感器独立采样周期(例如光照快采、温湿度慢采)
## 驱动分层
- `k0i05__esp_ahtxx`AHT30 驱动组件(通过组件管理器引入)
- `bh1750`:使用 ESP 组件管理器驱动
- `i2c_master_messager.c`:统一总线管理、任务轮询、线程安全数据缓存
## 对外数据结构
头文件:`include/i2c_master_messager.h`
- `i2c_master_messager_data_t`
- `bh1750.lux`光照强度lx
- `bh1750.valid`:当前数据是否有效
- `bh1750.last_update_ms`:最后一次成功更新时间(毫秒)
- `bh1750.last_error`:最后一次采集错误码
- `aht30.temperature_c`:温度(摄氏度)
- `aht30.humidity_rh`:湿度(%RH
- `aht30.valid`:当前数据是否有效
- `aht30.last_update_ms`:最后一次成功更新时间(毫秒)
- `aht30.last_error`:最后一次采集错误码
后续新增传感器时,建议继续在 `i2c_master_messager_data_t` 中增加对应字段。
## API
- `esp_err_t i2c_master_messager_init(const i2c_master_messager_config_t *config);`
- 初始化 I2C 总线(传感器驱动在任务中懒初始化)
- `esp_err_t i2c_master_messager_start(void);`
- 启动循环采集任务
- `esp_err_t i2c_master_messager_stop(void);`
- 停止采集任务
- `esp_err_t i2c_master_messager_get_data(i2c_master_messager_data_t *out_data);`
- 读取当前缓存数据(线程安全)
- `esp_err_t i2c_master_messager_deinit(void);`
- 释放总线和传感器资源
## menuconfig 配置
路径:`Component config -> I2C Master Messager`
- `启用 BH1750 光照传感器`:开关 BH1750
- `启用 I2C 内部上拉电阻`:控制是否打开芯片内部上拉
- `BH1750 采样周期 (ms)`:光照采样周期
- `启用 AHT30 温湿度传感器`:开关 AHT30
- `AHT30 采样周期 (ms)`:温湿度采样周期
这两个周期相互独立,可按业务需求分别调优。
## 使用示例
```c
#include "esp_check.h"
#include "i2c_master_messager.h"
void app_main(void)
{
i2c_master_messager_config_t cfg = {
.i2c_port = I2C_NUM_0,
.scl_io_num = GPIO_NUM_5,
.sda_io_num = GPIO_NUM_4,
.i2c_enable_internal_pullup = true,
.bh1750_enable = true,
.aht30_enable = true,
.bh1750_read_period_ms = 500,
.aht30_read_period_ms = 2000,
.bh1750_mode = BH1750_CONTINUE_1LX_RES,
};
ESP_ERROR_CHECK(i2c_master_messager_init(&cfg));
ESP_ERROR_CHECK(i2c_master_messager_start());
while (1) {
i2c_master_messager_data_t data;
if (i2c_master_messager_get_data(&data) == ESP_OK && data.bh1750.valid && data.aht30.valid) {
printf("BH1750: %.2f lx, AHT30: %.2f C %.2f %%RH\n",
data.bh1750.lux,
data.aht30.temperature_c,
data.aht30.humidity_rh);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
```
## 扩展建议
新增其他传感器时,建议按以下步骤扩展:
1.`i2c_master_messager_data_t` 中增加该传感器的数据结构
2.`i2c_master_messager_config_t` 中增加该传感器配置项
3. 在任务循环中按需完成驱动初始化与重试
4. 在采集任务中加入周期读取逻辑并更新共享数据
5.`deinit()` 中释放对应资源

View File

@@ -0,0 +1,356 @@
#include <string.h>
#include "driver/i2c_master.h"
#include "ahtxx.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "i2c_master_messager.h"
static const char *TAG = "i2c_master_messager";
#define BH1750_REINIT_INTERVAL_MS (3000)
#define AHT30_REINIT_INTERVAL_MS (3000)
typedef struct {
bool initialized;
bool owns_i2c_bus;
bool bh1750_ready;
bool aht30_ready;
i2c_master_messager_config_t config;
i2c_master_bus_handle_t i2c_bus;
bh1750_handle_t bh1750;
ahtxx_handle_t aht30;
i2c_master_messager_data_t data;
SemaphoreHandle_t lock;
TaskHandle_t task_handle;
} i2c_master_messager_ctx_t;
static i2c_master_messager_ctx_t g_ctx;
static esp_err_t i2c_master_messager_try_init_bh1750(void)
{
if (!g_ctx.config.bh1750_enable) {
return ESP_ERR_NOT_SUPPORTED;
}
if (g_ctx.bh1750 != NULL) {
bh1750_delete(g_ctx.bh1750);
g_ctx.bh1750 = NULL;
}
esp_err_t ret = bh1750_create(g_ctx.i2c_bus, BH1750_I2C_ADDRESS_DEFAULT, &g_ctx.bh1750);
if (ret != ESP_OK) {
return ret;
}
ret = bh1750_power_on(g_ctx.bh1750);
if (ret != ESP_OK) {
bh1750_delete(g_ctx.bh1750);
g_ctx.bh1750 = NULL;
return ret;
}
ret = bh1750_set_measure_mode(g_ctx.bh1750, g_ctx.config.bh1750_mode);
if (ret != ESP_OK) {
bh1750_delete(g_ctx.bh1750);
g_ctx.bh1750 = NULL;
return ret;
}
g_ctx.bh1750_ready = true;
return ESP_OK;
}
static esp_err_t i2c_master_messager_try_init_aht30(void)
{
if (!g_ctx.config.aht30_enable) {
return ESP_ERR_NOT_SUPPORTED;
}
if (g_ctx.aht30 != NULL) {
ahtxx_delete(g_ctx.aht30);
g_ctx.aht30 = NULL;
}
ahtxx_config_t aht_cfg = I2C_AHT30_CONFIG_DEFAULT;
esp_err_t ret = ahtxx_init(g_ctx.i2c_bus, &aht_cfg, &g_ctx.aht30);
if (ret != ESP_OK) {
return ret;
}
g_ctx.aht30_ready = true;
return ESP_OK;
}
static void i2c_master_messager_task(void *arg)
{
(void)arg;
int64_t last_bh1750_reinit_ms = 0;
int64_t last_bh1750_read_ms = 0;
int64_t last_aht30_read_ms = 0;
int64_t last_aht30_reinit_ms = 0;
while (1) {
int64_t now_ms = esp_timer_get_time() / 1000;
if (g_ctx.config.bh1750_enable && !g_ctx.bh1750_ready &&
(now_ms - last_bh1750_reinit_ms) >= BH1750_REINIT_INTERVAL_MS) {
last_bh1750_reinit_ms = now_ms;
esp_err_t init_ret = i2c_master_messager_try_init_bh1750();
if (init_ret == ESP_OK) {
ESP_LOGI(TAG, "BH1750 reinit success");
} else {
ESP_LOGW(TAG, "BH1750 reinit failed: %s", esp_err_to_name(init_ret));
if (xSemaphoreTake(g_ctx.lock, pdMS_TO_TICKS(100)) == pdTRUE) {
g_ctx.data.bh1750.valid = false;
g_ctx.data.bh1750.last_error = init_ret;
xSemaphoreGive(g_ctx.lock);
}
}
}
if (g_ctx.bh1750_ready && g_ctx.bh1750 != NULL &&
(now_ms - last_bh1750_read_ms) >= g_ctx.config.bh1750_read_period_ms) {
float lux = 0.0f;
esp_err_t bh1750_ret = bh1750_get_data(g_ctx.bh1750, &lux);
last_bh1750_read_ms = now_ms;
if (xSemaphoreTake(g_ctx.lock, pdMS_TO_TICKS(100)) == pdTRUE) {
g_ctx.data.bh1750.valid = (bh1750_ret == ESP_OK);
g_ctx.data.bh1750.last_error = bh1750_ret;
if (bh1750_ret == ESP_OK) {
g_ctx.data.bh1750.lux = lux;
g_ctx.data.bh1750.last_update_ms = now_ms;
}
xSemaphoreGive(g_ctx.lock);
}
if (bh1750_ret != ESP_OK) {
ESP_LOGW(TAG, "bh1750_get_data failed: %s", esp_err_to_name(bh1750_ret));
}
}
if (g_ctx.config.aht30_enable && !g_ctx.aht30_ready &&
(now_ms - last_aht30_reinit_ms) >= AHT30_REINIT_INTERVAL_MS) {
last_aht30_reinit_ms = now_ms;
esp_err_t init_ret = i2c_master_messager_try_init_aht30();
if (init_ret == ESP_OK) {
ESP_LOGI(TAG, "AHT30 reinit success");
} else {
ESP_LOGW(TAG, "AHT30 reinit failed: %s", esp_err_to_name(init_ret));
if (xSemaphoreTake(g_ctx.lock, pdMS_TO_TICKS(100)) == pdTRUE) {
g_ctx.data.aht30.valid = false;
g_ctx.data.aht30.last_error = init_ret;
xSemaphoreGive(g_ctx.lock);
}
}
}
if (g_ctx.aht30_ready && g_ctx.aht30 != NULL &&
(now_ms - last_aht30_read_ms) >= g_ctx.config.aht30_read_period_ms) {
float temperature_c = 0.0f;
float humidity_rh = 0.0f;
esp_err_t aht30_ret = ahtxx_get_measurement(g_ctx.aht30, &temperature_c, &humidity_rh);
last_aht30_read_ms = now_ms;
if (xSemaphoreTake(g_ctx.lock, pdMS_TO_TICKS(100)) == pdTRUE) {
g_ctx.data.aht30.valid = (aht30_ret == ESP_OK);
g_ctx.data.aht30.last_error = aht30_ret;
if (aht30_ret == ESP_OK) {
g_ctx.data.aht30.temperature_c = temperature_c;
g_ctx.data.aht30.humidity_rh = humidity_rh;
g_ctx.data.aht30.last_update_ms = now_ms;
}
xSemaphoreGive(g_ctx.lock);
}
if (aht30_ret != ESP_OK) {
ESP_LOGW(TAG, "ahtxx_get_measurement failed: %s", esp_err_to_name(aht30_ret));
if (aht30_ret == ESP_ERR_INVALID_STATE || aht30_ret == ESP_ERR_TIMEOUT) {
if (g_ctx.aht30 != NULL) {
ahtxx_delete(g_ctx.aht30);
g_ctx.aht30 = NULL;
}
g_ctx.aht30_ready = false;
}
}
}
vTaskDelay(pdMS_TO_TICKS(I2C_MASTER_MESSAGER_MIN_PERIOD_MS));
}
}
esp_err_t i2c_master_messager_init(const i2c_master_messager_config_t *config)
{
ESP_RETURN_ON_FALSE(config != NULL, ESP_ERR_INVALID_ARG, TAG, "config is null");
ESP_RETURN_ON_FALSE(config->bh1750_enable || config->aht30_enable,
ESP_ERR_INVALID_ARG,
TAG,
"at least one sensor must be enabled");
ESP_RETURN_ON_FALSE(!config->bh1750_enable ||
config->bh1750_read_period_ms >= I2C_MASTER_MESSAGER_MIN_PERIOD_MS,
ESP_ERR_INVALID_ARG,
TAG,
"bh1750_read_period_ms too small");
ESP_RETURN_ON_FALSE(!config->aht30_enable ||
config->aht30_read_period_ms >= I2C_MASTER_MESSAGER_MIN_PERIOD_MS,
ESP_ERR_INVALID_ARG,
TAG,
"aht30_read_period_ms too small");
if (g_ctx.initialized) {
return ESP_ERR_INVALID_STATE;
}
memset(&g_ctx, 0, sizeof(g_ctx));
g_ctx.config = *config;
g_ctx.lock = xSemaphoreCreateMutex();
ESP_RETURN_ON_FALSE(g_ctx.lock != NULL, ESP_ERR_NO_MEM, TAG, "failed to create mutex");
const i2c_master_bus_config_t bus_cfg = {
.i2c_port = config->i2c_port,
.sda_io_num = config->sda_io_num,
.scl_io_num = config->scl_io_num,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = config->i2c_enable_internal_pullup,
};
esp_err_t ret = i2c_new_master_bus(&bus_cfg, &g_ctx.i2c_bus);
if (ret != ESP_OK) {
if (ret == ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG,
"i2c port %d already initialized, trying to reuse existing master bus",
config->i2c_port);
ret = i2c_master_get_bus_handle(config->i2c_port, &g_ctx.i2c_bus);
if (ret != ESP_OK || g_ctx.i2c_bus == NULL) {
ESP_LOGE(TAG,
"failed to reuse i2c bus on port %d: %s",
config->i2c_port,
esp_err_to_name(ret));
vSemaphoreDelete(g_ctx.lock);
g_ctx.lock = NULL;
return (ret == ESP_OK) ? ESP_ERR_INVALID_STATE : ret;
}
g_ctx.owns_i2c_bus = false;
} else {
ESP_LOGE(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(ret));
vSemaphoreDelete(g_ctx.lock);
g_ctx.lock = NULL;
return ret;
}
} else {
g_ctx.owns_i2c_bus = true;
}
if (!config->bh1750_enable) {
g_ctx.data.bh1750.valid = false;
g_ctx.data.bh1750.last_error = ESP_ERR_NOT_SUPPORTED;
} else {
g_ctx.data.bh1750.valid = false;
g_ctx.data.bh1750.last_error = ESP_ERR_INVALID_STATE;
}
if (!config->aht30_enable) {
g_ctx.data.aht30.valid = false;
g_ctx.data.aht30.last_error = ESP_ERR_NOT_SUPPORTED;
} else {
g_ctx.data.aht30.valid = false;
g_ctx.data.aht30.last_error = ESP_ERR_INVALID_STATE;
}
g_ctx.initialized = true;
ESP_LOGI(TAG,
"initialized: port=%d scl=%d sda=%d pullup_int=%d bh1750(en=%d,ready=%d,default,%ums) aht30(en=%d,ready=%d,default,%ums)",
config->i2c_port,
config->scl_io_num,
config->sda_io_num,
config->i2c_enable_internal_pullup,
config->bh1750_enable,
g_ctx.bh1750_ready,
config->bh1750_read_period_ms,
config->aht30_enable,
g_ctx.aht30_ready,
config->aht30_read_period_ms);
ESP_LOGI(TAG,
"i2c bus is ready; sensor drivers will initialize lazily in task loop");
return ESP_OK;
}
esp_err_t i2c_master_messager_start(void)
{
ESP_RETURN_ON_FALSE(g_ctx.initialized, ESP_ERR_INVALID_STATE, TAG, "not initialized");
if (g_ctx.task_handle != NULL) {
return ESP_OK;
}
BaseType_t ok = xTaskCreate(i2c_master_messager_task,
"i2c_msg_task",
4096,
NULL,
5,
&g_ctx.task_handle);
ESP_RETURN_ON_FALSE(ok == pdPASS, ESP_ERR_NO_MEM, TAG, "failed to create task");
return ESP_OK;
}
esp_err_t i2c_master_messager_stop(void)
{
ESP_RETURN_ON_FALSE(g_ctx.initialized, ESP_ERR_INVALID_STATE, TAG, "not initialized");
if (g_ctx.task_handle != NULL) {
vTaskDelete(g_ctx.task_handle);
g_ctx.task_handle = NULL;
}
return ESP_OK;
}
esp_err_t i2c_master_messager_get_data(i2c_master_messager_data_t *out_data)
{
ESP_RETURN_ON_FALSE(g_ctx.initialized, ESP_ERR_INVALID_STATE, TAG, "not initialized");
ESP_RETURN_ON_FALSE(out_data != NULL, ESP_ERR_INVALID_ARG, TAG, "out_data is null");
ESP_RETURN_ON_FALSE(g_ctx.lock != NULL, ESP_ERR_INVALID_STATE, TAG, "lock not ready");
ESP_RETURN_ON_FALSE(xSemaphoreTake(g_ctx.lock, pdMS_TO_TICKS(100)) == pdTRUE,
ESP_ERR_TIMEOUT,
TAG,
"failed to lock shared data");
*out_data = g_ctx.data;
xSemaphoreGive(g_ctx.lock);
return ESP_OK;
}
esp_err_t i2c_master_messager_deinit(void)
{
if (!g_ctx.initialized) {
return ESP_OK;
}
i2c_master_messager_stop();
if (g_ctx.bh1750 != NULL) {
bh1750_delete(g_ctx.bh1750);
g_ctx.bh1750 = NULL;
}
if (g_ctx.aht30 != NULL) {
ahtxx_delete(g_ctx.aht30);
g_ctx.aht30 = NULL;
}
if (g_ctx.i2c_bus != NULL && g_ctx.owns_i2c_bus) {
i2c_del_master_bus(g_ctx.i2c_bus);
}
g_ctx.i2c_bus = NULL;
g_ctx.owns_i2c_bus = false;
if (g_ctx.lock != NULL) {
vSemaphoreDelete(g_ctx.lock);
g_ctx.lock = NULL;
}
memset(&g_ctx, 0, sizeof(g_ctx));
return ESP_OK;
}

View File

@@ -0,0 +1,57 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "bh1750.h"
#include "driver/gpio.h"
#include "driver/i2c_types.h"
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
i2c_port_num_t i2c_port;
gpio_num_t scl_io_num;
gpio_num_t sda_io_num;
bool i2c_enable_internal_pullup;
bool bh1750_enable;
bool aht30_enable;
uint16_t bh1750_read_period_ms;
uint16_t aht30_read_period_ms;
bh1750_measure_mode_t bh1750_mode;
} i2c_master_messager_config_t;
typedef struct {
float lux;
bool valid;
int64_t last_update_ms;
esp_err_t last_error;
} i2c_bh1750_data_t;
typedef struct {
float temperature_c;
float humidity_rh;
bool valid;
int64_t last_update_ms;
esp_err_t last_error;
} i2c_aht30_data_t;
typedef struct {
i2c_bh1750_data_t bh1750;
i2c_aht30_data_t aht30;
} i2c_master_messager_data_t;
#define I2C_MASTER_MESSAGER_MIN_PERIOD_MS (100)
esp_err_t i2c_master_messager_init(const i2c_master_messager_config_t *config);
esp_err_t i2c_master_messager_start(void);
esp_err_t i2c_master_messager_stop(void);
esp_err_t i2c_master_messager_get_data(i2c_master_messager_data_t *out_data);
esp_err_t i2c_master_messager_deinit(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "io_device_control.c"
INCLUDE_DIRS "include"
REQUIRES esp_driver_gpio)

View File

@@ -0,0 +1,50 @@
# io_device_control
`io_device_control` 组件用于统一管理项目中的简单 IO 外设控制。
当前定义:
- `GPIO1`:风扇控制(高电平有效)
- `GPIO0`:光照控制(高电平有效)
- `GPIO12`:加热控制(高电平有效)
- `GPIO13`:制冷控制(高电平有效)
## 对外接口
头文件:`include/io_device_control.h`
- `esp_err_t io_device_control_init(void);`
- 初始化 GPIO 输出方向,并将风扇/光照/加热/制冷默认置为关闭(低电平)。
- `esp_err_t io_device_control_set_fan(bool on);`
- 控制风扇开关,`true` 为开,`false` 为关。
- `esp_err_t io_device_control_set_light(bool on);`
- 控制光照开关,`true` 为开,`false` 为关。
- `esp_err_t io_device_control_set_hot(bool on);`
- 控制加热开关,`true` 为开,`false` 为关。
- `esp_err_t io_device_control_set_cool(bool on);`
- 控制制冷开关,`true` 为开,`false` 为关。
## 使用方式
`app_main` 中先初始化,后续在业务逻辑中按需调用控制接口。
```c
#include "esp_check.h"
#include "io_device_control.h"
void app_main(void)
{
ESP_ERROR_CHECK(io_device_control_init());
// 后续按需调用
// ESP_ERROR_CHECK(io_device_control_set_fan(true));
// ESP_ERROR_CHECK(io_device_control_set_light(true));
// ESP_ERROR_CHECK(io_device_control_set_hot(true));
// ESP_ERROR_CHECK(io_device_control_set_cool(true));
}
```
## 注意事项
- 控制接口在未初始化时会返回 `ESP_ERR_INVALID_STATE`
- 若硬件驱动电路为反相,请在硬件层或组件内部统一处理,不建议在业务层散落取反逻辑。

View File

@@ -0,0 +1,28 @@
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
// Initializes fan/light/hot/cool outputs and sets all devices off by default.
esp_err_t io_device_control_init(void);
// High level control APIs, all are active-high outputs.
esp_err_t io_device_control_set_fan(bool on);
esp_err_t io_device_control_set_light(bool on);
esp_err_t io_device_control_set_hot(bool on);
esp_err_t io_device_control_set_cool(bool on);
// Read current output states from GPIO.
esp_err_t io_device_control_get_states(bool *fan_on,
bool *light_on,
bool *hot_on,
bool *cool_on);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,106 @@
#include <stdbool.h>
#include "driver/gpio.h"
#include "esp_check.h"
#include "esp_log.h"
#include "io_device_control.h"
static const char *TAG = "io_device_control";
#define IO_DEVICE_FAN_GPIO GPIO_NUM_1
#define IO_DEVICE_LIGHT_GPIO GPIO_NUM_0
#define IO_DEVICE_HOT_GPIO GPIO_NUM_12
#define IO_DEVICE_COOL_GPIO GPIO_NUM_13
static bool s_inited = false;
static esp_err_t io_device_control_set_level(gpio_num_t pin, bool on)
{
ESP_RETURN_ON_FALSE(s_inited, ESP_ERR_INVALID_STATE, TAG, "not initialized");
return gpio_set_level(pin, on ? 1 : 0);
}
esp_err_t io_device_control_init(void)
{
if (s_inited) {
return ESP_OK;
}
const gpio_config_t out_cfg = {
.pin_bit_mask = (1ULL << IO_DEVICE_FAN_GPIO) |
(1ULL << IO_DEVICE_LIGHT_GPIO) |
(1ULL << IO_DEVICE_HOT_GPIO) |
(1ULL << IO_DEVICE_COOL_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&out_cfg), TAG, "gpio_config failed");
// Active-high outputs; default low keeps devices off at boot.
ESP_RETURN_ON_ERROR(gpio_set_level(IO_DEVICE_FAN_GPIO, 0), TAG, "set fan default failed");
ESP_RETURN_ON_ERROR(gpio_set_level(IO_DEVICE_LIGHT_GPIO, 0), TAG, "set light default failed");
ESP_RETURN_ON_ERROR(gpio_set_level(IO_DEVICE_HOT_GPIO, 0), TAG, "set hot default failed");
ESP_RETURN_ON_ERROR(gpio_set_level(IO_DEVICE_COOL_GPIO, 0), TAG, "set cool default failed");
s_inited = true;
ESP_LOGI(TAG,
"initialized: fan=GPIO%d light=GPIO%d hot=GPIO%d cool=GPIO%d active_high=1",
IO_DEVICE_FAN_GPIO,
IO_DEVICE_LIGHT_GPIO,
IO_DEVICE_HOT_GPIO,
IO_DEVICE_COOL_GPIO);
return ESP_OK;
}
esp_err_t io_device_control_set_fan(bool on)
{
ESP_RETURN_ON_ERROR(io_device_control_set_level(IO_DEVICE_FAN_GPIO, on),
TAG,
"set fan failed");
return ESP_OK;
}
esp_err_t io_device_control_set_light(bool on)
{
ESP_RETURN_ON_ERROR(io_device_control_set_level(IO_DEVICE_LIGHT_GPIO, on),
TAG,
"set light failed");
return ESP_OK;
}
esp_err_t io_device_control_set_hot(bool on)
{
ESP_RETURN_ON_ERROR(io_device_control_set_level(IO_DEVICE_HOT_GPIO, on),
TAG,
"set hot failed");
return ESP_OK;
}
esp_err_t io_device_control_set_cool(bool on)
{
ESP_RETURN_ON_ERROR(io_device_control_set_level(IO_DEVICE_COOL_GPIO, on),
TAG,
"set cool failed");
return ESP_OK;
}
esp_err_t io_device_control_get_states(bool *fan_on,
bool *light_on,
bool *hot_on,
bool *cool_on)
{
ESP_RETURN_ON_FALSE(fan_on != NULL && light_on != NULL && hot_on != NULL && cool_on != NULL,
ESP_ERR_INVALID_ARG,
TAG,
"null state pointer");
ESP_RETURN_ON_FALSE(s_inited, ESP_ERR_INVALID_STATE, TAG, "not initialized");
*fan_on = (gpio_get_level(IO_DEVICE_FAN_GPIO) != 0);
*light_on = (gpio_get_level(IO_DEVICE_LIGHT_GPIO) != 0);
*hot_on = (gpio_get_level(IO_DEVICE_HOT_GPIO) != 0);
*cool_on = (gpio_get_level(IO_DEVICE_COOL_GPIO) != 0);
return ESP_OK;
}

View File

@@ -0,0 +1,4 @@
idf_component_register(SRCS "lvgl_st7735s_use.c"
INCLUDE_DIRS "include"
REQUIRES driver esp_lcd esp_lvgl_port
)

View File

@@ -0,0 +1,105 @@
# lvgl_st7735s_use 组件说明
`lvgl_st7735s_use` 是项目中的 LCD 显示组件,基于 `esp_lcd + esp_lvgl_port`,用于快速驱动 ST77xx 系列 SPI 屏并显示 LVGL 界面。
---
## 功能概览
- 初始化 SPI LCD含背光、面板、显示偏移
- 初始化 LVGL 端口并注册显示设备
- 默认创建一个居中标签用于快速验证显示链路
- 提供运行时更新中心文本接口
- 支持可配置方向、镜像与偏移
- 支持可选三色测试图(调试用)
---
## 对外 API
头文件:`include/lvgl_st7735s_use.h`
- `esp_err_t start_lvgl_demo(void);`
- 完成 LCD + LVGL 初始化并创建默认界面
- `esp_err_t lvgl_st7735s_set_center_text(const char *text);`
- 运行时更新中心标签文字(线程安全,内部已加锁)
---
## 关键配置项(可直接改宏)
`include/lvgl_st7735s_use.h` 中:
### 1) 屏幕与 SPI
- `EXAMPLE_LCD_H_RES` / `EXAMPLE_LCD_V_RES`
- `EXAMPLE_LCD_PIXEL_CLK_HZ`
- `EXAMPLE_LCD_SPI_NUM`
- `EXAMPLE_LCD_CMD_BITS` / `EXAMPLE_LCD_PARAM_BITS`
建议:首次点亮优先用较低时钟(如 `10MHz`),稳定后再升频。
### 2) 方向与偏移(重点)
- `EXAMPLE_LCD_GAP_X`
- `EXAMPLE_LCD_GAP_Y`
- `EXAMPLE_LCD_ROT_SWAP_XY`
- `EXAMPLE_LCD_ROT_MIRROR_X`
- `EXAMPLE_LCD_ROT_MIRROR_Y`
说明:
- 当前项目已验证一组可用参数(顺时针 90° + 26 偏移)。
- 若出现“文字偏移/边缘花屏/方向反了”,优先微调上述宏,不要同时在多层重复旋转。
### 3) 调试项
- `EXAMPLE_LCD_ENABLE_COLOR_TEST`
- `1`:上电先画 RGB 三色测试图(便于确认硬件链路)
- `0`:跳过测试,直接进入 LVGL
---
## 在主程序中调用
```c
#include "esp_check.h"
#include "lvgl_st7735s_use.h"
void app_main(void)
{
ESP_ERROR_CHECK(start_lvgl_demo());
ESP_ERROR_CHECK(lvgl_st7735s_set_center_text("BotanicalBuddy"));
}
```
---
## 常见问题
### 1) 背光亮但没有内容
优先排查:
- 面板型号与驱动是否匹配ST7735S / ST7789
- SPI 模式、时钟是否过高
- 方向/偏移参数是否正确
### 2) 文字方向反了或显示偏移
优先调整:
- `EXAMPLE_LCD_ROT_*`
- `EXAMPLE_LCD_GAP_X / EXAMPLE_LCD_GAP_Y`
### 3) 想快速确认硬件链路是否通
`EXAMPLE_LCD_ENABLE_COLOR_TEST` 设为 `1`,观察是否能显示三色图。
---
## 依赖
由组件 `CMakeLists.txt` 声明:
- `driver`
- `esp_lcd`
- `esp_lvgl_port`

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* LCD size */
#define EXAMPLE_LCD_H_RES (160)
#define EXAMPLE_LCD_V_RES (80)
/* LCD SPI总线配置 */
#define EXAMPLE_LCD_SPI_NUM (SPI2_HOST) // 使用SPI2主机接口进行通信
/* LCD显示参数配置 */
#define EXAMPLE_LCD_PIXEL_CLK_HZ (10 * 1000 * 1000) // 先用10MHz提高兼容性点亮后再逐步升频
/* LCD命令和参数配置 */
#define EXAMPLE_LCD_CMD_BITS (8) // 命令位数为8位用于发送LCD控制命令
#define EXAMPLE_LCD_PARAM_BITS (8) // 参数位数为8位用于发送命令参数
/* LCD颜色和缓冲区配置 */
#define EXAMPLE_LCD_BITS_PER_PIXEL (16) // 每个像素使用16位颜色(RGB565格式)
#define EXAMPLE_LCD_DRAW_BUFF_DOUBLE (1) // 启用双缓冲模式,提高显示流畅度
#define EXAMPLE_LCD_DRAW_BUFF_HEIGHT (50) // 绘图缓冲区高度为50行影响刷新性能
/* LCD背光配置 */
#define EXAMPLE_LCD_BL_ON_LEVEL (1) // 背光开启电平为高电平(1)
/* LCD方向/偏移配置当前为顺时针90°并保留26偏移 */
#define EXAMPLE_LCD_GAP_X (1)
#define EXAMPLE_LCD_GAP_Y (26)
#define EXAMPLE_LCD_ROT_SWAP_XY (1)
#define EXAMPLE_LCD_ROT_MIRROR_X (1)
#define EXAMPLE_LCD_ROT_MIRROR_Y (0)
/* 调试项:上电后是否先显示三色测试图 */
#define EXAMPLE_LCD_ENABLE_COLOR_TEST (0)
/* LCD pins */
#define EXAMPLE_LCD_GPIO_SCLK (GPIO_NUM_2)
#define EXAMPLE_LCD_GPIO_MOSI (GPIO_NUM_3)
#define EXAMPLE_LCD_GPIO_RST (GPIO_NUM_10)
#define EXAMPLE_LCD_GPIO_DC (GPIO_NUM_8)
#define EXAMPLE_LCD_GPIO_CS (GPIO_NUM_7)
#define EXAMPLE_LCD_GPIO_BL (GPIO_NUM_6)
esp_err_t start_lvgl_demo(void);
esp_err_t lvgl_st7735s_set_center_text(const char *text);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,246 @@
#include <stdio.h>
#include <stdlib.h>
#include "lvgl_st7735s_use.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lvgl_port.h"
static const char *TAG = "lvgl_st7735s_use";
static esp_lcd_panel_io_handle_t lcd_io = NULL;
static esp_lcd_panel_handle_t lcd_panel = NULL;
static lv_display_t *lvgl_disp = NULL;
static lv_obj_t *s_center_label = NULL;
#if EXAMPLE_LCD_ENABLE_COLOR_TEST
static esp_err_t app_lcd_color_test(void)
{
const size_t pixels = EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES;
uint16_t *frame = calloc(pixels, sizeof(uint16_t));
ESP_RETURN_ON_FALSE(frame != NULL, ESP_ERR_NO_MEM, TAG, "分配测试帧缓冲失败");
for (int y = 0; y < EXAMPLE_LCD_V_RES; y++) {
for (int x = 0; x < EXAMPLE_LCD_H_RES; x++) {
uint16_t color;
if (x < EXAMPLE_LCD_H_RES / 3) {
color = 0xF800; // 红
} else if (x < (EXAMPLE_LCD_H_RES * 2) / 3) {
color = 0x07E0; // 绿
} else {
color = 0x001F; // 蓝
}
frame[y * EXAMPLE_LCD_H_RES + x] = color;
}
}
esp_err_t err = esp_lcd_panel_draw_bitmap(lcd_panel, 0, 0, EXAMPLE_LCD_H_RES, EXAMPLE_LCD_V_RES, frame);
free(frame);
ESP_RETURN_ON_ERROR(err, TAG, "三色测试绘制失败");
ESP_LOGI(TAG, "LCD三色测试图已发送");
return ESP_OK;
}
#endif
/**
* @brief 初始化LCD硬件和SPI接口
*
* 该函数负责初始化LCD所需的GPIO、SPI总线并配置LCD面板
* 包括背光控制、SPI总线配置、面板IO配置和面板驱动安装
*
* @return esp_err_t 初始化结果ESP_OK表示成功
*/
static esp_err_t app_lcd_init(void)
{
esp_err_t ret = ESP_OK;
gpio_config_t bk_gpio_config = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ULL << EXAMPLE_LCD_GPIO_BL
};
ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));
ESP_LOGI(TAG, "初始化SPI总线");
const spi_bus_config_t buscfg = {
.sclk_io_num = EXAMPLE_LCD_GPIO_SCLK,
.mosi_io_num = EXAMPLE_LCD_GPIO_MOSI,
.miso_io_num = GPIO_NUM_NC,
.quadwp_io_num = GPIO_NUM_NC,
.quadhd_io_num = GPIO_NUM_NC,
.max_transfer_sz = EXAMPLE_LCD_H_RES * EXAMPLE_LCD_DRAW_BUFF_HEIGHT * sizeof(uint16_t),
};
ESP_RETURN_ON_ERROR(spi_bus_initialize(EXAMPLE_LCD_SPI_NUM, &buscfg, SPI_DMA_CH_AUTO), TAG, "SPI初始化失败");
ESP_LOGI(TAG, "安装面板IO");
const esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = EXAMPLE_LCD_GPIO_DC,
.cs_gpio_num = EXAMPLE_LCD_GPIO_CS,
.pclk_hz = EXAMPLE_LCD_PIXEL_CLK_HZ,
.lcd_cmd_bits = EXAMPLE_LCD_CMD_BITS,
.lcd_param_bits = EXAMPLE_LCD_PARAM_BITS,
.spi_mode = 0,
.trans_queue_depth = 10,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)EXAMPLE_LCD_SPI_NUM, &io_config, &lcd_io), err, TAG, "创建面板IO失败");
ESP_LOGI(TAG, "安装LCD驱动");
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = EXAMPLE_LCD_GPIO_RST,
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0)
.rgb_endian = LCD_RGB_ENDIAN_RGB,
#else
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR,
#endif
.bits_per_pixel = EXAMPLE_LCD_BITS_PER_PIXEL,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7789(lcd_io, &panel_config, &lcd_panel), err, TAG, "创建面板失败");
ESP_GOTO_ON_ERROR(esp_lcd_panel_reset(lcd_panel), err, TAG, "面板复位失败");
ESP_GOTO_ON_ERROR(esp_lcd_panel_init(lcd_panel), err, TAG, "面板初始化失败");
ESP_GOTO_ON_ERROR(esp_lcd_panel_swap_xy(lcd_panel, false), err, TAG, "设置面板swap_xy失败");
ESP_GOTO_ON_ERROR(esp_lcd_panel_mirror(lcd_panel, false, false), err, TAG, "设置面板镜像失败");
ESP_GOTO_ON_ERROR(esp_lcd_panel_set_gap(lcd_panel, EXAMPLE_LCD_GAP_X, EXAMPLE_LCD_GAP_Y), err, TAG, "设置显示偏移失败");
ESP_LOGI(TAG, "面板基准参数已应用: gap=(%d,%d)", EXAMPLE_LCD_GAP_X, EXAMPLE_LCD_GAP_Y);
ESP_GOTO_ON_ERROR(esp_lcd_panel_invert_color(lcd_panel, true), err, TAG, "设置反色失败");
ESP_GOTO_ON_ERROR(esp_lcd_panel_disp_on_off(lcd_panel, true), err, TAG, "打开显示失败");
ESP_RETURN_ON_ERROR(gpio_set_level(EXAMPLE_LCD_GPIO_BL, EXAMPLE_LCD_BL_ON_LEVEL), TAG, "背光引脚置位失败");
ESP_LOGI(TAG, "背光已打开,电平=%d", EXAMPLE_LCD_BL_ON_LEVEL);
return ret;
// 错误处理标签,用于清理资源
err:
if (lcd_panel) {
esp_lcd_panel_del(lcd_panel);
lcd_panel = NULL;
}
if (lcd_io) {
esp_lcd_panel_io_del(lcd_io);
lcd_io = NULL;
}
spi_bus_free(EXAMPLE_LCD_SPI_NUM);
return ret;
}
/**
* @brief 初始化LVGL图形库
*
* 该函数负责初始化LVGL库并配置显示设备
* 包括LVGL任务配置、显示缓冲区配置和旋转设置
*
* @return esp_err_t 初始化结果ESP_OK表示成功
*/
static esp_err_t app_lvgl_init(void)
{
const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = 4,
.task_stack = 4096,
.task_affinity = -1,
.task_max_sleep_ms = 500,
.timer_period_ms = 5
};
ESP_RETURN_ON_ERROR(lvgl_port_init(&lvgl_cfg), TAG, "LVGL端口初始化失败");
ESP_LOGI(TAG, "添加LCD屏幕");
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = lcd_io,
.panel_handle = lcd_panel,
.buffer_size = EXAMPLE_LCD_H_RES * EXAMPLE_LCD_DRAW_BUFF_HEIGHT,
.double_buffer = EXAMPLE_LCD_DRAW_BUFF_DOUBLE,
.hres = EXAMPLE_LCD_H_RES,
.vres = EXAMPLE_LCD_V_RES,
.monochrome = false,
#if LVGL_VERSION_MAJOR >= 9
.color_format = LV_COLOR_FORMAT_RGB565,
#endif
.rotation = {
.swap_xy = EXAMPLE_LCD_ROT_SWAP_XY,
.mirror_x = EXAMPLE_LCD_ROT_MIRROR_X,
.mirror_y = EXAMPLE_LCD_ROT_MIRROR_Y,
},
.flags = {
.buff_dma = true,
#if LVGL_VERSION_MAJOR >= 9
.swap_bytes = false,
#endif
}};
lvgl_disp = lvgl_port_add_disp(&disp_cfg);
ESP_RETURN_ON_FALSE(lvgl_disp != NULL, ESP_FAIL, TAG, "添加LVGL显示设备失败");
ESP_LOGI(TAG, "LVGL旋转已应用: swap_xy=%d mirror_x=%d mirror_y=%d",
EXAMPLE_LCD_ROT_SWAP_XY, EXAMPLE_LCD_ROT_MIRROR_X, EXAMPLE_LCD_ROT_MIRROR_Y);
return ESP_OK;
}
/**
* @brief 创建并显示LVGL主界面
*
* 该函数负责创建LVGL的用户界面元素包括图像、标签和按钮
* 并设置它们的位置和属性
*/
static void app_main_display(void)
{
lv_obj_t *scr = lv_scr_act();
lvgl_port_lock(0);
lv_obj_set_style_bg_color(scr, lv_color_white(), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
s_center_label = lv_label_create(scr);
lv_label_set_text(s_center_label, "BotanicalBuddy\nloading...");
lv_label_set_recolor(s_center_label, false);
lv_label_set_long_mode(s_center_label, LV_LABEL_LONG_WRAP);
lv_obj_set_size(s_center_label, EXAMPLE_LCD_H_RES - 6, EXAMPLE_LCD_V_RES - 6);
lv_obj_set_style_text_color(s_center_label, lv_color_black(), 0);
lv_obj_set_style_text_font(s_center_label, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_align(s_center_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_pad_all(s_center_label, 0, 0);
lv_obj_align(s_center_label, LV_ALIGN_CENTER, 0, 0);
lvgl_port_unlock();
}
/**
* @brief 启动LVGL演示程序
*
* 该函数是程序的入口点负责初始化LCD硬件、LVGL库并显示主界面
*/
esp_err_t lvgl_st7735s_set_center_text(const char *text)
{
ESP_RETURN_ON_FALSE(text != NULL, ESP_ERR_INVALID_ARG, TAG, "text is null");
ESP_RETURN_ON_FALSE(s_center_label != NULL, ESP_ERR_INVALID_STATE, TAG, "label not ready");
lvgl_port_lock(0);
lv_label_set_text(s_center_label, text);
lv_obj_align(s_center_label, LV_ALIGN_CENTER, 0, 0);
lvgl_port_unlock();
return ESP_OK;
}
esp_err_t start_lvgl_demo(void)
{
ESP_RETURN_ON_ERROR(app_lcd_init(), TAG, "LCD初始化失败");
#if EXAMPLE_LCD_ENABLE_COLOR_TEST
ESP_RETURN_ON_ERROR(app_lcd_color_test(), TAG, "LCD测试图绘制失败");
vTaskDelay(pdMS_TO_TICKS(300));
#endif
ESP_RETURN_ON_ERROR(app_lvgl_init(), TAG, "LVGL初始化失败");
app_main_display();
return ESP_OK;
}

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "mqtt_control.c"
INCLUDE_DIRS "include"
REQUIRES mqtt cjson)

View File

@@ -0,0 +1,59 @@
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#include "mqtt_client.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
bool has_mode;
bool auto_mode;
bool has_thresholds;
float light_on_lux;
float light_off_lux;
float hot_on_temp_c;
float hot_off_temp_c;
float cool_on_temp_c;
float cool_off_temp_c;
float fan_on_hum_pct;
float fan_off_hum_pct;
bool has_fan;
bool fan_on;
bool has_light;
bool light_on;
bool has_hot;
bool hot_on;
bool has_cool;
bool cool_on;
} mqtt_control_command_t;
typedef esp_err_t (*mqtt_control_command_handler_t)(const mqtt_control_command_t *cmd, void *user_ctx);
esp_err_t mqtt_control_start(void);
esp_err_t mqtt_control_stop(void);
esp_err_t mqtt_control_register_command_handler(mqtt_control_command_handler_t handler, void *user_ctx);
bool mqtt_control_is_connected(void);
// Generic publish API for any topic.
esp_err_t mqtt_control_publish(const char *topic,
const char *payload,
int qos,
int retain);
// Publish telemetry payload to default sensor topic.
esp_err_t mqtt_control_publish_sensor(const char *payload, int qos, int retain);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,396 @@
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include "cJSON.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "mqtt_control.h"
// MQTT 服务器地址(协议+域名+端口)
#define MQTT_BROKER_URL "mqtt://beihong.wang:1883"
// MQTT 用户名
#define MQTT_USERNAME "BotanicalBuddy"
// MQTT 密码
#define MQTT_PASSWORD "YTGui8979HI"
// 传感器数据发布主题
#define MQTT_SENSOR_TOPIC "topic/sensor/esp32_BotanicalBuddy_001"
// 控制指令订阅主题
#define MQTT_CONTROL_TOPIC "topic/control/esp32_BotanicalBuddy_001"
static const char *TAG = "mqtt_control"; // 日志标签
static esp_mqtt_client_handle_t g_mqtt_client = NULL; // 全局 MQTT 客户端句柄
static bool g_mqtt_connected = false; // MQTT 连接状态标志
static mqtt_control_command_handler_t g_cmd_handler = NULL;
static void *g_cmd_user_ctx = NULL;
static bool json_read_bool(cJSON *root, const char *key, bool *out)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
if (item == NULL)
{
return false;
}
if (cJSON_IsBool(item))
{
*out = cJSON_IsTrue(item);
return true;
}
if (cJSON_IsNumber(item))
{
*out = (item->valuedouble != 0.0);
return true;
}
if (cJSON_IsString(item) && item->valuestring != NULL)
{
if (strcasecmp(item->valuestring, "on") == 0 ||
strcasecmp(item->valuestring, "true") == 0 ||
strcmp(item->valuestring, "1") == 0)
{
*out = true;
return true;
}
if (strcasecmp(item->valuestring, "off") == 0 ||
strcasecmp(item->valuestring, "false") == 0 ||
strcmp(item->valuestring, "0") == 0)
{
*out = false;
return true;
}
}
return false;
}
static bool json_read_float(cJSON *root, const char *key, float *out)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
if (!cJSON_IsNumber(item))
{
return false;
}
*out = (float)item->valuedouble;
return true;
}
static bool json_read_mode_auto(cJSON *root, const char *key, bool *out_auto)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(root, key);
if (item == NULL)
{
return false;
}
if (cJSON_IsString(item) && item->valuestring != NULL)
{
if (strcasecmp(item->valuestring, "auto") == 0)
{
*out_auto = true;
return true;
}
if (strcasecmp(item->valuestring, "manual") == 0)
{
*out_auto = false;
return true;
}
}
if (cJSON_IsBool(item))
{
*out_auto = cJSON_IsTrue(item);
return true;
}
if (cJSON_IsNumber(item))
{
*out_auto = (item->valuedouble != 0.0);
return true;
}
return false;
}
static esp_err_t mqtt_parse_control_command(const char *data, int data_len, mqtt_control_command_t *out_cmd)
{
ESP_RETURN_ON_FALSE(data != NULL && data_len > 0, ESP_ERR_INVALID_ARG, TAG, "invalid mqtt data");
ESP_RETURN_ON_FALSE(out_cmd != NULL, ESP_ERR_INVALID_ARG, TAG, "out_cmd is null");
memset(out_cmd, 0, sizeof(*out_cmd));
cJSON *root = cJSON_ParseWithLength(data, (size_t)data_len);
ESP_RETURN_ON_FALSE(root != NULL, ESP_ERR_INVALID_ARG, TAG, "control json parse failed");
float light_on_lux = 0.0f;
float light_off_lux = 0.0f;
float hot_on_temp_c = 0.0f;
float hot_off_temp_c = 0.0f;
float cool_on_temp_c = 0.0f;
float cool_off_temp_c = 0.0f;
float fan_on_hum_pct = 0.0f;
float fan_off_hum_pct = 0.0f;
bool has_light_on = json_read_float(root, "light_on", &light_on_lux);
bool has_light_off = json_read_float(root, "light_off", &light_off_lux);
bool has_hot_on = json_read_float(root, "hot_on_temp", &hot_on_temp_c);
bool has_hot_off = json_read_float(root, "hot_off_temp", &hot_off_temp_c);
bool has_cool_on = json_read_float(root, "cool_on_temp", &cool_on_temp_c);
bool has_cool_off = json_read_float(root, "cool_off_temp", &cool_off_temp_c);
bool has_fan_hum_on = json_read_float(root, "fan_on_hum", &fan_on_hum_pct);
bool has_fan_hum_off = json_read_float(root, "fan_off_hum", &fan_off_hum_pct);
out_cmd->has_mode = json_read_mode_auto(root, "mode", &out_cmd->auto_mode);
if (has_light_on && has_light_off && has_hot_on && has_hot_off &&
has_cool_on && has_cool_off && has_fan_hum_on && has_fan_hum_off)
{
out_cmd->has_thresholds = true;
out_cmd->light_on_lux = light_on_lux;
out_cmd->light_off_lux = light_off_lux;
out_cmd->hot_on_temp_c = hot_on_temp_c;
out_cmd->hot_off_temp_c = hot_off_temp_c;
out_cmd->cool_on_temp_c = cool_on_temp_c;
out_cmd->cool_off_temp_c = cool_off_temp_c;
out_cmd->fan_on_hum_pct = fan_on_hum_pct;
out_cmd->fan_off_hum_pct = fan_off_hum_pct;
}
out_cmd->has_fan = json_read_bool(root, "fan", &out_cmd->fan_on);
if (!out_cmd->has_fan) {
out_cmd->has_fan = json_read_bool(root, "pump", &out_cmd->fan_on);
}
out_cmd->has_light = json_read_bool(root, "light", &out_cmd->light_on);
out_cmd->has_hot = json_read_bool(root, "hot", &out_cmd->hot_on);
out_cmd->has_cool = json_read_bool(root, "cool", &out_cmd->cool_on);
cJSON_Delete(root);
ESP_RETURN_ON_FALSE(out_cmd->has_mode || out_cmd->has_thresholds || out_cmd->has_fan || out_cmd->has_light ||
out_cmd->has_hot || out_cmd->has_cool,
ESP_ERR_INVALID_ARG,
TAG,
"no valid control fields in payload");
return ESP_OK;
}
/**
* @brief 判断接收到的 MQTT 主题是否与预期主题匹配
*
* @param event_topic 事件中的主题字符串
* @param event_topic_len 事件中主题的长度
* @param expected 预期的主题字符串
* @return true 匹配成功false 匹配失败
*/
static bool mqtt_topic_match(const char *event_topic, int event_topic_len, const char *expected)
{
size_t expected_len = strlen(expected);
return expected_len == (size_t)event_topic_len && strncmp(event_topic, expected, expected_len) == 0;
}
/**
* @brief MQTT 事件处理回调函数
*
* 处理连接、断开、订阅、数据接收等事件。
*
* @param handler_args 用户传入的参数(未使用)
* @param base 事件基类型ESP-MQTT
* @param event_id 具体事件 ID
* @param event_data 事件数据指针
*/
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
(void)handler_args;
ESP_LOGD(TAG, "event base=%s id=%" PRIi32, base, event_id);
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data;
esp_mqtt_client_handle_t client = event->client;
switch ((esp_mqtt_event_id_t)event_id)
{
case MQTT_EVENT_CONNECTED: {
g_mqtt_connected = true;
ESP_LOGI(TAG, "MQTT connected");
// 连接成功后订阅控制主题
int msg_id = esp_mqtt_client_subscribe(client, MQTT_CONTROL_TOPIC, 1);
ESP_LOGI(TAG, "subscribe topic=%s msg_id=%d", MQTT_CONTROL_TOPIC, msg_id);
break;
}
case MQTT_EVENT_DISCONNECTED:
g_mqtt_connected = false;
ESP_LOGW(TAG, "MQTT disconnected");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT subscribed msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT data topic=%.*s data=%.*s",
event->topic_len,
event->topic,
event->data_len,
event->data);
// 如果是控制主题的数据,则解析控制命令(待实现)
if (mqtt_topic_match(event->topic, event->topic_len, MQTT_CONTROL_TOPIC))
{
mqtt_control_command_t cmd = {0};
esp_err_t parse_ret = mqtt_parse_control_command(event->data, event->data_len, &cmd);
if (parse_ret != ESP_OK)
{
ESP_LOGW(TAG, "控制命令解析失败: %s", esp_err_to_name(parse_ret));
break;
}
if (g_cmd_handler != NULL)
{
esp_err_t handle_ret = g_cmd_handler(&cmd, g_cmd_user_ctx);
if (handle_ret != ESP_OK)
{
ESP_LOGW(TAG, "控制命令处理失败: %s", esp_err_to_name(handle_ret));
}
}
else
{
ESP_LOGW(TAG, "未注册控制命令处理器,忽略控制消息");
}
}
break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT error type=%d", event->error_handle ? event->error_handle->error_type : -1);
break;
default:
break;
}
}
/**
* @brief 启动 MQTT 客户端
*
* 初始化客户端、注册事件回调、启动连接。
*
* @return esp_err_t 启动结果ESP_OK 表示成功
*/
esp_err_t mqtt_control_start(void)
{
if (g_mqtt_client != NULL)
{
return ESP_OK;
}
// 生成基于 MAC 地址后三字节的唯一客户端 ID
char client_id[32] = {0};
uint8_t mac[6] = {0};
ESP_RETURN_ON_ERROR(esp_read_mac(mac, ESP_MAC_WIFI_STA), TAG, "read mac failed");
snprintf(client_id, sizeof(client_id), "esp32_%02x%02x%02x", mac[3], mac[4], mac[5]);
// 配置 MQTT 客户端参数
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = MQTT_BROKER_URL,
.credentials.username = MQTT_USERNAME,
.credentials.client_id = client_id,
.credentials.authentication.password = MQTT_PASSWORD,
};
g_mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
ESP_RETURN_ON_FALSE(g_mqtt_client != NULL, ESP_FAIL, TAG, "mqtt client init failed");
ESP_RETURN_ON_ERROR(esp_mqtt_client_register_event(g_mqtt_client,
ESP_EVENT_ANY_ID,
mqtt_event_handler,
NULL),
TAG,
"register event failed");
ESP_RETURN_ON_ERROR(esp_mqtt_client_start(g_mqtt_client), TAG, "start mqtt client failed");
ESP_LOGI(TAG, "MQTT started with client_id=%s", client_id);
return ESP_OK;
}
esp_err_t mqtt_control_register_command_handler(mqtt_control_command_handler_t handler, void *user_ctx)
{
g_cmd_handler = handler;
g_cmd_user_ctx = user_ctx;
return ESP_OK;
}
/**
* @brief 停止并销毁 MQTT 客户端
*
* @return esp_err_t 停止结果ESP_OK 表示成功
*/
esp_err_t mqtt_control_stop(void)
{
if (g_mqtt_client == NULL)
{
return ESP_OK;
}
esp_err_t ret = esp_mqtt_client_stop(g_mqtt_client);
if (ret != ESP_OK)
{
return ret;
}
ret = esp_mqtt_client_destroy(g_mqtt_client);
if (ret != ESP_OK)
{
return ret;
}
g_mqtt_client = NULL;
g_mqtt_connected = false;
return ESP_OK;
}
/**
* @brief 查询 MQTT 当前连接状态
*
* @return true 已连接false 未连接
*/
bool mqtt_control_is_connected(void)
{
return g_mqtt_connected;
}
/**
* @brief 发布 MQTT 消息到指定主题
*
* @param topic 目标主题
* @param payload 消息载荷
* @param qos 服务质量等级0,1,2
* @param retain 是否保留消息
* @return esp_err_t 发布结果
*/
esp_err_t mqtt_control_publish(const char *topic,
const char *payload,
int qos,
int retain)
{
ESP_RETURN_ON_FALSE(topic != NULL, ESP_ERR_INVALID_ARG, TAG, "topic is null");
ESP_RETURN_ON_FALSE(payload != NULL, ESP_ERR_INVALID_ARG, TAG, "payload is null");
ESP_RETURN_ON_FALSE(g_mqtt_client != NULL, ESP_ERR_INVALID_STATE, TAG, "mqtt not started");
int msg_id = esp_mqtt_client_publish(g_mqtt_client, topic, payload, 0, qos, retain);
ESP_RETURN_ON_FALSE(msg_id >= 0, ESP_FAIL, TAG, "publish failed");
return ESP_OK;
}
/**
* @brief 发布传感器数据到预定义的传感器主题
*
* @param payload 传感器数据字符串
* @param qos 服务质量
* @param retain 是否保留消息
* @return esp_err_t 发布结果
*/
esp_err_t mqtt_control_publish_sensor(const char *payload, int qos, int retain)
{
return mqtt_control_publish(MQTT_SENSOR_TOPIC, payload, qos, retain);
}

View File

@@ -0,0 +1,17 @@
{
"files": [
"actions.h",
"fonts.h",
"images.c",
"images.h",
"screens.c",
"screens.h",
"structs.h",
"styles.c",
"styles.h",
"ui.c",
"ui.h",
"ui_font_24.c",
"vars.h"
]
}

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "."
INCLUDE_DIRS "."
REQUIRES lvgl esp_lvgl_port
)

14
components/ui/actions.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef EEZ_LVGL_UI_EVENTS_H
#define EEZ_LVGL_UI_EVENTS_H
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif /*EEZ_LVGL_UI_EVENTS_H*/

26
components/ui/fonts.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef EEZ_LVGL_UI_FONTS_H
#define EEZ_LVGL_UI_FONTS_H
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
extern const lv_font_t ui_font_24;
#ifndef EXT_FONT_DESC_T
#define EXT_FONT_DESC_T
typedef struct _ext_font_desc_t {
const char *name;
const void *font_ptr;
} ext_font_desc_t;
#endif
extern ext_font_desc_t fonts[];
#ifdef __cplusplus
}
#endif
#endif /*EEZ_LVGL_UI_FONTS_H*/

5
components/ui/images.c Normal file
View File

@@ -0,0 +1,5 @@
#include "images.h"
const ext_img_desc_t images[1] = {
0
};

24
components/ui/images.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef EEZ_LVGL_UI_IMAGES_H
#define EEZ_LVGL_UI_IMAGES_H
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef EXT_IMG_DESC_T
#define EXT_IMG_DESC_T
typedef struct _ext_img_desc_t {
const char *name;
const lv_img_dsc_t *img_dsc;
} ext_img_desc_t;
#endif
extern const ext_img_desc_t images[1];
#ifdef __cplusplus
}
#endif
#endif /*EEZ_LVGL_UI_IMAGES_H*/

215
components/ui/screens.c Normal file
View File

@@ -0,0 +1,215 @@
#include <string.h>
#include "screens.h"
#include "images.h"
#include "fonts.h"
#include "actions.h"
#include "vars.h"
#include "styles.h"
#include "ui.h"
#include <string.h>
objects_t objects;
//
// Event handlers
//
lv_obj_t *tick_value_change_obj;
//
// Screens
//
void create_screen_main() {
lv_obj_t *obj = lv_obj_create(0);
objects.main = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, 160, 80);
lv_obj_set_style_bg_color(obj, lv_color_hex(0xff72c801), LV_PART_MAIN | LV_STATE_DEFAULT);
{
lv_obj_t *parent_obj = obj;
{
lv_obj_t *obj = lv_label_create(parent_obj);
lv_obj_set_pos(obj, 6, 0);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_text_font(obj, &ui_font_24, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(obj, "温度");
}
{
lv_obj_t *obj = lv_label_create(parent_obj);
objects.obj0 = obj;
lv_obj_set_pos(obj, 74, 0);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_text_font(obj, &ui_font_24, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(obj, "");
}
{
lv_obj_t *obj = lv_label_create(parent_obj);
lv_obj_set_pos(obj, 6, 26);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_text_font(obj, &ui_font_24, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(obj, "湿度");
}
{
lv_obj_t *obj = lv_label_create(parent_obj);
lv_obj_set_pos(obj, 6, 52);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_text_font(obj, &ui_font_24, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(obj, "光强");
}
{
lv_obj_t *obj = lv_label_create(parent_obj);
objects.obj1 = obj;
lv_obj_set_pos(obj, 74, 26);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_text_font(obj, &ui_font_24, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(obj, "");
}
{
lv_obj_t *obj = lv_label_create(parent_obj);
objects.obj2 = obj;
lv_obj_set_pos(obj, 74, 52);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_text_font(obj, &ui_font_24, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(obj, "");
}
}
tick_screen_main();
}
void tick_screen_main() {
{
const char *new_val = get_var_air_temperature();
const char *cur_val = lv_label_get_text(objects.obj0);
if (strcmp(new_val, cur_val) != 0) {
tick_value_change_obj = objects.obj0;
lv_label_set_text(objects.obj0, new_val);
tick_value_change_obj = NULL;
}
}
{
const char *new_val = get_var_air_humidity();
const char *cur_val = lv_label_get_text(objects.obj1);
if (strcmp(new_val, cur_val) != 0) {
tick_value_change_obj = objects.obj1;
lv_label_set_text(objects.obj1, new_val);
tick_value_change_obj = NULL;
}
}
{
const char *new_val = get_var_light_intensity();
const char *cur_val = lv_label_get_text(objects.obj2);
if (strcmp(new_val, cur_val) != 0) {
tick_value_change_obj = objects.obj2;
lv_label_set_text(objects.obj2, new_val);
tick_value_change_obj = NULL;
}
}
}
typedef void (*tick_screen_func_t)();
tick_screen_func_t tick_screen_funcs[] = {
tick_screen_main,
};
void tick_screen(int screen_index) {
tick_screen_funcs[screen_index]();
}
void tick_screen_by_id(enum ScreensEnum screenId) {
tick_screen_funcs[screenId - 1]();
}
//
// Fonts
//
ext_font_desc_t fonts[] = {
{ "24", &ui_font_24 },
#if LV_FONT_MONTSERRAT_8
{ "MONTSERRAT_8", &lv_font_montserrat_8 },
#endif
#if LV_FONT_MONTSERRAT_10
{ "MONTSERRAT_10", &lv_font_montserrat_10 },
#endif
#if LV_FONT_MONTSERRAT_12
{ "MONTSERRAT_12", &lv_font_montserrat_12 },
#endif
#if LV_FONT_MONTSERRAT_14
{ "MONTSERRAT_14", &lv_font_montserrat_14 },
#endif
#if LV_FONT_MONTSERRAT_16
{ "MONTSERRAT_16", &lv_font_montserrat_16 },
#endif
#if LV_FONT_MONTSERRAT_18
{ "MONTSERRAT_18", &lv_font_montserrat_18 },
#endif
#if LV_FONT_MONTSERRAT_20
{ "MONTSERRAT_20", &lv_font_montserrat_20 },
#endif
#if LV_FONT_MONTSERRAT_22
{ "MONTSERRAT_22", &lv_font_montserrat_22 },
#endif
#if LV_FONT_MONTSERRAT_24
{ "MONTSERRAT_24", &lv_font_montserrat_24 },
#endif
#if LV_FONT_MONTSERRAT_26
{ "MONTSERRAT_26", &lv_font_montserrat_26 },
#endif
#if LV_FONT_MONTSERRAT_28
{ "MONTSERRAT_28", &lv_font_montserrat_28 },
#endif
#if LV_FONT_MONTSERRAT_30
{ "MONTSERRAT_30", &lv_font_montserrat_30 },
#endif
#if LV_FONT_MONTSERRAT_32
{ "MONTSERRAT_32", &lv_font_montserrat_32 },
#endif
#if LV_FONT_MONTSERRAT_34
{ "MONTSERRAT_34", &lv_font_montserrat_34 },
#endif
#if LV_FONT_MONTSERRAT_36
{ "MONTSERRAT_36", &lv_font_montserrat_36 },
#endif
#if LV_FONT_MONTSERRAT_38
{ "MONTSERRAT_38", &lv_font_montserrat_38 },
#endif
#if LV_FONT_MONTSERRAT_40
{ "MONTSERRAT_40", &lv_font_montserrat_40 },
#endif
#if LV_FONT_MONTSERRAT_42
{ "MONTSERRAT_42", &lv_font_montserrat_42 },
#endif
#if LV_FONT_MONTSERRAT_44
{ "MONTSERRAT_44", &lv_font_montserrat_44 },
#endif
#if LV_FONT_MONTSERRAT_46
{ "MONTSERRAT_46", &lv_font_montserrat_46 },
#endif
#if LV_FONT_MONTSERRAT_48
{ "MONTSERRAT_48", &lv_font_montserrat_48 },
#endif
};
//
// Color themes
//
uint32_t active_theme_index = 0;
//
//
//
void create_screens() {
// Set default LVGL theme
lv_display_t *dispp = lv_display_get_default();
lv_theme_t *theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT);
lv_display_set_theme(dispp, theme);
// Initialize screens
// Create screens
create_screen_main();
}

39
components/ui/screens.h Normal file
View File

@@ -0,0 +1,39 @@
#ifndef EEZ_LVGL_UI_SCREENS_H
#define EEZ_LVGL_UI_SCREENS_H
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
// Screens
enum ScreensEnum {
_SCREEN_ID_FIRST = 1,
SCREEN_ID_MAIN = 1,
_SCREEN_ID_LAST = 1
};
typedef struct _objects_t {
lv_obj_t *main;
lv_obj_t *obj0;
lv_obj_t *obj1;
lv_obj_t *obj2;
} objects_t;
extern objects_t objects;
void create_screen_main();
void tick_screen_main();
void tick_screen_by_id(enum ScreensEnum screenId);
void tick_screen(int screen_index);
void create_screens();
#ifdef __cplusplus
}
#endif
#endif /*EEZ_LVGL_UI_SCREENS_H*/

4
components/ui/structs.h Normal file
View File

@@ -0,0 +1,4 @@
#ifndef EEZ_LVGL_UI_STRUCTS_H
#define EEZ_LVGL_UI_STRUCTS_H
#endif /*EEZ_LVGL_UI_STRUCTS_H*/

6
components/ui/styles.c Normal file
View File

@@ -0,0 +1,6 @@
#include "styles.h"
#include "images.h"
#include "fonts.h"
#include "ui.h"
#include "screens.h"

14
components/ui/styles.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef EEZ_LVGL_UI_STYLES_H
#define EEZ_LVGL_UI_STYLES_H
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif /*EEZ_LVGL_UI_STYLES_H*/

32
components/ui/ui.c Normal file
View File

@@ -0,0 +1,32 @@
#include "ui.h"
#include "screens.h"
#include "images.h"
#include "actions.h"
#include "vars.h"
#include <string.h>
static int16_t currentScreen = -1;
static lv_obj_t *getLvglObjectFromIndex(int32_t index) {
if (index == -1) {
return 0;
}
return ((lv_obj_t **)&objects)[index];
}
void loadScreen(enum ScreensEnum screenId) {
currentScreen = screenId - 1;
lv_obj_t *screen = getLvglObjectFromIndex(currentScreen);
lv_scr_load_anim(screen, LV_SCR_LOAD_ANIM_FADE_IN, 200, 0, false);
}
void ui_init() {
create_screens();
loadScreen(SCREEN_ID_MAIN);
}
void ui_tick() {
tick_screen(currentScreen);
}

21
components/ui/ui.h Normal file
View File

@@ -0,0 +1,21 @@
#ifndef EEZ_LVGL_UI_GUI_H
#define EEZ_LVGL_UI_GUI_H
#include <lvgl.h>
#include "screens.h"
#ifdef __cplusplus
extern "C" {
#endif
void ui_init();
void ui_tick();
void loadScreen(enum ScreensEnum screenId);
#ifdef __cplusplus
}
#endif
#endif // EEZ_LVGL_UI_GUI_H

3477
components/ui/ui_font_24.c Normal file

File diff suppressed because it is too large Load Diff

36
components/ui/vars.c Normal file
View File

@@ -0,0 +1,36 @@
#include <string.h>
#include "vars.h"
char air_temperature[100] = { 0 };
const char *get_var_air_temperature() {
return air_temperature;
}
void set_var_air_temperature(const char *value) {
strncpy(air_temperature, value, sizeof(air_temperature) / sizeof(char));
air_temperature[sizeof(air_temperature) / sizeof(char) - 1] = 0;
}
char air_humidity[100] = { 0 };
const char *get_var_air_humidity() {
return air_humidity;
}
void set_var_air_humidity(const char *value) {
strncpy(air_humidity, value, sizeof(air_humidity) / sizeof(char));
air_humidity[sizeof(air_humidity) / sizeof(char) - 1] = 0;
}
char light_intensity[100] = { 0 };
const char *get_var_light_intensity() {
return light_intensity;
}
void set_var_light_intensity(const char *value) {
strncpy(light_intensity, value, sizeof(light_intensity) / sizeof(char));
light_intensity[sizeof(light_intensity) / sizeof(char) - 1] = 0;
}

34
components/ui/vars.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef EEZ_LVGL_UI_VARS_H
#define EEZ_LVGL_UI_VARS_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
// enum declarations
// Flow global variables
enum FlowGlobalVariables {
FLOW_GLOBAL_VARIABLE_AIR_TEMPERATURE = 0,
FLOW_GLOBAL_VARIABLE_AIR_HUMIDITY = 1,
FLOW_GLOBAL_VARIABLE_LIGHT_INTENSITY = 2
};
// Native global variables
extern const char *get_var_air_temperature();
extern void set_var_air_temperature(const char *value);
extern const char *get_var_air_humidity();
extern void set_var_air_humidity(const char *value);
extern const char *get_var_light_intensity();
extern void set_var_light_intensity(const char *value);
#ifdef __cplusplus
}
#endif
#endif /*EEZ_LVGL_UI_VARS_H*/

View File

@@ -0,0 +1,365 @@
# ESP32 配网组件设计实践:聚焦功能与实现,而不是项目绑定
很多 ESP32 设备在开发阶段都把“配网”当成一个小功能,但真正落地后会发现:
- 用户第一次接入要顺畅
- 失败后要能恢复
- 日志要便于现场排障
这篇文章只讲配网组件本身,聚焦能力设计和实现思路,不依赖具体业务项目。
---
## 一、目标能力定义
一个可落地的配网组件,建议至少包含以下能力:
- 按键触发配网
- 常驻配网模式(可选)
- SoftAP + Web Portal 配网
- DNS 劫持与常见 Captive Portal 兼容
- 凭据持久化NVS与重启自动重连
- 清除历史配置API + Web
- 状态机与可读日志
这几项组合起来,才能覆盖“首次成功 + 失败恢复 + 现场维护”三个关键场景。
图示(整体功能目标关联):
```mermaid
flowchart TD
A[按键任务/业务触发] --> B[进入 provisioning]
B --> C[启动 SoftAP]
B --> D[启动 HTTP Server]
B --> E[启动 DNS Hijack]
D --> F[/api/scan]
D --> G[/api/connect]
D --> H[/api/status]
D --> I[/api/clear]
G --> J[设置 STA 参数]
J --> K[发起 esp_wifi_connect]
K --> L{Wi-Fi/IP 事件}
L -->|成功| M[状态=connected]
L -->|失败| N[状态=failed]
I --> O[清除 NVS 凭据]
O --> P[清空运行态缓存]
P --> B
```
---
## 二、组件架构(通用)
```text
[按键任务] --> [进入配网]
|
+--> SoftAP + HTTP Server + DNS Hijack
[Web] -- /api/scan --> Wi-Fi 扫描
[Web] -- /api/connect --> 设置 STA 并发起连接
[Web] -- /api/status --> 轮询状态
[Web] -- /api/clear --> 清除已保存配置
[Wi-Fi/IP 事件] --> 更新状态机 + 打印日志 + 保存凭据
```
推荐状态机:
- `idle`
- `provisioning`
- `connecting`
- `connected`
- `failed`
- `timeout`
图示(状态机):
```mermaid
stateDiagram-v2
[*] --> idle
idle --> provisioning: wifi_connect_start()
provisioning --> connecting: POST /api/connect
connecting --> connected: GOT_IP
connecting --> failed: AUTH_FAIL / NO_AP / ...
connecting --> timeout: connect timeout
failed --> provisioning: POST /api/clear
timeout --> provisioning: POST /api/clear
connected --> provisioning: 常驻配网继续开放入口(可选)
connected --> idle: stop provisioning(按策略)
```
上图展示了组件的主要数据流与恢复路径。
---
## 三、对外 API 设计建议
推荐保持“少而稳”的接口:
```c
esp_err_t wifi_connect_init(void);
esp_err_t wifi_connect_start(void);
esp_err_t wifi_connect_stop(void);
wifi_connect_status_t wifi_connect_get_status(void);
esp_err_t wifi_connect_get_config(wifi_connect_config_t *config);
esp_err_t wifi_connect_clear_config(void);
```
设计原则:
- `init` 只做初始化和基础恢复
- `start/stop` 控制配网生命周期
- `get_status` 作为 UI/接口层统一读取入口
- `clear_config` 提供失败恢复通道
可以展示一段“典型调用顺序”代码:
```c
ESP_ERROR_CHECK(wifi_connect_init());
// 按键触发或业务触发时
ESP_ERROR_CHECK(wifi_connect_start());
// UI 侧轮询状态
wifi_connect_status_t st = wifi_connect_get_status();
// 需要恢复出厂配网时
ESP_ERROR_CHECK(wifi_connect_clear_config());
```
图示API 生命周期时序):
```mermaid
sequenceDiagram
participant App as 应用层
participant WC as wifi-connect
participant WiFi as esp_wifi
participant Web as 配网页面
App->>WC: wifi_connect_init()
WC-->>App: ESP_OK
App->>WC: wifi_connect_start()
WC->>WiFi: 开启 APSTA / 事件注册
WC-->>App: ESP_OK
Web->>WC: POST /api/connect(ssid,pass)
WC->>WiFi: esp_wifi_disconnect()
WC->>WiFi: esp_wifi_set_config()
WC->>WiFi: esp_wifi_connect()
WiFi-->>WC: WIFI_EVENT / IP_EVENT
alt 获取到 IP
WC-->>Web: status=connected
else 连接失败或超时
WC-->>Web: status=failed|timeout
end
Web->>WC: POST /api/clear
WC-->>Web: status=provisioning
```
上图对应完整 API 生命周期init -> start -> connecting -> connected/failed -> clear
---
## 四、关键实现点
### 1) Web 配网页面保持轻量
不一定要引入前端框架。对于资源受限设备,内嵌简洁 HTML/JS 往往更稳定。
建议页面只保留核心动作:
- 扫描网络
- 提交连接
- 查看状态
- 清除配置
### 2) Captive Portal 兼容是体验关键
仅提供首页 URL 通常不够。建议额外处理:
- 常见探测路径(如 `generate_204` 等)
- 未知路径统一 302 到配网页
这样手机系统弹门户页面成功率会明显提高。
示例代码(伪代码):
```c
static esp_err_t captive_redirect_handler(httpd_req_t *req)
{
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
return httpd_resp_send(req, NULL, 0);
}
// 注册常见探测路径
httpd_register_uri_handler(server, &uri_generate_204);
httpd_register_uri_handler(server, &uri_hotspot_detect);
httpd_register_uri_handler(server, &uri_ncsi);
```
可直接使用以下流程图(对应 Captive Portal 重定向路径):
```mermaid
flowchart LR
A[手机连接设备 AP] --> B[访问任意域名]
B --> C[DNS Hijack 返回 192.168.4.1]
C --> D[HTTP 探测路径请求<br/>/generate_204 等]
D --> E[302 Location: http://192.168.4.1/]
E --> F[打开配网页面]
F --> G[扫描 / 连接 / 清除配置]
```
### 3) 连接前主动断开旧连接
这是一个高频坑:设备已有 STA 连接时,直接连接新 AP 可能导致超时或异常状态。
建议在 `esp_wifi_set_config + esp_wifi_connect` 前先执行 `esp_wifi_disconnect()`,并对异常返回做日志记录。
示例代码:
```c
esp_err_t err = esp_wifi_disconnect();
if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_CONNECT) {
ESP_LOGW(TAG, "disconnect before reconnect failed: %s", esp_err_to_name(err));
}
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &cfg));
ESP_ERROR_CHECK(esp_wifi_connect());
```
### 4) “清除配置”必须打通全链路
建议同时提供:
- SDK API`wifi_connect_clear_config()`
- HTTP API`POST /api/clear`
- 页面按钮:`清除已保存`
这样现场人员无需改固件即可恢复设备。
建议把清除动作写成“存储层 + 运行态”两段:
```c
esp_err_t wifi_connect_clear_config(void)
{
// 1) 清 NVS 凭据
ESP_RETURN_ON_ERROR(nvs_erase_key(nvs, "ssid"), TAG, "erase ssid failed");
ESP_RETURN_ON_ERROR(nvs_erase_key(nvs, "pass"), TAG, "erase pass failed");
ESP_RETURN_ON_ERROR(nvs_commit(nvs), TAG, "nvs commit failed");
// 2) 清内存态并回到 provisioning
memset(&s_ctx.pending_cfg, 0, sizeof(s_ctx.pending_cfg));
s_ctx.status = WIFI_CONNECT_STATUS_PROVISIONING;
return ESP_OK;
}
```
可直接使用以下流程图(对应 clear 后状态回到 provisioning
```mermaid
flowchart TD
A[开始清除配置] --> B[擦除 NVS:ssid]
B --> C[擦除 NVS:pass]
C --> D[nvs_commit]
D --> E[清空 pending 配置缓存]
E --> F[清空错误原因/中间状态]
F --> G[状态切回 provisioning]
G --> H[前端轮询显示 可重新配网]
```
---
## 五、日志策略(非常重要)
建议日志遵循“状态 + 原因”格式,例如:
- `【状态】配网已启动热点已开启SSID=...`
- `【状态】开始连接路由器:目标网络=...`
- `【状态】联网成功:获取 IP=...`
- `【状态】连接失败:原因=...`
这样做的收益是:
- 开发调试快
- 测试可直接定位阶段
- 现场人员无需先理解底层驱动日志
可展示一个统一日志函数风格:
```c
static void log_state_i(const char *title, const char *detail)
{
ESP_LOGI(TAG, "【状态】%s%s", title, detail ? detail : "-");
}
```
如需补充非 Mermaid 图,建议仅放一张关键串口日志截图(启动配网、连接中、成功/失败重试)。
---
## 六、常见问题与排障思路
### 问题 1手机连上 AP 但页面不弹
排查:
- 手动访问 `http://192.168.4.1`
- 检查 DNS 劫持和门户探测路径是否启用
- 检查 HTTP 服务是否启动成功
### 问题 2提交密码后长时间连接失败
排查:
- 是否先断开旧 STA
- 是否正确处理了连接超时和重试
- 失败原因是否上报到状态机和前端
### 问题 3配网失败后无法恢复
排查:
- NVS 清除逻辑是否真正执行
- 内存态缓存是否同时清空
- 配网状态是否回到 `provisioning`
---
## 七、测试清单(可复用)
建议每次迭代最少覆盖:
1. 首次配网成功
2. 密码错误后重试成功
3. 连接旧网状态下切换新网成功
4. 清除配置后重新配网成功
5. 重启后自动重连
6. 空闲超时与手动停止路径可用
这 6 条通过后,组件稳定性通常会显著提升。
---
## 八、可继续增强的方向
- 配网页面安全增强(鉴权/会话)
- 多语言提示
- 更细粒度错误码映射
- BLE 辅助配网
- 命令行/远程维护接口联动
---
## 结语
配网组件的核心价值,不是“让设备连上一次网”,而是:
- 功能完整
- 异常可恢复
- 排障可落地
当这三件事做好后,它才是一个能复用、能维护、能上线的基础能力组件。

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "wifi-connect.c"
INCLUDE_DIRS "include"
REQUIRES esp_wifi esp_timer esp_event esp_netif nvs_flash esp_http_server lwip driver
)

View File

@@ -0,0 +1,87 @@
menu "WiFi Connect"
choice WIFI_CONNECT_PROVISION_MODE
prompt "Provisioning mode"
default WIFI_CONNECT_PROVISION_MODE_BUTTON
help
Select how provisioning mode is entered.
config WIFI_CONNECT_PROVISION_MODE_BUTTON
bool "Button triggered (default)"
help
Enter provisioning only when button long-press is detected.
config WIFI_CONNECT_PROVISION_MODE_ALWAYS_ON
bool "Always-on provisioning"
help
Start provisioning automatically on boot and keep it running.
Provisioning will not auto-stop after idle timeout or STA connect.
endchoice
config WIFI_CONNECT_BUTTON_GPIO
int "Provision button GPIO"
range 0 21
default 9
help
GPIO used for entering provisioning mode.
config WIFI_CONNECT_BUTTON_ACTIVE_LEVEL
int "Provision button active level"
range 0 1
default 0
help
Active level of provisioning button. 0 means active-low.
config WIFI_CONNECT_DEBOUNCE_MS
int "Button debounce time (ms)"
range 10 200
default 40
config WIFI_CONNECT_LONG_PRESS_MS
int "Button long press trigger time (ms)"
range 500 10000
default 2000
config WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS
int "Button startup guard time (ms)"
range 0 30000
default 5000
help
Ignore button long-press detection during startup guard window.
Useful when button pin is shared with other peripherals.
config WIFI_CONNECT_BUTTON_RELEASE_ARM_MS
int "Button release arm time (ms)"
range 20 2000
default 200
help
Require button to stay in released level for this duration
before long-press detection is armed.
config WIFI_CONNECT_CONNECT_TIMEOUT_SEC
int "Wi-Fi connect timeout (sec)"
range 5 180
default 30
config WIFI_CONNECT_IDLE_TIMEOUT_SEC
int "Provisioning idle timeout (sec)"
range 30 1800
default 300
config WIFI_CONNECT_MAX_SCAN_RESULTS
int "Maximum scan results"
range 5 50
default 20
config WIFI_CONNECT_AP_MAX_CONNECTIONS
int "SoftAP max connections"
range 1 10
default 4
config WIFI_CONNECT_AP_GRACEFUL_STOP_SEC
int "AP keep-alive after STA connected (sec)"
range 0 120
default 15
endmenu

View File

@@ -0,0 +1,28 @@
# BotanicalBuddy 配网四步卡(张贴版)
## 第 1 步:长按按键
长按设备配网键约 2 秒,进入配网模式。
## 第 2 步:连接热点
手机连接 Wi-FiESP32-xxxxxx
## 第 3 步:打开页面
浏览器访问http://192.168.4.1
## 第 4 步:提交路由器信息
选择家里 Wi-Fi输入密码点击“连接”。
---
## 失败时先做这三件事
1. 确认访问的是 http://192.168.4.1(不是 https
2. 确认输入的 Wi-Fi 密码正确
3. 路由器使用 2.4G,设备离路由器更近一点再试
---
## 二维码占位(贴纸用)
建议制作一个二维码,内容指向:
http://192.168.4.1
打印时把二维码贴在本段下方空白区域即可。

View File

@@ -0,0 +1,165 @@
# wifi-connect 组件说明
`wifi-connect` 是一个基于 ESP-IDF 的 Wi-Fi 配网组件,支持:
- 长按按键进入配网模式
- 启动 SoftAP + Captive Portal网页配网
- 手机连接热点后,通过网页扫描并选择路由器
- 保存 Wi-Fi 凭据到 NVS
- 下次开机自动重连
- 支持两种配网模式:按键触发 / 常驻配网
面向最终用户的一页版操作说明见:`USER_GUIDE.md`
现场打印张贴版(四步卡)见:`QUICK_POSTER.md`
---
## 目录结构
- `wifi-connect.c`组件主实现按键、APSTA、HTTP、DNS、状态机
- `include/wifi-connect.h`:对外 API
- `Kconfig.projbuild`:组件配置项
- `CMakeLists.txt`:组件构建依赖
---
## 对外 API
头文件:`include/wifi-connect.h`
- `esp_err_t wifi_connect_init(void);`
- 初始化组件NVS、Wi-Fi、事件、按键任务等
- 尝试自动连接已保存网络
- `esp_err_t wifi_connect_start(void);`
- 启动配网APSTA + HTTP + DNS
- `esp_err_t wifi_connect_stop(void);`
- 停止配网(关闭热点与相关服务)
- `wifi_connect_status_t wifi_connect_get_status(void);`
- 获取当前状态:`idle / provisioning / connecting / connected / failed / timeout`
- `esp_err_t wifi_connect_get_config(wifi_connect_config_t *config);`
- 读取已保存的 Wi-Fi 凭据
- `esp_err_t wifi_connect_clear_config(void);`
- 清除已保存的 Wi-Fi 凭据SSID/密码)
---
## 快速使用
`main/main.c`
```c
#include "esp_check.h"
#include "wifi-connect.h"
void app_main(void)
{
ESP_ERROR_CHECK(wifi_connect_init());
}
```
运行后:
1. 选择配网模式:
- 按键触发模式:长按配置按键进入配网
- 常驻配网模式:上电自动进入配网
2. 手机连接 `ESP32-xxxxxx` 热点
3. 打开 `http://192.168.4.1`
4. 选择 Wi-Fi 并输入密码提交
5. 配网行为:
- 按键触发模式:连接成功后按配置自动关闭热点
- 常驻配网模式:配网热点保持开启,不自动关闭
如需清空历史凭据,可在配网页面点击“清除已保存”。
---
## Kconfig 配置项
`idf.py menuconfig` 中:`WiFi Connect` 菜单
- `Provisioning mode`:配网模式(二选一)
- `Button triggered`:按键触发配网(默认)
- `Always-on provisioning`:常驻配网(上电自动进入且不自动关闭)
- `WIFI_CONNECT_BUTTON_GPIO`:进入配网的按键 GPIO
- `WIFI_CONNECT_BUTTON_ACTIVE_LEVEL`:按键有效电平
- `WIFI_CONNECT_DEBOUNCE_MS`:按键去抖时间
- `WIFI_CONNECT_LONG_PRESS_MS`:长按触发时长
- `WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS`:上电保护窗口(该时间内忽略长按检测)
- `WIFI_CONNECT_BUTTON_RELEASE_ARM_MS`:松手解锁时间(先稳定松手再允许长按触发)
- `WIFI_CONNECT_CONNECT_TIMEOUT_SEC`:连接路由器超时
- `WIFI_CONNECT_IDLE_TIMEOUT_SEC`:配网页面空闲超时
- `WIFI_CONNECT_MAX_SCAN_RESULTS`:扫描网络最大数量
- `WIFI_CONNECT_AP_MAX_CONNECTIONS`SoftAP 最大连接数
- `WIFI_CONNECT_AP_GRACEFUL_STOP_SEC`:联网成功后 AP 延迟关闭秒数
---
## 日志与状态说明(中文)
组件会输出统一中文状态日志,例如:
- `【状态】wifi-connect 初始化完成`
- `【状态】检测到按键长按:开始进入配网模式`
- `【状态】配网已启动配网热点已开启SSID=...`
- `【状态】开始连接路由器:收到配网请求,目标网络=...`
- `【状态】联网成功:已连接 ...,获取 IP=...`
- `【状态】配网已停止:热点已关闭,设备继续以 STA 模式运行`
说明ESP-IDF 驱动层(如 `wifi:``esp_netif_lwip:`)仍会输出英文日志,这是框架默认行为。
---
## 常见问题
### 1) 手机连上热点但不自动弹出页面
- 手动访问:`http://192.168.4.1`
- 确认手机没有强制使用 HTTPS
- 查看串口是否有 `配网已启动``DNS 劫持服务已启动` 日志
### 2) 提交后连接失败
- 检查密码是否正确
- 查看日志中的失败原因码(`连接失败,原因=...`
- 检查路由器是否禁用了新设备接入
- 若曾保存过旧配置,可先在页面点击“清除已保存”后再重试
### 4) 按键未按下却误触发配网
- 常见原因是按键引脚与 LCD/外设复用,初始化期间电平抖动被误判为长按
- 可增大 `WIFI_CONNECT_BUTTON_STARTUP_GUARD_MS`(如 8000~10000
- 可增大 `WIFI_CONNECT_BUTTON_RELEASE_ARM_MS`(如 300~500
- 若硬件允许,优先给配网按键使用独立 GPIO
### 5) 成功后热点消失是否正常
- 在按键触发模式下:正常,可通过 `WIFI_CONNECT_AP_GRACEFUL_STOP_SEC` 调整关闭延时
- 在常驻配网模式下:热点不会自动关闭
---
## 依赖
`CMakeLists.txt` 声明:
- `esp_wifi`
- `esp_timer`
- `esp_event`
- `esp_netif`
- `nvs_flash`
- `esp_http_server`
- `lwip`
- `driver`
---
## 版本建议
- 推荐 ESP-IDF `v5.5.x`
- 当前项目验证环境:`esp-idf v5.5.2`ESP32-C3

View File

@@ -0,0 +1,82 @@
# BotanicalBuddy 配网操作手册(用户版)
> 适用对象:现场测试、家人用户、非开发人员
> 目标:让设备快速连上家里 Wi-Fi
---
## 1. 开始前准备
- 手机已打开 Wi-Fi
- 记住家里 Wi-Fi 名称和密码
- 设备已上电
---
## 2. 进入配网模式
有两种工作模式,请按项目配置使用:
1. 按键触发模式:长按设备上的配网按键(约 2 秒)
2. 常驻配网模式:设备上电后会自动开启配网
3. 当看到“配网已启动”后,手机 Wi-Fi 列表会出现:`ESP32-xxxxxx`
---
## 3. 手机连接设备热点
1. 在手机 Wi-Fi 中连接 `ESP32-xxxxxx`
2. 若系统自动弹出页面,直接进入下一步
3. 若没有自动弹出,手动打开浏览器输入:
`http://192.168.4.1`
> 注意:必须是 `http`,不要用 `https`
---
## 4. 选择路由器并提交
1. 点击“扫描网络”
2. 选择你家的 Wi-Fi
3. 输入 Wi-Fi 密码
4. 点击“连接”
成功后页面会提示连接成功:
- 按键触发模式:设备热点会在几秒后自动关闭(正常现象)
- 常驻配网模式:设备热点保持开启(正常现象)
---
## 5. 成功后的现象
- 设备不再广播 `ESP32-xxxxxx`
- 串口会显示“联网成功,获取 IP=... ”
- 设备进入正常工作状态
---
## 6. 常见问题
### Q1手机连上热点但打不开页面
- 手动访问:`http://192.168.4.1`
- 关闭手机“私人 DNS / 智能网络切换 / VPN”后重试
- 确认没有强制跳 HTTPS
### Q2提示连接失败
- 检查 Wi-Fi 密码是否正确
- 确认路由器 2.4G 可用ESP32-C3 使用 2.4G
- 路由器信号太弱时,靠近路由器后重试
### Q3配网成功后热点消失了
- 这是正常设计:设备连上路由器后会自动关闭配网热点
---
## 7. 一句话速记
长按按键 → 连 `ESP32-xxxxxx` → 打开 `http://192.168.4.1` → 选 Wi-Fi + 输密码 → 等待成功提示。

View File

@@ -0,0 +1,34 @@
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
WIFI_CONNECT_STATUS_IDLE = 0,
WIFI_CONNECT_STATUS_PROVISIONING,
WIFI_CONNECT_STATUS_CONNECTING,
WIFI_CONNECT_STATUS_CONNECTED,
WIFI_CONNECT_STATUS_FAILED,
WIFI_CONNECT_STATUS_TIMEOUT,
} wifi_connect_status_t;
typedef struct {
bool has_config;
char ssid[33];
char password[65];
} wifi_connect_config_t;
esp_err_t wifi_connect_init(void);
esp_err_t wifi_connect_start(void);
esp_err_t wifi_connect_stop(void);
wifi_connect_status_t wifi_connect_get_status(void);
esp_err_t wifi_connect_get_config(wifi_connect_config_t *config);
esp_err_t wifi_connect_clear_config(void);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff