From 4c8d40ab2f6033babee2fca5a4148efc013330ca Mon Sep 17 00:00:00 2001 From: Wang Beihong Date: Fri, 6 Mar 2026 14:12:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=B5=E5=AE=B9?= =?UTF-8?q?=E5=BC=8F=E5=9C=9F=E5=A3=A4=E6=B9=BF=E5=BA=A6=E4=BC=A0=E6=84=9F?= =?UTF-8?q?=E5=99=A8=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=91=BD=E4=BB=A4=E5=92=8C=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CMakeLists.txt | 3 + .../capactive_soil_moisture_sensor_V2.0.c | 172 ++++++++++++++++++ .../capactive_soil_moisture_sensor_V2.0.h | 48 +++++ components/console_user_cmds/CMakeLists.txt | 2 +- .../console_user_cmds/console_user_cmds.c | 66 +++++++ main/CMakeLists.txt | 2 +- main/main.c | 123 +++++++++++-- 7 files changed, 402 insertions(+), 14 deletions(-) create mode 100644 components/capactive_soil_moisture_sensor_V2.0/CMakeLists.txt create mode 100644 components/capactive_soil_moisture_sensor_V2.0/capactive_soil_moisture_sensor_V2.0.c create mode 100644 components/capactive_soil_moisture_sensor_V2.0/include/capactive_soil_moisture_sensor_V2.0.h diff --git a/components/capactive_soil_moisture_sensor_V2.0/CMakeLists.txt b/components/capactive_soil_moisture_sensor_V2.0/CMakeLists.txt new file mode 100644 index 0000000..66660cd --- /dev/null +++ b/components/capactive_soil_moisture_sensor_V2.0/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "capactive_soil_moisture_sensor_V2.0.c" + INCLUDE_DIRS "include" + REQUIRES esp_adc) diff --git a/components/capactive_soil_moisture_sensor_V2.0/capactive_soil_moisture_sensor_V2.0.c b/components/capactive_soil_moisture_sensor_V2.0/capactive_soil_moisture_sensor_V2.0.c new file mode 100644 index 0000000..05c7a3d --- /dev/null +++ b/components/capactive_soil_moisture_sensor_V2.0/capactive_soil_moisture_sensor_V2.0.c @@ -0,0 +1,172 @@ +#include +#include + +#include "esp_adc/adc_cali.h" +#include "esp_adc/adc_cali_scheme.h" +#include "esp_check.h" +#include "esp_log.h" + +#include "capactive_soil_moisture_sensor_V2.0.h" + +static const char *TAG = "cap_soil_v2"; + +static adc_oneshot_unit_handle_t s_adc_handle = NULL; +static adc_cali_handle_t s_cali_handle = NULL; +static bool s_cali_enabled = false; +static bool s_inited = false; + +static cap_soil_sensor_config_t s_cfg = { + .unit = CAP_SOIL_SENSOR_DEFAULT_UNIT, + .channel = CAP_SOIL_SENSOR_DEFAULT_CHANNEL, + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_DEFAULT, + .air_raw = 620, + .water_raw = 308, +}; + +static float map_raw_to_percent(int raw) +{ + // 教程经验:湿度与输出值成反比,air_raw(干) > water_raw(湿)。 + int span = s_cfg.air_raw - s_cfg.water_raw; + if (span <= 0) { + return 0.0f; + } + + float percent = ((float)(s_cfg.air_raw - raw) * 100.0f) / (float)span; + if (percent < 0.0f) { + percent = 0.0f; + } + if (percent > 100.0f) { + percent = 100.0f; + } + return percent; +} + +static cap_soil_level_t map_percent_to_level(float percent) +{ + // 三段划分:0~33 干燥,34~66 湿润,67~100 非常潮湿。 + if (percent < 33.34f) { + return CAP_SOIL_LEVEL_DRY; + } + if (percent < 66.67f) { + return CAP_SOIL_LEVEL_MOIST; + } + return CAP_SOIL_LEVEL_WET; +} + +static esp_err_t try_create_adc_calibration(const cap_soil_sensor_config_t *cfg) +{ +#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED + adc_cali_curve_fitting_config_t cali_cfg = { + .unit_id = cfg->unit, + .chan = cfg->channel, + .atten = cfg->atten, + .bitwidth = cfg->bitwidth, + }; + esp_err_t ret = adc_cali_create_scheme_curve_fitting(&cali_cfg, &s_cali_handle); + if (ret == ESP_OK) { + s_cali_enabled = true; + ESP_LOGI(TAG, "ADC calibration enabled"); + return ESP_OK; + } + ESP_LOGW(TAG, "ADC calibration unavailable: %s", esp_err_to_name(ret)); + return ret; +#else + (void)cfg; + ESP_LOGW(TAG, "ADC calibration scheme not supported on this target"); + return ESP_ERR_NOT_SUPPORTED; +#endif +} + +esp_err_t cap_soil_sensor_init(const cap_soil_sensor_config_t *config) +{ + if (s_inited) { + return ESP_OK; + } + + if (config != NULL) { + s_cfg = *config; + } + + adc_oneshot_unit_init_cfg_t unit_cfg = { + .unit_id = s_cfg.unit, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + ESP_RETURN_ON_ERROR(adc_oneshot_new_unit(&unit_cfg, &s_adc_handle), TAG, "adc unit init failed"); + + adc_oneshot_chan_cfg_t chan_cfg = { + .atten = s_cfg.atten, + .bitwidth = s_cfg.bitwidth, + }; + esp_err_t ret = adc_oneshot_config_channel(s_adc_handle, s_cfg.channel, &chan_cfg); + if (ret != ESP_OK) { + adc_oneshot_del_unit(s_adc_handle); + s_adc_handle = NULL; + return ret; + } + + (void)try_create_adc_calibration(&s_cfg); + s_inited = true; + ESP_LOGI(TAG, + "sensor init ok (unit=%d, ch=%d, air_raw=%d, water_raw=%d)", + (int)s_cfg.unit, + (int)s_cfg.channel, + s_cfg.air_raw, + s_cfg.water_raw); + return ESP_OK; +} + +esp_err_t cap_soil_sensor_read(cap_soil_sensor_data_t *out_data) +{ + ESP_RETURN_ON_FALSE(out_data != NULL, ESP_ERR_INVALID_ARG, TAG, "out_data is null"); + ESP_RETURN_ON_FALSE(s_inited && s_adc_handle != NULL, ESP_ERR_INVALID_STATE, TAG, "sensor not initialized"); + + int raw = 0; + ESP_RETURN_ON_ERROR(adc_oneshot_read(s_adc_handle, s_cfg.channel, &raw), TAG, "adc read failed"); + + int mv = -1; + if (s_cali_enabled && s_cali_handle != NULL) { + if (adc_cali_raw_to_voltage(s_cali_handle, raw, &mv) != ESP_OK) { + mv = -1; + } + } + + float moisture = map_raw_to_percent(raw); + out_data->raw = raw; + out_data->voltage_mv = mv; + out_data->moisture_percent = moisture; + out_data->level = map_percent_to_level(moisture); + + return ESP_OK; +} + +esp_err_t cap_soil_sensor_set_calibration(int air_raw, int water_raw) +{ + ESP_RETURN_ON_FALSE(air_raw > water_raw, ESP_ERR_INVALID_ARG, TAG, "need air_raw > water_raw"); + s_cfg.air_raw = air_raw; + s_cfg.water_raw = water_raw; + return ESP_OK; +} + +esp_err_t cap_soil_sensor_deinit(void) +{ + if (!s_inited) { + return ESP_OK; + } + + if (s_cali_enabled && s_cali_handle != NULL) { +#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED + adc_cali_delete_scheme_curve_fitting(s_cali_handle); +#endif + s_cali_handle = NULL; + s_cali_enabled = false; + } + + if (s_adc_handle != NULL) { + ESP_RETURN_ON_ERROR(adc_oneshot_del_unit(s_adc_handle), TAG, "adc unit delete failed"); + s_adc_handle = NULL; + } + + s_inited = false; + return ESP_OK; +} diff --git a/components/capactive_soil_moisture_sensor_V2.0/include/capactive_soil_moisture_sensor_V2.0.h b/components/capactive_soil_moisture_sensor_V2.0/include/capactive_soil_moisture_sensor_V2.0.h new file mode 100644 index 0000000..3ca1521 --- /dev/null +++ b/components/capactive_soil_moisture_sensor_V2.0/include/capactive_soil_moisture_sensor_V2.0.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include "esp_adc/adc_oneshot.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// 你当前接在 GPIO0(ESP32-C3 对应 ADC1_CH0),这里作为默认值。 +#define CAP_SOIL_SENSOR_DEFAULT_UNIT ADC_UNIT_1 +#define CAP_SOIL_SENSOR_DEFAULT_CHANNEL ADC_CHANNEL_0 + +typedef enum { + CAP_SOIL_LEVEL_DRY = 0, + CAP_SOIL_LEVEL_MOIST, + CAP_SOIL_LEVEL_WET, +} cap_soil_level_t; + +typedef struct { + adc_unit_t unit; + adc_channel_t channel; + adc_atten_t atten; + adc_bitwidth_t bitwidth; + + // 标定值:空气中读数(干)通常更大,水中读数(湿)通常更小。 + int air_raw; + int water_raw; +} cap_soil_sensor_config_t; + +typedef struct { + int raw; + int voltage_mv; + float moisture_percent; + cap_soil_level_t level; +} cap_soil_sensor_data_t; + +esp_err_t cap_soil_sensor_init(const cap_soil_sensor_config_t *config); +esp_err_t cap_soil_sensor_read(cap_soil_sensor_data_t *out_data); +esp_err_t cap_soil_sensor_set_calibration(int air_raw, int water_raw); +esp_err_t cap_soil_sensor_deinit(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/console_user_cmds/CMakeLists.txt b/components/console_user_cmds/CMakeLists.txt index e1f6bd7..e4b92b1 100644 --- a/components/console_user_cmds/CMakeLists.txt +++ b/components/console_user_cmds/CMakeLists.txt @@ -1,3 +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) + REQUIRES console_simple_init console i2c_master_messager io_device_control wifi-connect capactive_soil_moisture_sensor_V2.0) diff --git a/components/console_user_cmds/console_user_cmds.c b/components/console_user_cmds/console_user_cmds.c index 7f3540c..c40a23b 100644 --- a/components/console_user_cmds/console_user_cmds.c +++ b/components/console_user_cmds/console_user_cmds.c @@ -1,9 +1,11 @@ #include +#include #include #include "esp_check.h" #include "console_simple_init.h" #include "console_user_cmds.h" +#include "capactive_soil_moisture_sensor_V2.0.h" #include "i2c_master_messager.h" #include "io_device_control.h" #include "wifi-connect.h" @@ -28,6 +30,20 @@ static const char *wifi_status_to_str(wifi_connect_status_t status) } } +static const char *soil_level_to_str(cap_soil_level_t level) +{ + switch (level) { + case CAP_SOIL_LEVEL_DRY: + return "dry"; + case CAP_SOIL_LEVEL_MOIST: + return "moist"; + case CAP_SOIL_LEVEL_WET: + return "wet"; + default: + return "unknown"; + } +} + // hello: 最小可用命令,用于验证 console 链路是否正常。 static int cmd_hello(int argc, char **argv) { @@ -182,6 +198,48 @@ static int cmd_wifi(int argc, char **argv) return 1; } +// soil: 查询土壤湿度,或动态更新空气/水中标定值。 +static int cmd_soil(int argc, char **argv) +{ + if (argc < 2 || strcmp(argv[1], "read") == 0) { + cap_soil_sensor_data_t data = {0}; + esp_err_t ret = cap_soil_sensor_read(&data); + if (ret != ESP_OK) { + printf("soil read failed: %s\n", esp_err_to_name(ret)); + return 1; + } + + printf("soil raw=%d, mv=%d, moisture=%.1f%%, level=%s\n", + data.raw, + data.voltage_mv, + data.moisture_percent, + soil_level_to_str(data.level)); + return 0; + } + + if (strcmp(argv[1], "cal") == 0) { + if (argc < 4) { + printf("usage: soil cal \n"); + return 1; + } + + int air_raw = (int)strtol(argv[2], NULL, 10); + int water_raw = (int)strtol(argv[3], NULL, 10); + esp_err_t ret = cap_soil_sensor_set_calibration(air_raw, water_raw); + if (ret != ESP_OK) { + printf("soil cal failed: %s\n", esp_err_to_name(ret)); + printf("hint: air_raw should be greater than water_raw\n"); + return 1; + } + + printf("soil calibration updated: air=%d, water=%d\n", air_raw, water_raw); + return 0; + } + + printf("usage: soil >\n"); + return 1; +} + esp_err_t console_user_cmds_register(void) { const esp_console_cmd_t hello_cmd = { @@ -222,5 +280,13 @@ esp_err_t console_user_cmds_register(void) }; ESP_RETURN_ON_ERROR(esp_console_cmd_register(&wifi_cmd), "console_user_cmds", "register wifi failed"); + const esp_console_cmd_t soil_cmd = { + .command = "soil", + .help = "土壤湿度读取与标定。用法: soil >", + .hint = "", + .func = cmd_soil, + }; + ESP_RETURN_ON_ERROR(esp_console_cmd_register(&soil_cmd), "console_user_cmds", "register soil failed"); + return ESP_OK; } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index d60eec1..5eaad13 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRCS "main.c" INCLUDE_DIRS "." - REQUIRES wifi-connect esp_lvgl_port lvgl_st7735s_use i2c_master_messager io_device_control console_simple_init console console_user_cmds + REQUIRES wifi-connect esp_lvgl_port lvgl_st7735s_use i2c_master_messager io_device_control console_simple_init console console_user_cmds capactive_soil_moisture_sensor_V2.0 ) diff --git a/main/main.c b/main/main.c index e1caac4..0404f32 100755 --- a/main/main.c +++ b/main/main.c @@ -11,6 +11,7 @@ #include "io_device_control.h" #include "console_simple_init.h" // 提供 console_cmd_user_register 和 console_cmd_all_register #include "console_user_cmds.h" +#include "capactive_soil_moisture_sensor_V2.0.h" #ifndef CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE #define CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE 0 @@ -113,6 +114,27 @@ void app_main(void) i2c_ready = true; } + // 初始化电容式土壤湿度传感器(GPIO0 / ADC1_CH0)。 + bool soil_ready = false; + cap_soil_sensor_config_t soil_cfg = { + .unit = CAP_SOIL_SENSOR_DEFAULT_UNIT, + .channel = CAP_SOIL_SENSOR_DEFAULT_CHANNEL, + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_DEFAULT, + // 标定值来自当前实测:空气中约 3824,水中约 1463。 + .air_raw = 3824, + .water_raw = 1463, + }; + ret = cap_soil_sensor_init(&soil_cfg); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "土壤湿度传感器初始化失败: %s", esp_err_to_name(ret)); + } + else + { + soil_ready = true; + } + // 按需求:仅在 Wi-Fi 确认连通后再初始化 console。 wait_for_wifi_connected(); @@ -123,6 +145,13 @@ void app_main(void) for (;;) { + cap_soil_sensor_data_t soil_data = {0}; + bool soil_ok = false; + if (soil_ready && cap_soil_sensor_read(&soil_data) == ESP_OK) + { + soil_ok = true; + } + i2c_master_messager_data_t sensor_data = {0}; if (i2c_ready && i2c_master_messager_get_data(&sensor_data) == ESP_OK) { @@ -131,26 +160,71 @@ void app_main(void) if (BOTANY_BH1750_ENABLE && BOTANY_AHT30_ENABLE && sensor_data.bh1750.valid && sensor_data.aht30.valid) { - snprintf(text, - sizeof(text), - "L:%.0f T:%.1fC\nH:%.1f%%", - sensor_data.bh1750.lux, - sensor_data.aht30.temperature_c, - sensor_data.aht30.humidity_rh); + if (soil_ok) + { + snprintf(text, + sizeof(text), + "L:%.0f S:%.0f%%\nT:%.1f H:%.1f%%", + sensor_data.bh1750.lux, + soil_data.moisture_percent, + sensor_data.aht30.temperature_c, + sensor_data.aht30.humidity_rh); + } + else + { + snprintf(text, + sizeof(text), + "L:%.0f T:%.1fC\nH:%.1f%%", + sensor_data.bh1750.lux, + sensor_data.aht30.temperature_c, + sensor_data.aht30.humidity_rh); + } ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text)); } else if (BOTANY_BH1750_ENABLE && sensor_data.bh1750.valid) { - snprintf(text, sizeof(text), "Light: %.1f lx", sensor_data.bh1750.lux); + if (soil_ok) + { + snprintf(text, + sizeof(text), + "Light:%.0f\nSoil:%.0f%%", + sensor_data.bh1750.lux, + soil_data.moisture_percent); + } + else + { + snprintf(text, sizeof(text), "Light: %.1f lx", sensor_data.bh1750.lux); + } ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text)); } else if (BOTANY_AHT30_ENABLE && sensor_data.aht30.valid) + { + if (soil_ok) + { + snprintf(text, + sizeof(text), + "T:%.1f H:%.1f%%\nSoil:%.0f%%", + sensor_data.aht30.temperature_c, + sensor_data.aht30.humidity_rh, + soil_data.moisture_percent); + } + else + { + snprintf(text, + sizeof(text), + "T:%.1fC\nH:%.1f%%", + sensor_data.aht30.temperature_c, + sensor_data.aht30.humidity_rh); + } + ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text)); + } + else if (soil_ok) { snprintf(text, sizeof(text), - "T:%.1fC\nH:%.1f%%", - sensor_data.aht30.temperature_c, - sensor_data.aht30.humidity_rh); + "Soil:%.0f%%\nADC:%d", + soil_data.moisture_percent, + soil_data.raw); ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text)); } else @@ -160,11 +234,36 @@ void app_main(void) } else if (i2c_ready) { - ESP_ERROR_CHECK(lvgl_st7735s_set_center_text("Sensor read fail")); + if (soil_ok) + { + char text[64] = {0}; + snprintf(text, + sizeof(text), + "I2C read fail\nSoil:%.0f%%", + soil_data.moisture_percent); + ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text)); + } + else + { + ESP_ERROR_CHECK(lvgl_st7735s_set_center_text("Sensor read fail")); + } } else { - vTaskDelay(pdMS_TO_TICKS(2000)); + if (soil_ok) + { + char text[64] = {0}; + snprintf(text, + sizeof(text), + "Soil:%.0f%%\nADC:%d", + soil_data.moisture_percent, + soil_data.raw); + ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text)); + } + else + { + vTaskDelay(pdMS_TO_TICKS(2000)); + } } vTaskDelay(pdMS_TO_TICKS(1000));