78/xiaozhi-esp32/main 274k tokens More Tools
```
├── .github/
   ├── ISSUE_TEMPLATE/
      ├── 01_build_install_bug.yml (700 tokens)
      ├── 02_runtime_bug.yml (700 tokens)
      ├── 03_feature_request.yml (300 tokens)
      ├── config.yml (100 tokens)
   ├── workflows/
      ├── build.yml (200 tokens)
├── .gitignore
├── CMakeLists.txt (100 tokens)
├── LICENSE (omitted)
├── README.md (1200 tokens)
├── README_en.md (1600 tokens)
├── README_ja.md (1300 tokens)
├── docs/
   ├── AtomMatrix-echo-base.jpg
   ├── ESP32-BreadBoard.jpg
   ├── atoms3r-echo-base.jpg
   ├── esp-sparkbot.jpg
   ├── esp32s3-box3.jpg
   ├── lichuang-s3.jpg
   ├── lilygo-t-circle-s3.jpg
   ├── m5stack-cores3.jpg
   ├── magiclick-2p4.jpg
   ├── v1/
      ├── atoms3r.jpg
      ├── espbox3.jpg
      ├── lichuang-s3.jpg
      ├── m5cores3.jpg
      ├── magiclick.jpg
      ├── movecall-cuican-esp32s3.jpg
      ├── movecall-moji-esp32s3.jpg
      ├── sensecap_watcher.jpg
      ├── waveshare.jpg
      ├── wmnologo_xingzhi_0.96.jpg
      ├── wmnologo_xingzhi_1.54.jpg
   ├── waveshare-esp32-s3-touch-amoled-1.8.jpg
   ├── websocket.md (1500 tokens)
   ├── wiring.jpg
   ├── wiring2.jpg
   ├── xmini-c3.jpg
├── main/
   ├── CMakeLists.txt (1700 tokens)
   ├── Kconfig.projbuild (1700 tokens)
   ├── application.cc (6.9k tokens)
   ├── application.h (800 tokens)
   ├── assets/
      ├── common/
         ├── exclamation.p3
         ├── low_battery.p3
         ├── success.p3
         ├── vibration.p3
      ├── en-US/
         ├── 0.p3
         ├── 1.p3
         ├── 2.p3
         ├── 3.p3
         ├── 4.p3
         ├── 5.p3
         ├── 6.p3
         ├── 7.p3
         ├── 8.p3
         ├── 9.p3
         ├── activation.p3
         ├── err_pin.p3
         ├── err_reg.p3
         ├── language.json (400 tokens)
         ├── upgrade.p3
         ├── welcome.p3
         ├── wificonfig.p3
      ├── ja-JP/
         ├── 0.p3
         ├── 1.p3
         ├── 2.p3
         ├── 3.p3
         ├── 4.p3
         ├── 5.p3
         ├── 6.p3
         ├── 7.p3
         ├── 8.p3
         ├── 9.p3
         ├── activation.p3
         ├── err_pin.p3
         ├── err_reg.p3
         ├── language.json (400 tokens)
         ├── upgrade.p3
         ├── welcome.p3
         ├── wificonfig.p3
      ├── zh-CN/
         ├── 0.p3
         ├── 1.p3
         ├── 2.p3
         ├── 3.p3
         ├── 4.p3
         ├── 5.p3
         ├── 6.p3
         ├── 7.p3
         ├── 8.p3
         ├── 9.p3
         ├── activation.p3
         ├── err_pin.p3
         ├── err_reg.p3
         ├── language.json (300 tokens)
         ├── upgrade.p3
         ├── welcome.p3
         ├── wificonfig.p3
      ├── zh-TW/
         ├── 0.p3
         ├── 1.p3
         ├── 2.p3
         ├── 3.p3
         ├── 4.p3
         ├── 5.p3
         ├── 6.p3
         ├── 7.p3
         ├── 8.p3
         ├── 9.p3
         ├── activation.p3
         ├── err_pin.p3
         ├── err_reg.p3
         ├── language.json (300 tokens)
         ├── upgrade.p3
         ├── welcome.p3
         ├── wificonfig.p3
   ├── audio_codecs/
      ├── audio_codec.cc (300 tokens)
      ├── audio_codec.h (300 tokens)
      ├── box_audio_codec.cc (1600 tokens)
      ├── box_audio_codec.h (300 tokens)
      ├── es8311_audio_codec.cc (1300 tokens)
      ├── es8311_audio_codec.h (300 tokens)
      ├── es8374_audio_codec.cc (1300 tokens)
      ├── es8374_audio_codec.h (300 tokens)
      ├── es8388_audio_codec.cc (1400 tokens)
      ├── es8388_audio_codec.h (300 tokens)
      ├── no_audio_codec.cc (2.7k tokens)
      ├── no_audio_codec.h (300 tokens)
   ├── audio_processing/
      ├── afe_audio_processor.cc (900 tokens)
      ├── afe_audio_processor.h (200 tokens)
      ├── audio_processor.h (100 tokens)
      ├── dummy_audio_processor.cc (200 tokens)
      ├── dummy_audio_processor.h (200 tokens)
      ├── wake_word_detect.cc (1300 tokens)
      ├── wake_word_detect.h (300 tokens)
   ├── background_task.cc (400 tokens)
   ├── background_task.h (100 tokens)
   ├── boards/
      ├── README.md (1700 tokens)
      ├── atk-dnesp32s3-box/
         ├── atk_dnesp32s3_box.cc (1700 tokens)
         ├── config.h (200 tokens)
         ├── config.json
      ├── atk-dnesp32s3-box0/
         ├── atk_dnesp32s3_box0.cc (3.2k tokens)
         ├── config.h (400 tokens)
         ├── config.json
         ├── power_manager.h (1200 tokens)
      ├── atk-dnesp32s3/
         ├── atk_dnesp32s3.cc (1200 tokens)
         ├── config.h (200 tokens)
         ├── config.json
      ├── atk-dnesp32s3m-4g/
         ├── atk_dnesp32s3m.cc (1600 tokens)
         ├── config.h (300 tokens)
      ├── atk-dnesp32s3m-wifi/
         ├── atk_dnesp32s3m.cc (1700 tokens)
         ├── config.h (300 tokens)
      ├── atommatrix-echo-base/
         ├── README.md (100 tokens)
         ├── atommatrix_echo_base.cc (800 tokens)
         ├── config.h (200 tokens)
         ├── config.json (100 tokens)
      ├── atoms3-echo-base/
         ├── README.md (100 tokens)
         ├── atoms3_echo_base.cc (1700 tokens)
         ├── config.h (200 tokens)
         ├── config.json (100 tokens)
      ├── atoms3r-cam-m12-echo-base/
         ├── README.md (200 tokens)
         ├── atoms3r_cam_m12_echo_base.cc (1000 tokens)
         ├── config.h (300 tokens)
         ├── config.json (100 tokens)
      ├── atoms3r-echo-base/
         ├── README.md (100 tokens)
         ├── atoms3r_echo_base.cc (2.2k tokens)
         ├── config.h (200 tokens)
         ├── config.json (100 tokens)
      ├── bread-compact-esp32-lcd/
         ├── config.h (1600 tokens)
         ├── config.json (100 tokens)
         ├── esp32_bread_board_lcd.cc (1600 tokens)
      ├── bread-compact-esp32/
         ├── README.md (100 tokens)
         ├── config.h (200 tokens)
         ├── config.json (100 tokens)
         ├── esp32_bread_board.cc (1100 tokens)
      ├── bread-compact-ml307/
         ├── compact_ml307_board.cc (1400 tokens)
         ├── config.h (300 tokens)
         ├── config.json (100 tokens)
      ├── bread-compact-wifi-lcd/
         ├── compact_wifi_board_lcd.cc (1500 tokens)
         ├── config.h (1600 tokens)
      ├── bread-compact-wifi/
         ├── compact_wifi_board.cc (1300 tokens)
         ├── config.h (300 tokens)
         ├── config.json (100 tokens)
      ├── common/
         ├── axp2101.cc (200 tokens)
         ├── axp2101.h (100 tokens)
         ├── backlight.cc (700 tokens)
         ├── backlight.h (200 tokens)
         ├── board.cc (1000 tokens)
         ├── board.h (300 tokens)
         ├── button.cc (800 tokens)
         ├── button.h (200 tokens)
         ├── dual_network_board.cc (600 tokens)
         ├── dual_network_board.h (300 tokens)
         ├── i2c_device.cc (200 tokens)
         ├── i2c_device.h (100 tokens)
         ├── knob.cc (300 tokens)
         ├── knob.h (100 tokens)
         ├── ml307_board.cc (800 tokens)
         ├── ml307_board.h (200 tokens)
         ├── power_save_timer.cc (600 tokens)
         ├── power_save_timer.h (200 tokens)
         ├── sy6970.cc (300 tokens)
         ├── sy6970.h (100 tokens)
         ├── system_reset.cc (400 tokens)
         ├── system_reset.h (100 tokens)
         ├── wifi_board.cc (1100 tokens)
         ├── wifi_board.h (200 tokens)
      ├── df-k10/
         ├── README.md (100 tokens)
         ├── config.h (200 tokens)
         ├── config.json
         ├── df_k10_board.cc (1900 tokens)
         ├── k10_audio_codec.cc (1500 tokens)
         ├── k10_audio_codec.h (300 tokens)
      ├── doit-s3-aibox/
         ├── README.md (100 tokens)
         ├── config.h (200 tokens)
         ├── config.json
         ├── doit_s3_aibox.cc (900 tokens)
      ├── du-chatx/
         ├── config.h (200 tokens)
         ├── config.json
         ├── du-chatx-wifi.cc (1300 tokens)
         ├── power_manager.h (1100 tokens)
      ├── esp-box-3/
         ├── config.h (200 tokens)
         ├── config.json
         ├── esp_box3_board.cc (1300 tokens)
      ├── esp-box-lite/
         ├── box_audio_codec_lite.cc (1900 tokens)
         ├── box_audio_codec_lite.h (300 tokens)
         ├── config.h (200 tokens)
         ├── config.json
         ├── esp_box_lite_board.cc (1800 tokens)
      ├── esp-box/
         ├── config.h (200 tokens)
         ├── config.json
         ├── esp_box_board.cc (1300 tokens)
      ├── esp-s3-lcd-ev-board/
         ├── README.md (100 tokens)
         ├── config.h (300 tokens)
         ├── config.json (100 tokens)
         ├── esp-s3-lcd-ev-board.cc (1500 tokens)
         ├── esp_io_expander_tca9554.c (1100 tokens)
         ├── esp_io_expander_tca9554.h (600 tokens)
         ├── esp_lcd_gc9503.c (4.7k tokens)
         ├── esp_lcd_gc9503.h (1600 tokens)
         ├── pin_config.h (400 tokens)
      ├── esp-sparkbot/
         ├── chassis.cc (600 tokens)
         ├── config.h (400 tokens)
         ├── config.json
         ├── esp_sparkbot_board.cc (1200 tokens)
      ├── esp-spot-s3/
         ├── README.md (200 tokens)
         ├── config.h (200 tokens)
         ├── esp_spot_s3_board.cc (1600 tokens)
      ├── esp32-cgc/
         ├── README.md (100 tokens)
         ├── config.h (1500 tokens)
         ├── config.json (100 tokens)
         ├── esp32_cgc_board.cc (1400 tokens)
      ├── esp32-s3-touch-amoled-1.8/
         ├── board_control.cc (200 tokens)
         ├── config.h (300 tokens)
         ├── config.json
         ├── esp32-s3-touch-amoled-1.8.cc (2.5k tokens)
      ├── esp32-s3-touch-lcd-1.46/
         ├── README.md
         ├── config.h (500 tokens)
         ├── config.json
         ├── esp32-s3-touch-lcd-1.46.cc (2.2k tokens)
      ├── esp32-s3-touch-lcd-1.85/
         ├── README.md
         ├── config.h (500 tokens)
         ├── config.json
         ├── esp32-s3-touch-lcd-1.85.cc (4k tokens)
      ├── esp32-s3-touch-lcd-1.85c/
         ├── README.md
         ├── config.h (500 tokens)
         ├── config.json
         ├── esp32-s3-touch-lcd-1.85c.cc (3.5k tokens)
      ├── esp32-s3-touch-lcd-3.5/
         ├── README.md
         ├── board_control.cc (200 tokens)
         ├── config.h (300 tokens)
         ├── config.json
         ├── esp32-s3-touch-lcd-3.5.cc (2.2k tokens)
      ├── esp32s3-korvo2-v3/
         ├── config.h (300 tokens)
         ├── config.json
         ├── esp32s3_korvo2_v3_board.cc (2.1k tokens)
      ├── kevin-box-1/
         ├── config.h (200 tokens)
         ├── config.json
         ├── kevin_box_board.cc (1400 tokens)
      ├── kevin-box-2/
         ├── config.h (200 tokens)
         ├── config.json
         ├── kevin_box_board.cc (2k tokens)
      ├── kevin-c3/
         ├── config.h (100 tokens)
         ├── config.json
         ├── kevin_c3_board.cc (600 tokens)
         ├── led_strip_control.cc (1000 tokens)
         ├── led_strip_control.h (100 tokens)
      ├── kevin-sp-v3-dev/
         ├── config.h (200 tokens)
         ├── kevin-sp-v3_board.cc (1000 tokens)
      ├── kevin-sp-v4-dev/
         ├── config.h (300 tokens)
         ├── config.json
         ├── kevin-sp-v4_board.cc (1100 tokens)
      ├── kevin-yuying-313lcd/
         ├── config.h (200 tokens)
         ├── config.json
         ├── esp_lcd_gc9503.c (4.5k tokens)
         ├── esp_lcd_gc9503.h (1400 tokens)
         ├── kevin_yuying_313lcd.cc (1300 tokens)
         ├── pin_config.h (300 tokens)
      ├── lichuang-c3-dev/
         ├── README.md
         ├── config.h (300 tokens)
         ├── config.json (100 tokens)
         ├── lichuang_c3_dev_board.cc (1000 tokens)
      ├── lichuang-dev/
         ├── config.h (200 tokens)
         ├── config.json
         ├── lichuang_dev_board.cc (1400 tokens)
      ├── lilygo-t-cameraplus-s3/
         ├── README.md (100 tokens)
         ├── config.h (300 tokens)
         ├── config.json
         ├── lilygo-t-cameraplus-s3.cc (2000 tokens)
         ├── pin_config.h (300 tokens)
         ├── tcamerapluss3_audio_codec.cc (900 tokens)
         ├── tcamerapluss3_audio_codec.h (300 tokens)
      ├── lilygo-t-circle-s3/
         ├── README.md (100 tokens)
         ├── config.h (300 tokens)
         ├── config.json
         ├── esp_lcd_gc9d01n.c (2.9k tokens)
         ├── esp_lcd_gc9d01n.h (800 tokens)
         ├── lilygo-t-circle-s3.cc (1800 tokens)
         ├── pin_config.h (200 tokens)
         ├── tcircles3_audio_codec.cc (1000 tokens)
         ├── tcircles3_audio_codec.h (300 tokens)
      ├── lilygo-t-display-s3-pro-mvsrlora/
         ├── README.md (100 tokens)
         ├── config.h (300 tokens)
         ├── config.json
         ├── lilygo-t-display-s3-pro-mvsrlora.cc (2k tokens)
         ├── pin_config.h (300 tokens)
         ├── tdisplays3promvsrlora_audio_codec.cc (1100 tokens)
         ├── tdisplays3promvsrlora_audio_codec.h (300 tokens)
      ├── m5stack-core-s3/
         ├── README.md (100 tokens)
         ├── config.h (200 tokens)
         ├── config.json
         ├── cores3_audio_codec.cc (1600 tokens)
         ├── cores3_audio_codec.h (300 tokens)
         ├── m5stack_core_s3.cc (2.4k tokens)
      ├── magiclick-2p4/
         ├── config.h (300 tokens)
         ├── config.json
         ├── magiclick_2p4_board.cc (2.1k tokens)
      ├── magiclick-2p5/
         ├── config.h (300 tokens)
         ├── config.json
         ├── magiclick_2p5_board.cc (2.2k tokens)
         ├── power_manager.h (1200 tokens)
      ├── magiclick-c3-v2/
         ├── config.h (200 tokens)
         ├── config.json
         ├── magiclick_c3_v2_board.cc (1900 tokens)
      ├── magiclick-c3/
         ├── config.h (200 tokens)
         ├── config.json
         ├── magiclick_c3_board.cc (1600 tokens)
      ├── mixgo-nova/
         ├── README.md (300 tokens)
         ├── config.h (300 tokens)
         ├── config.json (100 tokens)
         ├── mixgo-nova.cc (1300 tokens)
      ├── movecall-cuican-esp32s3/
         ├── README.md (100 tokens)
         ├── config.h (300 tokens)
         ├── config.json (100 tokens)
         ├── movecall_cuican_esp32s3.cc (1000 tokens)
      ├── movecall-moji-esp32s3/
         ├── README.md (100 tokens)
         ├── config.h (300 tokens)
         ├── config.json
         ├── movecall_moji_esp32s3.cc (1200 tokens)
      ├── sensecap-watcher/
         ├── README.md (100 tokens)
         ├── config.h (700 tokens)
         ├── config.json (100 tokens)
         ├── sensecap_audio_codec.cc (1400 tokens)
         ├── sensecap_audio_codec.h (300 tokens)
         ├── sensecap_watcher.cc (4.5k tokens)
      ├── taiji-pi-s3/
         ├── README.md
         ├── config.h (500 tokens)
         ├── config.json
         ├── taiji_pi_s3.cc (1700 tokens)
      ├── tudouzi/
         ├── config.h (200 tokens)
         ├── config.json (100 tokens)
         ├── kevin_box_board.cc (2000 tokens)
      ├── waveshare-p4-nano/
         ├── README.md (1900 tokens)
         ├── config.h (200 tokens)
         ├── config.json
         ├── esp32-p4-nano.cc (1700 tokens)
      ├── xingzhi-cube-0.85tft-ml307/
         ├── config.h (200 tokens)
         ├── config.json
         ├── xingzhi-cube-0.85tft-ml307.cc (1900 tokens)
      ├── xingzhi-cube-0.85tft-wifi/
         ├── config.h (200 tokens)
         ├── config.json
         ├── xingzhi-cube-0.85tft-wifi.cc (2000 tokens)
      ├── xingzhi-cube-0.96oled-ml307/
         ├── config.h (200 tokens)
         ├── config.json
         ├── xingzhi-cube-0.96oled-ml307.cc (1800 tokens)
      ├── xingzhi-cube-0.96oled-wifi/
         ├── config.h (200 tokens)
         ├── config.json
         ├── xingzhi-cube-0.96oled-wifi.cc (1700 tokens)
      ├── xingzhi-cube-1.54tft-ml307/
         ├── config.h (200 tokens)
         ├── config.json (100 tokens)
         ├── xingzhi-cube-1.54tft-ml307.cc (1800 tokens)
      ├── xingzhi-cube-1.54tft-wifi/
         ├── config.h (200 tokens)
         ├── config.json
         ├── power_manager.h (1100 tokens)
         ├── xingzhi-cube-1.54tft-wifi.cc (1600 tokens)
      ├── xmini-c3/
         ├── config.h (200 tokens)
         ├── config.json
         ├── xmini_c3_board.cc (1500 tokens)
   ├── display/
      ├── display.cc (1800 tokens)
      ├── display.h (500 tokens)
      ├── lcd_display.cc (7.5k tokens)
      ├── lcd_display.h (600 tokens)
      ├── oled_display.cc (2.2k tokens)
      ├── oled_display.h (200 tokens)
   ├── idf_component.yml (200 tokens)
   ├── iot/
      ├── README.md (900 tokens)
      ├── thing.cc (500 tokens)
      ├── thing.h (1900 tokens)
      ├── thing_manager.cc (300 tokens)
      ├── thing_manager.h (200 tokens)
      ├── things/
         ├── battery.cc (200 tokens)
         ├── lamp.cc (300 tokens)
         ├── screen.cc (400 tokens)
         ├── speaker.cc (200 tokens)
   ├── led/
      ├── circular_strip.cc (1500 tokens)
      ├── circular_strip.h (300 tokens)
      ├── gpio_led.cc (1500 tokens)
      ├── gpio_led.h (300 tokens)
      ├── led.h (100 tokens)
      ├── single_led.cc (900 tokens)
      ├── single_led.h (200 tokens)
   ├── main.cc (100 tokens)
   ├── ota.cc (3k tokens)
   ├── ota.h (400 tokens)
   ├── protocols/
      ├── mqtt_protocol.cc (2.2k tokens)
      ├── mqtt_protocol.h (300 tokens)
      ├── protocol.cc (900 tokens)
      ├── protocol.h (600 tokens)
      ├── websocket_protocol.cc (1700 tokens)
      ├── websocket_protocol.h (200 tokens)
   ├── settings.cc (500 tokens)
   ├── settings.h (100 tokens)
   ├── system_info.cc (800 tokens)
   ├── system_info.h (100 tokens)
├── partitions.csv (100 tokens)
├── partitions_32M_sensecap.csv (100 tokens)
├── partitions_4M.csv (100 tokens)
├── partitions_8M.csv (100 tokens)
├── scripts/
   ├── Image_Converter/
      ├── LVGLImage.py (9.2k tokens)
      ├── README.md (100 tokens)
      ├── lvgl_tools_gui.py (2000 tokens)
   ├── flash.sh
   ├── gen_lang.py (600 tokens)
   ├── p3_tools/
      ├── README.md (300 tokens)
      ├── batch_convert_gui.py (1800 tokens)
      ├── convert_audio_to_p3.py (600 tokens)
      ├── convert_p3_to_audio.py (300 tokens)
      ├── img/
         ├── img.png
      ├── p3_gui_player.py (1600 tokens)
      ├── play_p3.py (400 tokens)
      ├── requirements.txt
   ├── release.py (900 tokens)
   ├── versions.py (1400 tokens)
├── sdkconfig.defaults (300 tokens)
├── sdkconfig.defaults.esp32c3
├── sdkconfig.defaults.esp32p4 (100 tokens)
├── sdkconfig.defaults.esp32s3 (100 tokens)
```


## /.github/ISSUE_TEMPLATE/01_build_install_bug.yml

```yml path="/.github/ISSUE_TEMPLATE/01_build_install_bug.yml" 
name: Installation or build bug report
description: Report installation or build bugs
labels: ['bug']
body:
  - type: checkboxes
    id: checklist
    attributes:
      label: Answers checklist.
      description: Before submitting a new issue, please follow the checklist and try to find the answer.
      options:
        - label: I have read the documentation [XiaoZhi AI Programming Guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb) and the issue is not addressed there.
          required: true
        - label: I have updated my branch (master or release) to the latest version and checked that the issue is present there.
          required: true
        - label: I have searched the issue tracker for a similar issue and not found a similar issue.
          required: true
  - type: input
    id: xiaozhi_ai_version
    attributes:
      label: XiaoZhi AI version.
      description: On which XiaoZhi AI version does this issue occur on? Run `git describe --tags` to find it.
      placeholder: ex. v1.1.0-44-g140aab8
    validations:
      required: true
  - type: dropdown
    id: operating_system
    attributes:
      label: Operating System used.
      multiple: false
      options:
        - Windows
        - Linux
        - macOS
    validations:
      required: true
  - type: dropdown
    id: build
    attributes:
      label: How did you build your project?
      multiple: false
      options:
        - Command line with CMake
        - Command line with idf.py
        - CLion IDE
        - VS Code IDE/Cursor
        - Other (please specify in More Information)
    validations:
      required: true
  - type: dropdown
    id: windows_comand_line
    attributes:
      label: If you are using Windows, please specify command line type.
      multiple: false
      options:
        - PowerShell
        - CMD
    validations:
      required: false
  - type: textarea
    id: expected
    attributes:
      label: What is the expected behavior?
      description: Please provide a clear and concise description of the expected behavior.
      placeholder: I expected it to...
    validations:
      required: true
  - type: textarea
    id: actual
    attributes:
      label: What is the actual behavior?
      description: Please describe actual behavior.
      placeholder: Instead it...
    validations:
      required: true
  - type: textarea
    id: steps
    attributes:
      label: Steps to reproduce.
      description: 'How do you trigger this bug? Please walk us through it step by step. If this is build bug, please attach sdkconfig file (from your project folder). Please attach your code here.'
      value: |
        1. Step
        2. Step
        3. Step
        ...
    validations:
      required: true
  - type: textarea
    id: debug_logs
    attributes:
      label: Build or installation Logs.
      description: Build or installation log goes here, should contain the backtrace, as well as the reset source if it is a crash.
      placeholder: Your log goes here.
      render: plain
    validations:
      required: false
  - type: textarea
    id: more-info
    attributes:
      label: More Information.
      description: Do you have any other information from investigating this?
      placeholder: ex. Any more.
    validations:
      required: false
```

## /.github/ISSUE_TEMPLATE/02_runtime_bug.yml

```yml path="/.github/ISSUE_TEMPLATE/02_runtime_bug.yml" 
name: Runtime bug report
description: Report runtime bugs
labels: ['bug']
body:
  - type: checkboxes
    id: checklist
    attributes:
      label: Answers checklist.
      description: Before submitting a new issue, please follow the checklist and try to find the answer.
      options:
        - label: I  have read the documentation [XiaoZhi AI Programming Guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb) and the issue is not addressed there.
          required: true
        - label: I have updated my firmware to the latest version and checked that the issue is present there.
          required: true
        - label: I have searched the issue tracker for a similar issue and not found a similar issue.
          required: true
  - type: input
    id: xiaozhi_ai_firmware_version
    attributes:
      label: XiaoZhi AI firmware version.
      description: On which firmware version does this issue occur on?
      placeholder: ex. v1.2.1_bread-compact-wifi
    validations:
      required: true
  - type: dropdown
    id: operating_system
    attributes:
      label: Operating System used.
      multiple: false
      options:
        - Windows
        - Linux
        - macOS
    validations:
      required: true
  - type: dropdown
    id: build
    attributes:
      label: How did you build your project?
      multiple: false
      options:
        - Command line with CMake
        - Command line with idf.py
        - CLion IDE
        - VS Code IDE/Cursor
        - Other (please specify in More Information)
    validations:
      required: true
  - type: dropdown
    id: windows_comand_line
    attributes:
      label: If you are using Windows, please specify command line type.
      multiple: false
      options:
        - PowerShell
        - CMD
    validations:
      required: false
  - type: dropdown
    id: power_supply
    attributes:
      label: Power Supply used.
      multiple: false
      options:
        - USB
        - External 5V
        - External 3.3V
        - Battery
    validations:
      required: true
  - type: textarea
    id: expected
    attributes:
      label: What is the expected behavior?
      description: Please provide a clear and concise description of the expected behavior.
      placeholder: I expected it to...
    validations:
      required: true
  - type: textarea
    id: actual
    attributes:
      label: What is the actual behavior?
      description: Please describe actual behavior.
      placeholder: Instead it...
    validations:
      required: true
  - type: textarea
    id: steps
    attributes:
      label: Steps to reproduce.
      description: 'How do you trigger this bug? Please walk us through it step by step. Please attach your code here.'
      value: |
        1. Step
        2. Step
        3. Step
        ...
    validations:
      required: true
  - type: textarea
    id: debug_logs
    attributes:
      label: Debug Logs.
      description: Debug log goes here, should contain the backtrace, as well as the reset source if it is a crash.
      placeholder: Your log goes here.
      render: plain
    validations:
      required: false
  - type: textarea
    id: more-info
    attributes:
      label: More Information.
      description: Do you have any other information from investigating this?
      placeholder: ex. Any more.
    validations:
      required: false
```

## /.github/ISSUE_TEMPLATE/03_feature_request.yml

```yml path="/.github/ISSUE_TEMPLATE/03_feature_request.yml" 
name: Feature request
description: Suggest an idea for this project.
labels: ['enhancement']
body:
  - type: markdown
    attributes:
      value: |
        * We welcome any ideas or feature requests! It’s helpful if you can explain exactly why the feature would be useful.
        * There are usually some outstanding feature requests in the [existing issues list](https://github.com/78/xiaozhi-esp32/labels/enhancement), feel free to add comments to them.
        * If you would like to contribute, please read the [contributions guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb).
  - type: textarea
    id: problem-related
    attributes:
      label: Is your feature request related to a problem?
      description: Please provide a clear and concise description of what the problem is.
      placeholder: ex. I'm always frustrated when ...
  - type: textarea
    id: solution
    attributes:
      label: Describe the solution you'd like.
      description: Please provide a clear and concise description of what you want to happen.
      placeholder: ex. When using XiaoZhi ...
  - type: textarea
    id: alternatives
    attributes:
      label: Describe alternatives you've considered.
      description: Please provide a clear and concise description of any alternative solutions or features you've considered.
      placeholder: ex. Choosing other approach wouldn't work, because ...
  - type: textarea
    id: context
    attributes:
      label: Additional context.
      description: Please add any other context or screenshots about the feature request here.
      placeholder: ex. This would work only when ...
```

## /.github/ISSUE_TEMPLATE/config.yml

```yml path="/.github/ISSUE_TEMPLATE/config.yml" 
blank_issues_enabled: true
contact_links:
  - name: 小智 AI 官方网站
    url: https://xiaozhi.me/
    about: 激活设备、配置 AI、声纹识别、声音克隆等应有尽有,DIY 属于你自己的小智
  - name: 小智 AI 聊天机器人百科全书
    url: https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb
    about: 开发文档、硬件制作、烧录教程、FAQ尽在小智百科
```

## /.github/workflows/build.yml

```yml path="/.github/workflows/build.yml" 
name: Build and Test

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Espressif IoT Development Framework (ESP-IDF)
        # You may pin to the exact commit or the version.
        # uses: espressif/esp-idf-ci-action@8cd22ae10042fadc37890e81e9988a9113e7b506
        uses: espressif/esp-idf-ci-action@v1.1.0
        with:
          # Relative path under $GITHUB_WORKSPACE to place the repository
          #path: # optional, default is 
          # Version of ESP-IDF docker image to use
          esp_idf_version: release-v5.4
          # ESP32 variant to build for
          target: esp32s3
          # Command to run inside the docker container (default: builds the project)
          # command: # optional, default is idf.py build
                

```

## /.gitignore

```gitignore path="/.gitignore" 
tmp/
components/
managed_components/
build/
.vscode/
.devcontainer/
sdkconfig.old
sdkconfig
dependencies.lock
.env
releases/
main/assets/lang_config.h
.DS_Store
.cache
```

## /CMakeLists.txt

# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)

set(PROJECT_VER "1.6.2")

# Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(xiaozhi)



## /README.md

