mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-02-13 07:28:14 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd45d0de26 | ||
|
|
916ea39fad | ||
|
|
983d86a334 | ||
|
|
e7fc9ed489 | ||
|
|
e329fcc6b8 | ||
|
|
d3e7fee828 | ||
|
|
96e39bea1b | ||
|
|
a8687f3736 | ||
|
|
8d58bdb21b | ||
|
|
4616fa3486 |
49
.github/workflows/build.yml
vendored
49
.github/workflows/build.yml
vendored
@@ -14,10 +14,10 @@ permissions:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Determine boards to build
|
||||
name: Determine variants to build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
boards: ${{ steps.select.outputs.boards }}
|
||||
variants: ${{ steps.select.outputs.variants }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -28,30 +28,30 @@ jobs:
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
|
||||
- id: list
|
||||
name: Get all board list
|
||||
name: Get all variant list
|
||||
run: |
|
||||
echo "all_boards=$(python scripts/release.py --list-boards --json)" >> $GITHUB_OUTPUT
|
||||
echo "all_variants=$(python scripts/release.py --list-boards --json)" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: select
|
||||
name: Select boards based on changes
|
||||
name: Select variants based on changes
|
||||
env:
|
||||
ALL_BOARDS: ${{ steps.list.outputs.all_boards }}
|
||||
ALL_VARIANTS: ${{ steps.list.outputs.all_variants }}
|
||||
run: |
|
||||
EVENT_NAME="${{ github.event_name }}"
|
||||
|
||||
# For push to main branch, build all boards
|
||||
# push 到 main 分支,编译全部变体
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
echo "boards=$ALL_BOARDS" >> $GITHUB_OUTPUT
|
||||
echo "variants=$ALL_VARIANTS" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# For pull_request
|
||||
# pull_request 场景
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
||||
echo "Base: $BASE_SHA, Head: $HEAD_SHA"
|
||||
|
||||
CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA || true)
|
||||
echo "Changed files:\n$CHANGED"
|
||||
echo -e "Changed files:\n$CHANGED"
|
||||
|
||||
NEED_ALL=0
|
||||
declare -A AFFECTED
|
||||
@@ -60,6 +60,10 @@ jobs:
|
||||
NEED_ALL=1
|
||||
fi
|
||||
|
||||
if [[ "$file" == main/boards/common/* ]]; then
|
||||
NEED_ALL=1
|
||||
fi
|
||||
|
||||
if [[ "$file" == main/boards/* ]]; then
|
||||
board=$(echo "$file" | cut -d '/' -f3)
|
||||
AFFECTED[$board]=1
|
||||
@@ -67,24 +71,25 @@ jobs:
|
||||
done <<< "$CHANGED"
|
||||
|
||||
if [[ "$NEED_ALL" -eq 1 ]]; then
|
||||
echo "boards=$ALL_BOARDS" >> $GITHUB_OUTPUT
|
||||
echo "variants=$ALL_VARIANTS" >> $GITHUB_OUTPUT
|
||||
else
|
||||
if [[ ${#AFFECTED[@]} -eq 0 ]]; then
|
||||
echo "boards=[]" >> $GITHUB_OUTPUT
|
||||
echo "variants=[]" >> $GITHUB_OUTPUT
|
||||
else
|
||||
JSON=$(printf '%s\n' "${!AFFECTED[@]}" | sort -u | jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "boards=$JSON" >> $GITHUB_OUTPUT
|
||||
BOARDS_JSON=$(printf '%s\n' "${!AFFECTED[@]}" | sort -u | jq -R -s -c 'split("\n")[:-1]')
|
||||
FILTERED=$(echo "$ALL_VARIANTS" | jq -c --argjson boards "$BOARDS_JSON" 'map(select(.board as $b | $boards | index($b)))')
|
||||
echo "variants=$FILTERED" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.board }}
|
||||
name: Build ${{ matrix.name }}
|
||||
needs: prepare
|
||||
if: ${{ needs.prepare.outputs.boards != '[]' }}
|
||||
if: ${{ needs.prepare.outputs.variants != '[]' }}
|
||||
strategy:
|
||||
fail-fast: false # 单个 board 失败不影响其它 board
|
||||
fail-fast: false # 单个变体失败不影响其它变体
|
||||
matrix:
|
||||
board: ${{ fromJson(needs.prepare.outputs.boards) }}
|
||||
include: ${{ fromJson(needs.prepare.outputs.variants) }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:release-v5.4
|
||||
@@ -92,15 +97,15 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build current board
|
||||
- name: Build current variant
|
||||
shell: bash
|
||||
run: |
|
||||
source $IDF_PATH/export.sh
|
||||
python scripts/release.py ${{ matrix.board }}
|
||||
python scripts/release.py ${{ matrix.board }} --name ${{ matrix.name }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xiaozhi_${{ matrix.board }}_${{ github.sha }}.bin
|
||||
name: xiaozhi_${{ matrix.name }}_${{ github.sha }}.bin
|
||||
path: build/merged-binary.bin
|
||||
if-no-files-found: error
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(PROJECT_VER "2.0.2")
|
||||
set(PROJECT_VER "2.0.3")
|
||||
|
||||
# Add this line to disable the specific warning
|
||||
add_compile_options(-Wno-missing-field-initializers)
|
||||
|
||||
@@ -16,6 +16,7 @@ set(SOURCES "audio/audio_codec.cc"
|
||||
"display/lcd_display.cc"
|
||||
"display/oled_display.cc"
|
||||
"display/lvgl_display/lvgl_display.cc"
|
||||
"display/emote_display.cc"
|
||||
"display/lvgl_display/emoji_collection.cc"
|
||||
"display/lvgl_display/lvgl_theme.cc"
|
||||
"display/lvgl_display/lvgl_font.cc"
|
||||
@@ -212,11 +213,7 @@ elseif(CONFIG_BOARD_TYPE_ECHOEAR)
|
||||
set(BOARD_TYPE "echoear")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_20_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
# Find esp_emote_gfx component for ECHOEAR extra files
|
||||
find_component_by_pattern("esp_emote_gfx" EMOTE_GFX_COMPONENT EMOTE_GFX_COMPONENT_PATH)
|
||||
if(EMOTE_GFX_COMPONENT_PATH)
|
||||
set(DEFAULT_ASSETS_EXTRA_FILES "${EMOTE_GFX_COMPONENT_PATH}/emoji_normal")
|
||||
endif()
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_ESP32S3_AUDIO_BOARD)
|
||||
set(BOARD_TYPE "waveshare-s3-audio-board")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
|
||||
@@ -232,6 +229,11 @@ elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_30_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_4B)
|
||||
set(BOARD_TYPE "waveshare-s3-touch-lcd-4b")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_30_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75)
|
||||
set(BOARD_TYPE "waveshare-s3-touch-amoled-1.75")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
@@ -262,6 +264,11 @@ elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_3_5B)
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_16_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
|
||||
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_3_49)
|
||||
set(BOARD_TYPE "waveshare-s3-touch-lcd-3.49")
|
||||
set(LVGL_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(LVGL_ICON_FONT font_awesome_30_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
elseif(CONFIG_BOARD_TYPE_ESP32C6_LCD_1_69)
|
||||
set(BOARD_TYPE "waveshare-c6-lcd-1.69")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
|
||||
|
||||
@@ -86,37 +86,37 @@ choice BOARD_TYPE
|
||||
help
|
||||
Board type. 开发板类型
|
||||
config BOARD_TYPE_BREAD_COMPACT_WIFI
|
||||
bool "面包板新版接线(WiFi)"
|
||||
bool "Bread Compact WiFi (面包板)"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_BREAD_COMPACT_WIFI_LCD
|
||||
bool "面包板新版接线(WiFi)+ LCD"
|
||||
bool "Bread Compact WiFi + LCD (面包板)"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_BREAD_COMPACT_WIFI_CAM
|
||||
bool "面包板新版接线(WiFi)+ LCD + Camera"
|
||||
bool "Bread Compact WiFi + LCD + Camera (面包板)"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_BREAD_COMPACT_ML307
|
||||
bool "面包板新版接线(ML307 AT)"
|
||||
bool "Bread Compact ML307/EC801E (面包板 4G)"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_BREAD_COMPACT_ESP32
|
||||
bool "面包板(WiFi) ESP32 DevKit"
|
||||
bool "Bread Compact ESP32 DevKit (面包板)"
|
||||
depends on IDF_TARGET_ESP32
|
||||
config BOARD_TYPE_BREAD_COMPACT_ESP32_LCD
|
||||
bool "面包板(WiFi+ LCD) ESP32 DevKit"
|
||||
bool "Bread Compact ESP32 DevKit + LCD (面包板)"
|
||||
depends on IDF_TARGET_ESP32
|
||||
config BOARD_TYPE_XMINI_C3_V3
|
||||
bool "虾哥 Mini C3 V3"
|
||||
bool "Xmini C3 V3"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
config BOARD_TYPE_XMINI_C3_4G
|
||||
bool "虾哥 Mini C3 4G"
|
||||
bool "Xmini C3 4G"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
config BOARD_TYPE_XMINI_C3
|
||||
bool "虾哥 Mini C3"
|
||||
bool "Xmini C3"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
config BOARD_TYPE_ESP32S3_KORVO2_V3
|
||||
bool "ESP32S3_KORVO2_V3开发板"
|
||||
bool "ESP32S3 KORVO2 V3"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_ESP_SPARKBOT
|
||||
bool "ESP-SparkBot开发板"
|
||||
bool "ESP-SparkBot"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_ESP_SPOT_S3
|
||||
bool "ESP-Spot-S3"
|
||||
@@ -146,10 +146,10 @@ choice BOARD_TYPE
|
||||
bool "Kevin C3"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
config BOARD_TYPE_KEVIN_SP_V3_DEV
|
||||
bool "Kevin SP V3开发板"
|
||||
bool "Kevin SP V3"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_KEVIN_SP_V4_DEV
|
||||
bool "Kevin SP V4开发板"
|
||||
bool "Kevin SP V4"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_ESP32_CGC
|
||||
bool "ESP32 CGC"
|
||||
@@ -158,13 +158,13 @@ choice BOARD_TYPE
|
||||
bool "ESP32 CGC 144"
|
||||
depends on IDF_TARGET_ESP32
|
||||
config BOARD_TYPE_KEVIN_YUYING_313LCD
|
||||
bool "鱼鹰科技3.13LCD开发板"
|
||||
bool "鱼鹰科技 3.13LCD"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_LICHUANG_DEV
|
||||
bool "立创·实战派ESP32-S3开发板"
|
||||
bool "立创·实战派 ESP32-S3"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_LICHUANG_C3_DEV
|
||||
bool "立创·实战派ESP32-C3开发板"
|
||||
bool "立创·实战派 ESP32-C3"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
config BOARD_TYPE_DF_K10
|
||||
bool "DFRobot 行空板 k10"
|
||||
@@ -215,6 +215,8 @@ choice BOARD_TYPE
|
||||
bool "Waveshare ESP32-S3-Touch-AMOLED-2.06"
|
||||
config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75
|
||||
bool "Waveshare ESP32-S3-Touch-AMOLED-1.75"
|
||||
config BOARD_TYPE_ESP32S3_Touch_LCD_4B
|
||||
bool "Waveshare ESP32-S3-Touch-LCD-4B"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_ESP32S3_Touch_LCD_1_85C
|
||||
bool "Waveshare ESP32-S3-Touch-LCD-1.85C"
|
||||
@@ -231,6 +233,9 @@ choice BOARD_TYPE
|
||||
config BOARD_TYPE_ESP32C6_Touch_AMOLED_1_43
|
||||
bool "Waveshare ESP32-C6-Touch-AMOLOED-1.43"
|
||||
depends on IDF_TARGET_ESP32C6
|
||||
config BOARD_TYPE_ESP32S3_Touch_LCD_3_49
|
||||
bool "Waveshare ESP32-S3-Touch-LCD-3.49"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_ESP32S3_Touch_LCD_3_5
|
||||
bool "Waveshare ESP32-S3-Touch-LCD-3.5"
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
@@ -368,7 +373,7 @@ choice BOARD_TYPE
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
depends on IDF_TARGET_ESP32S3
|
||||
config BOARD_TYPE_SURFER_C3_1_14TFT
|
||||
bool "Surfer-C3-1-14TFT"
|
||||
bool "Surfer-C3-1.14TFT"
|
||||
depends on IDF_TARGET_ESP32C3
|
||||
config BOARD_TYPE_YUNLIAO_S3
|
||||
bool "小智云聊-S3"
|
||||
@@ -379,8 +384,6 @@ choice ESP_S3_LCD_EV_Board_Version_TYPE
|
||||
depends on BOARD_TYPE_ESP_S3_LCD_EV_Board
|
||||
prompt "EV_BOARD Type"
|
||||
default ESP_S3_LCD_EV_Board_1p4
|
||||
help
|
||||
开发板硬件版本型号选择
|
||||
config ESP_S3_LCD_EV_Board_1p4
|
||||
bool "乐鑫ESP32_S3_LCD_EV_Board-MB_V1.4"
|
||||
config ESP_S3_LCD_EV_Board_1p5
|
||||
@@ -392,13 +395,13 @@ choice DISPLAY_OLED_TYPE
|
||||
prompt "OLED Type"
|
||||
default OLED_SSD1306_128X32
|
||||
help
|
||||
OLED 屏幕类型选择
|
||||
OLED Monochrome Display Type
|
||||
config OLED_SSD1306_128X32
|
||||
bool "SSD1306, 分辨率128*32"
|
||||
bool "SSD1306 128*32"
|
||||
config OLED_SSD1306_128X64
|
||||
bool "SSD1306, 分辨率128*64"
|
||||
bool "SSD1306 128*64"
|
||||
config OLED_SH1106_128X64
|
||||
bool "SH1106, 分辨率128*64"
|
||||
bool "SH1106 128*64"
|
||||
endchoice
|
||||
|
||||
choice DISPLAY_LCD_TYPE
|
||||
@@ -406,37 +409,37 @@ choice DISPLAY_LCD_TYPE
|
||||
prompt "LCD Type"
|
||||
default LCD_ST7789_240X320
|
||||
help
|
||||
屏幕类型选择
|
||||
LCD Display Type
|
||||
config LCD_ST7789_240X320
|
||||
bool "ST7789, 分辨率240*320, IPS"
|
||||
bool "ST7789 240*320, IPS"
|
||||
config LCD_ST7789_240X320_NO_IPS
|
||||
bool "ST7789, 分辨率240*320, 非IPS"
|
||||
bool "ST7789 240*320, Non-IPS"
|
||||
config LCD_ST7789_170X320
|
||||
bool "ST7789, 分辨率170*320"
|
||||
bool "ST7789 170*320"
|
||||
config LCD_ST7789_172X320
|
||||
bool "ST7789, 分辨率172*320"
|
||||
bool "ST7789 172*320"
|
||||
config LCD_ST7789_240X280
|
||||
bool "ST7789, 分辨率240*280"
|
||||
bool "ST7789 240*280"
|
||||
config LCD_ST7789_240X240
|
||||
bool "ST7789, 分辨率240*240"
|
||||
bool "ST7789 240*240"
|
||||
config LCD_ST7789_240X240_7PIN
|
||||
bool "ST7789, 分辨率240*240, 7PIN"
|
||||
bool "ST7789 240*240, 7PIN"
|
||||
config LCD_ST7789_240X135
|
||||
bool "ST7789, 分辨率240*135"
|
||||
bool "ST7789 240*135"
|
||||
config LCD_ST7735_128X160
|
||||
bool "ST7735, 分辨率128*160"
|
||||
bool "ST7735 128*160"
|
||||
config LCD_ST7735_128X128
|
||||
bool "ST7735, 分辨率128*128"
|
||||
bool "ST7735 128*128"
|
||||
config LCD_ST7796_320X480
|
||||
bool "ST7796, 分辨率320*480 IPS"
|
||||
bool "ST7796 320*480 IPS"
|
||||
config LCD_ST7796_320X480_NO_IPS
|
||||
bool "ST7796, 分辨率320*480, 非IPS"
|
||||
bool "ST7796 320*480, Non-IPS"
|
||||
config LCD_ILI9341_240X320
|
||||
bool "ILI9341, 分辨率240*320"
|
||||
bool "ILI9341 240*320"
|
||||
config LCD_ILI9341_240X320_NO_IPS
|
||||
bool "ILI9341, 分辨率240*320, 非IPS"
|
||||
bool "ILI9341 240*320, Non-IPS"
|
||||
config LCD_GC9A01_240X240
|
||||
bool "GC9A01, 分辨率240*240, 圆屏"
|
||||
bool "GC9A01 240*240 Circle"
|
||||
config LCD_TYPE_800_1280_10_1_INCH
|
||||
bool "Waveshare 101M-8001280-IPS-CT-K Display"
|
||||
config LCD_TYPE_800_1280_10_1_INCH_A
|
||||
@@ -446,7 +449,7 @@ choice DISPLAY_LCD_TYPE
|
||||
config LCD_TYPE_720_720_4_INCH
|
||||
bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-4C with 720*720 4inch round display"
|
||||
config LCD_CUSTOM
|
||||
bool "自定义屏幕参数"
|
||||
bool "Custom LCD (自定义屏幕参数)"
|
||||
endchoice
|
||||
|
||||
choice DISPLAY_ESP32S3_KORVO2_V3
|
||||
@@ -454,11 +457,11 @@ choice DISPLAY_ESP32S3_KORVO2_V3
|
||||
prompt "ESP32S3_KORVO2_V3 LCD Type"
|
||||
default ESP32S3_KORVO2_V3_LCD_ST7789
|
||||
help
|
||||
屏幕类型选择
|
||||
LCD Display Type
|
||||
config ESP32S3_KORVO2_V3_LCD_ST7789
|
||||
bool "ST7789, 分辨率240*280"
|
||||
bool "ST7789 240*280"
|
||||
config ESP32S3_KORVO2_V3_LCD_ILI9341
|
||||
bool "ILI9341, 分辨率240*320"
|
||||
bool "ILI9341 240*320"
|
||||
endchoice
|
||||
|
||||
choice DISPLAY_ESP32S3_AUDIO_BOARD
|
||||
@@ -466,53 +469,75 @@ choice DISPLAY_ESP32S3_AUDIO_BOARD
|
||||
prompt "ESP32S3_AUDIO_BOARD LCD Type"
|
||||
default AUDIO_BOARD_LCD_JD9853
|
||||
help
|
||||
屏幕类型选择
|
||||
LCD Display Type
|
||||
config AUDIO_BOARD_LCD_JD9853
|
||||
bool "JD9853, 分辨率320*172"
|
||||
bool "JD9853 320*172"
|
||||
config AUDIO_BOARD_LCD_ST7789
|
||||
bool "ST7789, 分辨率240*320"
|
||||
bool "ST7789 240*320"
|
||||
endchoice
|
||||
|
||||
config USE_WECHAT_MESSAGE_STYLE
|
||||
bool "Enable WeChat Message Style"
|
||||
default n
|
||||
choice DISPLAY_STYLE
|
||||
prompt "Select display style"
|
||||
default USE_DEFAULT_MESSAGE_STYLE
|
||||
help
|
||||
使用微信聊天界面风格
|
||||
Select display style for Xiaozhi device
|
||||
|
||||
config USE_ESP_WAKE_WORD
|
||||
bool "Enable Wake Word Detection (without AFE)"
|
||||
default n
|
||||
depends on IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C5 || IDF_TARGET_ESP32C6 || (IDF_TARGET_ESP32 && SPIRAM)
|
||||
help
|
||||
支持 ESP32 C3、ESP32 C5 与 ESP32 C6,增加ESP32支持(需要开启PSRAM)
|
||||
config USE_DEFAULT_MESSAGE_STYLE
|
||||
bool "Enable default message style"
|
||||
|
||||
config USE_AFE_WAKE_WORD
|
||||
bool "Enable Wake Word Detection (AFE)"
|
||||
default y
|
||||
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
|
||||
help
|
||||
需要 ESP32 S3 与 PSRAM 支持
|
||||
config USE_WECHAT_MESSAGE_STYLE
|
||||
bool "Enable WeChat Message Style"
|
||||
|
||||
config USE_CUSTOM_WAKE_WORD
|
||||
bool "Enable Custom Wake Word Detection"
|
||||
default n
|
||||
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM && (!USE_AFE_WAKE_WORD)
|
||||
config USE_EMOTE_MESSAGE_STYLE
|
||||
bool "Emote animation style"
|
||||
depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR
|
||||
endchoice
|
||||
|
||||
choice WAKE_WORD_TYPE
|
||||
prompt "Wake Word Implementation Type"
|
||||
default USE_AFE_WAKE_WORD if (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
|
||||
default WAKE_WORD_DISABLED
|
||||
help
|
||||
需要 ESP32 S3 与 PSRAM 支持
|
||||
|
||||
Choose the type of wake word implementation to use
|
||||
|
||||
config WAKE_WORD_DISABLED
|
||||
bool "Disabled"
|
||||
help
|
||||
Disable wake word detection
|
||||
|
||||
config USE_ESP_WAKE_WORD
|
||||
bool "Wakenet model without AFE"
|
||||
depends on IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C5 || IDF_TARGET_ESP32C6 || (IDF_TARGET_ESP32 && SPIRAM)
|
||||
help
|
||||
Support ESP32 C3、ESP32 C5 与 ESP32 C6, and (ESP32 with PSRAM)
|
||||
|
||||
config USE_AFE_WAKE_WORD
|
||||
bool "Wakenet model with AFE"
|
||||
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
|
||||
help
|
||||
Support AEC if available, requires ESP32 S3 and PSRAM
|
||||
|
||||
config USE_CUSTOM_WAKE_WORD
|
||||
bool "Multinet model (Custom Wake Word)"
|
||||
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
|
||||
help
|
||||
Requires ESP32 S3 and PSRAM
|
||||
|
||||
endchoice
|
||||
|
||||
config CUSTOM_WAKE_WORD
|
||||
string "Custom Wake Word"
|
||||
default "xiao tu dou"
|
||||
depends on USE_CUSTOM_WAKE_WORD
|
||||
help
|
||||
自定义唤醒词,中文用拼音表示,每个字之间用空格隔开
|
||||
Custom Wake Word, use pinyin for Chinese, separated by spaces
|
||||
|
||||
config CUSTOM_WAKE_WORD_DISPLAY
|
||||
string "Custom Wake Word Display"
|
||||
default "小土豆"
|
||||
depends on USE_CUSTOM_WAKE_WORD
|
||||
help
|
||||
唤醒后发送给服务器的问候语
|
||||
Greeting sent to the server after wake word detection
|
||||
|
||||
config CUSTOM_WAKE_WORD_THRESHOLD
|
||||
int "Custom Wake Word Threshold (%)"
|
||||
@@ -520,68 +545,83 @@ config CUSTOM_WAKE_WORD_THRESHOLD
|
||||
range 1 99
|
||||
depends on USE_CUSTOM_WAKE_WORD
|
||||
help
|
||||
自定义唤醒词阈值,范围1-99,越小越敏感,默认10
|
||||
Custom Wake Word Threshold, range 1-99, the smaller the more sensitive, default 20
|
||||
|
||||
config SEND_WAKE_WORD_DATA
|
||||
bool "Send Wake Word Data"
|
||||
default y
|
||||
depends on USE_AFE_WAKE_WORD || USE_CUSTOM_WAKE_WORD
|
||||
help
|
||||
Send wake word data to the server as the first message of the conversation and wait for response
|
||||
|
||||
config USE_AUDIO_PROCESSOR
|
||||
bool "Enable Audio Noise Reduction"
|
||||
default y
|
||||
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
|
||||
help
|
||||
需要 ESP32 S3 与 PSRAM 支持
|
||||
Requires ESP32 S3 and PSRAM
|
||||
|
||||
config USE_DEVICE_AEC
|
||||
bool "Enable Device-Side AEC"
|
||||
default n
|
||||
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE \
|
||||
|| BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 \
|
||||
|| BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B \
|
||||
|| BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32S3_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B \
|
||||
|| BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3 \
|
||||
|| BOARD_TYPE_ECHOEAR)
|
||||
|| BOARD_TYPE_ECHOEAR || BOARD_TYPE_ESP32S3_Touch_LCD_3_49)
|
||||
help
|
||||
因为性能不够,不建议和微信聊天界面风格同时开启
|
||||
To work properly, device-side AEC requires a clean output reference path from the speaker signal and physical acoustic isolation between the microphone and speaker.
|
||||
|
||||
config USE_SERVER_AEC
|
||||
bool "Enable Server-Side AEC (Unstable)"
|
||||
default n
|
||||
depends on USE_AUDIO_PROCESSOR
|
||||
help
|
||||
启用服务器端 AEC,需要服务器支持
|
||||
To work perperly, server-side AEC requires server support
|
||||
|
||||
config USE_AUDIO_DEBUGGER
|
||||
bool "Enable Audio Debugger"
|
||||
default n
|
||||
help
|
||||
启用音频调试功能,通过UDP发送音频数据
|
||||
|
||||
config USE_ACOUSTIC_WIFI_PROVISIONING
|
||||
bool "Enable Acoustic WiFi Provisioning"
|
||||
default n
|
||||
help
|
||||
启用声波配网功能,使用音频信号传输 WiFi 配置数据
|
||||
Enable audio debugger, send audio data through UDP to the host machine
|
||||
|
||||
config AUDIO_DEBUG_UDP_SERVER
|
||||
string "Audio Debug UDP Server Address"
|
||||
default "192.168.2.100:8000"
|
||||
depends on USE_AUDIO_DEBUGGER
|
||||
help
|
||||
UDP服务器地址,格式: IP:PORT,用于接收音频调试数据
|
||||
UDP server address, format: IP:PORT, used to receive audio debugging data
|
||||
|
||||
config USE_ACOUSTIC_WIFI_PROVISIONING
|
||||
bool "Enable Acoustic WiFi Provisioning"
|
||||
default n
|
||||
help
|
||||
Enable acoustic WiFi provisioning, use audio signal to transmit WiFi configuration data
|
||||
|
||||
config RECEIVE_CUSTOM_MESSAGE
|
||||
bool "Enable Custom Message Reception"
|
||||
default n
|
||||
help
|
||||
启用接收自定义消息功能,允许设备接收来自服务器的自定义消息(最好通过 MQTT 协议)
|
||||
Enable custom message reception, allow the device to receive custom messages from the server (preferably through the MQTT protocol)
|
||||
|
||||
choice I2S_TYPE_TAIJIPI_S3
|
||||
menu TAIJIPAI_S3_CONFIG
|
||||
depends on BOARD_TYPE_ESP32S3_Taiji_Pi
|
||||
prompt "taiji-pi-S3 I2S Type"
|
||||
default TAIJIPAI_I2S_TYPE_STD
|
||||
choice I2S_TYPE_TAIJIPI_S3
|
||||
prompt "taiji-pi-S3 I2S Type"
|
||||
default TAIJIPAI_I2S_TYPE_STD
|
||||
help
|
||||
I2S 类型选择
|
||||
config TAIJIPAI_I2S_TYPE_STD
|
||||
bool "I2S Type STD"
|
||||
config TAIJIPAI_I2S_TYPE_PDM
|
||||
bool "I2S Type PDM"
|
||||
endchoice
|
||||
|
||||
config I2S_USE_2SLOT
|
||||
bool "Enable I2S 2 Slot"
|
||||
default n
|
||||
help
|
||||
I2S 类型选择
|
||||
config TAIJIPAI_I2S_TYPE_STD
|
||||
bool "I2S Type STD"
|
||||
config TAIJIPAI_I2S_TYPE_PDM
|
||||
bool "I2S Type PDM"
|
||||
endchoice
|
||||
启动双声道
|
||||
endmenu
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -630,7 +630,7 @@ void Application::OnWakeWordDetected() {
|
||||
|
||||
auto wake_word = audio_service_.GetLastWakeWord();
|
||||
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
|
||||
#if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD
|
||||
#if CONFIG_SEND_WAKE_WORD_DATA
|
||||
// Encode and send the wake word data to the server
|
||||
while (auto packet = audio_service_.PopWakeWordPacket()) {
|
||||
protocol_->SendAudio(std::move(packet));
|
||||
@@ -711,11 +711,7 @@ void Application::SetDeviceState(DeviceState state) {
|
||||
if (listening_mode_ != kListeningModeRealtime) {
|
||||
audio_service_.EnableVoiceProcessing(false);
|
||||
// Only AFE wake word can be detected in speaking mode
|
||||
#if CONFIG_USE_AFE_WAKE_WORD
|
||||
audio_service_.EnableWakeWordDetection(true);
|
||||
#else
|
||||
audio_service_.EnableWakeWordDetection(false);
|
||||
#endif
|
||||
audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord());
|
||||
}
|
||||
audio_service_.ResetDecoder();
|
||||
break;
|
||||
|
||||
121
main/assets.cc
121
main/assets.cc
@@ -3,6 +3,7 @@
|
||||
#include "display.h"
|
||||
#include "application.h"
|
||||
#include "lvgl_theme.h"
|
||||
#include "emote_display.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <spi_flash_mmap.h>
|
||||
@@ -107,6 +108,7 @@ bool Assets::Apply() {
|
||||
ESP_LOGE(TAG, "The index.json file is not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
|
||||
if (root == nullptr) {
|
||||
ESP_LOGE(TAG, "The index.json file is not valid");
|
||||
@@ -175,7 +177,8 @@ bool Assets::Apply() {
|
||||
if (cJSON_IsObject(emoji)) {
|
||||
cJSON* name = cJSON_GetObjectItem(emoji, "name");
|
||||
cJSON* file = cJSON_GetObjectItem(emoji, "file");
|
||||
if (cJSON_IsString(name) && cJSON_IsString(file)) {
|
||||
cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf");
|
||||
if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL== eaf)) {
|
||||
if (!GetAssetData(file->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring);
|
||||
continue;
|
||||
@@ -237,7 +240,6 @@ bool Assets::Apply() {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
ESP_LOGI(TAG, "Refreshing display theme...");
|
||||
@@ -246,6 +248,121 @@ bool Assets::Apply() {
|
||||
if (current_theme != nullptr) {
|
||||
display->SetTheme(current_theme);
|
||||
}
|
||||
#elif defined(CONFIG_USE_EMOTE_MESSAGE_STYLE)
|
||||
auto &board = Board::GetInstance();
|
||||
auto display = board.GetDisplay();
|
||||
auto emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
|
||||
cJSON* font = cJSON_GetObjectItem(root, "text_font");
|
||||
if (cJSON_IsString(font)) {
|
||||
std::string fonts_text_file = font->valuestring;
|
||||
if (GetAssetData(fonts_text_file, ptr, size)) {
|
||||
auto text_font = std::make_shared<LvglCBinFont>(ptr);
|
||||
if (text_font->font() == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to load fonts.bin");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (emote_display) {
|
||||
emote_display->AddTextFont(text_font);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
|
||||
if (cJSON_IsArray(emoji_collection)) {
|
||||
int emoji_count = cJSON_GetArraySize(emoji_collection);
|
||||
if (emote_display) {
|
||||
for (int i = 0; i < emoji_count; i++) {
|
||||
cJSON* icon = cJSON_GetArrayItem(emoji_collection, i);
|
||||
if (cJSON_IsObject(icon)) {
|
||||
cJSON* name = cJSON_GetObjectItem(icon, "name");
|
||||
cJSON* file = cJSON_GetObjectItem(icon, "file");
|
||||
|
||||
if (cJSON_IsString(name) && cJSON_IsString(file)) {
|
||||
if (GetAssetData(file->valuestring, ptr, size)) {
|
||||
cJSON* eaf = cJSON_GetObjectItem(icon, "eaf");
|
||||
bool lack_value = false;
|
||||
bool loop_value = false;
|
||||
int fps_value = 0;
|
||||
|
||||
if (cJSON_IsObject(eaf)) {
|
||||
cJSON* lack = cJSON_GetObjectItem(eaf, "lack");
|
||||
cJSON* loop = cJSON_GetObjectItem(eaf, "loop");
|
||||
cJSON* fps = cJSON_GetObjectItem(eaf, "fps");
|
||||
|
||||
lack_value = lack ? cJSON_IsTrue(lack) : false;
|
||||
loop_value = loop ? cJSON_IsTrue(loop) : false;
|
||||
fps_value = fps ? fps->valueint : 0;
|
||||
|
||||
emote_display->AddEmojiData(name->valuestring, ptr, size,
|
||||
static_cast<uint8_t>(fps_value),
|
||||
loop_value, lack_value);
|
||||
}
|
||||
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Emoji \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* icon_collection = cJSON_GetObjectItem(root, "icon_collection");
|
||||
if (cJSON_IsArray(icon_collection)) {
|
||||
if (emote_display) {
|
||||
int icon_count = cJSON_GetArraySize(icon_collection);
|
||||
for (int i = 0; i < icon_count; i++) {
|
||||
cJSON* icon = cJSON_GetArrayItem(icon_collection, i);
|
||||
if (cJSON_IsObject(icon)) {
|
||||
cJSON* name = cJSON_GetObjectItem(icon, "name");
|
||||
cJSON* file = cJSON_GetObjectItem(icon, "file");
|
||||
|
||||
if (cJSON_IsString(name) && cJSON_IsString(file)) {
|
||||
if (GetAssetData(file->valuestring, ptr, size)) {
|
||||
emote_display->AddIconData(name->valuestring, ptr, size);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Icon \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* layout_json = cJSON_GetObjectItem(root, "layout");
|
||||
if (cJSON_IsArray(layout_json)) {
|
||||
int layout_count = cJSON_GetArraySize(layout_json);
|
||||
|
||||
for (int i = 0; i < layout_count; i++) {
|
||||
cJSON* layout_item = cJSON_GetArrayItem(layout_json, i);
|
||||
if (cJSON_IsObject(layout_item)) {
|
||||
cJSON* name = cJSON_GetObjectItem(layout_item, "name");
|
||||
cJSON* align = cJSON_GetObjectItem(layout_item, "align");
|
||||
cJSON* x = cJSON_GetObjectItem(layout_item, "x");
|
||||
cJSON* y = cJSON_GetObjectItem(layout_item, "y");
|
||||
cJSON* width = cJSON_GetObjectItem(layout_item, "width");
|
||||
cJSON* height = cJSON_GetObjectItem(layout_item, "height");
|
||||
|
||||
if (cJSON_IsString(name) && cJSON_IsString(align) && cJSON_IsNumber(x) && cJSON_IsNumber(y)) {
|
||||
int width_val = cJSON_IsNumber(width) ? width->valueint : 0;
|
||||
int height_val = cJSON_IsNumber(height) ? height->valueint : 0;
|
||||
|
||||
if (emote_display) {
|
||||
emote_display->AddLayoutData(name->valuestring, align->valuestring,
|
||||
x->valueint, y->valueint, width_val, height_val);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid layout item %d: missing required fields", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
cJSON_Delete(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -214,6 +214,84 @@ NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sampl
|
||||
ESP_LOGI(TAG, "Simplex channels created");
|
||||
}
|
||||
|
||||
NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask,gpio_num_t mic_sck, gpio_num_t mic_din) {
|
||||
duplex_ = false;
|
||||
input_sample_rate_ = input_sample_rate;
|
||||
output_sample_rate_ = output_sample_rate;
|
||||
|
||||
// Create a new channel for speaker
|
||||
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)1, I2S_ROLE_MASTER);
|
||||
tx_chan_cfg.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM;
|
||||
tx_chan_cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM;
|
||||
tx_chan_cfg.auto_clear_after_cb = true;
|
||||
tx_chan_cfg.auto_clear_before_cb = false;
|
||||
tx_chan_cfg.intr_priority = 0;
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle_, NULL));
|
||||
|
||||
|
||||
i2s_std_config_t tx_std_cfg = {
|
||||
.clk_cfg = {
|
||||
.sample_rate_hz = (uint32_t)output_sample_rate_,
|
||||
.clk_src = I2S_CLK_SRC_DEFAULT,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
|
||||
#ifdef I2S_HW_VERSION_2
|
||||
.ext_clk_freq_hz = 0,
|
||||
#endif
|
||||
|
||||
},
|
||||
.slot_cfg = {
|
||||
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
|
||||
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
|
||||
.slot_mode = I2S_SLOT_MODE_MONO,
|
||||
.slot_mask = spk_slot_mask,
|
||||
.ws_width = I2S_DATA_BIT_WIDTH_32BIT,
|
||||
.ws_pol = false,
|
||||
.bit_shift = true,
|
||||
#ifdef I2S_HW_VERSION_2
|
||||
.left_align = true,
|
||||
.big_endian = false,
|
||||
.bit_order_lsb = false
|
||||
#endif
|
||||
|
||||
},
|
||||
.gpio_cfg = {
|
||||
.mclk = I2S_GPIO_UNUSED,
|
||||
.bclk = spk_bclk,
|
||||
.ws = spk_ws,
|
||||
.dout = spk_dout,
|
||||
.din = I2S_GPIO_UNUSED,
|
||||
.invert_flags = {
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &tx_std_cfg));
|
||||
#if SOC_I2S_SUPPORTS_PDM_RX
|
||||
// Create a new channel for MIC in PDM mode
|
||||
i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER);
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_));
|
||||
i2s_pdm_rx_config_t pdm_rx_cfg = {
|
||||
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_),
|
||||
/* The data bit-width of PDM mode is fixed to 16 */
|
||||
.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
|
||||
.gpio_cfg = {
|
||||
.clk = mic_sck,
|
||||
.din = mic_din,
|
||||
|
||||
.invert_flags = {
|
||||
.clk_inv = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle_, &pdm_rx_cfg));
|
||||
#else
|
||||
ESP_LOGE(TAG, "PDM is not supported");
|
||||
#endif
|
||||
ESP_LOGI(TAG, "Simplex channels created");
|
||||
}
|
||||
|
||||
NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din) {
|
||||
duplex_ = false;
|
||||
input_sample_rate_ = input_sample_rate;
|
||||
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
class NoAudioCodecSimplexPdm : public NoAudioCodec {
|
||||
public:
|
||||
NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din);
|
||||
NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_din);
|
||||
int Read(int16_t* dest, int samples);
|
||||
};
|
||||
|
||||
|
||||
@@ -53,8 +53,6 @@ void AfeAudioProcessor::Initialize(AudioCodec* codec, int frame_duration_ms, srm
|
||||
afe_config->ns_init = false;
|
||||
}
|
||||
|
||||
afe_config->afe_perferred_core = 1;
|
||||
afe_config->afe_perferred_priority = 1;
|
||||
afe_config->agc_init = false;
|
||||
afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM;
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ bool CustomWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list)
|
||||
if (models_list == nullptr) {
|
||||
language_ = "cn";
|
||||
models_ = esp_srmodel_init("model");
|
||||
#if CONFIG_CUSTOM_WAKE_WORD
|
||||
#ifdef CONFIG_CUSTOM_WAKE_WORD
|
||||
threshold_ = CONFIG_CUSTOM_WAKE_WORD_THRESHOLD / 100.0f;
|
||||
commands_.push_back({CONFIG_CUSTOM_WAKE_WORD, CONFIG_CUSTOM_WAKE_WORD_DISPLAY, "wake"});
|
||||
#endif
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
#define ASR_BUTTON_GPIO GPIO_NUM_19
|
||||
#define BUILTIN_LED_GPIO GPIO_NUM_2
|
||||
|
||||
#define ML307_RX_PIN GPIO_NUM_16
|
||||
#define ML307_TX_PIN GPIO_NUM_17
|
||||
|
||||
#ifdef CONFIG_LCD_ST7789_240X240_7PIN
|
||||
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_22
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "wifi_board.h"
|
||||
#include "dual_network_board.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
@@ -58,7 +58,7 @@ static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = {
|
||||
|
||||
#define TAG "ESP32-LCD-MarsbearSupport"
|
||||
|
||||
class CompactWifiBoardLCD : public WifiBoard {
|
||||
class CompactWifiBoardLCD : public DualNetworkBoard {
|
||||
private:
|
||||
Button boot_button_;
|
||||
Button touch_button_;
|
||||
@@ -137,13 +137,25 @@ private:
|
||||
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (GetNetworkType() == NetworkType::WIFI) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
// cast to WifiBoard
|
||||
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
|
||||
wifi_board.ResetWifiConfiguration();
|
||||
}
|
||||
}
|
||||
gpio_set_level(BUILTIN_LED_GPIO, 1);
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
|
||||
boot_button_.OnDoubleClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
|
||||
SwitchNetworkType();
|
||||
}
|
||||
});
|
||||
|
||||
asr_button_.OnClick([this]() {
|
||||
std::string wake_word="你好小智";
|
||||
Application::GetInstance().WakeWordInvoke(wake_word);
|
||||
@@ -163,6 +175,7 @@ private:
|
||||
|
||||
public:
|
||||
CompactWifiBoardLCD() :
|
||||
DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN),
|
||||
boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO) {
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
#define ASR_BUTTON_GPIO GPIO_NUM_19
|
||||
#define BUILTIN_LED_GPIO GPIO_NUM_2
|
||||
|
||||
#define ML307_RX_PIN GPIO_NUM_16
|
||||
#define ML307_TX_PIN GPIO_NUM_17
|
||||
|
||||
#define DISPLAY_SDA_PIN GPIO_NUM_4
|
||||
#define DISPLAY_SCL_PIN GPIO_NUM_15
|
||||
#define DISPLAY_WIDTH 128
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "wifi_board.h"
|
||||
#include "dual_network_board.h"
|
||||
#include "codecs/no_audio_codec.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
#define TAG "ESP32-MarsbearSupport"
|
||||
|
||||
class CompactWifiBoard : public WifiBoard {
|
||||
class CompactWifiBoard : public DualNetworkBoard {
|
||||
private:
|
||||
Button boot_button_;
|
||||
Button touch_button_;
|
||||
@@ -105,13 +105,25 @@ private:
|
||||
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (GetNetworkType() == NetworkType::WIFI) {
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
// cast to WifiBoard
|
||||
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
|
||||
wifi_board.ResetWifiConfiguration();
|
||||
}
|
||||
}
|
||||
gpio_set_level(BUILTIN_LED_GPIO, 1);
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
|
||||
boot_button_.OnDoubleClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
|
||||
SwitchNetworkType();
|
||||
}
|
||||
});
|
||||
|
||||
asr_button_.OnClick([this]() {
|
||||
std::string wake_word="你好小智";
|
||||
Application::GetInstance().WakeWordInvoke(wake_word);
|
||||
@@ -133,7 +145,7 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
CompactWifiBoard() : boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO)
|
||||
CompactWifiBoard() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO)
|
||||
{
|
||||
InitializeDisplayI2c();
|
||||
InitializeSsd1306Display();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "wifi_board.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "display/emote_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "backlight.h"
|
||||
#include "emote_display.h"
|
||||
|
||||
#include <wifi_station.h>
|
||||
#include <esp_log.h>
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
#define TAG "EchoEar"
|
||||
|
||||
#define USE_LVGL_DEFAULT 0
|
||||
|
||||
temperature_sensor_handle_t temp_sensor = NULL;
|
||||
static const st77916_lcd_init_cmd_t vendor_specific_init_yysj[] = {
|
||||
@@ -387,11 +386,7 @@ private:
|
||||
Cst816s* cst816s_;
|
||||
Charge* charge_;
|
||||
Button boot_button_;
|
||||
#if USE_LVGL_DEFAULT
|
||||
LcdDisplay* display_;
|
||||
#else
|
||||
anim::EmoteDisplay* display_ = nullptr;
|
||||
#endif
|
||||
Display* display_ = nullptr;
|
||||
PwmBacklight* backlight_ = nullptr;
|
||||
esp_timer_handle_t touchpad_timer_;
|
||||
esp_lcd_touch_handle_t tp; // LCD touch handle
|
||||
@@ -517,13 +512,12 @@ private:
|
||||
|
||||
void InitializeSpi()
|
||||
{
|
||||
spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK,
|
||||
QSPI_PIN_NUM_LCD_DATA0,
|
||||
QSPI_PIN_NUM_LCD_DATA1,
|
||||
QSPI_PIN_NUM_LCD_DATA2,
|
||||
QSPI_PIN_NUM_LCD_DATA3,
|
||||
QSPI_LCD_H_RES * 80 * sizeof(uint16_t));
|
||||
// bus_config.isr_cpu_id = ESP_INTR_CPU_AFFINITY_1;
|
||||
const spi_bus_config_t bus_config = TAIJIPI_ST77916_PANEL_BUS_QSPI_CONFIG(QSPI_PIN_NUM_LCD_PCLK,
|
||||
QSPI_PIN_NUM_LCD_DATA0,
|
||||
QSPI_PIN_NUM_LCD_DATA1,
|
||||
QSPI_PIN_NUM_LCD_DATA2,
|
||||
QSPI_PIN_NUM_LCD_DATA3,
|
||||
QSPI_LCD_H_RES * 80 * sizeof(uint16_t));
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(QSPI_LCD_HOST, &bus_config, SPI_DMA_CH_AUTO));
|
||||
}
|
||||
|
||||
@@ -559,11 +553,11 @@ private:
|
||||
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
|
||||
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
|
||||
|
||||
#if USE_LVGL_DEFAULT
|
||||
#if CONFIG_USE_EMOTE_MESSAGE_STYLE
|
||||
display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||
#else
|
||||
display_ = new SpiLcdDisplay(panel_io, panel,
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
#else
|
||||
display_ = new anim::EmoteDisplay(panel, panel_io);
|
||||
#endif
|
||||
backlight_ = new PwmBacklight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
backlight_->RestoreBrightness();
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
"builds": [
|
||||
{
|
||||
"name": "echoear",
|
||||
"sdkconfig_append": [
|
||||
]
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"",
|
||||
"CONFIG_USE_EMOTE_MESSAGE_STYLE=y",
|
||||
"CONFIG_BOARD_TYPE_ECHOEAR=y",
|
||||
"CONFIG_FLASH_CUSTOM_ASSETS=y",
|
||||
"CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-echoear.bin\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
22
main/boards/echoear/emote.json
Normal file
22
main/boards/echoear/emote.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20},
|
||||
{"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20}
|
||||
]
|
||||
@@ -1,454 +0,0 @@
|
||||
#include "emote_display.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <tuple>
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <model_path.h>
|
||||
|
||||
#include "display/lcd_display.h"
|
||||
#include "config.h"
|
||||
#include "gfx.h"
|
||||
#include "application.h"
|
||||
|
||||
namespace anim {
|
||||
|
||||
static const char* TAG = "emoji";
|
||||
|
||||
// Asset name mapping from the old constants to file names
|
||||
static const std::unordered_map<std::string, std::string> asset_name_map = {
|
||||
{"angry_one", "angry_one.aaf"},
|
||||
{"dizzy_one", "dizzy_one.aaf"},
|
||||
{"enjoy_one", "enjoy_one.aaf"},
|
||||
{"happy_one", "happy_one.aaf"},
|
||||
{"idle_one", "idle_one.aaf"},
|
||||
{"listen", "listen.aaf"},
|
||||
{"sad_one", "sad_one.aaf"},
|
||||
{"shocked_one", "shocked_one.aaf"},
|
||||
{"thinking_one", "thinking_one.aaf"},
|
||||
{"icon_battery", "icon_Battery.bin"},
|
||||
{"icon_wifi_failed", "icon_WiFi_failed.bin"},
|
||||
{"icon_mic", "icon_mic.bin"},
|
||||
{"icon_speaker_zzz", "icon_speaker_zzz.bin"},
|
||||
{"icon_wifi", "icon_wifi.bin"},
|
||||
{"srmodels", "srmodels.bin"},
|
||||
{"kaiti", "KaiTi.ttf"}
|
||||
};
|
||||
|
||||
// UI element management
|
||||
static gfx_obj_t* obj_label_tips = nullptr;
|
||||
static gfx_obj_t* obj_label_time = nullptr;
|
||||
static gfx_obj_t* obj_anim_eye = nullptr;
|
||||
static gfx_obj_t* obj_anim_mic = nullptr;
|
||||
static gfx_obj_t* obj_img_icon = nullptr;
|
||||
static gfx_image_dsc_t icon_img_dsc;
|
||||
|
||||
// Track current icon to determine when to show time
|
||||
static std::string current_icon_type = "icon_battery";
|
||||
|
||||
enum class UIDisplayMode : uint8_t {
|
||||
SHOW_ANIM_TOP = 1, // Show obj_anim_mic
|
||||
SHOW_TIME = 2, // Show obj_label_time
|
||||
SHOW_TIPS = 3 // Show obj_label_tips
|
||||
};
|
||||
|
||||
static void SetUIDisplayMode(UIDisplayMode mode)
|
||||
{
|
||||
gfx_obj_set_visible(obj_anim_mic, false);
|
||||
gfx_obj_set_visible(obj_label_time, false);
|
||||
gfx_obj_set_visible(obj_label_tips, false);
|
||||
|
||||
// Show the selected control
|
||||
switch (mode) {
|
||||
case UIDisplayMode::SHOW_ANIM_TOP:
|
||||
gfx_obj_set_visible(obj_anim_mic, true);
|
||||
break;
|
||||
case UIDisplayMode::SHOW_TIME:
|
||||
gfx_obj_set_visible(obj_label_time, true);
|
||||
break;
|
||||
case UIDisplayMode::SHOW_TIPS:
|
||||
gfx_obj_set_visible(obj_label_tips, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void clock_tm_callback(void* user_data)
|
||||
{
|
||||
// Only display time when battery icon is shown
|
||||
if (current_icon_type == "icon_battery") {
|
||||
time_t now;
|
||||
struct tm timeinfo;
|
||||
time(&now);
|
||||
|
||||
setenv("TZ", "GMT+0", 1);
|
||||
tzset();
|
||||
localtime_r(&now, &timeinfo);
|
||||
|
||||
char time_str[6];
|
||||
snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
|
||||
|
||||
gfx_label_set_text(obj_label_time, time_str);
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void InitializeGraphics(esp_lcd_panel_handle_t panel, gfx_handle_t* engine_handle)
|
||||
{
|
||||
gfx_core_config_t gfx_cfg = {
|
||||
.flush_cb = EmoteEngine::OnFlush,
|
||||
.user_data = panel,
|
||||
.flags = {
|
||||
.swap = true,
|
||||
.double_buffer = true,
|
||||
.buff_dma = true,
|
||||
},
|
||||
.h_res = DISPLAY_WIDTH,
|
||||
.v_res = DISPLAY_HEIGHT,
|
||||
.fps = 30,
|
||||
.buffers = {
|
||||
.buf1 = nullptr,
|
||||
.buf2 = nullptr,
|
||||
.buf_pixels = DISPLAY_WIDTH * 16,
|
||||
},
|
||||
.task = GFX_EMOTE_INIT_CONFIG()
|
||||
};
|
||||
|
||||
gfx_cfg.task.task_stack_caps = MALLOC_CAP_DEFAULT;
|
||||
gfx_cfg.task.task_affinity = 1;
|
||||
gfx_cfg.task.task_priority = 1;
|
||||
gfx_cfg.task.task_stack = 20 * 1024;
|
||||
|
||||
*engine_handle = gfx_emote_init(&gfx_cfg);
|
||||
}
|
||||
|
||||
static void InitializeEyeAnimation(gfx_handle_t engine_handle)
|
||||
{
|
||||
obj_anim_eye = gfx_anim_create(engine_handle);
|
||||
|
||||
void* anim_data = nullptr;
|
||||
size_t anim_size = 0;
|
||||
auto& assets = Assets::GetInstance();
|
||||
if (!assets.GetAssetData(asset_name_map.at("idle_one"), anim_data, anim_size)) {
|
||||
ESP_LOGE(TAG, "Failed to get idle_one animation data");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_anim_set_src(obj_anim_eye, anim_data, anim_size);
|
||||
|
||||
gfx_obj_align(obj_anim_eye, GFX_ALIGN_LEFT_MID, 10, -20);
|
||||
gfx_anim_set_mirror(obj_anim_eye, true, (DISPLAY_WIDTH - (173 + 10) * 2));
|
||||
gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, 20, false);
|
||||
gfx_anim_start(obj_anim_eye);
|
||||
}
|
||||
|
||||
static void InitializeFont(gfx_handle_t engine_handle)
|
||||
{
|
||||
gfx_font_t font;
|
||||
void* font_data = nullptr;
|
||||
size_t font_size = 0;
|
||||
auto& assets = Assets::GetInstance();
|
||||
if (!assets.GetAssetData(asset_name_map.at("kaiti"), font_data, font_size)) {
|
||||
ESP_LOGE(TAG, "Failed to get kaiti font data");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_label_cfg_t font_cfg = {
|
||||
.name = "DejaVuSans.ttf",
|
||||
.mem = font_data,
|
||||
.mem_size = font_size,
|
||||
};
|
||||
gfx_label_new_font(engine_handle, &font_cfg, &font);
|
||||
|
||||
ESP_LOGI(TAG, "stack: %d", uxTaskGetStackHighWaterMark(nullptr));
|
||||
}
|
||||
|
||||
static void InitializeLabels(gfx_handle_t engine_handle)
|
||||
{
|
||||
// Initialize tips label
|
||||
obj_label_tips = gfx_label_create(engine_handle);
|
||||
gfx_obj_align(obj_label_tips, GFX_ALIGN_TOP_MID, 0, 45);
|
||||
gfx_obj_set_size(obj_label_tips, 160, 40);
|
||||
gfx_label_set_text(obj_label_tips, "启动中...");
|
||||
gfx_label_set_font_size(obj_label_tips, 20);
|
||||
gfx_label_set_color(obj_label_tips, GFX_COLOR_HEX(0xFFFFFF));
|
||||
gfx_label_set_text_align(obj_label_tips, GFX_TEXT_ALIGN_LEFT);
|
||||
gfx_label_set_long_mode(obj_label_tips, GFX_LABEL_LONG_SCROLL);
|
||||
gfx_label_set_scroll_speed(obj_label_tips, 20);
|
||||
gfx_label_set_scroll_loop(obj_label_tips, true);
|
||||
|
||||
// Initialize time label
|
||||
obj_label_time = gfx_label_create(engine_handle);
|
||||
gfx_obj_align(obj_label_time, GFX_ALIGN_TOP_MID, 0, 30);
|
||||
gfx_obj_set_size(obj_label_time, 160, 50);
|
||||
gfx_label_set_text(obj_label_time, "--:--");
|
||||
gfx_label_set_font_size(obj_label_time, 40);
|
||||
gfx_label_set_color(obj_label_time, GFX_COLOR_HEX(0xFFFFFF));
|
||||
gfx_label_set_text_align(obj_label_time, GFX_TEXT_ALIGN_CENTER);
|
||||
}
|
||||
|
||||
static void InitializeMicAnimation(gfx_handle_t engine_handle)
|
||||
{
|
||||
obj_anim_mic = gfx_anim_create(engine_handle);
|
||||
gfx_obj_align(obj_anim_mic, GFX_ALIGN_TOP_MID, 0, 25);
|
||||
|
||||
void* anim_data = nullptr;
|
||||
size_t anim_size = 0;
|
||||
auto& assets = Assets::GetInstance();
|
||||
if (!assets.GetAssetData(asset_name_map.at("listen"), anim_data, anim_size)) {
|
||||
ESP_LOGE(TAG, "Failed to get listen animation data");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_anim_set_src(obj_anim_mic, anim_data, anim_size);
|
||||
gfx_anim_start(obj_anim_mic);
|
||||
gfx_obj_set_visible(obj_anim_mic, false);
|
||||
}
|
||||
|
||||
static void InitializeIcon(gfx_handle_t engine_handle)
|
||||
{
|
||||
obj_img_icon = gfx_img_create(engine_handle);
|
||||
gfx_obj_align(obj_img_icon, GFX_ALIGN_TOP_MID, -100, 38);
|
||||
|
||||
SetupImageDescriptor(&icon_img_dsc, "icon_wifi_failed");
|
||||
gfx_img_set_src(obj_img_icon, static_cast<void*>(&icon_img_dsc));
|
||||
}
|
||||
|
||||
static void RegisterCallbacks(esp_lcd_panel_io_handle_t panel_io, gfx_handle_t engine_handle)
|
||||
{
|
||||
const esp_lcd_panel_io_callbacks_t cbs = {
|
||||
.on_color_trans_done = EmoteEngine::OnFlushIoReady,
|
||||
};
|
||||
esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, engine_handle);
|
||||
}
|
||||
|
||||
void SetupImageDescriptor(gfx_image_dsc_t* img_dsc, const std::string& asset_name)
|
||||
{
|
||||
auto& assets = Assets::GetInstance();
|
||||
std::string filename = asset_name_map.at(asset_name);
|
||||
|
||||
void* img_data = nullptr;
|
||||
size_t img_size = 0;
|
||||
if (!assets.GetAssetData(filename, img_data, img_size)) {
|
||||
ESP_LOGE(TAG, "Failed to get asset data for %s", asset_name.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::memcpy(&img_dsc->header, img_data, sizeof(gfx_image_header_t));
|
||||
img_dsc->data = static_cast<const uint8_t*>(img_data) + sizeof(gfx_image_header_t);
|
||||
img_dsc->data_size = img_size - sizeof(gfx_image_header_t);
|
||||
}
|
||||
|
||||
EmoteEngine::EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
|
||||
{
|
||||
ESP_LOGI(TAG, "Create EmoteEngine, panel: %p, panel_io: %p", panel, panel_io);
|
||||
|
||||
InitializeGraphics(panel, &engine_handle_);
|
||||
|
||||
gfx_emote_lock(engine_handle_);
|
||||
gfx_emote_set_bg_color(engine_handle_, GFX_COLOR_HEX(0x000000));
|
||||
|
||||
// Initialize all UI components
|
||||
InitializeEyeAnimation(engine_handle_);
|
||||
InitializeFont(engine_handle_);
|
||||
InitializeLabels(engine_handle_);
|
||||
InitializeMicAnimation(engine_handle_);
|
||||
InitializeIcon(engine_handle_);
|
||||
|
||||
current_icon_type = "icon_wifi_failed";
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
|
||||
|
||||
gfx_timer_create(engine_handle_, clock_tm_callback, 1000, obj_label_tips);
|
||||
|
||||
gfx_emote_unlock(engine_handle_);
|
||||
|
||||
RegisterCallbacks(panel_io, engine_handle_);
|
||||
}
|
||||
|
||||
EmoteEngine::~EmoteEngine()
|
||||
{
|
||||
if (engine_handle_) {
|
||||
gfx_emote_deinit(engine_handle_);
|
||||
engine_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteEngine::setEyes(const std::string& asset_name, bool repeat, int fps)
|
||||
{
|
||||
if (!engine_handle_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& assets = Assets::GetInstance();
|
||||
std::string filename = asset_name_map.at(asset_name);
|
||||
|
||||
void* src_data = nullptr;
|
||||
size_t src_len = 0;
|
||||
if (!assets.GetAssetData(filename, src_data, src_len)) {
|
||||
ESP_LOGE(TAG, "Failed to get asset data for %s", asset_name.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Lock();
|
||||
gfx_anim_set_src(obj_anim_eye, src_data, src_len);
|
||||
gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, fps, repeat);
|
||||
gfx_anim_start(obj_anim_eye);
|
||||
Unlock();
|
||||
}
|
||||
|
||||
void EmoteEngine::stopEyes()
|
||||
{
|
||||
// Implementation if needed
|
||||
}
|
||||
|
||||
void EmoteEngine::Lock()
|
||||
{
|
||||
if (engine_handle_) {
|
||||
gfx_emote_lock(engine_handle_);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteEngine::Unlock()
|
||||
{
|
||||
if (engine_handle_) {
|
||||
gfx_emote_unlock(engine_handle_);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteEngine::SetIcon(const std::string& asset_name)
|
||||
{
|
||||
if (!engine_handle_) {
|
||||
return;
|
||||
}
|
||||
|
||||
Lock();
|
||||
SetupImageDescriptor(&icon_img_dsc, asset_name);
|
||||
gfx_img_set_src(obj_img_icon, static_cast<void*>(&icon_img_dsc));
|
||||
current_icon_type = asset_name;
|
||||
Unlock();
|
||||
}
|
||||
|
||||
bool EmoteEngine::OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_io_event_data_t* edata,
|
||||
void* user_ctx)
|
||||
{
|
||||
gfx_emote_flush_ready((gfx_handle_t)user_ctx, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void EmoteEngine::OnFlush(gfx_handle_t handle, int x_start, int y_start,
|
||||
int x_end, int y_end, const void* color_data)
|
||||
{
|
||||
auto* panel = static_cast<esp_lcd_panel_handle_t>(gfx_emote_get_user_data(handle));
|
||||
if (panel) {
|
||||
esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data);
|
||||
}
|
||||
}
|
||||
|
||||
// EmoteDisplay implementation
|
||||
EmoteDisplay::EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
|
||||
{
|
||||
InitializeEngine(panel, panel_io);
|
||||
}
|
||||
|
||||
EmoteDisplay::~EmoteDisplay() = default;
|
||||
|
||||
void EmoteDisplay::SetEmotion(const char* emotion)
|
||||
{
|
||||
if (!engine_) {
|
||||
return;
|
||||
}
|
||||
|
||||
using EmotionParam = std::tuple<std::string, bool, int>;
|
||||
static const std::unordered_map<std::string, EmotionParam> emotion_map = {
|
||||
{"happy", {"happy_one", true, 20}},
|
||||
{"laughing", {"enjoy_one", true, 20}},
|
||||
{"funny", {"happy_one", true, 20}},
|
||||
{"loving", {"happy_one", true, 20}},
|
||||
{"embarrassed", {"happy_one", true, 20}},
|
||||
{"confident", {"happy_one", true, 20}},
|
||||
{"delicious", {"happy_one", true, 20}},
|
||||
{"sad", {"sad_one", true, 20}},
|
||||
{"crying", {"happy_one", true, 20}},
|
||||
{"sleepy", {"happy_one", true, 20}},
|
||||
{"silly", {"happy_one", true, 20}},
|
||||
{"angry", {"angry_one", true, 20}},
|
||||
{"surprised", {"happy_one", true, 20}},
|
||||
{"shocked", {"shocked_one", true, 20}},
|
||||
{"thinking", {"thinking_one", true, 20}},
|
||||
{"winking", {"happy_one", true, 20}},
|
||||
{"relaxed", {"happy_one", true, 20}},
|
||||
{"confused", {"dizzy_one", true, 20}},
|
||||
{"neutral", {"idle_one", false, 20}},
|
||||
{"idle", {"idle_one", false, 20}},
|
||||
};
|
||||
|
||||
auto it = emotion_map.find(emotion);
|
||||
if (it != emotion_map.end()) {
|
||||
std::string asset_name = std::get<0>(it->second);
|
||||
bool repeat = std::get<1>(it->second);
|
||||
int fps = std::get<2>(it->second);
|
||||
engine_->setEyes(asset_name, repeat, fps);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetChatMessage(const char* role, const char* content)
|
||||
{
|
||||
engine_->Lock();
|
||||
if (content && strlen(content) > 0) {
|
||||
gfx_label_set_text(obj_label_tips, content);
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
|
||||
}
|
||||
engine_->Unlock();
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetStatus(const char* status)
|
||||
{
|
||||
if (!engine_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::strcmp(status, "聆听中...") == 0) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_ANIM_TOP);
|
||||
engine_->setEyes("happy_one", true, 20);
|
||||
engine_->SetIcon("icon_mic");
|
||||
} else if (std::strcmp(status, "待命") == 0) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIME);
|
||||
engine_->SetIcon("icon_battery");
|
||||
} else if (std::strcmp(status, "说话中...") == 0) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
|
||||
engine_->SetIcon("icon_speaker_zzz");
|
||||
} else if (std::strcmp(status, "错误") == 0) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
|
||||
engine_->SetIcon("icon_wifi_failed");
|
||||
}
|
||||
|
||||
engine_->Lock();
|
||||
if (std::strcmp(status, "连接中...") != 0) {
|
||||
gfx_label_set_text(obj_label_tips, status);
|
||||
}
|
||||
engine_->Unlock();
|
||||
}
|
||||
|
||||
void EmoteDisplay::InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
|
||||
{
|
||||
engine_ = std::make_unique<EmoteEngine>(panel, panel_io);
|
||||
}
|
||||
|
||||
bool EmoteDisplay::Lock(int timeout_ms)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void EmoteDisplay::Unlock()
|
||||
{
|
||||
// Implementation if needed
|
||||
}
|
||||
|
||||
} // namespace anim
|
||||
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "display/lcd_display.h"
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include "gfx.h"
|
||||
#include "assets.h"
|
||||
|
||||
namespace anim {
|
||||
|
||||
// Helper function for setting up image descriptors
|
||||
void SetupImageDescriptor(gfx_image_dsc_t* img_dsc, const std::string& asset_name);
|
||||
|
||||
class EmoteEngine;
|
||||
|
||||
using FlushIoReadyCallback = std::function<bool(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void*)>;
|
||||
using FlushCallback = std::function<void(gfx_handle_t, int, int, int, int, const void*)>;
|
||||
|
||||
class EmoteEngine {
|
||||
public:
|
||||
EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
|
||||
~EmoteEngine();
|
||||
|
||||
void setEyes(const std::string& asset_name, bool repeat, int fps);
|
||||
void stopEyes();
|
||||
|
||||
void Lock();
|
||||
void Unlock();
|
||||
|
||||
void SetIcon(const std::string& asset_name);
|
||||
|
||||
// Callback functions (public to be accessible from static helper functions)
|
||||
static bool OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx);
|
||||
static void OnFlush(gfx_handle_t handle, int x_start, int y_start, int x_end, int y_end, const void *color_data);
|
||||
|
||||
private:
|
||||
gfx_handle_t engine_handle_;
|
||||
};
|
||||
|
||||
class EmoteDisplay : public Display {
|
||||
public:
|
||||
EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
|
||||
virtual ~EmoteDisplay();
|
||||
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetStatus(const char* status) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
|
||||
anim::EmoteEngine* GetEngine()
|
||||
{
|
||||
return engine_.get();
|
||||
}
|
||||
|
||||
private:
|
||||
void InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
|
||||
virtual bool Lock(int timeout_ms = 0) override;
|
||||
virtual void Unlock() override;
|
||||
|
||||
std::unique_ptr<anim::EmoteEngine> engine_;
|
||||
};
|
||||
|
||||
} // namespace anim
|
||||
37
main/boards/echoear/layout.json
Normal file
37
main/boards/echoear/layout.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"name": "eye_anim",
|
||||
"align": "GFX_ALIGN_LEFT_MID",
|
||||
"x": 10,
|
||||
"y": 10
|
||||
},
|
||||
{
|
||||
"name": "status_icon",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": -100,
|
||||
"y": 38
|
||||
},
|
||||
{
|
||||
"name": "toast_label",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 40,
|
||||
"width": 160,
|
||||
"height": 40
|
||||
},
|
||||
{
|
||||
"name": "clock_label",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 40,
|
||||
"width": 60,
|
||||
"height": 50
|
||||
},
|
||||
{
|
||||
"name": "listen_anim",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 25
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
{
|
||||
"name": "esp-box-3",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_USE_DEVICE_AEC=y"
|
||||
"CONFIG_USE_DEVICE_AEC=y",
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"",
|
||||
"CONFIG_USE_EMOTE_MESSAGE_STYLE=y",
|
||||
"CONFIG_BOARD_TYPE_ESP_BOX_3=y",
|
||||
"CONFIG_FLASH_CUSTOM_ASSETS=y",
|
||||
"CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-esp-box-3.bin\""
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
22
main/boards/esp-box-3/emote.json
Normal file
22
main/boards/esp-box-3/emote.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20},
|
||||
{"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20},
|
||||
{"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20}
|
||||
]
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "wifi_board.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/display.h"
|
||||
#include "display/emote_display.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "esp_lcd_ili9341.h"
|
||||
#include "application.h"
|
||||
@@ -39,7 +41,7 @@ class EspBox3Board : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
Button boot_button_;
|
||||
LcdDisplay* display_;
|
||||
Display* display_;
|
||||
|
||||
void InitializeI2c() {
|
||||
// Initialize I2C peripheral
|
||||
@@ -125,8 +127,13 @@ private:
|
||||
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
|
||||
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
|
||||
esp_lcd_panel_disp_on_off(panel, true);
|
||||
|
||||
#if CONFIG_USE_EMOTE_MESSAGE_STYLE
|
||||
display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||
#else
|
||||
display_ = new SpiLcdDisplay(panel_io, panel,
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
37
main/boards/esp-box-3/layout.json
Normal file
37
main/boards/esp-box-3/layout.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"name": "eye_anim",
|
||||
"align": "GFX_ALIGN_LEFT_MID",
|
||||
"x": 10,
|
||||
"y": 30
|
||||
},
|
||||
{
|
||||
"name": "status_icon",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": -120,
|
||||
"y": 18
|
||||
},
|
||||
{
|
||||
"name": "toast_label",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
"width": 200,
|
||||
"height": 40
|
||||
},
|
||||
{
|
||||
"name": "clock_label",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
"width": 200,
|
||||
"height": 50
|
||||
},
|
||||
{
|
||||
"name": "listen_anim",
|
||||
"align": "GFX_ALIGN_TOP_MID",
|
||||
"x": 0,
|
||||
"y": 5
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 由于原来的麦克风型号停产,2025年7月之后的太极派(JC3636W518)更换了麦克风,并且更换了屏幕玻璃,所以在产品标签上批次号大于2528的用户请选择I2S Type PDM,
|
||||
|
||||
# 新增双声道配置
|
||||
|
||||
# 编译配置命令
|
||||
|
||||
**配置编译目标为 ESP32S3:**
|
||||
@@ -19,9 +21,16 @@ idf.py menuconfig
|
||||
```
|
||||
Xiaozhi Assistant -> Board Type -> 太极小派esp32s3
|
||||
|
||||
Xiaozhi Assistant -> taiji-pi-S3 I2S Type -> I2S Type PDM
|
||||
Xiaozhi Assistant -> TAIJIPAI_S3_CONFIG -> taiji-pi-S3 I2S Type -> I2S Type PDM
|
||||
```
|
||||
|
||||
**如果需要选择双声道:**
|
||||
```
|
||||
|
||||
Xiaozhi Assistant -> TAIJIPAI_S3_CONFIG -> Enabel use 2 slot
|
||||
```
|
||||
|
||||
|
||||
**修改PSRAM配置:**
|
||||
|
||||
```
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
|
||||
#define DISPLAY_WIDTH 360
|
||||
#define DISPLAY_HEIGHT 360
|
||||
#define DISPLAY_MIRROR_X false
|
||||
#define DISPLAY_MIRROR_Y false
|
||||
#define DISPLAY_MIRROR_X true
|
||||
#define DISPLAY_MIRROR_Y true
|
||||
#define DISPLAY_SWAP_XY false
|
||||
|
||||
#define QSPI_LCD_H_RES (360)
|
||||
|
||||
@@ -618,9 +618,17 @@ public:
|
||||
AUDIO_I2S_GPIO_BCLK,
|
||||
AUDIO_I2S_GPIO_WS,
|
||||
AUDIO_I2S_GPIO_DOUT,
|
||||
#ifdef CONFIG_I2S_USE_2SLOT
|
||||
I2S_STD_SLOT_BOTH,
|
||||
#endif
|
||||
AUDIO_MIC_SCK_PIN,
|
||||
AUDIO_MIC_WS_PIN,
|
||||
#ifdef CONFIG_I2S_USE_2SLOT
|
||||
AUDIO_MIC_SD_PIN,
|
||||
I2S_STD_SLOT_LEFT
|
||||
#else
|
||||
AUDIO_MIC_SD_PIN
|
||||
#endif
|
||||
);
|
||||
#else
|
||||
static NoAudioCodecSimplexPdm audio_codec(
|
||||
@@ -629,6 +637,9 @@ public:
|
||||
AUDIO_I2S_GPIO_BCLK,
|
||||
AUDIO_I2S_GPIO_WS,
|
||||
AUDIO_I2S_GPIO_DOUT,
|
||||
#ifdef CONFIG_I2S_USE_2SLOT
|
||||
I2S_STD_SLOT_BOTH,
|
||||
#endif
|
||||
AUDIO_MIC_WS_PIN,
|
||||
AUDIO_MIC_SD_PIN
|
||||
);
|
||||
@@ -650,4 +661,4 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(TaijiPiS3Board);
|
||||
DECLARE_BOARD(TaijiPiS3Board);
|
||||
|
||||
3
main/boards/waveshare-s3-touch-lcd-3.49/README.md
Normal file
3
main/boards/waveshare-s3-touch-lcd-3.49/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
新增 微雪 开发板: ESP32-S3-Touch-LCD-3.49
|
||||
产品链接:
|
||||
https://www.waveshare.net/shop/ESP32-S3-Touch-LCD-3.49.htm
|
||||
62
main/boards/waveshare-s3-touch-lcd-3.49/config.h
Normal file
62
main/boards/waveshare-s3-touch-lcd-3.49/config.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#ifndef _BOARD_CONFIG_H_
|
||||
#define _BOARD_CONFIG_H_
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_master.h>
|
||||
#include "lvgl.h"
|
||||
|
||||
#define AUDIO_INPUT_SAMPLE_RATE 24000
|
||||
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
|
||||
|
||||
#define AUDIO_INPUT_REFERENCE true
|
||||
|
||||
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_7
|
||||
#define AUDIO_I2S_GPIO_WS GPIO_NUM_46
|
||||
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_15
|
||||
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6
|
||||
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_45
|
||||
|
||||
#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC
|
||||
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_47
|
||||
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_48
|
||||
#define Dev_Touch_I2C_SDA_PIN GPIO_NUM_17
|
||||
#define Dev_Touch_I2C_SCL_PIN GPIO_NUM_18
|
||||
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
|
||||
#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR
|
||||
|
||||
#define I2C_Touch_ADDRESS 0x3b
|
||||
#define I2C_Touch_SDA_PIN GPIO_NUM_17
|
||||
#define I2C_Touch_SCL_PIN GPIO_NUM_18
|
||||
|
||||
#define BOOT_BUTTON_GPIO GPIO_NUM_0
|
||||
#define PWR_BUTTON_GPIO GPIO_NUM_16
|
||||
|
||||
#define LCD_CS GPIO_NUM_9
|
||||
#define LCD_PCLK GPIO_NUM_10
|
||||
#define LCD_D0 GPIO_NUM_11
|
||||
#define LCD_D1 GPIO_NUM_12
|
||||
#define LCD_D2 GPIO_NUM_13
|
||||
#define LCD_D3 GPIO_NUM_14
|
||||
#define LCD_RST GPIO_NUM_21
|
||||
#define LCD_LIGHT (-1)
|
||||
|
||||
#define DISPLAY_WIDTH 172
|
||||
#define DISPLAY_HEIGHT 640
|
||||
#define LVGL_DMA_BUFF_LEN (DISPLAY_WIDTH * 64 * 2)
|
||||
#define LVGL_SPIRAM_BUFF_LEN (DISPLAY_WIDTH * DISPLAY_HEIGHT * 2)
|
||||
|
||||
#define DISPLAY_ROTATION_90 false
|
||||
|
||||
#define DISPLAY_MIRROR_X false
|
||||
#define DISPLAY_MIRROR_Y false
|
||||
#define DISPLAY_SWAP_XY false
|
||||
|
||||
#define DISPLAY_OFFSET_X 0
|
||||
#define DISPLAY_OFFSET_Y 0
|
||||
|
||||
|
||||
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_8
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
|
||||
|
||||
|
||||
#endif // _BOARD_CONFIG_H_
|
||||
12
main/boards/waveshare-s3-touch-lcd-3.49/config.json
Normal file
12
main/boards/waveshare-s3-touch-lcd-3.49/config.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "waveshare-s3-touch-lcd-3.49",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_USE_WECHAT_MESSAGE_STYLE=n",
|
||||
"CONFIG_USE_DEVICE_AEC=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
145
main/boards/waveshare-s3-touch-lcd-3.49/custom_lcd_display.cc
Normal file
145
main/boards/waveshare-s3-touch-lcd-3.49/custom_lcd_display.cc
Normal file
@@ -0,0 +1,145 @@
|
||||
#include "custom_lcd_display.h"
|
||||
|
||||
#include "lcd_display.h"
|
||||
|
||||
#include <vector>
|
||||
#include <esp_log.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
#include "assets/lang_config.h"
|
||||
#include <cstring>
|
||||
#include "settings.h"
|
||||
|
||||
#include "esp_lcd_panel_io.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "board.h"
|
||||
|
||||
#define TAG "CustomLcdDisplay"
|
||||
|
||||
static SemaphoreHandle_t trans_done_sem = NULL;
|
||||
static uint16_t *trans_buf_1;
|
||||
|
||||
#if (DISPLAY_ROTATION_90 == true)
|
||||
static uint16_t *dest_map;
|
||||
#endif
|
||||
|
||||
|
||||
bool CustomLcdDisplay::lvgl_port_flush_io_ready_callback(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) {
|
||||
BaseType_t taskAwake = pdFALSE;
|
||||
lv_display_t *disp_drv = (lv_display_t *)user_ctx;
|
||||
assert(disp_drv != NULL);
|
||||
if (trans_done_sem) {
|
||||
xSemaphoreGiveFromISR(trans_done_sem, &taskAwake);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CustomLcdDisplay::lvgl_port_flush_callback(lv_display_t *drv, const lv_area_t *area, uint8_t *color_map) {
|
||||
assert(drv != NULL);
|
||||
esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)lv_display_get_user_data(drv);
|
||||
assert(panel_handle != NULL);
|
||||
|
||||
lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area));
|
||||
|
||||
#if (DISPLAY_ROTATION_90 == true)
|
||||
lv_display_rotation_t rotation = lv_display_get_rotation(drv);
|
||||
lv_area_t rotated_area;
|
||||
if(rotation != LV_DISPLAY_ROTATION_0) {
|
||||
lv_color_format_t cf = lv_display_get_color_format(drv);
|
||||
/*Calculate the position of the rotated area*/
|
||||
rotated_area = *area;
|
||||
lv_display_rotate_area(drv, &rotated_area);
|
||||
/*Calculate the source stride (bytes in a line) from the width of the area*/
|
||||
uint32_t src_stride = lv_draw_buf_width_to_stride(lv_area_get_width(area), cf);
|
||||
/*Calculate the stride of the destination (rotated) area too*/
|
||||
uint32_t dest_stride = lv_draw_buf_width_to_stride(lv_area_get_width(&rotated_area), cf);
|
||||
/*Have a buffer to store the rotated area and perform the rotation*/
|
||||
|
||||
int32_t src_w = lv_area_get_width(area);
|
||||
int32_t src_h = lv_area_get_height(area);
|
||||
lv_draw_sw_rotate(color_map, dest_map, src_w, src_h, src_stride, dest_stride, rotation, cf);
|
||||
/*Use the rotated area and rotated buffer from now on*/
|
||||
area = &rotated_area;
|
||||
}
|
||||
#endif
|
||||
const int flush_coun = (LVGL_SPIRAM_BUFF_LEN / LVGL_DMA_BUFF_LEN);
|
||||
const int offgap = (DISPLAY_HEIGHT / flush_coun);
|
||||
const int dmalen = (LVGL_DMA_BUFF_LEN / 2);
|
||||
int offsetx1 = 0;
|
||||
int offsety1 = 0;
|
||||
int offsetx2 = DISPLAY_WIDTH;
|
||||
int offsety2 = offgap;
|
||||
#if (DISPLAY_ROTATION_90 == true)
|
||||
uint16_t *map = (uint16_t*)dest_map;
|
||||
#else
|
||||
uint16_t *map = (uint16_t*)color_map;
|
||||
#endif
|
||||
xSemaphoreGive(trans_done_sem);
|
||||
|
||||
for(int i = 0; i<flush_coun; i++) {
|
||||
xSemaphoreTake(trans_done_sem,portMAX_DELAY);
|
||||
memcpy(trans_buf_1,map,LVGL_DMA_BUFF_LEN);
|
||||
esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2, offsety2, trans_buf_1);
|
||||
offsety1 += offgap;
|
||||
offsety2 += offgap;
|
||||
map += dmalen;
|
||||
}
|
||||
xSemaphoreTake(trans_done_sem,portMAX_DELAY);
|
||||
lv_disp_flush_ready(drv);
|
||||
}
|
||||
|
||||
|
||||
CustomLcdDisplay::CustomLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
||||
int width, int height, int offset_x, int offset_y,
|
||||
bool mirror_x, bool mirror_y, bool swap_xy)
|
||||
: LcdDisplay(panel_io, panel, width, height) {
|
||||
|
||||
ESP_LOGI(TAG, "Initialize LVGL library");
|
||||
lv_init();
|
||||
|
||||
ESP_LOGI(TAG, "Initialize LVGL port");
|
||||
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
|
||||
port_cfg.task_priority = 2;
|
||||
port_cfg.timer_period_ms = 50;
|
||||
lvgl_port_init(&port_cfg);
|
||||
trans_done_sem = xSemaphoreCreateBinary();
|
||||
trans_buf_1 = (uint16_t *)heap_caps_malloc(LVGL_DMA_BUFF_LEN, MALLOC_CAP_DMA);
|
||||
|
||||
uint32_t buffer_size = 0;
|
||||
lv_color_t *buf1 = NULL;
|
||||
lvgl_port_lock(0);
|
||||
uint8_t color_bytes = lv_color_format_get_size(LV_COLOR_FORMAT_RGB565);
|
||||
display_ = lv_display_create(width_, height_);
|
||||
lv_display_set_flush_cb(display_, lvgl_port_flush_callback);
|
||||
buffer_size = width_ * height_;
|
||||
buf1 = (lv_color_t *)heap_caps_aligned_alloc(1, buffer_size * color_bytes, MALLOC_CAP_SPIRAM);
|
||||
#if (DISPLAY_ROTATION_90 == true)
|
||||
dest_map = (uint16_t *)heap_caps_malloc(buffer_size * color_bytes, MALLOC_CAP_SPIRAM);
|
||||
lv_display_set_rotation(display_, LV_DISPLAY_ROTATION_90);
|
||||
#endif
|
||||
lv_display_set_buffers(display_, buf1, NULL, buffer_size * color_bytes, LV_DISPLAY_RENDER_MODE_FULL);
|
||||
lv_display_set_user_data(display_, panel_);
|
||||
lvgl_port_unlock();
|
||||
|
||||
esp_lcd_panel_io_callbacks_t cbs = {
|
||||
.on_color_trans_done = lvgl_port_flush_io_ready_callback,
|
||||
};
|
||||
/* Register done callback */
|
||||
esp_lcd_panel_io_register_event_callbacks(panel_io_, &cbs, display_);
|
||||
|
||||
if (display_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to add display");
|
||||
return;
|
||||
}
|
||||
|
||||
if (offset_x != 0 || offset_y != 0) {
|
||||
lv_display_set_offset(display_, offset_x, offset_y);
|
||||
}
|
||||
SetupUI();
|
||||
}
|
||||
17
main/boards/waveshare-s3-touch-lcd-3.49/custom_lcd_display.h
Normal file
17
main/boards/waveshare-s3-touch-lcd-3.49/custom_lcd_display.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef __CUSTOM_LCD_DISPLAY_H__
|
||||
#define __CUSTOM_LCD_DISPLAY_H__
|
||||
|
||||
#include "lcd_display.h"
|
||||
|
||||
// // SPI LCD显示器
|
||||
class CustomLcdDisplay : public LcdDisplay {
|
||||
public:
|
||||
CustomLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
||||
int width, int height, int offset_x, int offset_y,
|
||||
bool mirror_x, bool mirror_y, bool swap_xy);
|
||||
private:
|
||||
static bool lvgl_port_flush_io_ready_callback(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx);
|
||||
static void lvgl_port_flush_callback(lv_display_t *drv, const lv_area_t *area, uint8_t *color_map);
|
||||
};
|
||||
|
||||
#endif // __CUSTOM_LCD_DISPLAY_H__
|
||||
@@ -0,0 +1,253 @@
|
||||
#include "wifi_board.h"
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "system_reset.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include "i2c_device.h"
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/ledc.h>
|
||||
#include <wifi_station.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
|
||||
#include <esp_timer.h>
|
||||
#include "esp_io_expander_tca9554.h"
|
||||
|
||||
#include "esp_lcd_axs15231b.h"
|
||||
|
||||
#include "custom_lcd_display.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#define TAG "waveshare_lcd_3_39"
|
||||
|
||||
|
||||
static const axs15231b_lcd_init_cmd_t lcd_init_cmds[] = {
|
||||
{0x11, (uint8_t []){0x00}, 0, 100},
|
||||
{0x29, (uint8_t []){0x00}, 0, 100},
|
||||
};
|
||||
|
||||
class CustomBoard : public WifiBoard {
|
||||
private:
|
||||
Button boot_button_;
|
||||
Button pwr_button_;
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
esp_io_expander_handle_t io_expander = NULL;
|
||||
LcdDisplay* display_;
|
||||
i2c_master_dev_handle_t disp_touch_dev_handle = NULL;
|
||||
lv_indev_t *touch_indev = NULL; //touch
|
||||
bool is_PwrControlEn = false;
|
||||
|
||||
void InitializeI2c() {
|
||||
// Initialize I2C peripheral
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {
|
||||
.i2c_port = (i2c_port_t)I2C_NUM_0,
|
||||
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
|
||||
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.intr_priority = 0,
|
||||
.trans_queue_depth = 0,
|
||||
.flags = {
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
|
||||
}
|
||||
|
||||
void InitializeTca9554(void) {
|
||||
esp_err_t ret = esp_io_expander_new_i2c_tca9554(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000, &io_expander);
|
||||
if(ret != ESP_OK)
|
||||
ESP_LOGE(TAG, "TCA9554 create returned error");
|
||||
ret = esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_7 | IO_EXPANDER_PIN_NUM_6, IO_EXPANDER_OUTPUT);
|
||||
ESP_ERROR_CHECK(ret);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
ret = esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_7 | IO_EXPANDER_PIN_NUM_6, 1);
|
||||
ESP_ERROR_CHECK(ret);
|
||||
}
|
||||
|
||||
void InitializeSpi() {
|
||||
ESP_LOGI(TAG, "Initialize QSPI bus");
|
||||
spi_bus_config_t buscfg = {};
|
||||
buscfg.data0_io_num = LCD_D0;
|
||||
buscfg.data1_io_num = LCD_D1;
|
||||
buscfg.data2_io_num = LCD_D2;
|
||||
buscfg.data3_io_num = LCD_D3;
|
||||
buscfg.sclk_io_num = LCD_PCLK;
|
||||
buscfg.max_transfer_sz = LVGL_DMA_BUFF_LEN;
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
|
||||
}
|
||||
|
||||
void InitializeLcdDisplay() {
|
||||
esp_lcd_panel_io_handle_t panel_io = nullptr;
|
||||
esp_lcd_panel_handle_t panel = nullptr;
|
||||
// RESET PIN INIT
|
||||
gpio_config_t gpio_conf = {};
|
||||
gpio_conf.intr_type = GPIO_INTR_DISABLE;
|
||||
gpio_conf.mode = GPIO_MODE_OUTPUT;
|
||||
gpio_conf.pin_bit_mask = ((uint64_t)0x01<<LCD_RST);
|
||||
gpio_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
gpio_conf.pull_up_en = GPIO_PULLUP_ENABLE;
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(gpio_config(&gpio_conf));
|
||||
|
||||
// 液晶屏控制IO初始化
|
||||
ESP_LOGI(TAG, "Install panel IO");
|
||||
esp_lcd_panel_io_spi_config_t io_config = AXS15231B_PANEL_IO_QSPI_CONFIG(
|
||||
LCD_CS,
|
||||
NULL,
|
||||
NULL);
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io));
|
||||
|
||||
// 初始化液晶屏驱动芯片
|
||||
ESP_LOGI(TAG, "Install LCD driver");
|
||||
const axs15231b_vendor_config_t vendor_config = {
|
||||
.init_cmds = lcd_init_cmds, // Uncomment these line if use custom initialization commands
|
||||
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(lcd_init_cmds[0]),
|
||||
.flags = {
|
||||
.use_qspi_interface = 1,
|
||||
},
|
||||
};
|
||||
esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = -1,
|
||||
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
|
||||
.bits_per_pixel = 16,
|
||||
.vendor_config = (void *)&vendor_config,
|
||||
};
|
||||
esp_lcd_new_panel_axs15231b(panel_io, &panel_config, &panel);
|
||||
|
||||
gpio_set_level(LCD_RST,1);
|
||||
vTaskDelay(pdMS_TO_TICKS(30));
|
||||
gpio_set_level(LCD_RST,0);
|
||||
vTaskDelay(pdMS_TO_TICKS(250));
|
||||
gpio_set_level(LCD_RST,1);
|
||||
vTaskDelay(pdMS_TO_TICKS(30));
|
||||
esp_lcd_panel_init(panel);
|
||||
|
||||
display_ = new CustomLcdDisplay(panel_io, panel,
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
}
|
||||
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
pwr_button_.OnLongPress([this]() {
|
||||
if(is_PwrControlEn) {
|
||||
is_PwrControlEn = false;
|
||||
esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_6, 0);
|
||||
}
|
||||
});
|
||||
|
||||
pwr_button_.OnPressUp([this]() {
|
||||
if(!is_PwrControlEn) {
|
||||
is_PwrControlEn = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void InitializeTouch() {
|
||||
i2c_master_bus_handle_t touch_i2c_bus_;
|
||||
// Initialize I2C peripheral
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {};
|
||||
i2c_bus_cfg.i2c_port = (i2c_port_t)I2C_NUM_1;
|
||||
i2c_bus_cfg.sda_io_num = I2C_Touch_SDA_PIN;
|
||||
i2c_bus_cfg.scl_io_num = I2C_Touch_SCL_PIN;
|
||||
i2c_bus_cfg.clk_source = I2C_CLK_SRC_DEFAULT;
|
||||
i2c_bus_cfg.glitch_ignore_cnt = 7;
|
||||
i2c_bus_cfg.intr_priority = 0;
|
||||
i2c_bus_cfg.trans_queue_depth = 0;
|
||||
i2c_bus_cfg.flags.enable_internal_pullup = 1;
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &touch_i2c_bus_));
|
||||
|
||||
i2c_device_config_t dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = I2C_Touch_ADDRESS,
|
||||
.scl_speed_hz = 300000,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(touch_i2c_bus_, &dev_cfg, &disp_touch_dev_handle));
|
||||
|
||||
touch_indev = lv_indev_create();
|
||||
lv_indev_set_type(touch_indev, LV_INDEV_TYPE_POINTER);
|
||||
lv_indev_set_read_cb(touch_indev, TouchInputReadCallback);
|
||||
lv_indev_set_user_data(touch_indev, disp_touch_dev_handle);
|
||||
}
|
||||
|
||||
static void TouchInputReadCallback(lv_indev_t * indev, lv_indev_data_t *indevData) {
|
||||
i2c_master_dev_handle_t i2c_dev = (i2c_master_dev_handle_t)lv_indev_get_user_data(indev);
|
||||
uint8_t read_touchpad_cmd[11] = {0xb5, 0xab, 0xa5, 0x5a, 0x0, 0x0, 0x0, 0x0e,0x0, 0x0, 0x0};
|
||||
uint8_t buff[32] = {0};
|
||||
i2c_master_transmit_receive(i2c_dev,read_touchpad_cmd,11,buff,32,1000);
|
||||
uint16_t pointX;
|
||||
uint16_t pointY;
|
||||
pointX = (((uint16_t)buff[2] & 0x0f) << 8) | (uint16_t)buff[3];
|
||||
pointY = (((uint16_t)buff[4] & 0x0f) << 8) | (uint16_t)buff[5];
|
||||
if (buff[1]>0 && buff[1]<5) {
|
||||
indevData->state = LV_INDEV_STATE_PRESSED;
|
||||
if(pointX > DISPLAY_WIDTH) pointX = DISPLAY_WIDTH;
|
||||
if(pointY > DISPLAY_HEIGHT) pointY = DISPLAY_HEIGHT;
|
||||
indevData->point.x = pointY;
|
||||
indevData->point.y = (DISPLAY_HEIGHT-pointX);
|
||||
ESP_LOGE("Touch","(%ld,%ld)",indevData->point.x,indevData->point.y);
|
||||
} else {
|
||||
indevData->state = LV_INDEV_STATE_RELEASED;
|
||||
}
|
||||
}
|
||||
|
||||
void GetPwrCurrentState() {
|
||||
if(gpio_get_level(PWR_BUTTON_GPIO)) {
|
||||
is_PwrControlEn = true;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
CustomBoard() :
|
||||
boot_button_(BOOT_BUTTON_GPIO),
|
||||
pwr_button_(PWR_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
InitializeTca9554();
|
||||
InitializeSpi();
|
||||
InitializeLcdDisplay();
|
||||
InitializeButtons();
|
||||
InitializeTouch();
|
||||
GetPwrCurrentState();
|
||||
GetBacklight()->RestoreBrightness();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
static BoxAudioCodec audio_codec(
|
||||
i2c_bus_,
|
||||
AUDIO_INPUT_SAMPLE_RATE,
|
||||
AUDIO_OUTPUT_SAMPLE_RATE,
|
||||
AUDIO_I2S_GPIO_MCLK,
|
||||
AUDIO_I2S_GPIO_BCLK,
|
||||
AUDIO_I2S_GPIO_WS,
|
||||
AUDIO_I2S_GPIO_DOUT,
|
||||
AUDIO_I2S_GPIO_DIN,
|
||||
AUDIO_CODEC_PA_PIN,
|
||||
AUDIO_CODEC_ES8311_ADDR,
|
||||
AUDIO_CODEC_ES7210_ADDR,
|
||||
AUDIO_INPUT_REFERENCE);
|
||||
return &audio_codec;
|
||||
}
|
||||
|
||||
virtual Display* GetDisplay() override {
|
||||
return display_;
|
||||
}
|
||||
|
||||
virtual Backlight* GetBacklight() override {
|
||||
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
return &backlight;
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(CustomBoard);
|
||||
12
main/boards/waveshare-s3-touch-lcd-4b/README.md
Normal file
12
main/boards/waveshare-s3-touch-lcd-4b/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Waveshare ESP32-S3-Touch-LCD-4B
|
||||
|
||||
|
||||
[ESP32-S3-Touch-LCD-4B](https://www.waveshare.com/esp32-s3-touch-lcd-4b.htm) is waveshare electronics designed an intelligent 86 box based on ESP32-S3 module equipped with a 480*480 IPS capacitive touch screen
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration in `menuconfig`.
|
||||
|
||||
Selection Board Type `Xiaozhi Assistant --> Board Type`
|
||||
- Waveshare ESP32-S3-Touch-LCD-4B
|
||||
65
main/boards/waveshare-s3-touch-lcd-4b/config.h
Normal file
65
main/boards/waveshare-s3-touch-lcd-4b/config.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#ifndef _BOARD_CONFIG_H_
|
||||
#define _BOARD_CONFIG_H_
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#define AUDIO_INPUT_SAMPLE_RATE 24000
|
||||
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
|
||||
|
||||
#define AUDIO_INPUT_REFERENCE true
|
||||
|
||||
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_5
|
||||
#define AUDIO_I2S_GPIO_WS GPIO_NUM_7
|
||||
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_16
|
||||
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_15
|
||||
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_6
|
||||
|
||||
#define AUDIO_CODEC_PA_PIN GPIO_NUM_NC
|
||||
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_47
|
||||
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_48
|
||||
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
|
||||
#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR
|
||||
|
||||
#define I2C_ADDRESS ESP_IO_EXPANDER_I2C_TCA9554_ADDRESS_000
|
||||
#define BOOT_BUTTON_GPIO GPIO_NUM_0
|
||||
|
||||
#define BSP_LCD_VSYNC (GPIO_NUM_3)
|
||||
#define BSP_LCD_HSYNC (GPIO_NUM_46)
|
||||
#define BSP_LCD_DE (GPIO_NUM_17)
|
||||
#define BSP_LCD_PCLK (GPIO_NUM_9)
|
||||
#define BSP_LCD_DISP (GPIO_NUM_NC)
|
||||
#define BSP_LCD_DATA0 (GPIO_NUM_40)
|
||||
#define BSP_LCD_DATA1 (GPIO_NUM_41)
|
||||
#define BSP_LCD_DATA2 (GPIO_NUM_42)
|
||||
#define BSP_LCD_DATA3 (GPIO_NUM_2)
|
||||
#define BSP_LCD_DATA4 (GPIO_NUM_1)
|
||||
#define BSP_LCD_DATA5 (GPIO_NUM_21)
|
||||
#define BSP_LCD_DATA6 (GPIO_NUM_8)
|
||||
#define BSP_LCD_DATA7 (GPIO_NUM_18)
|
||||
#define BSP_LCD_DATA8 (GPIO_NUM_45)
|
||||
#define BSP_LCD_DATA9 (GPIO_NUM_38)
|
||||
#define BSP_LCD_DATA10 (GPIO_NUM_39)
|
||||
#define BSP_LCD_DATA11 (GPIO_NUM_10)
|
||||
#define BSP_LCD_DATA12 (GPIO_NUM_11)
|
||||
#define BSP_LCD_DATA13 (GPIO_NUM_12)
|
||||
#define BSP_LCD_DATA14 (GPIO_NUM_13)
|
||||
#define BSP_LCD_DATA15 (GPIO_NUM_14)
|
||||
|
||||
#define BSP_LCD_IO_SPI_CS (IO_EXPANDER_PIN_NUM_0)
|
||||
#define BSP_LCD_IO_SPI_SCL (IO_EXPANDER_PIN_NUM_2)
|
||||
#define BSP_LCD_IO_SPI_SDA (IO_EXPANDER_PIN_NUM_1)
|
||||
|
||||
#define DISPLAY_WIDTH 480
|
||||
#define DISPLAY_HEIGHT 480
|
||||
|
||||
#define DISPLAY_SWAP_XY false
|
||||
#define DISPLAY_MIRROR_X false
|
||||
#define DISPLAY_MIRROR_Y false
|
||||
|
||||
#define DISPLAY_OFFSET_X 0
|
||||
#define DISPLAY_OFFSET_Y 0
|
||||
|
||||
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_4
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
|
||||
|
||||
#endif // _BOARD_CONFIG_H_
|
||||
12
main/boards/waveshare-s3-touch-lcd-4b/config.json
Normal file
12
main/boards/waveshare-s3-touch-lcd-4b/config.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "waveshare-s3-touch-lcd-4b",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_USE_WECHAT_MESSAGE_STYLE=n",
|
||||
"CONFIG_USE_DEVICE_AEC=y"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
432
main/boards/waveshare-s3-touch-lcd-4b/esp32-s3-touch-lcd-4b.cc
Normal file
432
main/boards/waveshare-s3-touch-lcd-4b/esp32-s3-touch-lcd-4b.cc
Normal file
@@ -0,0 +1,432 @@
|
||||
#include "wifi_board.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "esp_lcd_st7701.h"
|
||||
|
||||
#include "codecs/box_audio_codec.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "led/single_led.h"
|
||||
#include "mcp_server.h"
|
||||
#include "config.h"
|
||||
#include "power_save_timer.h"
|
||||
#include "axp2101.h"
|
||||
#include "i2c_device.h"
|
||||
#include <wifi_station.h>
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_vendor.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_master.h>
|
||||
#include "esp_io_expander_tca9554.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <esp_lcd_touch_gt911.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lcd_panel_io_additions.h>
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#define TAG "WaveshareEsp32s3TouchLCD4b"
|
||||
|
||||
class Pmic : public Axp2101 {
|
||||
public:
|
||||
Pmic(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : Axp2101(i2c_bus, addr) {
|
||||
WriteReg(0x22, 0b110); // PWRON > OFFLEVEL as POWEROFF Source enable
|
||||
WriteReg(0x27, 0x10); // hold 4s to power off
|
||||
|
||||
// Disable All DCs but DC1
|
||||
WriteReg(0x80, 0x01);
|
||||
// Disable All LDOs
|
||||
WriteReg(0x90, 0x00);
|
||||
WriteReg(0x91, 0x00);
|
||||
|
||||
// Set DC1 to 3.3V
|
||||
WriteReg(0x82, (3300 - 1500) / 100);
|
||||
|
||||
// Set ALDO1 to 3.3V
|
||||
WriteReg(0x92, (3300 - 500) / 100);
|
||||
|
||||
// Enable ALDO1(MIC)
|
||||
WriteReg(0x90, 0x01);
|
||||
|
||||
WriteReg(0x64, 0x02); // CV charger voltage setting to 4.1V
|
||||
|
||||
WriteReg(0x61, 0x02); // set Main battery precharge current to 50mA
|
||||
WriteReg(0x62, 0x08); // set Main battery charger current to 400mA ( 0x08-200mA, 0x09-300mA, 0x0A-400mA )
|
||||
WriteReg(0x63, 0x01); // set Main battery term charge current to 25mA
|
||||
}
|
||||
};
|
||||
|
||||
#define LCD_OPCODE_WRITE_CMD (0x02ULL)
|
||||
#define LCD_OPCODE_READ_CMD (0x03ULL)
|
||||
#define LCD_OPCODE_WRITE_COLOR (0x32ULL)
|
||||
|
||||
static const st7701_lcd_init_cmd_t lcd_init_cmds[] = {
|
||||
// {cmd, { data }, data_size, delay_ms}
|
||||
{0x11, (uint8_t[]){0x00}, 0, 120},
|
||||
{0xFF, (uint8_t[]){0x77, 0x01, 0x00, 0x00, 0x10}, 5, 0},
|
||||
{0xC0, (uint8_t[]){0x3B, 0x00}, 2, 0},
|
||||
{0xC1, (uint8_t[]){0x0D, 0x02}, 2, 0},
|
||||
{0xC2, (uint8_t[]){0x21, 0x08}, 2, 0},
|
||||
{0xCD, (uint8_t[]){0x08}, 1, 0},
|
||||
{0xB0, (uint8_t[]){0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18}, 16, 0},
|
||||
{0xB1, (uint8_t[]){0x00, 0x11, 0x19, 0x0E, 0x12, 0x07, 0x08, 0x08, 0x08, 0x22, 0x04, 0x11, 0x11, 0xA9, 0x32, 0x18}, 16, 0},
|
||||
{0xFF, (uint8_t[]){0x77, 0x01, 0x00, 0x00, 0x11}, 5, 0},
|
||||
{0xB0, (uint8_t[]){0x60}, 1, 0},
|
||||
{0xB1, (uint8_t[]){0x30}, 1, 0},
|
||||
{0xB2, (uint8_t[]){0x87}, 1, 0},
|
||||
{0xB3, (uint8_t[]){0x80}, 1, 0},
|
||||
{0xB5, (uint8_t[]){0x49}, 1, 0},
|
||||
{0xB7, (uint8_t[]){0x85}, 1, 0},
|
||||
{0xB8, (uint8_t[]){0x21}, 1, 0},
|
||||
{0xC1, (uint8_t[]){0x78}, 1, 0},
|
||||
{0xC2, (uint8_t[]){0x78}, 1, 20},
|
||||
{0xE0, (uint8_t[]){0x00, 0x1B, 0x02}, 3, 0},
|
||||
{0xE1, (uint8_t[]){0x08, 0xA0, 0x00, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x44, 0x44}, 11, 0},
|
||||
{0xE2, (uint8_t[]){0x11, 0x11, 0x44, 0x44, 0xED, 0xA0, 0x00, 0x00, 0xEC, 0xA0, 0x00, 0x00}, 12, 0},
|
||||
{0xE3, (uint8_t[]){0x00, 0x00, 0x11, 0x11}, 4, 0},
|
||||
{0xE4, (uint8_t[]){0x44, 0x44}, 2, 0},
|
||||
{0xE5, (uint8_t[]){0x0A, 0xE9, 0xD8, 0xA0, 0x0C, 0xEB, 0xD8, 0xA0, 0x0E, 0xED, 0xD8, 0xA0, 0x10, 0xEF, 0xD8, 0xA0}, 16, 0},
|
||||
{0xE6, (uint8_t[]){0x00, 0x00, 0x11, 0x11}, 4, 0},
|
||||
{0xE7, (uint8_t[]){0x44, 0x44}, 2, 0},
|
||||
{0xE8, (uint8_t[]){0x09, 0xE8, 0xD8, 0xA0, 0x0B, 0xEA, 0xD8, 0xA0, 0x0D, 0xEC, 0xD8, 0xA0, 0x0F, 0xEE, 0xD8, 0xA0}, 16, 0},
|
||||
{0xEB, (uint8_t[]){0x02, 0x00, 0xE4, 0xE4, 0x88, 0x00, 0x40}, 7, 0},
|
||||
{0xEC, (uint8_t[]){0x3C, 0x00}, 2, 0},
|
||||
{0xED, (uint8_t[]){0xAB, 0x89, 0x76, 0x54, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x45, 0x67, 0x98, 0xBA}, 16, 0},
|
||||
{0xFF, (uint8_t[]){0x77, 0x01, 0x00, 0x00, 0x00}, 5, 0},
|
||||
{0x36, (uint8_t[]){0x00}, 1, 0},
|
||||
{0x3A, (uint8_t[]){0x66}, 1, 0},
|
||||
{0x21, (uint8_t[]){0x00}, 0, 120},
|
||||
{0x29, (uint8_t[]){0x00}, 0, 0},
|
||||
};
|
||||
|
||||
class WaveshareEsp32s3TouchLCD4b : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t i2c_bus_;
|
||||
Pmic* pmic_ = nullptr;
|
||||
Button boot_button_;
|
||||
LcdDisplay* display_;
|
||||
esp_io_expander_handle_t io_expander = NULL;
|
||||
PowerSaveTimer* power_save_timer_;
|
||||
|
||||
uint32_t key_press_start;
|
||||
bool key_pressed;
|
||||
bool key_handled;
|
||||
|
||||
void InitializePowerSaveTimer() {
|
||||
power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
|
||||
power_save_timer_->OnEnterSleepMode([this]() {
|
||||
GetDisplay()->SetPowerSaveMode(true);
|
||||
GetBacklight()->SetBrightness(70); });
|
||||
power_save_timer_->OnExitSleepMode([this]() {
|
||||
GetDisplay()->SetPowerSaveMode(false);
|
||||
GetBacklight()->RestoreBrightness(); });
|
||||
power_save_timer_->OnShutdownRequest([this](){
|
||||
pmic_->PowerOff(); });
|
||||
power_save_timer_->SetEnabled(true);
|
||||
}
|
||||
|
||||
void InitializeCodecI2c() {
|
||||
// Initialize I2C peripheral
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
|
||||
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.trans_queue_depth = 0,
|
||||
.flags = {
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
|
||||
}
|
||||
|
||||
void InitializeTca9554(void) {
|
||||
esp_io_expander_new_i2c_tca9554(i2c_bus_, I2C_ADDRESS, &io_expander);
|
||||
esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_3|IO_EXPANDER_PIN_NUM_5 | IO_EXPANDER_PIN_NUM_6 , IO_EXPANDER_OUTPUT);
|
||||
esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_3, 1);
|
||||
esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_6, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_5, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
esp_io_expander_set_level(io_expander, IO_EXPANDER_PIN_NUM_5, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
esp_io_expander_set_dir(io_expander, IO_EXPANDER_PIN_NUM_4|IO_EXPANDER_PIN_NUM_6, IO_EXPANDER_INPUT);
|
||||
}
|
||||
void InitializeAxp2101() {
|
||||
ESP_LOGI(TAG, "Init AXP2101");
|
||||
pmic_ = new Pmic(i2c_bus_, 0x34);
|
||||
}
|
||||
|
||||
void InitializeRGB() {
|
||||
esp_lcd_panel_io_handle_t panel_io = nullptr;
|
||||
|
||||
spi_line_config_t line_config = {
|
||||
.cs_io_type = IO_TYPE_EXPANDER,
|
||||
.cs_expander_pin = BSP_LCD_IO_SPI_CS,
|
||||
.scl_io_type = IO_TYPE_EXPANDER,
|
||||
.scl_expander_pin = BSP_LCD_IO_SPI_SCL,
|
||||
.sda_io_type = IO_TYPE_EXPANDER,
|
||||
.sda_expander_pin = BSP_LCD_IO_SPI_SDA,
|
||||
.io_expander = io_expander,
|
||||
};
|
||||
esp_lcd_panel_io_3wire_spi_config_t io_config = ST7701_PANEL_IO_3WIRE_SPI_CONFIG(line_config, 0);
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_3wire_spi(&io_config, &panel_io));
|
||||
esp_lcd_panel_handle_t panel_handle = NULL;
|
||||
esp_lcd_rgb_panel_config_t rgb_config = {
|
||||
.clk_src = LCD_CLK_SRC_DEFAULT,
|
||||
.timings = {
|
||||
.pclk_hz = 16 * 1000 * 1000,
|
||||
.h_res = DISPLAY_WIDTH,
|
||||
.v_res = DISPLAY_HEIGHT,
|
||||
.hsync_pulse_width = 10,
|
||||
.hsync_back_porch = 10,
|
||||
.hsync_front_porch = 20,
|
||||
.vsync_pulse_width = 10,
|
||||
.vsync_back_porch = 10,
|
||||
.vsync_front_porch = 10,
|
||||
.flags = {
|
||||
.pclk_active_neg = false
|
||||
}
|
||||
},
|
||||
.data_width = 16,
|
||||
.bits_per_pixel = 16,
|
||||
.num_fbs = 2,
|
||||
.bounce_buffer_size_px = 480 * 20,
|
||||
.psram_trans_align = 64,
|
||||
.hsync_gpio_num = BSP_LCD_HSYNC,
|
||||
.vsync_gpio_num = BSP_LCD_VSYNC,
|
||||
.de_gpio_num = BSP_LCD_DE,
|
||||
.pclk_gpio_num = BSP_LCD_PCLK,
|
||||
.disp_gpio_num = BSP_LCD_DISP,
|
||||
.data_gpio_nums = {
|
||||
BSP_LCD_DATA0, BSP_LCD_DATA1, BSP_LCD_DATA2, BSP_LCD_DATA3,
|
||||
BSP_LCD_DATA4, BSP_LCD_DATA5, BSP_LCD_DATA6, BSP_LCD_DATA7,
|
||||
BSP_LCD_DATA8, BSP_LCD_DATA9, BSP_LCD_DATA10, BSP_LCD_DATA11,
|
||||
BSP_LCD_DATA12, BSP_LCD_DATA13, BSP_LCD_DATA14, BSP_LCD_DATA15
|
||||
},
|
||||
.flags = {
|
||||
.fb_in_psram = 1,
|
||||
},
|
||||
};
|
||||
|
||||
rgb_config.timings.h_res = DISPLAY_WIDTH;
|
||||
rgb_config.timings.v_res = DISPLAY_HEIGHT;
|
||||
st7701_vendor_config_t vendor_config = {
|
||||
.init_cmds = lcd_init_cmds,
|
||||
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(lcd_init_cmds[0]),
|
||||
.rgb_config = &rgb_config,
|
||||
.flags = {
|
||||
.mirror_by_cmd = 0,
|
||||
.auto_del_panel_io = 1,
|
||||
}
|
||||
};
|
||||
const esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = GPIO_NUM_NC,
|
||||
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
|
||||
.bits_per_pixel = 18,
|
||||
.vendor_config = &vendor_config,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_st7701(panel_io, &panel_config, &panel_handle));
|
||||
esp_lcd_panel_init(panel_handle);
|
||||
|
||||
display_ = new RgbLcdDisplay(panel_io, panel_handle,
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X,
|
||||
DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
}
|
||||
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
|
||||
#if CONFIG_USE_DEVICE_AEC
|
||||
boot_button_.OnDoubleClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateIdle) {
|
||||
app.SetAecMode(app.GetAecMode() == kAecOff ? kAecOnDeviceSide : kAecOff);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void InitializeTouch() {
|
||||
esp_lcd_touch_handle_t tp;
|
||||
esp_lcd_touch_config_t tp_cfg = {
|
||||
.x_max = DISPLAY_WIDTH - 1,
|
||||
.y_max = DISPLAY_HEIGHT - 1,
|
||||
.rst_gpio_num = GPIO_NUM_NC,
|
||||
.int_gpio_num = GPIO_NUM_NC,
|
||||
.levels = {
|
||||
.reset = 0,
|
||||
.interrupt = 0,
|
||||
},
|
||||
.flags = {
|
||||
.swap_xy = 0,
|
||||
.mirror_x = 0,
|
||||
.mirror_y = 0,
|
||||
},
|
||||
};
|
||||
esp_lcd_panel_io_handle_t tp_io_handle = NULL;
|
||||
esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
|
||||
tp_io_config.scl_speed_hz = 400* 1000;
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_, &tp_io_config, &tp_io_handle));
|
||||
ESP_LOGI(TAG, "Initialize touch controller");
|
||||
ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, &tp));
|
||||
const lvgl_port_touch_cfg_t touch_cfg = {
|
||||
.disp = lv_display_get_default(),
|
||||
.handle = tp,
|
||||
};
|
||||
lvgl_port_add_touch(&touch_cfg);
|
||||
ESP_LOGI(TAG, "Touch panel initialized successfully");
|
||||
}
|
||||
|
||||
void InitializeTools() {
|
||||
auto &mcp_server = McpServer::GetInstance();
|
||||
mcp_server.AddTool("self.system.reconfigure_wifi",
|
||||
"Reboot the device and enter WiFi configuration mode.\n"
|
||||
"**CAUTION** You must ask the user to confirm this action.",
|
||||
PropertyList(), [this](const PropertyList& properties) {
|
||||
ResetWifiConfiguration();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
void CheckKeyState() {
|
||||
if (!io_expander) return;
|
||||
|
||||
uint32_t current_level;
|
||||
esp_err_t ret = esp_io_expander_get_level(io_expander, IO_EXPANDER_PIN_NUM_4, ¤t_level);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read IO_EXPANDER_PIN_NUM_4 level");
|
||||
return;
|
||||
}
|
||||
|
||||
static uint32_t last_level = 0;
|
||||
static uint64_t press_start_time_ms = 0;
|
||||
|
||||
if (current_level != last_level) {
|
||||
last_level = current_level;
|
||||
|
||||
if (current_level > 0) {
|
||||
press_start_time_ms = esp_timer_get_time() / 1000;
|
||||
ESP_LOGD(TAG, "Button pressed, start time recorded");
|
||||
} else {
|
||||
uint64_t press_duration = (esp_timer_get_time() / 1000) - press_start_time_ms;
|
||||
ESP_LOGI(TAG, "Button released after %llums", press_duration);
|
||||
|
||||
if (press_duration < 1000) {
|
||||
ESP_LOGI(TAG, "Short press detected, switching to factory partition");
|
||||
|
||||
const esp_partition_t* factory_partition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_APP,
|
||||
ESP_PARTITION_SUBTYPE_APP_FACTORY,
|
||||
nullptr
|
||||
);
|
||||
if (factory_partition) {
|
||||
ESP_LOGI(TAG, "Found factory partition: %s", factory_partition->label);
|
||||
ESP_ERROR_CHECK(esp_ota_set_boot_partition(factory_partition));
|
||||
esp_restart();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Factory partition not found");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Long press detected (>1000ms), no action");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeKeyMonitor() {
|
||||
key_press_start = 0;
|
||||
key_pressed = false;
|
||||
key_handled = false;
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
[](void* arg) {
|
||||
auto* board = static_cast<WaveshareEsp32s3TouchLCD4b*>(arg);
|
||||
while (true) {
|
||||
board->CheckKeyState();
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
}
|
||||
},
|
||||
"key_monitor_task",
|
||||
4096,
|
||||
this,
|
||||
5,
|
||||
nullptr,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
public:
|
||||
WaveshareEsp32s3TouchLCD4b() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializePowerSaveTimer();
|
||||
InitializeCodecI2c();
|
||||
InitializeTca9554();
|
||||
InitializeAxp2101();
|
||||
InitializeRGB();
|
||||
InitializeTouch();
|
||||
InitializeButtons();
|
||||
InitializeTools();
|
||||
InitializeKeyMonitor(); // 启动按键监听
|
||||
GetBacklight()->SetBrightness(100);
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
static BoxAudioCodec audio_codec(
|
||||
i2c_bus_,
|
||||
AUDIO_INPUT_SAMPLE_RATE,
|
||||
AUDIO_OUTPUT_SAMPLE_RATE,
|
||||
AUDIO_I2S_GPIO_MCLK,
|
||||
AUDIO_I2S_GPIO_BCLK,
|
||||
AUDIO_I2S_GPIO_WS,
|
||||
AUDIO_I2S_GPIO_DOUT,
|
||||
AUDIO_I2S_GPIO_DIN,
|
||||
AUDIO_CODEC_PA_PIN,
|
||||
AUDIO_CODEC_ES8311_ADDR,
|
||||
AUDIO_CODEC_ES7210_ADDR,
|
||||
AUDIO_INPUT_REFERENCE);
|
||||
return &audio_codec;
|
||||
}
|
||||
|
||||
virtual Display* GetDisplay() override {
|
||||
return display_;
|
||||
}
|
||||
|
||||
virtual Backlight* GetBacklight() override {
|
||||
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
return &backlight;
|
||||
}
|
||||
|
||||
virtual bool GetBatteryLevel(int &level, bool &charging, bool &discharging) override {
|
||||
static bool last_discharging = false;
|
||||
charging = pmic_->IsCharging();
|
||||
discharging = pmic_->IsDischarging();
|
||||
if (discharging != last_discharging)
|
||||
{
|
||||
power_save_timer_->SetEnabled(discharging);
|
||||
last_discharging = discharging;
|
||||
}
|
||||
|
||||
level = pmic_->GetBatteryLevel();
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void SetPowerSaveMode(bool enabled) override {
|
||||
if (!enabled)
|
||||
{
|
||||
power_save_timer_->WakeUp();
|
||||
}
|
||||
WifiBoard::SetPowerSaveMode(enabled);
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(WaveshareEsp32s3TouchLCD4b);
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
static QueueHandle_t gpio_evt_queue = NULL;
|
||||
uint16_t battCnt;//闪灯次数
|
||||
int battLife = -1; //电量
|
||||
int battLife = 70; //电量
|
||||
|
||||
// 中断服务程序
|
||||
static void IRAM_ATTR batt_mon_isr_handler(void* arg) {
|
||||
|
||||
@@ -145,7 +145,6 @@ public:
|
||||
power_manager_->CheckStartup();
|
||||
InitializePowerSaveTimer();
|
||||
InitializeSpi();
|
||||
InitializeButtons();
|
||||
InitializeSt7789Display();
|
||||
power_manager_->OnChargingStatusDisChanged([this](bool is_discharging) {
|
||||
if(power_save_timer_){
|
||||
@@ -158,8 +157,14 @@ public:
|
||||
});
|
||||
if(GetNetworkType() == NetworkType::WIFI){
|
||||
power_manager_->Shutdown4G();
|
||||
}else{
|
||||
power_manager_->Start4G();
|
||||
}
|
||||
GetBacklight()->RestoreBrightness();
|
||||
while(gpio_get_level(BOOT_BUTTON_PIN) == 0){
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
InitializeButtons();
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "emoji_collection.h"
|
||||
|
||||
#ifdef LVGL_VERSION_MAJOR
|
||||
#ifndef CONFIG_USE_EMOTE_MESSAGE_STYLE
|
||||
#define HAVE_LVGL 1
|
||||
#include <lvgl.h>
|
||||
#endif
|
||||
|
||||
655
main/display/emote_display.cc
Normal file
655
main/display/emote_display.cc
Normal file
@@ -0,0 +1,655 @@
|
||||
#include "emote_display.h"
|
||||
|
||||
// Standard C++ headers
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <tuple>
|
||||
|
||||
// Standard C headers
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
// ESP-IDF headers
|
||||
#include <esp_log.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_timer.h>
|
||||
|
||||
// FreeRTOS headers
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
// Project headers
|
||||
#include "assets.h"
|
||||
#include "assets/lang_config.h"
|
||||
#include "board.h"
|
||||
#include "gfx.h"
|
||||
|
||||
LV_FONT_DECLARE(BUILTIN_TEXT_FONT);
|
||||
|
||||
namespace emote {
|
||||
|
||||
// ============================================================================
|
||||
// Constants and Type Definitions
|
||||
// ============================================================================
|
||||
|
||||
static const char* TAG = "EmoteDisplay";
|
||||
|
||||
// UI Element Names - Centralized Management
|
||||
#define UI_ELEMENT_EYE_ANIM "eye_anim"
|
||||
#define UI_ELEMENT_TOAST_LABEL "toast_label"
|
||||
#define UI_ELEMENT_CLOCK_LABEL "clock_label"
|
||||
#define UI_ELEMENT_LISTEN_ANIM "listen_anim"
|
||||
#define UI_ELEMENT_STATUS_ICON "status_icon"
|
||||
|
||||
// Icon Names - Centralized Management
|
||||
#define ICON_MIC "icon_mic"
|
||||
#define ICON_BATTERY "icon_Battery"
|
||||
#define ICON_SPEAKER_ZZZ "icon_speaker_zzz"
|
||||
#define ICON_WIFI_FAILED "icon_WiFi_failed"
|
||||
#define ICON_WIFI_OK "icon_wifi"
|
||||
#define ICON_LISTEN "listen"
|
||||
|
||||
using FlushIoReadyCallback = std::function<bool(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void*)>;
|
||||
using FlushCallback = std::function<void(gfx_handle_t, int, int, int, int, const void*)>;
|
||||
|
||||
// ============================================================================
|
||||
// Global Variables
|
||||
// ============================================================================
|
||||
|
||||
// UI element management
|
||||
static gfx_obj_t* g_obj_label_toast = nullptr;
|
||||
static gfx_obj_t* g_obj_label_clock = nullptr;
|
||||
static gfx_obj_t* g_obj_anim_eye = nullptr;
|
||||
static gfx_obj_t* g_obj_anim_listen = nullptr;
|
||||
static gfx_obj_t* g_obj_img_status = nullptr;
|
||||
|
||||
// Track current icon to determine when to show time
|
||||
static std::string g_current_icon_type = ICON_WIFI_FAILED;
|
||||
static gfx_image_dsc_t g_icon_img_dsc;
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Forward Declarations
|
||||
// ============================================================================
|
||||
|
||||
class EmoteDisplay;
|
||||
class EmoteEngine;
|
||||
|
||||
enum class UIDisplayMode : uint8_t {
|
||||
SHOW_LISTENING = 1, // Show g_obj_anim_listen
|
||||
SHOW_TIME = 2, // Show g_obj_label_clock
|
||||
SHOW_TIPS = 3 // Show g_obj_label_toast
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
// Function to convert align string to GFX_ALIGN enum value
|
||||
char StringToGfxAlign(const std::string &align_str)
|
||||
{
|
||||
static const std::unordered_map<std::string, char> align_map = {
|
||||
{"GFX_ALIGN_DEFAULT", GFX_ALIGN_DEFAULT},
|
||||
{"GFX_ALIGN_TOP_LEFT", GFX_ALIGN_TOP_LEFT},
|
||||
{"GFX_ALIGN_TOP_MID", GFX_ALIGN_TOP_MID},
|
||||
{"GFX_ALIGN_TOP_RIGHT", GFX_ALIGN_TOP_RIGHT},
|
||||
{"GFX_ALIGN_LEFT_MID", GFX_ALIGN_LEFT_MID},
|
||||
{"GFX_ALIGN_CENTER", GFX_ALIGN_CENTER},
|
||||
{"GFX_ALIGN_RIGHT_MID", GFX_ALIGN_RIGHT_MID},
|
||||
{"GFX_ALIGN_BOTTOM_LEFT", GFX_ALIGN_BOTTOM_LEFT},
|
||||
{"GFX_ALIGN_BOTTOM_MID", GFX_ALIGN_BOTTOM_MID},
|
||||
{"GFX_ALIGN_BOTTOM_RIGHT", GFX_ALIGN_BOTTOM_RIGHT},
|
||||
{"GFX_ALIGN_OUT_TOP_LEFT", GFX_ALIGN_OUT_TOP_LEFT},
|
||||
{"GFX_ALIGN_OUT_TOP_MID", GFX_ALIGN_OUT_TOP_MID},
|
||||
{"GFX_ALIGN_OUT_TOP_RIGHT", GFX_ALIGN_OUT_TOP_RIGHT},
|
||||
{"GFX_ALIGN_OUT_LEFT_TOP", GFX_ALIGN_OUT_LEFT_TOP},
|
||||
{"GFX_ALIGN_OUT_LEFT_MID", GFX_ALIGN_OUT_LEFT_MID},
|
||||
{"GFX_ALIGN_OUT_LEFT_BOTTOM", GFX_ALIGN_OUT_LEFT_BOTTOM},
|
||||
{"GFX_ALIGN_OUT_RIGHT_TOP", GFX_ALIGN_OUT_RIGHT_TOP},
|
||||
{"GFX_ALIGN_OUT_RIGHT_MID", GFX_ALIGN_OUT_RIGHT_MID},
|
||||
{"GFX_ALIGN_OUT_RIGHT_BOTTOM", GFX_ALIGN_OUT_RIGHT_BOTTOM},
|
||||
{"GFX_ALIGN_OUT_BOTTOM_LEFT", GFX_ALIGN_OUT_BOTTOM_LEFT},
|
||||
{"GFX_ALIGN_OUT_BOTTOM_MID", GFX_ALIGN_OUT_BOTTOM_MID},
|
||||
{"GFX_ALIGN_OUT_BOTTOM_RIGHT", GFX_ALIGN_OUT_BOTTOM_RIGHT}
|
||||
};
|
||||
|
||||
const auto it = align_map.find(align_str);
|
||||
if (it != align_map.cend()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Unknown align string: %s, using GFX_ALIGN_DEFAULT", align_str.c_str());
|
||||
return GFX_ALIGN_DEFAULT;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EmoteEngine Class Declaration
|
||||
// ============================================================================
|
||||
|
||||
class EmoteEngine {
|
||||
public:
|
||||
EmoteEngine(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io,
|
||||
const int width, const int height, EmoteDisplay* const display);
|
||||
~EmoteEngine();
|
||||
|
||||
void SetEyes(const std::string &emoji_name, const bool repeat, const int fps, EmoteDisplay* const display);
|
||||
void SetIcon(const std::string &icon_name, EmoteDisplay* const display);
|
||||
|
||||
void* GetEngineHandle() const
|
||||
{
|
||||
return engine_handle_;
|
||||
}
|
||||
|
||||
// Callback functions (public to be accessible from static helper functions)
|
||||
static bool OnFlushIoReady(const esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t* const edata, void* const user_ctx);
|
||||
static void OnFlush(const gfx_handle_t handle, const int x_start, const int y_start, const int x_end, const int y_end, const void* const color_data);
|
||||
|
||||
private:
|
||||
gfx_handle_t engine_handle_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// UI Management Functions
|
||||
// ============================================================================
|
||||
|
||||
static void SetUIDisplayMode(const UIDisplayMode mode, EmoteDisplay* const display)
|
||||
{
|
||||
if (!display) {
|
||||
ESP_LOGE(TAG, "SetUIDisplayMode: display is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_obj_set_visible(g_obj_anim_listen, false);
|
||||
gfx_obj_set_visible(g_obj_label_clock, false);
|
||||
gfx_obj_set_visible(g_obj_label_toast, false);
|
||||
|
||||
// Show the selected control
|
||||
switch (mode) {
|
||||
case UIDisplayMode::SHOW_LISTENING: {
|
||||
gfx_obj_set_visible(g_obj_anim_listen, true);
|
||||
const AssetData emoji_data = display->GetIconData(ICON_LISTEN);
|
||||
if (emoji_data.data) {
|
||||
gfx_anim_set_src(g_obj_anim_listen, emoji_data.data, emoji_data.size);
|
||||
gfx_anim_set_segment(g_obj_anim_listen, 0, 0xFFFF, 20, true);
|
||||
gfx_anim_start(g_obj_anim_listen);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UIDisplayMode::SHOW_TIME:
|
||||
gfx_obj_set_visible(g_obj_label_clock, true);
|
||||
break;
|
||||
case UIDisplayMode::SHOW_TIPS:
|
||||
gfx_obj_set_visible(g_obj_label_toast, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Graphics Initialization Functions
|
||||
// ============================================================================
|
||||
|
||||
static void InitializeGraphics(const esp_lcd_panel_handle_t panel, gfx_handle_t* const engine_handle,
|
||||
const int width, const int height)
|
||||
{
|
||||
if (!panel || !engine_handle) {
|
||||
ESP_LOGE(TAG, "InitializeGraphics: Invalid parameters");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_core_config_t gfx_cfg = {
|
||||
.flush_cb = EmoteEngine::OnFlush,
|
||||
.user_data = panel,
|
||||
.flags = {
|
||||
.swap = true,
|
||||
.double_buffer = true,
|
||||
.buff_dma = true,
|
||||
},
|
||||
.h_res = static_cast<uint32_t>(width),
|
||||
.v_res = static_cast<uint32_t>(height),
|
||||
.fps = 30,
|
||||
.buffers = {
|
||||
.buf1 = nullptr,
|
||||
.buf2 = nullptr,
|
||||
.buf_pixels = static_cast<size_t>(width * 16),
|
||||
},
|
||||
.task = GFX_EMOTE_INIT_CONFIG()
|
||||
};
|
||||
|
||||
gfx_cfg.task.task_stack_caps = MALLOC_CAP_DEFAULT;
|
||||
gfx_cfg.task.task_affinity = 0;
|
||||
gfx_cfg.task.task_priority = 5;
|
||||
gfx_cfg.task.task_stack = 8 * 1024;
|
||||
|
||||
*engine_handle = gfx_emote_init(&gfx_cfg);
|
||||
}
|
||||
|
||||
static void SetupUI(const gfx_handle_t engine_handle, EmoteDisplay* const display)
|
||||
{
|
||||
if (!display) {
|
||||
ESP_LOGE(TAG, "SetupUI: display is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
gfx_emote_set_bg_color(engine_handle, GFX_COLOR_HEX(0x000000));
|
||||
|
||||
g_obj_anim_eye = gfx_anim_create(engine_handle);
|
||||
gfx_obj_align(g_obj_anim_eye, GFX_ALIGN_LEFT_MID, 10, 30);
|
||||
gfx_anim_set_auto_mirror(g_obj_anim_eye, true);
|
||||
gfx_obj_set_visible(g_obj_anim_eye, false);
|
||||
|
||||
g_obj_label_toast = gfx_label_create(engine_handle);
|
||||
gfx_obj_align(g_obj_label_toast, GFX_ALIGN_TOP_MID, 0, 20);
|
||||
gfx_obj_set_size(g_obj_label_toast, 200, 40);
|
||||
gfx_label_set_text(g_obj_label_toast, Lang::Strings::INITIALIZING);
|
||||
gfx_label_set_color(g_obj_label_toast, GFX_COLOR_HEX(0xFFFFFF));
|
||||
gfx_label_set_text_align(g_obj_label_toast, GFX_TEXT_ALIGN_CENTER);
|
||||
gfx_label_set_long_mode(g_obj_label_toast, GFX_LABEL_LONG_SCROLL);
|
||||
gfx_label_set_scroll_speed(g_obj_label_toast, 20);
|
||||
gfx_label_set_scroll_loop(g_obj_label_toast, true);
|
||||
gfx_label_set_font(g_obj_label_toast, (gfx_font_t)&BUILTIN_TEXT_FONT);
|
||||
|
||||
g_obj_label_clock = gfx_label_create(engine_handle);
|
||||
gfx_obj_align(g_obj_label_clock, GFX_ALIGN_TOP_MID, 0, 15);
|
||||
gfx_obj_set_size(g_obj_label_clock, 200, 50);
|
||||
gfx_label_set_text(g_obj_label_clock, "--:--");
|
||||
gfx_label_set_color(g_obj_label_clock, GFX_COLOR_HEX(0xFFFFFF));
|
||||
gfx_label_set_text_align(g_obj_label_clock, GFX_TEXT_ALIGN_CENTER);
|
||||
gfx_label_set_font(g_obj_label_clock, (gfx_font_t)&BUILTIN_TEXT_FONT);
|
||||
|
||||
g_obj_anim_listen = gfx_anim_create(engine_handle);
|
||||
gfx_obj_align(g_obj_anim_listen, GFX_ALIGN_TOP_MID, 0, 5);
|
||||
gfx_anim_start(g_obj_anim_listen);
|
||||
gfx_obj_set_visible(g_obj_anim_listen, false);
|
||||
|
||||
g_obj_img_status = gfx_img_create(engine_handle);
|
||||
gfx_obj_align(g_obj_img_status, GFX_ALIGN_TOP_MID, -120, 18);
|
||||
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, display);
|
||||
}
|
||||
|
||||
static void RegisterCallbacks(const esp_lcd_panel_io_handle_t panel_io, const gfx_handle_t engine_handle)
|
||||
{
|
||||
if (!panel_io) {
|
||||
ESP_LOGE(TAG, "RegisterCallbacks: panel_io is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
const esp_lcd_panel_io_callbacks_t cbs = {
|
||||
.on_color_trans_done = EmoteEngine::OnFlushIoReady,
|
||||
};
|
||||
esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, engine_handle);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EmoteEngine Class Implementation
|
||||
// ============================================================================
|
||||
|
||||
EmoteEngine::EmoteEngine(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io,
|
||||
const int width, const int height, EmoteDisplay* const display)
|
||||
{
|
||||
InitializeGraphics(panel, &engine_handle_, width, height);
|
||||
|
||||
if (display) {
|
||||
gfx_emote_lock(engine_handle_);
|
||||
SetupUI(engine_handle_, display);
|
||||
gfx_emote_unlock(engine_handle_);
|
||||
}
|
||||
|
||||
RegisterCallbacks(panel_io, engine_handle_);
|
||||
}
|
||||
|
||||
EmoteEngine::~EmoteEngine()
|
||||
{
|
||||
if (engine_handle_) {
|
||||
gfx_emote_deinit(engine_handle_);
|
||||
engine_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteEngine::SetEyes(const std::string &emoji_name, const bool repeat, const int fps, EmoteDisplay* const display)
|
||||
{
|
||||
if (!engine_handle_) {
|
||||
ESP_LOGE(TAG, "SetEyes: engine_handle_ is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!display) {
|
||||
ESP_LOGE(TAG, "SetEyes: display is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
const AssetData emoji_data = display->GetEmojiData(emoji_name);
|
||||
if (emoji_data.data) {
|
||||
DisplayLockGuard lock(display);
|
||||
gfx_anim_set_src(g_obj_anim_eye, emoji_data.data, emoji_data.size);
|
||||
gfx_anim_set_segment(g_obj_anim_eye, 0, 0xFFFF, fps, repeat);
|
||||
gfx_obj_set_visible(g_obj_anim_eye, true);
|
||||
gfx_anim_start(g_obj_anim_eye);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "SetEyes: No emoji data found for %s", emoji_name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteEngine::SetIcon(const std::string &icon_name, EmoteDisplay* const display)
|
||||
{
|
||||
if (!engine_handle_) {
|
||||
ESP_LOGE(TAG, "SetIcon: engine_handle_ is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!display) {
|
||||
ESP_LOGE(TAG, "SetIcon: display is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
const AssetData icon_data = display->GetIconData(icon_name);
|
||||
if (icon_data.data) {
|
||||
DisplayLockGuard lock(display);
|
||||
|
||||
std::memcpy(&g_icon_img_dsc.header, icon_data.data, sizeof(gfx_image_header_t));
|
||||
g_icon_img_dsc.data = static_cast<const uint8_t*>(icon_data.data) + sizeof(gfx_image_header_t);
|
||||
g_icon_img_dsc.data_size = icon_data.size - sizeof(gfx_image_header_t);
|
||||
|
||||
gfx_img_set_src(g_obj_img_status, &g_icon_img_dsc);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "SetIcon: No icon data found for %s", icon_name.c_str());
|
||||
}
|
||||
g_current_icon_type = icon_name;
|
||||
}
|
||||
|
||||
bool EmoteEngine::OnFlushIoReady(const esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_io_event_data_t* const edata,
|
||||
void* const user_ctx)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void EmoteEngine::OnFlush(const gfx_handle_t handle, const int x_start, const int y_start,
|
||||
const int x_end, const int y_end, const void* const color_data)
|
||||
{
|
||||
auto* const panel = static_cast<esp_lcd_panel_handle_t>(gfx_emote_get_user_data(handle));
|
||||
if (panel) {
|
||||
esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data);
|
||||
}
|
||||
gfx_emote_flush_ready(handle, true);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EmoteDisplay Class Implementation
|
||||
// ============================================================================
|
||||
|
||||
EmoteDisplay::EmoteDisplay(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io,
|
||||
const int width, const int height)
|
||||
{
|
||||
InitializeEngine(panel, panel_io, width, height);
|
||||
}
|
||||
|
||||
EmoteDisplay::~EmoteDisplay() = default;
|
||||
|
||||
void EmoteDisplay::SetEmotion(const char* const emotion)
|
||||
{
|
||||
if (!emotion) {
|
||||
ESP_LOGE(TAG, "SetEmotion: emotion is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "SetEmotion: %s", emotion);
|
||||
if (!engine_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AssetData emoji_data = GetEmojiData(emotion);
|
||||
bool repeat = emoji_data.loop;
|
||||
int fps = emoji_data.fps > 0 ? emoji_data.fps : 20;
|
||||
|
||||
if (std::strcmp(emotion, "idle") == 0 || std::strcmp(emotion, "neutral") == 0) {
|
||||
repeat = false;
|
||||
}
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
engine_->SetEyes(emotion, repeat, fps, this);
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetChatMessage(const char* const role, const char* const content)
|
||||
{
|
||||
if (!engine_) {
|
||||
return;
|
||||
}
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
if (content && strlen(content) > 0) {
|
||||
gfx_label_set_text(g_obj_label_toast, content);
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetStatus(const char* const status)
|
||||
{
|
||||
if (!status) {
|
||||
ESP_LOGE(TAG, "SetStatus: status is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!engine_) {
|
||||
return;
|
||||
}
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
|
||||
if (std::strcmp(status, Lang::Strings::LISTENING) == 0) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_LISTENING, this);
|
||||
engine_->SetEyes("happy", true, 20, this);
|
||||
engine_->SetIcon(ICON_MIC, this);
|
||||
} else if (std::strcmp(status, Lang::Strings::STANDBY) == 0) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIME, this);
|
||||
engine_->SetIcon(ICON_BATTERY, this);
|
||||
} else if (std::strcmp(status, Lang::Strings::SPEAKING) == 0) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this);
|
||||
engine_->SetIcon(ICON_SPEAKER_ZZZ, this);
|
||||
} else if (std::strcmp(status, Lang::Strings::ERROR) == 0) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this);
|
||||
engine_->SetIcon(ICON_WIFI_FAILED, this);
|
||||
}
|
||||
|
||||
if (std::strcmp(status, Lang::Strings::CONNECTING) != 0) {
|
||||
gfx_label_set_text(g_obj_label_toast, status);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::ShowNotification(const char* notification, int duration_ms)
|
||||
{
|
||||
if (!notification || !engine_) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "ShowNotification: %s", notification);
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
gfx_label_set_text(g_obj_label_toast, notification);
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this);
|
||||
}
|
||||
|
||||
void EmoteDisplay::UpdateStatusBar(bool update_all)
|
||||
{
|
||||
if (!engine_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only display time when battery icon is shown
|
||||
DisplayLockGuard lock(this);
|
||||
if (g_current_icon_type == ICON_BATTERY) {
|
||||
time_t now;
|
||||
struct tm timeinfo;
|
||||
time(&now);
|
||||
|
||||
setenv("TZ", "GMT+0", 1);
|
||||
tzset();
|
||||
localtime_r(&now, &timeinfo);
|
||||
|
||||
char time_str[6];
|
||||
snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
gfx_label_set_text(g_obj_label_clock, time_str);
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIME, this);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetPowerSaveMode(bool on)
|
||||
{
|
||||
if (!engine_) {
|
||||
return;
|
||||
}
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
ESP_LOGI(TAG, "SetPowerSaveMode: %s", on ? "ON" : "OFF");
|
||||
if (on) {
|
||||
gfx_anim_stop(g_obj_anim_eye);
|
||||
} else {
|
||||
gfx_anim_start(g_obj_anim_eye);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetPreviewImage(const void* image)
|
||||
{
|
||||
if (image) {
|
||||
ESP_LOGI(TAG, "SetPreviewImage: Preview image not supported, using default icon");
|
||||
if (engine_) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::SetTheme(Theme* const theme)
|
||||
{
|
||||
ESP_LOGI(TAG, "SetTheme: %p", theme);
|
||||
|
||||
}
|
||||
void EmoteDisplay::AddEmojiData(const std::string &name, const void* const data, const size_t size,
|
||||
uint8_t fps, bool loop, bool lack)
|
||||
{
|
||||
emoji_data_map_[name] = AssetData(data, size, fps, loop, lack);
|
||||
ESP_LOGD(TAG, "Added emoji data: %s, size: %d, fps: %d, loop: %s, lack: %s",
|
||||
name.c_str(), size, fps, loop ? "true" : "false", lack ? "true" : "false");
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
if (name == "happy") {
|
||||
engine_->SetEyes("happy", loop, fps > 0 ? fps : 20, this);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::AddIconData(const std::string &name, const void* const data, const size_t size)
|
||||
{
|
||||
icon_data_map_[name] = AssetData(data, size);
|
||||
ESP_LOGD(TAG, "Added icon data: %s, size: %d", name.c_str(), size);
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
if (name == ICON_WIFI_FAILED) {
|
||||
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS, this);
|
||||
engine_->SetIcon(ICON_WIFI_FAILED, this);
|
||||
}
|
||||
}
|
||||
|
||||
void EmoteDisplay::AddLayoutData(const std::string &name, const std::string &align_str,
|
||||
const int x, const int y, const int width, const int height)
|
||||
{
|
||||
const char align_enum = StringToGfxAlign(align_str);
|
||||
ESP_LOGI(TAG, "layout: %-12s | %-20s(%d) | %4d, %4d | %4dx%-4d",
|
||||
name.c_str(), align_str.c_str(), align_enum, x, y, width, height);
|
||||
|
||||
struct UIElement {
|
||||
gfx_obj_t* obj;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
const UIElement elements[] = {
|
||||
{g_obj_anim_eye, UI_ELEMENT_EYE_ANIM},
|
||||
{g_obj_label_toast, UI_ELEMENT_TOAST_LABEL},
|
||||
{g_obj_label_clock, UI_ELEMENT_CLOCK_LABEL},
|
||||
{g_obj_anim_listen, UI_ELEMENT_LISTEN_ANIM},
|
||||
{g_obj_img_status, UI_ELEMENT_STATUS_ICON}
|
||||
};
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
for (const auto &element : elements) {
|
||||
if (name == element.name && element.obj) {
|
||||
gfx_obj_align(element.obj, align_enum, x, y);
|
||||
if (width > 0 && height > 0) {
|
||||
gfx_obj_set_size(element.obj, width, height);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "AddLayoutData: UI element '%s' not found", name.c_str());
|
||||
}
|
||||
|
||||
void EmoteDisplay::AddTextFont(std::shared_ptr<LvglFont> text_font)
|
||||
{
|
||||
if (!text_font) {
|
||||
ESP_LOGW(TAG, "AddTextFont: text_font is nullptr");
|
||||
return;
|
||||
}
|
||||
|
||||
text_font_ = text_font;
|
||||
ESP_LOGD(TAG, "AddTextFont: Text font added successfully");
|
||||
|
||||
DisplayLockGuard lock(this);
|
||||
if (g_obj_label_toast && text_font_) {
|
||||
gfx_label_set_font(g_obj_label_toast, const_cast<void*>(static_cast<const void*>(text_font_->font())));
|
||||
}
|
||||
if (g_obj_label_clock && text_font_) {
|
||||
gfx_label_set_font(g_obj_label_clock, const_cast<void*>(static_cast<const void*>(text_font_->font())));
|
||||
}
|
||||
}
|
||||
|
||||
AssetData EmoteDisplay::GetEmojiData(const std::string &name) const
|
||||
{
|
||||
const auto it = emoji_data_map_.find(name);
|
||||
if (it != emoji_data_map_.cend()) {
|
||||
return it->second;
|
||||
}
|
||||
return AssetData();
|
||||
}
|
||||
|
||||
AssetData EmoteDisplay::GetIconData(const std::string &name) const
|
||||
{
|
||||
const auto it = icon_data_map_.find(name);
|
||||
if (it != icon_data_map_.cend()) {
|
||||
return it->second;
|
||||
}
|
||||
return AssetData();
|
||||
}
|
||||
|
||||
EmoteEngine* EmoteDisplay::GetEngine() const
|
||||
{
|
||||
return engine_.get();
|
||||
}
|
||||
|
||||
void* EmoteDisplay::GetEngineHandle() const
|
||||
{
|
||||
return engine_ ? engine_->GetEngineHandle() : nullptr;
|
||||
}
|
||||
|
||||
void EmoteDisplay::InitializeEngine(const esp_lcd_panel_handle_t panel, const esp_lcd_panel_io_handle_t panel_io,
|
||||
const int width, const int height)
|
||||
{
|
||||
engine_ = std::make_unique<EmoteEngine>(panel, panel_io, width, height, this);
|
||||
}
|
||||
|
||||
bool EmoteDisplay::Lock(const int timeout_ms)
|
||||
{
|
||||
if (engine_ && engine_->GetEngineHandle()) {
|
||||
gfx_emote_lock(engine_->GetEngineHandle());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void EmoteDisplay::Unlock()
|
||||
{
|
||||
if (engine_ && engine_->GetEngineHandle()) {
|
||||
gfx_emote_unlock(engine_->GetEngineHandle());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace emote
|
||||
102
main/display/emote_display.h
Normal file
102
main/display/emote_display.h
Normal file
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include "display.h"
|
||||
#include "lvgl_font.h"
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
|
||||
namespace emote {
|
||||
|
||||
// Simple data structure for storing asset data without LVGL dependency
|
||||
struct AssetData {
|
||||
const void* data;
|
||||
size_t size;
|
||||
union {
|
||||
uint8_t flags; // 1 byte for all animation flags
|
||||
struct {
|
||||
uint8_t fps : 6; // FPS (0-63) - 6 bits
|
||||
uint8_t loop : 1; // Loop animation - 1 bit
|
||||
uint8_t lack : 1; // Lack animation - 1 bit
|
||||
};
|
||||
};
|
||||
|
||||
AssetData() : data(nullptr), size(0), flags(0) {}
|
||||
AssetData(const void* d, size_t s) : data(d), size(s), flags(0) {}
|
||||
AssetData(const void* d, size_t s, uint8_t f, bool l, bool k)
|
||||
: data(d), size(s)
|
||||
{
|
||||
fps = f > 63 ? 63 : f; // 限制 FPS 到 6 位范围
|
||||
loop = l;
|
||||
lack = k;
|
||||
}
|
||||
};
|
||||
|
||||
// Layout element data structure
|
||||
struct LayoutData {
|
||||
char align; // Store as char instead of string
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
int height;
|
||||
bool has_size;
|
||||
|
||||
LayoutData() : align(0), x(0), y(0), width(0), height(0), has_size(false) {}
|
||||
LayoutData(char a, int x_pos, int y_pos, int w = 0, int h = 0)
|
||||
: align(a), x(x_pos), y(y_pos), width(w), height(h), has_size(w > 0 && h > 0) {}
|
||||
};
|
||||
|
||||
// Function to convert align string to GFX_ALIGN enum value
|
||||
char StringToGfxAlign(const std::string &align_str);
|
||||
|
||||
class EmoteEngine;
|
||||
|
||||
class EmoteDisplay : public Display {
|
||||
public:
|
||||
EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io, int width, int height);
|
||||
virtual ~EmoteDisplay();
|
||||
|
||||
virtual void SetEmotion(const char* emotion) override;
|
||||
virtual void SetStatus(const char* status) override;
|
||||
virtual void SetChatMessage(const char* role, const char* content) override;
|
||||
virtual void SetTheme(Theme* theme) override;
|
||||
virtual void ShowNotification(const char* notification, int duration_ms = 3000) override;
|
||||
virtual void UpdateStatusBar(bool update_all = false) override;
|
||||
virtual void SetPowerSaveMode(bool on) override;
|
||||
virtual void SetPreviewImage(const void* image);
|
||||
|
||||
void AddEmojiData(const std::string &name, const void* data, size_t size, uint8_t fps = 0, bool loop = false, bool lack = false);
|
||||
void AddIconData(const std::string &name, const void* data, size_t size);
|
||||
void AddLayoutData(const std::string &name, const std::string &align_str, int x, int y, int width = 0, int height = 0);
|
||||
void AddTextFont(std::shared_ptr<LvglFont> text_font);
|
||||
AssetData GetEmojiData(const std::string &name) const;
|
||||
AssetData GetIconData(const std::string &name) const;
|
||||
|
||||
EmoteEngine* GetEngine() const;
|
||||
void* GetEngineHandle() const;
|
||||
|
||||
inline std::shared_ptr<LvglFont> text_font() const
|
||||
{
|
||||
return text_font_;
|
||||
}
|
||||
|
||||
private:
|
||||
void InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io, int width, int height);
|
||||
virtual bool Lock(int timeout_ms = 0) override;
|
||||
virtual void Unlock() override;
|
||||
|
||||
std::unique_ptr<EmoteEngine> engine_;
|
||||
|
||||
// Font management
|
||||
std::shared_ptr<LvglFont> text_font_ = nullptr;
|
||||
|
||||
// Non-LVGL asset data storage
|
||||
std::map<std::string, AssetData> emoji_data_map_;
|
||||
std::map<std::string, AssetData> icon_data_map_;
|
||||
|
||||
};
|
||||
|
||||
} // namespace emote
|
||||
@@ -5,10 +5,14 @@ dependencies:
|
||||
espressif/esp_lcd_gc9a01: ==2.0.1
|
||||
espressif/esp_lcd_st77916: ^1.0.1
|
||||
espressif/esp_lcd_axs15231b: ^1.0.0
|
||||
espressif/esp_lcd_st7796:
|
||||
version: 1.3.4
|
||||
espressif/esp_lcd_st7701:
|
||||
version: ^1.1.4
|
||||
rules:
|
||||
- if: target not in [esp32c3]
|
||||
- if: target in [esp32s3, esp32p4]
|
||||
espressif/esp_lcd_st7796:
|
||||
version: 1.3.5
|
||||
rules:
|
||||
- if: target not in [esp32c3, esp32c6]
|
||||
espressif/esp_lcd_spd2010: ==1.0.2
|
||||
espressif/esp_io_expander_tca9554: ==2.0.0
|
||||
espressif/esp_lcd_panel_io_additions: ^1.0.1
|
||||
@@ -32,7 +36,7 @@ dependencies:
|
||||
esp_lvgl_port: ~2.6.0
|
||||
espressif/esp_io_expander_tca95xx_16bit: ^2.0.0
|
||||
espressif2022/image_player: ==1.1.0~1
|
||||
espressif2022/esp_emote_gfx: ==1.0.0~2
|
||||
espressif2022/esp_emote_gfx: ^1.1.0
|
||||
espressif/adc_mic: ^0.2.1
|
||||
espressif/esp_mmap_assets: '>=1.2'
|
||||
txp666/otto-emoji-gif-component: ~1.0.2
|
||||
|
||||
@@ -17,8 +17,6 @@ import shutil
|
||||
import sys
|
||||
import json
|
||||
import struct
|
||||
import math
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -156,9 +154,9 @@ def copy_directory(src, dst):
|
||||
return False
|
||||
|
||||
|
||||
def process_sr_models(wakenet_model_dir, multinet_model_dirs, build_dir, assets_dir):
|
||||
def process_sr_models(wakenet_model_dirs, multinet_model_dirs, build_dir, assets_dir):
|
||||
"""Process SR models (wakenet and multinet) and generate srmodels.bin"""
|
||||
if not wakenet_model_dir and not multinet_model_dirs:
|
||||
if not wakenet_model_dirs and not multinet_model_dirs:
|
||||
return None
|
||||
|
||||
# Create SR models build directory
|
||||
@@ -169,13 +167,14 @@ def process_sr_models(wakenet_model_dir, multinet_model_dirs, build_dir, assets_
|
||||
|
||||
models_processed = 0
|
||||
|
||||
# Copy wakenet model if available
|
||||
if wakenet_model_dir:
|
||||
wakenet_name = os.path.basename(wakenet_model_dir)
|
||||
wakenet_dst = os.path.join(sr_models_build_dir, wakenet_name)
|
||||
if copy_directory(wakenet_model_dir, wakenet_dst):
|
||||
models_processed += 1
|
||||
print(f"Added wakenet model: {wakenet_name}")
|
||||
# Copy wakenet models if available
|
||||
if wakenet_model_dirs:
|
||||
for wakenet_model_dir in wakenet_model_dirs:
|
||||
wakenet_name = os.path.basename(wakenet_model_dir)
|
||||
wakenet_dst = os.path.join(sr_models_build_dir, wakenet_name)
|
||||
if copy_directory(wakenet_model_dir, wakenet_dst):
|
||||
models_processed += 1
|
||||
print(f"Added wakenet model: {wakenet_name}")
|
||||
|
||||
# Copy multinet models if available
|
||||
if multinet_model_dirs:
|
||||
@@ -203,11 +202,6 @@ def process_sr_models(wakenet_model_dir, multinet_model_dirs, build_dir, assets_
|
||||
return None
|
||||
|
||||
|
||||
def process_wakenet_model(wakenet_model_dir, build_dir, assets_dir):
|
||||
"""Process wakenet_model parameter (legacy compatibility function)"""
|
||||
return process_sr_models(wakenet_model_dir, None, build_dir, assets_dir)
|
||||
|
||||
|
||||
def process_text_font(text_font_file, assets_dir):
|
||||
"""Process text_font parameter"""
|
||||
if not text_font_file:
|
||||
@@ -440,12 +434,12 @@ def pack_assets_simple(target_path, include_path, out_file, assets_path, max_nam
|
||||
|
||||
def read_wakenet_from_sdkconfig(sdkconfig_path):
|
||||
"""
|
||||
Read wakenet model from sdkconfig (based on movemodel.py logic)
|
||||
Returns the wakenet model name or None if no wakenet is configured
|
||||
Read wakenet models from sdkconfig (based on movemodel.py logic)
|
||||
Returns a list of wakenet model names
|
||||
"""
|
||||
if not os.path.exists(sdkconfig_path):
|
||||
print(f"Warning: sdkconfig file not found: {sdkconfig_path}")
|
||||
return None
|
||||
return []
|
||||
|
||||
models = []
|
||||
with io.open(sdkconfig_path, "r") as f:
|
||||
@@ -461,8 +455,7 @@ def read_wakenet_from_sdkconfig(sdkconfig_path):
|
||||
model_name = label.split("_SR_WN_")[-1].lower()
|
||||
models.append(model_name)
|
||||
|
||||
# Return the first model found, or None if no models
|
||||
return models[0] if models else None
|
||||
return models
|
||||
|
||||
|
||||
def read_multinet_from_sdkconfig(sdkconfig_path):
|
||||
@@ -514,6 +507,46 @@ def read_multinet_from_sdkconfig(sdkconfig_path):
|
||||
return models
|
||||
|
||||
|
||||
def read_wake_word_type_from_sdkconfig(sdkconfig_path):
|
||||
"""
|
||||
Read wake word type configuration from sdkconfig
|
||||
Returns a dict with wake word type info
|
||||
"""
|
||||
if not os.path.exists(sdkconfig_path):
|
||||
print(f"Warning: sdkconfig file not found: {sdkconfig_path}")
|
||||
return {
|
||||
'use_esp_wake_word': False,
|
||||
'use_afe_wake_word': False,
|
||||
'use_custom_wake_word': False,
|
||||
'wake_word_disabled': True
|
||||
}
|
||||
|
||||
config_values = {
|
||||
'use_esp_wake_word': False,
|
||||
'use_afe_wake_word': False,
|
||||
'use_custom_wake_word': False,
|
||||
'wake_word_disabled': False
|
||||
}
|
||||
|
||||
with io.open(sdkconfig_path, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip("\n")
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
|
||||
# Check for wake word type configuration
|
||||
if 'CONFIG_USE_ESP_WAKE_WORD=y' in line:
|
||||
config_values['use_esp_wake_word'] = True
|
||||
elif 'CONFIG_USE_AFE_WAKE_WORD=y' in line:
|
||||
config_values['use_afe_wake_word'] = True
|
||||
elif 'CONFIG_USE_CUSTOM_WAKE_WORD=y' in line:
|
||||
config_values['use_custom_wake_word'] = True
|
||||
elif 'CONFIG_WAKE_WORD_DISABLED=y' in line:
|
||||
config_values['wake_word_disabled'] = True
|
||||
|
||||
return config_values
|
||||
|
||||
|
||||
def read_custom_wake_word_from_sdkconfig(sdkconfig_path):
|
||||
"""
|
||||
Read custom wake word configuration from sdkconfig
|
||||
@@ -591,19 +624,23 @@ def get_language_from_multinet_models(multinet_models):
|
||||
return 'cn' # Default to Chinese
|
||||
|
||||
|
||||
def get_wakenet_model_path(model_name, esp_sr_model_path):
|
||||
def get_wakenet_model_paths(model_names, esp_sr_model_path):
|
||||
"""
|
||||
Get the full path to the wakenet model directory
|
||||
Get the full paths to the wakenet model directories
|
||||
Returns a list of valid model paths
|
||||
"""
|
||||
if not model_name:
|
||||
return None
|
||||
if not model_names:
|
||||
return []
|
||||
|
||||
wakenet_model_path = os.path.join(esp_sr_model_path, 'wakenet_model', model_name)
|
||||
if os.path.exists(wakenet_model_path):
|
||||
return wakenet_model_path
|
||||
else:
|
||||
print(f"Warning: Wakenet model directory not found: {wakenet_model_path}")
|
||||
return None
|
||||
valid_paths = []
|
||||
for model_name in model_names:
|
||||
wakenet_model_path = os.path.join(esp_sr_model_path, 'wakenet_model', model_name)
|
||||
if os.path.exists(wakenet_model_path):
|
||||
valid_paths.append(wakenet_model_path)
|
||||
else:
|
||||
print(f"Warning: Wakenet model directory not found: {wakenet_model_path}")
|
||||
|
||||
return valid_paths
|
||||
|
||||
|
||||
def get_multinet_model_paths(model_names, esp_sr_model_path):
|
||||
@@ -661,7 +698,7 @@ def get_emoji_collection_path(default_emoji_collection, xiaozhi_fonts_path):
|
||||
return None
|
||||
|
||||
|
||||
def build_assets_integrated(wakenet_model_path, multinet_model_paths, text_font_path, emoji_collection_path, extra_files_path, output_path, multinet_model_info=None):
|
||||
def build_assets_integrated(wakenet_model_paths, multinet_model_paths, text_font_path, emoji_collection_path, extra_files_path, output_path, multinet_model_info=None):
|
||||
"""
|
||||
Build assets using integrated functions (no external dependencies)
|
||||
"""
|
||||
@@ -679,7 +716,7 @@ def build_assets_integrated(wakenet_model_path, multinet_model_paths, text_font_
|
||||
print("Starting to build assets...")
|
||||
|
||||
# Process each component
|
||||
srmodels = process_sr_models(wakenet_model_path, multinet_model_paths, temp_build_dir, assets_dir) if (wakenet_model_path or multinet_model_paths) else None
|
||||
srmodels = process_sr_models(wakenet_model_paths, multinet_model_paths, temp_build_dir, assets_dir) if (wakenet_model_paths or multinet_model_paths) else None
|
||||
text_font = process_text_font(text_font_path, assets_dir) if text_font_path else None
|
||||
emoji_collection = process_emoji_collection(emoji_collection_path, assets_dir) if emoji_collection_path else None
|
||||
extra_files = process_extra_files(extra_files_path, assets_dir) if extra_files_path else None
|
||||
@@ -734,19 +771,17 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get script directory (not needed anymore but keep for future use)
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Set default paths if not provided
|
||||
if not args.esp_sr_model_path:
|
||||
# Default ESP-SR model path relative to project root
|
||||
project_root = os.path.dirname(os.path.dirname(script_dir))
|
||||
args.esp_sr_model_path = os.path.join(project_root, "managed_components", "espressif__esp-sr", "model")
|
||||
|
||||
if not args.xiaozhi_fonts_path:
|
||||
# Default xiaozhi-fonts path relative to project root
|
||||
project_root = os.path.dirname(os.path.dirname(script_dir))
|
||||
args.xiaozhi_fonts_path = os.path.join(project_root, "managed_components", "78__xiaozhi-fonts")
|
||||
if not args.esp_sr_model_path or not args.xiaozhi_fonts_path:
|
||||
# Calculate project root from script location
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(script_dir)
|
||||
|
||||
if not args.esp_sr_model_path:
|
||||
args.esp_sr_model_path = os.path.join(project_root, "managed_components", "espressif__esp-sr", "model")
|
||||
|
||||
if not args.xiaozhi_fonts_path:
|
||||
args.xiaozhi_fonts_path = os.path.join(project_root, "components", "xiaozhi-fonts")
|
||||
|
||||
print("Building default assets...")
|
||||
print(f" sdkconfig: {args.sdkconfig}")
|
||||
@@ -754,19 +789,40 @@ def main():
|
||||
print(f" emoji_collection: {args.emoji_collection}")
|
||||
print(f" output: {args.output}")
|
||||
|
||||
# Read wake word type configuration from sdkconfig
|
||||
wake_word_config = read_wake_word_type_from_sdkconfig(args.sdkconfig)
|
||||
|
||||
# Read SR models from sdkconfig
|
||||
wakenet_model_name = read_wakenet_from_sdkconfig(args.sdkconfig)
|
||||
wakenet_model_names = read_wakenet_from_sdkconfig(args.sdkconfig)
|
||||
multinet_model_names = read_multinet_from_sdkconfig(args.sdkconfig)
|
||||
|
||||
# Get model paths
|
||||
wakenet_model_path = get_wakenet_model_path(wakenet_model_name, args.esp_sr_model_path)
|
||||
multinet_model_paths = get_multinet_model_paths(multinet_model_names, args.esp_sr_model_path)
|
||||
# Apply wake word logic to decide which models to package
|
||||
wakenet_model_paths = []
|
||||
multinet_model_paths = []
|
||||
|
||||
# Print model information
|
||||
if wakenet_model_name:
|
||||
print(f" wakenet model: {wakenet_model_name}")
|
||||
if multinet_model_names:
|
||||
print(f" multinet models: {', '.join(multinet_model_names)}")
|
||||
# 1. Only package wakenet models if USE_ESP_WAKE_WORD=y or USE_AFE_WAKE_WORD=y
|
||||
if wake_word_config['use_esp_wake_word'] or wake_word_config['use_afe_wake_word']:
|
||||
wakenet_model_paths = get_wakenet_model_paths(wakenet_model_names, args.esp_sr_model_path)
|
||||
elif wakenet_model_names:
|
||||
print(f" Note: Found wakenet models {wakenet_model_names} but wake word type is not ESP/AFE, skipping")
|
||||
|
||||
# 2. Error check: if USE_CUSTOM_WAKE_WORD=y but no multinet models selected, report error
|
||||
if wake_word_config['use_custom_wake_word'] and not multinet_model_names:
|
||||
print("Error: USE_CUSTOM_WAKE_WORD is enabled but no multinet models are selected in sdkconfig")
|
||||
print("Please select appropriate CONFIG_SR_MN_* options in menuconfig, or disable USE_CUSTOM_WAKE_WORD")
|
||||
sys.exit(1)
|
||||
|
||||
# 3. Only package multinet models if USE_CUSTOM_WAKE_WORD=y
|
||||
if wake_word_config['use_custom_wake_word']:
|
||||
multinet_model_paths = get_multinet_model_paths(multinet_model_names, args.esp_sr_model_path)
|
||||
elif multinet_model_names:
|
||||
print(f" Note: Found multinet models {multinet_model_names} but USE_CUSTOM_WAKE_WORD is disabled, skipping")
|
||||
|
||||
# Print model information (only for models that will actually be packaged)
|
||||
if wakenet_model_paths:
|
||||
print(f" wakenet models: {', '.join(wakenet_model_names)} (will be packaged)")
|
||||
if multinet_model_paths:
|
||||
print(f" multinet models: {', '.join(multinet_model_names)} (will be packaged)")
|
||||
|
||||
# Get text font path if needed
|
||||
text_font_path = get_text_font_path(args.builtin_text_font, args.xiaozhi_fonts_path)
|
||||
@@ -781,7 +837,7 @@ def main():
|
||||
custom_wake_word_config = read_custom_wake_word_from_sdkconfig(args.sdkconfig)
|
||||
multinet_model_info = None
|
||||
|
||||
if custom_wake_word_config and multinet_model_names:
|
||||
if custom_wake_word_config and multinet_model_paths:
|
||||
# Determine language from multinet models
|
||||
language = get_language_from_multinet_models(multinet_model_names)
|
||||
|
||||
@@ -803,7 +859,7 @@ def main():
|
||||
print(f" wake word threshold: {custom_wake_word_config['threshold']}")
|
||||
|
||||
# Check if we have anything to build
|
||||
if not wakenet_model_path and not multinet_model_paths and not text_font_path and not emoji_collection_path and not extra_files_path and not multinet_model_info:
|
||||
if not wakenet_model_paths and not multinet_model_paths and not text_font_path and not emoji_collection_path and not extra_files_path and not multinet_model_info:
|
||||
print("Warning: No assets to build (no SR models, text font, emoji collection, extra files, or custom wake word)")
|
||||
# Create an empty assets.bin file
|
||||
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
||||
@@ -813,7 +869,7 @@ def main():
|
||||
return
|
||||
|
||||
# Build the assets
|
||||
success = build_assets_integrated(wakenet_model_path, multinet_model_paths, text_font_path, emoji_collection_path,
|
||||
success = build_assets_integrated(wakenet_model_paths, multinet_model_paths, text_font_path, emoji_collection_path,
|
||||
extra_files_path, args.output, multinet_model_info)
|
||||
|
||||
if not success:
|
||||
|
||||
@@ -3,107 +3,186 @@ import os
|
||||
import json
|
||||
import zipfile
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# 切换到项目根目录
|
||||
os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
# Switch to project root directory
|
||||
os.chdir(Path(__file__).resolve().parent.parent)
|
||||
|
||||
def get_board_type():
|
||||
with open("build/compile_commands.json") as f:
|
||||
################################################################################
|
||||
# Common utility functions
|
||||
################################################################################
|
||||
|
||||
def get_board_type_from_compile_commands() -> Optional[str]:
|
||||
"""Parse the current compiled BOARD_TYPE from build/compile_commands.json"""
|
||||
compile_file = Path("build/compile_commands.json")
|
||||
if not compile_file.exists():
|
||||
return None
|
||||
with compile_file.open() as f:
|
||||
data = json.load(f)
|
||||
for item in data:
|
||||
if not item["file"].endswith("main.cc"):
|
||||
continue
|
||||
command = item["command"]
|
||||
# extract -DBOARD_TYPE=xxx
|
||||
board_type = command.split("-DBOARD_TYPE=\\\"")[1].split("\\\"")[0].strip()
|
||||
return board_type
|
||||
for item in data:
|
||||
if not item["file"].endswith("main.cc"):
|
||||
continue
|
||||
cmd = item["command"]
|
||||
if "-DBOARD_TYPE=\\\"" in cmd:
|
||||
return cmd.split("-DBOARD_TYPE=\\\"")[1].split("\\\"")[0].strip()
|
||||
return None
|
||||
|
||||
def get_project_version():
|
||||
with open("CMakeLists.txt") as f:
|
||||
|
||||
def get_project_version() -> Optional[str]:
|
||||
"""Read set(PROJECT_VER "x.y.z") from root CMakeLists.txt"""
|
||||
with Path("CMakeLists.txt").open() as f:
|
||||
for line in f:
|
||||
if line.startswith("set(PROJECT_VER"):
|
||||
return line.split("\"")[1].split("\"")[0].strip()
|
||||
return line.split("\"")[1]
|
||||
return None
|
||||
|
||||
def merge_bin():
|
||||
|
||||
def merge_bin() -> None:
|
||||
if os.system("idf.py merge-bin") != 0:
|
||||
print("merge bin failed")
|
||||
print("merge-bin failed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def zip_bin(board_type, project_version):
|
||||
if not os.path.exists("releases"):
|
||||
os.makedirs("releases")
|
||||
output_path = f"releases/v{project_version}_{board_type}.zip"
|
||||
if os.path.exists(output_path):
|
||||
os.remove(output_path)
|
||||
with zipfile.ZipFile(output_path, 'w', compression=zipfile.ZIP_DEFLATED) as zipf:
|
||||
|
||||
def zip_bin(name: str, version: str) -> None:
|
||||
"""Zip build/merged-binary.bin to releases/v{version}_{name}.zip"""
|
||||
out_dir = Path("releases")
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
output_path = out_dir / f"v{version}_{name}.zip"
|
||||
|
||||
if output_path.exists():
|
||||
output_path.unlink()
|
||||
|
||||
with zipfile.ZipFile(output_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
|
||||
zipf.write("build/merged-binary.bin", arcname="merged-binary.bin")
|
||||
print(f"zip bin to {output_path} done")
|
||||
|
||||
|
||||
def release_current():
|
||||
merge_bin()
|
||||
board_type = get_board_type()
|
||||
print("board type:", board_type)
|
||||
project_version = get_project_version()
|
||||
print("project version:", project_version)
|
||||
zip_bin(board_type, project_version)
|
||||
################################################################################
|
||||
# board / variant related functions
|
||||
################################################################################
|
||||
|
||||
def get_all_board_types():
|
||||
board_configs = {}
|
||||
with open("main/CMakeLists.txt", encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
for i, line in enumerate(lines):
|
||||
# 查找 if(CONFIG_BOARD_TYPE_*) 行
|
||||
if "if(CONFIG_BOARD_TYPE_" in line:
|
||||
config_name = line.strip().split("if(")[1].split(")")[0]
|
||||
# 查找下一行的 set(BOARD_TYPE "xxx")
|
||||
next_line = lines[i + 1].strip()
|
||||
_BOARDS_DIR = Path("main/boards")
|
||||
|
||||
|
||||
def _collect_variants(config_filename: str = "config.json") -> list[dict[str, str]]:
|
||||
"""Traverse all boards under main/boards, collect variant information.
|
||||
|
||||
Return example:
|
||||
[{"board": "bread-compact-ml307", "name": "bread-compact-ml307"}, ...]
|
||||
"""
|
||||
variants: list[dict[str, str]] = []
|
||||
for board_path in _BOARDS_DIR.iterdir():
|
||||
if not board_path.is_dir():
|
||||
continue
|
||||
if board_path.name == "common":
|
||||
continue
|
||||
cfg_path = board_path / config_filename
|
||||
if not cfg_path.exists():
|
||||
print(f"[WARN] {cfg_path} does not exist, skip", file=sys.stderr)
|
||||
continue
|
||||
try:
|
||||
with cfg_path.open() as f:
|
||||
cfg = json.load(f)
|
||||
for build in cfg.get("builds", []):
|
||||
variants.append({"board": board_path.name, "name": build["name"]})
|
||||
except Exception as e:
|
||||
print(f"[ERROR] 解析 {cfg_path} 失败: {e}", file=sys.stderr)
|
||||
return variants
|
||||
|
||||
|
||||
def _parse_board_config_map() -> dict[str, str]:
|
||||
"""Build the mapping of CONFIG_BOARD_TYPE_xxx and board_type from main/CMakeLists.txt"""
|
||||
cmake_file = Path("main/CMakeLists.txt")
|
||||
mapping: dict[str, str] = {}
|
||||
lines = cmake_file.read_text(encoding="utf-8").splitlines()
|
||||
for idx, line in enumerate(lines):
|
||||
if "if(CONFIG_BOARD_TYPE_" in line:
|
||||
config_name = line.strip().split("if(")[1].split(")")[0]
|
||||
if idx + 1 < len(lines):
|
||||
next_line = lines[idx + 1].strip()
|
||||
if next_line.startswith("set(BOARD_TYPE"):
|
||||
board_type = next_line.split('"')[1]
|
||||
board_configs[config_name] = board_type
|
||||
return board_configs
|
||||
mapping[config_name] = board_type
|
||||
return mapping
|
||||
|
||||
def release(board_type, board_config, config_filename="config.json"):
|
||||
config_path = f"main/boards/{board_type}/{config_filename}"
|
||||
if not os.path.exists(config_path):
|
||||
print(f"跳过 {board_type} 因为 {config_filename} 不存在")
|
||||
|
||||
def _find_board_config(board_type: str) -> Optional[str]:
|
||||
"""Find the corresponding CONFIG_BOARD_TYPE_xxx for the given board_type"""
|
||||
for config, b_type in _parse_board_config_map().items():
|
||||
if b_type == board_type:
|
||||
return config
|
||||
return None
|
||||
|
||||
################################################################################
|
||||
# Check board_type in CMakeLists
|
||||
################################################################################
|
||||
|
||||
def _board_type_exists(board_type: str) -> bool:
|
||||
cmake_file = Path("main/CMakeLists.txt")
|
||||
pattern = f'set(BOARD_TYPE "{board_type}")'
|
||||
return pattern in cmake_file.read_text(encoding="utf-8")
|
||||
|
||||
################################################################################
|
||||
# Compile implementation
|
||||
################################################################################
|
||||
|
||||
def release(board_type: str, config_filename: str = "config.json", *, filter_name: Optional[str] = None) -> None:
|
||||
"""Compile and package all/specified variants of the specified board_type
|
||||
|
||||
Args:
|
||||
board_type: directory name under main/boards
|
||||
config_filename: config.json name (default: config.json)
|
||||
filter_name: if specified, only compile the build["name"] that matches
|
||||
"""
|
||||
cfg_path = _BOARDS_DIR / board_type / config_filename
|
||||
if not cfg_path.exists():
|
||||
print(f"[WARN] {cfg_path} 不存在,跳过 {board_type}")
|
||||
return
|
||||
|
||||
# Print Project Version
|
||||
project_version = get_project_version()
|
||||
print(f"Project Version: {project_version}", config_path)
|
||||
print(f"Project Version: {project_version} ({cfg_path})")
|
||||
|
||||
with cfg_path.open() as f:
|
||||
cfg = json.load(f)
|
||||
target = cfg["target"]
|
||||
|
||||
builds = cfg.get("builds", [])
|
||||
if filter_name:
|
||||
builds = [b for b in builds if b["name"] == filter_name]
|
||||
if not builds:
|
||||
print(f"[ERROR] 未在 {board_type} 的 {config_filename} 中找到变体 {filter_name}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config = json.load(f)
|
||||
target = config["target"]
|
||||
builds = config["builds"]
|
||||
|
||||
for build in builds:
|
||||
name = build["name"]
|
||||
if not name.startswith(board_type):
|
||||
raise ValueError(f"name {name} 必须以 {board_type} 开头")
|
||||
output_path = f"releases/v{project_version}_{name}.zip"
|
||||
if os.path.exists(output_path):
|
||||
print(f"跳过 {board_type} 因为 {output_path} 已存在")
|
||||
raise ValueError(f"build.name {name} 必须以 {board_type} 开头")
|
||||
|
||||
output_path = Path("releases") / f"v{project_version}_{name}.zip"
|
||||
if output_path.exists():
|
||||
print(f"跳过 {name} 因为 {output_path} 已存在")
|
||||
continue
|
||||
|
||||
sdkconfig_append = [f"{board_config}=y"]
|
||||
for append in build.get("sdkconfig_append", []):
|
||||
sdkconfig_append.append(append)
|
||||
# Process sdkconfig_append
|
||||
board_type_config = _find_board_config(board_type)
|
||||
sdkconfig_append = [f"{board_type_config}=y"]
|
||||
sdkconfig_append.extend(build.get("sdkconfig_append", []))
|
||||
|
||||
print("-" * 80)
|
||||
print(f"name: {name}")
|
||||
print(f"target: {target}")
|
||||
for append in sdkconfig_append:
|
||||
print(f"sdkconfig_append: {append}")
|
||||
# unset IDF_TARGET
|
||||
for item in sdkconfig_append:
|
||||
print(f"sdkconfig_append: {item}")
|
||||
|
||||
os.environ.pop("IDF_TARGET", None)
|
||||
|
||||
# Call set-target
|
||||
if os.system(f"idf.py set-target {target}") != 0:
|
||||
print("set-target failed")
|
||||
print("set-target failed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Append sdkconfig
|
||||
with open("sdkconfig", "a") as f:
|
||||
with Path("sdkconfig").open("a") as f:
|
||||
f.write("\n")
|
||||
f.write("# Append by release.py\n")
|
||||
for append in sdkconfig_append:
|
||||
@@ -112,43 +191,72 @@ def release(board_type, board_config, config_filename="config.json"):
|
||||
if os.system(f"idf.py -DBOARD_NAME={name} build") != 0:
|
||||
print("build failed")
|
||||
sys.exit(1)
|
||||
# Call merge-bin
|
||||
if os.system("idf.py merge-bin") != 0:
|
||||
print("merge-bin failed")
|
||||
sys.exit(1)
|
||||
# Zip bin
|
||||
|
||||
# merge-bin
|
||||
merge_bin()
|
||||
|
||||
# Zip
|
||||
zip_bin(name, project_version)
|
||||
print("-" * 80)
|
||||
|
||||
################################################################################
|
||||
# CLI entry
|
||||
################################################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("board", nargs="?", default=None, help="板子类型或 all")
|
||||
parser.add_argument("-c", "--config", default="config.json", help="指定 config 文件名,默认 config.json")
|
||||
parser.add_argument("--list-boards", action="store_true", help="列出所有支持的 board 列表")
|
||||
parser.add_argument("--list-boards", action="store_true", help="列出所有支持的 board 及变体列表")
|
||||
parser.add_argument("--json", action="store_true", help="配合 --list-boards,JSON 格式输出")
|
||||
parser.add_argument("--name", help="指定变体名称,仅编译匹配的变体")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# List mode
|
||||
if args.list_boards:
|
||||
board_configs = get_all_board_types()
|
||||
boards = list(board_configs.values())
|
||||
variants = _collect_variants(config_filename=args.config)
|
||||
if args.json:
|
||||
print(json.dumps(boards))
|
||||
print(json.dumps(variants))
|
||||
else:
|
||||
for board in boards:
|
||||
print(board)
|
||||
for v in variants:
|
||||
print(f"{v['board']}: {v['name']}")
|
||||
sys.exit(0)
|
||||
|
||||
if args.board:
|
||||
board_configs = get_all_board_types()
|
||||
found = False
|
||||
for board_config, board_type in board_configs.items():
|
||||
if args.board == 'all' or board_type == args.board:
|
||||
release(board_type, board_config, config_filename=args.config)
|
||||
found = True
|
||||
if not found:
|
||||
print(f"未找到板子类型: {args.board}")
|
||||
print("可用的板子类型:")
|
||||
for board_type in board_configs.values():
|
||||
print(f" {board_type}")
|
||||
# Current directory firmware packaging mode
|
||||
if args.board is None:
|
||||
merge_bin()
|
||||
curr_board_type = get_board_type_from_compile_commands()
|
||||
if curr_board_type is None:
|
||||
print("未能从 compile_commands.json 解析 board_type", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
project_ver = get_project_version()
|
||||
zip_bin(curr_board_type, project_ver)
|
||||
sys.exit(0)
|
||||
|
||||
# Compile mode
|
||||
board_type_input: str = args.board
|
||||
name_filter: str | None = args.name
|
||||
|
||||
# Check board_type in CMakeLists
|
||||
if board_type_input != "all" and not _board_type_exists(board_type_input):
|
||||
print(f"[ERROR] main/CMakeLists.txt 中未找到 board_type {board_type_input}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
variants_all = _collect_variants(config_filename=args.config)
|
||||
|
||||
# Filter board_type list
|
||||
target_board_types: set[str]
|
||||
if board_type_input == "all":
|
||||
target_board_types = {v["board"] for v in variants_all}
|
||||
else:
|
||||
release_current()
|
||||
target_board_types = {board_type_input}
|
||||
|
||||
for bt in sorted(target_board_types):
|
||||
if not _board_type_exists(bt):
|
||||
print(f"[ERROR] main/CMakeLists.txt 中未找到 board_type {bt}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
cfg_path = _BOARDS_DIR / bt / args.config
|
||||
if bt == board_type_input and not cfg_path.exists():
|
||||
print(f"开发板 {bt} 未定义 {args.config} 配置文件,跳过")
|
||||
sys.exit(0)
|
||||
release(bt, config_filename=args.config, filter_name=name_filter if bt == board_type_input else None)
|
||||
|
||||
185
scripts/spiffs_assets/build.py
Executable file → Normal file
185
scripts/spiffs_assets/build.py
Executable file → Normal file
@@ -113,8 +113,170 @@ def process_emoji_collection(emoji_collection_dir, assets_dir):
|
||||
|
||||
return emoji_list
|
||||
|
||||
def load_emoji_config(emoji_collection_dir):
|
||||
"""Load emoji config from config.json file"""
|
||||
config_path = os.path.join(emoji_collection_dir, "emote.json")
|
||||
if not os.path.exists(config_path):
|
||||
print(f"Warning: Config file not found: {config_path}")
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
# Convert list format to dict for easy lookup
|
||||
config_dict = {}
|
||||
for item in config_data:
|
||||
if "emote" in item:
|
||||
config_dict[item["emote"]] = item
|
||||
|
||||
return config_dict
|
||||
except Exception as e:
|
||||
print(f"Error loading config file {config_path}: {e}")
|
||||
return {}
|
||||
|
||||
def generate_index_json(assets_dir, srmodels, text_font, emoji_collection):
|
||||
def process_board_emoji_collection(emoji_collection_dir, target_board_dir, assets_dir):
|
||||
"""Process emoji_collection parameter"""
|
||||
if not emoji_collection_dir:
|
||||
return []
|
||||
|
||||
emoji_config = load_emoji_config(target_board_dir)
|
||||
print(f"Loaded emoji config with {len(emoji_config)} entries")
|
||||
|
||||
emoji_list = []
|
||||
|
||||
for emote_name, config in emoji_config.items():
|
||||
|
||||
if "src" not in config:
|
||||
print(f"Error: No src field found for emote '{emote_name}' in config")
|
||||
continue
|
||||
|
||||
eaf_file_path = os.path.join(emoji_collection_dir, config["src"])
|
||||
file_exists = os.path.exists(eaf_file_path)
|
||||
|
||||
if not file_exists:
|
||||
print(f"Warning: EAF file not found for emote '{emote_name}': {eaf_file_path}")
|
||||
else:
|
||||
# Copy eaf file to assets directory
|
||||
copy_file(eaf_file_path, os.path.join(assets_dir, config["src"]))
|
||||
|
||||
# Create emoji entry with src as file (merge file and src)
|
||||
emoji_entry = {
|
||||
"name": emote_name,
|
||||
"file": config["src"] # Use src as the actual file
|
||||
}
|
||||
|
||||
eaf_properties = {}
|
||||
|
||||
if not file_exists:
|
||||
eaf_properties["lack"] = True
|
||||
|
||||
if "loop" in config:
|
||||
eaf_properties["loop"] = config["loop"]
|
||||
|
||||
if "fps" in config:
|
||||
eaf_properties["fps"] = config["fps"]
|
||||
|
||||
if eaf_properties:
|
||||
emoji_entry["eaf"] = eaf_properties
|
||||
|
||||
status = "MISSING" if not file_exists else "OK"
|
||||
eaf_info = emoji_entry.get('eaf', {})
|
||||
print(f"emote '{emote_name}': file='{emoji_entry['file']}', status={status}, lack={eaf_info.get('lack', False)}, loop={eaf_info.get('loop', 'none')}, fps={eaf_info.get('fps', 'none')}")
|
||||
|
||||
emoji_list.append(emoji_entry)
|
||||
|
||||
print(f"Successfully processed {len(emoji_list)} emotes from config")
|
||||
return emoji_list
|
||||
|
||||
def process_board_icon_collection(icon_collection_dir, assets_dir):
|
||||
"""Process emoji_collection parameter"""
|
||||
if not icon_collection_dir:
|
||||
return []
|
||||
|
||||
icon_list = []
|
||||
|
||||
for root, dirs, files in os.walk(icon_collection_dir):
|
||||
for file in files:
|
||||
if file.lower().endswith(('.bin')) or file.lower() == 'listen.eaf':
|
||||
src_file = os.path.join(root, file)
|
||||
dst_file = os.path.join(assets_dir, file)
|
||||
copy_file(src_file, dst_file)
|
||||
|
||||
filename_without_ext = os.path.splitext(file)[0]
|
||||
|
||||
icon_list.append({
|
||||
"name": filename_without_ext,
|
||||
"file": file
|
||||
})
|
||||
|
||||
return icon_list
|
||||
def process_board_layout(layout_json_file, assets_dir):
|
||||
"""Process layout_json parameter"""
|
||||
if not layout_json_file:
|
||||
print(f"Warning: Layout json file not provided")
|
||||
return []
|
||||
|
||||
print(f"Processing layout_json: {layout_json_file}")
|
||||
print(f"assets_dir: {assets_dir}")
|
||||
|
||||
if os.path.isdir(layout_json_file):
|
||||
layout_json_path = os.path.join(layout_json_file, "layout.json")
|
||||
if not os.path.exists(layout_json_path):
|
||||
print(f"Warning: layout.json not found in directory: {layout_json_file}")
|
||||
return []
|
||||
layout_json_file = layout_json_path
|
||||
elif not os.path.isfile(layout_json_file):
|
||||
print(f"Warning: Layout json file not found: {layout_json_file}")
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(layout_json_file, 'r', encoding='utf-8') as f:
|
||||
layout_data = json.load(f)
|
||||
|
||||
# Layout data is now directly an array, no need to get "layout" key
|
||||
layout_items = layout_data if isinstance(layout_data, list) else layout_data.get("layout", [])
|
||||
|
||||
processed_layout = []
|
||||
for item in layout_items:
|
||||
processed_item = {
|
||||
"name": item.get("name", ""),
|
||||
"align": item.get("align", ""),
|
||||
"x": item.get("x", 0),
|
||||
"y": item.get("y", 0)
|
||||
}
|
||||
|
||||
if "width" in item:
|
||||
processed_item["width"] = item["width"]
|
||||
if "height" in item:
|
||||
processed_item["height"] = item["height"]
|
||||
|
||||
processed_layout.append(processed_item)
|
||||
|
||||
print(f"Processed {len(processed_layout)} layout elements")
|
||||
return processed_layout
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading/processing layout.json: {e}")
|
||||
return []
|
||||
|
||||
def process_board_collection(target_board_dir, res_path, assets_dir):
|
||||
"""Process board collection - merge icon, emoji, and layout processing"""
|
||||
|
||||
# Process all collections
|
||||
if os.path.exists(res_path) and os.path.exists(target_board_dir):
|
||||
emoji_collection = process_board_emoji_collection(res_path, target_board_dir, assets_dir)
|
||||
icon_collection = process_board_icon_collection(res_path, assets_dir)
|
||||
layout_json = process_board_layout(target_board_dir, assets_dir)
|
||||
else:
|
||||
print(f"Warning: EAF directory not found: {res_path} or {target_board_dir}")
|
||||
emoji_collection = []
|
||||
icon_collection = []
|
||||
layout_json = []
|
||||
|
||||
return emoji_collection, icon_collection, layout_json
|
||||
|
||||
def generate_index_json(assets_dir, srmodels, text_font, emoji_collection, icon_collection, layout_json):
|
||||
"""Generate index.json file"""
|
||||
index_data = {
|
||||
"version": 1
|
||||
@@ -128,6 +290,12 @@ def generate_index_json(assets_dir, srmodels, text_font, emoji_collection):
|
||||
|
||||
if emoji_collection:
|
||||
index_data["emoji_collection"] = emoji_collection
|
||||
|
||||
if icon_collection:
|
||||
index_data["icon_collection"] = icon_collection
|
||||
|
||||
if layout_json:
|
||||
index_data["layout"] = layout_json
|
||||
|
||||
# Write index.json
|
||||
index_path = os.path.join(assets_dir, "index.json")
|
||||
@@ -148,7 +316,7 @@ def generate_config_json(build_dir, assets_dir):
|
||||
"image_file": os.path.join(workspace_dir, "build/output/assets.bin"),
|
||||
"lvgl_ver": "9.3.0",
|
||||
"assets_size": "0x400000",
|
||||
"support_format": ".png, .gif, .jpg, .bin, .json",
|
||||
"support_format": ".png, .gif, .jpg, .bin, .json, .eaf",
|
||||
"name_length": "32",
|
||||
"split_height": "0",
|
||||
"support_qoi": False,
|
||||
@@ -174,6 +342,9 @@ def main():
|
||||
parser.add_argument('--wakenet_model', help='Path to wakenet model directory')
|
||||
parser.add_argument('--text_font', help='Path to text font file')
|
||||
parser.add_argument('--emoji_collection', help='Path to emoji collection directory')
|
||||
|
||||
parser.add_argument('--res_path', help='Path to res directory')
|
||||
parser.add_argument('--target_board', help='Path to target board directory')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -195,10 +366,16 @@ def main():
|
||||
# Process each parameter
|
||||
srmodels = process_wakenet_model(args.wakenet_model, build_dir, assets_dir)
|
||||
text_font = process_text_font(args.text_font, assets_dir)
|
||||
emoji_collection = process_emoji_collection(args.emoji_collection, assets_dir)
|
||||
|
||||
if(args.target_board):
|
||||
emoji_collection, icon_collection, layout_json = process_board_collection(args.target_board, args.res_path, assets_dir)
|
||||
else:
|
||||
emoji_collection = process_emoji_collection(args.emoji_collection, assets_dir)
|
||||
icon_collection = []
|
||||
layout_json = []
|
||||
|
||||
# Generate index.json
|
||||
generate_index_json(assets_dir, srmodels, text_font, emoji_collection)
|
||||
generate_index_json(assets_dir, srmodels, text_font, emoji_collection, icon_collection, layout_json)
|
||||
|
||||
# Generate config.json
|
||||
config_path = generate_config_json(build_dir, assets_dir)
|
||||
|
||||
@@ -31,7 +31,7 @@ def get_file_path(base_dir, filename):
|
||||
return os.path.join(base_dir, f"{filename}.bin" if not filename.startswith("emojis_") else filename)
|
||||
|
||||
|
||||
def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_dir):
|
||||
def build_assets(wakenet_model, text_font, emoji_collection, target_board, build_dir, final_dir):
|
||||
"""Build assets.bin using build.py with given parameters"""
|
||||
|
||||
# Prepare arguments for build.py
|
||||
@@ -42,14 +42,21 @@ def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_di
|
||||
cmd.extend(["--wakenet_model", wakenet_path])
|
||||
|
||||
if text_font != "none":
|
||||
text_font_path = os.path.join("../../components/xiaozhi-fonts/build", f"{text_font}.bin")
|
||||
text_font_path = os.path.join("../../components/78__xiaozhi-fonts/cbin", f"{text_font}.bin")
|
||||
cmd.extend(["--text_font", text_font_path])
|
||||
|
||||
if emoji_collection != "none":
|
||||
emoji_path = os.path.join("../../components/xiaozhi-fonts/build", emoji_collection)
|
||||
cmd.extend(["--emoji_collection", emoji_path])
|
||||
|
||||
if target_board != "none":
|
||||
res_path = os.path.join("../../managed_components/espressif2022__esp_emote_gfx/emoji_large", "")
|
||||
cmd.extend(["--res_path", res_path])
|
||||
|
||||
target_board_path = os.path.join("../../main/boards/", f"{target_board}")
|
||||
cmd.extend(["--target_board", target_board_path])
|
||||
|
||||
print(f"\n正在构建: {wakenet_model}-{text_font}-{emoji_collection}")
|
||||
print(f"\n正在构建: {wakenet_model}-{text_font}-{emoji_collection}-{target_board}")
|
||||
print(f"执行命令: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
@@ -57,7 +64,10 @@ def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_di
|
||||
result = subprocess.run(cmd, check=True, cwd=os.path.dirname(__file__))
|
||||
|
||||
# Generate output filename
|
||||
output_name = f"{wakenet_model}-{text_font}-{emoji_collection}.bin"
|
||||
if(target_board != "none"):
|
||||
output_name = f"{wakenet_model}-{text_font}-{target_board}.bin"
|
||||
else:
|
||||
output_name = f"{wakenet_model}-{text_font}-{emoji_collection}.bin"
|
||||
|
||||
# Copy generated assets.bin to final directory with new name
|
||||
src_path = os.path.join(build_dir, "assets.bin")
|
||||
@@ -80,6 +90,15 @@ def build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_di
|
||||
|
||||
|
||||
def main():
|
||||
# Parse command line arguments
|
||||
parser = argparse.ArgumentParser(description='构建多个 SPIFFS assets 分区')
|
||||
parser.add_argument('--mode',
|
||||
choices=['emoji_collections', 'emoji_target_boards'],
|
||||
default='emoji_collections',
|
||||
help='选择运行模式: emoji_collections 或 emoji_target_boards (默认: emoji_collections)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Configuration
|
||||
wakenet_models = [
|
||||
"none",
|
||||
@@ -100,6 +119,11 @@ def main():
|
||||
"emojis_32",
|
||||
"emojis_64",
|
||||
]
|
||||
|
||||
emoji_target_boards = [
|
||||
"esp-box-3",
|
||||
"echoear",
|
||||
]
|
||||
|
||||
# Get script directory
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -113,18 +137,33 @@ def main():
|
||||
ensure_dir(final_dir)
|
||||
|
||||
print("开始构建多个 SPIFFS assets 分区...")
|
||||
print(f"运行模式: {args.mode}")
|
||||
print(f"输出目录: {final_dir}")
|
||||
|
||||
# Track successful builds
|
||||
successful_builds = 0
|
||||
total_combinations = len(wakenet_models) * len(text_fonts) * len(emoji_collections)
|
||||
|
||||
# Build all combinations
|
||||
for wakenet_model in wakenet_models:
|
||||
for text_font in text_fonts:
|
||||
for emoji_collection in emoji_collections:
|
||||
if build_assets(wakenet_model, text_font, emoji_collection, build_dir, final_dir):
|
||||
successful_builds += 1
|
||||
if args.mode == 'emoji_collections':
|
||||
# Calculate total combinations for emoji_collections mode
|
||||
total_combinations = len(wakenet_models) * len(text_fonts) * len(emoji_collections)
|
||||
|
||||
# Build all combinations with emoji_collections
|
||||
for wakenet_model in wakenet_models:
|
||||
for text_font in text_fonts:
|
||||
for emoji_collection in emoji_collections:
|
||||
if build_assets(wakenet_model, text_font, emoji_collection, "none", build_dir, final_dir):
|
||||
successful_builds += 1
|
||||
|
||||
elif args.mode == 'emoji_target_boards':
|
||||
# Calculate total combinations for emoji_target_boards mode
|
||||
total_combinations = len(wakenet_models) * len(text_fonts) * len(emoji_target_boards)
|
||||
|
||||
# Build all combinations with emoji_target_boards
|
||||
for wakenet_model in wakenet_models:
|
||||
for text_font in text_fonts:
|
||||
for emoji_target_board in emoji_target_boards:
|
||||
if build_assets(wakenet_model, text_font, "none", emoji_target_board, build_dir, final_dir):
|
||||
successful_builds += 1
|
||||
|
||||
print(f"\n构建完成!")
|
||||
print(f"成功构建: {successful_builds}/{total_combinations}")
|
||||
|
||||
Reference in New Issue
Block a user