diff --git a/components/control_plane/include/control_plane.h b/components/control_plane/include/control_plane.h index fb409db..8abaa9f 100644 --- a/components/control_plane/include/control_plane.h +++ b/components/control_plane/include/control_plane.h @@ -25,12 +25,15 @@ typedef enum { typedef enum { CONTROL_PLANE_MODEL_COMMAND_UNKNOWN = 0, CONTROL_PLANE_MODEL_COMMAND_DIRECTION, - CONTROL_PLANE_MODEL_COMMAND_CALIBRATE + CONTROL_PLANE_MODEL_COMMAND_CALIBRATE, + CONTROL_PLANE_MODEL_COMMAND_CHOUQIAN } control_plane_model_command_type_t; typedef void (*control_plane_model_command_callback_t)( control_plane_model_command_type_t command_type, const char *value, + size_t value_index, + bool value_index_valid, void *user_data); typedef enum { diff --git a/components/control_plane/internal/control_plane_internal.h b/components/control_plane/internal/control_plane_internal.h index 405d6b0..ae0e753 100644 --- a/components/control_plane/internal/control_plane_internal.h +++ b/components/control_plane/internal/control_plane_internal.h @@ -20,7 +20,9 @@ control_plane_direction_command_t control_plane_direction_parse_command_internal bool control_plane_extract_model_command(const char *text, control_plane_model_command_type_t *out_command_type, char *out_value, - size_t out_value_capacity); + size_t out_value_capacity, + size_t *out_value_index, + bool *out_value_index_valid); const char *control_plane_multimodal_resolve_upstream_mode(void); bool control_plane_multimodal_is_push2talk_mode(void); const char *control_plane_multimodal_resolve_voice_id(const char *auth_voice_id); diff --git a/components/control_plane/src/multimodal/control_plane_model_command_extractor.c b/components/control_plane/src/multimodal/control_plane_model_command_extractor.c index f88ca13..ab70438 100644 --- a/components/control_plane/src/multimodal/control_plane_model_command_extractor.c +++ b/components/control_plane/src/multimodal/control_plane_model_command_extractor.c @@ -51,10 +51,44 @@ static const char *find_command_prefix(const char *text) return strstr(cursor, "/*{"); } +static bool text_contains_chouqian_result(const char *text) +{ + return text != NULL && + (strstr(text, "签文:") != NULL || strstr(text, "签文:") != NULL); +} + +static bool resolve_chouqian_result_index(const char *value, size_t *out_index) +{ + static const char *const s_chouqian_result_names[] = { + "卜运签", + "和平签", + "上上签", + "小吉签", + "中上签", + }; + + if (value == NULL) { + return false; + } + + for (size_t i = 0; i < sizeof(s_chouqian_result_names) / sizeof(s_chouqian_result_names[0]); ++i) { + if (strcmp(value, s_chouqian_result_names[i]) == 0) { + if (out_index != NULL) { + *out_index = i; + } + return true; + } + } + + return false; +} + bool control_plane_extract_model_command(const char *text, control_plane_model_command_type_t *out_command_type, char *out_value, - size_t out_value_capacity) + size_t out_value_capacity, + size_t *out_value_index, + bool *out_value_index_valid) { if (text == NULL || out_command_type == NULL || out_value == NULL || out_value_capacity == 0U) { @@ -63,9 +97,20 @@ bool control_plane_extract_model_command(const char *text, *out_command_type = CONTROL_PLANE_MODEL_COMMAND_UNKNOWN; out_value[0] = '\0'; + if (out_value_index != NULL) { + *out_value_index = 0U; + } + if (out_value_index_valid != NULL) { + *out_value_index_valid = false; + } const char *command_start = find_command_prefix(text); if (command_start == NULL || command_start[0] != '/' || command_start[1] != '*') { + if (text_contains_chouqian_result(text)) { + *out_command_type = CONTROL_PLANE_MODEL_COMMAND_CHOUQIAN; + strlcpy(out_value, "签文", out_value_capacity); + return true; + } return false; } @@ -101,6 +146,7 @@ bool control_plane_extract_model_command(const char *text, bool found = false; cJSON *direction = cJSON_GetObjectItemCaseSensitive(cmd_json, "转向"); cJSON *calibrate = cJSON_GetObjectItemCaseSensitive(cmd_json, "校准"); + cJSON *chouqian = cJSON_GetObjectItemCaseSensitive(cmd_json, "抽签"); if (cJSON_IsString(direction) && direction->valuestring != NULL) { if (is_supported_model_direction(direction->valuestring)) { @@ -114,6 +160,19 @@ bool control_plane_extract_model_command(const char *text, strlcpy(out_value, calibrate->valuestring, out_value_capacity); found = true; } + } else if (cJSON_IsString(chouqian) && chouqian->valuestring != NULL) { + size_t result_index = 0U; + if (resolve_chouqian_result_index(chouqian->valuestring, &result_index)) { + *out_command_type = CONTROL_PLANE_MODEL_COMMAND_CHOUQIAN; + strlcpy(out_value, chouqian->valuestring, out_value_capacity); + if (out_value_index != NULL) { + *out_value_index = result_index; + } + if (out_value_index_valid != NULL) { + *out_value_index_valid = true; + } + found = true; + } } cJSON_Delete(cmd_json); diff --git a/components/control_plane/src/multimodal/control_plane_multimodal_runtime.c b/components/control_plane/src/multimodal/control_plane_multimodal_runtime.c index d72bda2..a338b7b 100644 --- a/components/control_plane/src/multimodal/control_plane_multimodal_runtime.c +++ b/components/control_plane/src/multimodal/control_plane_multimodal_runtime.c @@ -197,7 +197,14 @@ static void control_plane_responding_text_bridge(const char *text, control_plane_model_command_type_t command_type = CONTROL_PLANE_MODEL_COMMAND_UNKNOWN; char value[32] = {0}; - if (!control_plane_extract_model_command(text, &command_type, value, sizeof(value))) { + size_t value_index = 0U; + bool value_index_valid = false; + if (!control_plane_extract_model_command(text, + &command_type, + value, + sizeof(value), + &value_index, + &value_index_valid)) { return; } @@ -213,8 +220,13 @@ static void control_plane_responding_text_bridge(const char *text, return; } - ESP_LOGI(TAG, "Device command: type=%d value=%s", (int)command_type, value); - cb(command_type, value, cb_user_data); + ESP_LOGI(TAG, + "Device command: type=%d value=%s index=%u valid=%d", + (int)command_type, + value, + (unsigned int)value_index, + value_index_valid ? 1 : 0); + cb(command_type, value, value_index, value_index_valid, cb_user_data); } static esp_err_t control_plane_prepare_for_multimodal_start(void) diff --git a/components/control_plane/src/runtime/control_plane_runtime_workers.c b/components/control_plane/src/runtime/control_plane_runtime_workers.c index 98bb3de..a21d0ce 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -47,6 +47,7 @@ /* Liveness safety bound for the fixed two-cycle wealth spin intro (not the * animation length). On timeout the worker proceeds to reveal + rotate anyway. */ #define CONTROL_PLANE_SPIN_INTRO_TIMEOUT_MS 6000 +#define CONTROL_PLANE_COMMAND_ROTATION_DEG 45.0f #if (configSUPPORT_STATIC_ALLOCATION == 1) && \ ((defined(CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) && CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM) || \ @@ -128,6 +129,8 @@ typedef struct { control_plane_model_command_type_t command_type; bool stop_requested; char value[32]; + size_t value_index; + bool value_index_valid; } control_plane_model_command_t; typedef enum { @@ -654,6 +657,8 @@ static void control_plane_task_start_gate(void *arg) static void control_plane_on_model_command(control_plane_model_command_type_t command_type, const char *value, + size_t value_index, + bool value_index_valid, void *user_data) { (void)user_data; @@ -667,6 +672,8 @@ static void control_plane_on_model_command(control_plane_model_command_type_t co .command_type = command_type, .stop_requested = false, .value = {0}, + .value_index = value_index, + .value_index_valid = value_index_valid, }; if (value != NULL) { strlcpy(cmd.value, value, sizeof(cmd.value)); @@ -707,7 +714,33 @@ static esp_err_t control_plane_apply_wealth_direction(size_t direction_index, ESP_LOGW(TAG, "Wealth reveal request failed: %s", esp_err_to_name(reveal_err)); } - return control_plane_rotate_to_direction(direction_index); + (void)direction_index; + + float current_angle = domain_get_current_angle(); + return control_plane_rotate_to_angle(current_angle + CONTROL_PLANE_COMMAND_ROTATION_DEG); +} + +static esp_err_t control_plane_run_model_calibration_motion(void) +{ + float start_angle = domain_get_current_angle(); + + esp_err_t err = control_plane_rotate_to_angle(start_angle + CONTROL_PLANE_COMMAND_ROTATION_DEG); + if (err != ESP_OK) { + return err; + } + + return control_plane_rotate_to_angle(start_angle); +} + +static esp_err_t control_plane_show_chouqian_expression(size_t result_index, bool result_index_valid) +{ + const domain_expression_request_t request = { + .id = DOMAIN_EXPRESSION_CHOUQIAN, + .trigger = DOMAIN_EXPRESSION_TRIGGER_MODEL_DIRECTION, + .chouqian_result_index = result_index, + .chouqian_result_index_valid = result_index_valid, + }; + return domain_runtime_request_expression(&request); } static void control_plane_model_command_task(void *arg) @@ -750,15 +783,22 @@ static void control_plane_model_command_task(void *arg) ESP_LOGW(TAG, "Unknown model direction: %s", cmd.value); } } else if (cmd.command_type == CONTROL_PLANE_MODEL_COMMAND_CALIBRATE) { - ESP_LOGI(TAG, "Calibration triggered by model command"); - esp_err_t err = control_plane_calibrate(); + ESP_LOGI(TAG, "Calibration motion triggered by model command"); + esp_err_t err = control_plane_run_model_calibration_motion(); if (err != ESP_OK) { if (err == ESP_ERR_TIMEOUT || err == ESP_ERR_INVALID_STATE) { - ESP_LOGI(TAG, "Calibrate not completed: %s", esp_err_to_name(err)); + ESP_LOGI(TAG, "Calibration motion not completed: %s", esp_err_to_name(err)); } else { - ESP_LOGW(TAG, "Calibrate failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Calibration motion failed: %s", esp_err_to_name(err)); } } + } else if (cmd.command_type == CONTROL_PLANE_MODEL_COMMAND_CHOUQIAN) { + ESP_LOGI(TAG, "Chouqian triggered by model command: %s", cmd.value); + esp_err_t err = control_plane_show_chouqian_expression(cmd.value_index, + cmd.value_index_valid); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, "Chouqian expression failed: %s", esp_err_to_name(err)); + } } } diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index 131a34e..73ebedc 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -110,6 +110,8 @@ typedef struct { domain_expression_id_t id; domain_expression_trigger_t trigger; size_t direction_index; /* Only used by DOMAIN_EXPRESSION_WEALTH_DIRECTION */ + size_t chouqian_result_index; /* Optional for DOMAIN_EXPRESSION_CHOUQIAN */ + bool chouqian_result_index_valid; } domain_expression_request_t; esp_err_t domain_init(const domain_direction_axis_config_t *config, diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index 167d828..0e2f14f 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -1275,7 +1275,18 @@ static esp_err_t domain_resolve_expression_platform_request( static platform_lcd_presentation_step_t s_chouqian_steps[6]; const size_t result_count = sizeof(s_chouqian_result_asset_dirs) / sizeof(s_chouqian_result_asset_dirs[0]); - const char *result_asset = s_chouqian_result_asset_dirs[esp_random() % result_count]; + size_t result_index = esp_random() % result_count; + if (request->chouqian_result_index_valid) { + if (request->chouqian_result_index >= result_count) { + ESP_LOGW(TAG, + "Chouqian result index out of range: %u (max %u)", + (unsigned int)request->chouqian_result_index, + (unsigned int)result_count); + return ESP_ERR_INVALID_ARG; + } + result_index = request->chouqian_result_index; + } + const char *result_asset = s_chouqian_result_asset_dirs[result_index]; s_chouqian_steps[0] = (platform_lcd_presentation_step_t) { .asset_dir = "chouqian_qianzhi", diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index 1c362fe..b86bd4e 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -10,6 +10,7 @@ set(platform_srcs "src/runtime/platform_lcd_runtime.c" "src/platform_stepper_driver.c" "src/platform_digital_input_driver.c" + "src/platform_io_effects.c" "src/lcd_direct.c" "src/lcd_direct_assets.c" "src/multimodal/audio_pipeline.c" @@ -27,6 +28,7 @@ set(platform_priv_requires esp_driver_gpio esp_driver_i2c esp_driver_i2s + esp_driver_rmt esp_driver_spi esp_event esp_app_format diff --git a/components/platform/internal/platform_internal.h b/components/platform/internal/platform_internal.h index e93e9da..f00637f 100644 --- a/components/platform/internal/platform_internal.h +++ b/components/platform/internal/platform_internal.h @@ -16,5 +16,6 @@ esp_err_t platform_stepper_driver_rotate_angle_with_accel(float angle, int timeout_ms); void platform_stepper_driver_power_off(void); void platform_power_hold_init(void); +esp_err_t platform_io_effects_init(void); esp_err_t platform_digital_input_driver_configure(const platform_digital_input_config_t *config); bool platform_digital_input_driver_is_active(const platform_digital_input_config_t *config); diff --git a/components/platform/src/platform_io_effects.c b/components/platform/src/platform_io_effects.c new file mode 100644 index 0000000..6e64782 --- /dev/null +++ b/components/platform/src/platform_io_effects.c @@ -0,0 +1,313 @@ +/* + * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "platform_internal.h" + +#include +#include + +#include "driver/gpio.h" +#include "driver/rmt_tx.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "sdkconfig.h" + +#ifndef CONFIG_TQ_LOCAL_TASK_CORE +#define CONFIG_TQ_LOCAL_TASK_CORE 1 +#endif + +#if CONFIG_TQ_LOCAL_TASK_CORE < 0 +#define PLATFORM_IO_EFFECTS_TASK_CORE tskNO_AFFINITY +#else +#define PLATFORM_IO_EFFECTS_TASK_CORE ((BaseType_t)CONFIG_TQ_LOCAL_TASK_CORE) +#endif + +#define PLATFORM_RMT_GPIO GPIO_NUM_4 +#define PLATFORM_PULSE_GPIO GPIO_NUM_5 + +#define PLATFORM_PULSE_HIGH_MS 500 +#define PLATFORM_PULSE_LOW_MS 200 + +#define PLATFORM_RMT_LED_COUNT 22 +#define PLATFORM_RMT_LED_BYTES_PER_PIXEL 3 +#define PLATFORM_RMT_RESOLUTION_HZ 10000000 +#define PLATFORM_RMT_FRAME_MS 30 +#define PLATFORM_RMT_WAIT_DONE_MS 100 +#define PLATFORM_RMT_BRIGHTNESS_MIN 8 +#define PLATFORM_RMT_BRIGHTNESS_MAX 180 + +static const char *TAG = "platform_io_effects"; + +static TaskHandle_t s_pulse_task; +static TaskHandle_t s_rmt_task; + +static const rmt_symbol_word_t s_ws2812_zero = { + .level0 = 1, + .duration0 = PLATFORM_RMT_RESOLUTION_HZ / 10000000 * 3, + .level1 = 0, + .duration1 = PLATFORM_RMT_RESOLUTION_HZ / 10000000 * 9, +}; + +static const rmt_symbol_word_t s_ws2812_one = { + .level0 = 1, + .duration0 = PLATFORM_RMT_RESOLUTION_HZ / 10000000 * 9, + .level1 = 0, + .duration1 = PLATFORM_RMT_RESOLUTION_HZ / 10000000 * 3, +}; + +static const rmt_symbol_word_t s_ws2812_reset = { + .level0 = 0, + .duration0 = PLATFORM_RMT_RESOLUTION_HZ / 1000000 * 25, + .level1 = 0, + .duration1 = PLATFORM_RMT_RESOLUTION_HZ / 1000000 * 25, +}; + +static size_t platform_ws2812_encoder_callback(const void *data, + size_t data_size, + size_t symbols_written, + size_t symbols_free, + rmt_symbol_word_t *symbols, + bool *done, + void *arg) +{ + (void)arg; + + if (symbols_free < 8U) { + return 0U; + } + + size_t data_pos = symbols_written / 8U; + const uint8_t *data_bytes = (const uint8_t *)data; + if (data_pos < data_size) { + size_t symbol_pos = 0U; + for (uint8_t bitmask = 0x80U; bitmask != 0U; bitmask >>= 1U) { + symbols[symbol_pos++] = + (data_bytes[data_pos] & bitmask) ? s_ws2812_one : s_ws2812_zero; + } + return symbol_pos; + } + + symbols[0] = s_ws2812_reset; + *done = true; + return 1U; +} + +static uint8_t scale_u8(uint8_t value, uint8_t scale) +{ + return (uint8_t)(((uint16_t)value * (uint16_t)scale) / 255U); +} + +static void hsv_to_rgb(uint8_t hue, uint8_t value, uint8_t *red, uint8_t *green, uint8_t *blue) +{ + uint8_t region = hue / 43U; + uint8_t remainder = (uint8_t)((hue - (region * 43U)) * 6U); + uint8_t p = 0U; + uint8_t q = (uint8_t)(((uint16_t)value * (255U - remainder)) / 255U); + uint8_t t = (uint8_t)(((uint16_t)value * remainder) / 255U); + + switch (region) { + case 0: + *red = value; + *green = t; + *blue = p; + break; + case 1: + *red = q; + *green = value; + *blue = p; + break; + case 2: + *red = p; + *green = value; + *blue = t; + break; + case 3: + *red = p; + *green = q; + *blue = value; + break; + case 4: + *red = t; + *green = p; + *blue = value; + break; + default: + *red = value; + *green = p; + *blue = q; + break; + } +} + +static uint8_t breathing_brightness(uint16_t phase) +{ + uint16_t wave = phase & 0x01FFU; + if (wave > 255U) { + wave = 511U - wave; + } + + return (uint8_t)(PLATFORM_RMT_BRIGHTNESS_MIN + + ((wave * (PLATFORM_RMT_BRIGHTNESS_MAX - PLATFORM_RMT_BRIGHTNESS_MIN)) / 255U)); +} + +static void fill_breathing_pixels(uint8_t *pixels, size_t pixel_len, uint8_t hue_base, uint8_t brightness) +{ + if (pixel_len < (PLATFORM_RMT_LED_COUNT * PLATFORM_RMT_LED_BYTES_PER_PIXEL)) { + return; + } + + for (size_t led = 0; led < PLATFORM_RMT_LED_COUNT; ++led) { + uint8_t red = 0U; + uint8_t green = 0U; + uint8_t blue = 0U; + uint8_t hue = (uint8_t)(hue_base + ((led * 256U) / PLATFORM_RMT_LED_COUNT)); + hsv_to_rgb(hue, 255U, &red, &green, &blue); + + pixels[(led * 3U) + 0U] = scale_u8(green, brightness); + pixels[(led * 3U) + 1U] = scale_u8(red, brightness); + pixels[(led * 3U) + 2U] = scale_u8(blue, brightness); + } +} + +static void platform_pulse_task(void *arg) +{ + (void)arg; + + while (true) { + gpio_set_level(PLATFORM_PULSE_GPIO, 1); + vTaskDelay(pdMS_TO_TICKS(PLATFORM_PULSE_HIGH_MS)); + gpio_set_level(PLATFORM_PULSE_GPIO, 0); + vTaskDelay(pdMS_TO_TICKS(PLATFORM_PULSE_LOW_MS)); + } +} + +static void platform_rmt_led_task(void *arg) +{ + (void)arg; + + uint8_t pixels[PLATFORM_RMT_LED_COUNT * PLATFORM_RMT_LED_BYTES_PER_PIXEL] = {0}; + rmt_channel_handle_t led_channel = NULL; + rmt_encoder_handle_t led_encoder = NULL; + + rmt_tx_channel_config_t tx_channel_config = { + .gpio_num = PLATFORM_RMT_GPIO, + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = PLATFORM_RMT_RESOLUTION_HZ, + .mem_block_symbols = 64, + .trans_queue_depth = 4, + .flags = { + .invert_out = false, + .with_dma = false, + .io_loop_back = false, + .io_od_mode = false, + }, + }; + + esp_err_t err = rmt_new_tx_channel(&tx_channel_config, &led_channel); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Create RMT TX channel failed: %s", esp_err_to_name(err)); + s_rmt_task = NULL; + vTaskDelete(NULL); + return; + } + + rmt_simple_encoder_config_t encoder_config = { + .callback = platform_ws2812_encoder_callback, + .min_chunk_size = 64, + }; + err = rmt_new_simple_encoder(&encoder_config, &led_encoder); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Create RMT encoder failed: %s", esp_err_to_name(err)); + rmt_del_channel(led_channel); + s_rmt_task = NULL; + vTaskDelete(NULL); + return; + } + + err = rmt_enable(led_channel); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Enable RMT channel failed: %s", esp_err_to_name(err)); + rmt_del_encoder(led_encoder); + rmt_del_channel(led_channel); + s_rmt_task = NULL; + vTaskDelete(NULL); + return; + } + + ESP_LOGI(TAG, "GPIO4 RMT breathing effect started for %u LEDs", (unsigned)PLATFORM_RMT_LED_COUNT); + + rmt_transmit_config_t tx_config = { + .loop_count = 0, + }; + uint8_t hue_base = 0U; + uint16_t phase = 0U; + while (true) { + fill_breathing_pixels(pixels, sizeof(pixels), hue_base, breathing_brightness(phase)); + + err = rmt_transmit(led_channel, led_encoder, pixels, sizeof(pixels), &tx_config); + if (err == ESP_OK) { + err = rmt_tx_wait_all_done(led_channel, pdMS_TO_TICKS(PLATFORM_RMT_WAIT_DONE_MS)); + } + if (err != ESP_OK) { + ESP_LOGW(TAG, "RMT LED frame failed: %s", esp_err_to_name(err)); + } + + hue_base++; + phase += 4U; + vTaskDelay(pdMS_TO_TICKS(PLATFORM_RMT_FRAME_MS)); + } +} + +esp_err_t platform_io_effects_init(void) +{ + gpio_config_t pulse_config = { + .pin_bit_mask = BIT64(PLATFORM_PULSE_GPIO), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t err = gpio_config(&pulse_config); + if (err != ESP_OK) { + return err; + } + gpio_set_level(PLATFORM_PULSE_GPIO, 0); + + if (s_pulse_task == NULL) { + BaseType_t created = xTaskCreatePinnedToCore(platform_pulse_task, + "platform_pulse", + 2048, + NULL, + 2, + &s_pulse_task, + PLATFORM_IO_EFFECTS_TASK_CORE); + if (created != pdPASS) { + s_pulse_task = NULL; + return ESP_ERR_NO_MEM; + } + } + + if (s_rmt_task == NULL) { + BaseType_t created = xTaskCreatePinnedToCore(platform_rmt_led_task, + "platform_rmt_led", + 4096, + NULL, + 2, + &s_rmt_task, + PLATFORM_IO_EFFECTS_TASK_CORE); + if (created != pdPASS) { + s_rmt_task = NULL; + return ESP_ERR_NO_MEM; + } + } + + ESP_LOGI(TAG, + "GPIO5 pulse output started (%ums high, %ums low); GPIO4 RMT task starting", + (unsigned)PLATFORM_PULSE_HIGH_MS, + (unsigned)PLATFORM_PULSE_LOW_MS); + return ESP_OK; +} diff --git a/components/platform/src/runtime/platform_core_runtime.c b/components/platform/src/runtime/platform_core_runtime.c index 7700676..193ecd3 100644 --- a/components/platform/src/runtime/platform_core_runtime.c +++ b/components/platform/src/runtime/platform_core_runtime.c @@ -118,6 +118,11 @@ esp_err_t platform_init(void) platform_power_hold_init(); + esp_err_t io_effects_err = platform_io_effects_init(); + if (io_effects_err != ESP_OK) { + ESP_LOGW(TAG, "IO effects init failed: %s", esp_err_to_name(io_effects_err)); + } + #if CONFIG_TQ_STEPPER_ENABLE esp_err_t err = platform_stepper_driver_init(); if (err != ESP_OK) { diff --git a/docs/TQ_Cat_examples.md b/docs/TQ_Cat_examples.md index 62e8193..3966f51 100644 --- a/docs/TQ_Cat_examples.md +++ b/docs/TQ_Cat_examples.md @@ -20,7 +20,7 @@ ## B. 抽签 1. **问:** 帮我抽个签 - **答:** 签文:稳中求进。当下不用急着冲,像猫揣手手晒太阳一样,先把眼前小事做好,好时机自然来。 + **答:** `/*{"抽签":"上上签"}*/`当下不用急着冲,像猫揣手手晒太阳一样,先把眼前小事做好,好时机自然来。 2. **问:** 我想求个签 **答:** 签文:小满即安。今天少跟人较劲,多夸自己两句,就像发现猫碗里多了条小鱼干,小小的快乐也值得开心。 diff --git a/docs/firmware-pin-usage.md b/docs/firmware-pin-usage.md index e6c7e07..fc92611 100644 --- a/docs/firmware-pin-usage.md +++ b/docs/firmware-pin-usage.md @@ -12,6 +12,8 @@ is from the ESP32-S3 MCU perspective. | 1 | `CONFIG_MULTIMODAL_AUDIO_I2C_SDA_GPIO` | Bidirectional | Audio codec | ES8311 I2C SDA | I2C port 0, codec control bus | | 2 | `CONFIG_MULTIMODAL_AUDIO_I2C_SCL_GPIO` | Output | Audio codec | ES8311 I2C SCL | I2C port 0, codec control bus | | 3 | `CONFIG_TQ_SCREEN_RESET_PIN` | Output | LCD | ST7789 panel reset | Set high after GPIO init; `-1` disables hardware reset | +| 4 | IO4 RMT control | Output | RMT control | RMT output control | Driven by platform IO effects RMT task | +| 5 | IO5 PWM control | Output | PWM control | PWM output control | Driven by platform IO effects pulse task | | 6 | `CONFIG_TQ_SCREEN_SPI_CS_GPIO` | Output | LCD | ST7789 SPI chip select | SPI2 host | | 7 | `CONFIG_TQ_SCREEN_DC_GPIO` | Output | LCD | ST7789 data/command select | Initialized low | | 9 | `CONFIG_TQ_SCREEN_BACKLIGHT_GPIO` | Output | LCD | LCD backlight enable | Active-high by default | @@ -73,8 +75,6 @@ them to an active project feature. | GPIO / Net | Board function | Firmware status | | --- | --- | --- | | GPIO0 / K1 | Switch to GND, commonly boot/download related | Not consumed by application code | -| GPIO4 | Unassigned | Not used | -| GPIO5 | Unassigned | Not used | | GPIO8 | Unassigned | Not used | | GPIO10 / `SWING_PWM` | Swing coil control interface | No current firmware driver/config found | | GPIO11 | Unassigned | Not used | diff --git a/docs/gpio-map.md b/docs/gpio-map.md index 5ccf3b1..bf978e4 100644 --- a/docs/gpio-map.md +++ b/docs/gpio-map.md @@ -11,8 +11,8 @@ perspective. | 1 | GND | GND | N/A | Ground reference | Ground reference | | 2 | 3V3 | 3V3 | Power | 3.3 V supply rail | Power rail | | 3 | EN/RST | EN | Input | Chip enable and reset input | Reset and enable | -| 4 | GPIO4 | UNASSIGNED | N/A | No connection assigned | Reserved; no signal assigned | -| 5 | GPIO5 | UNASSIGNED | N/A | No connection assigned | Reserved; no signal assigned | +| 4 | GPIO4 | IO4_RMT | Output | RMT control | RMT output control | +| 5 | GPIO5 | IO5_PWM | Output | PWM control | PWM output control | | 6 | GPIO6 | ESP_IO6 | Output | Display interface CS | U9 | | 7 | GPIO7 | ESP_IO7 | Output | Display interface A0 (RS) | U9 | | 8 | GPIO15 | ESP_IO15 | Input | Function key K2 | 10 kOhm pull-up to 3V3; pulled to GND when pressed |