# 小智 AI 聊天机器人 (XiaoZhi AI Chatbot)

(中文 | [English](README_en.md) | [日本語](README_ja.md))

## 视频介绍

👉 [ESP32+SenseVoice+Qwen72B打造你的AI聊天伴侣!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/)

👉 [给小智装上 DeepSeek 的聪明大脑【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/)

👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)

## 项目目的

本项目是由虾哥开源的一个开源项目,以 MIT 许可证发布,允许任何人免费使用,并可以用于商业用途。

我们希望通过这个项目,能够帮助更多人入门 AI 硬件开发,了解如何将当下飞速发展的大语言模型应用到实际的硬件设备中。无论你是对 AI 感兴趣的学生,还是想要探索新技术的开发者,都可以通过这个项目获得宝贵的学习经验。

欢迎所有人参与到项目的开发和改进中来。如果你有任何想法或建议,请随时提出 Issue 或加入群聊。

学习交流 QQ 群:376893254

## 已实现功能

- Wi-Fi / ML307 Cat.1 4G
- BOOT 键唤醒和打断,支持点击和长按两种触发方式
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
- 流式语音对话(WebSocket 或 UDP 协议)
- 支持国语、粤语、英语、日语、韩语 5 种语言识别 [SenseVoice](https://github.com/FunAudioLLM/SenseVoice)
- 声纹识别,识别是谁在喊 AI 的名字 [3D Speaker](https://github.com/modelscope/3D-Speaker)
- 大模型 TTS(火山引擎 或 CosyVoice)
- 大模型 LLM(Qwen, DeepSeek, Doubao)
- 可配置的提示词和音色(自定义角色)
- 短期记忆,每轮对话后自我总结
- OLED / LCD 显示屏,显示信号强弱或对话内容
- 支持 LCD 显示图片表情
- 支持多语言(中文、英文)

## 硬件部分

### 面包板手工制作实践

详见飞书文档教程:

👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)

面包板效果图如下:

![面包板效果图](docs/wiring2.jpg)

### 已支持的开源硬件

- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">AtomS3R + Echo Base</a>
- <a href="https://docs.m5stack.com/en/core/ATOM%20Matrix" target="_blank" title="AtomMatrix + Echo Base">AtomMatrix + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a>
- <a href="https://oshwhub.com/movecall/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Moji 小智AI衍生版</a>
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">璀璨·AI吊坠</a>
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="无名科技Nologo-星智-1.54">无名科技Nologo-星智-1.54TFT</a>
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
<div style="display: flex; justify-content: space-between;">
  <a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板">
    <img src="docs/v1/lichuang-s3.jpg" width="240" />
  </a>
  <a href="docs/v1/espbox3.jpg" target="_blank" title="乐鑫 ESP32-S3-BOX3">
    <img src="docs/v1/espbox3.jpg" width="240" />
  </a>
  <a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
    <img src="docs/v1/m5cores3.jpg" width="240" />
  </a>
  <a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
    <img src="docs/v1/atoms3r.jpg" width="240" />
  </a>
  <a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4">
    <img src="docs/v1/magiclick.jpg" width="240" />
  </a>
  <a href="docs/v1/waveshare.jpg" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">
    <img src="docs/v1/waveshare.jpg" width="240" />
  </a>
  <a href="docs/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
    <img src="docs/lilygo-t-circle-s3.jpg" width="240" />
  </a>
  <a href="docs/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
    <img src="docs/xmini-c3.jpg" width="240" />
  </a>
  <a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Movecall Moji 小智AI衍生版">
    <img src="docs/v1/movecall-moji-esp32s3.jpg" width="240" />
  </a>
  <a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
    <img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
  </a>
  <a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="无名科技Nologo-星智-1.54">
    <img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
  </a>
  <a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
    <img src="docs/v1/sensecap_watcher.jpg" width="240" />
  </a>
</div>

## 固件部分

### 免开发环境烧录

新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。

固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,目前个人用户注册账号可以免费使用 Qwen 实时模型。

👉 [Flash烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) 


### 开发环境

- Cursor 或 VSCode
- 安装 ESP-IDF 插件,选择 SDK 版本 5.3 或以上
- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
- 使用 Google C++ 代码风格,提交代码时请确保符合规范

### 开发者文档

- [开发板定制指南](main/boards/README.md) - 学习如何为小智创建自定义开发板适配
- [物联网控制模块](main/iot/README.md) - 了解如何通过AI语音控制物联网设备


## 智能体配置

如果你已经拥有一个小智 AI 聊天机器人设备,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。

👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)

## 技术原理与私有化部署

👉 [一份详细的 WebSocket 通信协议文档](docs/websocket.md)

在个人电脑上部署服务器,可以参考另一位作者同样以 MIT 许可证开源的项目 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server)

## Star History

<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
 </picture>
</a>


## /README_en.md

# XiaoZhi AI Chatbot

([中文](README.md) | English | [日本語](README_ja.md))

## Introduction

👉 [Build your AI chat companion with ESP32+SenseVoice+Qwen72B!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/)

