diff --git a/components/embed_claw/CMakeLists.txt b/components/embed_claw/CMakeLists.txt index 7239ccf..150a4b1 100644 --- a/components/embed_claw/CMakeLists.txt +++ b/components/embed_claw/CMakeLists.txt @@ -9,6 +9,7 @@ idf_component_register( "." REQUIRES driver + esp_adc esp_http_client esp_http_server esp_netif diff --git a/components/embed_claw/tools/ec_tools_reg.inc b/components/embed_claw/tools/ec_tools_reg.inc index f243479..d08a93b 100644 --- a/components/embed_claw/tools/ec_tools_reg.inc +++ b/components/embed_claw/tools/ec_tools_reg.inc @@ -10,4 +10,8 @@ EC_TOOLS_REG(cron_add) EC_TOOLS_REG(cron_list) EC_TOOLS_REG(cron_remove) EC_TOOLS_REG(web_search) +EC_TOOLS_REG(bh1750) +EC_TOOLS_REG(fill_light) +EC_TOOLS_REG(water_ion_temp_uart) +EC_TOOLS_REG(ntu_test_adc) diff --git a/components/embed_claw/tools/tools_bh1750.c b/components/embed_claw/tools/tools_bh1750.c new file mode 100644 index 0000000..daa185f --- /dev/null +++ b/components/embed_claw/tools/tools_bh1750.c @@ -0,0 +1,186 @@ +/** + * @file ec_tool_bh1750.c + * @author your_name (your_email@example.com) + * @brief BH1750 light sensor tool implementation + * @version 0.1 + * @date 2026-03-21 + * + * @copyright Copyright (c) 2026, Wireless-Tag. All rights reserved. + * + */ + +/* ==================== [Includes] ========================================== */ + +#include "core/ec_tools.h" + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_err.h" +#include "driver/i2c.h" + + +/* ==================== [Defines] =========================================== */ + +#define TAG "ec_tool_bh1750" + +/* 定义 BH1750 的 I2C 地址与指令 */ +#define BH1750_I2C_ADDR 0x23 +#define BH1750_CMD_PWR_ON 0x01 /* 打开模块等待测量指令 */ +#define BH1750_CMD_ONE_TIME_H_RES 0x20 /* 一次高分辨率模式,测量后自动转入 PowerDown */ +#define BH1750_MEASURE_TIME_MS 180 /* 高分辨率模式典型转换时间 */ +#define BH1750_DIVISOR 1.2f /* 光照强度计算系数 */ + +/* I2C 主机参数 */ +#define I2C_MASTER_SCL_IO 8 /*!< 根据实际接线的 SCL 引脚修改 */ +#define I2C_MASTER_SDA_IO 9 /*!< 根据实际接线的 SDA 引脚修改 */ +#define I2C_MASTER_NUM 0 /*!< I2C 端口号 */ +#define I2C_MASTER_FREQ_HZ 100000 /*!< I2C 时钟频率 100KHz */ +#define I2C_MASTER_TIMEOUT_MS 1000 + +/* ==================== [Typedefs] ========================================== */ + +/* ==================== [Static Prototypes] ================================= */ + +static esp_err_t i2c_master_init(void); +static esp_err_t ec_tool_bh1750_execute(const char *input_json, char *output, size_t output_size); + +/* ==================== [Static Variables] ================================== */ + +static const ec_tools_t s_bh1750_tool = { + .name = "read_light_intensity", + .description = "Read the current ambient light intensity in lux from the BH1750 sensor.", + .input_schema_json = "{\"type\":\"object\",\"properties\":{},\"required\":[]}", + .execute = ec_tool_bh1750_execute, +}; + +/* ==================== [Macros] ============================================ */ + +/* ==================== [Global Functions] ================================== */ + +esp_err_t ec_tools_bh1750(void) +{ + esp_err_t err = i2c_master_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "BH1750 I2C init failed: %s", esp_err_to_name(err)); + return err; + } + + ec_tools_register(&s_bh1750_tool); + return ESP_OK; +} + +static esp_err_t i2c_master_init(void) +{ + int i2c_master_port = I2C_MASTER_NUM; + + ESP_LOGI(TAG, "Configuring I2C master: port=%d, SDA=%d, SCL=%d, freq=%dHz", + i2c_master_port, I2C_MASTER_SDA_IO, I2C_MASTER_SCL_IO, I2C_MASTER_FREQ_HZ); + + i2c_config_t conf = { + .mode = I2C_MODE_MASTER, + .sda_io_num = I2C_MASTER_SDA_IO, + .scl_io_num = I2C_MASTER_SCL_IO, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master.clk_speed = I2C_MASTER_FREQ_HZ, + }; + + esp_err_t err = i2c_param_config(i2c_master_port, &conf); + if (err != ESP_OK) { + ESP_LOGE(TAG, "I2C param config failed: %s", esp_err_to_name(err)); + return err; + } + + err = i2c_driver_install(i2c_master_port, conf.mode, 0, 0, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "I2C driver install failed: %s", esp_err_to_name(err)); + return err; + } + + ESP_LOGI(TAG, "I2C master initialized successfully"); + + return ESP_OK; +} + +/** + * @brief 底层 BH1750 读取硬件接口 + */ +esp_err_t s_bh1750_read_lux(float *lux) +{ + esp_err_t err; + uint8_t cmd; + uint8_t data[2] = {0}; + uint16_t level = 0; + + if (lux == NULL) { + return ESP_ERR_INVALID_ARG; + } + + // 1. 发送 Power On 启动命令 + cmd = BH1750_CMD_PWR_ON; + err = i2c_master_write_to_device(I2C_MASTER_NUM, BH1750_I2C_ADDR, &cmd, 1, + I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS); + if (err != ESP_OK) { + ESP_LOGE(TAG, "BH1750 i2c write PWR_ON failed: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGD(TAG, "BH1750: sent PWR_ON"); + + // 2. 发送 One-Time 高分辨率模式指令 (测量一次后自动断电) + cmd = BH1750_CMD_ONE_TIME_H_RES; + err = i2c_master_write_to_device(I2C_MASTER_NUM, BH1750_I2C_ADDR, &cmd, 1, + I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS); + if (err != ESP_OK) { + ESP_LOGE(TAG, "BH1750 i2c write ONE_TIME_H_RES failed: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGD(TAG, "BH1750: sent ONE_TIME_H_RES"); + + // 3. 等待传感器完成转换测量 (手册要求高分辨率下至少等 120ms,这里给 180ms 留出裕量) + vTaskDelay(pdMS_TO_TICKS(BH1750_MEASURE_TIME_MS)); + + // 4. 读取 2 字节的测量数据 + err = i2c_master_read_from_device(I2C_MASTER_NUM, BH1750_I2C_ADDR, data, 2, + I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS); + if (err != ESP_OK) { + ESP_LOGE(TAG, "BH1750 i2c read failed: %s", esp_err_to_name(err)); + return err; + } + + ESP_LOGD(TAG, "BH1750 raw bytes: 0x%02x 0x%02x", data[0], data[1]); + + // 5. 数据解析与计算 + level = (data[0] << 8) | data[1]; + *lux = (float)level / BH1750_DIVISOR; + + ESP_LOGI(TAG, "BH1750 level=%u => %.2f lux", level, *lux); + + return ESP_OK; +} + +/* ==================== [Static Functions] ================================== */ + +static esp_err_t ec_tool_bh1750_execute(const char *input_json, char *output, size_t output_size) +{ + float lux = 0.0f; + esp_err_t err; + + if (output == NULL || output_size == 0) { + return ESP_ERR_INVALID_ARG; + } + + err = s_bh1750_read_lux(&lux); + if (err != ESP_OK) { + ESP_LOGE(TAG, "BH1750 read failed: %s", esp_err_to_name(err)); + snprintf(output, output_size, "{\"error\": \"Failed to read BH1750 sensor, err: %d\"}", err); + return err; + } + + snprintf(output, output_size, "{\"lux\": %.2f}", lux); + + return ESP_OK; +} + diff --git a/components/embed_claw/tools/tools_fill_light.c b/components/embed_claw/tools/tools_fill_light.c new file mode 100644 index 0000000..08dd232 --- /dev/null +++ b/components/embed_claw/tools/tools_fill_light.c @@ -0,0 +1,189 @@ +/** + * @file ec_tool_fill_light.c + * @author embedclaw_developer + * @brief 补光灯 PWM 控制工具 (Fill Light Control Tool) + * @version 0.1 + * @date 2026-03-27 + * + * @copyright Copyright (c) 2026, Wireless-Tag. All rights reserved. + * + */ + +/* ==================== [Includes] ========================================== */ + +#include "core/ec_tools.h" + +#include "esp_log.h" +#include "driver/ledc.h" +#include "cJSON.h" +#include +#include + +/* ==================== [Defines] =========================================== */ + +#define TAG "ec_tool_fill_light" + +/* 硬件引脚与外设配置宏 */ +#define FILL_LIGHT_GPIO GPIO_NUM_21 +#define FILL_LIGHT_LEDC_TIMER LEDC_TIMER_0 +#define FILL_LIGHT_LEDC_MODE LEDC_LOW_SPEED_MODE +#define FILL_LIGHT_LEDC_CHANNEL LEDC_CHANNEL_0 +#define FILL_LIGHT_LEDC_RESOLUTION LEDC_TIMER_10_BIT +#define FILL_LIGHT_LEDC_FREQ_HZ 5000 + +/* 业务逻辑约束宏 */ +#define FILL_LIGHT_MAX_DUTY ((1 << 10) - 1) /* 10-bit 分辨率对应的最大占空比 1023 */ +#define FILL_LIGHT_MAX_BRIGHTNESS 100 +#define FILL_LIGHT_MIN_BRIGHTNESS 0 + +/* ==================== [Typedefs] ========================================== */ + +/* ==================== [Static Prototypes] ================================= */ + +static esp_err_t fill_light_init(void); +static esp_err_t ec_tool_fill_light_execute(const char *input_json, char *output, size_t output_size); + +/* ==================== [Static Variables] ================================== */ + +static bool s_fill_light_inited = false; + +static const ec_tools_t s_fill_light_tool = { + .name = "control_fill_light", + .description = "CRITICAL HARDWARE ACTUATOR. Use this to physically change LED brightness. " + "PROTOCOL FOR AUTO-ADJUST: " + "1. You MUST have current lux from 'read_light_intensity' (DO NOT GUESS). " + "2. Map lux to brightness: <50lx=100%, 50-200lx=80%, 200-500lx=60%, 500-1000lx=40%, >1000lx=10%. " + "3. CALL THIS TOOL IMMEDIATELY after reading lux. " + "4. DO NOT generate a final text response to the user until you see '{\"status\": \"success\"}' in this tool's output. " + "A reply without calling this tool is a system violation.", + .input_schema_json = "{\"type\":\"object\",\"properties\":{\"brightness\":{\"type\":\"integer\",\"description\":\"Brightness percentage (0-100). 0=OFF, 100=Max.\"}},\"required\":[\"brightness\"]}", + .execute = ec_tool_fill_light_execute, +}; + +/* ==================== [Macros] ============================================ */ + +/* ==================== [Global Functions] ================================== */ + +esp_err_t fill_light_set_brightness(int brightness) +{ + esp_err_t err = fill_light_init(); + if (err != ESP_OK) { + return err; + } + + /* 参数合法性边界收束 */ + if (brightness < FILL_LIGHT_MIN_BRIGHTNESS) { + brightness = FILL_LIGHT_MIN_BRIGHTNESS; + } else if (brightness > FILL_LIGHT_MAX_BRIGHTNESS) { + brightness = FILL_LIGHT_MAX_BRIGHTNESS; + } + + uint32_t duty = (brightness * FILL_LIGHT_MAX_DUTY) / FILL_LIGHT_MAX_BRIGHTNESS; + + err = ledc_set_duty(FILL_LIGHT_LEDC_MODE, FILL_LIGHT_LEDC_CHANNEL, duty); + if (err != ESP_OK) { + ESP_LOGE(TAG, "LEDC set duty failed"); + return err; + } + + err = ledc_update_duty(FILL_LIGHT_LEDC_MODE, FILL_LIGHT_LEDC_CHANNEL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "LEDC update duty failed"); + return err; + } + + ESP_LOGI(TAG, "Fill light brightness set to %d%% (duty %" PRIu32 ")", brightness, duty); + + return ESP_OK; +} + +esp_err_t ec_tools_fill_light(void) +{ + ec_tools_register(&s_fill_light_tool); + return ESP_OK; +} + +/* ==================== [Static Functions] ================================== */ + +static esp_err_t fill_light_init(void) +{ + if (s_fill_light_inited) { + return ESP_OK; + } + + ledc_timer_config_t ledc_timer = { + .duty_resolution = FILL_LIGHT_LEDC_RESOLUTION, + .freq_hz = FILL_LIGHT_LEDC_FREQ_HZ, + .speed_mode = FILL_LIGHT_LEDC_MODE, + .timer_num = FILL_LIGHT_LEDC_TIMER, + .clk_cfg = LEDC_AUTO_CLK, + }; + + esp_err_t err = ledc_timer_config(&ledc_timer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "LEDC timer config failed"); + return err; + } + + ledc_channel_config_t ledc_channel = { + .channel = FILL_LIGHT_LEDC_CHANNEL, + .duty = FILL_LIGHT_MIN_BRIGHTNESS, + .gpio_num = FILL_LIGHT_GPIO, + .speed_mode = FILL_LIGHT_LEDC_MODE, + .hpoint = 0, + .timer_sel = FILL_LIGHT_LEDC_TIMER, + .flags.output_invert = 0 + }; + + err = ledc_channel_config(&ledc_channel); + if (err != ESP_OK) { + ESP_LOGE(TAG, "LEDC channel config failed"); + return err; + } + + s_fill_light_inited = true; + ESP_LOGI(TAG, "Fill light initialized on GPIO %d", FILL_LIGHT_GPIO); + + return ESP_OK; +} + +static esp_err_t ec_tool_fill_light_execute(const char *input_json, char *output, size_t output_size) +{ + if (input_json == NULL) { + snprintf(output, output_size, "{\"error\": \"input is null\"}"); + return ESP_ERR_INVALID_ARG; + } + + ESP_LOGI(TAG, "control_fill_light input_json: %s", input_json); + + cJSON *input = cJSON_Parse(input_json); + if (input == NULL) { + snprintf(output, output_size, "{\"error\": \"invalid json\"}"); + return ESP_ERR_INVALID_ARG; + } + + cJSON *brightness_item = cJSON_GetObjectItem(input, "brightness"); + if (brightness_item == NULL || !cJSON_IsNumber(brightness_item)) { + cJSON_Delete(input); + snprintf(output, output_size, "{\"error\": \"'brightness' (integer) is required\"}"); + return ESP_ERR_INVALID_ARG; + } + + int brightness = brightness_item->valueint; + ESP_LOGI(TAG, "control_fill_light parsed brightness: %d", brightness); + if (brightness < FILL_LIGHT_MIN_BRIGHTNESS || brightness > FILL_LIGHT_MAX_BRIGHTNESS) { + cJSON_Delete(input); + snprintf(output, output_size, "{\"error\": \"'brightness' must be between 0 and 100\"}"); + return ESP_ERR_INVALID_ARG; + } + + cJSON_Delete(input); + + if (fill_light_set_brightness(brightness) == ESP_OK) { + snprintf(output, output_size, "{\"status\": \"success\", \"brightness\": %d}", brightness); + return ESP_OK; + } else { + snprintf(output, output_size, "{\"error\": \"failed to set brightness\"}"); + return ESP_FAIL; + } +} \ No newline at end of file diff --git a/components/embed_claw/tools/tools_ntu_test_adc.c b/components/embed_claw/tools/tools_ntu_test_adc.c new file mode 100644 index 0000000..65a2363 --- /dev/null +++ b/components/embed_claw/tools/tools_ntu_test_adc.c @@ -0,0 +1,229 @@ +/** + * @file tools_ntu_test_adc.c + * @author embedclaw_developer + * @brief ADC initialization and read tool for NTU test sensor + * @version 0.4 + * @date 2026-04-10 + * + * @copyright Copyright (c) 2026, Wireless-Tag. All rights reserved. + * + */ + +#include "core/ec_tools.h" +#include +#include +#include "esp_adc/adc_cali.h" +#include "esp_adc/adc_cali_scheme.h" +#include "esp_adc/adc_oneshot.h" +#include "esp_err.h" +#include "esp_log.h" + +#define TAG "tools_ntu_test_adc" + +#define NTU_ADC_UNIT ADC_UNIT_1 +#define NTU_ADC_CHANNEL ADC_CHANNEL_2 +#define NTU_ADC_ATTEN ADC_ATTEN_DB_12 +#define NTU_ADC_BITWIDTH ADC_BITWIDTH_DEFAULT + +#define NTU_TEMP_DATA_C 25.0f +#define NTU_K_VALUE 3985.59f +#define NTU_VOLTAGE_DIVIDER_RATIO 2.0f + +static esp_err_t ec_tool_ntu_test_adc_execute(const char *input_json, char *output, size_t output_size); +static esp_err_t ntu_adc_init(void); +static esp_err_t ntu_adc_calibration_init(void); +static esp_err_t ntu_adc_read_raw(int *raw_value); +static float ntu_compute_value(float tu_calibration); + +static adc_oneshot_unit_handle_t s_adc_handle = NULL; +static bool s_adc_inited = false; +static adc_cali_handle_t s_cali_handle = NULL; +static bool s_cali_inited = false; +static bool s_cali_available = false; + +static const ec_tools_t s_ntu_test_adc_tool = { + .name = "ntu_test_adc", + .description = "Initialize ADC, apply ESP-IDF calibration, and read the NTU test analog value. No input is required.", + .input_schema_json = "{\"type\":\"object\",\"properties\":{},\"required\":[]}", + .execute = ec_tool_ntu_test_adc_execute, +}; + +esp_err_t ec_tools_ntu_test_adc(void) +{ + ec_tools_register(&s_ntu_test_adc_tool); + return ESP_OK; +} + +static esp_err_t ec_tool_ntu_test_adc_execute(const char *input_json, char *output, size_t output_size) +{ + int raw = 0; + int voltage_mv = 0; + float tu_voltage = 0.0f; + float tu_calibration = 0.0f; + float tu_value = 0.0f; + esp_err_t err; + + (void)input_json; + + if (!output || output_size == 0) { + return ESP_ERR_INVALID_ARG; + } + + err = ntu_adc_init(); + if (err != ESP_OK) { + snprintf(output, output_size, "{\"error\":\"adc init failed\",\"err\":%d}", err); + return err; + } + + err = ntu_adc_calibration_init(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "ADC calibration init failed: %s", esp_err_to_name(err)); + } + + err = ntu_adc_read_raw(&raw); + if (err != ESP_OK) { + snprintf(output, output_size, "{\"error\":\"adc read failed\",\"err\":%d}", err); + return err; + } + + if (s_cali_inited && s_cali_available && s_cali_handle) { + esp_err_t cali_err = adc_cali_raw_to_voltage(s_cali_handle, raw, &voltage_mv); + if (cali_err == ESP_OK) { + tu_voltage = ((float)voltage_mv / 1000.0f) * NTU_VOLTAGE_DIVIDER_RATIO; + tu_calibration = -0.0192f * (NTU_TEMP_DATA_C - 25.0f) + tu_voltage; + tu_value = ntu_compute_value(tu_calibration); + + snprintf(output, + output_size, + "{\"status\":\"success\",\"raw\":%d,\"voltage_mv\":%d,\"tu_voltage\":%.4f,\"tu_calibration\":%.4f,\"ntu_value\":%.2f,\"calibrated\":true,\"unit\":%d,\"channel\":%d}", + raw, + voltage_mv, + tu_voltage, + tu_calibration, + tu_value, + (int)NTU_ADC_UNIT, + (int)NTU_ADC_CHANNEL); + ESP_LOGI(TAG, "ADC read OK: raw=%d, voltage_mv=%d, tu_voltage=%.4f, tu_cal=%.4f, ntu=%.2f", + raw, voltage_mv, tu_voltage, tu_calibration, tu_value); + return ESP_OK; + } + ESP_LOGW(TAG, "adc_cali_raw_to_voltage failed: %s", esp_err_to_name(cali_err)); + } + + snprintf(output, + output_size, + "{\"status\":\"success\",\"raw\":%d,\"calibrated\":false,\"unit\":%d,\"channel\":%d}", + raw, + (int)NTU_ADC_UNIT, + (int)NTU_ADC_CHANNEL); + ESP_LOGI(TAG, "ADC read OK: raw=%d", raw); + return ESP_OK; +} + +static esp_err_t ntu_adc_init(void) +{ + if (s_adc_inited) { + return ESP_OK; + } + + adc_oneshot_unit_init_cfg_t init_cfg = { + .unit_id = NTU_ADC_UNIT, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }; + + esp_err_t err = adc_oneshot_new_unit(&init_cfg, &s_adc_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "adc_oneshot_new_unit failed: %s", esp_err_to_name(err)); + return err; + } + + adc_oneshot_chan_cfg_t chan_cfg = { + .atten = NTU_ADC_ATTEN, + .bitwidth = NTU_ADC_BITWIDTH, + }; + + err = adc_oneshot_config_channel(s_adc_handle, NTU_ADC_CHANNEL, &chan_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "adc_oneshot_config_channel failed: %s", esp_err_to_name(err)); + adc_oneshot_del_unit(s_adc_handle); + s_adc_handle = NULL; + return err; + } + + s_adc_inited = true; + ESP_LOGI(TAG, "ADC initialized: unit=%d channel=%d", (int)NTU_ADC_UNIT, (int)NTU_ADC_CHANNEL); + return ESP_OK; +} + +static esp_err_t ntu_adc_calibration_init(void) +{ + if (s_cali_inited) { + return ESP_OK; + } + +#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED + adc_cali_curve_fitting_config_t cali_cfg_curve = { + .unit_id = NTU_ADC_UNIT, + .atten = NTU_ADC_ATTEN, + .bitwidth = NTU_ADC_BITWIDTH, + }; + if (adc_cali_create_scheme_curve_fitting(&cali_cfg_curve, &s_cali_handle) == ESP_OK) { + s_cali_available = true; + s_cali_inited = true; + ESP_LOGI(TAG, "ADC calibration initialized with curve fitting"); + return ESP_OK; + } +#endif + +#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED + adc_cali_line_fitting_config_t cali_cfg_line = { + .unit_id = NTU_ADC_UNIT, + .atten = NTU_ADC_ATTEN, + .bitwidth = NTU_ADC_BITWIDTH, + }; + if (adc_cali_create_scheme_line_fitting(&cali_cfg_line, &s_cali_handle) == ESP_OK) { + s_cali_available = true; + s_cali_inited = true; + ESP_LOGI(TAG, "ADC calibration initialized with line fitting"); + return ESP_OK; + } +#endif + + s_cali_inited = true; + s_cali_available = false; + ESP_LOGW(TAG, "ADC calibration is not available on this target/configuration"); + return ESP_OK; +} + +static esp_err_t ntu_adc_read_raw(int *raw_value) +{ + if (!raw_value) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = ntu_adc_init(); + if (err != ESP_OK) { + return err; + } + + err = adc_oneshot_read(s_adc_handle, NTU_ADC_CHANNEL, raw_value); + if (err != ESP_OK) { + ESP_LOGE(TAG, "adc_oneshot_read failed: %s", esp_err_to_name(err)); + return err; + } + + return ESP_OK; +} + +static float ntu_compute_value(float tu_calibration) +{ + float tu_value = -865.68f * tu_calibration + NTU_K_VALUE; + + if (tu_value <= 0.0f) { + tu_value = 0.0f; + } else if (tu_value >= 3000.0f) { + tu_value = 3000.0f; + } + + return tu_value; +} \ No newline at end of file diff --git a/components/embed_claw/tools/tools_water_ion_temp_uart.c b/components/embed_claw/tools/tools_water_ion_temp_uart.c new file mode 100644 index 0000000..8b97bfd --- /dev/null +++ b/components/embed_claw/tools/tools_water_ion_temp_uart.c @@ -0,0 +1,277 @@ +/** + * @file tools_water_ion_temp_uart.c + * @author cangyu (sky.kirto@qq.com) + * @brief + * @version 0.1 + * @date 2026-03-29 + * + * @copyright Copyright (c) 2026, Wireless-Tag. All rights reserved. + * + */ + +/* ==================== [Includes] ========================================== */ + +#include "core/ec_tools.h" +#include "ec_config_internal.h" + +#include +#include +#include +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "driver/uart.h" + +/* ==================== [Defines] =========================================== */ + +#define WATER_ION_UART_PORT UART_NUM_1 +#define WATER_ION_UART_BAUD_RATE 9600 +#define WATER_ION_UART_TX_PIN 14 +#define WATER_ION_UART_RX_PIN 13 +#define WATER_ION_UART_TIMEOUT_MS 1200 + +#define WATER_ION_UART_RX_BUF_SIZE 256 +#define WATER_ION_UART_TX_BUF_SIZE 256 +#define WATER_ION_PROTO_FRAME_SIZE 6 +#define WATER_ION_PROTO_CMD_READ 0xA0 +#define WATER_ION_PROTO_RESP_OK 0xAA +#define WATER_ION_PROTO_RESP_ERR 0xAC + +/* ==================== [Typedefs] ========================================== */ + +/* ==================== [Static Prototypes] ================================= */ + +static esp_err_t ec_tool_water_ion_temp_uart_execute(const char *input_json, char *output, size_t output_size); +static esp_err_t uart_init_for_sensor(uart_port_t uart_port); +static esp_err_t read_sensor_frame_uart(uint8_t frame_out[WATER_ION_PROTO_FRAME_SIZE]); +static uint8_t proto_checksum(const uint8_t frame[WATER_ION_PROTO_FRAME_SIZE]); +static void format_hex_frame(const uint8_t frame[WATER_ION_PROTO_FRAME_SIZE], char *out, size_t out_size); +static const char *sensor_error_desc(uint8_t err_code); + +/* ==================== [Static Variables] ================================== */ + +static const char *TAG = "tools_water_ion_temp"; + +static const ec_tools_t s_water_ion_temp_uart = { + .name = "water_ion_temp_uart", + .description = "Read water ion concentration and temperature from a UART sensor and return structured JSON.", + .input_schema_json = + "{\"type\":\"object\"," + "\"properties\":{}," + "\"required\":[]}", + .execute = ec_tool_water_ion_temp_uart_execute, +}; + +/* ==================== [Macros] ============================================ */ + +/* ==================== [Global Functions] ================================== */ + +esp_err_t ec_tools_water_ion_temp_uart(void) +{ + ec_tools_register(&s_water_ion_temp_uart); + return ESP_OK; +} + +/* ==================== [Static Functions] ================================== */ + +static esp_err_t ec_tool_water_ion_temp_uart_execute(const char *input_json, char *output, size_t output_size) +{ + (void)input_json; + + if (!output || output_size == 0) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t frame[WATER_ION_PROTO_FRAME_SIZE] = {0}; + char frame_hex[32] = {0}; + bool checksum_ok = false; + + esp_err_t err = read_sensor_frame_uart(frame); + if (err != ESP_OK) { + snprintf(output, output_size, "Error: UART read failed (%s)", esp_err_to_name(err)); + return err; + } + + format_hex_frame(frame, frame_hex, sizeof(frame_hex)); + checksum_ok = (frame[5] == proto_checksum(frame)); + + if (frame[0] == WATER_ION_PROTO_RESP_OK) { + uint16_t ion_raw = ((uint16_t)frame[1] << 8) | frame[2]; + uint16_t temp_raw = ((uint16_t)frame[3] << 8) | frame[4]; + float ion_mg_l = (float)ion_raw; + float temp_c = (float)temp_raw / 100.0f; + + snprintf(output, + output_size, + "{\"ion_mg_l\":%.2f,\"temperature_c\":%.2f,\"checksum_ok\":%s,\"raw_hex\":\"%s\"}", + ion_mg_l, + temp_c, + checksum_ok ? "true" : "false", + frame_hex); + + ESP_LOGI(TAG, + "UART sensor parsed: ion=%.2f mg/L, temp=%.2f C, frame=%s, checksum_ok=%s", + ion_mg_l, + temp_c, + frame_hex, + checksum_ok ? "true" : "false"); + return ESP_OK; + } + + if (frame[0] == WATER_ION_PROTO_RESP_ERR) { + uint8_t err_code = frame[1]; + snprintf(output, + output_size, + "Error: Sensor returned error code 0x%02X (%s), raw=%s", + err_code, + sensor_error_desc(err_code), + frame_hex); + return ESP_FAIL; + } + + snprintf(output, + output_size, + "Error: Unsupported response frame header 0x%02X, raw=%s", + frame[0], + frame_hex); + return ESP_ERR_INVALID_RESPONSE; +} + +static esp_err_t uart_init_for_sensor(uart_port_t uart_port) +{ + uart_config_t uart_cfg = { + .baud_rate = WATER_ION_UART_BAUD_RATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + + esp_err_t err = uart_driver_install(uart_port, + WATER_ION_UART_RX_BUF_SIZE, + WATER_ION_UART_TX_BUF_SIZE, + 0, + NULL, + 0); + if (err != ESP_OK) { + return err; + } + + err = uart_param_config(uart_port, &uart_cfg); + if (err != ESP_OK) { + uart_driver_delete(uart_port); + return err; + } + + err = uart_set_pin(uart_port, + WATER_ION_UART_TX_PIN, + WATER_ION_UART_RX_PIN, + UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + uart_driver_delete(uart_port); + return err; + } + + return ESP_OK; +} + +static esp_err_t read_sensor_frame_uart(uint8_t frame_out[WATER_ION_PROTO_FRAME_SIZE]) +{ + if (!frame_out) { + return ESP_ERR_INVALID_ARG; + } + + const uart_port_t uart_port = WATER_ION_UART_PORT; + + esp_err_t err = uart_init_for_sensor(uart_port); + if (err != ESP_OK) { + return err; + } + + uart_flush(uart_port); + + uint8_t tx_frame[WATER_ION_PROTO_FRAME_SIZE]; + tx_frame[0] = WATER_ION_PROTO_CMD_READ; + tx_frame[1] = 0x00; + tx_frame[2] = 0x00; + tx_frame[3] = 0x00; + tx_frame[4] = 0x00; + tx_frame[5] = proto_checksum(tx_frame); + + int wrote = uart_write_bytes(uart_port, (const char *)tx_frame, sizeof(tx_frame)); + if (wrote != sizeof(tx_frame)) { + uart_driver_delete(uart_port); + return ESP_FAIL; + } + + int total = 0; + TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(WATER_ION_UART_TIMEOUT_MS); + while (total < WATER_ION_PROTO_FRAME_SIZE) { + TickType_t now = xTaskGetTickCount(); + if (now >= deadline) { + break; + } + + TickType_t remain = deadline - now; + int n = uart_read_bytes(uart_port, + frame_out + total, + WATER_ION_PROTO_FRAME_SIZE - total, + remain); + if (n > 0) { + total += n; + } + } + + uart_driver_delete(uart_port); + + if (total != WATER_ION_PROTO_FRAME_SIZE) { + return ESP_ERR_TIMEOUT; + } + + return ESP_OK; +} + +static uint8_t proto_checksum(const uint8_t frame[WATER_ION_PROTO_FRAME_SIZE]) +{ + uint32_t sum = 0; + for (int i = 0; i < WATER_ION_PROTO_FRAME_SIZE - 1; i++) { + sum += frame[i]; + } + return (uint8_t)(sum & 0xFF); +} + +static void format_hex_frame(const uint8_t frame[WATER_ION_PROTO_FRAME_SIZE], char *out, size_t out_size) +{ + if (!frame || !out || out_size == 0) { + return; + } + + snprintf(out, + out_size, + "%02X %02X %02X %02X %02X %02X", + frame[0], + frame[1], + frame[2], + frame[3], + frame[4], + frame[5]); +} + +static const char *sensor_error_desc(uint8_t err_code) +{ + switch (err_code) { + case 0x01: + return "command order error"; + case 0x02: + return "busy"; + case 0x03: + return "calibration failed"; + case 0x04: + return "temperature out of range"; + default: + return "unknown error"; + } +} diff --git a/spiffs_data/skills/auto-fish.md b/spiffs_data/skills/auto-fish.md new file mode 100644 index 0000000..a0152e4 --- /dev/null +++ b/spiffs_data/skills/auto-fish.md @@ -0,0 +1,90 @@ +# Auto Fish Keeping + +Use this skill when the user says auto fish keeping, automatic fish tank care, or mentions "自动养鱼". + +## Fish tank profile +- Tank type: small tank +- Size: 35 x 20 x 22 cm +- Fish stock: + - 3 goldfish + - 3 black-tailed fish (black tail hook type) + - 3 small fish + +## Available existing skills to reuse +- water-quality +- water-filter-gpio +- oxygen-pump-gpio +- light-auto-adjust +- fish-feed-gpio + +## Available tools for this orchestration +- get_current_time +- ntu_test_adc +- water_ion_temp_uart +- read_light_intensity +- fill_light +- gpio_control + +## When to use +When the user asks for full automatic fish tank management, including feeding, water quality control, oxygen pumping, light compensation, and scheduled status reports. + +## How to use +1. Treat this skill as a coordinator that reuses the existing fish-related workflows. +2. When the user starts automatic fish keeping, always run the startup sequence first: + - Run oxygen pump logic for 60 seconds. +3. Every run, first call get_current_time and decide which scheduled jobs should run now. +4. Run feeding workflow: + - At 19:00 every day, execute fish feeding logic. + - Every feeding action must keep the feeding GPIO high for exactly 30 seconds. + - After 30 seconds, immediately drive the corresponding feeding GPIO low. +5. Run water quality and filtration workflow: + - Read water quality (NTU, ion, temperature). + - If turbidity and ion concentration both rise, execute filtration logic (GPIO1 on for 1 hour, then off). +6. Run oxygen pump workflow: + - Compare current temperature with baseline. + - If temperature rises slightly, execute oxygen pump logic (GPIO2 on for 10 minutes, then off). +7. Run lighting workflow: + - Read BH1750 lux via read_light_intensity. + - Call fill_light using the existing lux-to-brightness mapping from light-auto-adjust. +8. Run background status polling every 10 minutes: + - Every 10 minutes, read core status data (at least light intensity; also include NTU/ion/temperature if tools are available in that cycle). + - Do not send user-facing messages for this 10-minute polling cycle. + - Keep results as internal state/history for later trend judgment in scheduled reports. + - If a critical threshold breach is detected, allow control actions (for safety) but still do not send routine polling messages. +9. Run scheduled tank reports: + - Report automatically at 10:00, 14:00, and 20:00 every day. + - Scheduled report timing remains unchanged; do not replace or shift these report times with 10-minute polling. + - Each report should include: + - current time + - NTU (turbidity) + - ion_mg_l + - temperature_c + - light lux and fill-light brightness decision + - GPIO1/GPIO2/GPIO4 status if available + - whether feeding/filtering/oxygen actions were triggered in this cycle + - intelligent water-condition summary (\"water status\") with one-line conclusion and reasons + - intelligent fish-condition summary (\"fish status\") with one-line conclusion and reasons + - Summary generation rules for every scheduled report: + - Water status must be one of: stable, watch, needs attention. + - Water status should be derived from trend + current readings (NTU, ion_mg_l, temperature_c), not from a single metric only. + - Fish status must be one of: normal, mild stress risk, high stress risk. + - Fish status should be inferred from water status, temperature change, dissolved-ion/turbidity changes, and whether feeding/filtering/oxygen actions were recently triggered. + - If direct fish-behavior sensing is unavailable, explicitly mark fish status as inferred and include confidence (high/medium/low). + - Keep each summary concise: conclusion first, then 2-4 key evidence points. + +## Output guidance +- If the user asks for plain conclusion, summarize tank status as stable or needs attention. +- If any tool fails, report the failed part explicitly and continue with other available checks. +- Always separate observed values from control actions. +- 10-minute background polling is silent by default (no routine message output). +- For scheduled reports, always output this structure in order: + - Observed values + - Triggered control actions + - Water status summary (conclusion + evidence) + - Fish status summary (conclusion + evidence + confidence) + +## Notes +- Always prefer real sensor/tool results; do not guess values. +- Any GPIO operation must be executed through gpio_control. +- This skill coordinates existing capabilities; do not replace the existing workflows with invented logic. +- If no external scheduler is configured, execute due tasks whenever the user triggers this skill and the current time matches scheduled windows. diff --git a/spiffs_data/skills/fish-feed-gpio.md b/spiffs_data/skills/fish-feed-gpio.md new file mode 100644 index 0000000..6642e33 --- /dev/null +++ b/spiffs_data/skills/fish-feed-gpio.md @@ -0,0 +1,24 @@ +# Fish Feeding GPIO Schedule + +Use this skill to control daily fish feeding by toggling GPIO4 on a fixed schedule. + +## When to use +When the user asks for a daily 19:00 fish feeding action. + +## How to use +1. Use get_current_time to get local date and time. +2. Determine whether it is time to feed: + - target time is 19:00 local time every day +3. At 19:00, call gpio_control with pin=4 and action=on. +4. Keep GPIO4 high for 30 seconds. +5. After 30 seconds, call gpio_control with pin=4 and action=off. +6. If current time is not 19:00, report next trigger time and do not force GPIO4 on. + +## Output guidance +- Report current time and whether the schedule condition is met. +- Report GPIO4 on/off execution status. +- If the user asks for immediate manual feeding, execute the same on-30s-off sequence immediately. + +## Notes +- Any GPIO operation must be executed through gpio_control. +- This skill describes the feeding control logic; external scheduler/cron can be used to trigger this skill automatically each day. diff --git a/spiffs_data/skills/light-auto-adjust.md b/spiffs_data/skills/light-auto-adjust.md new file mode 100644 index 0000000..9ef3604 --- /dev/null +++ b/spiffs_data/skills/light-auto-adjust.md @@ -0,0 +1,23 @@ +# Light Auto Adjust + +Automatically test ambient light with BH1750 and adjust the fill light. + +## When to use +When the user asks to test light intensity, measure ambient brightness, or automatically tune the fill light. + +## How to use +1. First call `read_light_intensity` to read the current lux value from the BH1750 sensor. +2. Use the returned lux value as the only source of truth. Do not guess brightness without reading the sensor first. +3. Map lux to brightness with this rule: + - `< 50 lx` -> `100` + - `50-200 lx` -> `80` + - `200-500 lx` -> `60` + - `500-1000 lx` -> `40` + - `> 1000 lx` -> `10` +4. Then call `fill_light` with the computed `brightness` value. +5. Wait until the tool returns `{"status": "success"}` before giving the final response to the user. + +## Notes +- Always run the BH1750 reading step before controlling the fill light. +- Use `fill_light` as the actual tool name; do not invent alternate names. +- This workflow is intended for automatic light testing and LEDC-driven brightness control. \ No newline at end of file diff --git a/spiffs_data/skills/oxygen-pump-gpio.md b/spiffs_data/skills/oxygen-pump-gpio.md new file mode 100644 index 0000000..49e4371 --- /dev/null +++ b/spiffs_data/skills/oxygen-pump-gpio.md @@ -0,0 +1,28 @@ +# Oxygen Pump GPIO Control + +Use this skill to drive oxygen pumping when water temperature shows a slight rise. + +## When to use +When the user asks to pump oxygen if water temperature rises slightly. + +## How to use +1. Call water_ion_temp_uart to read the current temperature. +2. Get baseline and current temperature: + - if historical temperature exists, use the latest previous value as baseline + - otherwise take two measurements with a short interval (for example 30-60 seconds) +3. Evaluate slight rise with this default rule: + - slight rise means delta_t is between 0.5 C and 2.0 C (inclusive) + - delta_t = current_temperature_c - baseline_temperature_c +4. If slight rise is detected, call gpio_control with pin=2 and action=on. +5. Keep GPIO2 high for 10 minutes. +6. After 10 minutes, call gpio_control with pin=2 and action=off. +7. If delta_t is outside the slight-rise range, keep GPIO2 unchanged and explain the decision. + +## Output guidance +- Report baseline temperature, current temperature, and delta_t. +- State whether the slight-rise condition was met. +- Report GPIO2 on/off execution status. + +## Notes +- Always use measured temperature from water_ion_temp_uart. +- Any GPIO operation must be executed through gpio_control. diff --git a/spiffs_data/skills/water-filter-gpio.md b/spiffs_data/skills/water-filter-gpio.md new file mode 100644 index 0000000..63bf569 --- /dev/null +++ b/spiffs_data/skills/water-filter-gpio.md @@ -0,0 +1,32 @@ +# Water Filter GPIO Control + +Use this skill to combine water quality detection and GPIO-based filtration control. + +## When to use +When the user asks to auto-enable filtration after water ion concentration and turbidity rise. +This skill should be used together with Water Quality Monitoring. + +## How to use +1. First run the Water Quality Monitoring workflow: + - call ntu_test_adc for turbidity + - call water_ion_temp_uart for ion concentration and temperature +2. Get a baseline reading and a current reading: + - if historical readings are available, use the latest previous reading as baseline + - otherwise take two readings with a short interval (for example 30-60 seconds) +3. Determine whether both indicators increased: + - turbidity increased: current_ntu > baseline_ntu + - ion concentration increased: current_ion_mg_l > baseline_ion_mg_l +4. If both increased, call gpio_control with pin=1 and action=on. +5. Keep GPIO1 high for 1 hour. +6. After 1 hour, call gpio_control with pin=1 and action=off. +7. If either increase condition is not met, do not enable GPIO1 and explain why. + +## Output guidance +- Clearly report baseline vs current values for NTU and ion concentration. +- Explicitly state whether the trigger condition was met. +- Report GPIO1 action result for both on and off operations. +- If GPIO control fails, return the tool error and keep water quality conclusion. + +## Notes +- Always use real tool readings. Do not guess values. +- Any GPIO operation must be executed through gpio_control. diff --git a/spiffs_data/skills/water-quality.md b/spiffs_data/skills/water-quality.md new file mode 100644 index 0000000..eea72f1 --- /dev/null +++ b/spiffs_data/skills/water-quality.md @@ -0,0 +1,23 @@ +# Water Quality Monitoring + +Use this skill when the conversation is about water quality, water testing, turbidity, water ion concentration, or water temperature. + +## When to use +When the user asks about water quality inspection, water quality analysis, water testing, or related sensor readings. + +## How to use +1. First call ntu_test_adc to read the turbidity sensor result. +2. Then call water_ion_temp_uart to read ion concentration and temperature. +3. Treat ntu_test_adc as the turbidity source and water_ion_temp_uart as the ion/temperature source. +4. Combine both results into one water quality summary. +5. If either tool returns an error, report the failed sensor separately and still return the other valid result when available. +6. Do not guess water quality values without reading the tools first. + +## Output guidance +- Use the NTU value from ntu_test_adc for turbidity judgment. +- Use ion_mg_l and temperature_c from water_ion_temp_uart for auxiliary water quality context. +- If the user asks for a plain conclusion, summarize whether the water looks clear, moderately turbid, or highly turbid, then include ion concentration and temperature if available. + +## Notes +- Always prioritize actual sensor readings over assumptions. +- This skill is intended to make the agent automatically enable both tools whenever water quality is discussed.