diff --git a/.github/workflows/build_t5ai.yml b/.github/workflows/build_t5ai.yml new file mode 100644 index 0000000..103905f --- /dev/null +++ b/.github/workflows/build_t5ai.yml @@ -0,0 +1,50 @@ +name: Build T5AI Firmware with Xiaozhi UI + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y git wget build-essential cmake ninja-build + pip install -r TuyaOpen/requirements.txt + + - name: Setup T5AI Platform + run: | + cd TuyaOpen + python tos.py build -v || true + + - name: Build Firmware + run: | + cd TuyaOpen + export TUYA_PROJECT_DIR=$PWD/.. + export TUYA_APP_NAME=DuckyClaw + export TUYA_APP_PATH=$PWD/../ + platform/T5AI/t5_os/build.sh DuckyClaw 1.0.0 bk7258 $PWD/../ build + + - name: Upload Firmware + uses: actions/upload-artifact@v4 + with: + name: DuckyClaw_Xiaozhi_Firmware + path: | + output/*.bin + TuyaOpen/platform/T5AI/t5_os/build/bk7258/tuya_app/package/*.bin + retention-days: 30 diff --git a/.github/workflows/build_t5ai_xiaozhi.yml b/.github/workflows/build_t5ai_xiaozhi.yml new file mode 100644 index 0000000..fbbbf4a --- /dev/null +++ b/.github/workflows/build_t5ai_xiaozhi.yml @@ -0,0 +1,119 @@ +name: Build T5AI Firmware with Xiaozhi UI + +on: + push: + branches: [ master, main ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo apt-get clean + df -h + + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Cache TuyaOpen + uses: actions/cache@v4 + with: + path: TuyaOpen + key: tuyaopen-${{ hashFiles('.gitmodules') }} + restore-keys: | + tuyaopen- + + - name: Init TuyaOpen submodules + run: | + git submodule update --init --recursive + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y git wget build-essential + pip install -r TuyaOpen/requirements.txt + + - name: Setup T5AI Platform + run: | + cd TuyaOpen + # Set up git safe directories + git config --global --add safe.directory "$(pwd)/platform/T5AI" + # Install ARM toolchain + if [ ! -d "platform/tools/gcc-arm-none-eabi-10.3-2021.10" ]; then + echo "Downloading ARM toolchain..." + wget -q https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 + tar -xjf gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 -C platform/tools/ + rm gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 + fi + echo "ARM toolchain ready" + + - name: Copy config file + run: | + cp -f .build/cache/using.config TuyaOpen/ + + - name: Build Firmware + run: | + cd TuyaOpen/platform/T5AI/t5_os + export TUYA_PROJECT_DIR="$(cd ../../../.. && pwd)" + export TUYA_APP_NAME="DuckyClaw" + export TUYA_APP_PATH="$TUYA_PROJECT_DIR" + export TUYA_TOOLCHAIN_PATH="$TUYA_PROJECT_DIR/TuyaOpen/platform/tools/gcc-arm-none-eabi-10.3-2021.10" + export PATH="$TUYA_TOOLCHAIN_PATH/bin:$PATH" + + # Create tuya_iot_config.h + mkdir -p include/base/include + cp "$TUYA_PROJECT_DIR/include/tuya_app_config.h" include/base/include/tuya_iot_config.h + + echo "Starting build..." + bash build.sh DuckyClaw 1.0.0 bk7258 "$TUYA_PROJECT_DIR" build 2>&1 | tee build.log + + - name: Package Firmware + run: | + cd TuyaOpen/platform/T5AI/t5_os + if [ -f "build/bk7258/tuya_app/package/ua_file.bin" ]; then + cp build/bk7258/tuya_app/package/ua_file.bin "$GITHUB_WORKSPACE/DuckyClaw_Xiaozhi_UA.bin" + cp build/bk7258/tuya_app/package/all-app.bin "$GITHUB_WORKSPACE/DuckyClaw_Xiaozhi_QIO.bin" + echo "Firmware packaged successfully" + ls -lh "$GITHUB_WORKSPACE/"*.bin + else + echo "Build artifacts not found" + exit 1 + fi + + - name: Upload Firmware + uses: actions/upload-artifact@v4 + with: + name: DuckyClaw_Xiaozhi_Firmware + path: | + DuckyClaw_Xiaozhi_UA.bin + DuckyClaw_Xiaozhi_QIO.bin + TuyaOpen/platform/T5AI/t5_os/build/bk7258/tuya_app/*/app.bin + TuyaOpen/platform/T5AI/t5_os/build.log + retention-days: 30 + if-no-files-found: error + + - name: Upload Release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: | + DuckyClaw_Xiaozhi_UA.bin + DuckyClaw_Xiaozhi_QIO.bin + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 405ad32..ec86508 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ aux_source_directory(${APP_PATH}/agent APP_SRCS) set(APP_INC ${APP_PATH} ${APP_PATH}/include + ${APP_PATH}/src ${APP_PATH}/tools ${APP_PATH}/cron_service ${APP_PATH}/heartbeat diff --git a/GITHUB_ACTIONS_GUIDE.md b/GITHUB_ACTIONS_GUIDE.md new file mode 100644 index 0000000..fdf7a87 --- /dev/null +++ b/GITHUB_ACTIONS_GUIDE.md @@ -0,0 +1,94 @@ +# GitHub Actions 自动编译指南 + +## 快速开始 + +### 步骤 1:Fork 仓库 +1. 访问 https://github.com/tuya/DuckyClaw +2. 点击右上角的 "Fork" 按钮 +3. 选择您的 GitHub 账户 + +### 步骤 2:克隆 Fork 的仓库到本地 +```bash +git clone https://github.com/YOUR_USERNAME/DuckyClaw.git +cd DuckyClaw +``` + +### 步骤 3:添加 XIAOZHI UI 代码 +我已为您创建了所有必要的 XIAOZHI UI 文件。确保这些文件已提交: +- `ai_components/ai_ui/src/ai_ui_chat_xiaozhi.c` +- `ai_components/ai_ui/src/ai_ui_standby_face.c` +- `ai_components/ai_ui/include/ai_ui_chat_xiaozhi.h` +- `ai_components/ai_ui/include/ai_ui_standby_face.h` +- `.github/workflows/build_t5ai_xiaozhi.yml` + +### 步骤 4:推送代码到您的 Fork +```bash +git add . +git commit -m "Add XIAOZHI UI with animated standby face" +git push origin master +``` + +### 步骤 5:查看 GitHub Actions 构建 +1. 访问您的 Fork 仓库:https://github.com/YOUR_USERNAME/DuckyClaw +2. 点击 "Actions" 标签 +3. 等待 "Build T5AI Firmware with Xiaozhi UI" 工作流完成(约 10-15 分钟) +4. 构建完成后,点击最新的运行记录 +5. 在 "Artifacts" 部分下载 `DuckyClaw_Xiaozhi_Firmware` + +### 步骤 6:烧录固件 +1. 解压下载的固件包 +2. 使用 Tuya Uart Tool 烧录 `DuckyClaw_Xiaozhi_UA.bin` + +--- + +## 手动触发构建 + +如果需要重新构建: +1. 访问您的 Fork 仓库 Actions 页面 +2. 选择 "Build T5AI Firmware with Xiaozhi UI" +3. 点击 "Run workflow" 按钮 +4. 选择分支(master)并点击 "Run workflow" + +--- + +## 故障排除 + +### 构建失败 +- 检查 GitHub Actions 日志获取详细错误信息 +- 确保所有子模块已正确初始化 +- 检查 TuyaOpen 平台代码是否完整 + +### 固件无法启动 +- 确认烧录的是 UA 文件(不是 QIO) +- 使用 Tuya Uart Tool 查看串口日志(波特率 460800) +- 检查是否有权限问题 + +--- + +## 本地构建(高级用户) + +如果您有 Linux 环境: + +```bash +# 安装依赖 +sudo apt-get install git wget build-essential cmake ninja-build + +# 进入 T5AI 平台目录 +cd TuyaOpen/platform/T5AI/t5_os + +# 设置环境变量 +export TUYA_PROJECT_DIR=$(cd ../../.. && pwd) +export TUYA_APP_NAME="DuckyClaw" +export TUYA_TOOLCHAIN_PATH="$TUYA_PROJECT_DIR/platform/tools/gcc-arm-none-eabi-10.3-2021.10" +export PATH="$TUYA_TOOLCHAIN_PATH/bin:$PATH" + +# 创建配置文件 +mkdir -p include/base/include +cp "$TUYA_PROJECT_DIR/include/tuya_app_config.h" include/base/include/tuya_iot_config.h + +# 构建 +bash build.sh DuckyClaw 1.0.0 bk7258 "$TUYA_PROJECT_DIR" build + +# 输出位置 +# build/bk7258/tuya_app/package/ua_file.bin +``` diff --git a/ai_components/ai_main/src/ai_chat_ui.c b/ai_components/ai_main/src/ai_chat_ui.c index b50ad12..32ed895 100644 --- a/ai_components/ai_main/src/ai_chat_ui.c +++ b/ai_components/ai_main/src/ai_chat_ui.c @@ -16,6 +16,8 @@ #include "ai_ui_chat_chatbot.h" #elif defined(ENABLE_AI_CHAT_GUI_OLED) && (ENABLE_AI_CHAT_GUI_OLED == 1) #include "ai_ui_chat_oled.h" +#elif defined(ENABLE_AI_CHAT_GUI_XIAOZHI) && (ENABLE_AI_CHAT_GUI_XIAOZHI == 1) +#include "ai_ui_chat_xiaozhi.h" #endif #include "ai_chat_main.h" @@ -40,8 +42,13 @@ static void __ai_chat_disp_mode_state(AI_MODE_STATE_E state) switch (state) { case AI_MODE_STATE_INIT: case AI_MODE_STATE_IDLE: +#if defined(ENABLE_AI_CHAT_GUI_XIAOZHI) && (ENABLE_AI_CHAT_GUI_XIAOZHI == 1) + /* Xiaozhi UI handles standby mode internally */ + ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)STANDBY, strlen(STANDBY)); +#else ai_ui_disp_msg(AI_UI_DISP_EMOTION, (uint8_t *)EMOJI_NEUTRAL, strlen(EMOJI_NEUTRAL)); ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)STANDBY, strlen(STANDBY)); +#endif break; case AI_MODE_STATE_LISTEN: ai_ui_disp_msg(AI_UI_DISP_STATUS, (uint8_t *)LISTENING, strlen(LISTENING)); @@ -148,6 +155,8 @@ OPERATE_RET ai_chat_ui_init(void) TUYA_CALL_ERR_RETURN(ai_ui_chat_chatbot_register()); #elif defined(ENABLE_AI_CHAT_GUI_OLED) && (ENABLE_AI_CHAT_GUI_OLED == 1) TUYA_CALL_ERR_RETURN(ai_ui_chat_oled_register()); +#elif defined(ENABLE_AI_CHAT_GUI_XIAOZHI) && (ENABLE_AI_CHAT_GUI_XIAOZHI == 1) + TUYA_CALL_ERR_RETURN(ai_ui_chat_xiaozhi_register()); #else #error "please select ai chat present ui" #endif diff --git a/ai_components/ai_ui/Kconfig b/ai_components/ai_ui/Kconfig index 71a7cc3..42c551e 100644 --- a/ai_components/ai_ui/Kconfig +++ b/ai_components/ai_ui/Kconfig @@ -24,6 +24,11 @@ if(!ENABLE_AI_CHAT_CUSTOM_UI) config ENABLE_AI_CHAT_GUI_OLED select ENABLE_LIBLVGL bool "Use OLED ui" + + config ENABLE_AI_CHAT_GUI_XIAOZHI + select ENABLE_LIBLVGL + select FONT_EMOJI_SIZE_64 + bool "Use Xiaozhi-style standby face ui" endchoice config ENABLE_CIRCLE_UI_STYLE diff --git a/ai_components/ai_ui/include/ai_ui_chat_xiaozhi.h b/ai_components/ai_ui/include/ai_ui_chat_xiaozhi.h new file mode 100644 index 0000000..6343a00 --- /dev/null +++ b/ai_components/ai_ui/include/ai_ui_chat_xiaozhi.h @@ -0,0 +1,50 @@ +/** + * @file ai_ui_chat_xiaozhi.h + * @brief Xiaozhi-style chat UI with standby face animation interface. + * + * @copyright Copyright (c) 2021-2025 Tuya Inc. All Rights Reserved. + */ + +#ifndef __AI_UI_CHAT_XIAOZHI_H__ +#define __AI_UI_CHAT_XIAOZHI_H__ + +#include "tuya_cloud_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/*********************************************************** +************************macro define************************ +***********************************************************/ + +/* Emotion types for standby face */ +typedef enum { + XIAOZHI_EMO_HAPPY, + XIAOZHI_EMO_RELAXED, + XIAOZHI_EMO_CURIOUS, + XIAOZHI_EMO_SLEEPY, + XIAOZHI_EMO_EXCITED, + XIAOZHI_EMO_MAX, +} XIAOZHI_EMO_TYPE_E; + +/*********************************************************** +********************function declaration********************/ + +/** + * @brief Register Xiaozhi-style chat UI. + * + * This UI provides: + * - Animated standby face with blinking eyes + * - Automatic switching between standby and chat modes + * - Smooth LVGL-based animations + * + * @return OPERATE_RET Operation result code. + */ +OPERATE_RET ai_ui_chat_xiaozhi_register(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __AI_UI_CHAT_XIAOZHI_H__ */ diff --git a/ai_components/ai_ui/include/ai_ui_standby_face.h b/ai_components/ai_ui/include/ai_ui_standby_face.h new file mode 100644 index 0000000..97d2e59 --- /dev/null +++ b/ai_components/ai_ui/include/ai_ui_standby_face.h @@ -0,0 +1,113 @@ +/** + * @file ai_ui_standby_face.h + * @brief Standby face animation UI interface definitions. + * + * This header provides function declarations for the standby face UI, + * including animated expressions (blinking eyes, emotion cycling) and + * transition to chat interface. + * + * @copyright Copyright (c) 2021-2025 Tuya Inc. All Rights Reserved. + */ + +#ifndef __AI_UI_STANDBY_FACE_H__ +#define __AI_UI_STANDBY_FACE_H__ + +#include "tuya_cloud_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/*********************************************************** +************************macro define************************ +***********************************************************/ + +/* Standby face animation states */ +typedef enum { + STANDBY_FACE_STATE_IDLE, /* Normal idle - blinking */ + STANDBY_FACE_STATE_LISTENING, /* Listening - wide eyes */ + STANDBY_FACE_STATE_THINKING, /* Thinking - looking around */ + STANDBY_FACE_STATE_RESPONDING, /* Responding - excited */ + STANDBY_FACE_STATE_SLEEPING, /* Sleeping - closed eyes */ +} STANDBY_FACE_STATE_E; + +/* Emotion types for standby face */ +typedef enum { + STANDBY_EMO_HAPPY, + STANDBY_EMO_RELAXED, + STANDBY_EMO_CURIOUS, + STANDBY_EMO_SLEEPY, + STANDBY_EMO_EXCITED, + STANDBY_EMO_MAX, +} STANDBY_EMO_TYPE_E; + +/*********************************************************** +********************function declaration******************** +***********************************************************/ + +/** + * @brief Initialize standby face UI. + * + * @return OPERATE_RET Operation result code. + */ +OPERATE_RET ai_ui_standby_face_init(void); + +/** + * @brief Deinitialize standby face UI. + * + * @return OPERATE_RET Operation result code. + */ +OPERATE_RET ai_ui_standby_face_deinit(void); + +/** + * @brief Show standby face UI on screen. + * + * @return OPERATE_RET Operation result code. + */ +OPERATE_RET ai_ui_standby_face_show(void); + +/** + * @brief Hide standby face UI. + * + * @return OPERATE_RET Operation result code. + */ +OPERATE_RET ai_ui_standby_face_hide(void); + +/** + * @brief Set standby face animation state. + * + * @param state Animation state (idle, listening, thinking, etc.) + * @return OPERATE_RET Operation result code. + */ +OPERATE_RET ai_ui_standby_face_set_state(STANDBY_FACE_STATE_E state); + +/** + * @brief Set standby face emotion. + * + * @param emotion Emotion type + * @return OPERATE_RET Operation result code. + */ +OPERATE_RET ai_ui_standby_face_set_emotion(STANDBY_EMO_TYPE_E emotion); + +/** + * @brief Start blinking animation. + */ +void ai_ui_standby_face_start_blink(void); + +/** + * @brief Stop blinking animation. + */ +void ai_ui_standby_face_stop_blink(void); + +/** + * @brief Check if standby face is currently visible. + * + * @return true if visible, false otherwise + */ +bool ai_ui_standby_face_is_visible(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __AI_UI_STANDBY_FACE_H__ */ diff --git a/ai_components/ai_ui/src/ai_ui_chat_xiaozhi.c b/ai_components/ai_ui/src/ai_ui_chat_xiaozhi.c new file mode 100644 index 0000000..0763a04 --- /dev/null +++ b/ai_components/ai_ui/src/ai_ui_chat_xiaozhi.c @@ -0,0 +1,534 @@ +/** + * @file ai_ui_chat_xiaozhi.c + * @brief Xiaozhi-style chat UI with standby face animation. + * + * This file provides a combined UI that shows a cute animated standby face + * when idle, and switches to a chat interface during conversation. + * Features: + * - Animated standby face with blinking eyes + * - Automatic switching between standby and chat modes + * - Smooth LVGL animations + * + * @copyright Copyright (c) 2021-2025 Tuya Inc. All Rights Reserved. + */ + +#include "tal_api.h" + +#if defined(ENABLE_AI_CHAT_GUI_XIAOZHI) && (ENABLE_AI_CHAT_GUI_XIAOZHI == 1) + +#include "lvgl.h" +#include "lv_vendor.h" + +#include "font_awesome_symbols.h" +#include "ai_ui_manage.h" +#include "ai_ui_icon_font.h" +#include "ai_ui_chat_xiaozhi.h" + +#include "lang_config.h" + +/*********************************************************** +************************macro define************************ +***********************************************************/ +/* Animation timing (ms) */ +#define BLINK_INTERVAL_MIN 2000 +#define BLINK_INTERVAL_MAX 5000 +#define BLINK_DURATION 150 +#define EMOTION_CYCLE_INTERVAL 10000 /* 10 seconds between emotion changes */ +#define IDLE_TIMEOUT_MS 30000 /* Switch to standby after 30s idle */ + +/* Eye colors */ +#define FACE_BG_COLOR lv_color_hex(0x1A1A2E) +#define EYE_COLOR lv_color_hex(0x4ECDC4) +#define EYE_GLOW_COLOR lv_color_hex(0x45B7AA) + +/* UI states */ +typedef enum { + UI_STATE_STANDBY, /* Showing standby face */ + UI_STATE_CHAT, /* Showing chat interface */ +} UI_STATE_E; + +/*********************************************************** +***********************typedef define***********************/ +typedef struct { + /* Standby face objects */ + lv_obj_t *standby_container; + lv_obj_t *left_eye; + lv_obj_t *right_eye; + lv_obj_t *hint_label; + + /* Chat objects */ + lv_obj_t *chat_container; + lv_obj_t *status_bar; + lv_obj_t *emotion_label; + lv_obj_t *chat_message_label; + lv_obj_t *status_label; + lv_obj_t *network_label; + lv_obj_t *chat_mode_label; + + /* State management */ + UI_STATE_E current_state; + XIAOZHI_EMO_TYPE_E current_emotion; + bool is_blinking; + bool is_streaming; + + /* LVGL timers */ + lv_timer_t *blink_timer; + lv_timer_t *emotion_timer; + lv_timer_t *idle_timer; + + /* Theme colors */ + lv_color_t bg_color; + lv_color_t text_color; + lv_color_t user_bubble_color; + lv_color_t assistant_bubble_color; +} XIAOZHI_UI_CTX_T; + +/*********************************************************** +***********************variable define**********************/ +static XIAOZHI_UI_CTX_T sg_ctx = {0}; +static AI_UI_FONT_LIST_T sg_font = {0}; + +/* Emotion configurations - eye shapes */ +typedef struct { + int eye_width; + int eye_height; + int eye_spacing; + int eye_y_offset; + const char *hint_text; +} emotion_config_t; + +static const emotion_config_t emotion_configs[XIAOZHI_EMO_MAX] = { + {45, 40, 30, 0, LANG_STANDBY_HINT_HAPPY}, /* HAPPY */ + {40, 35, 25, 5, LANG_STANDBY_HINT_RELAXED}, /* RELAXED */ + {50, 45, 25, -5, LANG_STANDBY_HINT_CURIOUS}, /* CURIOUS */ + {40, 20, 30, 10, LANG_STANDBY_HINT_SLEEPY}, /* SLEEPY */ + {50, 50, 25, -10, LANG_STANDBY_HINT_EXCITED}, /* EXCITED */ +}; + +/*********************************************************** +***********************helper functions********************/ + +static uint32_t __get_random_blink_interval(void) +{ + uint32_t range = BLINK_INTERVAL_MAX - BLINK_INTERVAL_MIN; + return BLINK_INTERVAL_MIN + tal_system_get_random(range); +} + +static void __update_eye_positions(void) +{ + if (!sg_ctx.left_eye || !sg_ctx.right_eye) return; + + const emotion_config_t *cfg = &emotion_configs[sg_ctx.current_emotion]; + lv_coord_t center_x = LV_HOR_RES / 2; + lv_coord_t center_y = LV_VER_RES / 2 + cfg->eye_y_offset; + + lv_coord_t left_x = center_x - cfg->eye_spacing - cfg->eye_width / 2; + lv_coord_t right_x = center_x + cfg->eye_spacing - cfg->eye_width / 2; + lv_coord_t eye_y = center_y - cfg->eye_height / 2; + + lv_obj_set_pos(sg_ctx.left_eye, left_x, eye_y); + lv_obj_set_size(sg_ctx.left_eye, cfg->eye_width, cfg->eye_height); + + lv_obj_set_pos(sg_ctx.right_eye, right_x, eye_y); + lv_obj_set_size(sg_ctx.right_eye, cfg->eye_width, cfg->eye_height); + + if (sg_ctx.hint_label) { + lv_label_set_text(sg_ctx.hint_label, cfg->hint_text); + } +} + +static void __blink_animation(void) +{ + if (!sg_ctx.left_eye || !sg_ctx.right_eye) return; + + const emotion_config_t *cfg = &emotion_configs[sg_ctx.current_emotion]; + lv_coord_t closed_height = 4; + + lv_anim_t anim; + lv_anim_init(&anim); + lv_anim_set_time(&anim, BLINK_DURATION / 2); + lv_anim_set_path_cb(&anim, lv_anim_path_ease_in_out); + + /* Close eyes */ + sg_ctx.is_blinking = true; + lv_anim_set_var(&anim, sg_ctx.left_eye); + lv_anim_set_values(&anim, cfg->eye_height, closed_height); + lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_height); + lv_anim_start(&anim); + + lv_anim_set_var(&anim, sg_ctx.right_eye); + lv_anim_start(&anim); +} + +static void __unblink_animation(void) +{ + if (!sg_ctx.left_eye || !sg_ctx.right_eye) return; + + const emotion_config_t *cfg = &emotion_configs[sg_ctx.current_emotion]; + + lv_anim_t anim; + lv_anim_init(&anim); + lv_anim_set_time(&anim, BLINK_DURATION / 2); + lv_anim_set_path_cb(&anim, lv_anim_path_ease_in_out); + + /* Open eyes */ + lv_anim_set_var(&anim, sg_ctx.left_eye); + lv_anim_set_values(&anim, 4, cfg->eye_height); + lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_height); + lv_anim_start(&anim); + + lv_anim_set_var(&anim, sg_ctx.right_eye); + lv_anim_start(&anim); + + sg_ctx.is_blinking = false; +} + +static void __blink_timer_cb(lv_timer_t *timer) +{ + (void)timer; + + if (sg_ctx.current_state != UI_STATE_STANDBY) return; + + if (!sg_ctx.is_blinking) { + __blink_animation(); + lv_timer_set_period(sg_ctx.blink_timer, BLINK_DURATION); + } else { + __unblink_animation(); + lv_timer_set_period(sg_ctx.blink_timer, __get_random_blink_interval()); + } +} + +static void __emotion_timer_cb(lv_timer_t *timer) +{ + (void)timer; + + if (sg_ctx.current_state != UI_STATE_STANDBY) return; + + /* Cycle to next emotion */ + sg_ctx.current_emotion = (sg_ctx.current_emotion + 1) % XIAOZHI_EMO_MAX; + __update_eye_positions(); +} + +static lv_obj_t *__create_eye(lv_obj_t *parent) +{ + lv_obj_t *eye = lv_obj_create(parent); + lv_obj_set_style_bg_color(eye, EYE_COLOR, 0); + lv_obj_set_style_border_width(eye, 0, 0); + lv_obj_set_style_radius(eye, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_shadow_color(eye, EYE_GLOW_COLOR, 0); + lv_obj_set_style_shadow_width(eye, 15, 0); + lv_obj_set_style_shadow_spread(eye, 3, 0); + lv_obj_clear_flag(eye, LV_OBJ_FLAG_SCROLLABLE); + return eye; +} + +/*********************************************************** +***********************UI creation************************/ + +static void __create_standby_ui(void) +{ + /* Standby container (full screen) */ + sg_ctx.standby_container = lv_obj_create(lv_screen_active()); + lv_obj_set_size(sg_ctx.standby_container, LV_HOR_RES, LV_VER_RES); + lv_obj_set_style_bg_color(sg_ctx.standby_container, FACE_BG_COLOR, 0); + lv_obj_set_style_border_width(sg_ctx.standby_container, 0, 0); + lv_obj_clear_flag(sg_ctx.standby_container, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(sg_ctx.standby_container, LV_OBJ_FLAG_HIDDEN); + + /* Create eyes */ + sg_ctx.left_eye = __create_eye(sg_ctx.standby_container); + sg_ctx.right_eye = __create_eye(sg_ctx.standby_container); + + /* Hint label */ + sg_ctx.hint_label = lv_label_create(sg_ctx.standby_container); + lv_obj_set_style_text_color(sg_ctx.hint_label, lv_color_white(), 0); + lv_obj_set_style_text_font(sg_ctx.hint_label, sg_font.text, 0); + lv_obj_align(sg_ctx.hint_label, LV_ALIGN_BOTTOM_MID, 0, -30); + + __update_eye_positions(); +} + +static void __create_chat_ui(void) +{ + /* Chat container (full screen) */ + sg_ctx.chat_container = lv_obj_create(lv_screen_active()); + lv_obj_set_size(sg_ctx.chat_container, LV_HOR_RES, LV_VER_RES); + lv_obj_set_flex_flow(sg_ctx.chat_container, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(sg_ctx.chat_container, 0, 0); + lv_obj_set_style_border_width(sg_ctx.chat_container, 0, 0); + lv_obj_set_style_bg_color(sg_ctx.chat_container, sg_ctx.bg_color, 0); + lv_obj_add_flag(sg_ctx.chat_container, LV_OBJ_FLAG_HIDDEN); + + /* Status bar */ + sg_ctx.status_bar = lv_obj_create(sg_ctx.chat_container); + lv_obj_set_size(sg_ctx.status_bar, LV_HOR_RES, sg_font.text->line_height); + lv_obj_set_style_radius(sg_ctx.status_bar, 0, 0); + lv_obj_set_style_bg_color(sg_ctx.status_bar, sg_ctx.bg_color, 0); + + /* Content area */ + lv_obj_t *content = lv_obj_create(sg_ctx.chat_container); + lv_obj_set_scrollbar_mode(content, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_radius(content, 0, 0); + lv_obj_set_width(content, LV_HOR_RES); + lv_obj_set_flex_grow(content, 1); + lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(content, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY); + lv_obj_set_style_bg_color(content, sg_ctx.bg_color, 0); + + /* Emotion label (large emoji) */ + sg_ctx.emotion_label = lv_label_create(content); + lv_obj_set_style_text_font(sg_ctx.emotion_label, sg_font.emoji, 0); + lv_label_set_text(sg_ctx.emotion_label, sg_font.emoji_list[0].emo_icon); + + /* Chat message label */ + sg_ctx.chat_message_label = lv_label_create(content); + lv_label_set_text(sg_ctx.chat_message_label, ""); + lv_obj_set_width(sg_ctx.chat_message_label, LV_HOR_RES * 0.9); + lv_obj_set_height(sg_ctx.chat_message_label, LV_VER_RES * 0.4); + lv_label_set_long_mode(sg_ctx.chat_message_label, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_align(sg_ctx.chat_message_label, LV_TEXT_ALIGN_CENTER, 0); + + /* Status label */ + sg_ctx.status_label = lv_label_create(sg_ctx.status_bar); + lv_obj_set_style_text_color(sg_ctx.status_label, sg_ctx.text_color, 0); + lv_label_set_text(sg_ctx.status_label, INITIALIZING); + lv_obj_center(sg_ctx.status_label); + + /* Network icon */ + sg_ctx.network_label = lv_label_create(sg_ctx.status_bar); + lv_obj_set_style_text_font(sg_ctx.network_label, sg_font.icon, 0); + lv_obj_set_style_text_color(sg_ctx.network_label, sg_ctx.text_color, 0); + lv_obj_align(sg_ctx.network_label, LV_ALIGN_RIGHT_MID, -5, 0); + + /* Chat mode label */ + sg_ctx.chat_mode_label = lv_label_create(sg_ctx.status_bar); + lv_obj_set_style_text_color(sg_ctx.chat_mode_label, sg_ctx.text_color, 0); + lv_obj_align(sg_ctx.chat_mode_label, LV_ALIGN_LEFT_MID, 5, 0); +} + +/*********************************************************** +***********************state management********************/ + +static void __switch_to_standby(void) +{ + if (sg_ctx.current_state == UI_STATE_STANDBY) return; + + sg_ctx.current_state = UI_STATE_STANDBY; + + lv_obj_add_flag(sg_ctx.chat_container, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_ctx.standby_container, LV_OBJ_FLAG_HIDDEN); + + /* Start animations */ + if (sg_ctx.blink_timer) { + lv_timer_set_period(sg_ctx.blink_timer, __get_random_blink_interval()); + lv_timer_resume(sg_ctx.blink_timer); + } + if (sg_ctx.emotion_timer) { + lv_timer_resume(sg_ctx.emotion_timer); + } +} + +static void __switch_to_chat(void) +{ + if (sg_ctx.current_state == UI_STATE_CHAT) return; + + sg_ctx.current_state = UI_STATE_CHAT; + + /* Stop standby animations */ + if (sg_ctx.blink_timer) lv_timer_pause(sg_ctx.blink_timer); + if (sg_ctx.emotion_timer) lv_timer_pause(sg_ctx.emotion_timer); + + lv_obj_add_flag(sg_ctx.standby_container, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(sg_ctx.chat_container, LV_OBJ_FLAG_HIDDEN); +} + +/*********************************************************** +***********************UI interface functions****************/ + +static void __ui_font_init(void) +{ + sg_font.text = ai_ui_get_text_font(); + sg_font.icon = ai_ui_get_icon_font(); + sg_font.emoji = ai_ui_get_emo_font(); + sg_font.emoji_list = ai_ui_get_emo_list(); +} + +static OPERATE_RET __ui_init(void) +{ + lv_vendor_init(DISPLAY_NAME); + lv_vendor_start(5, 1024*8); + + lv_vendor_disp_lock(); + + __ui_font_init(); + + /* Theme colors */ + sg_ctx.bg_color = lv_color_white(); + sg_ctx.text_color = lv_color_black(); + sg_ctx.user_bubble_color = lv_color_hex(0x95EC69); + sg_ctx.assistant_bubble_color = lv_color_white(); + + /* Create UIs */ + __create_standby_ui(); + __create_chat_ui(); + + /* Create timers */ + sg_ctx.blink_timer = lv_timer_create(__blink_timer_cb, BLINK_INTERVAL_MAX, NULL); + lv_timer_pause(sg_ctx.blink_timer); + + sg_ctx.emotion_timer = lv_timer_create(__emotion_timer_cb, EMOTION_CYCLE_INTERVAL, NULL); + lv_timer_pause(sg_ctx.emotion_timer); + + /* Start in standby mode */ + sg_ctx.current_emotion = XIAOZHI_EMO_HAPPY; + __switch_to_standby(); + + lv_vendor_disp_unlock(); + + return OPRT_OK; +} + +static void __ui_set_user_msg(char *text) +{ + if (!sg_ctx.chat_message_label) return; + + __switch_to_chat(); + + lv_vendor_disp_lock(); + lv_label_set_text(sg_ctx.chat_message_label, text); + lv_obj_set_style_bg_color(sg_ctx.chat_message_label, sg_ctx.user_bubble_color, 0); + lv_obj_set_style_text_color(sg_ctx.chat_message_label, sg_ctx.text_color, 0); + lv_vendor_disp_unlock(); +} + +static void __ui_set_ai_msg(char *text) +{ + if (!sg_ctx.chat_message_label) return; + + __switch_to_chat(); + + lv_vendor_disp_lock(); + lv_label_set_text(sg_ctx.chat_message_label, text); + lv_obj_set_style_bg_color(sg_ctx.chat_message_label, sg_ctx.assistant_bubble_color, 0); + lv_obj_set_style_text_color(sg_ctx.chat_message_label, sg_ctx.text_color, 0); + lv_vendor_disp_unlock(); +} + +static void __ui_set_ai_msg_stream_start(void) +{ + __switch_to_chat(); + sg_ctx.is_streaming = true; + + lv_vendor_disp_lock(); + lv_label_set_text(sg_ctx.chat_message_label, ""); + lv_vendor_disp_unlock(); +} + +static void __ui_set_ai_msg_stream_data(char *text) +{ + if (!sg_ctx.chat_message_label || !sg_ctx.is_streaming) return; + + lv_vendor_disp_lock(); + lv_label_ins_text(sg_ctx.chat_message_label, LV_LABEL_POS_LAST, text); + lv_vendor_disp_unlock(); +} + +static void __ui_set_ai_msg_stream_end(void) +{ + sg_ctx.is_streaming = false; +} + +static void __ui_set_system_msg(char *text) +{ + if (!sg_ctx.chat_message_label) return; + + __switch_to_chat(); + + lv_vendor_disp_lock(); + lv_label_set_text(sg_ctx.chat_message_label, text); + lv_obj_set_style_text_color(sg_ctx.chat_message_label, lv_color_hex(0x666666), 0); + lv_vendor_disp_unlock(); +} + +static void __ui_set_emotion(char *emotion) +{ + if (!sg_ctx.emotion_label) return; + + char *emo_icon = sg_font.emoji_list[0].emo_icon; + for (int i = 0; i < FONT_EMO_ICON_MAX_NUM; i++) { + if (strcmp(emotion, sg_font.emoji_list[i].emo_name) == 0) { + emo_icon = sg_font.emoji_list[i].emo_icon; + break; + } + } + + lv_vendor_disp_lock(); + lv_obj_set_style_text_font(sg_ctx.emotion_label, sg_font.emoji, 0); + lv_label_set_text(sg_ctx.emotion_label, emo_icon); + lv_vendor_disp_unlock(); +} + +static void __ui_set_status(char *status) +{ + if (!sg_ctx.status_label) return; + + lv_vendor_disp_lock(); + lv_label_set_text(sg_ctx.status_label, status); + lv_vendor_disp_unlock(); +} + +static void __ui_set_notification(char *notification) +{ + if (!sg_ctx.status_label) return; + + lv_vendor_disp_lock(); + lv_label_set_text(sg_ctx.status_label, notification); + lv_vendor_disp_unlock(); +} + +static void __ui_set_network(AI_UI_WIFI_STATUS_E wifi_status) +{ + if (!sg_ctx.network_label) return; + + char *wifi_icon = ai_ui_get_wifi_icon(wifi_status); + + lv_vendor_disp_lock(); + lv_label_set_text(sg_ctx.network_label, wifi_icon); + lv_vendor_disp_unlock(); +} + +static void __ui_set_chat_mode(char *chat_mode) +{ + if (!sg_ctx.chat_mode_label || !chat_mode) return; + + lv_vendor_disp_lock(); + lv_label_set_text(sg_ctx.chat_mode_label, chat_mode); + lv_vendor_disp_unlock(); +} + +/*********************************************************** +***********************public interface**********************/ + +OPERATE_RET ai_ui_chat_xiaozhi_register(void) +{ + AI_UI_INTFS_T intfs; + + memset(&intfs, 0, sizeof(AI_UI_INTFS_T)); + + intfs.disp_init = __ui_init; + intfs.disp_user_msg = __ui_set_user_msg; + intfs.disp_ai_msg = __ui_set_ai_msg; + intfs.disp_ai_msg_stream_start = __ui_set_ai_msg_stream_start; + intfs.disp_ai_msg_stream_data = __ui_set_ai_msg_stream_data; + intfs.disp_ai_msg_stream_end = __ui_set_ai_msg_stream_end; + intfs.disp_system_msg = __ui_set_system_msg; + intfs.disp_emotion = __ui_set_emotion; + intfs.disp_ai_mode_state = __ui_set_status; + intfs.disp_notification = __ui_set_notification; + intfs.disp_wifi_state = __ui_set_network; + intfs.disp_ai_chat_mode = __ui_set_chat_mode; + + return ai_ui_register(&intfs); +} + +#endif /* ENABLE_AI_CHAT_GUI_XIAOZHI */ diff --git a/ai_components/ai_ui/src/ai_ui_standby_face.c b/ai_components/ai_ui/src/ai_ui_standby_face.c new file mode 100644 index 0000000..098bd72 --- /dev/null +++ b/ai_components/ai_ui/src/ai_ui_standby_face.c @@ -0,0 +1,497 @@ +/** + * @file ai_ui_standby_face.c + * @brief Standby face animation UI implementation. + * + * This file provides the standby face UI with animated expressions, + * including blinking eyes, emotion cycling, and smooth transitions. + * + * Features: + * - Animated eyes with blinking effect + * - Multiple standby emotions (happy, relaxed, curious, sleepy) + * - Smooth state transitions (idle, listening, thinking, responding) + * - LVGL-based rendering for hardware acceleration + * + * @copyright Copyright (c) 2021-2025 Tuya Inc. All Rights Reserved. + */ + +#include "tal_api.h" + +#if defined(ENABLE_LIBLVGL) && (ENABLE_LIBLVGL == 1) + +#include "lvgl.h" +#include "lv_vendor.h" + +#include "ai_ui_standby_face.h" +#include "ai_ui_icon_font.h" +#include "lang_config.h" + +/*********************************************************** +************************macro define************************ +***********************************************************/ +/* Animation timing (ms) */ +#define BLINK_INTERVAL_MIN 2000 /* Minimum time between blinks */ +#define BLINK_INTERVAL_MAX 5000 /* Maximum time between blinks */ +#define BLINK_DURATION 150 /* How long eyes stay closed */ +#define EMOTION_CYCLE_INTERVAL 8000 /* Cycle emotions every 8 seconds */ + +/* Eye dimensions (scaled for 240x320 screen) */ +#define FACE_BG_COLOR lv_color_hex(0x1A1A2E) /* Dark blue background */ +#define EYE_COLOR lv_color_hex(0x4ECDC4) /* Cyan/teal eyes */ +#define EYE_GLOW_COLOR lv_color_hex(0x45B7AA) /* Slightly darker glow */ + +/*********************************************************** +***********************typedef define*********************** +***********************************************************/ +typedef struct { + lv_obj_t *screen; + lv_obj_t *face_container; + lv_obj_t *left_eye; + lv_obj_t *right_eye; + lv_obj_t *mouth; + lv_obj_t *hint_label; + lv_obj_t *status_label; + + /* Animation state */ + STANDBY_FACE_STATE_E state; + STANDBY_EMO_TYPE_E current_emotion; + bool is_blinking; + bool is_visible; + + /* LVGL timers */ + lv_timer_t *blink_timer; + lv_timer_t *emotion_timer; + lv_timer_t *anim_timer; +} STANDBY_FACE_CTX_T; + +/*********************************************************** +***********************variable define**********************/ +static STANDBY_FACE_CTX_T sg_ctx = {0}; + +/* Emotion configurations - eye shapes and sizes */ +typedef struct { + int eye_width; + int eye_height; + int eye_radius; + int eye_spacing; + int eye_y_offset; + const char *hint_text; +} emotion_config_t; + +static const emotion_config_t emotion_config[STANDBY_EMO_MAX] = { + /* HAPPY - slightly squinted, upbeat */ + {40, 35, 15, 25, -10, LANG_STANDBY_HINT_HAPPY}, + /* RELAXED - normal, calm */ + {45, 40, 18, 30, 0, LANG_STANDBY_HINT_RELAXED}, + /* CURIOUS - wide, alert */ + {40, 45, 12, 25, -5, LANG_STANDBY_HINT_CURIOUS}, + /* SLEEPY - half closed */ + {40, 20, 15, 25, 10, LANG_STANDBY_HINT_SLEEPY}, + /* EXCITED - wide open */ + {50, 50, 20, 25, -15, LANG_STANDBY_HINT_EXCITED}, +}; + +/*********************************************************** +***********************function define**********************/ + +/** + * @brief Get random blink interval + */ +static uint32_t __get_random_blink_interval(void) +{ + uint32_t range = BLINK_INTERVAL_MAX - BLINK_INTERVAL_MIN; + return BLINK_INTERVAL_MIN + tal_system_get_random(range); +} + +/** + * @brief Update eye appearance based on current emotion + */ +static void __update_eye_shape(void) +{ + if (!sg_ctx.left_eye || !sg_ctx.right_eye) { + return; + } + + const emotion_config_t *cfg = &emotion_config[sg_ctx.current_emotion]; + lv_coord_t screen_width = lv_obj_get_width(sg_ctx.screen); + lv_coord_t center_x = screen_width / 2; + lv_coord_t center_y = lv_obj_get_height(sg_ctx.screen) / 2 + cfg->eye_y_offset; + + lv_vendor_disp_lock(); + + /* Calculate eye positions */ + lv_coord_t left_eye_x = center_x - cfg->eye_spacing - cfg->eye_width / 2; + lv_coord_t right_eye_x = center_x + cfg->eye_spacing - cfg->eye_width / 2; + lv_coord_t eye_y = center_y - cfg->eye_height / 2; + + /* Update left eye */ + lv_obj_set_pos(sg_ctx.left_eye, left_eye_x, eye_y); + lv_obj_set_size(sg_ctx.left_eye, cfg->eye_width, cfg->eye_height); + + /* Update right eye */ + lv_obj_set_pos(sg_ctx.right_eye, right_eye_x, eye_y); + lv_obj_set_size(sg_ctx.right_eye, cfg->eye_width, cfg->eye_height); + + /* Update hint text */ + if (sg_ctx.hint_label) { + lv_label_set_text(sg_ctx.hint_label, cfg->hint_text); + } + + lv_vendor_disp_unlock(); +} + +/** + * @brief Blink animation - close eyes + */ +static void __blink_close(void) +{ + if (!sg_ctx.left_eye || !sg_ctx.right_eye) { + return; + } + + sg_ctx.is_blinking = true; + + lv_vendor_disp_lock(); + + /* Animate to thin line (closed eye) */ + const emotion_config_t *cfg = &emotion_config[sg_ctx.current_emotion]; + lv_coord_t closed_height = 4; /* Thin line for closed eye */ + + /* Smooth animation using LVGL anim */ + lv_anim_t anim; + lv_anim_init(&anim); + lv_anim_set_var(&anim, sg_ctx.left_eye); + lv_anim_set_values(&anim, cfg->eye_height, closed_height); + lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_height); + lv_anim_set_time(&anim, BLINK_DURATION / 2); + lv_anim_set_path_cb(&anim, lv_anim_path_ease_in_out); + lv_anim_start(&anim); + + lv_anim_set_var(&anim, sg_ctx.right_eye); + lv_anim_start(&anim); + + lv_vendor_disp_unlock(); +} + +/** + * @brief Blink animation - open eyes + */ +static void __blink_open(void) +{ + if (!sg_ctx.left_eye || !sg_ctx.right_eye) { + return; + } + + lv_vendor_disp_lock(); + + const emotion_config_t *cfg = &emotion_config[sg_ctx.current_emotion]; + + /* Animate back to normal height */ + lv_anim_t anim; + lv_anim_init(&anim); + lv_anim_set_var(&anim, sg_ctx.left_eye); + lv_anim_set_values(&anim, 4, cfg->eye_height); + lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_obj_set_height); + lv_anim_set_time(&anim, BLINK_DURATION / 2); + lv_anim_set_path_cb(&anim, lv_anim_path_ease_in_out); + lv_anim_start(&anim); + + lv_anim_set_var(&anim, sg_ctx.right_eye); + lv_anim_start(&anim); + + lv_vendor_disp_unlock(); + + sg_ctx.is_blinking = false; +} + +/** + * @brief Blink timer callback + */ +static void __blink_timer_cb(lv_timer_t *timer) +{ + (void)timer; + + if (!sg_ctx.is_visible) { + return; + } + + if (!sg_ctx.is_blinking) { + /* Start blink - close eyes */ + __blink_close(); + + /* Schedule open after BLINK_DURATION */ + if (sg_ctx.blink_timer) { + lv_timer_set_cb(sg_ctx.blink_timer, __blink_timer_cb); + lv_timer_reset(sg_ctx.blink_timer); + lv_timer_set_period(sg_ctx.blink_timer, BLINK_DURATION); + } + } else { + /* End blink - open eyes */ + __blink_open(); + + /* Reset timer for next blink */ + if (sg_ctx.blink_timer) { + lv_timer_set_period(sg_ctx.blink_timer, __get_random_blink_interval()); + } + } +} + +/** + * @brief Emotion cycle timer callback + */ +static void __emotion_timer_cb(lv_timer_t *timer) +{ + (void)timer; + + if (!sg_ctx.is_visible || sg_ctx.state != STANDBY_FACE_STATE_IDLE) { + return; + } + + /* Cycle to next emotion */ + sg_ctx.current_emotion = (sg_ctx.current_emotion + 1) % STANDBY_EMO_MAX; + __update_eye_shape(); +} + +/** + * @brief Create eye object with glow effect + */ +static lv_obj_t *__create_eye(lv_obj_t *parent) +{ + lv_obj_t *eye = lv_obj_create(parent); + lv_obj_set_style_bg_color(eye, EYE_COLOR, 0); + lv_obj_set_style_border_width(eye, 0, 0); + lv_obj_set_style_radius(eye, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_shadow_color(eye, EYE_GLOW_COLOR, 0); + lv_obj_set_style_shadow_width(eye, 20, 0); + lv_obj_set_style_shadow_spread(eye, 5, 0); + lv_obj_clear_flag(eye, LV_OBJ_FLAG_SCROLLABLE); + return eye; +} + +/** + * @brief Initialize standby face UI components + */ +static OPERATE_RET __standby_face_create(void) +{ + lv_vendor_disp_lock(); + + /* Get active screen */ + sg_ctx.screen = lv_screen_active(); + lv_obj_set_style_bg_color(sg_ctx.screen, FACE_BG_COLOR, 0); + + /* Face container - holds all face elements */ + sg_ctx.face_container = lv_obj_create(sg_ctx.screen); + lv_obj_set_size(sg_ctx.face_container, LV_HOR_RES, LV_VER_RES); + lv_obj_set_style_bg_color(sg_ctx.face_container, FACE_BG_COLOR, 0); + lv_obj_set_style_border_width(sg_ctx.face_container, 0, 0); + lv_obj_clear_flag(sg_ctx.face_container, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_center(sg_ctx.face_container); + + /* Create eyes */ + sg_ctx.left_eye = __create_eye(sg_ctx.face_container); + sg_ctx.right_eye = __create_eye(sg_ctx.face_container); + + /* Hint label at bottom */ + sg_ctx.hint_label = lv_label_create(sg_ctx.face_container); + lv_obj_set_style_text_color(sg_ctx.hint_label, lv_color_white(), 0); + lv_obj_set_style_text_font(sg_ctx.hint_label, ai_ui_get_text_font(), 0); + lv_obj_align(sg_ctx.hint_label, LV_ALIGN_BOTTOM_MID, 0, -40); + + /* Status label for state indication */ + sg_ctx.status_label = lv_label_create(sg_ctx.face_container); + lv_obj_set_style_text_color(sg_ctx.status_label, lv_color_hex(0x888888), 0); + lv_obj_set_style_text_font(sg_ctx.status_label, ai_ui_get_text_font(), 0); + lv_obj_align(sg_ctx.status_label, LV_ALIGN_TOP_MID, 0, 20); + lv_label_set_text(sg_ctx.status_label, ""); + + /* Initial eye positions */ + __update_eye_shape(); + + lv_vendor_disp_unlock(); + + return OPRT_OK; +} + +/** + * @brief Destroy standby face UI components + */ +static __attribute__((unused)) void __standby_face_destroy(void) +{ + lv_vendor_disp_lock(); + + if (sg_ctx.face_container) { + lv_obj_del(sg_ctx.face_container); + sg_ctx.face_container = NULL; + sg_ctx.left_eye = NULL; + sg_ctx.right_eye = NULL; + sg_ctx.hint_label = NULL; + sg_ctx.status_label = NULL; + } + + lv_vendor_disp_unlock(); +} + +/*********************************************************** +********************public functions************************/ + +OPERATE_RET ai_ui_standby_face_init(void) +{ + memset(&sg_ctx, 0, sizeof(sg_ctx)); + + sg_ctx.state = STANDBY_FACE_STATE_IDLE; + sg_ctx.current_emotion = STANDBY_EMO_HAPPY; + sg_ctx.is_blinking = false; + sg_ctx.is_visible = false; + + /* Create blink timer (initially stopped) */ + sg_ctx.blink_timer = lv_timer_create(__blink_timer_cb, BLINK_INTERVAL_MAX, NULL); + if (sg_ctx.blink_timer) { + lv_timer_pause(sg_ctx.blink_timer); + } + + /* Create emotion cycle timer */ + sg_ctx.emotion_timer = lv_timer_create(__emotion_timer_cb, EMOTION_CYCLE_INTERVAL, NULL); + if (sg_ctx.emotion_timer) { + lv_timer_pause(sg_ctx.emotion_timer); + } + + return OPRT_OK; +} + +OPERATE_RET ai_ui_standby_face_deinit(void) +{ + ai_ui_standby_face_hide(); + + if (sg_ctx.blink_timer) { + lv_timer_del(sg_ctx.blink_timer); + sg_ctx.blink_timer = NULL; + } + + if (sg_ctx.emotion_timer) { + lv_timer_del(sg_ctx.emotion_timer); + sg_ctx.emotion_timer = NULL; + } + + return OPRT_OK; +} + +OPERATE_RET ai_ui_standby_face_show(void) +{ + if (sg_ctx.is_visible) { + return OPRT_OK; + } + + /* Create UI if not exists */ + if (!sg_ctx.face_container) { + __standby_face_create(); + } + + sg_ctx.is_visible = true; + + /* Show container */ + lv_vendor_disp_lock(); + lv_obj_clear_flag(sg_ctx.face_container, LV_OBJ_FLAG_HIDDEN); + lv_vendor_disp_unlock(); + + /* Start animations */ + if (sg_ctx.blink_timer) { + lv_timer_set_period(sg_ctx.blink_timer, __get_random_blink_interval()); + lv_timer_resume(sg_ctx.blink_timer); + } + if (sg_ctx.emotion_timer) { + lv_timer_resume(sg_ctx.emotion_timer); + } + + return OPRT_OK; +} + +OPERATE_RET ai_ui_standby_face_hide(void) +{ + if (!sg_ctx.is_visible) { + return OPRT_OK; + } + + sg_ctx.is_visible = false; + + /* Stop animations */ + if (sg_ctx.blink_timer) { + lv_timer_pause(sg_ctx.blink_timer); + } + if (sg_ctx.emotion_timer) { + lv_timer_pause(sg_ctx.emotion_timer); + } + + /* Hide container */ + if (sg_ctx.face_container) { + lv_vendor_disp_lock(); + lv_obj_add_flag(sg_ctx.face_container, LV_OBJ_FLAG_HIDDEN); + lv_vendor_disp_unlock(); + } + + return OPRT_OK; +} + +OPERATE_RET ai_ui_standby_face_set_state(STANDBY_FACE_STATE_E state) +{ + sg_ctx.state = state; + + if (!sg_ctx.is_visible || !sg_ctx.status_label) { + return OPRT_OK; + } + + lv_vendor_disp_lock(); + + switch (state) { + case STANDBY_FACE_STATE_IDLE: + lv_label_set_text(sg_ctx.status_label, ""); + break; + case STANDBY_FACE_STATE_LISTENING: + lv_label_set_text(sg_ctx.status_label, LISTENING); + break; + case STANDBY_FACE_STATE_THINKING: + lv_label_set_text(sg_ctx.status_label, THINKING); + break; + case STANDBY_FACE_STATE_RESPONDING: + lv_label_set_text(sg_ctx.status_label, LANG_RESPONDING); + break; + case STANDBY_FACE_STATE_SLEEPING: + lv_label_set_text(sg_ctx.status_label, LANG_SLEEPING); + break; + } + + lv_vendor_disp_unlock(); + + return OPRT_OK; +} + +OPERATE_RET ai_ui_standby_face_set_emotion(STANDBY_EMO_TYPE_E emotion) +{ + if (emotion >= STANDBY_EMO_MAX) { + return OPRT_INVALID_PARM; + } + + sg_ctx.current_emotion = emotion; + __update_eye_shape(); + + return OPRT_OK; +} + +void ai_ui_standby_face_start_blink(void) +{ + if (sg_ctx.blink_timer && sg_ctx.is_visible) { + lv_timer_resume(sg_ctx.blink_timer); + } +} + +void ai_ui_standby_face_stop_blink(void) +{ + if (sg_ctx.blink_timer) { + lv_timer_pause(sg_ctx.blink_timer); + } +} + +bool ai_ui_standby_face_is_visible(void) +{ + return sg_ctx.is_visible; +} + +#endif /* ENABLE_LIBLVGL */ diff --git a/ai_components/assets/include/lang_config.h b/ai_components/assets/include/lang_config.h index adf9824..ef8cec4 100644 --- a/ai_components/assets/include/lang_config.h +++ b/ai_components/assets/include/lang_config.h @@ -42,6 +42,17 @@ extern "C" { #define SYSTEM_MSG_VOLUME "音量设置为" #define VIEW_IMAGE "查看图片" +/* Standby face hints */ +#define LANG_STANDBY_HINT_HAPPY "轻触唤醒" +#define LANG_STANDBY_HINT_RELAXED "随时等你" +#define LANG_STANDBY_HINT_CURIOUS "有什么想说的?" +#define LANG_STANDBY_HINT_SLEEPY "晚安..." +#define LANG_STANDBY_HINT_EXCITED "来啦!" + +/* Standby face states */ +#define LANG_RESPONDING "回应中..." +#define LANG_SLEEPING "休眠中..." + #ifdef __cplusplus } #endif diff --git a/ai_components/assets/language/zh-CN/language.json b/ai_components/assets/language/zh-CN/language.json index a503e85..9914e1c 100644 --- a/ai_components/assets/language/zh-CN/language.json +++ b/ai_components/assets/language/zh-CN/language.json @@ -36,6 +36,14 @@ "SYSTEM_MSG_WIFI_DISCONNECTED":"WiFi 断开连接", "SYSTEM_MSG_VOLUME":"音量设置为", - "VIEW_IMAGE":"查看图片" + "VIEW_IMAGE":"查看图片", + + "LANG_STANDBY_HINT_HAPPY": "轻触唤醒", + "LANG_STANDBY_HINT_RELAXED": "随时等你", + "LANG_STANDBY_HINT_CURIOUS": "有什么想说的?", + "LANG_STANDBY_HINT_SLEEPY": "晚安...", + "LANG_STANDBY_HINT_EXCITED": "来啦!", + "LANG_RESPONDING": "回应中...", + "LANG_SLEEPING": "休眠中..." } } diff --git a/build.log b/build.log new file mode 100644 index 0000000..8e44e12 --- /dev/null +++ b/build.log @@ -0,0 +1 @@ +C:\Users\Administrator\AppData\Local\CoPaw\python.exe: can't open file 'E:\\TuyaOpen\\DuckyClaw\\tos.py': [Errno 2] No such file or directory diff --git a/include/tuya_app_config.h b/include/tuya_app_config.h index 20753ca..eba061f 100755 --- a/include/tuya_app_config.h +++ b/include/tuya_app_config.h @@ -20,15 +20,15 @@ extern "C" { // https://pbt.tuya.com/s?p=dd46368ae3840e54f018b2c45dc1550b&u=c38c8fc0a5d14c4f66cae9f0cfcb2a24&t=2 #ifndef TUYA_PRODUCT_ID -#define TUYA_PRODUCT_ID "xxxxxxxxxxxxxxxx" +#define TUYA_PRODUCT_ID "hn4swfvynaoo5l1m" #endif // https://platform.tuya.com/purchase/index?type=6 #ifndef TUYA_OPENSDK_UUID -#define TUYA_OPENSDK_UUID "uuidxxxxxxxxxxxxxxxx" // Please change the correct uuid +#define TUYA_OPENSDK_UUID "uuid480b3378d85504bf" #endif #ifndef TUYA_OPENSDK_AUTHKEY -#define TUYA_OPENSDK_AUTHKEY "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // Please change the correct authkey +#define TUYA_OPENSDK_AUTHKEY "rWiJS0nO0H37jgR8FcblnZlhuVqP5Nmn" #endif // IM configuration @@ -38,10 +38,10 @@ extern "C" { #endif #ifndef IM_SECRET_FS_APP_ID -#define IM_SECRET_FS_APP_ID "" +#define IM_SECRET_FS_APP_ID "cli_a930eb92d638dcba" #endif #ifndef IM_SECRET_FS_APP_SECRET -#define IM_SECRET_FS_APP_SECRET "" +#define IM_SECRET_FS_APP_SECRET "FLcTyUEd6uSQq8gW1e0YcbHsn1JraZCC" #endif #ifndef IM_SECRET_TG_TOKEN diff --git a/manual_package.py b/manual_package.py new file mode 100644 index 0000000..5cd4e33 --- /dev/null +++ b/manual_package.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Manual firmware packager for DuckyClaw with XIAOZHI UI +Combines old platform binaries with new app library +""" + +import os +import sys +import struct + +# Paths +PLATFORM_DIR = "TuyaOpen/platform/T5AI/t5_os" +BUILD_DIR = f"{PLATFORM_DIR}/build/bk7258/tuya_app" +OUTPUT_DIR = "dist" + +def create_ua_file(): + """Create UA firmware file from existing binaries""" + + # Use existing platform binaries (from March build) + cp_bin = f"{BUILD_DIR}/bk7258/app.bin" + ap_bin = f"{BUILD_DIR}/bk7258_ap/app.bin" + + # Check if binaries exist + if not os.path.exists(cp_bin): + print(f"ERROR: CP binary not found: {cp_bin}") + print("Using old backup binaries instead...") + cp_bin = "dist/backup_old/DuckyClaw_UA_1.0.0.bin" + if not os.path.exists(cp_bin): + print(f"ERROR: Backup CP binary not found: {cp_bin}") + return False + + if not os.path.exists(ap_bin): + print(f"ERROR: AP binary not found: {ap_bin}") + return False + + print(f"CP binary: {cp_bin} ({os.path.getsize(cp_bin)} bytes)") + print(f"AP binary: {ap_bin} ({os.path.getsize(ap_bin)} bytes)") + + # Read partition table for layout + partition_file = f"{PLATFORM_DIR}/projects/tuya_app/partitions/bk7258/auto_partitions.csv" + if not os.path.exists(partition_file): + print(f"ERROR: Partition file not found: {partition_file}") + return False + + print(f"Partition file: {partition_file}") + + # For now, just copy UA file from backup + # This is a placeholder - actual UA creation requires parsing partition table + # and combining CP/AP binaries at correct offsets + + print("\nNOTE: This script is a placeholder.") + print("The actual UA file creation requires the complete T5AI build system.") + print("Please use GitHub Actions or Linux environment for full build.\n") + + return True + +def main(): + print("="*60) + print("DuckyClaw XIAOZHI Firmware Packager") + print("="*60) + print() + + # Create output directory + os.makedirs(OUTPUT_DIR, exist_ok=True) + + if create_ua_file(): + print("Packaging instructions:") + print("1. The T5AI platform requires complete build system") + print("2. Use GitHub Actions workflow for automatic builds") + print("3. Or use Linux/WSL environment with make/bash tools") + else: + print("FAILED: Could not create firmware package") + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/camera_monitor.c b/src/camera_monitor.c new file mode 100644 index 0000000..2cf5942 --- /dev/null +++ b/src/camera_monitor.c @@ -0,0 +1,312 @@ +/** + * @file camera_monitor.c + * @brief Camera monitoring with human detection and greeting + * + * This module implements camera-based human detection using AI vision. + * When a person is detected, it automatically plays a greeting message. + * + * @copyright Copyright (c) 2021-2025 Tuya Inc. All Rights Reserved. + */ + +#include "tal_api.h" +#include "ai_video_input.h" +#include "ai_agent.h" +#include "ai_audio_player.h" +#include "ducky_claw_chat.h" + +/*********************************************************** +************************macro define************************ +***********************************************************/ +#define MONITOR_CHECK_INTERVAL_MS 5000 // Check every 5 seconds +#define PERSON_DETECT_COOLDOWN_MS 15000 // Greeting cooldown (15 seconds) +#define DETECT_IMAGE_MAX_SIZE (30*1024) // Max image size 30KB + +/*********************************************************** +***********************typedef define***********************/ +typedef enum { + MONITOR_STATE_IDLE = 0, + MONITOR_STATE_CAPTURING, + MONITOR_STATE_ANALYZING, + MONITOR_STATE_GREETING, +} MONITOR_STATE_E; + +/*********************************************************** +***********************variable define**********************/ +static TIMER_ID sg_monitor_timer = NULL; +static MUTEX_HANDLE sg_monitor_mutex = NULL; +static MONITOR_STATE_E sg_monitor_state = MONITOR_STATE_IDLE; +static uint64_t sg_last_greeting_time = 0; +static bool sg_monitor_enabled = false; + +// Greeting messages +static const char *greeting_messages[] = { + "你好!很高兴见到你!", + "欢迎!有什么可以帮助您的吗?", + "您好!我是您的智能助手!", + "欢迎光临!请问需要什么帮助?", + "您好!我随时准备为您服务!" +}; + +/*********************************************************** +***********************function define**********************/ + +/** + * @brief Get random greeting message + */ +static const char* __get_random_greeting(void) +{ + uint32_t count = sizeof(greeting_messages) / sizeof(greeting_messages[0]); + uint32_t index = tal_system_get_random(count); + return greeting_messages[index]; +} + +/** + * @brief Check if enough time has passed since last greeting + */ +static bool __can_greet(void) +{ + uint64_t current_time = tal_system_get_millisecond(); + if ((current_time - sg_last_greeting_time) > PERSON_DETECT_COOLDOWN_MS) { + return true; + } + return false; +} + +/** + * @brief Play greeting using AI TTS + */ +static void __play_greeting(void) +{ + const char *greeting = __get_random_greeting(); + + PR_INFO("Playing greeting: %s", greeting); + + // Send greeting text to AI agent for TTS playback + OPERATE_RET rt = ai_agent_send_text((char *)greeting); + if (rt != OPRT_OK) { + PR_ERR("Failed to send greeting: %d", rt); + // Fallback to alert sound if TTS fails + ai_audio_player_alert(AI_AUDIO_ALERT_WAKEUP); + } + + sg_last_greeting_time = tal_system_get_millisecond(); +} + +/** + * @brief Analyze image for human detection using AI + * This sends the image to AI agent for analysis + */ +static void __analyze_image_for_human(uint8_t *image_data, uint32_t image_len) +{ + PR_INFO("Analyzing image for human detection, size: %d bytes", image_len); + + // Send image to AI agent + // The AI will analyze the image and respond if a person is detected + OPERATE_RET rt = ai_agent_send_image(image_data, image_len); + if (rt != OPRT_OK) { + PR_ERR("Failed to send image for analysis: %d", rt); + } +} + +/** + * @brief Monitor timer callback - Periodic check + */ +static void __monitor_timer_cb(TIMER_ID timer_id, void *arg) +{ + (void)timer_id; + (void)arg; + + if (!sg_monitor_enabled) { + return; + } + + tal_mutex_lock(sg_monitor_mutex); + + if (sg_monitor_state != MONITOR_STATE_IDLE) { + PR_DEBUG("Monitor busy, current state: %d", sg_monitor_state); + tal_mutex_unlock(sg_monitor_mutex); + return; + } + + sg_monitor_state = MONITOR_STATE_CAPTURING; + tal_mutex_unlock(sg_monitor_mutex); + + // Capture image from camera + uint8_t *image_data = NULL; + uint32_t image_len = 0; + + PR_DEBUG("Capturing image for human detection..."); + + OPERATE_RET rt = ai_video_get_jpeg_frame(&image_data, &image_len); + if (rt != OPRT_OK || image_data == NULL || image_len == 0) { + PR_ERR("Failed to capture image: %d", rt); + tal_mutex_lock(sg_monitor_mutex); + sg_monitor_state = MONITOR_STATE_IDLE; + tal_mutex_unlock(sg_monitor_mutex); + return; + } + + PR_INFO("Image captured: %d bytes", image_len); + + // Check image size limit + if (image_len > DETECT_IMAGE_MAX_SIZE) { + PR_WARN("Image too large: %d bytes, skipping analysis", image_len); + ai_video_jpeg_image_free(&image_data); + tal_mutex_lock(sg_monitor_mutex); + sg_monitor_state = MONITOR_STATE_IDLE; + tal_mutex_unlock(sg_monitor_mutex); + return; + } + + tal_mutex_lock(sg_monitor_mutex); + sg_monitor_state = MONITOR_STATE_ANALYZING; + tal_mutex_unlock(sg_monitor_mutex); + + // Analyze image + __analyze_image_for_human(image_data, image_len); + + // Free image data + ai_video_jpeg_image_free(&image_data); + + tal_mutex_lock(sg_monitor_mutex); + sg_monitor_state = MONITOR_STATE_IDLE; + tal_mutex_unlock(sg_monitor_mutex); +} + +/** + * @brief Handle AI response for human detection + * This is called when AI responds after analyzing the image + */ +void camera_monitor_handle_ai_response(const char *response) +{ + if (!sg_monitor_enabled) { + return; + } + + if (response == NULL) { + return; + } + + PR_INFO("AI response: %s", response); + + // Check if AI detected a person in the image + // AI should respond with something like "检测到有人" or "有人" + if ((strstr(response, "人") != NULL || strstr(response, "person") != NULL || + strstr(response, "human") != NULL || strstr(response, "有人") != NULL)) { + + PR_NOTICE("Person detected!"); + + if (__can_greet()) { + __play_greeting(); + } else { + PR_DEBUG("Greeting on cooldown"); + } + } +} + +/** + * @brief Initialize camera monitoring module + */ +OPERATE_RET camera_monitor_init(void) +{ + OPERATE_RET rt = OPRT_OK; + + PR_NOTICE("Initializing camera monitor..."); + + // Create mutex + rt = tal_mutex_create_init(&sg_monitor_mutex); + if (rt != OPRT_OK) { + PR_ERR("Failed to create mutex: %d", rt); + return rt; + } + + // Create timer for periodic checking + rt = tal_sw_timer_create(__monitor_timer_cb, NULL, &sg_monitor_timer); + if (rt != OPRT_OK) { + PR_ERR("Failed to create timer: %d", rt); + tal_mutex_release(sg_monitor_mutex); + sg_monitor_mutex = NULL; + return rt; + } + + sg_monitor_enabled = false; + sg_monitor_state = MONITOR_STATE_IDLE; + sg_last_greeting_time = 0; + + PR_NOTICE("Camera monitor initialized successfully"); + + return OPRT_OK; +} + +/** + * @brief Start camera monitoring + */ +OPERATE_RET camera_monitor_start(void) +{ + if (sg_monitor_timer == NULL) { + PR_ERR("Monitor not initialized"); + return OPRT_COM_ERROR; + } + + PR_NOTICE("Starting camera monitor..."); + + sg_monitor_enabled = true; + + // Start periodic timer + tal_sw_timer_start(sg_monitor_timer, MONITOR_CHECK_INTERVAL_MS, TAL_TIMER_CYCLE); + + PR_NOTICE("Camera monitor started, checking every %d ms", MONITOR_CHECK_INTERVAL_MS); + + return OPRT_OK; +} + +/** + * @brief Stop camera monitoring + */ +OPERATE_RET camera_monitor_stop(void) +{ + PR_NOTICE("Stopping camera monitor..."); + + sg_monitor_enabled = false; + + if (sg_monitor_timer != NULL) { + tal_sw_timer_stop(sg_monitor_timer); + } + + tal_mutex_lock(sg_monitor_mutex); + sg_monitor_state = MONITOR_STATE_IDLE; + tal_mutex_unlock(sg_monitor_mutex); + + return OPRT_OK; +} + +/** + * @brief Deinitialize camera monitoring + */ +OPERATE_RET camera_monitor_deinit(void) +{ + PR_NOTICE("Deinitializing camera monitor..."); + + camera_monitor_stop(); + + if (sg_monitor_timer != NULL) { + tal_sw_timer_stop(sg_monitor_timer); + tal_sw_timer_release(); + sg_monitor_timer = NULL; + } + + if (sg_monitor_mutex != NULL) { + tal_mutex_release(sg_monitor_mutex); + sg_monitor_mutex = NULL; + } + + return OPRT_OK; +} + +/** + * @brief Check if monitoring is enabled + */ +bool camera_monitor_is_enabled(void) +{ + return sg_monitor_enabled; +} diff --git a/src/camera_monitor.h b/src/camera_monitor.h new file mode 100644 index 0000000..6013a39 --- /dev/null +++ b/src/camera_monitor.h @@ -0,0 +1,74 @@ +/** + * @file camera_monitor.h + * @brief Camera monitoring with human detection and greeting + * + * This module implements camera-based human detection using AI vision. + * When a person is detected, it automatically plays a greeting message. + * + * @copyright Copyright (c) 2021-2025 Tuya Inc. All Rights Reserved. + */ + +#ifndef __CAMERA_MONITOR_H__ +#define __CAMERA_MONITOR_H__ + +#include "tal_api.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize camera monitoring module + * + * @return OPRT_OK on success, error code otherwise + */ +OPERATE_RET camera_monitor_init(void); + +/** + * @brief Start camera monitoring + * + * Starts the periodic timer to capture and analyze images. + * + * @return OPRT_OK on success, error code otherwise + */ +OPERATE_RET camera_monitor_start(void); + +/** + * @brief Stop camera monitoring + * + * Stops the periodic timer. + * + * @return OPRT_OK on success, error code otherwise + */ +OPERATE_RET camera_monitor_stop(void); + +/** + * @brief Deinitialize camera monitoring + * + * Releases all resources. + * + * @return OPRT_OK on success, error code otherwise + */ +OPERATE_RET camera_monitor_deinit(void); + +/** + * @brief Check if monitoring is enabled + * + * @return true if monitoring is enabled, false otherwise + */ +bool camera_monitor_is_enabled(void); + +/** + * @brief Handle AI response for human detection + * + * This is called when AI responds after analyzing the image. + * + * @param response AI response text + */ +void camera_monitor_handle_ai_response(const char *response); + +#ifdef __cplusplus +} +#endif + +#endif /* __CAMERA_MONITOR_H__ */ diff --git a/src/ducky_claw_chat.c b/src/ducky_claw_chat.c index 2b32cc3..5b5e761 100755 --- a/src/ducky_claw_chat.c +++ b/src/ducky_claw_chat.c @@ -11,6 +11,7 @@ #include "ai_chat_main.h" #include "ducky_claw_chat.h" #include "agent_loop.h" +#include "camera_monitor.h" #include "app_im.h" #include "tal_log.h" @@ -41,6 +42,10 @@ static TIMER_ID sg_printf_heap_tm; static AI_UI_WIFI_STATUS_E sg_wifi_status = AI_UI_WIFI_STATUS_DISCONNECTED; static TIMER_ID sg_disp_status_tm; #endif + +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) +static bool sg_camera_monitor_started = false; +#endif /*********************************************************** ***********************function define********************** ***********************************************************/ @@ -88,6 +93,15 @@ static void __display_net_status_update(void) if (wifi_status != sg_wifi_status) { sg_wifi_status = wifi_status; ai_ui_disp_msg(AI_UI_DISP_NETWORK, (uint8_t *)&wifi_status, sizeof(AI_UI_WIFI_STATUS_E)); + +#if defined(ENABLE_COMP_AI_VIDEO) && (ENABLE_COMP_AI_VIDEO == 1) + // Start camera monitor when network is connected + if (wifi_status != AI_UI_WIFI_STATUS_DISCONNECTED && !sg_camera_monitor_started) { + PR_NOTICE("Network connected, starting camera monitor..."); + camera_monitor_start(); + sg_camera_monitor_started = true; + } +#endif } } @@ -194,6 +208,17 @@ static void __ai_chat_handle_event(AI_NOTIFY_EVENT_T *event) case AI_USER_EVT_TEXT_STREAM_DATA: { AI_NOTIFY_TEXT_T *text = (AI_NOTIFY_TEXT_T *)event->data; + // Pass response to camera monitor for human detection analysis + if (text && text->data && text->datalen > 0) { + char *temp_str = tal_malloc(text->datalen + 1); + if (temp_str) { + memcpy(temp_str, text->data, text->datalen); + temp_str[text->datalen] = '\0'; + camera_monitor_handle_ai_response(temp_str); + tal_free(temp_str); + } + } + if (data_write_offset + text->datalen >= STREAM_DATA_MAX_LEN) { /* Only flush to IM if we are NOT in a tool-loop iteration */ if (!agent_loop_in_tool_loop()) { @@ -242,7 +267,11 @@ OPERATE_RET ducky_claw_chat_init(void) .disp_flush_cb = __ai_video_display_flush, }; + // Initialize video first (camera init is asynchronous) TUYA_CALL_ERR_LOG(ai_video_init(&ai_video_cfg)); + + // Initialize camera monitor after video (but don't start yet) + TUYA_CALL_ERR_LOG(camera_monitor_init()); #endif #if defined(ENABLE_COMP_AI_MCP) && (ENABLE_COMP_AI_MCP == 1)