👉 [Equipping XiaoZhi with DeepSeek's smart brain【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/)

👉 [Build your own AI companion, a beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)

## Project Purpose

This is an open-source project released under the MIT license, allowing anyone to use it freely, including for commercial purposes.

Through this project, we aim to help more people get started with AI hardware development and understand how to implement rapidly evolving large language models in actual hardware devices. Whether you're a student interested in AI or a developer exploring new technologies, this project offers valuable learning experiences.

Everyone is welcome to participate in the project's development and improvement. If you have any ideas or suggestions, please feel free to raise an Issue or join the chat group.

Learning & Discussion QQ Group: 376893254

## Implemented Features

- Wi-Fi / ML307 Cat.1 4G
- BOOT button wake-up and interruption, supporting both click and long-press triggers
- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
- Streaming voice dialogue (WebSocket or UDP protocol)
- Support for 5 languages: Mandarin, Cantonese, English, Japanese, Korean [SenseVoice](https://github.com/FunAudioLLM/SenseVoice)
- Voice print recognition to identify who's calling AI's name [3D Speaker](https://github.com/modelscope/3D-Speaker)
- Large model TTS (Volcano Engine or CosyVoice)
- Large Language Models (Qwen, DeepSeek, Doubao)
- Configurable prompts and voice tones (custom characters)
- Short-term memory, self-summarizing after each conversation round
- OLED / LCD display showing signal strength or conversation content
- Support for LCD image expressions
- Multi-language support (Chinese, English)

## Hardware Section

### Breadboard DIY Practice

See the Feishu document tutorial:

👉 [XiaoZhi AI Chatbot Encyclopedia](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)

Breadboard demonstration:

![Breadboard Demo](docs/wiring2.jpg)

### Supported Open Source Hardware

- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">AtomS3R + Echo Base</a>
- <a href="https://docs.m5stack.com/en/core/ATOM%20Matrix" target="_blank" title="AtomMatrix + Echo Base">AtomMatrix + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="Magic Button 2.4">Magic Button 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a>
- <a href="https://oshwhub.com/movecall/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Moji XiaoZhi AI Derivative Version</a>
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AI pendant</a>
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="WMnologo-Xingzhi-1.54">WMnologo-Xingzhi-1.54TFT</a>
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>

<div style="display: flex; justify-content: space-between;">
  <a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
    <img src="docs/v1/lichuang-s3.jpg" width="240" />
  </a>
  <a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
    <img src="docs/v1/espbox3.jpg" width="240" />
  </a>
  <a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
    <img src="docs/v1/m5cores3.jpg" width="240" />
  </a>
  <a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
    <img src="docs/v1/atoms3r.jpg" width="240" />
  </a>
  <a href="docs/AtomMatrix-echo-base.jpg" target="_blank" title="AtomMatrix-echo-base + Echo Base">
    <img src="docs/AtomMatrix-echo-base.jpg" width="240" />
  </a>  
  <a href="docs/v1/magiclick.jpg" target="_blank" title="MagiClick 2.4">
    <img src="docs/v1/magiclick.jpg" width="240" />
  </a>
  <a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
    <img src="docs/v1/waveshare.jpg" width="240" />
  </a>
  <a href="docs/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
    <img src="docs/lilygo-t-circle-s3.jpg" width="240" />
  </a>
  <a href="docs/xmini-c3.jpg" target="_blank" title="Xmini C3">
    <img src="docs/xmini-c3.jpg" width="240" />
  </a>
  <a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Moji">
    <img src="docs/v1/movecall-moji-esp32s3.jpg" width="240" />
  </a>
  <a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
    <img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
  </a>
  <a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="WMnologo-Xingzhi-1.54">
    <img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
  </a>
  <a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
    <img src="docs/v1/sensecap_watcher.jpg" width="240" />
  </a>
</div>

## Firmware Section

### Flashing Without Development Environment

For beginners, it's recommended to first use the firmware that can be flashed without setting up a development environment.

The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by default. Currently, personal users can register an account to use the Qwen real-time model for free.

👉 [Flash Firmware Guide (No IDF Environment)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)

### Development Environment

- Cursor or VSCode
- Install ESP-IDF plugin, select SDK version 5.3 or above
- Linux is preferred over Windows for faster compilation and fewer driver issues
- Use Google C++ code style, ensure compliance when submitting code

### Developer Documentation

- [Board Customization Guide](main/boards/README.md) - Learn how to create custom board adaptations for XiaoZhi
- [IoT Control Module](main/iot/README.md) - Understand how to control IoT devices through AI voice commands

## AI Agent Configuration

If you already have a XiaoZhi AI chatbot device, you can configure it through the [xiaozhi.me](https://xiaozhi.me) console.

👉 [Backend Operation Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/)

## Technical Principles and Private Deployment

👉 [Detailed WebSocket Communication Protocol Documentation](docs/websocket.md)

For server deployment on personal computers, refer to another MIT-licensed project [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server)

## Star History

<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
 </picture>
</a> 


## /README_ja.md

# シャオジー AI チャットボット

([中文](README.md) | [English](README_en.md) | 日本語)

## プロジェクト紹介

👉 [ESP32+SenseVoice+Qwen72Bで AI チャット仲間を作ろう!【bilibili】](https://www.bilibili.com/video/BV11msTenEH3/)

👉 [シャオジーに DeepSeek のスマートな頭脳を搭載【bilibili】](https://www.bilibili.com/video/BV1GQP6eNEFG/)

👉 [自分だけの AI パートナーを作る、初心者向けガイド【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)

## プロジェクトの目的

このプロジェクトは MIT ライセンスの下で公開されているオープンソースプロジェクトで、商用利用を含め、誰でも自由に使用することができます。

このプロジェクトを通じて、より多くの人々が AI ハードウェア開発を始め、急速に進化している大規模言語モデルを実際のハードウェアデバイスに実装する方法を理解できるようになることを目指しています。AI に興味のある学生でも、新しい技術を探求する開発者でも、このプロジェクトから貴重な学習経験を得ることができます。

プロジェクトの開発と改善には誰でも参加できます。アイデアや提案がありましたら、Issue を立てるかチャットグループにご参加ください。

学習・交流 QQ グループ:376893254

## 実装済みの機能

- Wi-Fi / ML307 Cat.1 4G
- BOOT ボタンによる起動と中断、クリックと長押しの2種類のトリガーに対応
- オフライン音声起動 [ESP-SR](https://github.com/espressif/esp-sr)
- ストリーミング音声対話(WebSocket または UDP プロトコル)
- 5言語対応:標準中国語、広東語、英語、日本語、韓国語 [SenseVoice](https://github.com/FunAudioLLM/SenseVoice)
- 話者認識、AI の名前を呼んでいる人を識別 [3D Speaker](https://github.com/modelscope/3D-Speaker)
- 大規模モデル TTS(Volcano Engine または CosyVoice)
- 大規模言語モデル(Qwen, DeepSeek, Doubao)
- 設定可能なプロンプトと音声トーン(カスタムキャラクター)
- 短期記憶、各会話ラウンド後の自己要約
- OLED / LCD ディスプレイ、信号強度や会話内容を表示
- LCD での画像表情表示に対応
- 多言語対応(中国語、英語)

## ハードウェア部分

### ブレッドボード DIY 実践

Feishu ドキュメントチュートリアルをご覧ください:

👉 [シャオジー AI チャットボット百科事典](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)

ブレッドボードのデモ:

![ブレッドボードデモ](docs/wiring2.jpg)

### サポートされているオープンソースハードウェア

- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 開発ボード">LiChuang ESP32-S3 開発ボード</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">AtomS3R + Echo Base</a>
- <a href="https://docs.m5stack.com/en/core/ATOM%20Matrix" target="_blank" title="AtomMatrix + Echo Base">AtomMatrix + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="マジックボタン 2.4">マジックボタン 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a>
- <a href="https://oshwhub.com/movecall/moji-xiaozhi-ai-derivative-editi" target="_blank" title="Movecall Moji ESP32S3">Moji シャオジー AI 派生版</a>
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">Cuican AI ペンダント</a>
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="無名科技Nologo-星智-1.54">無名科技Nologo-星智-1.54TFT</a>
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>

<div style="display: flex; justify-content: space-between;">
  <a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 開発ボード">
    <img src="docs/v1/lichuang-s3.jpg" width="240" />
  </a>
  <a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
    <img src="docs/v1/espbox3.jpg" width="240" />
  </a>
  <a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
    <img src="docs/v1/m5cores3.jpg" width="240" />
  </a>
  <a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
    <img src="docs/v1/atoms3r.jpg" width="240" />
  </a>
  <a href="docs/v1/magiclick.jpg" target="_blank" title="MagiClick 2.4">
    <img src="docs/v1/magiclick.jpg" width="240" />
  </a>
  <a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
    <img src="docs/v1/waveshare.jpg" width="240" />
  </a>
  <a href="docs/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
    <img src="docs/lilygo-t-circle-s3.jpg" width="240" />
  </a>
  <a href="docs/xmini-c3.jpg" target="_blank" title="Xmini C3">
    <img src="docs/xmini-c3.jpg" width="240" />
  </a>
  <a href="docs/v1/movecall-moji-esp32s3.jpg" target="_blank" title="Moji">
    <img src="docs/v1/movecall-moji-esp32s3.jpg" width="240" />
  </a>
  <a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
    <img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
  </a>
  <a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="無名科技Nologo-星智-1.54">
    <img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
  </a>
  <a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
    <img src="docs/v1/sensecap_watcher.jpg" width="240" />
  </a>
</div>

## ファームウェア部分

### 開発環境なしのフラッシュ

初心者の方は、まず開発環境のセットアップなしでフラッシュできるファームウェアを使用することをお勧めします。

ファームウェアはデフォルトで公式 [xiaozhi.me](https://xiaozhi.me) サーバーに接続します。現在、個人ユーザーはアカウントを登録することで、Qwen リアルタイムモデルを無料で使用できます。

👉 [フラッシュファームウェアガイド(IDF環境なし)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)

### 開発環境

- Cursor または VSCode
- ESP-IDF プラグインをインストール、SDK バージョン 5.3 以上を選択
- Linux は Windows より好ましい(コンパイルが速く、ドライバーの問題も少ない)
- Google C++ コードスタイルを使用、コード提出時にはコンプライアンスを確認

### 開発者ドキュメント

- [ボードカスタマイズガイド](main/boards/README.md) - シャオジー向けのカスタムボード適応を作成する方法を学ぶ
- [IoT 制御モジュール](main/iot/README.md) - AI 音声コマンドでIoTデバイスを制御する方法を理解する

## AI エージェント設定

シャオジー AI チャットボットデバイスをお持ちの場合は、[xiaozhi.me](https://xiaozhi.me) コンソールで設定できます。

👉 [バックエンド操作チュートリアル(旧インターフェース)](https://www.bilibili.com/video/BV1jUCUY2EKM/)

## 技術原理とプライベートデプロイメント

👉 [詳細な WebSocket 通信プロトコルドキュメント](docs/websocket.md)

個人のコンピュータでのサーバーデプロイメントについては、同じく MIT ライセンスで公開されている別のプロジェクト [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) を参照してください。

## スター履歴

<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
 </picture>
</a> 


## /docs/AtomMatrix-echo-base.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/AtomMatrix-echo-base.jpg

## /docs/ESP32-BreadBoard.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/ESP32-BreadBoard.jpg

## /docs/atoms3r-echo-base.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/atoms3r-echo-base.jpg

## /docs/esp-sparkbot.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/esp-sparkbot.jpg

## /docs/esp32s3-box3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/esp32s3-box3.jpg

## /docs/lichuang-s3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/lichuang-s3.jpg

## /docs/lilygo-t-circle-s3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/lilygo-t-circle-s3.jpg

## /docs/m5stack-cores3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/m5stack-cores3.jpg

## /docs/magiclick-2p4.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/magiclick-2p4.jpg

## /docs/v1/atoms3r.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/atoms3r.jpg

## /docs/v1/espbox3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/espbox3.jpg

## /docs/v1/lichuang-s3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/lichuang-s3.jpg

## /docs/v1/m5cores3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/m5cores3.jpg

## /docs/v1/magiclick.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/magiclick.jpg

## /docs/v1/movecall-cuican-esp32s3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/movecall-cuican-esp32s3.jpg

## /docs/v1/movecall-moji-esp32s3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/movecall-moji-esp32s3.jpg

## /docs/v1/sensecap_watcher.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/sensecap_watcher.jpg

## /docs/v1/waveshare.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/waveshare.jpg

## /docs/v1/wmnologo_xingzhi_0.96.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/wmnologo_xingzhi_0.96.jpg

## /docs/v1/wmnologo_xingzhi_1.54.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/v1/wmnologo_xingzhi_1.54.jpg

## /docs/waveshare-esp32-s3-touch-amoled-1.8.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/waveshare-esp32-s3-touch-amoled-1.8.jpg

## /docs/websocket.md

以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述客户端(设备)与服务器之间如何通过 WebSocket 进行交互。该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。

---

## 1. 总体流程概览

1. **设备端初始化**  
   - 设备上电、初始化 `Application`:  
     - 初始化音频编解码器、显示屏、LED 等  
     - 连接网络  
     - 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol`)  
   - 进入主循环等待事件(音频输入、音频输出、调度任务等)。

2. **建立 WebSocket 连接**  
   - 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()`:  
     - 根据配置获取 WebSocket URL
     - 设置若干请求头(`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`)  
     - 调用 `Connect()` 与服务器建立 WebSocket 连接  

3. **发送客户端 “hello” 消息**  
   - 连接成功后,设备会发送一条 JSON 消息,示例结构如下:  
   ```json
   {
     "type": "hello",
     "version": 1,
     "transport": "websocket",
     "audio_params": {
       "format": "opus",
       "sample_rate": 16000,
       "channels": 1,
       "frame_duration": 60
     }
   }
   ```
   - 其中 `"frame_duration"` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms)。

4. **服务器回复 “hello”**  
   - 设备等待服务器返回一条包含 `"type": "hello"` 的 JSON 消息,并检查 `"transport": "websocket"` 是否匹配。  
   - 如果匹配,则认为服务器已就绪,标记音频通道打开成功。  
   - 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。

5. **后续消息交互**  
   - 设备端和服务器端之间可发送两种主要类型的数据:  
     1. **二进制音频数据**(Opus 编码)  
     2. **文本 JSON 消息**(用于传输聊天状态、TTS/STT 事件、IoT 命令等)  

   - 在代码里,接收回调主要分为:  
     - `OnData(...)`:  
       - 当 `binary` 为 `true` 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。  
       - 当 `binary` 为 `false` 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理(见下文消息结构)。  

   - 当服务器或网络出现断连,回调 `OnDisconnected()` 被触发:  
     - 设备会调用 `on_audio_channel_closed_()`,并最终回到空闲状态。

6. **关闭 WebSocket 连接**  
   - 设备在需要结束语音会话时,会调用 `CloseAudioChannel()` 主动断开连接,并回到空闲状态。  
   - 或者如果服务器端主动断开,也会引发同样的回调流程。

---

## 2. 通用请求头

在建立 WebSocket 连接时,代码示例中设置了以下请求头:

- `Authorization`: 用于存放访问令牌,形如 `"Bearer <token>"`  
- `Protocol-Version`: 固定示例中为 `"1"`  
- `Device-Id`: 设备物理网卡 MAC 地址  
- `Client-Id`: 设备 UUID(可在应用中唯一标识设备)

这些头会随着 WebSocket 握手一起发送到服务器,服务器可根据需求进行校验、认证等。

---

## 3. JSON 消息结构

WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。

### 3.1 客户端→服务器

1. **Hello**  
   - 连接成功后,由客户端发送,告知服务器基本参数。  
   - 例:
     ```json
     {
       "type": "hello",
       "version": 1,
       "transport": "websocket",
       "audio_params": {
         "format": "opus",
         "sample_rate": 16000,
         "channels": 1,
         "frame_duration": 60
       }
     }
     ```

2. **Listen**  
   - 表示客户端开始或停止录音监听。  
   - 常见字段:  
     - `"session_id"`:会话标识  
     - `"type": "listen"`  
     - `"state"`:`"start"`, `"stop"`, `"detect"`(唤醒检测已触发)  
     - `"mode"`:`"auto"`, `"manual"` 或 `"realtime"`,表示识别模式。  
   - 例:开始监听  
     ```json
     {
       "session_id": "xxx",
       "type": "listen",
       "state": "start",
       "mode": "manual"
     }
     ```

3. **Abort**  
   - 终止当前说话(TTS 播放)或语音通道。  
   - 例:
     ```json
     {
       "session_id": "xxx",
       "type": "abort",
       "reason": "wake_word_detected"
     }
     ```
   - `reason` 值可为 `"wake_word_detected"` 或其他。

4. **Wake Word Detected**  
   - 用于客户端向服务器告知检测到唤醒词。  
   - 例:
     ```json
     {
       "session_id": "xxx",
       "type": "listen",
       "state": "detect",
       "text": "你好小明"
     }
     ```

5. **IoT**  
   - 发送当前设备的物联网相关信息:  
     - **Descriptors**(描述设备功能、属性等)  
     - **States**(设备状态的实时更新)  
   - 例:  
     ```json
     {
       "session_id": "xxx",
       "type": "iot",
       "descriptors": { ... }
     }
     ```
     或
     ```json
     {
       "session_id": "xxx",
       "type": "iot",
       "states": { ... }
     }
     ```

---

### 3.2 服务器→客户端

1. **Hello**  
   - 服务器端返回的握手确认消息。  
   - 必须包含 `"type": "hello"` 和 `"transport": "websocket"`。  
   - 可能会带有 `audio_params`,表示服务器期望的音频参数,或与客户端对齐的配置。  
   - 成功接收后客户端会设置事件标志,表示 WebSocket 通道就绪。

2. **STT**  
   - `{"type": "stt", "text": "..."}`
   - 表示服务器端识别到了用户语音。(例如语音转文本结果)  
   - 设备可能将此文本显示到屏幕上,后续再进入回答等流程。

3. **LLM**  
   - `{"type": "llm", "emotion": "happy", "text": "😀"}`
   - 服务器指示设备调整表情动画 / UI 表达。  

4. **TTS**  
   - `{"type": "tts", "state": "start"}`:服务器准备下发 TTS 音频,客户端进入 “speaking” 播放状态。  
   - `{"type": "tts", "state": "stop"}`:表示本次 TTS 结束。  
   - `{"type": "tts", "state": "sentence_start", "text": "..."}`
     - 让设备在界面上显示当前要播放或朗读的文本片段(例如用于显示给用户)。  

5. **IoT**  
   - `{"type": "iot", "commands": [ ... ]}`
   - 服务器向设备发送物联网的动作指令,设备解析并执行(如打开灯、设置温度等)。

6. **音频数据:二进制帧**  
   - 当服务器发送音频二进制帧(Opus 编码)时,客户端解码并播放。  
   - 若客户端正在处于 “listening” (录音)状态,收到的音频帧会被忽略或清空以防冲突。

---

## 4. 音频编解码

1. **客户端发送录音数据**  
   - 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。  
   - 如果客户端每次编码生成的二进制帧大小为 N 字节,则会通过 WebSocket 的 **binary** 消息发送这块数据。

2. **客户端播放收到的音频**  
   - 收到服务器的二进制帧时,同样认定是 Opus 数据。  
   - 设备端会进行解码,然后交由音频输出接口播放。  
   - 如果服务器的音频采样率与设备不一致,会在解码后再进行重采样。

---

## 5. 常见状态流转

以下简述设备端关键状态流转,与 WebSocket 消息对应:

1. **Idle** → **Connecting**  
   - 用户触发或唤醒后,设备调用 `OpenAudioChannel()` → 建立 WebSocket 连接 → 发送 `"type":"hello"`。  

2. **Connecting** → **Listening**  
   - 成功建立连接后,若继续执行 `SendStartListening(...)`,则进入录音状态。此时设备会持续编码麦克风数据并发送到服务器。  

3. **Listening** → **Speaking**  
   - 收到服务器 TTS Start 消息 (`{"type":"tts","state":"start"}`) → 停止录音并播放接收到的音频。  

4. **Speaking** → **Idle**  
   - 服务器 TTS Stop (`{"type":"tts","state":"stop"}`) → 音频播放结束。若未继续进入自动监听,则返回 Idle;如果配置了自动循环,则再度进入 Listening。  

5. **Listening** / **Speaking** → **Idle**(遇到异常或主动中断)  
   - 调用 `SendAbortSpeaking(...)` 或 `CloseAudioChannel()` → 中断会话 → 关闭 WebSocket → 状态回到 Idle。  

---

## 6. 错误处理

1. **连接失败**  
   - 如果 `Connect(url)` 返回失败或在等待服务器 “hello” 消息时超时,触发 `on_network_error_()` 回调。设备会提示“无法连接到服务”或类似错误信息。

2. **服务器断开**  
   - 如果 WebSocket 异常断开,回调 `OnDisconnected()`:  
     - 设备回调 `on_audio_channel_closed_()`  
     - 切换到 Idle 或其他重试逻辑。

---

## 7. 其它注意事项

1. **鉴权**  
   - 设备通过设置 `Authorization: Bearer <token>` 提供鉴权,服务器端需验证是否有效。  
   - 如果令牌过期或无效,服务器可拒绝握手或在后续断开。

2. **会话控制**  
   - 代码中部分消息包含 `session_id`,用于区分独立的对话或操作。服务端可根据需要对不同会话做分离处理,WebSocket 协议为空。

3. **音频负载**  
   - 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。

4. **IoT 指令**  
   - `"type":"iot"` 的消息用户端代码对接 `thing_manager` 执行具体命令,因设备定制而不同。服务器端需确保下发格式与客户端保持一致。

5. **错误或异常 JSON**  
   - 当 JSON 中缺少必要字段,例如 `{"type": ...}`,客户端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。

---

## 8. 消息示例

下面给出一个典型的双向消息示例(流程简化示意):

1. **客户端 → 服务器**(握手)
   ```json
   {
     "type": "hello",
     "version": 1,
     "transport": "websocket",
     "audio_params": {
       "format": "opus",
       "sample_rate": 16000,
       "channels": 1,
       "frame_duration": 60
     }
   }
   ```

2. **服务器 → 客户端**(握手应答)
   ```json
   {
     "type": "hello",
     "transport": "websocket",
     "audio_params": {
       "sample_rate": 16000
     }
   }
   ```

3. **客户端 → 服务器**(开始监听)
   ```json
   {
     "session_id": "",
     "type": "listen",
     "state": "start",
     "mode": "auto"
   }
   ```
   同时客户端开始发送二进制帧(Opus 数据)。

4. **服务器 → 客户端**(ASR 结果)
   ```json
   {
     "type": "stt",
     "text": "用户说的话"
   }
   ```

5. **服务器 → 客户端**(TTS开始)
   ```json
   {
     "type": "tts",
     "state": "start"
   }
   ```
   接着服务器发送二进制音频帧给客户端播放。

6. **服务器 → 客户端**(TTS结束)
   ```json
   {
     "type": "tts",
     "state": "stop"
   }
   ```
   客户端停止播放音频,若无更多指令,则回到空闲状态。

---

## 9. 总结

本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧,完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、IoT 指令下发等。其核心特征:

- **握手阶段**:发送 `"type":"hello"`,等待服务器返回。  
- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流。  
- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、IoT、WakeWord 等。  
- **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。

服务器与客户端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。


## /docs/wiring.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/wiring.jpg

## /docs/wiring2.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/wiring2.jpg

## /docs/xmini-c3.jpg

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/docs/xmini-c3.jpg

## /main/CMakeLists.txt

set(SOURCES "audio_codecs/audio_codec.cc"
            "audio_codecs/no_audio_codec.cc"
            "audio_codecs/box_audio_codec.cc"
            "audio_codecs/es8311_audio_codec.cc"
            "audio_codecs/es8374_audio_codec.cc"
            "audio_codecs/es8388_audio_codec.cc"
            "led/single_led.cc"
            "led/circular_strip.cc"
            "led/gpio_led.cc"
            "display/display.cc"
            "display/lcd_display.cc"
            "display/oled_display.cc"
            "protocols/protocol.cc"
            "protocols/mqtt_protocol.cc"
            "protocols/websocket_protocol.cc"
            "iot/thing.cc"
            "iot/thing_manager.cc"
            "system_info.cc"
            "application.cc"
            "ota.cc"
            "settings.cc"
            "background_task.cc"
            "main.cc"
            )

set(INCLUDE_DIRS "." "display" "audio_codecs" "protocols" "audio_processing")

# 添加 IOT 相关文件
file(GLOB IOT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/iot/things/*.cc)
list(APPEND SOURCES ${IOT_SOURCES})

# 添加板级公共文件
file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc)
list(APPEND SOURCES ${BOARD_COMMON_SOURCES})
list(APPEND INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/boards/common)

# 根据 BOARD_TYPE 配置添加对应的板级文件
if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)
    set(BOARD_TYPE "bread-compact-wifi")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307)
    set(BOARD_TYPE "bread-compact-ml307")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32)
    set(BOARD_TYPE "bread-compact-esp32")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ESP32_LCD)
    set(BOARD_TYPE "bread-compact-esp32-lcd")
elseif(CONFIG_BOARD_TYPE_DF_K10)
    set(BOARD_TYPE "df-k10")
elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
    set(BOARD_TYPE "esp-box-3")
elseif(CONFIG_BOARD_TYPE_ESP_BOX)
    set(BOARD_TYPE "esp-box")
elseif(CONFIG_BOARD_TYPE_ESP_BOX_LITE)
    set(BOARD_TYPE "esp-box-lite")
elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_1)
    set(BOARD_TYPE "kevin-box-1")
elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_2)
    set(BOARD_TYPE "kevin-box-2")
elseif(CONFIG_BOARD_TYPE_KEVIN_C3)
    set(BOARD_TYPE "kevin-c3")
elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V3_DEV)
    set(BOARD_TYPE "kevin-sp-v3-dev")
elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V4_DEV)
    set(BOARD_TYPE "kevin-sp-v4-dev")
elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD)
    set(BOARD_TYPE "kevin-yuying-313lcd")
elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV)
    set(BOARD_TYPE "lichuang-dev")
elseif(CONFIG_BOARD_TYPE_LICHUANG_C3_DEV)
    set(BOARD_TYPE "lichuang-c3-dev")
elseif(CONFIG_BOARD_TYPE_MAGICLICK_2P4)
    set(BOARD_TYPE "magiclick-2p4")
elseif(CONFIG_BOARD_TYPE_MAGICLICK_2P5)
    set(BOARD_TYPE "magiclick-2p5")
elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3)
    set(BOARD_TYPE "magiclick-c3")
elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3_V2)
    set(BOARD_TYPE "magiclick-c3-v2")
elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_S3)
    set(BOARD_TYPE "m5stack-core-s3")
elseif(CONFIG_BOARD_TYPE_ATOMS3_ECHO_BASE)
    set(BOARD_TYPE "atoms3-echo-base")
elseif(CONFIG_BOARD_TYPE_ATOMS3R_ECHO_BASE)
    set(BOARD_TYPE "atoms3r-echo-base")
elseif(CONFIG_BOARD_TYPE_ATOMS3R_CAM_M12_ECHO_BASE)
    set(BOARD_TYPE "atoms3r-cam-m12-echo-base")
elseif(CONFIG_BOARD_TYPE_ATOMMATRIX_ECHO_BASE)
    set(BOARD_TYPE "atommatrix-echo-base")
elseif(CONFIG_BOARD_TYPE_XMINI_C3)
    set(BOARD_TYPE "xmini-c3")
elseif(CONFIG_BOARD_TYPE_ESP32S3_KORVO2_V3)
    set(BOARD_TYPE "esp32s3-korvo2-v3")
elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT)
    set(BOARD_TYPE "esp-sparkbot")
elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3)
    set(BOARD_TYPE "esp-spot-s3")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8)
    set(BOARD_TYPE "esp32-s3-touch-amoled-1.8")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85C)
    set(BOARD_TYPE "esp32-s3-touch-lcd-1.85c")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85)
    set(BOARD_TYPE "esp32-s3-touch-lcd-1.85")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_46)
    set(BOARD_TYPE "esp32-s3-touch-lcd-1.46")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_3_5)
    set(BOARD_TYPE "esp32-s3-touch-lcd-3.5")
elseif(CONFIG_BOARD_TYPE_ESP32P4_NANO)
    set(BOARD_TYPE "waveshare-p4-nano")
elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD)
    set(BOARD_TYPE "bread-compact-wifi-lcd")
elseif(CONFIG_BOARD_TYPE_TUDOUZI)
    set(BOARD_TYPE "tudouzi")
elseif(CONFIG_BOARD_TYPE_LILYGO_T_CIRCLE_S3)
    set(BOARD_TYPE "lilygo-t-circle-s3")
elseif(CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3)
    set(BOARD_TYPE "lilygo-t-cameraplus-s3")
elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA)
    set(BOARD_TYPE "lilygo-t-display-s3-pro-mvsrlora")
elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA_NO_BATTERY)
    set(BOARD_TYPE "lilygo-t-display-s3-pro-mvsrlora")
elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3)
    set(BOARD_TYPE "movecall-moji-esp32s3")
    elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3)
    set(BOARD_TYPE "movecall-cuican-esp32s3")
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3)
    set(BOARD_TYPE "atk-dnesp32s3")
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX)
    set(BOARD_TYPE "atk-dnesp32s3-box")
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX0)
    set(BOARD_TYPE "atk-dnesp32s3-box0")
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_WIFI)
    set(BOARD_TYPE "atk-dnesp32s3m-wifi")
elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_4G)
    set(BOARD_TYPE "atk-dnesp32s3m-4g")
elseif(CONFIG_BOARD_TYPE_DU_CHATX)
    set(BOARD_TYPE "du-chatx")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Taiji_Pi)
    set(BOARD_TYPE "taiji-pi-s3")
elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_85TFT_WIFI)
    set(BOARD_TYPE "xingzhi-cube-0.85tft-wifi")
elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_85TFT_ML307)
    set(BOARD_TYPE "xingzhi-cube-0.85tft-ml307")
elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_96OLED_WIFI)
    set(BOARD_TYPE "xingzhi-cube-0.96oled-wifi")
elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_96OLED_ML307)
    set(BOARD_TYPE "xingzhi-cube-0.96oled-ml307")
elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_1_54TFT_WIFI)
    set(BOARD_TYPE "xingzhi-cube-1.54tft-wifi")
elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_1_54TFT_ML307)
    set(BOARD_TYPE "xingzhi-cube-1.54tft-ml307")
elseif(CONFIG_BOARD_TYPE_SENSECAP_WATCHER)
    set(BOARD_TYPE "sensecap-watcher")
elseif(CONFIG_BOARD_TYPE_DOIT_S3_AIBOX)
    set(BOARD_TYPE "doit-s3-aibox")
elseif(CONFIG_BOARD_TYPE_MIXGO_NOVA)
    set(BOARD_TYPE "mixgo-nova")
elseif(CONFIG_BOARD_TYPE_ESP32_CGC)
    set(BOARD_TYPE "esp32-cgc")
elseif(CONFIG_BOARD_TYPE_ESP_S3_LCD_EV_Board)
    set(BOARD_TYPE "esp-s3-lcd-ev-board")
endif()
file(GLOB BOARD_SOURCES
    ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc
    ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.c
)
list(APPEND SOURCES ${BOARD_SOURCES})

if(CONFIG_USE_AUDIO_PROCESSOR)
    list(APPEND SOURCES "audio_processing/afe_audio_processor.cc")
else()
    list(APPEND SOURCES "audio_processing/dummy_audio_processor.cc")
endif()
if(CONFIG_USE_WAKE_WORD_DETECT)
    list(APPEND SOURCES "audio_processing/wake_word_detect.cc")
endif()

# 根据Kconfig选择语言目录
if(CONFIG_LANGUAGE_ZH_CN)
    set(LANG_DIR "zh-CN")
elseif(CONFIG_LANGUAGE_ZH_TW)
    set(LANG_DIR "zh-TW")
elseif(CONFIG_LANGUAGE_EN_US)
    set(LANG_DIR "en-US")
elseif(CONFIG_LANGUAGE_JA_JP)
    set(LANG_DIR "ja-JP")
endif()

# 定义生成路径
set(LANG_JSON "${CMAKE_CURRENT_SOURCE_DIR}/assets/${LANG_DIR}/language.json")
set(LANG_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/assets/lang_config.h")
file(GLOB LANG_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/${LANG_DIR}/*.p3)
file(GLOB COMMON_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/common/*.p3)

# 如果目标芯片是 ESP32,则排除特定文件
if(CONFIG_IDF_TARGET_ESP32)
    list(REMOVE_ITEM SOURCES "audio_codecs/box_audio_codec.cc"
                             "audio_codecs/es8388_audio_codec.cc"
                             "led/gpio_led.cc"
                             )
endif()

idf_component_register(SRCS ${SOURCES}
                    EMBED_FILES ${LANG_SOUNDS} ${COMMON_SOUNDS}
                    INCLUDE_DIRS ${INCLUDE_DIRS}
                    WHOLE_ARCHIVE
                    )

# 使用 target_compile_definitions 来定义 BOARD_TYPE, BOARD_NAME
# 如果 BOARD_NAME 为空,则使用 BOARD_TYPE
if(NOT BOARD_NAME)
    set(BOARD_NAME ${BOARD_TYPE})
endif()
target_compile_definitions(${COMPONENT_LIB}
                    PRIVATE BOARD_TYPE=\"${BOARD_TYPE}\" BOARD_NAME=\"${BOARD_NAME}\"
                    )

# 添加生成规则
add_custom_command(
    OUTPUT ${LANG_HEADER}
    COMMAND python ${PROJECT_DIR}/scripts/gen_lang.py
            --input "${LANG_JSON}"
            --output "${LANG_HEADER}"
    DEPENDS
        ${LANG_JSON}
        ${PROJECT_DIR}/scripts/gen_lang.py
    COMMENT "Generating ${LANG_DIR} language config"
)

# 强制建立生成依赖
add_custom_target(lang_header ALL
    DEPENDS ${LANG_HEADER}
)


## /main/Kconfig.projbuild

```projbuild path="/main/Kconfig.projbuild" 
menu "Xiaozhi Assistant"

config OTA_URL
    string "Default OTA URL"
    default "https://api.tenclass.net/xiaozhi/ota/"
    help
        The application will access this URL to check for new firmwares and server address.


choice
    prompt "语言选择"
    default LANGUAGE_ZH_CN
    help
        Select device display language

    config LANGUAGE_ZH_CN
        bool "Chinese"
    config LANGUAGE_ZH_TW
        bool "Chinese Traditional"
    config LANGUAGE_EN_US
        bool "English"
    config LANGUAGE_JA_JP
        bool "Japanese"
endchoice

choice BOARD_TYPE
    prompt "Board Type"
    default BOARD_TYPE_BREAD_COMPACT_WIFI
    help
        Board type. 开发板类型
    config BOARD_TYPE_BREAD_COMPACT_WIFI
        bool "面包板新版接线(WiFi)"
    config BOARD_TYPE_BREAD_COMPACT_WIFI_LCD
        bool "面包板新版接线(WiFi)+ LCD"
    config BOARD_TYPE_BREAD_COMPACT_ML307
        bool "面包板新版接线(ML307 AT)"
    config BOARD_TYPE_BREAD_COMPACT_ESP32
        bool "面包板(WiFi) ESP32 DevKit"
    config BOARD_TYPE_BREAD_COMPACT_ESP32_LCD
        bool "面包板(WiFi+ LCD) ESP32 DevKit"
    config BOARD_TYPE_ESP32_CGC
        bool "ESP32 CGC"
    config BOARD_TYPE_ESP_BOX_3
        bool "ESP BOX 3"
    config BOARD_TYPE_ESP_BOX
        bool "ESP BOX"
    config BOARD_TYPE_ESP_BOX_LITE
        bool "ESP BOX Lite"
    config BOARD_TYPE_KEVIN_BOX_1
        bool "Kevin Box 1"
    config BOARD_TYPE_KEVIN_BOX_2
        bool "Kevin Box 2"
    config BOARD_TYPE_KEVIN_C3
        bool "Kevin C3"
    config BOARD_TYPE_KEVIN_SP_V3_DEV
        bool "Kevin SP V3开发板"
    config BOARD_TYPE_KEVIN_SP_V4_DEV
        bool "Kevin SP V4开发板"
    config BOARD_TYPE_KEVIN_YUYING_313LCD
        bool "鱼鹰科技3.13LCD开发板"
    config BOARD_TYPE_LICHUANG_DEV
        bool "立创·实战派ESP32-S3开发板"
    config BOARD_TYPE_LICHUANG_C3_DEV
        bool "立创·实战派ESP32-C3开发板"
    config BOARD_TYPE_DF_K10
        bool "DFRobot 行空板 k10"
    config BOARD_TYPE_MAGICLICK_2P4
        bool "神奇按钮 Magiclick_2.4"
    config BOARD_TYPE_MAGICLICK_2P5
        bool "神奇按钮 Magiclick_2.5"
    config BOARD_TYPE_MAGICLICK_C3
        bool "神奇按钮 Magiclick_C3"
    config BOARD_TYPE_MAGICLICK_C3_V2
        bool "神奇按钮 Magiclick_C3_v2"
    config BOARD_TYPE_M5STACK_CORE_S3
        bool "M5Stack CoreS3"
    config BOARD_TYPE_ATOMS3_ECHO_BASE
        bool "AtomS3 + Echo Base"
    config BOARD_TYPE_ATOMS3R_ECHO_BASE
        bool "AtomS3R + Echo Base"
    config BOARD_TYPE_ATOMS3R_CAM_M12_ECHO_BASE
        bool "AtomS3R CAM/M12 + Echo Base"
    config BOARD_TYPE_ATOMMATRIX_ECHO_BASE
        bool "AtomMatrix + Echo Base"
    config BOARD_TYPE_XMINI_C3
        bool "虾哥 Mini C3"
    config BOARD_TYPE_ESP32S3_KORVO2_V3
        bool "ESP32S3_KORVO2_V3开发板"
    config BOARD_TYPE_ESP_SPARKBOT
        bool "ESP-SparkBot开发板"
    config BOARD_TYPE_ESP_SPOT_S3
        bool "ESP-Spot-S3"
    config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8
        bool "Waveshare ESP32-S3-Touch-AMOLED-1.8"
    config BOARD_TYPE_ESP32S3_Touch_LCD_1_85C
        bool "Waveshare ESP32-S3-Touch-LCD-1.85C"
    config BOARD_TYPE_ESP32S3_Touch_LCD_1_85
        bool "Waveshare ESP32-S3-Touch-LCD-1.85"
    config BOARD_TYPE_ESP32S3_Touch_LCD_1_46
        bool "Waveshare ESP32-S3-Touch-LCD-1.46"
    config BOARD_TYPE_ESP32S3_Touch_LCD_3_5
        bool "Waveshare ESP32-S3-Touch-LCD-3.5"
    config BOARD_TYPE_ESP32P4_NANO
        bool "Waveshare ESP32-P4-NANO"
    config BOARD_TYPE_TUDOUZI
        bool "土豆子"
    config BOARD_TYPE_LILYGO_T_CIRCLE_S3
        bool "LILYGO T-Circle-S3"
    config BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3
        bool "LILYGO T-CameraPlus-S3"
    config BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA
        bool "LILYGO T-Display-S3-Pro-MVSRLora"
    config BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA_NO_BATTERY
        bool "LILYGO T-Display-S3-Pro-MVSRLora_No_Battery"
    config BOARD_TYPE_MOVECALL_MOJI_ESP32S3
         bool "Movecall Moji 小智AI衍生版"
    config BOARD_TYPE_MOVECALL_CUICAN_ESP32S3
         bool "Movecall CuiCan 璀璨·AI吊坠"
    config BOARD_TYPE_ATK_DNESP32S3
        bool "正点原子DNESP32S3开发板"
    config BOARD_TYPE_ATK_DNESP32S3_BOX
        bool "正点原子DNESP32S3-BOX"
    config BOARD_TYPE_ATK_DNESP32S3_BOX0
        bool "正点原子DNESP32S3-BOX0"
    config BOARD_TYPE_ATK_DNESP32S3M_WIFI
        bool "正点原子DNESP32S3M-WIFI"
    config BOARD_TYPE_ATK_DNESP32S3M_4G
        bool "正点原子DNESP32S3M-4G"
    config BOARD_TYPE_DU_CHATX
        bool "嘟嘟开发板CHATX(wifi)"
    config BOARD_TYPE_ESP32S3_Taiji_Pi
        bool "太极小派esp32s3"
    config BOARD_TYPE_XINGZHI_Cube_0_85TFT_WIFI
        bool "无名科技星智0.85(WIFI)"
    config BOARD_TYPE_XINGZHI_Cube_0_85TFT_ML307
        bool "无名科技星智0.85(ML307)"
    config BOARD_TYPE_XINGZHI_Cube_0_96OLED_WIFI
        bool "无名科技星智0.96(WIFI)"
    config BOARD_TYPE_XINGZHI_Cube_0_96OLED_ML307
        bool "无名科技星智0.96(ML307)"
    config BOARD_TYPE_XINGZHI_Cube_1_54TFT_WIFI
        bool "无名科技星智1.54(WIFI)"
    config BOARD_TYPE_XINGZHI_Cube_1_54TFT_ML307
        bool "无名科技星智1.54(ML307)"
    config BOARD_TYPE_SENSECAP_WATCHER
        bool "SenseCAP Watcher"
    config BOARD_TYPE_DOIT_S3_AIBOX
        bool "四博智联AI陪伴盒子"
    config BOARD_TYPE_MIXGO_NOVA
        bool "元控·青春"
    config BOARD_TYPE_ESP_S3_LCD_EV_Board
        bool "乐鑫ESP S3 LCD EV Board开发板"
endchoice

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
        bool "乐鑫ESP32_S3_LCD_EV_Board-MB_V1.5"
endchoice

choice DISPLAY_OLED_TYPE
    depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32
    prompt "OLED Type"
    default OLED_SSD1306_128X32
    help
        OLED 屏幕类型选择
    config OLED_SSD1306_128X32
        bool "SSD1306, 分辨率128*32"
    config OLED_SSD1306_128X64
        bool "SSD1306, 分辨率128*64"
    config OLED_SH1106_128X64
        bool "SH1106, 分辨率128*64"
endchoice

choice DISPLAY_LCD_TYPE
    depends on BOARD_TYPE_BREAD_COMPACT_WIFI_LCD || BOARD_TYPE_BREAD_COMPACT_ESP32_LCD || BOARD_TYPE_ESP32_CGC || BOARD_TYPE_ESP32P4_NANO
    prompt "LCD Type"
    default LCD_ST7789_240X320
    help
        屏幕类型选择
    config LCD_ST7789_240X320
        bool "ST7789, 分辨率240*320, IPS"
    config LCD_ST7789_240X320_NO_IPS
        bool "ST7789, 分辨率240*320, 非IPS"
    config LCD_ST7789_170X320
        bool "ST7789, 分辨率170*320"
    config LCD_ST7789_172X320
        bool "ST7789, 分辨率172*320"
    config LCD_ST7789_240X280
        bool "ST7789, 分辨率240*280"
    config LCD_ST7789_240X240
        bool "ST7789, 分辨率240*240"
    config LCD_ST7789_240X240_7PIN
        bool "ST7789, 分辨率240*240, 7PIN"
    config LCD_ST7789_240X135
        bool "ST7789, 分辨率240*135"
    config LCD_ST7735_128X160
        bool "ST7735, 分辨率128*160"
    config LCD_ST7735_128X128
        bool "ST7735, 分辨率128*128"
    config LCD_ST7796_320X480
        bool "ST7796, 分辨率320*480 IPS"
    config LCD_ST7796_320X480_NO_IPS
        bool "ST7796, 分辨率320*480, 非IPS"
    config LCD_ILI9341_240X320
        bool "ILI9341, 分辨率240*320"
    config LCD_ILI9341_240X320_NO_IPS
        bool "ILI9341, 分辨率240*320, 非IPS"
    config LCD_GC9A01_240X240
        bool "GC9A01, 分辨率240*240, 圆屏"
    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
        bool "Waveshare 10.1-DSI-TOUCH-A Display"
    config LCD_CUSTOM
        bool "自定义屏幕参数"
endchoice

choice DISPLAY_ESP32S3_KORVO2_V3
    depends on BOARD_TYPE_ESP32S3_KORVO2_V3
    prompt "ESP32S3_KORVO2_V3 LCD Type"
    default LCD_ST7789
    help
        屏幕类型选择
    config LCD_ST7789
        bool "ST7789, 分辨率240*280"
    config LCD_ILI9341
        bool "ILI9341, 分辨率240*320"
endchoice

config USE_WECHAT_MESSAGE_STYLE
    bool "使用微信聊天界面风格"
    default n
    help
        使用微信聊天界面风格

config USE_WAKE_WORD_DETECT
    bool "启用唤醒词检测"
    default y
    depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4 && SPIRAM
    help
        需要 ESP32 S3 与 AFE 支持

config USE_AUDIO_PROCESSOR
    bool "启用音频降噪、增益处理"
    default y
    depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4 && SPIRAM
    help
        需要 ESP32 S3 与 AFE 支持

config USE_DEVICE_AEC
    bool "在通话过程中启用设备端 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)
    help
        因为性能不够,不建议和微信聊天界面风格同时开启

config USE_SERVER_AEC
    bool "在通话过程中启用服务器端 AEC"
    default n
    depends on USE_AUDIO_PROCESSOR
    help
        启用服务器端 AEC,需要服务器支持

endmenu

```

## /main/application.cc

```cc path="/main/application.cc" 
#include "application.h"
#include "board.h"
#include "display.h"
#include "system_info.h"
#include "ml307_ssl_transport.h"
#include "audio_codec.h"
#include "mqtt_protocol.h"
#include "websocket_protocol.h"
#include "font_awesome_symbols.h"
#include "iot/thing_manager.h"
#include "assets/lang_config.h"

#if CONFIG_USE_AUDIO_PROCESSOR
#include "afe_audio_processor.h"
#else
#include "dummy_audio_processor.h"
#endif

#include <cstring>
#include <esp_log.h>
#include <cJSON.h>
#include <driver/gpio.h>
#include <arpa/inet.h>

#define TAG "Application"


static const char* const STATE_STRINGS[] = {
    "unknown",
    "starting",
    "configuring",
    "idle",
    "connecting",
    "listening",
    "speaking",
    "upgrading",
    "activating",
    "fatal_error",
    "invalid_state"
};

Application::Application() {
    event_group_ = xEventGroupCreate();
    background_task_ = new BackgroundTask(4096 * 8);

#if CONFIG_USE_AUDIO_PROCESSOR
    audio_processor_ = std::make_unique<AfeAudioProcessor>();
#else
    audio_processor_ = std::make_unique<DummyAudioProcessor>();
#endif

    esp_timer_create_args_t clock_timer_args = {
        .callback = [](void* arg) {
            Application* app = (Application*)arg;
            app->OnClockTimer();
        },
        .arg = this,
        .dispatch_method = ESP_TIMER_TASK,
        .name = "clock_timer",
        .skip_unhandled_events = true
    };
    esp_timer_create(&clock_timer_args, &clock_timer_handle_);
    esp_timer_start_periodic(clock_timer_handle_, 1000000);
}

Application::~Application() {
    if (clock_timer_handle_ != nullptr) {
        esp_timer_stop(clock_timer_handle_);
        esp_timer_delete(clock_timer_handle_);
    }
    if (background_task_ != nullptr) {
        delete background_task_;
    }
    vEventGroupDelete(event_group_);
}

void Application::CheckNewVersion() {
    const int MAX_RETRY = 10;
    int retry_count = 0;
    int retry_delay = 10; // 初始重试延迟为10秒

    while (true) {
        SetDeviceState(kDeviceStateActivating);
        auto display = Board::GetInstance().GetDisplay();
        display->SetStatus(Lang::Strings::CHECKING_NEW_VERSION);

        if (!ota_.CheckVersion()) {
            retry_count++;
            if (retry_count >= MAX_RETRY) {
                ESP_LOGE(TAG, "Too many retries, exit version check");
                return;
            }

            char buffer[128];
            snprintf(buffer, sizeof(buffer), Lang::Strings::CHECK_NEW_VERSION_FAILED, retry_delay, ota_.GetCheckVersionUrl().c_str());
            Alert(Lang::Strings::ERROR, buffer, "sad", Lang::Sounds::P3_EXCLAMATION);

            ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", retry_delay, retry_count, MAX_RETRY);
            for (int i = 0; i < retry_delay; i++) {
                vTaskDelay(pdMS_TO_TICKS(1000));
                if (device_state_ == kDeviceStateIdle) {
                    break;
                }
            }
            retry_delay *= 2; // 每次重试后延迟时间翻倍
            continue;
        }
        retry_count = 0;
        retry_delay = 10; // 重置重试延迟时间

        if (ota_.HasNewVersion()) {
            Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "happy", Lang::Sounds::P3_UPGRADE);

            vTaskDelay(pdMS_TO_TICKS(3000));

            SetDeviceState(kDeviceStateUpgrading);
            
            display->SetIcon(FONT_AWESOME_DOWNLOAD);
            std::string message = std::string(Lang::Strings::NEW_VERSION) + ota_.GetFirmwareVersion();
            display->SetChatMessage("system", message.c_str());

            auto& board = Board::GetInstance();
            board.SetPowerSaveMode(false);
#if CONFIG_USE_WAKE_WORD_DETECT
            wake_word_detect_.StopDetection();
#endif
            // 预先关闭音频输出,避免升级过程有音频操作
            auto codec = board.GetAudioCodec();
            codec->EnableInput(false);
            codec->EnableOutput(false);
            {
                std::lock_guard<std::mutex> lock(mutex_);
                audio_decode_queue_.clear();
            }
            background_task_->WaitForCompletion();
            delete background_task_;
            background_task_ = nullptr;
            vTaskDelay(pdMS_TO_TICKS(1000));

            ota_.StartUpgrade([display](int progress, size_t speed) {
                char buffer[64];
                snprintf(buffer, sizeof(buffer), "%d%% %zuKB/s", progress, speed / 1024);
                display->SetChatMessage("system", buffer);
            });

            // If upgrade success, the device will reboot and never reach here
            display->SetStatus(Lang::Strings::UPGRADE_FAILED);
            ESP_LOGI(TAG, "Firmware upgrade failed...");
            vTaskDelay(pdMS_TO_TICKS(3000));
            Reboot();
            return;
        }

        // No new version, mark the current version as valid
        ota_.MarkCurrentVersionValid();
        if (!ota_.HasActivationCode() && !ota_.HasActivationChallenge()) {
            xEventGroupSetBits(event_group_, CHECK_NEW_VERSION_DONE_EVENT);
            // Exit the loop if done checking new version
            break;
        }

        display->SetStatus(Lang::Strings::ACTIVATION);
        // Activation code is shown to the user and waiting for the user to input
        if (ota_.HasActivationCode()) {
            ShowActivationCode();
        }

        // This will block the loop until the activation is done or timeout
        for (int i = 0; i < 10; ++i) {
            ESP_LOGI(TAG, "Activating... %d/%d", i + 1, 10);
            esp_err_t err = ota_.Activate();
            if (err == ESP_OK) {
                xEventGroupSetBits(event_group_, CHECK_NEW_VERSION_DONE_EVENT);
                break;
            } else if (err == ESP_ERR_TIMEOUT) {
                vTaskDelay(pdMS_TO_TICKS(3000));
            } else {
                vTaskDelay(pdMS_TO_TICKS(10000));
            }
            if (device_state_ == kDeviceStateIdle) {
                break;
            }
        }
    }
}

void Application::ShowActivationCode() {
    auto& message = ota_.GetActivationMessage();
    auto& code = ota_.GetActivationCode();

    struct digit_sound {
        char digit;
        const std::string_view& sound;
    };
    static const std::array<digit_sound, 10> digit_sounds{{
        digit_sound{'0', Lang::Sounds::P3_0},
        digit_sound{'1', Lang::Sounds::P3_1}, 
        digit_sound{'2', Lang::Sounds::P3_2},
        digit_sound{'3', Lang::Sounds::P3_3},
        digit_sound{'4', Lang::Sounds::P3_4},
        digit_sound{'5', Lang::Sounds::P3_5},
        digit_sound{'6', Lang::Sounds::P3_6},
        digit_sound{'7', Lang::Sounds::P3_7},
        digit_sound{'8', Lang::Sounds::P3_8},
        digit_sound{'9', Lang::Sounds::P3_9}
    }};

    // This sentence uses 9KB of SRAM, so we need to wait for it to finish
    Alert(Lang::Strings::ACTIVATION, message.c_str(), "happy", Lang::Sounds::P3_ACTIVATION);

    for (const auto& digit : code) {
        auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(),
            [digit](const digit_sound& ds) { return ds.digit == digit; });
        if (it != digit_sounds.end()) {
            PlaySound(it->sound);
        }
    }
}

void Application::Alert(const char* status, const char* message, const char* emotion, const std::string_view& sound) {
    ESP_LOGW(TAG, "Alert %s: %s [%s]", status, message, emotion);
    auto display = Board::GetInstance().GetDisplay();
    display->SetStatus(status);
    display->SetEmotion(emotion);
    display->SetChatMessage("system", message);
    if (!sound.empty()) {
        ResetDecoder();
        PlaySound(sound);
    }
}

void Application::DismissAlert() {
    if (device_state_ == kDeviceStateIdle) {
        auto display = Board::GetInstance().GetDisplay();
        display->SetStatus(Lang::Strings::STANDBY);
        display->SetEmotion("neutral");
        display->SetChatMessage("system", "");
    }
}

void Application::PlaySound(const std::string_view& sound) {
    // Wait for the previous sound to finish
    {
        std::unique_lock<std::mutex> lock(mutex_);
        audio_decode_cv_.wait(lock, [this]() {
            return audio_decode_queue_.empty();
        });
    }
    background_task_->WaitForCompletion();

    // The assets are encoded at 16000Hz, 60ms frame duration
    SetDecodeSampleRate(16000, 60);
    const char* data = sound.data();
    size_t size = sound.size();
    for (const char* p = data; p < data + size; ) {
        auto p3 = (BinaryProtocol3*)p;
        p += sizeof(BinaryProtocol3);

        auto payload_size = ntohs(p3->payload_size);
        AudioStreamPacket packet;
        packet.payload.resize(payload_size);
        memcpy(packet.payload.data(), p3->payload, payload_size);
        p += payload_size;

        std::lock_guard<std::mutex> lock(mutex_);
        audio_decode_queue_.emplace_back(std::move(packet));
    }
}

void Application::ToggleChatState() {
    if (device_state_ == kDeviceStateActivating) {
        SetDeviceState(kDeviceStateIdle);
        return;
    }

    if (!protocol_) {
        ESP_LOGE(TAG, "Protocol not initialized");
        return;
    }

    if (device_state_ == kDeviceStateIdle) {
        Schedule([this]() {
            SetDeviceState(kDeviceStateConnecting);
            if (!protocol_->OpenAudioChannel()) {
                return;
            }

            SetListeningMode(realtime_chat_enabled_ ? kListeningModeRealtime : kListeningModeAutoStop);
        });
    } else if (device_state_ == kDeviceStateSpeaking) {
        Schedule([this]() {
            AbortSpeaking(kAbortReasonNone);
        });
    } else if (device_state_ == kDeviceStateListening) {
        Schedule([this]() {
            protocol_->CloseAudioChannel();
        });
    }
}

void Application::StartListening() {
    if (device_state_ == kDeviceStateActivating) {
        SetDeviceState(kDeviceStateIdle);
        return;
    }

    if (!protocol_) {
        ESP_LOGE(TAG, "Protocol not initialized");
        return;
    }
    
    if (device_state_ == kDeviceStateIdle) {
        Schedule([this]() {
            if (!protocol_->IsAudioChannelOpened()) {
                SetDeviceState(kDeviceStateConnecting);
                if (!protocol_->OpenAudioChannel()) {
                    return;
                }
            }

            SetListeningMode(kListeningModeManualStop);
        });
    } else if (device_state_ == kDeviceStateSpeaking) {
        Schedule([this]() {
            AbortSpeaking(kAbortReasonNone);
            SetListeningMode(kListeningModeManualStop);
        });
    }
}

void Application::StopListening() {
    const std::array<int, 3> valid_states = {
        kDeviceStateListening,
        kDeviceStateSpeaking,
        kDeviceStateIdle,
    };
    // If not valid, do nothing
    if (std::find(valid_states.begin(), valid_states.end(), device_state_) == valid_states.end()) {
        return;
    }

    Schedule([this]() {
        if (device_state_ == kDeviceStateListening) {
            protocol_->SendStopListening();
            SetDeviceState(kDeviceStateIdle);
        }
    });
}

void Application::Start() {
    auto& board = Board::GetInstance();
    SetDeviceState(kDeviceStateStarting);

    /* Setup the display */
    auto display = board.GetDisplay();

    /* Setup the audio codec */
    auto codec = board.GetAudioCodec();
    opus_decoder_ = std::make_unique<OpusDecoderWrapper>(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS);
    opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
    if (realtime_chat_enabled_) {
        ESP_LOGI(TAG, "Realtime chat enabled, setting opus encoder complexity to 0");
        opus_encoder_->SetComplexity(0);
    } else if (board.GetBoardType() == "ml307") {
        ESP_LOGI(TAG, "ML307 board detected, setting opus encoder complexity to 5");
        opus_encoder_->SetComplexity(5);
    } else {
        ESP_LOGI(TAG, "WiFi board detected, setting opus encoder complexity to 3");
        opus_encoder_->SetComplexity(3);
    }

    if (codec->input_sample_rate() != 16000) {
        input_resampler_.Configure(codec->input_sample_rate(), 16000);
        reference_resampler_.Configure(codec->input_sample_rate(), 16000);
    }
    codec->Start();

#if CONFIG_USE_AUDIO_PROCESSOR
    xTaskCreatePinnedToCore([](void* arg) {
        Application* app = (Application*)arg;
        app->AudioLoop();
        vTaskDelete(NULL);
    }, "audio_loop", 4096 * 2, this, 8, &audio_loop_task_handle_, 1);
#else
    xTaskCreate([](void* arg) {
        Application* app = (Application*)arg;
        app->AudioLoop();
        vTaskDelete(NULL);
    }, "audio_loop", 4096 * 2, this, 8, &audio_loop_task_handle_);
#endif

    /* Wait for the network to be ready */
    board.StartNetwork();

    // Check for new firmware version or get the MQTT broker address
    CheckNewVersion();

    // Initialize the protocol
    display->SetStatus(Lang::Strings::LOADING_PROTOCOL);

    if (ota_.HasMqttConfig()) {
        protocol_ = std::make_unique<MqttProtocol>();
    } else if (ota_.HasWebsocketConfig()) {
        protocol_ = std::make_unique<WebsocketProtocol>();
    } else {
        ESP_LOGW(TAG, "No protocol specified in the OTA config, using MQTT");
        protocol_ = std::make_unique<MqttProtocol>();
    }

    protocol_->OnNetworkError([this](const std::string& message) {
        SetDeviceState(kDeviceStateIdle);
        Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION);
    });
    protocol_->OnIncomingAudio([this](AudioStreamPacket&& packet) {
        const int max_packets_in_queue = 600 / OPUS_FRAME_DURATION_MS;
        std::lock_guard<std::mutex> lock(mutex_);
        if (audio_decode_queue_.size() < max_packets_in_queue) {
            audio_decode_queue_.emplace_back(std::move(packet));
        }
    });
    protocol_->OnAudioChannelOpened([this, codec, &board]() {
        board.SetPowerSaveMode(false);
        if (protocol_->server_sample_rate() != codec->output_sample_rate()) {
            ESP_LOGW(TAG, "Server sample rate %d does not match device output sample rate %d, resampling may cause distortion",
                protocol_->server_sample_rate(), codec->output_sample_rate());
        }
        SetDecodeSampleRate(protocol_->server_sample_rate(), protocol_->server_frame_duration());
        auto& thing_manager = iot::ThingManager::GetInstance();
        protocol_->SendIotDescriptors(thing_manager.GetDescriptorsJson());
        std::string states;
        if (thing_manager.GetStatesJson(states, false)) {
            protocol_->SendIotStates(states);
        }
    });
    protocol_->OnAudioChannelClosed([this, &board]() {
        board.SetPowerSaveMode(true);
        Schedule([this]() {
            auto display = Board::GetInstance().GetDisplay();
            display->SetChatMessage("system", "");
            SetDeviceState(kDeviceStateIdle);
        });
    });
    protocol_->OnIncomingJson([this, display](const cJSON* root) {
        // Parse JSON data
        auto type = cJSON_GetObjectItem(root, "type");
        if (strcmp(type->valuestring, "tts") == 0) {
            auto state = cJSON_GetObjectItem(root, "state");
            if (strcmp(state->valuestring, "start") == 0) {
                Schedule([this]() {
                    aborted_ = false;
                    if (device_state_ == kDeviceStateIdle || device_state_ == kDeviceStateListening) {
                        SetDeviceState(kDeviceStateSpeaking);
                    }
                });
            } else if (strcmp(state->valuestring, "stop") == 0) {
                Schedule([this]() {
                    background_task_->WaitForCompletion();
                    if (device_state_ == kDeviceStateSpeaking) {
                        if (listening_mode_ == kListeningModeManualStop) {
                            SetDeviceState(kDeviceStateIdle);
                        } else {
                            SetDeviceState(kDeviceStateListening);
                        }
                    }
                });
            } else if (strcmp(state->valuestring, "sentence_start") == 0) {
                auto text = cJSON_GetObjectItem(root, "text");
                if (text != NULL) {
                    ESP_LOGI(TAG, "<< %s", text->valuestring);
                    Schedule([this, display, message = std::string(text->valuestring)]() {
                        display->SetChatMessage("assistant", message.c_str());
                    });
                }
            }
        } else if (strcmp(type->valuestring, "stt") == 0) {
            auto text = cJSON_GetObjectItem(root, "text");
            if (text != NULL) {
                ESP_LOGI(TAG, ">> %s", text->valuestring);
                Schedule([this, display, message = std::string(text->valuestring)]() {
                    display->SetChatMessage("user", message.c_str());
                });
            }
        } else if (strcmp(type->valuestring, "llm") == 0) {
            auto emotion = cJSON_GetObjectItem(root, "emotion");
            if (emotion != NULL) {
                Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() {
                    display->SetEmotion(emotion_str.c_str());
                });
            }
        } else if (strcmp(type->valuestring, "iot") == 0) {
            auto commands = cJSON_GetObjectItem(root, "commands");
            if (commands != NULL) {
                auto& thing_manager = iot::ThingManager::GetInstance();
                for (int i = 0; i < cJSON_GetArraySize(commands); ++i) {
                    auto command = cJSON_GetArrayItem(commands, i);
                    thing_manager.Invoke(command);
                }
            }
        } else if (strcmp(type->valuestring, "system") == 0) {
            auto command = cJSON_GetObjectItem(root, "command");
            if (command != NULL) {
                ESP_LOGI(TAG, "System command: %s", command->valuestring);
                if (strcmp(command->valuestring, "reboot") == 0) {
                    // Do a reboot if user requests a OTA update
                    Schedule([this]() {
                        Reboot();
                    });
                } else {
                    ESP_LOGW(TAG, "Unknown system command: %s", command->valuestring);
                }
            }
        } else if (strcmp(type->valuestring, "alert") == 0) {
            auto status = cJSON_GetObjectItem(root, "status");
            auto message = cJSON_GetObjectItem(root, "message");
            auto emotion = cJSON_GetObjectItem(root, "emotion");
            if (status != NULL && message != NULL && emotion != NULL) {
                Alert(status->valuestring, message->valuestring, emotion->valuestring, Lang::Sounds::P3_VIBRATION);
            } else {
                ESP_LOGW(TAG, "Alert command requires status, message and emotion");
            }
        }
    });
    bool protocol_started = protocol_->Start();

    audio_processor_->Initialize(codec);
    audio_processor_->OnOutput([this](std::vector<int16_t>&& data) {
        background_task_->Schedule([this, data = std::move(data)]() mutable {
            if (protocol_->IsAudioChannelBusy()) {
                return;
            }
            opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
                AudioStreamPacket packet;
                packet.payload = std::move(opus);
                uint32_t last_output_timestamp_value = last_output_timestamp_.load();
                {
                    std::lock_guard<std::mutex> lock(timestamp_mutex_);
                    if (!timestamp_queue_.empty()) {
                        packet.timestamp = timestamp_queue_.front();
                        timestamp_queue_.pop_front();
                    } else {
                        packet.timestamp = 0;
                    }

                    if (timestamp_queue_.size() > 3) { // 限制队列长度3
                        timestamp_queue_.pop_front(); // 该包发送前先出队保持队列长度
                        return;
                    }
                }
                Schedule([this, last_output_timestamp_value, packet = std::move(packet)]() {
                    protocol_->SendAudio(packet);
                    // ESP_LOGI(TAG, "Send %zu bytes, timestamp %lu, last_ts %lu, qsize %zu",
                    //     packet.payload.size(), packet.timestamp, last_output_timestamp_value, timestamp_queue_.size());
                });
            });
        });
    });
    audio_processor_->OnVadStateChange([this](bool speaking) {
        if (device_state_ == kDeviceStateListening) {
            Schedule([this, speaking]() {
                if (speaking) {
                    voice_detected_ = true;
                } else {
                    voice_detected_ = false;
                }
                auto led = Board::GetInstance().GetLed();
                led->OnStateChanged();
            });
        }
    });

#if CONFIG_USE_WAKE_WORD_DETECT
    wake_word_detect_.Initialize(codec);
    wake_word_detect_.OnWakeWordDetected([this](const std::string& wake_word) {
        Schedule([this, &wake_word]() {
            if (device_state_ == kDeviceStateIdle) {
                SetDeviceState(kDeviceStateConnecting);
                wake_word_detect_.EncodeWakeWordData();

                if (!protocol_ || !protocol_->OpenAudioChannel()) {
                    wake_word_detect_.StartDetection();
                    return;
                }
                
                AudioStreamPacket packet;
                // Encode and send the wake word data to the server
                while (wake_word_detect_.GetWakeWordOpus(packet.payload)) {
                    protocol_->SendAudio(packet);
                }
                // Set the chat state to wake word detected
                protocol_->SendWakeWordDetected(wake_word);
                ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
                SetListeningMode(realtime_chat_enabled_ ? kListeningModeRealtime : kListeningModeAutoStop);
            } else if (device_state_ == kDeviceStateSpeaking) {
                AbortSpeaking(kAbortReasonWakeWordDetected);
            } else if (device_state_ == kDeviceStateActivating) {
                SetDeviceState(kDeviceStateIdle);
            }
        });
    });
    wake_word_detect_.StartDetection();
#endif

    // Wait for the new version check to finish
    xEventGroupWaitBits(event_group_, CHECK_NEW_VERSION_DONE_EVENT, pdTRUE, pdFALSE, portMAX_DELAY);
    SetDeviceState(kDeviceStateIdle);

    if (protocol_started) {
        std::string message = std::string(Lang::Strings::VERSION) + ota_.GetCurrentVersion();
        display->ShowNotification(message.c_str());
        display->SetChatMessage("system", "");
        // Play the success sound to indicate the device is ready
        ResetDecoder();
        PlaySound(Lang::Sounds::P3_SUCCESS);
    }
    
    // Enter the main event loop
    MainEventLoop();
}

void Application::OnClockTimer() {
    clock_ticks_++;

    // Print the debug info every 10 seconds
    if (clock_ticks_ % 10 == 0) {
        // SystemInfo::PrintRealTimeStats(pdMS_TO_TICKS(1000));

        int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
        int min_free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL);
        ESP_LOGI(TAG, "Free internal: %u minimal internal: %u", free_sram, min_free_sram);

        // If we have synchronized server time, set the status to clock "HH:MM" if the device is idle
        if (ota_.HasServerTime()) {
            if (device_state_ == kDeviceStateIdle) {
                Schedule([this]() {
                    // Set status to clock "HH:MM"
                    time_t now = time(NULL);
                    char time_str[64];
                    strftime(time_str, sizeof(time_str), "%H:%M  ", localtime(&now));
                    Board::GetInstance().GetDisplay()->SetStatus(time_str);
                });
            }
        }
    }
}

// Add a async task to MainLoop
void Application::Schedule(std::function<void()> callback) {
    {
        std::lock_guard<std::mutex> lock(mutex_);
        main_tasks_.push_back(std::move(callback));
    }
    xEventGroupSetBits(event_group_, SCHEDULE_EVENT);
}

// The Main Event Loop controls the chat state and websocket connection
// If other tasks need to access the websocket or chat state,
// they should use Schedule to call this function
void Application::MainEventLoop() {
    while (true) {
        auto bits = xEventGroupWaitBits(event_group_, SCHEDULE_EVENT, pdTRUE, pdFALSE, portMAX_DELAY);

        if (bits & SCHEDULE_EVENT) {
            std::unique_lock<std::mutex> lock(mutex_);
            std::list<std::function<void()>> tasks = std::move(main_tasks_);
            lock.unlock();
            for (auto& task : tasks) {
                task();
            }
        }
    }
}

// The Audio Loop is used to input and output audio data
void Application::AudioLoop() {
    auto codec = Board::GetInstance().GetAudioCodec();
    while (true) {
        OnAudioInput();
        if (codec->output_enabled()) {
            OnAudioOutput();
        }
    }
}

void Application::OnAudioOutput() {
    if (busy_decoding_audio_) {
        return;
    }

    auto now = std::chrono::steady_clock::now();
    auto codec = Board::GetInstance().GetAudioCodec();
    const int max_silence_seconds = 10;

    std::unique_lock<std::mutex> lock(mutex_);
    if (audio_decode_queue_.empty()) {
        // Disable the output if there is no audio data for a long time
        if (device_state_ == kDeviceStateIdle) {
            auto duration = std::chrono::duration_cast<std::chrono::seconds>(now - last_output_time_).count();
            if (duration > max_silence_seconds) {
                codec->EnableOutput(false);
            }
        }
        return;
    }

    if (device_state_ == kDeviceStateListening) {
        audio_decode_queue_.clear();
        audio_decode_cv_.notify_all();
        return;
    }

    auto packet = std::move(audio_decode_queue_.front());
    audio_decode_queue_.pop_front();
    lock.unlock();
    audio_decode_cv_.notify_all();

    busy_decoding_audio_ = true;
    background_task_->Schedule([this, codec, packet = std::move(packet)]() mutable {
        busy_decoding_audio_ = false;
        if (aborted_) {
            return;
        }

        std::vector<int16_t> pcm;
        if (!opus_decoder_->Decode(std::move(packet.payload), pcm)) {
            return;
        }
        // Resample if the sample rate is different
        if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
            int target_size = output_resampler_.GetOutputSamples(pcm.size());
            std::vector<int16_t> resampled(target_size);
            output_resampler_.Process(pcm.data(), pcm.size(), resampled.data());
            pcm = std::move(resampled);
        }
        codec->OutputData(pcm);
        {
            std::lock_guard<std::mutex> lock(timestamp_mutex_);
            timestamp_queue_.push_back(packet.timestamp);
            last_output_timestamp_ = packet.timestamp;
        }
        last_output_time_ = std::chrono::steady_clock::now();
    });
}

void Application::OnAudioInput() {
#if CONFIG_USE_WAKE_WORD_DETECT
    if (wake_word_detect_.IsDetectionRunning()) {
        std::vector<int16_t> data;
        int samples = wake_word_detect_.GetFeedSize();
        if (samples > 0) {
            ReadAudio(data, 16000, samples);
            wake_word_detect_.Feed(data);
            return;
        }
    }
#endif
    if (audio_processor_->IsRunning()) {
        std::vector<int16_t> data;
        int samples = audio_processor_->GetFeedSize();
        if (samples > 0) {
            ReadAudio(data, 16000, samples);
            audio_processor_->Feed(data);
            return;
        }
    }

    vTaskDelay(pdMS_TO_TICKS(30));
}

void Application::ReadAudio(std::vector<int16_t>& data, int sample_rate, int samples) {
    auto codec = Board::GetInstance().GetAudioCodec();
    if (codec->input_sample_rate() != sample_rate) {
        data.resize(samples * codec->input_sample_rate() / sample_rate);
        if (!codec->InputData(data)) {
            return;
        }
        if (codec->input_channels() == 2) {
            auto mic_channel = std::vector<int16_t>(data.size() / 2);
            auto reference_channel = std::vector<int16_t>(data.size() / 2);
            for (size_t i = 0, j = 0; i < mic_channel.size(); ++i, j += 2) {
                mic_channel[i] = data[j];
                reference_channel[i] = data[j + 1];
            }
            auto resampled_mic = std::vector<int16_t>(input_resampler_.GetOutputSamples(mic_channel.size()));
            auto resampled_reference = std::vector<int16_t>(reference_resampler_.GetOutputSamples(reference_channel.size()));
            input_resampler_.Process(mic_channel.data(), mic_channel.size(), resampled_mic.data());
            reference_resampler_.Process(reference_channel.data(), reference_channel.size(), resampled_reference.data());
            data.resize(resampled_mic.size() + resampled_reference.size());
            for (size_t i = 0, j = 0; i < resampled_mic.size(); ++i, j += 2) {
                data[j] = resampled_mic[i];
                data[j + 1] = resampled_reference[i];
            }
        } else {
            auto resampled = std::vector<int16_t>(input_resampler_.GetOutputSamples(data.size()));
            input_resampler_.Process(data.data(), data.size(), resampled.data());
            data = std::move(resampled);
        }
    } else {
        data.resize(samples);
        if (!codec->InputData(data)) {
            return;
        }
    }
}

void Application::AbortSpeaking(AbortReason reason) {
    ESP_LOGI(TAG, "Abort speaking");
    aborted_ = true;
    protocol_->SendAbortSpeaking(reason);
}

void Application::SetListeningMode(ListeningMode mode) {
    listening_mode_ = mode;
    SetDeviceState(kDeviceStateListening);
}

void Application::SetDeviceState(DeviceState state) {
    if (device_state_ == state) {
        return;
    }
    
    clock_ticks_ = 0;
    auto previous_state = device_state_;
    device_state_ = state;
    ESP_LOGI(TAG, "STATE: %s", STATE_STRINGS[device_state_]);
    // The state is changed, wait for all background tasks to finish
    background_task_->WaitForCompletion();

    auto& board = Board::GetInstance();
    auto display = board.GetDisplay();
    auto led = board.GetLed();
    led->OnStateChanged();
    switch (state) {
        case kDeviceStateUnknown:
        case kDeviceStateIdle:
            display->SetStatus(Lang::Strings::STANDBY);
            display->SetEmotion("neutral");
            audio_processor_->Stop();
            
#if CONFIG_USE_WAKE_WORD_DETECT
            wake_word_detect_.StartDetection();
#endif
            break;
        case kDeviceStateConnecting:
            display->SetStatus(Lang::Strings::CONNECTING);
            display->SetEmotion("neutral");
            display->SetChatMessage("system", "");
            timestamp_queue_.clear();
            last_output_timestamp_ = 0;
            break;
        case kDeviceStateListening:
            display->SetStatus(Lang::Strings::LISTENING);
            display->SetEmotion("neutral");
            // Update the IoT states before sending the start listening command
            UpdateIotStates();

            // Make sure the audio processor is running
            if (!audio_processor_->IsRunning()) {
                // Send the start listening command
                protocol_->SendStartListening(listening_mode_);
                if (listening_mode_ == kListeningModeAutoStop && previous_state == kDeviceStateSpeaking) {
                    // FIXME: Wait for the speaker to empty the buffer
                    vTaskDelay(pdMS_TO_TICKS(120));
                }
                opus_encoder_->ResetState();
#if CONFIG_USE_WAKE_WORD_DETECT
                wake_word_detect_.StopDetection();
#endif
                audio_processor_->Start();
            }
            break;
        case kDeviceStateSpeaking:
            display->SetStatus(Lang::Strings::SPEAKING);

            if (listening_mode_ != kListeningModeRealtime) {
                audio_processor_->Stop();
#if CONFIG_USE_WAKE_WORD_DETECT
                wake_word_detect_.StartDetection();
#endif
            }
            ResetDecoder();
            break;
        default:
            // Do nothing
            break;
    }
}

void Application::ResetDecoder() {
    std::lock_guard<std::mutex> lock(mutex_);
    opus_decoder_->ResetState();
    audio_decode_queue_.clear();
    audio_decode_cv_.notify_all();
    last_output_time_ = std::chrono::steady_clock::now();
    auto codec = Board::GetInstance().GetAudioCodec();
    codec->EnableOutput(true);
}

void Application::SetDecodeSampleRate(int sample_rate, int frame_duration) {
    if (opus_decoder_->sample_rate() == sample_rate && opus_decoder_->duration_ms() == frame_duration) {
        return;
    }

    opus_decoder_.reset();
    opus_decoder_ = std::make_unique<OpusDecoderWrapper>(sample_rate, 1, frame_duration);

    auto codec = Board::GetInstance().GetAudioCodec();
    if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
        ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decoder_->sample_rate(), codec->output_sample_rate());
        output_resampler_.Configure(opus_decoder_->sample_rate(), codec->output_sample_rate());
    }
}

void Application::UpdateIotStates() {
    auto& thing_manager = iot::ThingManager::GetInstance();
    std::string states;
    if (thing_manager.GetStatesJson(states, true)) {
        protocol_->SendIotStates(states);
    }
}

void Application::Reboot() {
    ESP_LOGI(TAG, "Rebooting...");
    esp_restart();
}

void Application::WakeWordInvoke(const std::string& wake_word) {
    if (device_state_ == kDeviceStateIdle) {
        ToggleChatState();
        Schedule([this, wake_word]() {
            if (protocol_) {
                protocol_->SendWakeWordDetected(wake_word); 
            }
        }); 
    } else if (device_state_ == kDeviceStateSpeaking) {
        Schedule([this]() {
            AbortSpeaking(kAbortReasonNone);
        });
    } else if (device_state_ == kDeviceStateListening) {   
        Schedule([this]() {
            if (protocol_) {
                protocol_->CloseAudioChannel();
            }
        });
    }
}

bool Application::CanEnterSleepMode() {
    if (device_state_ != kDeviceStateIdle) {
        return false;
    }

    if (protocol_ && protocol_->IsAudioChannelOpened()) {
        return false;
    }

    // Now it is safe to enter sleep mode
    return true;
}

```

## /main/application.h

```h path="/main/application.h" 
#ifndef _APPLICATION_H_
#define _APPLICATION_H_

#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <freertos/task.h>
#include <esp_timer.h>

#include <string>
#include <mutex>
#include <list>
#include <vector>
#include <condition_variable>
#include <memory>

#include <opus_encoder.h>
#include <opus_decoder.h>
#include <opus_resampler.h>

#include "protocol.h"
#include "ota.h"
#include "background_task.h"
#include "audio_processor.h"

#if CONFIG_USE_WAKE_WORD_DETECT
#include "wake_word_detect.h"
#endif

#define SCHEDULE_EVENT (1 << 0)
#define AUDIO_INPUT_READY_EVENT (1 << 1)
#define AUDIO_OUTPUT_READY_EVENT (1 << 2)
#define CHECK_NEW_VERSION_DONE_EVENT (1 << 3)

enum DeviceState {
    kDeviceStateUnknown,
    kDeviceStateStarting,
    kDeviceStateWifiConfiguring,
    kDeviceStateIdle,
    kDeviceStateConnecting,
    kDeviceStateListening,
    kDeviceStateSpeaking,
    kDeviceStateUpgrading,
    kDeviceStateActivating,
    kDeviceStateFatalError
};

#define OPUS_FRAME_DURATION_MS 60

class Application {
public:
    static Application& GetInstance() {
        static Application instance;
        return instance;
    }
    // 删除拷贝构造函数和赋值运算符
    Application(const Application&) = delete;
    Application& operator=(const Application&) = delete;

    void Start();
    DeviceState GetDeviceState() const { return device_state_; }
    bool IsVoiceDetected() const { return voice_detected_; }
    void Schedule(std::function<void()> callback);
    void SetDeviceState(DeviceState state);
    void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");
    void DismissAlert();
    void AbortSpeaking(AbortReason reason);
    void ToggleChatState();
    void StartListening();
    void StopListening();
    void UpdateIotStates();
    void Reboot();
    void WakeWordInvoke(const std::string& wake_word);
    void PlaySound(const std::string_view& sound);
    bool CanEnterSleepMode();

private:
    Application();
    ~Application();

#if CONFIG_USE_WAKE_WORD_DETECT
    WakeWordDetect wake_word_detect_;
#endif
    std::unique_ptr<AudioProcessor> audio_processor_;
    Ota ota_;
    std::mutex mutex_;
    std::list<std::function<void()>> main_tasks_;
    std::unique_ptr<Protocol> protocol_;
    EventGroupHandle_t event_group_ = nullptr;
    esp_timer_handle_t clock_timer_handle_ = nullptr;
    volatile DeviceState device_state_ = kDeviceStateUnknown;
    ListeningMode listening_mode_ = kListeningModeAutoStop;
#if CONFIG_USE_DEVICE_AEC || CONFIG_USE_SERVER_AEC
    bool realtime_chat_enabled_ = true;
#else
    bool realtime_chat_enabled_ = false;
#endif
    bool aborted_ = false;
    bool voice_detected_ = false;
    bool busy_decoding_audio_ = false;
    int clock_ticks_ = 0;
    TaskHandle_t check_new_version_task_handle_ = nullptr;

    // Audio encode / decode
    TaskHandle_t audio_loop_task_handle_ = nullptr;
    BackgroundTask* background_task_ = nullptr;
    std::chrono::steady_clock::time_point last_output_time_;
    std::list<AudioStreamPacket> audio_decode_queue_;
    std::condition_variable audio_decode_cv_;

    // 新增:用于维护音频包的timestamp队列
    std::list<uint32_t> timestamp_queue_;
    std::mutex timestamp_mutex_;
    std::atomic<uint32_t> last_output_timestamp_ = 0;

    std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
    std::unique_ptr<OpusDecoderWrapper> opus_decoder_;

    OpusResampler input_resampler_;
    OpusResampler reference_resampler_;
    OpusResampler output_resampler_;

    void MainEventLoop();
    void OnAudioInput();
    void OnAudioOutput();
    void ReadAudio(std::vector<int16_t>& data, int sample_rate, int samples);
    void ResetDecoder();
    void SetDecodeSampleRate(int sample_rate, int frame_duration);
    void CheckNewVersion();
    void ShowActivationCode();
    void OnClockTimer();
    void SetListeningMode(ListeningMode mode);
    void AudioLoop();
};

#endif // _APPLICATION_H_

```

## /main/assets/common/exclamation.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/common/exclamation.p3

## /main/assets/common/low_battery.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/common/low_battery.p3

## /main/assets/common/success.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/common/success.p3

## /main/assets/common/vibration.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/common/vibration.p3

## /main/assets/en-US/0.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/0.p3

## /main/assets/en-US/1.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/1.p3

## /main/assets/en-US/2.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/2.p3

## /main/assets/en-US/3.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/3.p3

## /main/assets/en-US/4.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/4.p3

## /main/assets/en-US/5.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/5.p3

## /main/assets/en-US/6.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/6.p3

## /main/assets/en-US/7.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/7.p3

## /main/assets/en-US/8.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/8.p3

## /main/assets/en-US/9.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/9.p3

## /main/assets/en-US/activation.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/activation.p3

## /main/assets/en-US/err_pin.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/err_pin.p3

## /main/assets/en-US/err_reg.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/err_reg.p3

## /main/assets/en-US/language.json

```json path="/main/assets/en-US/language.json" 
{
    "language": {
        "type": "en-US"
    },
    "strings": {
        "WARNING": "Warning",
        "INFO": "Information",
        "ERROR": "Error",
        "VERSION": "Ver ",
        "LOADING_PROTOCOL": "Logging in...",
        "INITIALIZING": "Initializing...",
        "PIN_ERROR": "Please insert SIM card",
        "REG_ERROR": "Unable to access network, please check SIM card status",
        "DETECTING_MODULE": "Detecting module...",
        "REGISTERING_NETWORK": "Waiting for network...",
        "CHECKING_NEW_VERSION": "Checking for new version...",
        "CHECK_NEW_VERSION_FAILED": "Check for new version failed, will retry in %d seconds: %s",
        "SWITCH_TO_WIFI_NETWORK": "Switching to Wi-Fi...",
        "SWITCH_TO_4G_NETWORK": "Switching to 4G...",

        "STANDBY": "Standby",
        "CONNECT_TO": "Connect to ",
        "CONNECTING": "Connecting...",
        "CONNECTION_SUCCESSFUL": "Connection Successful",
        "CONNECTED_TO": "Connected to ",

        "LISTENING": "Listening...",
        "SPEAKING": "Speaking...",

        "SERVER_NOT_FOUND": "Looking for available service",
        "SERVER_NOT_CONNECTED": "Unable to connect to service, please try again later",
        "SERVER_TIMEOUT": "Waiting for response timeout",
        "SERVER_ERROR": "Sending failed, please check the network",

        "CONNECT_TO_HOTSPOT": "Hotspot: ",
        "ACCESS_VIA_BROWSER": " Config URL: ",
        "WIFI_CONFIG_MODE": "Wi-Fi Configuration Mode",
        "ENTERING_WIFI_CONFIG_MODE": "Entering Wi-Fi configuration mode...",
        "SCANNING_WIFI": "Scanning Wi-Fi...",

        "NEW_VERSION": "New version ",
        "OTA_UPGRADE": "OTA Upgrade",
        "UPGRADING": "System is upgrading...",
        "UPGRADE_FAILED": "Upgrade failed",
        "ACTIVATION": "Activation",

        "BATTERY_LOW": "Low battery",
        "BATTERY_CHARGING": "Charging",
        "BATTERY_FULL": "Battery full",
        "BATTERY_NEED_CHARGE": "Low battery, please charge",

        "VOLUME": "Volume ",
        "MUTED": "Muted",
        "MAX_VOLUME": "Max volume"
    }
}
```

## /main/assets/en-US/upgrade.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/upgrade.p3

## /main/assets/en-US/welcome.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/welcome.p3

## /main/assets/en-US/wificonfig.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/en-US/wificonfig.p3

## /main/assets/ja-JP/0.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/0.p3

## /main/assets/ja-JP/1.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/1.p3

## /main/assets/ja-JP/2.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/2.p3

## /main/assets/ja-JP/3.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/3.p3

## /main/assets/ja-JP/4.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/4.p3

## /main/assets/ja-JP/5.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/5.p3

## /main/assets/ja-JP/6.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/6.p3

## /main/assets/ja-JP/7.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/7.p3

## /main/assets/ja-JP/8.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/8.p3

## /main/assets/ja-JP/9.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/9.p3

## /main/assets/ja-JP/activation.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/activation.p3

## /main/assets/ja-JP/err_pin.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/err_pin.p3

## /main/assets/ja-JP/err_reg.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/err_reg.p3

## /main/assets/ja-JP/language.json

```json path="/main/assets/ja-JP/language.json" 
{
    "language": {
        "type": "ja-JP"
    },
    "strings": {
        "WARNING": "警告",
        "INFO": "情報",
        "ERROR": "エラー",
        "VERSION": "バージョン ",
        "LOADING_PROTOCOL": "サーバーにログイン中...",
        "INITIALIZING": "初期化中...",
        "PIN_ERROR": "SIMカードを挿入してください",
        "REG_ERROR": "ネットワークに接続できません。ネットワーク状態を確認してください",
        "DETECTING_MODULE": "モジュールを検出中...",
        "REGISTERING_NETWORK": "ネットワーク接続待機中...",
        "CHECKING_NEW_VERSION": "新しいバージョンを確認中...",
        "CHECK_NEW_VERSION_FAILED": "更新確認に失敗しました。%d 秒後に再試行します: %s",
        "SWITCH_TO_WIFI_NETWORK": "Wi-Fiに切り替え中...",
        "SWITCH_TO_4G_NETWORK": "4Gに切り替え中...",

        "STANDBY": "待機中",
        "CONNECT_TO": "接続先 ",
        "CONNECTING": "接続中...",
        "CONNECTED_TO": "接続完了 ",

        "LISTENING": "リスニング中...",
        "SPEAKING": "話しています...",

        "SERVER_NOT_FOUND": "利用可能なサーバーを探しています",
        "SERVER_NOT_CONNECTED": "サーバーに接続できません。後でもう一度お試しください",
        "SERVER_TIMEOUT": "応答待機時間が終了しました",
        "SERVER_ERROR": "送信に失敗しました。ネットワークを確認してください",

        "CONNECT_TO_HOTSPOT": "スマートフォンをWi-Fi ",
        "ACCESS_VIA_BROWSER": " に接続し、ブラウザでアクセスしてください ",
        "WIFI_CONFIG_MODE": "ネットワーク設定モード",
        "ENTERING_WIFI_CONFIG_MODE": "ネットワーク設定中...",
        "SCANNING_WIFI": "Wi-Fiをスキャン中...",

        "NEW_VERSION": "新しいバージョン ",
        "OTA_UPGRADE": "OTAアップグレード",
        "UPGRADING": "システムをアップグレード中...",
        "UPGRADE_FAILED": "アップグレード失敗",
        "ACTIVATION": "デバイスをアクティベート",

        "BATTERY_LOW": "バッテリーが少なくなっています",
        "BATTERY_CHARGING": "充電中",
        "BATTERY_FULL": "バッテリー満タン",
        "BATTERY_NEED_CHARGE": "バッテリーが低下しています。充電してください",

        "VOLUME": "音量 ",
        "MUTED": "ミュートされています",
        "MAX_VOLUME": "最大音量"
    }
}

```

## /main/assets/ja-JP/upgrade.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/upgrade.p3

## /main/assets/ja-JP/welcome.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/welcome.p3

## /main/assets/ja-JP/wificonfig.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/ja-JP/wificonfig.p3

## /main/assets/zh-CN/0.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/0.p3

## /main/assets/zh-CN/1.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/1.p3

## /main/assets/zh-CN/2.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/2.p3

## /main/assets/zh-CN/3.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/3.p3

## /main/assets/zh-CN/4.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/4.p3

## /main/assets/zh-CN/5.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/5.p3

## /main/assets/zh-CN/6.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/6.p3

## /main/assets/zh-CN/7.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/7.p3

## /main/assets/zh-CN/8.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/8.p3

## /main/assets/zh-CN/9.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/9.p3

## /main/assets/zh-CN/activation.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/activation.p3

## /main/assets/zh-CN/err_pin.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/err_pin.p3

## /main/assets/zh-CN/err_reg.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/err_reg.p3

## /main/assets/zh-CN/language.json

```json path="/main/assets/zh-CN/language.json" 
{
    "language": {
        "type" :"zh-CN"
    },
    "strings": {
        "WARNING":"警告",
        "INFO":"信息",
        "ERROR":"错误",
        "VERSION": "版本 ",
        "LOADING_PROTOCOL":"登录服务器...",
        "INITIALIZING":"正在初始化...",
        "PIN_ERROR":"请插入 SIM 卡",
        "REG_ERROR":"无法接入网络,请检查流量卡状态",
        "DETECTING_MODULE":"检测模组...",
        "REGISTERING_NETWORK":"等待网络...",
        "CHECKING_NEW_VERSION":"检查新版本...",
        "CHECK_NEW_VERSION_FAILED":"检查新版本失败,将在 %d 秒后重试:%s",
        "SWITCH_TO_WIFI_NETWORK":"切换到 Wi-Fi...",
        "SWITCH_TO_4G_NETWORK":"切换到 4G...",

        "STANDBY":"待命",
        "CONNECT_TO":"连接 ",
        "CONNECTING":"连接中...",
        "CONNECTED_TO":"已连接 ",

        "LISTENING":"聆听中...",
        "SPEAKING":"说话中...",

        "SERVER_NOT_FOUND":"正在寻找可用服务",
        "SERVER_NOT_CONNECTED":"无法连接服务,请稍后再试",
        "SERVER_TIMEOUT":"等待响应超时",
        "SERVER_ERROR":"发送失败,请检查网络",

        "CONNECT_TO_HOTSPOT":"手机连接热点 ",
        "ACCESS_VIA_BROWSER":",浏览器访问 ",
        "WIFI_CONFIG_MODE":"配网模式",
        "ENTERING_WIFI_CONFIG_MODE":"进入配网模式...",
        "SCANNING_WIFI":"扫描 Wi-Fi...",

        "NEW_VERSION": "新版本 ",
        "OTA_UPGRADE":"OTA 升级",
        "UPGRADING":"正在升级系统...",
        "UPGRADE_FAILED":"升级失败",
        "ACTIVATION":"激活设备",

        "BATTERY_LOW":"电量不足",
        "BATTERY_CHARGING":"正在充电",
        "BATTERY_FULL":"电量已满",
        "BATTERY_NEED_CHARGE":"电量低,请充电",

        "VOLUME":"音量 ",
        "MUTED":"已静音",
        "MAX_VOLUME":"最大音量"
    }
}

```

## /main/assets/zh-CN/upgrade.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/upgrade.p3

## /main/assets/zh-CN/welcome.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/welcome.p3

## /main/assets/zh-CN/wificonfig.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-CN/wificonfig.p3

## /main/assets/zh-TW/0.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/0.p3

## /main/assets/zh-TW/1.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/1.p3

## /main/assets/zh-TW/2.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/2.p3

## /main/assets/zh-TW/3.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/3.p3

## /main/assets/zh-TW/4.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/4.p3

## /main/assets/zh-TW/5.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/5.p3

## /main/assets/zh-TW/6.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/6.p3

## /main/assets/zh-TW/7.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/7.p3

## /main/assets/zh-TW/8.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/8.p3

## /main/assets/zh-TW/9.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/9.p3

## /main/assets/zh-TW/activation.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/activation.p3

## /main/assets/zh-TW/err_pin.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/err_pin.p3

## /main/assets/zh-TW/err_reg.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/err_reg.p3

## /main/assets/zh-TW/language.json

```json path="/main/assets/zh-TW/language.json" 
{
    "language": {
        "type": "zh-TW"
    },
    "strings": {
        "WARNING": "警告",
        "INFO": "資訊",
        "ERROR": "錯誤",
        "VERSION": "版本 ",
        "LOADING_PROTOCOL": "登入伺服器...",
        "INITIALIZING": "正在初始化...",
        "PIN_ERROR": "請插入 SIM 卡",
        "REG_ERROR": "無法接入網絡,請檢查網路狀態",
        "DETECTING_MODULE": "檢測模組...",
        "REGISTERING_NETWORK": "等待網絡...",
        "CHECKING_NEW_VERSION": "檢查新版本...",
        "CHECK_NEW_VERSION_FAILED": "檢查新版本失敗,將在 %d 秒後重試:%s",
        "SWITCH_TO_WIFI_NETWORK": "切換到 Wi-Fi...",
        "SWITCH_TO_4G_NETWORK": "切換到 4G...",

        "STANDBY": "待命",
        "CONNECT_TO": "連接 ",
        "CONNECTING": "連接中...",
        "CONNECTED_TO": "已連接 ",

        "LISTENING": "聆聽中...",
        "SPEAKING": "說話中...",

        "SERVER_NOT_FOUND": "正在尋找可用服務",
        "SERVER_NOT_CONNECTED": "無法連接服務,請稍後再試",
        "SERVER_TIMEOUT": "等待響應超時",
        "SERVER_ERROR": "發送失敗,請檢查網絡",

        "CONNECT_TO_HOTSPOT": "手機連接WiFi ",
        "ACCESS_VIA_BROWSER": ",瀏覽器訪問 ",
        "WIFI_CONFIG_MODE": "網路設定模式",
        "ENTERING_WIFI_CONFIG_MODE": "正在設定網路...",
        "SCANNING_WIFI": "掃描 Wi-Fi...",

        "NEW_VERSION": "新版本 ",
        "OTA_UPGRADE": "OTA 升級",
        "UPGRADING": "正在升級系統...",
        "UPGRADE_FAILED": "升級失敗",
        "ACTIVATION": "啟用設備",

        "BATTERY_LOW": "電量不足",
        "BATTERY_CHARGING": "正在充電",
        "BATTERY_FULL": "電量已滿",
        "BATTERY_NEED_CHARGE": "電量低,請充電",

        "VOLUME": "音量 ",
        "MUTED": "已靜音",
        "MAX_VOLUME": "最大音量"
    }
}

```

## /main/assets/zh-TW/upgrade.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/upgrade.p3

## /main/assets/zh-TW/welcome.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/welcome.p3

## /main/assets/zh-TW/wificonfig.p3

Binary file available at https://raw.githubusercontent.com/78/xiaozhi-esp32/refs/heads/main/main/assets/zh-TW/wificonfig.p3

## /main/audio_codecs/audio_codec.cc

```cc path="/main/audio_codecs/audio_codec.cc" 
#include "audio_codec.h"
#include "board.h"
#include "settings.h"

#include <esp_log.h>
#include <cstring>
#include <driver/i2s_common.h>

#define TAG "AudioCodec"

AudioCodec::AudioCodec() {
}

AudioCodec::~AudioCodec() {
}

void AudioCodec::OutputData(std::vector<int16_t>& data) {
    Write(data.data(), data.size());
}

bool AudioCodec::InputData(std::vector<int16_t>& data) {
    int samples = Read(data.data(), data.size());
    if (samples > 0) {
        return true;
    }
    return false;
}

void AudioCodec::Start() {
    Settings settings("audio", false);
    output_volume_ = settings.GetInt("output_volume", output_volume_);
    if (output_volume_ <= 0) {
        ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_);
        output_volume_ = 10;
    }

    ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
    ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));

    EnableInput(true);
    EnableOutput(true);
    ESP_LOGI(TAG, "Audio codec started");
}

