feat: 添加电容式土壤湿度传感器支持,更新相关命令和初始化逻辑

This commit is contained in:
Wang Beihong
2026-03-06 14:12:10 +08:00
parent 132118c786
commit 4c8d40ab2f
7 changed files with 402 additions and 14 deletions

View File

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

View File

@@ -0,0 +1,172 @@
#include <stdbool.h>
#include <stdio.h>
#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;
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_adc/adc_oneshot.h"
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
// 你当前接在 GPIO0ESP32-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

View File

@@ -1,3 +1,3 @@
idf_component_register(SRCS "console_user_cmds.c" idf_component_register(SRCS "console_user_cmds.c"
INCLUDE_DIRS "include" 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)

View File

@@ -1,9 +1,11 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "esp_check.h" #include "esp_check.h"
#include "console_simple_init.h" #include "console_simple_init.h"
#include "console_user_cmds.h" #include "console_user_cmds.h"
#include "capactive_soil_moisture_sensor_V2.0.h"
#include "i2c_master_messager.h" #include "i2c_master_messager.h"
#include "io_device_control.h" #include "io_device_control.h"
#include "wifi-connect.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 链路是否正常。 // hello: 最小可用命令,用于验证 console 链路是否正常。
static int cmd_hello(int argc, char **argv) static int cmd_hello(int argc, char **argv)
{ {
@@ -182,6 +198,48 @@ static int cmd_wifi(int argc, char **argv)
return 1; 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 <air_raw> <water_raw>\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 <read|cal <air_raw> <water_raw>>\n");
return 1;
}
esp_err_t console_user_cmds_register(void) esp_err_t console_user_cmds_register(void)
{ {
const esp_console_cmd_t hello_cmd = { 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"); 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 <read|cal <air_raw> <water_raw>>",
.hint = "<read|cal>",
.func = cmd_soil,
};
ESP_RETURN_ON_ERROR(esp_console_cmd_register(&soil_cmd), "console_user_cmds", "register soil failed");
return ESP_OK; return ESP_OK;
} }

View File

@@ -1,4 +1,4 @@
idf_component_register(SRCS "main.c" idf_component_register(SRCS "main.c"
INCLUDE_DIRS "." 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
) )

View File

@@ -11,6 +11,7 @@
#include "io_device_control.h" #include "io_device_control.h"
#include "console_simple_init.h" // 提供 console_cmd_user_register 和 console_cmd_all_register #include "console_simple_init.h" // 提供 console_cmd_user_register 和 console_cmd_all_register
#include "console_user_cmds.h" #include "console_user_cmds.h"
#include "capactive_soil_moisture_sensor_V2.0.h"
#ifndef CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE #ifndef CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE
#define CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE 0 #define CONFIG_I2C_MASTER_MESSAGER_BH1750_ENABLE 0
@@ -113,6 +114,27 @@ void app_main(void)
i2c_ready = true; 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。 // 按需求:仅在 Wi-Fi 确认连通后再初始化 console。
wait_for_wifi_connected(); wait_for_wifi_connected();
@@ -123,6 +145,13 @@ void app_main(void)
for (;;) 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}; i2c_master_messager_data_t sensor_data = {0};
if (i2c_ready && i2c_master_messager_get_data(&sensor_data) == ESP_OK) 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 && if (BOTANY_BH1750_ENABLE && BOTANY_AHT30_ENABLE &&
sensor_data.bh1750.valid && sensor_data.aht30.valid) sensor_data.bh1750.valid && sensor_data.aht30.valid)
{ {
snprintf(text, if (soil_ok)
sizeof(text), {
"L:%.0f T:%.1fC\nH:%.1f%%", snprintf(text,
sensor_data.bh1750.lux, sizeof(text),
sensor_data.aht30.temperature_c, "L:%.0f S:%.0f%%\nT:%.1f H:%.1f%%",
sensor_data.aht30.humidity_rh); 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)); ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text));
} }
else if (BOTANY_BH1750_ENABLE && sensor_data.bh1750.valid) 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)); ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text));
} }
else if (BOTANY_AHT30_ENABLE && sensor_data.aht30.valid) 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, snprintf(text,
sizeof(text), sizeof(text),
"T:%.1fC\nH:%.1f%%", "Soil:%.0f%%\nADC:%d",
sensor_data.aht30.temperature_c, soil_data.moisture_percent,
sensor_data.aht30.humidity_rh); soil_data.raw);
ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text)); ESP_ERROR_CHECK(lvgl_st7735s_set_center_text(text));
} }
else else
@@ -160,11 +234,36 @@ void app_main(void)
} }
else if (i2c_ready) 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 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)); vTaskDelay(pdMS_TO_TICKS(1000));