void AudioCodec::SetOutputVolume(int volume) {
    output_volume_ = volume;
    ESP_LOGI(TAG, "Set output volume to %d", output_volume_);
    
    Settings settings("audio", true);
    settings.SetInt("output_volume", output_volume_);
}

void AudioCodec::EnableInput(bool enable) {
    if (enable == input_enabled_) {
        return;
    }
    input_enabled_ = enable;
    ESP_LOGI(TAG, "Set input enable to %s", enable ? "true" : "false");
}

void AudioCodec::EnableOutput(bool enable) {
    if (enable == output_enabled_) {
        return;
    }
    output_enabled_ = enable;
    ESP_LOGI(TAG, "Set output enable to %s", enable ? "true" : "false");
}

```

## /main/audio_codecs/audio_codec.h

```h path="/main/audio_codecs/audio_codec.h" 
#ifndef _AUDIO_CODEC_H
#define _AUDIO_CODEC_H

#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <driver/i2s_std.h>

#include <vector>
#include <string>
#include <functional>

#include "board.h"

#define AUDIO_CODEC_DMA_DESC_NUM 6
#define AUDIO_CODEC_DMA_FRAME_NUM 240

class AudioCodec {
public:
    AudioCodec();
    virtual ~AudioCodec();
    
    virtual void SetOutputVolume(int volume);
    virtual void EnableInput(bool enable);
    virtual void EnableOutput(bool enable);

    void Start();
    void OutputData(std::vector<int16_t>& data);
    bool InputData(std::vector<int16_t>& data);

    inline bool duplex() const { return duplex_; }
    inline bool input_reference() const { return input_reference_; }
    inline int input_sample_rate() const { return input_sample_rate_; }
    inline int output_sample_rate() const { return output_sample_rate_; }
    inline int input_channels() const { return input_channels_; }
    inline int output_channels() const { return output_channels_; }
    inline int output_volume() const { return output_volume_; }
    inline bool input_enabled() const { return input_enabled_; }
    inline bool output_enabled() const { return output_enabled_; }

protected:
    i2s_chan_handle_t tx_handle_ = nullptr;
    i2s_chan_handle_t rx_handle_ = nullptr;

    bool duplex_ = false;
    bool input_reference_ = false;
    bool input_enabled_ = false;
    bool output_enabled_ = false;
    int input_sample_rate_ = 0;
    int output_sample_rate_ = 0;
    int input_channels_ = 1;
    int output_channels_ = 1;
    int output_volume_ = 70;

    virtual int Read(int16_t* dest, int samples) = 0;
    virtual int Write(const int16_t* data, int samples) = 0;
};

#endif // _AUDIO_CODEC_H

```

## /main/audio_codecs/box_audio_codec.cc

```cc path="/main/audio_codecs/box_audio_codec.cc" 
#include "box_audio_codec.h"

#include <esp_log.h>
#include <driver/i2c_master.h>
#include <driver/i2s_tdm.h>

static const char TAG[] = "BoxAudioCodec";

BoxAudioCodec::BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate,
    gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
    gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference) {
    duplex_ = true; // 是否双工
    input_reference_ = input_reference; // 是否使用参考输入,实现回声消除
    input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
    input_sample_rate_ = input_sample_rate;
    output_sample_rate_ = output_sample_rate;

    CreateDuplexChannels(mclk, bclk, ws, dout, din);

    // Do initialize of related interface: data_if, ctrl_if and gpio_if
    audio_codec_i2s_cfg_t i2s_cfg = {
        .port = I2S_NUM_0,
        .rx_handle = rx_handle_,
        .tx_handle = tx_handle_,
    };
    data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
    assert(data_if_ != NULL);

    // Output
    audio_codec_i2c_cfg_t i2c_cfg = {
        .port = (i2c_port_t)1,
        .addr = es8311_addr,
        .bus_handle = i2c_master_handle,
    };
    out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
    assert(out_ctrl_if_ != NULL);

    gpio_if_ = audio_codec_new_gpio();
    assert(gpio_if_ != NULL);

    es8311_codec_cfg_t es8311_cfg = {};
    es8311_cfg.ctrl_if = out_ctrl_if_;
    es8311_cfg.gpio_if = gpio_if_;
    es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC;
    es8311_cfg.pa_pin = pa_pin;
    es8311_cfg.use_mclk = true;
    es8311_cfg.hw_gain.pa_voltage = 5.0;
    es8311_cfg.hw_gain.codec_dac_voltage = 3.3;
    out_codec_if_ = es8311_codec_new(&es8311_cfg);
    assert(out_codec_if_ != NULL);

    esp_codec_dev_cfg_t dev_cfg = {
        .dev_type = ESP_CODEC_DEV_TYPE_OUT,
        .codec_if = out_codec_if_,
        .data_if = data_if_,
    };
    output_dev_ = esp_codec_dev_new(&dev_cfg);
    assert(output_dev_ != NULL);

    // Input
    i2c_cfg.addr = es7210_addr;
    in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
    assert(in_ctrl_if_ != NULL);

    es7210_codec_cfg_t es7210_cfg = {};
    es7210_cfg.ctrl_if = in_ctrl_if_;
    es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2 | ES7120_SEL_MIC3 | ES7120_SEL_MIC4;
    in_codec_if_ = es7210_codec_new(&es7210_cfg);
    assert(in_codec_if_ != NULL);

    dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
    dev_cfg.codec_if = in_codec_if_;
    input_dev_ = esp_codec_dev_new(&dev_cfg);
    assert(input_dev_ != NULL);

    ESP_LOGI(TAG, "BoxAudioDevice initialized");
}

BoxAudioCodec::~BoxAudioCodec() {
    ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
    esp_codec_dev_delete(output_dev_);
    ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
    esp_codec_dev_delete(input_dev_);

    audio_codec_delete_codec_if(in_codec_if_);
    audio_codec_delete_ctrl_if(in_ctrl_if_);
    audio_codec_delete_codec_if(out_codec_if_);
    audio_codec_delete_ctrl_if(out_ctrl_if_);
    audio_codec_delete_gpio_if(gpio_if_);
    audio_codec_delete_data_if(data_if_);
}

void BoxAudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
    assert(input_sample_rate_ == output_sample_rate_);

    i2s_chan_config_t chan_cfg = {
        .id = I2S_NUM_0,
        .role = I2S_ROLE_MASTER,
        .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
        .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));

    i2s_std_config_t std_cfg = {
        .clk_cfg = {
            .sample_rate_hz = (uint32_t)output_sample_rate_,
            .clk_src = I2S_CLK_SRC_DEFAULT,
            .ext_clk_freq_hz = 0,
            .mclk_multiple = I2S_MCLK_MULTIPLE_256
        },
        .slot_cfg = {
            .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
            .slot_mode = I2S_SLOT_MODE_STEREO,
            .slot_mask = I2S_STD_SLOT_BOTH,
            .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
            .ws_pol = false,
            .bit_shift = true,
            .left_align = true,
            .big_endian = false,
            .bit_order_lsb = false
        },
        .gpio_cfg = {
            .mclk = mclk,
            .bclk = bclk,
            .ws = ws,
            .dout = dout,
            .din = I2S_GPIO_UNUSED,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };

    i2s_tdm_config_t tdm_cfg = {
        .clk_cfg = {
            .sample_rate_hz = (uint32_t)input_sample_rate_,
            .clk_src = I2S_CLK_SRC_DEFAULT,
            .ext_clk_freq_hz = 0,
            .mclk_multiple = I2S_MCLK_MULTIPLE_256,
            .bclk_div = 8,
        },
        .slot_cfg = {
            .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
            .slot_mode = I2S_SLOT_MODE_STEREO,
            .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3),
            .ws_width = I2S_TDM_AUTO_WS_WIDTH,
            .ws_pol = false,
            .bit_shift = true,
            .left_align = false,
            .big_endian = false,
            .bit_order_lsb = false,
            .skip_mask = false,
            .total_slot = I2S_TDM_AUTO_SLOT_NUM
        },
        .gpio_cfg = {
            .mclk = mclk,
            .bclk = bclk,
            .ws = ws,
            .dout = I2S_GPIO_UNUSED,
            .din = din,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };

    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg));
    ESP_LOGI(TAG, "Duplex channels created");
}

void BoxAudioCodec::SetOutputVolume(int volume) {
    ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
    AudioCodec::SetOutputVolume(volume);
}

void BoxAudioCodec::EnableInput(bool enable) {
    if (enable == input_enabled_) {
        return;
    }
    if (enable) {
        esp_codec_dev_sample_info_t fs = {
            .bits_per_sample = 16,
            .channel = 4,
            .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
            .sample_rate = (uint32_t)output_sample_rate_,
            .mclk_multiple = 0,
        };
        if (input_reference_) {
            fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1);
        }
        ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
        ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), 40.0));
    } else {
        ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
    }
    AudioCodec::EnableInput(enable);
}

void BoxAudioCodec::EnableOutput(bool enable) {
    if (enable == output_enabled_) {
        return;
    }
    if (enable) {
        // Play 16bit 1 channel
        esp_codec_dev_sample_info_t fs = {
            .bits_per_sample = 16,
            .channel = 1,
            .channel_mask = 0,
            .sample_rate = (uint32_t)output_sample_rate_,
            .mclk_multiple = 0,
        };
        ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
        ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
    } else {
        ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
    }
    AudioCodec::EnableOutput(enable);
}

int BoxAudioCodec::Read(int16_t* dest, int samples) {
    if (input_enabled_) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
    }
    return samples;
}

int BoxAudioCodec::Write(const int16_t* data, int samples) {
    if (output_enabled_) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
    }
    return samples;
}
```

## /main/audio_codecs/box_audio_codec.h

```h path="/main/audio_codecs/box_audio_codec.h" 
#ifndef _BOX_AUDIO_CODEC_H
#define _BOX_AUDIO_CODEC_H

#include "audio_codec.h"

#include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h>

class BoxAudioCodec : public AudioCodec {
private:
    const audio_codec_data_if_t* data_if_ = nullptr;
    const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr;
    const audio_codec_if_t* out_codec_if_ = nullptr;
    const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr;
    const audio_codec_if_t* in_codec_if_ = nullptr;
    const audio_codec_gpio_if_t* gpio_if_ = nullptr;

    esp_codec_dev_handle_t output_dev_ = nullptr;
    esp_codec_dev_handle_t input_dev_ = nullptr;

    void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);

    virtual int Read(int16_t* dest, int samples) override;
    virtual int Write(const int16_t* data, int samples) override;

public:
    BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate,
        gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
        gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference);
    virtual ~BoxAudioCodec();

    virtual void SetOutputVolume(int volume) override;
    virtual void EnableInput(bool enable) override;
    virtual void EnableOutput(bool enable) override;
};

#endif // _BOX_AUDIO_CODEC_H

```

## /main/audio_codecs/es8311_audio_codec.cc

```cc path="/main/audio_codecs/es8311_audio_codec.cc" 
#include "es8311_audio_codec.h"

#include <esp_log.h>

static const char TAG[] = "Es8311AudioCodec";

Es8311AudioCodec::Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
    gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
    gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk) {
    duplex_ = true; // 是否双工
    input_reference_ = false; // 是否使用参考输入,实现回声消除
    input_channels_ = 1; // 输入通道数
    input_sample_rate_ = input_sample_rate;
    output_sample_rate_ = output_sample_rate;
    pa_pin_ = pa_pin;
    CreateDuplexChannels(mclk, bclk, ws, dout, din);

    // Do initialize of related interface: data_if, ctrl_if and gpio_if
    audio_codec_i2s_cfg_t i2s_cfg = {
        .port = I2S_NUM_0,
        .rx_handle = rx_handle_,
        .tx_handle = tx_handle_,
    };
    data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
    assert(data_if_ != NULL);

    // Output
    audio_codec_i2c_cfg_t i2c_cfg = {
        .port = i2c_port,
        .addr = es8311_addr,
        .bus_handle = i2c_master_handle,
    };
    ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
    assert(ctrl_if_ != NULL);

    gpio_if_ = audio_codec_new_gpio();
    assert(gpio_if_ != NULL);

    es8311_codec_cfg_t es8311_cfg = {};
    es8311_cfg.ctrl_if = ctrl_if_;
    es8311_cfg.gpio_if = gpio_if_;
    es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
    es8311_cfg.pa_pin = pa_pin;
    es8311_cfg.use_mclk = use_mclk;
    es8311_cfg.hw_gain.pa_voltage = 5.0;
    es8311_cfg.hw_gain.codec_dac_voltage = 3.3;
    codec_if_ = es8311_codec_new(&es8311_cfg);
    assert(codec_if_ != NULL);

    esp_codec_dev_cfg_t dev_cfg = {
        .dev_type = ESP_CODEC_DEV_TYPE_OUT,
        .codec_if = codec_if_,
        .data_if = data_if_,
    };
    output_dev_ = esp_codec_dev_new(&dev_cfg);
    assert(output_dev_ != NULL);
    dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
    input_dev_ = esp_codec_dev_new(&dev_cfg);
    assert(input_dev_ != NULL);
    esp_codec_set_disable_when_closed(output_dev_, false);
    esp_codec_set_disable_when_closed(input_dev_, false);
    ESP_LOGI(TAG, "Es8311AudioCodec initialized");
}

Es8311AudioCodec::~Es8311AudioCodec() {
    ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
    esp_codec_dev_delete(output_dev_);
    ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
    esp_codec_dev_delete(input_dev_);

    audio_codec_delete_codec_if(codec_if_);
    audio_codec_delete_ctrl_if(ctrl_if_);
    audio_codec_delete_gpio_if(gpio_if_);
    audio_codec_delete_data_if(data_if_);
}

void Es8311AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
    assert(input_sample_rate_ == output_sample_rate_);

    i2s_chan_config_t chan_cfg = {
        .id = I2S_NUM_0,
        .role = I2S_ROLE_MASTER,
        .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
        .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));

    i2s_std_config_t 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_16BIT,
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
            .slot_mode = I2S_SLOT_MODE_STEREO,
            .slot_mask = I2S_STD_SLOT_BOTH,
            .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
            .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 = mclk,
            .bclk = bclk,
            .ws = ws,
            .dout = dout,
            .din = din,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };

    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
    ESP_LOGI(TAG, "Duplex channels created");
}

void Es8311AudioCodec::SetOutputVolume(int volume) {
    ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
    AudioCodec::SetOutputVolume(volume);
}

void Es8311AudioCodec::EnableInput(bool enable) {
    if (enable == input_enabled_) {
        return;
    }
    if (enable) {
        esp_codec_dev_sample_info_t fs = {
            .bits_per_sample = 16,
            .channel = 1,
            .channel_mask = 0,
            .sample_rate = (uint32_t)input_sample_rate_,
            .mclk_multiple = 0,
        };
        ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
        ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 40.0));
    } else {
        ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
    }
    AudioCodec::EnableInput(enable);
}

void Es8311AudioCodec::EnableOutput(bool enable) {
    if (enable == output_enabled_) {
        return;
    }
    if (enable) {
        // Play 16bit 1 channel
        esp_codec_dev_sample_info_t fs = {
            .bits_per_sample = 16,
            .channel = 1,
            .channel_mask = 0,
            .sample_rate = (uint32_t)output_sample_rate_,
            .mclk_multiple = 0,
        };
        ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
        ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
        if (pa_pin_ != GPIO_NUM_NC) {
            gpio_set_level(pa_pin_, 1);
        }
    } else {
        ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
        if (pa_pin_ != GPIO_NUM_NC) {
            gpio_set_level(pa_pin_, 0);
        }
    }
    AudioCodec::EnableOutput(enable);
}

int Es8311AudioCodec::Read(int16_t* dest, int samples) {
    if (input_enabled_) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
    }
    return samples;
}

int Es8311AudioCodec::Write(const int16_t* data, int samples) {
    if (output_enabled_) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
    }
    return samples;
}
```

## /main/audio_codecs/es8311_audio_codec.h

```h path="/main/audio_codecs/es8311_audio_codec.h" 
#ifndef _ES8311_AUDIO_CODEC_H
#define _ES8311_AUDIO_CODEC_H

#include "audio_codec.h"

#include <driver/i2c_master.h>
#include <driver/gpio.h>
#include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h>

class Es8311AudioCodec : public AudioCodec {
private:
    const audio_codec_data_if_t* data_if_ = nullptr;
    const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
    const audio_codec_if_t* codec_if_ = nullptr;
    const audio_codec_gpio_if_t* gpio_if_ = nullptr;

    esp_codec_dev_handle_t output_dev_ = nullptr;
    esp_codec_dev_handle_t input_dev_ = nullptr;
    gpio_num_t pa_pin_ = GPIO_NUM_NC;

    void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);

    virtual int Read(int16_t* dest, int samples) override;
    virtual int Write(const int16_t* data, int samples) override;

public:
    Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
        gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
        gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk = true);
    virtual ~Es8311AudioCodec();

    virtual void SetOutputVolume(int volume) override;
    virtual void EnableInput(bool enable) override;
    virtual void EnableOutput(bool enable) override;
};

#endif // _ES8311_AUDIO_CODEC_H
```

## /main/audio_codecs/es8374_audio_codec.cc

```cc path="/main/audio_codecs/es8374_audio_codec.cc" 
#include "es8374_audio_codec.h"

#include <esp_log.h>

static const char TAG[] = "Es8374AudioCodec";

Es8374AudioCodec::Es8374AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
    gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
    gpio_num_t pa_pin, uint8_t es8374_addr, bool use_mclk) {
    duplex_ = true; // 是否双工
    input_reference_ = false; // 是否使用参考输入,实现回声消除
    input_channels_ = 1; // 输入通道数
    input_sample_rate_ = input_sample_rate;
    output_sample_rate_ = output_sample_rate;
    pa_pin_ = pa_pin;
    CreateDuplexChannels(mclk, bclk, ws, dout, din);

    // Do initialize of related interface: data_if, ctrl_if and gpio_if
    audio_codec_i2s_cfg_t i2s_cfg = {
        .port = I2S_NUM_0,
        .rx_handle = rx_handle_,
        .tx_handle = tx_handle_,
    };
    data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
    assert(data_if_ != NULL);

    // Output
    audio_codec_i2c_cfg_t i2c_cfg = {
        .port = i2c_port,
        .addr = es8374_addr,
        .bus_handle = i2c_master_handle,
    };
    ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
    assert(ctrl_if_ != NULL);

    gpio_if_ = audio_codec_new_gpio();
    assert(gpio_if_ != NULL);

    es8374_codec_cfg_t es8374_cfg = {};
    es8374_cfg.ctrl_if = ctrl_if_;
    es8374_cfg.gpio_if = gpio_if_;
    es8374_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
    es8374_cfg.pa_pin = pa_pin;
    codec_if_ = es8374_codec_new(&es8374_cfg);
    assert(codec_if_ != NULL);

    esp_codec_dev_cfg_t dev_cfg = {
        .dev_type = ESP_CODEC_DEV_TYPE_OUT,
        .codec_if = codec_if_,
        .data_if = data_if_,
    };
    output_dev_ = esp_codec_dev_new(&dev_cfg);
    assert(output_dev_ != NULL);
    dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
    input_dev_ = esp_codec_dev_new(&dev_cfg);
    assert(input_dev_ != NULL);
    esp_codec_set_disable_when_closed(output_dev_, false);
    esp_codec_set_disable_when_closed(input_dev_, false);
    ESP_LOGI(TAG, "Es8374AudioCodec initialized");
}

Es8374AudioCodec::~Es8374AudioCodec() {
    ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
    esp_codec_dev_delete(output_dev_);
    ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
    esp_codec_dev_delete(input_dev_);

    audio_codec_delete_codec_if(codec_if_);
    audio_codec_delete_ctrl_if(ctrl_if_);
    audio_codec_delete_gpio_if(gpio_if_);
    audio_codec_delete_data_if(data_if_);
}

void Es8374AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
    assert(input_sample_rate_ == output_sample_rate_);

    i2s_chan_config_t chan_cfg = {
        .id = I2S_NUM_0,
        .role = I2S_ROLE_MASTER,
        .dma_desc_num = 6,
        .dma_frame_num = 240,
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));

    i2s_std_config_t 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_16BIT,
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
            .slot_mode = I2S_SLOT_MODE_STEREO,
            .slot_mask = I2S_STD_SLOT_BOTH,
            .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
            .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 = mclk,
            .bclk = bclk,
            .ws = ws,
            .dout = dout,
            .din = din,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };

    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
    ESP_LOGI(TAG, "Duplex channels created");
}

void Es8374AudioCodec::SetOutputVolume(int volume) {
    ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
    AudioCodec::SetOutputVolume(volume);
}

void Es8374AudioCodec::EnableInput(bool enable) {
    if (enable == input_enabled_) {
        return;
    }
    if (enable) {
        esp_codec_dev_sample_info_t fs = {
            .bits_per_sample = 16,
            .channel = 1,
            .channel_mask = 0,
            .sample_rate = (uint32_t)input_sample_rate_,
            .mclk_multiple = 0,
        };
        ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
        ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 40.0));
    } else {
        ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
    }
    AudioCodec::EnableInput(enable);
}

void Es8374AudioCodec::EnableOutput(bool enable) {
    if (enable == output_enabled_) {
        return;
    }
    if (enable) {
        // Play 16bit 1 channel
        esp_codec_dev_sample_info_t fs = {
            .bits_per_sample = 16,
            .channel = 1,
            .channel_mask = 0,
            .sample_rate = (uint32_t)output_sample_rate_,
            .mclk_multiple = 0,
        };
        ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
        ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
        if (pa_pin_ != GPIO_NUM_NC) {
            gpio_set_level(pa_pin_, 1);
        }
    } else {
        ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
        if (pa_pin_ != GPIO_NUM_NC) {
            gpio_set_level(pa_pin_, 0);
        }
    }
    AudioCodec::EnableOutput(enable);
}

int Es8374AudioCodec::Read(int16_t* dest, int samples) {
    if (input_enabled_) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
    }
    return samples;
}

int Es8374AudioCodec::Write(const int16_t* data, int samples) {
    if (output_enabled_) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
    }
    return samples;
}
```

## /main/audio_codecs/es8374_audio_codec.h

```h path="/main/audio_codecs/es8374_audio_codec.h" 
#ifndef _ES8374_AUDIO_CODEC_H
#define _ES8374_AUDIO_CODEC_H

#include "audio_codec.h"

#include <driver/i2c.h>
#include <driver/gpio.h>
#include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h>

class Es8374AudioCodec : public AudioCodec {
private:
    const audio_codec_data_if_t* data_if_ = nullptr;
    const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
    const audio_codec_if_t* codec_if_ = nullptr;
    const audio_codec_gpio_if_t* gpio_if_ = nullptr;

    esp_codec_dev_handle_t output_dev_ = nullptr;
    esp_codec_dev_handle_t input_dev_ = nullptr;
    gpio_num_t pa_pin_ = GPIO_NUM_NC;

    void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);

    virtual int Read(int16_t* dest, int samples) override;
    virtual int Write(const int16_t* data, int samples) override;

public:
    Es8374AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
        gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
        gpio_num_t pa_pin, uint8_t es8374_addr, bool use_mclk = true);
    virtual ~Es8374AudioCodec();

    virtual void SetOutputVolume(int volume) override;
    virtual void EnableInput(bool enable) override;
    virtual void EnableOutput(bool enable) override;
};

#endif // _ES8374_AUDIO_CODEC_H
```

## /main/audio_codecs/es8388_audio_codec.cc

```cc path="/main/audio_codecs/es8388_audio_codec.cc" 
#include "es8388_audio_codec.h"

#include <esp_log.h>

static const char TAG[] = "Es8388AudioCodec";

Es8388AudioCodec::Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
    gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
    gpio_num_t pa_pin, uint8_t es8388_addr) {
    duplex_ = true; // 是否双工
    input_reference_ = false; // 是否使用参考输入,实现回声消除
    input_channels_ = 1; // 输入通道数
    input_sample_rate_ = input_sample_rate;
    output_sample_rate_ = output_sample_rate;
    pa_pin_ = pa_pin;                                                                                                                                                                                     CreateDuplexChannels(mclk, bclk, ws, dout, din);

    // Do initialize of related interface: data_if, ctrl_if and gpio_if
    audio_codec_i2s_cfg_t i2s_cfg = {
        .port = I2S_NUM_0,
        .rx_handle = rx_handle_,
        .tx_handle = tx_handle_,
    };
    data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
    assert(data_if_ != NULL);

    // Output
    audio_codec_i2c_cfg_t i2c_cfg = {
        .port = i2c_port,
        .addr = es8388_addr,
        .bus_handle = i2c_master_handle,
    };
    ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
    assert(ctrl_if_ != NULL);

    gpio_if_ = audio_codec_new_gpio();
    assert(gpio_if_ != NULL);

    es8388_codec_cfg_t es8388_cfg = {};
    es8388_cfg.ctrl_if = ctrl_if_;
    es8388_cfg.gpio_if = gpio_if_;
    es8388_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
    es8388_cfg.master_mode = true;
    es8388_cfg.pa_pin = pa_pin;
    es8388_cfg.pa_reverted = false;
    es8388_cfg.hw_gain.pa_voltage = 5.0;
    es8388_cfg.hw_gain.codec_dac_voltage = 3.3;
    codec_if_ = es8388_codec_new(&es8388_cfg);
    assert(codec_if_ != NULL);

    esp_codec_dev_cfg_t outdev_cfg = {
        .dev_type = ESP_CODEC_DEV_TYPE_OUT,
        .codec_if = codec_if_,
        .data_if = data_if_,
    };
    output_dev_ = esp_codec_dev_new(&outdev_cfg);
    assert(output_dev_ != NULL);

    esp_codec_dev_cfg_t indev_cfg = {
        .dev_type = ESP_CODEC_DEV_TYPE_IN,
        .codec_if = codec_if_,
        .data_if = data_if_,
    };
    input_dev_ = esp_codec_dev_new(&indev_cfg);
    assert(input_dev_ != NULL);
    esp_codec_set_disable_when_closed(output_dev_, false);
    esp_codec_set_disable_when_closed(input_dev_, false);
    ESP_LOGI(TAG, "Es8388AudioCodec initialized");
}

Es8388AudioCodec::~Es8388AudioCodec() {
    ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
    esp_codec_dev_delete(output_dev_);
    ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
    esp_codec_dev_delete(input_dev_);

    audio_codec_delete_codec_if(codec_if_);
    audio_codec_delete_ctrl_if(ctrl_if_);
    audio_codec_delete_gpio_if(gpio_if_);
    audio_codec_delete_data_if(data_if_);
}

void Es8388AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din){
    assert(input_sample_rate_ == output_sample_rate_);

    i2s_chan_config_t chan_cfg = {
        .id = I2S_NUM_0,
        .role = I2S_ROLE_MASTER,
        .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
        .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));

    i2s_std_config_t std_cfg = {
        .clk_cfg = {
            .sample_rate_hz = (uint32_t)output_sample_rate_,
            .clk_src = I2S_CLK_SRC_DEFAULT,
            .ext_clk_freq_hz = 0,
            .mclk_multiple = I2S_MCLK_MULTIPLE_256
        },
        .slot_cfg = {
            .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
            .slot_mode = I2S_SLOT_MODE_STEREO,
            .slot_mask = I2S_STD_SLOT_BOTH,
            .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
            .ws_pol = false,
            .bit_shift = true,
            .left_align = true,
            .big_endian = false,
            .bit_order_lsb = false
        },
        .gpio_cfg = {
            .mclk = mclk,
            .bclk = bclk,
            .ws = ws,
            .dout = dout,
            .din = din,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };

    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
    ESP_LOGI(TAG, "Duplex channels created");
}

void Es8388AudioCodec::SetOutputVolume(int volume) {
    ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
    AudioCodec::SetOutputVolume(volume);
}

void Es8388AudioCodec::EnableInput(bool enable) {
    if (enable == input_enabled_) {
        return;
    }
    if (enable) {
        esp_codec_dev_sample_info_t fs = {
            .bits_per_sample = 16,
            .channel = 1,
            .channel_mask = 0,
            .sample_rate = (uint32_t)input_sample_rate_,
            .mclk_multiple = 0,
        };
        ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
        ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0));
    } else {
        ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
    }
    AudioCodec::EnableInput(enable);
}

void Es8388AudioCodec::EnableOutput(bool enable) {
    if (enable == output_enabled_) {
        return;
    }
    if (enable) {
        esp_codec_dev_sample_info_t fs = {
            .bits_per_sample = 16,
            .channel = 1,
            .channel_mask = 0,
            .sample_rate = (uint32_t)output_sample_rate_,
            .mclk_multiple = 0,
        };
        ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
        ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));

        // Set analog output volume to 0dB, default is -45dB
        uint8_t reg_val = 30; // 0dB
        uint8_t regs[] = { 46, 47, 48, 49 }; // HP_LVOL, HP_RVOL, SPK_LVOL, SPK_RVOL
        for (uint8_t reg : regs) {
            ctrl_if_->write_reg(ctrl_if_, reg, 1, &reg_val, 1);
        }

        if (pa_pin_ != GPIO_NUM_NC) {
            gpio_set_level(pa_pin_, 1);
        }
    } else {
        ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
        if (pa_pin_ != GPIO_NUM_NC) {
            gpio_set_level(pa_pin_, 0);
        }
    }
    AudioCodec::EnableOutput(enable);
}

int Es8388AudioCodec::Read(int16_t* dest, int samples) {
    if (input_enabled_) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
    }
    return samples;
}

int Es8388AudioCodec::Write(const int16_t* data, int samples) {
    if (output_enabled_) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
    }
    return samples;
}

```

## /main/audio_codecs/es8388_audio_codec.h

```h path="/main/audio_codecs/es8388_audio_codec.h" 
#ifndef _ES8388_AUDIO_CODEC_H
#define _ES8388_AUDIO_CODEC_H

#include "audio_codec.h"

#include <driver/i2c_master.h>
#include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h>

class Es8388AudioCodec : public AudioCodec {
private:
    const audio_codec_data_if_t* data_if_ = nullptr;
    const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
    const audio_codec_if_t* codec_if_ = nullptr;
    const audio_codec_gpio_if_t* gpio_if_ = nullptr;

    esp_codec_dev_handle_t output_dev_ = nullptr;
    esp_codec_dev_handle_t input_dev_ = nullptr;
    gpio_num_t pa_pin_ = GPIO_NUM_NC;

    void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);

    virtual int Read(int16_t* dest, int samples) override;
    virtual int Write(const int16_t* data, int samples) override;

public:
    Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
        gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
        gpio_num_t pa_pin, uint8_t es8388_addr);
    virtual ~Es8388AudioCodec();

    virtual void SetOutputVolume(int volume) override;
    virtual void EnableInput(bool enable) override;
    virtual void EnableOutput(bool enable) override;
};

#endif // _ES8388_AUDIO_CODEC_H

```

## /main/audio_codecs/no_audio_codec.cc

```cc path="/main/audio_codecs/no_audio_codec.cc" 
#include "no_audio_codec.h"

#include <esp_log.h>
#include <cmath>
#include <cstring>

#define TAG "NoAudioCodec"

NoAudioCodec::~NoAudioCodec() {
    if (rx_handle_ != nullptr) {
        ESP_ERROR_CHECK(i2s_channel_disable(rx_handle_));
    }
    if (tx_handle_ != nullptr) {
        ESP_ERROR_CHECK(i2s_channel_disable(tx_handle_));
    }
}

NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
    duplex_ = true;
    input_sample_rate_ = input_sample_rate;
    output_sample_rate_ = output_sample_rate;

    i2s_chan_config_t chan_cfg = {
        .id = I2S_NUM_0,
        .role = I2S_ROLE_MASTER,
        .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
        .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));

    i2s_std_config_t 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 = I2S_STD_SLOT_LEFT,
            .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 = bclk,
            .ws = ws,
            .dout = dout,
            .din = din,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
    ESP_LOGI(TAG, "Duplex channels created");
}

ATK_NoAudioCodecDuplex::ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
    duplex_ = true;
    input_sample_rate_ = input_sample_rate;
    output_sample_rate_ = output_sample_rate;

    i2s_chan_config_t chan_cfg = {
        .id = I2S_NUM_0,
        .role = I2S_ROLE_MASTER,
        .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
        .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));

    i2s_std_config_t 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_16BIT,
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
            .slot_mode = I2S_SLOT_MODE_STEREO,
            .slot_mask = I2S_STD_SLOT_BOTH,
            .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
            .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 = bclk,
            .ws = ws,
            .dout = dout,
            .din = din,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
    ESP_LOGI(TAG, "Duplex channels created");
}


NoAudioCodecSimplex::NoAudioCodecSimplex(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_ws, 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 chan_cfg = {
        .id = (i2s_port_t)0,
        .role = I2S_ROLE_MASTER,
        .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
        .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr));

    i2s_std_config_t 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 = I2S_STD_SLOT_LEFT,
            .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_, &std_cfg));

    // Create a new channel for MIC
    chan_cfg.id = (i2s_port_t)1;
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_));
    std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
    std_cfg.gpio_cfg.bclk = mic_sck;
    std_cfg.gpio_cfg.ws = mic_ws;
    std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
    std_cfg.gpio_cfg.din = mic_din;
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
    ESP_LOGI(TAG, "Simplex channels created");
}

NoAudioCodecSimplex::NoAudioCodecSimplex(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_ws, gpio_num_t mic_din, i2s_std_slot_mask_t mic_slot_mask){
    duplex_ = false;
    input_sample_rate_ = input_sample_rate;
    output_sample_rate_ = output_sample_rate;

    // Create a new channel for speaker
    i2s_chan_config_t chan_cfg = {
        .id = (i2s_port_t)0,
        .role = I2S_ROLE_MASTER,
        .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
        .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr));

    i2s_std_config_t 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_, &std_cfg));

    // Create a new channel for MIC
    chan_cfg.id = (i2s_port_t)1;
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_));
    std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
    std_cfg.slot_cfg.slot_mask = mic_slot_mask;
    std_cfg.gpio_cfg.bclk = mic_sck;
    std_cfg.gpio_cfg.ws = mic_ws;
    std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
    std_cfg.gpio_cfg.din = mic_din;
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
    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;
    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 = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
        .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");
}

int NoAudioCodec::Write(const int16_t* data, int samples) {
    std::vector<int32_t> buffer(samples);

    // output_volume_: 0-100
    // volume_factor_: 0-65536
    int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536;
    for (int i = 0; i < samples; i++) {
        int64_t temp = int64_t(data[i]) * volume_factor; // 使用 int64_t 进行乘法运算
        if (temp > INT32_MAX) {
            buffer[i] = INT32_MAX;
        } else if (temp < INT32_MIN) {
            buffer[i] = INT32_MIN;
        } else {
            buffer[i] = static_cast<int32_t>(temp);
        }
    }

    size_t bytes_written;
    ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY));
    return bytes_written / sizeof(int32_t);
}

int NoAudioCodec::Read(int16_t* dest, int samples) {
    size_t bytes_read;

    std::vector<int32_t> bit32_buffer(samples);
    if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
        ESP_LOGE(TAG, "Read Failed!");
        return 0;
    }

    samples = bytes_read / sizeof(int32_t);
    for (int i = 0; i < samples; i++) {
        int32_t value = bit32_buffer[i] >> 12;
        dest[i] = (value > INT16_MAX) ? INT16_MAX : (value < -INT16_MAX) ? -INT16_MAX : (int16_t)value;
    }
    return samples;
}

int NoAudioCodecSimplexPdm::Read(int16_t* dest, int samples) {
    size_t bytes_read;

    // PDM 解调后的数据位宽为 16 位
    std::vector<int16_t> bit16_buffer(samples);
    if (i2s_channel_read(rx_handle_, bit16_buffer.data(), samples * sizeof(int16_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
        ESP_LOGE(TAG, "Read Failed!");
        return 0;
    }

    // 计算实际读取的样本数
    samples = bytes_read / sizeof(int16_t);

    // 将 16 位数据直接复制到目标缓冲区
    memcpy(dest, bit16_buffer.data(), samples * sizeof(int16_t));

    return samples;
}

```

## /main/audio_codecs/no_audio_codec.h

```h path="/main/audio_codecs/no_audio_codec.h" 
#ifndef _NO_AUDIO_CODEC_H
#define _NO_AUDIO_CODEC_H

#include "audio_codec.h"

#include <driver/gpio.h>
#include <driver/i2s_pdm.h>

class NoAudioCodec : public AudioCodec {
private:
    virtual int Write(const int16_t* data, int samples) override;
    virtual int Read(int16_t* dest, int samples) override;

public:
    virtual ~NoAudioCodec();
};

class NoAudioCodecDuplex : public NoAudioCodec {
public:
    NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
};

class ATK_NoAudioCodecDuplex : public NoAudioCodec {
public:
    ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
};

class NoAudioCodecSimplex : public NoAudioCodec {
public:
    NoAudioCodecSimplex(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_ws, gpio_num_t mic_din);
    NoAudioCodecSimplex(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_ws, gpio_num_t mic_din, i2s_std_slot_mask_t mic_slot_mask);
};

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);
    int Read(int16_t* dest, int samples);
};

#endif // _NO_AUDIO_CODEC_H

```

## /main/audio_processing/afe_audio_processor.cc

```cc path="/main/audio_processing/afe_audio_processor.cc" 
#include "afe_audio_processor.h"
#include <esp_log.h>

#define PROCESSOR_RUNNING 0x01

static const char* TAG = "AfeAudioProcessor";

AfeAudioProcessor::AfeAudioProcessor()
    : afe_data_(nullptr) {
    event_group_ = xEventGroupCreate();
}

void AfeAudioProcessor::Initialize(AudioCodec* codec) {
    codec_ = codec;
    int ref_num = codec_->input_reference() ? 1 : 0;

    std::string input_format;
    for (int i = 0; i < codec_->input_channels() - ref_num; i++) {
        input_format.push_back('M');
    }
    for (int i = 0; i < ref_num; i++) {
        input_format.push_back('R');
    }

    srmodel_list_t *models = esp_srmodel_init("model");
    char* ns_model_name = esp_srmodel_filter(models, ESP_NSNET_PREFIX, NULL);

    afe_config_t* afe_config = afe_config_init(input_format.c_str(), NULL, AFE_TYPE_VC, AFE_MODE_HIGH_PERF);
#ifdef CONFIG_USE_DEVICE_AEC
    afe_config->aec_init = true;
    afe_config->aec_mode = AEC_MODE_VOIP_HIGH_PERF;
#else
    afe_config->aec_init = false;
#endif
    afe_config->ns_init = true;
    afe_config->ns_model_name = ns_model_name;
    afe_config->afe_ns_mode = AFE_NS_MODE_NET;
#ifdef CONFIG_USE_DEVICE_AEC
    afe_config->vad_init = false;
#else
    afe_config->vad_init = true;
    afe_config->vad_mode = VAD_MODE_0;
    afe_config->vad_min_noise_ms = 100;
#endif
    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;

    afe_iface_ = esp_afe_handle_from_config(afe_config);
    afe_data_ = afe_iface_->create_from_config(afe_config);
    
    xTaskCreate([](void* arg) {
        auto this_ = (AfeAudioProcessor*)arg;
        this_->AudioProcessorTask();
        vTaskDelete(NULL);
    }, "audio_communication", 4096, this, 3, NULL);
}

AfeAudioProcessor::~AfeAudioProcessor() {
    if (afe_data_ != nullptr) {
        afe_iface_->destroy(afe_data_);
    }
    vEventGroupDelete(event_group_);
}

size_t AfeAudioProcessor::GetFeedSize() {
    if (afe_data_ == nullptr) {
        return 0;
    }
    return afe_iface_->get_feed_chunksize(afe_data_) * codec_->input_channels();
}

void AfeAudioProcessor::Feed(const std::vector<int16_t>& data) {
    if (afe_data_ == nullptr) {
        return;
    }
    afe_iface_->feed(afe_data_, data.data());
}

void AfeAudioProcessor::Start() {
    xEventGroupSetBits(event_group_, PROCESSOR_RUNNING);
}

void AfeAudioProcessor::Stop() {
    xEventGroupClearBits(event_group_, PROCESSOR_RUNNING);
    if (afe_data_ != nullptr) {
        afe_iface_->reset_buffer(afe_data_);
    }
}

bool AfeAudioProcessor::IsRunning() {
    return xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING;
}

void AfeAudioProcessor::OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) {
    output_callback_ = callback;
}

void AfeAudioProcessor::OnVadStateChange(std::function<void(bool speaking)> callback) {
    vad_state_change_callback_ = callback;
}

void AfeAudioProcessor::AudioProcessorTask() {
    auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_);
    auto feed_size = afe_iface_->get_feed_chunksize(afe_data_);
    ESP_LOGI(TAG, "Audio communication task started, feed size: %d fetch size: %d",
        feed_size, fetch_size);

    while (true) {
        xEventGroupWaitBits(event_group_, PROCESSOR_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY);

        auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
        if ((xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING) == 0) {
            continue;
        }
        if (res == nullptr || res->ret_value == ESP_FAIL) {
            if (res != nullptr) {
                ESP_LOGI(TAG, "Error code: %d", res->ret_value);
            }
            continue;
        }

        // VAD state change
        if (vad_state_change_callback_) {
            if (res->vad_state == VAD_SPEECH && !is_speaking_) {
                is_speaking_ = true;
                vad_state_change_callback_(true);
            } else if (res->vad_state == VAD_SILENCE && is_speaking_) {
                is_speaking_ = false;
                vad_state_change_callback_(false);
            }
        }

        if (output_callback_) {
            output_callback_(std::vector<int16_t>(res->data, res->data + res->data_size / sizeof(int16_t)));
        }
    }
} 
```

## /main/audio_processing/afe_audio_processor.h

```h path="/main/audio_processing/afe_audio_processor.h" 
#ifndef AFE_AUDIO_PROCESSOR_H
#define AFE_AUDIO_PROCESSOR_H

#include <esp_afe_sr_models.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>

#include <string>
#include <vector>
#include <functional>

#include "audio_processor.h"
#include "audio_codec.h"

class AfeAudioProcessor : public AudioProcessor {
public:
    AfeAudioProcessor();
    ~AfeAudioProcessor();

    void Initialize(AudioCodec* codec) override;
    void Feed(const std::vector<int16_t>& data) override;
    void Start() override;
    void Stop() override;
    bool IsRunning() override;
    void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) override;
    void OnVadStateChange(std::function<void(bool speaking)> callback) override;
    size_t GetFeedSize() override;

private:
    EventGroupHandle_t event_group_ = nullptr;
    esp_afe_sr_iface_t* afe_iface_ = nullptr;
    esp_afe_sr_data_t* afe_data_ = nullptr;
    std::function<void(std::vector<int16_t>&& data)> output_callback_;
    std::function<void(bool speaking)> vad_state_change_callback_;
    AudioCodec* codec_ = nullptr;
    bool is_speaking_ = false;

    void AudioProcessorTask();
};

#endif 
```

## /main/audio_processing/audio_processor.h

```h path="/main/audio_processing/audio_processor.h" 
#ifndef AUDIO_PROCESSOR_H
#define AUDIO_PROCESSOR_H

#include <string>
#include <vector>
#include <functional>

#include "audio_codec.h"

class AudioProcessor {
public:
    virtual ~AudioProcessor() = default;
    
    virtual void Initialize(AudioCodec* codec) = 0;
    virtual void Feed(const std::vector<int16_t>& data) = 0;
    virtual void Start() = 0;
    virtual void Stop() = 0;
    virtual bool IsRunning() = 0;
    virtual void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) = 0;
    virtual void OnVadStateChange(std::function<void(bool speaking)> callback) = 0;
    virtual size_t GetFeedSize() = 0;
};

#endif

```

## /main/audio_processing/dummy_audio_processor.cc

```cc path="/main/audio_processing/dummy_audio_processor.cc" 
#include "dummy_audio_processor.h"
#include <esp_log.h>

#define TAG "DummyAudioProcessor"

void DummyAudioProcessor::Initialize(AudioCodec* codec) {
    codec_ = codec;
}

void DummyAudioProcessor::Feed(const std::vector<int16_t>& data) {
    if (!is_running_ || !output_callback_) {
        return;
    }
    // 直接将输入数据传递给输出回调
    output_callback_(std::vector<int16_t>(data));
}

void DummyAudioProcessor::Start() {
    is_running_ = true;
}

void DummyAudioProcessor::Stop() {
    is_running_ = false;
}

bool DummyAudioProcessor::IsRunning() {
    return is_running_;
}

void DummyAudioProcessor::OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) {
    output_callback_ = callback;
}

void DummyAudioProcessor::OnVadStateChange(std::function<void(bool speaking)> callback) {
    vad_state_change_callback_ = callback;
}

size_t DummyAudioProcessor::GetFeedSize() {
    if (!codec_) {
        return 0;
    }
    // 返回一个固定的帧大小,比如 30ms 的数据
    return 30 * codec_->input_sample_rate() / 1000;
}
```

## /main/audio_processing/dummy_audio_processor.h

```h path="/main/audio_processing/dummy_audio_processor.h" 
#ifndef DUMMY_AUDIO_PROCESSOR_H
#define DUMMY_AUDIO_PROCESSOR_H

#include <vector>
#include <functional>

#include "audio_processor.h"
#include "audio_codec.h"

class DummyAudioProcessor : public AudioProcessor {
public:
    DummyAudioProcessor() = default;
    ~DummyAudioProcessor() = default;

    void Initialize(AudioCodec* codec) override;
    void Feed(const std::vector<int16_t>& data) override;
    void Start() override;
    void Stop() override;
    bool IsRunning() override;
    void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) override;
    void OnVadStateChange(std::function<void(bool speaking)> callback) override;
    size_t GetFeedSize() override;

private:
    AudioCodec* codec_ = nullptr;
    std::function<void(std::vector<int16_t>&& data)> output_callback_;
    std::function<void(bool speaking)> vad_state_change_callback_;
    bool is_running_ = false;
};

#endif 
```

## /main/audio_processing/wake_word_detect.cc

```cc path="/main/audio_processing/wake_word_detect.cc" 
#include "wake_word_detect.h"
#include "application.h"

#include <esp_log.h>
#include <model_path.h>
#include <arpa/inet.h>
#include <sstream>

#define DETECTION_RUNNING_EVENT 1

static const char* TAG = "WakeWordDetect";

WakeWordDetect::WakeWordDetect()
    : afe_data_(nullptr),
      wake_word_pcm_(),
      wake_word_opus_() {

    event_group_ = xEventGroupCreate();
}

WakeWordDetect::~WakeWordDetect() {
    if (afe_data_ != nullptr) {
        afe_iface_->destroy(afe_data_);
    }

    if (wake_word_encode_task_stack_ != nullptr) {
        heap_caps_free(wake_word_encode_task_stack_);
    }

    vEventGroupDelete(event_group_);
}

void WakeWordDetect::Initialize(AudioCodec* codec) {
    codec_ = codec;
    int ref_num = codec_->input_reference() ? 1 : 0;

    srmodel_list_t *models = esp_srmodel_init("model");
    for (int i = 0; i < models->num; i++) {
        ESP_LOGI(TAG, "Model %d: %s", i, models->model_name[i]);
        if (strstr(models->model_name[i], ESP_WN_PREFIX) != NULL) {
            wakenet_model_ = models->model_name[i];
            auto words = esp_srmodel_get_wake_words(models, wakenet_model_);
            // split by ";" to get all wake words
            std::stringstream ss(words);
            std::string word;
            while (std::getline(ss, word, ';')) {
                wake_words_.push_back(word);
            }
        }
    }

    std::string input_format;
    for (int i = 0; i < codec_->input_channels() - ref_num; i++) {
        input_format.push_back('M');
    }
    for (int i = 0; i < ref_num; i++) {
        input_format.push_back('R');
    }
    afe_config_t* afe_config = afe_config_init(input_format.c_str(), models, AFE_TYPE_SR, AFE_MODE_HIGH_PERF);
    afe_config->aec_init = codec_->input_reference();
    afe_config->aec_mode = AEC_MODE_SR_HIGH_PERF;
    afe_config->afe_perferred_core = 1;
    afe_config->afe_perferred_priority = 1;
    afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM;
    
    afe_iface_ = esp_afe_handle_from_config(afe_config);
    afe_data_ = afe_iface_->create_from_config(afe_config);

    xTaskCreate([](void* arg) {
        auto this_ = (WakeWordDetect*)arg;
        this_->AudioDetectionTask();
        vTaskDelete(NULL);
    }, "audio_detection", 4096, this, 3, nullptr);
}

void WakeWordDetect::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
    wake_word_detected_callback_ = callback;
}

void WakeWordDetect::StartDetection() {
    xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT);
}

void WakeWordDetect::StopDetection() {
    xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT);
    if (afe_data_ != nullptr) {
        afe_iface_->reset_buffer(afe_data_);
    }
}

bool WakeWordDetect::IsDetectionRunning() {
    return xEventGroupGetBits(event_group_) & DETECTION_RUNNING_EVENT;
}

void WakeWordDetect::Feed(const std::vector<int16_t>& data) {
    if (afe_data_ == nullptr) {
        return;
    }
    afe_iface_->feed(afe_data_, data.data());
}

size_t WakeWordDetect::GetFeedSize() {
    if (afe_data_ == nullptr) {
        return 0;
    }
    return afe_iface_->get_feed_chunksize(afe_data_) * codec_->input_channels();
}

void WakeWordDetect::AudioDetectionTask() {
    auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_);
    auto feed_size = afe_iface_->get_feed_chunksize(afe_data_);
    ESP_LOGI(TAG, "Audio detection task started, feed size: %d fetch size: %d",
        feed_size, fetch_size);

    while (true) {
        xEventGroupWaitBits(event_group_, DETECTION_RUNNING_EVENT, pdFALSE, pdTRUE, portMAX_DELAY);

        auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
        if (res == nullptr || res->ret_value == ESP_FAIL) {
            continue;;
        }

        // Store the wake word data for voice recognition, like who is speaking
        StoreWakeWordData((uint16_t*)res->data, res->data_size / sizeof(uint16_t));

        if (res->wakeup_state == WAKENET_DETECTED) {
            StopDetection();
            last_detected_wake_word_ = wake_words_[res->wake_word_index - 1];

            if (wake_word_detected_callback_) {
                wake_word_detected_callback_(last_detected_wake_word_);
            }
        }
    }
}

void WakeWordDetect::StoreWakeWordData(uint16_t* data, size_t samples) {
    // store audio data to wake_word_pcm_
    wake_word_pcm_.emplace_back(std::vector<int16_t>(data, data + samples));
    // keep about 2 seconds of data, detect duration is 32ms (sample_rate == 16000, chunksize == 512)
    while (wake_word_pcm_.size() > 2000 / 32) {
        wake_word_pcm_.pop_front();
    }
}

void WakeWordDetect::EncodeWakeWordData() {
    wake_word_opus_.clear();
    if (wake_word_encode_task_stack_ == nullptr) {
        wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(4096 * 8, MALLOC_CAP_SPIRAM);
    }
    wake_word_encode_task_ = xTaskCreateStatic([](void* arg) {
        auto this_ = (WakeWordDetect*)arg;
        {
            auto start_time = esp_timer_get_time();
            auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
            encoder->SetComplexity(0); // 0 is the fastest

            for (auto& pcm: this_->wake_word_pcm_) {
                encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) {
                    std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
                    this_->wake_word_opus_.emplace_back(std::move(opus));
                    this_->wake_word_cv_.notify_all();
                });
            }
            this_->wake_word_pcm_.clear();

            auto end_time = esp_timer_get_time();
            ESP_LOGI(TAG, "Encode wake word opus %zu packets in %lld ms",
                this_->wake_word_opus_.size(), (end_time - start_time) / 1000);

            std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
            this_->wake_word_opus_.push_back(std::vector<uint8_t>());
            this_->wake_word_cv_.notify_all();
        }
        vTaskDelete(NULL);
    }, "encode_detect_packets", 4096 * 8, this, 2, wake_word_encode_task_stack_, &wake_word_encode_task_buffer_);
}

bool WakeWordDetect::GetWakeWordOpus(std::vector<uint8_t>& opus) {
    std::unique_lock<std::mutex> lock(wake_word_mutex_);
    wake_word_cv_.wait(lock, [this]() {
        return !wake_word_opus_.empty();
    });
    opus.swap(wake_word_opus_.front());
    wake_word_opus_.pop_front();
    return !opus.empty();
}

```

## /main/audio_processing/wake_word_detect.h

```h path="/main/audio_processing/wake_word_detect.h" 
#ifndef WAKE_WORD_DETECT_H
#define WAKE_WORD_DETECT_H

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>

#include <esp_afe_sr_models.h>
#include <esp_nsn_models.h>

#include <list>
#include <string>
#include <vector>
#include <functional>
#include <mutex>
#include <condition_variable>

#include "audio_codec.h"

class WakeWordDetect {
public:
    WakeWordDetect();
    ~WakeWordDetect();

    void Initialize(AudioCodec* codec);
    void Feed(const std::vector<int16_t>& data);
    void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
    void StartDetection();
    void StopDetection();
    bool IsDetectionRunning();
    size_t GetFeedSize();
    void EncodeWakeWordData();
    bool GetWakeWordOpus(std::vector<uint8_t>& opus);
    const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; }

private:
    esp_afe_sr_iface_t* afe_iface_ = nullptr;
    esp_afe_sr_data_t* afe_data_ = nullptr;
    char* wakenet_model_ = NULL;
    std::vector<std::string> wake_words_;
    EventGroupHandle_t event_group_;
    std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
    AudioCodec* codec_ = nullptr;
    std::string last_detected_wake_word_;

    TaskHandle_t wake_word_encode_task_ = nullptr;
    StaticTask_t wake_word_encode_task_buffer_;
    StackType_t* wake_word_encode_task_stack_ = nullptr;
    std::list<std::vector<int16_t>> wake_word_pcm_;
    std::list<std::vector<uint8_t>> wake_word_opus_;
    std::mutex wake_word_mutex_;
    std::condition_variable wake_word_cv_;

    void StoreWakeWordData(uint16_t* data, size_t size);
    void AudioDetectionTask();
};

#endif

```

## /main/background_task.cc

```cc path="/main/background_task.cc" 
#include "background_task.h"

#include <esp_log.h>
#include <esp_task_wdt.h>

#define TAG "BackgroundTask"

BackgroundTask::BackgroundTask(uint32_t stack_size) {
    xTaskCreate([](void* arg) {
        BackgroundTask* task = (BackgroundTask*)arg;
        task->BackgroundTaskLoop();
    }, "background_task", stack_size, this, 2, &background_task_handle_);
}

BackgroundTask::~BackgroundTask() {
    if (background_task_handle_ != nullptr) {
        vTaskDelete(background_task_handle_);
    }
}

void BackgroundTask::Schedule(std::function<void()> callback) {
    std::lock_guard<std::mutex> lock(mutex_);
    if (active_tasks_ >= 30) {
        int free_sram = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
        if (free_sram < 10000) {
            ESP_LOGW(TAG, "active_tasks_ == %u, free_sram == %u", active_tasks_.load(), free_sram);
        }
    }
    active_tasks_++;
    main_tasks_.emplace_back([this, cb = std::move(callback)]() {
        cb();
        {
            std::lock_guard<std::mutex> lock(mutex_);
            active_tasks_--;
            if (main_tasks_.empty() && active_tasks_ == 0) {
                condition_variable_.notify_all();
            }
        }
    });
    condition_variable_.notify_all();
}

void BackgroundTask::WaitForCompletion() {
    std::unique_lock<std::mutex> lock(mutex_);
    condition_variable_.wait(lock, [this]() {
        return main_tasks_.empty() && active_tasks_ == 0;
    });
}

void BackgroundTask::BackgroundTaskLoop() {
    ESP_LOGI(TAG, "background_task started");
    while (true) {
        std::unique_lock<std::mutex> lock(mutex_);
        condition_variable_.wait(lock, [this]() { return !main_tasks_.empty(); });
        
        std::list<std::function<void()>> tasks = std::move(main_tasks_);
        lock.unlock();

        for (auto& task : tasks) {
            task();
        }
    }
}

```

## /main/background_task.h

```h path="/main/background_task.h" 
#ifndef BACKGROUND_TASK_H
#define BACKGROUND_TASK_H

#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <mutex>
#include <list>
#include <condition_variable>
#include <atomic>

class BackgroundTask {
public:
    BackgroundTask(uint32_t stack_size = 4096 * 2);
    ~BackgroundTask();

    void Schedule(std::function<void()> callback);
    void WaitForCompletion();

private:
    std::mutex mutex_;
    std::list<std::function<void()>> main_tasks_;
    std::condition_variable condition_variable_;
    TaskHandle_t background_task_handle_ = nullptr;
    std::atomic<size_t> active_tasks_{0};

    void BackgroundTaskLoop();
};

#endif

```

## /main/boards/README.md

# 自定义开发板指南

本指南介绍如何为小智AI语音聊天机器人项目定制一个新的开发板初始化程序。小智AI支持50多种ESP32系列开发板,每个开发板的初始化代码都放在对应的目录下。

## 重要提示

> **警告**: 对于自定义开发板,当IO配置与原有开发板不同时,切勿直接覆盖原有开发板的配置编译固件。必须创建新的开发板类型,或者通过config.json文件中的builds配置不同的name和sdkconfig宏定义来区分。使用 `python scripts/release.py [开发板目录名字]` 来编译打包固件。
>
> 如果直接覆盖原有配置,将来OTA升级时,您的自定义固件可能会被原有开发板的标准固件覆盖,导致您的设备无法正常工作。每个开发板有唯一的标识和对应的固件升级通道,保持开发板标识的唯一性非常重要。

## 目录结构

每个开发板的目录结构通常包含以下文件:

- `xxx_board.cc` - 主要的板级初始化代码,实现了板子相关的初始化和功能
- `config.h` - 板级配置文件,定义了硬件管脚映射和其他配置项
- `config.json` - 编译配置,指定目标芯片和特殊的编译选项
- `README.md` - 开发板相关的说明文档

## 定制开发板步骤

### 1. 创建新的开发板目录

首先在`boards/`目录下创建一个新的目录,例如`my-custom-board/`:

```bash
mkdir main/boards/my-custom-board
```

### 2. 创建配置文件

#### config.h

在`config.h`中定义所有的硬件配置,包括:

- 音频采样率和I2S引脚配置
- 音频编解码芯片地址和I2C引脚配置
- 按钮和LED引脚配置
- 显示屏参数和引脚配置

参考示例(来自lichuang-c3-dev):

```c
#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_I2S_GPIO_MCLK GPIO_NUM_10
#define AUDIO_I2S_GPIO_WS   GPIO_NUM_12
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8
#define AUDIO_I2S_GPIO_DIN  GPIO_NUM_7
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11

#define AUDIO_CODEC_PA_PIN       GPIO_NUM_13
#define AUDIO_CODEC_I2C_SDA_PIN  GPIO_NUM_0
#define AUDIO_CODEC_I2C_SCL_PIN  GPIO_NUM_1
#define AUDIO_CODEC_ES8311_ADDR  ES8311_CODEC_DEFAULT_ADDR

// 按钮配置
#define BOOT_BUTTON_GPIO        GPIO_NUM_9

// 显示屏配置
#define DISPLAY_SPI_SCK_PIN     GPIO_NUM_3
#define DISPLAY_SPI_MOSI_PIN    GPIO_NUM_5
#define DISPLAY_DC_PIN          GPIO_NUM_6
#define DISPLAY_SPI_CS_PIN      GPIO_NUM_4

#define DISPLAY_WIDTH   320
#define DISPLAY_HEIGHT  240
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY true

#define DISPLAY_OFFSET_X  0
#define DISPLAY_OFFSET_Y  0

#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true

#endif // _BOARD_CONFIG_H_
```

#### config.json

在`config.json`中定义编译配置:

```json
{
    "target": "esp32s3",  // 目标芯片型号: esp32, esp32s3, esp32c3等
    "builds": [
        {
            "name": "my-custom-board",  // 开发板名称
            "sdkconfig_append": [
                // 额外需要的编译配置
                "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
                "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_8M.csv\""
            ]
        }
    ]
}
```

### 3. 编写板级初始化代码

创建一个`my_custom_board.cc`文件,实现开发板的所有初始化逻辑。

一个基本的开发板类定义包含以下几个部分:

1. **类定义**:继承自`WifiBoard`或`ML307Board`
2. **初始化函数**:包括I2C、显示屏、按钮、IoT等组件的初始化
3. **虚函数重写**:如`GetAudioCodec()`、`GetDisplay()`、`GetBacklight()`等
4. **注册开发板**:使用`DECLARE_BOARD`宏注册开发板

```cpp
#include "wifi_board.h"
#include "audio_codecs/es8311_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "iot/thing_manager.h"

#include <esp_log.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>

#define TAG "MyCustomBoard"

// 声明字体
LV_FONT_DECLARE(font_puhui_16_4);
LV_FONT_DECLARE(font_awesome_16_4);

class MyCustomBoard : public WifiBoard {
private:
    i2c_master_bus_handle_t codec_i2c_bus_;
    Button boot_button_;
    LcdDisplay* display_;

    // I2C初始化
    void InitializeI2c() {
        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,
            .intr_priority = 0,
            .trans_queue_depth = 0,
            .flags = {
                .enable_internal_pullup = 1,
            },
        };
        ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
    }

    // SPI初始化(用于显示屏)
    void InitializeSpi() {
        spi_bus_config_t buscfg = {};
        buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
        buscfg.miso_io_num = GPIO_NUM_NC;
        buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN;
        buscfg.quadwp_io_num = GPIO_NUM_NC;
        buscfg.quadhd_io_num = GPIO_NUM_NC;
        buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
        ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
    }

    // 按钮初始化
    void InitializeButtons() {
        boot_button_.OnClick([this]() {
            auto& app = Application::GetInstance();
            if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
                ResetWifiConfiguration();
            }
            app.ToggleChatState();
        });
    }

    // 显示屏初始化(以ST7789为例)
    void InitializeDisplay() {
        esp_lcd_panel_io_handle_t panel_io = nullptr;
        esp_lcd_panel_handle_t panel = nullptr;
        
        esp_lcd_panel_io_spi_config_t io_config = {};
        io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
        io_config.dc_gpio_num = DISPLAY_DC_PIN;
        io_config.spi_mode = 2;
        io_config.pclk_hz = 80 * 1000 * 1000;
        io_config.trans_queue_depth = 10;
        io_config.lcd_cmd_bits = 8;
        io_config.lcd_param_bits = 8;
        ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io));

        esp_lcd_panel_dev_config_t panel_config = {};
        panel_config.reset_gpio_num = GPIO_NUM_NC;
        panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
        panel_config.bits_per_pixel = 16;
        ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
        
        esp_lcd_panel_reset(panel);
        esp_lcd_panel_init(panel);
        esp_lcd_panel_invert_color(panel, true);
        esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
        esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
        
        // 创建显示屏对象
        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,
                                    {
                                        .text_font = &font_puhui_16_4,
                                        .icon_font = &font_awesome_16_4,
                                        .emoji_font = font_emoji_32_init(),
                                    });
    }

    // IoT设备初始化
    void InitializeIot() {
        auto& thing_manager = iot::ThingManager::GetInstance();
        thing_manager.AddThing(iot::CreateThing("Speaker"));
        thing_manager.AddThing(iot::CreateThing("Screen"));
        // 可以添加更多IoT设备
    }

public:
    // 构造函数
    MyCustomBoard() : boot_button_(BOOT_BUTTON_GPIO) {
        InitializeI2c();
        InitializeSpi();
        InitializeDisplay();
        InitializeButtons();
        InitializeIot();
        GetBacklight()->SetBrightness(100);
    }

    // 获取音频编解码器
    virtual AudioCodec* GetAudioCodec() override {
        static Es8311AudioCodec audio_codec(
            codec_i2c_bus_, 
            I2C_NUM_0, 
            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);
        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(MyCustomBoard);
```

### 4. 创建README.md

在README.md中说明开发板的特性、硬件要求、编译和烧录步骤:


## 常见开发板组件

### 1. 显示屏

项目支持多种显示屏驱动,包括:
- ST7789 (SPI)
- ILI9341 (SPI)
- SH8601 (QSPI)
- 等...

### 2. 音频编解码器

支持的编解码器包括:
- ES8311 (常用)
- ES7210 (麦克风阵列)
- AW88298 (功放)
- 等...

### 3. 电源管理

一些开发板使用电源管理芯片:
- AXP2101
- 其他可用的PMIC

### 4. IoT设备

可以添加各种IoT设备,让AI能够"看到"和控制:
- Speaker (扬声器)
- Screen (屏幕)
- Battery (电池)
- Light (灯光)
- 等...

## 开发板类继承关系

- `Board` - 基础板级类
  - `WifiBoard` - WiFi连接的开发板
  - `ML307Board` - 使用4G模块的开发板

## 开发技巧

1. **参考相似的开发板**:如果您的新开发板与现有开发板有相似之处,可以参考现有实现
2. **分步调试**:先实现基础功能(如显示),再添加更复杂的功能(如音频)
3. **管脚映射**:确保在config.h中正确配置所有管脚映射
4. **检查硬件兼容性**:确认所有芯片和驱动程序的兼容性

## 可能遇到的问题

1. **显示屏不正常**:检查SPI配置、镜像设置和颜色反转设置
2. **音频无输出**:检查I2S配置、PA使能引脚和编解码器地址
3. **无法连接网络**:检查WiFi凭据和网络配置
4. **无法与服务器通信**:检查MQTT或WebSocket配置

## 参考资料

- ESP-IDF 文档: https://docs.espressif.com/projects/esp-idf/
- LVGL 文档: https://docs.lvgl.io/
- ESP-SR 文档: https://github.com/espressif/esp-sr 

## /main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc

```cc path="/main/boards/atk-dnesp32s3-box/atk_dnesp32s3_box.cc" 
#include "wifi_board.h"
#include "audio_codec.h"
#include "es8311_audio_codec.h"
#include "no_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "led/single_led.h"
#include "iot/thing_manager.h"
#include "i2c_device.h"

#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/timers.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_lcd_panel_ops.h>

#define TAG "atk_dnesp32s3_box"

LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);

class XL9555_IN : public I2cDevice {
public:
    XL9555_IN(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
        WriteReg(0x06, 0x3B);
        WriteReg(0x07, 0xFE);
    }

    void xl9555_cfg(void) {
        WriteReg(0x06, 0x1B);
        WriteReg(0x07, 0xFE);
    }

    void SetOutputState(uint8_t bit, uint8_t level) {
        uint16_t data;
        int index = bit;

        if (bit < 8) {
            data = ReadReg(0x02);
        } else {
            data = ReadReg(0x03);
            index -= 8;
        }

        data = (data & ~(1 << index)) | (level << index);

        if (bit < 8) {
            WriteReg(0x02, data);
        } else {
            WriteReg(0x03, data);
        }
    }

    int GetPingState(uint16_t pin) {
        uint8_t data;
        if (pin <= 0x0080) {
            data = ReadReg(0x00);
            return (data & (uint8_t)(pin & 0xFF)) ? 1 : 0;
        } else {
            data = ReadReg(0x01);
            return (data & (uint8_t)((pin >> 8) & 0xFF )) ? 1 : 0;
        }

        return 0;
    }
};

class atk_dnesp32s3_box : public WifiBoard {
private:
    i2c_master_bus_handle_t i2c_bus_;
    i2c_master_dev_handle_t xl9555_handle_;
    Button boot_button_;
    LcdDisplay* display_;
    XL9555_IN* xl9555_in_;
    bool es8311_detected_ = false;
    
    void InitializeI2c() {
        // Initialize I2C peripheral
        i2c_master_bus_config_t i2c_bus_cfg = {
            .i2c_port = (i2c_port_t)0,
            .sda_io_num = GPIO_NUM_48,
            .scl_io_num = GPIO_NUM_45,
            .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_));

        // Initialize XL9555
        xl9555_in_ = new XL9555_IN(i2c_bus_, 0x20);

        if (xl9555_in_->GetPingState(0x0020) == 1) {
            es8311_detected_ = true;    /* 音频设备标志位,SPK_CTRL_IO为高电平时,该标志位置1,且判定为ES8311 */
        } else {
            es8311_detected_ = false;    /* 音频设备标志位,SPK_CTRL_IO为低电平时,该标志位置0,且判定为NS4168 */
        }

        xl9555_in_->xl9555_cfg();
    }

    void InitializeATK_ST7789_80_Display() {
        esp_lcd_panel_io_handle_t panel_io = nullptr;
        esp_lcd_panel_handle_t panel = nullptr;
        /* 配置RD引脚 */
        gpio_config_t gpio_init_struct;
        gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
        gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT;
        gpio_init_struct.pin_bit_mask = 1ull << LCD_NUM_RD;
        gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
        gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
        gpio_config(&gpio_init_struct);
        gpio_set_level(LCD_NUM_RD, 1);

        esp_lcd_i80_bus_handle_t i80_bus = NULL;
        esp_lcd_i80_bus_config_t bus_config = {
            .dc_gpio_num = LCD_NUM_DC,
            .wr_gpio_num = LCD_NUM_WR,
            .clk_src = LCD_CLK_SRC_DEFAULT,
            .data_gpio_nums = {
                GPIO_LCD_D0,
                GPIO_LCD_D1,
                GPIO_LCD_D2,
                GPIO_LCD_D3,
                GPIO_LCD_D4,
                GPIO_LCD_D5,
                GPIO_LCD_D6,
                GPIO_LCD_D7,
            },
            .bus_width = 8,
            .max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t),
            .psram_trans_align = 64,
            .sram_trans_align = 4,
        };
        ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));

        esp_lcd_panel_io_i80_config_t io_config = {
            .cs_gpio_num = LCD_NUM_CS,
            .pclk_hz = (10 * 1000 * 1000),
            .trans_queue_depth = 10,
            .on_color_trans_done = nullptr,
            .user_ctx = nullptr,
            .lcd_cmd_bits = 8,
            .lcd_param_bits = 8,
            .dc_levels = {
                .dc_idle_level = 0,
                .dc_cmd_level = 0,
                .dc_dummy_level = 0,
                .dc_data_level = 1,
            },
            .flags = {
                .swap_color_bytes = 0,
            },
        };
        ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io));

        esp_lcd_panel_dev_config_t panel_config = {
            .reset_gpio_num = LCD_NUM_RST,
            .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
            .bits_per_pixel = 16,
        };
        ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));

        esp_lcd_panel_reset(panel);
        esp_lcd_panel_init(panel);
        esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
        esp_lcd_panel_set_gap(panel, 0, 0);
        uint8_t data0[] = {0x00};
        uint8_t data1[] = {0x65};
        esp_lcd_panel_io_tx_param(panel_io, 0x36, data0, 1);
        esp_lcd_panel_io_tx_param(panel_io, 0x3A, data1, 1);
        esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
        esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
        ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true));

        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,
                                    {
                                        .text_font = &font_puhui_20_4,
                                        .icon_font = &font_awesome_20_4,
                                        #if CONFIG_USE_WECHAT_MESSAGE_STYLE
                                            .emoji_font = font_emoji_32_init(),
                                        #else
                                            .emoji_font = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(),
                                        #endif
                                    });
    }

    void InitializeButtons() {
        boot_button_.OnClick([this]() {
            auto& app = Application::GetInstance();
            if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
                ResetWifiConfiguration();
            }
            app.ToggleChatState();
        });
    }

    // 物联网初始化,添加对 AI 可见设备
    void InitializeIot() {
        auto& thing_manager = iot::ThingManager::GetInstance();
        thing_manager.AddThing(iot::CreateThing("Speaker"));
        thing_manager.AddThing(iot::CreateThing("Screen"));
    }

public:
    atk_dnesp32s3_box() : boot_button_(BOOT_BUTTON_GPIO) {
        InitializeI2c();
        InitializeATK_ST7789_80_Display();
        xl9555_in_->SetOutputState(5, 1);
        xl9555_in_->SetOutputState(7, 1);
        InitializeButtons();
        InitializeIot();
    }

    virtual AudioCodec* GetAudioCodec() override {
        /* 根据探测结果初始化编解码器 */
        if (es8311_detected_) {
            /* 使用ES8311 驱动 */
            static Es8311AudioCodec audio_codec(
                i2c_bus_, 
                I2C_NUM_0, 
                AUDIO_INPUT_SAMPLE_RATE,
                AUDIO_OUTPUT_SAMPLE_RATE,
                GPIO_NUM_NC, 
                AUDIO_I2S_GPIO_BCLK, 
                AUDIO_I2S_GPIO_WS,
                AUDIO_I2S_GPIO_DOUT,
                AUDIO_I2S_GPIO_DIN,
                GPIO_NUM_NC, 
                AUDIO_CODEC_ES8311_ADDR, 
                false);
                return &audio_codec;
        } else {
            static ATK_NoAudioCodecDuplex audio_codec(
                AUDIO_INPUT_SAMPLE_RATE,
                AUDIO_OUTPUT_SAMPLE_RATE,
                AUDIO_I2S_GPIO_BCLK,
                AUDIO_I2S_GPIO_WS,
                AUDIO_I2S_GPIO_DOUT,
                AUDIO_I2S_GPIO_DIN);
                return &audio_codec;
        }
        return NULL;
    }
    
    virtual Display* GetDisplay() override {
        return display_;
    }
};

DECLARE_BOARD(atk_dnesp32s3_box);

```

## /main/boards/atk-dnesp32s3-box/config.h

```h path="/main/boards/atk-dnesp32s3-box/config.h" 
#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_I2S_GPIO_WS GPIO_NUM_13
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_21
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_47
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_14
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR

#define BUILTIN_LED_GPIO GPIO_NUM_4
#define BOOT_BUTTON_GPIO GPIO_NUM_0

#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define DISPLAY_WIDTH    320
#define DISPLAY_HEIGHT   240
#define DISPLAY_SWAP_XY  true
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false

#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true


// Pin Definitions 
#define LCD_NUM_CS GPIO_NUM_1
#define LCD_NUM_DC GPIO_NUM_2
#define LCD_NUM_RD GPIO_NUM_41
#define LCD_NUM_WR GPIO_NUM_42
#define LCD_NUM_RST GPIO_NUM_NC

#define GPIO_LCD_D0 GPIO_NUM_40
#define GPIO_LCD_D1 GPIO_NUM_39
#define GPIO_LCD_D2 GPIO_NUM_38
#define GPIO_LCD_D3 GPIO_NUM_12
#define GPIO_LCD_D4 GPIO_NUM_11
#define GPIO_LCD_D5 GPIO_NUM_10
#define GPIO_LCD_D6 GPIO_NUM_9
#define GPIO_LCD_D7 GPIO_NUM_46

#endif // _BOARD_CONFIG_H_

```

## /main/boards/atk-dnesp32s3-box/config.json

```json path="/main/boards/atk-dnesp32s3-box/config.json" 
{
    "target": "esp32s3",
    "builds": [
        {
            "name": "atk-dnesp32s3-box",
            "sdkconfig_append": [
                "CONFIG_USE_WECHAT_MESSAGE_STYLE=y"
            ]
        }
    ]
}
```

## /main/boards/atk-dnesp32s3-box0/atk_dnesp32s3_box0.cc

```cc path="/main/boards/atk-dnesp32s3-box0/atk_dnesp32s3_box0.cc" 
#include "wifi_board.h"
#include "es8311_audio_codec.h"
#include "display/lcd_display.h"
#include "system_reset.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "power_save_timer.h"
#include "iot/thing_manager.h"
#include "led/single_led.h"
#include "assets/lang_config.h"
#include "power_manager.h"

#include "i2c_device.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <wifi_station.h>

#include <driver/rtc_io.h>
#include <esp_sleep.h>

#define TAG "atk_dnesp32s3_box0"

LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);

class atk_dnesp32s3_box0  : public WifiBoard {
private:
    i2c_master_bus_handle_t i2c_bus_;
    Button right_button_;   
    Button left_button_;    
    Button middle_button_;
    LcdDisplay* display_;
    PowerSaveTimer* power_save_timer_;
    PowerManager* power_manager_;
    PowerSupply power_status_;
    LcdStatus LcdStatus_ = kDevicelcdbacklightOn;
    PowerSleep power_sleep_ = kDeviceNoSleep;
    WakeStatus wake_status_ = kDeviceAwakened;
    XiaozhiStatus XiaozhiStatus_ = kDevice_Exit_Distributionnetwork;
    esp_timer_handle_t wake_timer_handle_;
    esp_lcd_panel_io_handle_t panel_io = nullptr;
    esp_lcd_panel_handle_t panel = nullptr;

    int ticks_ = 0;
    const int kChgCtrlInterval = 5;

    void InitializeBoardPowerManager() {
        gpio_config_t gpio_init_struct = {0};
        gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
        gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT;
        gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
        gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
        gpio_init_struct.pin_bit_mask = (1ull << CODEC_PWR_PIN) | (1ull << SYS_POW_PIN);
        gpio_config(&gpio_init_struct);

        gpio_set_level(CODEC_PWR_PIN, 1); 
        gpio_set_level(SYS_POW_PIN, 1); 

        gpio_config_t chg_init_struct = {0};

        chg_init_struct.intr_type = GPIO_INTR_DISABLE;
        chg_init_struct.mode = GPIO_MODE_INPUT;
        chg_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
        chg_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
        chg_init_struct.pin_bit_mask = 1ull << CHRG_PIN;
        ESP_ERROR_CHECK(gpio_config(&chg_init_struct));

        chg_init_struct.mode = GPIO_MODE_OUTPUT;
        chg_init_struct.pull_up_en = GPIO_PULLUP_DISABLE;
        chg_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
        chg_init_struct.pin_bit_mask = 1ull << CHG_CTRL_PIN;
        ESP_ERROR_CHECK(gpio_config(&chg_init_struct));
        gpio_set_level(CHG_CTRL_PIN, 1);

        if (gpio_get_level(CHRG_PIN) == 0) {
            power_status_ = kDeviceTypecSupply;
        } else {
            power_status_ = kDeviceBatterySupply;
        }

        esp_timer_create_args_t wake_display_timer_args = {
            .callback = [](void *arg) {
                atk_dnesp32s3_box0* self = static_cast<atk_dnesp32s3_box0*>(arg);
                if (self->LcdStatus_ == kDevicelcdbacklightOff && Application::GetInstance().GetDeviceState() == kDeviceStateListening 
                    && self->wake_status_ == kDeviceWaitWake) {

                    if (self->power_sleep_ == kDeviceNeutralSleep) {
                        self->power_save_timer_->WakeUp();
                    }

                    self->GetBacklight()->RestoreBrightness();
                    self->wake_status_ = kDeviceAwakened;
                    self->LcdStatus_ = kDevicelcdbacklightOn;
                } else if (self->power_sleep_ == kDeviceNeutralSleep && Application::GetInstance().GetDeviceState() == kDeviceStateListening 
                         && self->LcdStatus_ != kDevicelcdbacklightOff && self->wake_status_ == kDeviceAwakened) {
                    self->power_save_timer_->WakeUp();
                    self->power_sleep_ = kDeviceNoSleep;
                } else {
                    self->ticks_ ++;
                    if (self->ticks_ % self->kChgCtrlInterval == 0) {
                        if (gpio_get_level(CHRG_PIN) == 0) {
                            self->power_status_ = kDeviceTypecSupply;
                        } else {
                            self->power_status_ = kDeviceBatterySupply;
                        }

                        if (self->power_manager_->low_voltage_ < 2877 && self->power_status_ != kDeviceTypecSupply) {
                            esp_timer_stop(self->power_manager_->timer_handle_);
                            gpio_set_level(CHG_CTRL_PIN, 0);
                            vTaskDelay(pdMS_TO_TICKS(100));
                            gpio_set_level(SYS_POW_PIN, 0);     
                            vTaskDelay(pdMS_TO_TICKS(100));
                        }
                    }
                }
            },
            .arg = this,
            .dispatch_method = ESP_TIMER_TASK,
            .name = "wake_update_timer",
            .skip_unhandled_events = true,
        };
        ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_));
        ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 300000));
    }

    void InitializePowerManager() {
        power_manager_ = new PowerManager(CHRG_PIN);
        power_manager_->OnChargingStatusChanged([this](bool is_charging) {
            if (is_charging) {
                power_save_timer_->SetEnabled(false);
            } else {
                power_save_timer_->SetEnabled(true);
            }
        });
    }

    void InitializePowerSaveTimer() {
        power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
        power_save_timer_->OnEnterSleepMode([this]() {
            power_sleep_ = kDeviceNeutralSleep;
            XiaozhiStatus_ = kDevice_join_Sleep;
            display_->SetChatMessage("system", "");
            display_->SetEmotion("sleepy");

            if (LcdStatus_ != kDevicelcdbacklightOff) {
                GetBacklight()->SetBrightness(1);
            }
        });
        power_save_timer_->OnExitSleepMode([this]() {
            power_sleep_ = kDeviceNoSleep;
            display_->SetChatMessage("system", "");
            display_->SetEmotion("neutral");

            if (XiaozhiStatus_ != kDevice_Exit_Sleep) {
                GetBacklight()->RestoreBrightness();
            }
        });
        power_save_timer_->OnShutdownRequest([this]() {
            if (power_status_ == kDeviceBatterySupply) {
                esp_timer_stop(power_manager_->timer_handle_);
                gpio_set_level(CHG_CTRL_PIN, 0);
                vTaskDelay(pdMS_TO_TICKS(100));
                gpio_set_level(SYS_POW_PIN, 0);
                vTaskDelay(pdMS_TO_TICKS(100));
            }
        });

        power_save_timer_->SetEnabled(true);
    }

    // Initialize I2C peripheral
    void InitializeI2c() {
        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_));
    }

    // Initialize spi peripheral
    void InitializeSpi() {
        spi_bus_config_t buscfg = {};
        buscfg.mosi_io_num = LCD_MOSI_PIN;
        buscfg.miso_io_num = GPIO_NUM_NC;
        buscfg.sclk_io_num = LCD_SCLK_PIN;
        buscfg.quadwp_io_num = GPIO_NUM_NC;
        buscfg.quadhd_io_num = GPIO_NUM_NC;
        buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
        ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
    }

    void InitializeButtons() {
        middle_button_.OnClick([this]() {
            auto& app = Application::GetInstance();

            if (LcdStatus_ != kDevicelcdbacklightOff) {
                if (power_sleep_ == kDeviceNeutralSleep) {
                    power_save_timer_->WakeUp();
                    power_sleep_ = kDeviceNoSleep;
                }

                app.ToggleChatState();
            }
        });

        middle_button_.OnPressUp([this]() {
            if (LcdStatus_ == kDevicelcdbacklightOff) {
                Application::GetInstance().StopListening();
                Application::GetInstance().SetDeviceState(kDeviceStateIdle);
                wake_status_ = kDeviceWaitWake;
            }

            if (XiaozhiStatus_ == kDevice_Distributionnetwork || XiaozhiStatus_ == kDevice_Exit_Sleep) {
                esp_timer_stop(power_manager_->timer_handle_);
                gpio_set_level(CHG_CTRL_PIN, 0);
                vTaskDelay(pdMS_TO_TICKS(100));
                gpio_set_level(SYS_POW_PIN, 0);
                vTaskDelay(pdMS_TO_TICKS(100));
            } else if (XiaozhiStatus_ == kDevice_join_Sleep) {
                GetBacklight()->RestoreBrightness();
                XiaozhiStatus_ = kDevice_null;
            }
        });

        middle_button_.OnLongPress([this]() {
            auto& app = Application::GetInstance();
            if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
                ResetWifiConfiguration();
            }

            if (app.GetDeviceState() != kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
                if (app.GetDeviceState() == kDeviceStateWifiConfiguring && power_status_ != kDeviceTypecSupply) {
                    GetBacklight()->SetBrightness(0);
                    XiaozhiStatus_ = kDevice_Distributionnetwork;
                } else if (power_status_ == kDeviceBatterySupply && LcdStatus_ != kDevicelcdbacklightOff) {
                    Application::GetInstance().StartListening();
                    GetBacklight()->SetBrightness(0);   
                    XiaozhiStatus_ = kDevice_Exit_Sleep;
                } else if (power_status_ == kDeviceTypecSupply && LcdStatus_ == kDevicelcdbacklightOn && Application::GetInstance().GetDeviceState() != kDeviceStateStarting) {
                    Application::GetInstance().StartListening();
                    GetBacklight()->SetBrightness(0);
                    LcdStatus_ = kDevicelcdbacklightOff;
                } else if (LcdStatus_ == kDevicelcdbacklightOff && (power_status_ == kDeviceTypecSupply || power_status_ == kDeviceBatterySupply)) {
                    GetDisplay()->SetChatMessage("system", "");
                    GetBacklight()->RestoreBrightness();
                    wake_status_ = kDeviceAwakened;
                    LcdStatus_ = kDevicelcdbacklightOn;
                }
            }
        });

        left_button_.OnClick([this]() {
            if (power_sleep_ == kDeviceNeutralSleep && LcdStatus_ != kDevicelcdbacklightOff) {
                power_save_timer_->WakeUp();
                power_sleep_ = kDeviceNoSleep;
            }

            auto codec = GetAudioCodec();
            auto volume = codec->output_volume() - 10;
            if (volume < 0) {
                volume = 0;
            }
            codec->SetOutputVolume(volume);
            GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
        });

        left_button_.OnLongPress([this]() {
            GetAudioCodec()->SetOutputVolume(0);
            GetDisplay()->ShowNotification(Lang::Strings::MUTED);
        });

        right_button_.OnClick([this]() {
            if (power_sleep_ == kDeviceNeutralSleep && LcdStatus_ != kDevicelcdbacklightOff) {
                power_save_timer_->WakeUp();
                power_sleep_ = kDeviceNoSleep;
            }
            auto codec = GetAudioCodec();
            auto volume = codec->output_volume() + 10;
            if (volume > 100) {
                volume = 100;
            }
            codec->SetOutputVolume(volume);
            GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
        });

        right_button_.OnLongPress([this]() {
            GetAudioCodec()->SetOutputVolume(100);
            GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME);
        });


    }

    void InitializeSt7789Display() {
        ESP_LOGI(TAG, "Install panel IO");

        esp_lcd_panel_io_spi_config_t io_config = {};
        io_config.cs_gpio_num = LCD_CS_PIN;
        io_config.dc_gpio_num = LCD_DC_PIN;
        io_config.spi_mode = 0;
        io_config.pclk_hz = 80 * 1000 * 1000;
        io_config.trans_queue_depth = 7;
        io_config.lcd_cmd_bits = 8;
        io_config.lcd_param_bits = 8;
        esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io);

        ESP_LOGI(TAG, "Install LCD driver");
        esp_lcd_panel_dev_config_t panel_config = {};
        panel_config.reset_gpio_num = LCD_RST_PIN;
        panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
        panel_config.bits_per_pixel = 16;
        panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
        esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel);
        
        esp_lcd_panel_reset(panel);
        esp_lcd_panel_invert_color(panel, true);
        esp_lcd_panel_init(panel);
        esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); 
        esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);

        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,
                                    {
                                        .text_font = &font_puhui_20_4,
                                        .icon_font = &font_awesome_20_4,
                                        .emoji_font = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(),
                                    });
    }

    // 物联网初始化,添加对 AI 可见设备 
    void InitializeIot() {
        auto& thing_manager = iot::ThingManager::GetInstance();
        thing_manager.AddThing(iot::CreateThing("Speaker"));
        thing_manager.AddThing(iot::CreateThing("Screen"));
        thing_manager.AddThing(iot::CreateThing("Battery"));
    }

public:
    atk_dnesp32s3_box0() :
        right_button_(R_BUTTON_GPIO, false),
        left_button_(L_BUTTON_GPIO, false),
        middle_button_(M_BUTTON_GPIO, true) {
        InitializeBoardPowerManager();
        InitializePowerManager();
        InitializePowerSaveTimer();
        InitializeI2c();
        InitializeSpi();
        InitializeSt7789Display();
        InitializeButtons();
        InitializeIot();
        GetBacklight()->RestoreBrightness();
    }

    virtual AudioCodec* GetAudioCodec() override {
        static Es8311AudioCodec audio_codec(
            i2c_bus_, 
            I2C_NUM_0, 
            AUDIO_INPUT_SAMPLE_RATE, 
            AUDIO_OUTPUT_SAMPLE_RATE,
            GPIO_NUM_NC, 
            AUDIO_I2S_GPIO_BCLK, 
            AUDIO_I2S_GPIO_WS, 
            AUDIO_I2S_GPIO_DOUT, 
            AUDIO_I2S_GPIO_DIN,
            GPIO_NUM_NC, 
            AUDIO_CODEC_ES8311_ADDR, 
            false);
        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 = power_manager_->IsCharging();
        discharging = power_manager_->IsDischarging();
        if (discharging != last_discharging) {
            power_save_timer_->SetEnabled(discharging);
            last_discharging = discharging;
        }
        level = power_manager_->GetBatteryLevel();
        return true;
    }

    virtual void SetPowerSaveMode(bool enabled) override {
        if (!enabled) {
            power_save_timer_->WakeUp();
        }
        WifiBoard::SetPowerSaveMode(enabled);
    }
};

DECLARE_BOARD(atk_dnesp32s3_box0);

```

## /main/boards/atk-dnesp32s3-box0/config.h

```h path="/main/boards/atk-dnesp32s3-box0/config.h" 
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_

#include <driver/gpio.h>

enum XiaozhiStatus {
    kDevice_null,
    kDevice_join_Sleep,
    kDevice_Exit_Sleep,
    kDevice_Distributionnetwork,
    kDevice_Exit_Distributionnetwork,
};

enum LcdStatus {
    kDevicelcdbacklightOn,
    kDevicelcdbacklightOff,
};

enum WakeStatus {
    kDeviceAwakened,
    kDeviceWaitWake,
    kDeviceSleeped,
};

enum PowerSupply {
    kDeviceTypecSupply,
    kDeviceBatterySupply,
};

enum PowerSleep {
    kDeviceNoSleep,
    kDeviceDeepSleep,
    kDeviceNeutralSleep,
};

#define SYS_POW_PIN GPIO_NUM_2
#define CHG_CTRL_PIN GPIO_NUM_47
#define CODEC_PWR_PIN GPIO_NUM_14
#define CHRG_PIN GPIO_NUM_48

#define BAT_VSEN_PIN GPIO_NUM_1

#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 16000

#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_13
#define AUDIO_I2S_GPIO_WS GPIO_NUM_10
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_9

#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_11
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_12
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR

#define AUDIO_SPK_GPIO_PIN GPIO_NUM_21

#define R_BUTTON_GPIO GPIO_NUM_0
#define M_BUTTON_GPIO GPIO_NUM_4
#define L_BUTTON_GPIO GPIO_NUM_3

#define BUILTIN_LED_GPIO GPIO_NUM_13

#define LCD_SCLK_PIN GPIO_NUM_39
#define LCD_MOSI_PIN GPIO_NUM_40
#define LCD_MISO_PIN GPIO_NUM_NC
#define LCD_DC_PIN GPIO_NUM_38
#define LCD_CS_PIN GPIO_NUM_41
#define LCD_RST_PIN GPIO_NUM_NC

#define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 240
#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_42
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false

#endif // _BOARD_CONFIG_H_


```

## /main/boards/atk-dnesp32s3-box0/config.json

```json path="/main/boards/atk-dnesp32s3-box0/config.json" 
{
    "target": "esp32s3",
    "builds": [
        {
            "name": "atk-dnesp32s3-box0",
            "sdkconfig_append": []
        }
    ]
}
```

## /main/boards/atk-dnesp32s3-box0/power_manager.h

```h path="/main/boards/atk-dnesp32s3-box0/power_manager.h" 
#pragma once
#include <vector>
#include <functional>

#include <esp_timer.h>
#include <driver/gpio.h>
#include <esp_adc/adc_oneshot.h>


class PowerManager {
private:
    std::function<void(bool)> on_charging_status_changed_;
    std::function<void(bool)> on_low_battery_status_changed_;

    gpio_num_t charging_pin_ = GPIO_NUM_NC;
    std::vector<uint16_t> adc_values_;
    uint32_t battery_level_ = 0;
    bool is_charging_ = false;
    bool is_low_battery_ = false;
    int ticks_ = 0;
    const int kBatteryAdcInterval = 60;
    const int kBatteryAdcDataCount = 3;
    const int kLowBatteryLevel = 20;

    adc_oneshot_unit_handle_t adc_handle_;

    void CheckBatteryStatus() {
        // Get charging status
        bool new_charging_status = gpio_get_level(charging_pin_) == 0;
        if (new_charging_status != is_charging_) {
            is_charging_ = new_charging_status;
            if (on_charging_status_changed_) {
                on_charging_status_changed_(is_charging_);
            }
            ReadBatteryAdcData();
            return;
        }

        // 如果电池电量数据不足,则读取电池电量数据
        if (adc_values_.size() < kBatteryAdcDataCount) {
            ReadBatteryAdcData();
            return;
        }

        // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据
        ticks_++;
        if (ticks_ % kBatteryAdcInterval == 0) {
            ReadBatteryAdcData();
        }
    }

    void ReadBatteryAdcData() {
        int adc_value;
        uint32_t temp_val = 0;

        gpio_set_level(CHG_CTRL_PIN, 0);
        vTaskDelay(pdMS_TO_TICKS(100));

        for(int t = 0; t < 10; t ++) {
            ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_0, &adc_value));
            temp_val += adc_value;
        }

        gpio_set_level(CHG_CTRL_PIN, 1);
        vTaskDelay(pdMS_TO_TICKS(100));

        adc_value = temp_val / 10;
        
        // 将 ADC 值添加到队列中
        adc_values_.push_back(adc_value);
        if (adc_values_.size() > kBatteryAdcDataCount) {
            adc_values_.erase(adc_values_.begin());
        }
        uint32_t average_adc = 0;
        for (auto value : adc_values_) {
            average_adc += value;
        }
        average_adc /= adc_values_.size();

        // 定义电池电量区间
        const struct {
            uint16_t adc;
            uint8_t level;
        } levels[] = {
            {2951, 0},  /*  3.80V */
            {3019, 20},
            {3037, 40},
            {3091, 60}, /* 3.88 */
            {3124, 80},
            {3231, 100}
        };

        // 低于最低值时
        if (average_adc < levels[0].adc) {
            battery_level_ = 0;
        }
        // 高于最高值时
        else if (average_adc >= levels[5].adc) {
            battery_level_ = 100;
        } else {
            // 线性插值计算中间值
            for (int i = 0; i < 5; i++) {
                if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) {
                    float ratio = static_cast<float>(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc);
                    battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level);
                    break;
                }
            }
        }

        // Check low battery status
        if (adc_values_.size() >= kBatteryAdcDataCount) {
            bool new_low_battery_status = battery_level_ <= kLowBatteryLevel;
            if (new_low_battery_status != is_low_battery_) {
                is_low_battery_ = new_low_battery_status;
                if (on_low_battery_status_changed_) {
                    on_low_battery_status_changed_(is_low_battery_);
                }
            }
        }
        
        low_voltage_ = adc_value;

        ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_);
    }

public:
    esp_timer_handle_t timer_handle_;
    uint16_t low_voltage_ = 2877;
    PowerManager(gpio_num_t pin) : charging_pin_(pin) {
        // 创建电池电量检查定时器
        esp_timer_create_args_t timer_args = {
            .callback = [](void* arg) {
                PowerManager* self = static_cast<PowerManager*>(arg);
                self->CheckBatteryStatus();
            },
            .arg = this,
            .dispatch_method = ESP_TIMER_TASK,
            .name = "battery_check_timer",
            .skip_unhandled_events = true,
        };
        ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_));
        ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000));

        // 初始化 ADC
        adc_oneshot_unit_init_cfg_t init_config = {
            .unit_id = ADC_UNIT_1,
            .ulp_mode = ADC_ULP_MODE_DISABLE,
        };
        ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_));
        
        adc_oneshot_chan_cfg_t chan_config = {
            .atten = ADC_ATTEN_DB_12,
            .bitwidth = ADC_BITWIDTH_12,
        };
        ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_0, &chan_config));
    }

    ~PowerManager() {
        if (timer_handle_) {
            esp_timer_stop(timer_handle_);
            esp_timer_delete(timer_handle_);
        }
        if (adc_handle_) {
            adc_oneshot_del_unit(adc_handle_);
        }
    }

    bool IsCharging() {
        // 如果电量已经满了,则不再显示充电中
        if (battery_level_ == 100) {
            return false;
        }
        return is_charging_;
    }

    bool IsDischarging() {
        // 没有区分充电和放电,所以直接返回相反状态
        return !is_charging_;
    }

    uint8_t GetBatteryLevel() {
        return battery_level_;
    }

    void OnLowBatteryStatusChanged(std::function<void(bool)> callback) {
        on_low_battery_status_changed_ = callback;
    }

    void OnChargingStatusChanged(std::function<void(bool)> callback) {
        on_charging_status_changed_ = callback;
    }
};

```

## /main/boards/atk-dnesp32s3/atk_dnesp32s3.cc

```cc path="/main/boards/atk-dnesp32s3/atk_dnesp32s3.cc" 
#include "wifi_board.h"
#include "es8388_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "i2c_device.h"
#include "iot/thing_manager.h"
#include "led/single_led.h"

#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <wifi_station.h>

#define TAG "atk_dnesp32s3"

LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(font_awesome_20_4);

class XL9555 : public I2cDevice {
public:
    XL9555(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
        WriteReg(0x06, 0x03);
        WriteReg(0x07, 0xF0);
    }

    void SetOutputState(uint8_t bit, uint8_t level) {
        uint16_t data;
        int index = bit;

        if (bit < 8) {
            data = ReadReg(0x02);
        } else {
            data = ReadReg(0x03);
            index -= 8;
        }

        data = (data & ~(1 << index)) | (level << index);

        if (bit < 8) {
            WriteReg(0x02, data);
        } else {
            WriteReg(0x03, data);
        }
    }
};


class atk_dnesp32s3 : public WifiBoard {
private:
    i2c_master_bus_handle_t i2c_bus_;
    Button boot_button_;
    LcdDisplay* display_;
    XL9555* xl9555_;

    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_));

        // Initialize XL9555
        xl9555_ = new XL9555(i2c_bus_, 0x20);
    }

    // Initialize spi peripheral
    void InitializeSpi() {
        spi_bus_config_t buscfg = {};
        buscfg.mosi_io_num = LCD_MOSI_PIN;
        buscfg.miso_io_num = GPIO_NUM_NC;
        buscfg.sclk_io_num = LCD_SCLK_PIN;
        buscfg.quadwp_io_num = GPIO_NUM_NC;
        buscfg.quadhd_io_num = GPIO_NUM_NC;
        buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
        ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
    }

    void InitializeButtons() {
        boot_button_.OnClick([this]() {
            auto& app = Application::GetInstance();
            if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
                ResetWifiConfiguration();
            }
            app.ToggleChatState();
        });
    }

    void InitializeSt7789Display() {
        esp_lcd_panel_io_handle_t panel_io = nullptr;
        esp_lcd_panel_handle_t panel = nullptr;
        ESP_LOGD(TAG, "Install panel IO");
        // 液晶屏控制IO初始化
        esp_lcd_panel_io_spi_config_t io_config = {};
        io_config.cs_gpio_num = LCD_CS_PIN;
        io_config.dc_gpio_num = LCD_DC_PIN;
        io_config.spi_mode = 0;
        io_config.pclk_hz = 20 * 1000 * 1000;
        io_config.trans_queue_depth = 7;
        io_config.lcd_cmd_bits = 8;
        io_config.lcd_param_bits = 8;
        esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io);

        // 初始化液晶屏驱动芯片ST7789
        ESP_LOGD(TAG, "Install LCD driver");
        esp_lcd_panel_dev_config_t panel_config = {};
        panel_config.reset_gpio_num = GPIO_NUM_NC;
        panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
        panel_config.bits_per_pixel = 16;
        panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
        esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel);
        
        esp_lcd_panel_reset(panel);
        xl9555_->SetOutputState(8, 1);
        xl9555_->SetOutputState(2, 0);

        esp_lcd_panel_init(panel);
        esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
        esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); 
        esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
        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,
                                    {
                                        .text_font = &font_puhui_20_4,
                                        .icon_font = &font_awesome_20_4,
                                        #if CONFIG_USE_WECHAT_MESSAGE_STYLE
                                            .emoji_font = font_emoji_32_init(),
                                        #else
                                            .emoji_font = DISPLAY_HEIGHT >= 240 ? font_emoji_64_init() : font_emoji_32_init(),
                                        #endif
                                    });
    }

    // 物联网初始化,添加对 AI 可见设备 
    void InitializeIot() {
        auto& thing_manager = iot::ThingManager::GetInstance();
        thing_manager.AddThing(iot::CreateThing("Speaker"));
        thing_manager.AddThing(iot::CreateThing("Screen"));
    }

public:
    atk_dnesp32s3() : boot_button_(BOOT_BUTTON_GPIO) {
        InitializeI2c();
        InitializeSpi();
        InitializeSt7789Display();
        InitializeButtons();
        InitializeIot();
    }

    virtual Led* GetLed() override {
        static SingleLed led(BUILTIN_LED_GPIO);
        return &led;
    }

    virtual AudioCodec* GetAudioCodec() override {
        static Es8388AudioCodec audio_codec(
            i2c_bus_, 
            I2C_NUM_0, 
            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,
            GPIO_NUM_NC, 
            AUDIO_CODEC_ES8388_ADDR
        );
        return &audio_codec;
    }

    virtual Display* GetDisplay() override {
        return display_;
    }
};

DECLARE_BOARD(atk_dnesp32s3);

```

## /main/boards/atk-dnesp32s3/config.h

```h path="/main/boards/atk-dnesp32s3/config.h" 

#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_I2S_GPIO_MCLK GPIO_NUM_3
#define AUDIO_I2S_GPIO_WS GPIO_NUM_9
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_46
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_14
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_10

#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_41
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_42
#define AUDIO_CODEC_ES8388_ADDR ES8388_CODEC_DEFAULT_ADDR

#define BOOT_BUTTON_GPIO GPIO_NUM_0

#define BUILTIN_LED_GPIO GPIO_NUM_1

#define LCD_SCLK_PIN GPIO_NUM_12
#define LCD_MOSI_PIN GPIO_NUM_11
#define LCD_MISO_PIN GPIO_NUM_13
#define LCD_DC_PIN GPIO_NUM_40
#define LCD_CS_PIN GPIO_NUM_21

#define DISPLAY_WIDTH    320
#define DISPLAY_HEIGHT   240
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY  true

#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0

#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true

#endif // _BOARD_CONFIG_H_


```

## /main/boards/atk-dnesp32s3/config.json

```json path="/main/boards/atk-dnesp32s3/config.json" 
{
    "target": "esp32s3",
    "builds": [
        {
            "name": "atk-dnesp32s3",
            "sdkconfig_append": []
        }
    ]
}
```

## /main/boards/atk-dnesp32s3m-4g/atk_dnesp32s3m.cc

```cc path="/main/boards/atk-dnesp32s3m-4g/atk_dnesp32s3m.cc" 
#include "ml307_board.h"
#include "es8388_audio_codec.h"
#include "display/lcd_display.h"
#include "system_reset.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "i2c_device.h"
#include "iot/thing_manager.h"
#include "led/single_led.h"
#include "driver/gpio.h"
#include "assets/lang_config.h"

#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <driver/spi_common.h>

#define TAG "atk_dnesp32s3m_4g"

LV_FONT_DECLARE(font_puhui_16_4);
LV_FONT_DECLARE(font_awesome_16_4);

class atk_dnesp32s3m_4g : public Ml307Board {
private:
    i2c_master_bus_handle_t i2c_bus_;
    Button boot_button_;
    Button volume_up_button_;
    Button volume_down_button_;
    Button phone_button_;
    LcdDisplay* display_;

    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_));
    }

    // Initialize spi peripheral
    void InitializeSpi() {
        spi_bus_config_t buscfg = {};
        buscfg.mosi_io_num = LCD_MOSI_PIN;
        buscfg.miso_io_num = GPIO_NUM_NC;
        buscfg.sclk_io_num = LCD_SCLK_PIN;
        buscfg.quadwp_io_num = GPIO_NUM_NC;
        buscfg.quadhd_io_num = GPIO_NUM_NC;
        buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
        ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
    }

    void InitializeButtons() {
        boot_button_.OnClick([this]() {
            Application::GetInstance().ToggleChatState();
        });

        volume_up_button_.OnClick([this]() {
            auto codec = GetAudioCodec();
            auto volume = codec->output_volume() + 10;
            if (volume > 100) {
                volume = 100;
            }
            codec->SetOutputVolume(volume);
            GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
        });

        volume_up_button_.OnLongPress([this]() {
            GetAudioCodec()->SetOutputVolume(100);
            GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME);
        });

        volume_down_button_.OnClick([this]() {
            auto codec = GetAudioCodec();
            auto volume = codec->output_volume() - 10;
            if (volume < 0) {
                volume = 0;
            }
            codec->SetOutputVolume(volume);
            GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
        });

        volume_down_button_.OnLongPress([this]() {
            GetAudioCodec()->SetOutputVolume(0);
            GetDisplay()->ShowNotification(Lang::Strings::MUTED);
        });

        //不插耳机
        phone_button_.OnPressDown([this]() {
            gpio_set_level(SPK_EN_PIN, 1);
        });

        //插入耳机
        phone_button_.OnPressUp([this]() {
            gpio_set_level(SPK_EN_PIN, 0);
        });
    }

    void InitializeSt7735Display() {
        esp_lcd_panel_io_handle_t panel_io = nullptr;
        esp_lcd_panel_handle_t panel = nullptr;
        // 液晶屏控制IO初始化
        ESP_LOGD(TAG, "Install panel IO");
        esp_lcd_panel_io_spi_config_t io_config = {};
        io_config.cs_gpio_num = LCD_CS_PIN;
        io_config.dc_gpio_num = LCD_DC_PIN;
        io_config.spi_mode = 0;
        io_config.pclk_hz = 60 * 1000 * 1000;
        io_config.trans_queue_depth = 7;
        io_config.lcd_cmd_bits = 8;
        io_config.lcd_param_bits = 8;
        esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io);

        // 初始化液晶屏驱动芯片ST7735
        ESP_LOGD(TAG, "Install LCD driver");
        esp_lcd_panel_dev_config_t panel_config = {};
        panel_config.reset_gpio_num = LCD_RST_PIN;
        panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR;
        panel_config.bits_per_pixel = 16;
        panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
        esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel);
        
        //使能功放引脚
        gpio_config_t io_conf;
        io_conf.intr_type = GPIO_INTR_DISABLE;
        io_conf.mode = GPIO_MODE_OUTPUT;
        io_conf.pin_bit_mask = (1ULL << SPK_EN_PIN);
        io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
        io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
        gpio_config(&io_conf);
        gpio_set_level(SPK_EN_PIN, 0);

        //检测耳机是否插入,插入时为高电平
        io_conf.intr_type = GPIO_INTR_DISABLE;
        io_conf.mode = GPIO_MODE_INPUT;
        io_conf.pin_bit_mask = (1ULL << PHONE_CK_PIN);
        io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
        io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
        gpio_config(&io_conf);

        //耳机插入
        if (gpio_get_level(PHONE_CK_PIN)) {
            gpio_set_level(SPK_EN_PIN, 1);
        }

        esp_lcd_panel_reset(panel);
        esp_lcd_panel_init(panel);

        uint8_t data0[] = {0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29, 0x25, 0x2B, 0x39, 0x00, 0x01, 0x03, 0x10};
        uint8_t data1[] = {0x03, 0x1d, 0x07, 0x06, 0x2E, 0x2C, 0x29, 0x2D, 0x2E, 0x2E, 0x37, 0x3F, 0x00, 0x00, 0x02, 0x10};
        esp_lcd_panel_io_tx_param(panel_io, 0xe0, data0, 16);
        esp_lcd_panel_io_tx_param(panel_io, 0xe1, data1, 16);

        esp_lcd_panel_invert_color(panel, true);
        esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); 
        esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);

        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,
                                    {
                                        .text_font = &font_puhui_16_4,
                                        .icon_font = &font_awesome_16_4,
                                        .emoji_font = font_emoji_32_init(),
                                    });
    }

    // 物联网初始化,添加对 AI 可见设备 
    void InitializeIot() {
        auto& thing_manager = iot::ThingManager::GetInstance();
        thing_manager.AddThing(iot::CreateThing("Speaker"));
        thing_manager.AddThing(iot::CreateThing("Screen"));
    }

public:
    atk_dnesp32s3m_4g() : Ml307Board(Module_4G_TX_PIN, Module_4G_RX_PIN, 4096),
        boot_button_(BOOT_BUTTON_GPIO), 
        volume_up_button_(VOLUME_UP_BUTTON_GPIO),
        volume_down_button_(VOLUME_DOWN_BUTTON_GPIO), 
        phone_button_(PHONE_CK_PIN, true) {
        InitializeI2c();
        InitializeSpi();
        InitializeSt7735Display();
        InitializeButtons();
        InitializeIot();
        if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
            GetBacklight()->RestoreBrightness();
        }
    }

    virtual Led* GetLed() override {
        static SingleLed led(BUILTIN_LED_GPIO);
        return &led;
    }

    virtual AudioCodec* GetAudioCodec() override {
        static Es8388AudioCodec audio_codec(
            i2c_bus_, 
            I2C_NUM_0, 
            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,
            GPIO_NUM_NC, 
            AUDIO_CODEC_ES8388_ADDR
        );
        return &audio_codec;
    }

    virtual Display* GetDisplay() override {
        return display_;
    }

    virtual Backlight* GetBacklight() override {
        if (DISPLAY_BACKLIGHT_PIN != GPIO_NUM_NC) {
            static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
            return &backlight;
        }
        return nullptr;
    }
};

DECLARE_BOARD(atk_dnesp32s3m_4g);

```

## /main/boards/atommatrix-echo-base/README.md

# 编译配置命令

**配置编译目标为 ESP32:**

```bash
idf.py set-target esp32
```

**打开 menuconfig:**

```bash
idf.py menuconfig
```

**选择板子:**

```
Xiaozhi Assistant -> Board Type -> AtomMatrix + Echo Base
```

**修改 flash 大小:**

```
Serial flasher config -> Flash size -> 4 MB
```

**修改分区表:**

```
Partition Table -> Custom partition CSV file -> partitions_4M.csv
```

**编译:**

```bash
idf.py build
```

## /main/boards/atommatrix-echo-base/config.json

```json path="/main/boards/atommatrix-echo-base/config.json" 
{
    "target": "esp32",
    "builds": [
        {
            "name": "atommatrix-echo-base",
            "sdkconfig_append": [
                "CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
                "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_4M.csv\""
            ]
        }
    ]
}
```

## /main/boards/atoms3r-cam-m12-echo-base/config.json

```json path="/main/boards/atoms3r-cam-m12-echo-base/config.json" 
{
    "target": "esp32s3",
    "builds": [
        {
            "name": "atoms3r-cam-m12-echo-base",
            "sdkconfig_append": [
                "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
                "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions_8M.csv\""
            ]
        }
    ]
}
```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!