xuxinyi 4 månader sedan
incheckning
2d198c1ba3
100 ändrade filer med 2432 tillägg och 0 borttagningar
  1. 2 0
      .clangd
  2. 13 0
      .gitignore
  3. 14 0
      CMakeLists.txt
  4. 21 0
      LICENSE
  5. 158 0
      README.md
  6. 142 0
      README_en.md
  7. 139 0
      README_ja.md
  8. BIN
      docs/AtomMatrix-echo-base.jpg
  9. BIN
      docs/ESP32-BreadBoard.jpg
  10. BIN
      docs/atoms3r-echo-base.jpg
  11. BIN
      docs/esp-sparkbot.jpg
  12. BIN
      docs/esp32s3-box3.jpg
  13. BIN
      docs/how_to_use_0.png
  14. BIN
      docs/how_to_use_1.png
  15. BIN
      docs/lichuang-s3.jpg
  16. BIN
      docs/lilygo-t-circle-s3.jpg
  17. BIN
      docs/m5stack-cores3.jpg
  18. BIN
      docs/magiclick-2p4.jpg
  19. BIN
      docs/v1/atoms3r.jpg
  20. BIN
      docs/v1/espbox3.jpg
  21. BIN
      docs/v1/lichuang-s3.jpg
  22. BIN
      docs/v1/m5cores3.jpg
  23. BIN
      docs/v1/magiclick.jpg
  24. BIN
      docs/v1/movecall-cuican-esp32s3.jpg
  25. BIN
      docs/v1/movecall-moji-esp32s3.jpg
  26. BIN
      docs/v1/sensecap_watcher.jpg
  27. BIN
      docs/v1/waveshare.jpg
  28. BIN
      docs/v1/wmnologo_xingzhi_0.96.jpg
  29. BIN
      docs/v1/wmnologo_xingzhi_1.54.jpg
  30. BIN
      docs/waveshare-esp32-s3-touch-amoled-1.8.jpg
  31. 338 0
      docs/websocket.md
  32. BIN
      docs/wiring.jpg
  33. BIN
      docs/wiring2.jpg
  34. BIN
      docs/xmini-c3.jpg
  35. 214 0
      main/CMakeLists.txt
  36. 260 0
      main/Kconfig.projbuild
  37. 855 0
      main/application.cc
  38. 122 0
      main/application.h
  39. BIN
      main/assets/common/exclamation.p3
  40. BIN
      main/assets/common/low_battery.p3
  41. BIN
      main/assets/common/success.p3
  42. BIN
      main/assets/common/vibration.p3
  43. BIN
      main/assets/en-US/0.p3
  44. BIN
      main/assets/en-US/1.p3
  45. BIN
      main/assets/en-US/2.p3
  46. BIN
      main/assets/en-US/3.p3
  47. BIN
      main/assets/en-US/4.p3
  48. BIN
      main/assets/en-US/5.p3
  49. BIN
      main/assets/en-US/6.p3
  50. BIN
      main/assets/en-US/7.p3
  51. BIN
      main/assets/en-US/8.p3
  52. BIN
      main/assets/en-US/9.p3
  53. BIN
      main/assets/en-US/activation.p3
  54. BIN
      main/assets/en-US/err_pin.p3
  55. BIN
      main/assets/en-US/err_reg.p3
  56. 52 0
      main/assets/en-US/language.json
  57. BIN
      main/assets/en-US/upgrade.p3
  58. BIN
      main/assets/en-US/welcome.p3
  59. BIN
      main/assets/en-US/wificonfig.p3
  60. BIN
      main/assets/ja-JP/0.p3
  61. BIN
      main/assets/ja-JP/1.p3
  62. BIN
      main/assets/ja-JP/2.p3
  63. BIN
      main/assets/ja-JP/3.p3
  64. BIN
      main/assets/ja-JP/4.p3
  65. BIN
      main/assets/ja-JP/5.p3
  66. BIN
      main/assets/ja-JP/6.p3
  67. BIN
      main/assets/ja-JP/7.p3
  68. BIN
      main/assets/ja-JP/8.p3
  69. BIN
      main/assets/ja-JP/9.p3
  70. BIN
      main/assets/ja-JP/activation.p3
  71. BIN
      main/assets/ja-JP/err_pin.p3
  72. BIN
      main/assets/ja-JP/err_reg.p3
  73. 51 0
      main/assets/ja-JP/language.json
  74. BIN
      main/assets/ja-JP/upgrade.p3
  75. BIN
      main/assets/ja-JP/welcome.p3
  76. BIN
      main/assets/ja-JP/wificonfig.p3
  77. BIN
      main/assets/zh-CN/0.p3
  78. BIN
      main/assets/zh-CN/1.p3
  79. BIN
      main/assets/zh-CN/2.p3
  80. BIN
      main/assets/zh-CN/3.p3
  81. BIN
      main/assets/zh-CN/4.p3
  82. BIN
      main/assets/zh-CN/5.p3
  83. BIN
      main/assets/zh-CN/6.p3
  84. BIN
      main/assets/zh-CN/7.p3
  85. BIN
      main/assets/zh-CN/8.p3
  86. BIN
      main/assets/zh-CN/9.p3
  87. BIN
      main/assets/zh-CN/activation.p3
  88. BIN
      main/assets/zh-CN/err_pin.p3
  89. BIN
      main/assets/zh-CN/err_reg.p3
  90. 51 0
      main/assets/zh-CN/language.json
  91. BIN
      main/assets/zh-CN/upgrade.p3
  92. BIN
      main/assets/zh-CN/welcome.p3
  93. BIN
      main/assets/zh-CN/wificonfig.p3
  94. BIN
      main/assets/zh-TW/0.p3
  95. BIN
      main/assets/zh-TW/1.p3
  96. BIN
      main/assets/zh-TW/2.p3
  97. BIN
      main/assets/zh-TW/3.p3
  98. BIN
      main/assets/zh-TW/4.p3
  99. BIN
      main/assets/zh-TW/5.p3
  100. BIN
      main/assets/zh-TW/6.p3

+ 2 - 0
.clangd

@@ -0,0 +1,2 @@
+CompileFlags:
+    Remove: [-f*, -m*]

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+tmp/
+components/
+managed_components/
+build/
+.vscode/
+.devcontainer/
+sdkconfig.old
+sdkconfig
+dependencies.lock
+.env
+releases/
+main/assets/lang_config.h
+.DS_Store

+ 14 - 0
CMakeLists.txt

@@ -0,0 +1,14 @@
+# 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.5.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)
+

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Xiaoxia
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 158 - 0
README.md

@@ -0,0 +1,158 @@
+# 支持ESP32-P4-Function-EV-Board
+
+在menuconfig中将Board Type修改为ESP32-P4-Function-EV-Board
+
+![修改Board Type](docs/how_to_use_0.png)
+
+在menuconfig中将Wi-Fi Remote中choose slave target修改为esp32c6
+
+![choose slave target](docs/how_to_use_1.png)
+
+# 小智 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://github.com/WMnologo/xingzhi-ai" target="_blank" title="无名科技Nologo-星智-0.96">无名科技Nologo-星智-0.96TFT</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/wmnologo_xingzhi_0.96.jpg" target="_blank" title="无名科技Nologo-星智-0.96">
+    <img src="docs/v1/wmnologo_xingzhi_0.96.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++ 代码风格,提交代码时请确保符合规范
+
+
+## 智能体配置
+
+如果你已经拥有一个小智 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>

+ 142 - 0
README_en.md

@@ -0,0 +1,142 @@
+# XiaoZhi AI Chatbot
+
+([中文](README.md) | English | [日本語](README_ja.md))
+
+This is Terrence's first hardware project.
+
+👉 [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://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/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
+
+## 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> 

+ 139 - 0
README_ja.md

@@ -0,0 +1,139 @@
+# シャオジー AI チャットボット
+
+([中文](README.md) | [English](README_en.md) | 日本語)
+
+これは シャーガー(Terrence)の最初のハードウェア作品です。
+
+👉 [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://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/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++ コードスタイルを使用、コード提出時にはコンプライアンスを確認
+
+## 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> 

BIN
docs/AtomMatrix-echo-base.jpg


BIN
docs/ESP32-BreadBoard.jpg


BIN
docs/atoms3r-echo-base.jpg


BIN
docs/esp-sparkbot.jpg


BIN
docs/esp32s3-box3.jpg


BIN
docs/how_to_use_0.png


BIN
docs/how_to_use_1.png


BIN
docs/lichuang-s3.jpg


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


BIN
docs/m5stack-cores3.jpg


BIN
docs/magiclick-2p4.jpg


BIN
docs/v1/atoms3r.jpg


BIN
docs/v1/espbox3.jpg


BIN
docs/v1/lichuang-s3.jpg


BIN
docs/v1/m5cores3.jpg


BIN
docs/v1/magiclick.jpg


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


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


BIN
docs/v1/sensecap_watcher.jpg


BIN
docs/v1/waveshare.jpg


BIN
docs/v1/wmnologo_xingzhi_0.96.jpg


BIN
docs/v1/wmnologo_xingzhi_1.54.jpg


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


+ 338 - 0
docs/websocket.md

@@ -0,0 +1,338 @@
+以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述客户端(设备)与服务器之间如何通过 WebSocket 进行交互。该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。
+
+---
+
+## 1. 总体流程概览
+
+1. **设备端初始化**  
+   - 设备上电、初始化 `Application`:  
+     - 初始化音频编解码器、显示屏、LED 等  
+     - 连接网络  
+     - 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol`)  
+   - 进入主循环等待事件(音频输入、音频输出、调度任务等)。
+
+2. **建立 WebSocket 连接**  
+   - 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()`:  
+     - 根据编译配置获取 WebSocket URL(`CONFIG_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 里进行额外鉴权。
+
+服务器与客户端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。

BIN
docs/wiring.jpg


BIN
docs/wiring2.jpg


BIN
docs/xmini-c3.jpg


+ 214 - 0
main/CMakeLists.txt

@@ -0,0 +1,214 @@
+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/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"
+            "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_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_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_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_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_ESP32_CGC)
+    set(BOARD_TYPE "esp32-cgc")
+elseif(CONFIG_BOARD_TYPE_ESP32P4_FUNCTION_EV_BOARD)
+    set(BOARD_TYPE "ESP32-P4-Function-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_CONNECTION_TYPE_MQTT_UDP)
+    list(APPEND SOURCES "protocols/mqtt_protocol.cc")
+elseif(CONFIG_CONNECTION_TYPE_WEBSOCKET)
+    list(APPEND SOURCES "protocols/websocket_protocol.cc")
+endif()
+
+if(CONFIG_USE_AUDIO_PROCESSOR)
+    list(APPEND SOURCES "audio_processing/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}
+)

+ 260 - 0
main/Kconfig.projbuild

@@ -0,0 +1,260 @@
+menu "Xiaozhi Assistant"
+
+config OTA_VERSION_URL
+    string "OTA Version URL"
+    default "https://api.tenclass.net/xiaozhi/ota/"
+    help
+        The application will access this URL to check for updates.
+
+
+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 CONNECTION_TYPE
+    prompt "Connection Type"
+    default CONNECTION_TYPE_MQTT_UDP
+    help
+        网络数据传输协议
+    config CONNECTION_TYPE_MQTT_UDP
+        bool "MQTT + UDP"
+    config CONNECTION_TYPE_WEBSOCKET
+        bool "Websocket"
+endchoice
+
+config WEBSOCKET_URL
+    depends on CONNECTION_TYPE_WEBSOCKET
+    string "Websocket URL"
+    default "wss://api.tenclass.net/xiaozhi/v1/"
+    help
+        Communication with the server through websocket after wake up.
+
+config WEBSOCKET_ACCESS_TOKEN
+    depends on CONNECTION_TYPE_WEBSOCKET
+    string "Websocket Access Token"
+    default "test-token"
+    help
+        Access token for websocket communication.
+
+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_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_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_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_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_ESP32P4_FUNCTION_EV_BOARD
+        bool "ESP32-P4-FUNCTION-EV-BOARD"
+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
+    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_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
+
+choice DISPLAY_ESP32P4_FUNCTION_EV_BOARD
+    depends on BOARD_TYPE_ESP32P4_FUNCTION_EV_BOARD
+    prompt "ESP32P4_FUNCTION_EV_BOARD LCD Type"
+    default LCD_EK79007_1024X600
+    help
+        屏幕类型选择
+    config LCD_EK79007_1024X600
+        bool "EK79007, 分辨率1024*600"
+    config LCD_TYPE_800_1280_10_1_INCH_A
+        bool "Waveshare 10.1-DSI-TOUCH-A Display"
+endchoice
+
+config USE_WECHAT_MESSAGE_STYLE
+    bool "使用微信聊天界面风格"
+    default n
+    help
+        使用微信聊天界面风格   
+
+config USE_AUDIO_PROCESSOR
+    bool "启用音频降噪、增益处理"
+    default y
+    depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
+    help
+        需要 ESP32 S3 与 AFE 支持
+
+config USE_WAKE_WORD_DETECT
+    bool "启用唤醒词检测"
+    default y
+    depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
+    help
+        需要 ESP32 S3 与 AFE 支持
+endmenu

+ 855 - 0
main/application.cc

@@ -0,0 +1,855 @@
+#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"
+
+#include <cstring>
+#include <esp_log.h>
+#include <cJSON.h>
+#include <driver/gpio.h>
+#include <arpa/inet.h>
+#include <esp_app_desc.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);
+
+    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_);
+}
+
+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() {
+    auto& board = Board::GetInstance();
+    auto display = board.GetDisplay();
+    // Check if there is a new firmware version available
+    ota_.SetPostData(board.GetJson());
+
+    const int MAX_RETRY = 10;
+    int retry_count = 0;
+
+    while (true) {
+        if (!ota_.CheckVersion()) {
+            retry_count++;
+            if (retry_count >= MAX_RETRY) {
+                ESP_LOGE(TAG, "Too many retries, exit version check");
+                return;
+            }
+            ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", 60, retry_count, MAX_RETRY);
+            vTaskDelay(pdMS_TO_TICKS(60000));
+            continue;
+        }
+        retry_count = 0;
+
+        if (ota_.HasNewVersion()) {
+            Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "happy", Lang::Sounds::P3_UPGRADE);
+            // Wait for the chat state to be idle
+            do {
+                vTaskDelay(pdMS_TO_TICKS(3000));
+            } while (GetDeviceState() != kDeviceStateIdle);
+
+            // Use main task to do the upgrade, not cancelable
+            Schedule([this, display]() {
+                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();
+        std::string message = std::string(Lang::Strings::VERSION) + ota_.GetCurrentVersion();
+        display->ShowNotification(message.c_str());
+    
+        if (ota_.HasActivationCode()) {
+            // Activation code is valid
+            SetDeviceState(kDeviceStateActivating);
+            ShowActivationCode();
+
+            // Check again in 60 seconds or until the device is idle
+            for (int i = 0; i < 60; ++i) {
+                if (device_state_ == kDeviceStateIdle) {
+                    break;
+                }
+                vTaskDelay(pdMS_TO_TICKS(1000));
+            }
+            continue;
+        }
+
+        SetDeviceState(kDeviceStateIdle);
+        display->SetChatMessage("system", "");
+        PlaySound(Lang::Sounds::P3_SUCCESS);
+        // Exit the loop if upgrade or idle
+        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);
+    vTaskDelay(pdMS_TO_TICKS(1000));
+    background_task_->WaitForCompletion();
+
+    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()) {
+        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) {
+    auto codec = Board::GetInstance().GetAudioCodec();
+    codec->EnableOutput(true);
+    SetDecodeSampleRate(16000);
+    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);
+        std::vector<uint8_t> opus;
+        opus.resize(payload_size);
+        memcpy(opus.data(), p3->payload, payload_size);
+        p += payload_size;
+
+        std::lock_guard<std::mutex> lock(mutex_);
+        audio_decode_queue_.emplace_back(std::move(opus));
+    }
+}
+
+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;
+            }
+
+            keep_listening_ = true;
+            protocol_->SendStartListening(kListeningModeAutoStop);
+            SetDeviceState(kDeviceStateListening);
+        });
+    } 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;
+    }
+    
+    keep_listening_ = false;
+    if (device_state_ == kDeviceStateIdle) {
+        Schedule([this]() {
+            if (!protocol_->IsAudioChannelOpened()) {
+                SetDeviceState(kDeviceStateConnecting);
+                if (!protocol_->OpenAudioChannel()) {
+                    return;
+                }
+            }
+            protocol_->SendStartListening(kListeningModeManualStop);
+            SetDeviceState(kDeviceStateListening);
+        });
+    } else if (device_state_ == kDeviceStateSpeaking) {
+        Schedule([this]() {
+            AbortSpeaking(kAbortReasonNone);
+            protocol_->SendStartListening(kListeningModeManualStop);
+            SetDeviceState(kDeviceStateListening);
+        });
+    }
+}
+
+void Application::StopListening() {
+    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_decode_sample_rate_ = codec->output_sample_rate();
+    opus_decoder_ = std::make_unique<OpusDecoderWrapper>(opus_decode_sample_rate_, 1);
+    opus_encoder_ = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
+    // For ML307 boards, we use complexity 5 to save bandwidth
+    // For other boards, we use complexity 3 to save CPU
+    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->OnInputReady([this, codec]() {
+        BaseType_t higher_priority_task_woken = pdFALSE;
+        xEventGroupSetBitsFromISR(event_group_, AUDIO_INPUT_READY_EVENT, &higher_priority_task_woken);
+        return higher_priority_task_woken == pdTRUE;
+    });
+    codec->OnOutputReady([this]() {
+        BaseType_t higher_priority_task_woken = pdFALSE;
+        xEventGroupSetBitsFromISR(event_group_, AUDIO_OUTPUT_READY_EVENT, &higher_priority_task_woken);
+        return higher_priority_task_woken == pdTRUE;
+    });
+    codec->Start();
+
+    /* Start the main loop */
+    xTaskCreate([](void* arg) {
+        Application* app = (Application*)arg;
+        app->MainLoop();
+        vTaskDelete(NULL);
+    }, "main_loop", 4096 * 2, this, 4, nullptr);
+
+    /* Wait for the network to be ready */
+    board.StartNetwork();
+
+    // Initialize the protocol
+    display->SetStatus(Lang::Strings::LOADING_PROTOCOL);
+#ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET
+    protocol_ = std::make_unique<WebsocketProtocol>();
+#else
+    protocol_ = std::make_unique<MqttProtocol>();
+#endif
+    protocol_->OnNetworkError([this](const std::string& message) {
+        SetDeviceState(kDeviceStateIdle);
+        Alert(Lang::Strings::ERROR, message.c_str(), "sad", Lang::Sounds::P3_EXCLAMATION);
+    });
+    protocol_->OnIncomingAudio([this](std::vector<uint8_t>&& data) {
+        std::lock_guard<std::mutex> lock(mutex_);
+        if (device_state_ == kDeviceStateSpeaking) {
+            audio_decode_queue_.emplace_back(std::move(data));
+        }
+    });
+    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());
+        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]() {
+                    if (device_state_ == kDeviceStateSpeaking) {
+                        background_task_->WaitForCompletion();
+                        if (keep_listening_) {
+                            protocol_->SendStartListening(kListeningModeAutoStop);
+                            SetDeviceState(kDeviceStateListening);
+                        } else {
+                            SetDeviceState(kDeviceStateIdle);
+                        }
+                    }
+                });
+            } else if (strcmp(state->valuestring, "sentence_start") == 0) {
+                auto text = cJSON_GetObjectItem(root, "text");
+                if (text != NULL) {
+                    ESP_LOGI(TAG, "<< %s", text->valuestring);
+                    std::string message = text->valuestring;
+                    // Schedule([this, display, message = std::string(text->valuestring)]() {
+                    //     display->SetChatMessage("assistant", message.c_str());
+                    // });
+                                // 简化去重:只对比上一次显示的消息
+                    if (message != last_displayed_message_) {
+                        Schedule([this, display, message]() {
+                            display->SetChatMessage("assistant", message.c_str());
+                            last_displayed_message_ = message; // 更新上一次显示的消息
+                        });
+                    }
+
+                }
+            }
+        } 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);
+                }
+            }
+        }
+    });
+    protocol_->Start();
+
+    codec->SetOutputVolume(90);
+
+    // Check for new firmware version or get the MQTT broker address
+    ota_.SetCheckVersionUrl(CONFIG_OTA_VERSION_URL);
+    ota_.SetHeader("Device-Id", SystemInfo::GetMacAddress().c_str());
+    ota_.SetHeader("Client-Id", board.GetUuid());
+    ota_.SetHeader("Accept-Language", Lang::CODE);
+    auto app_desc = esp_app_get_description();
+    ota_.SetHeader("User-Agent", std::string(BOARD_NAME "/") + app_desc->version);
+
+    xTaskCreate([](void* arg) {
+        Application* app = (Application*)arg;
+        app->CheckNewVersion();
+        vTaskDelete(NULL);          //这个意思是任务完成之后自动删除自身
+    }, "check_new_version", 4096 * 2, this, 2, nullptr);
+
+#if CONFIG_USE_AUDIO_PROCESSOR
+    audio_processor_.Initialize(codec->input_channels(), codec->input_reference());
+    audio_processor_.OnOutput([this](std::vector<int16_t>&& data) {
+        background_task_->Schedule([this, data = std::move(data)]() mutable {
+            opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
+                Schedule([this, opus = std::move(opus)]() {
+                    protocol_->SendAudio(opus);
+                });
+            });
+        });
+    });
+    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();
+            });
+        }
+    });
+#endif
+
+#if CONFIG_USE_WAKE_WORD_DETECT
+    wake_word_detect_.Initialize(codec->input_channels(), codec->input_reference());
+    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_->OpenAudioChannel()) {
+                    wake_word_detect_.StartDetection();
+                    return;
+                }
+                
+                std::vector<uint8_t> opus;
+                // Encode and send the wake word data to the server
+                while (wake_word_detect_.GetWakeWordOpus(opus)) {
+                    protocol_->SendAudio(opus);
+                }
+                // Set the chat state to wake word detected
+                protocol_->SendWakeWordDetected(wake_word);
+                ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
+                keep_listening_ = true;
+                SetDeviceState(kDeviceStateIdle);
+            } else if (device_state_ == kDeviceStateSpeaking) {
+                AbortSpeaking(kAbortReasonWakeWordDetected);
+            } else if (device_state_ == kDeviceStateActivating) {
+                SetDeviceState(kDeviceStateIdle);
+            }
+        });
+    });
+    wake_word_detect_.StartDetection();
+#endif
+
+    SetDeviceState(kDeviceStateIdle);
+    esp_timer_start_periodic(clock_timer_handle_, 1000000);
+}
+
+void Application::OnClockTimer() {
+    vTaskPrioritySet(NULL, 1);
+    clock_ticks_++;
+
+    // Print the debug info every 10 seconds
+    if (clock_ticks_ % 10 == 0) {
+        // SystemInfo::PrintTaskList();
+        // 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);
+                });
+            }
+        }
+    }
+}
+
+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 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::MainLoop() {
+    while (true) {
+        auto bits = xEventGroupWaitBits(event_group_,
+            SCHEDULE_EVENT | AUDIO_INPUT_READY_EVENT | AUDIO_OUTPUT_READY_EVENT,
+            pdTRUE, pdFALSE, portMAX_DELAY);
+
+        if (bits & AUDIO_INPUT_READY_EVENT) {
+            InputAudio();
+        }
+        if (bits & AUDIO_OUTPUT_READY_EVENT) {
+            OutputAudio();
+        }
+        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();
+            }
+        }
+    }
+}
+
+void Application::ResetDecoder() {
+    std::lock_guard<std::mutex> lock(mutex_);
+    opus_decoder_->ResetState();
+    audio_decode_queue_.clear();
+    last_output_time_ = std::chrono::steady_clock::now();
+}
+
+void Application::OutputAudio() {
+    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();
+        return;
+    }
+
+    last_output_time_ = now;
+    auto opus = std::move(audio_decode_queue_.front());
+    audio_decode_queue_.pop_front();
+    lock.unlock();
+
+    background_task_->Schedule([this, codec, opus = std::move(opus)]() mutable {
+        if (aborted_) {
+            return;
+        }
+
+        std::vector<int16_t> pcm;
+        if (!opus_decoder_->Decode(std::move(opus), pcm)) {
+            return;
+        }
+
+        // Resample if the sample rate is different
+        if (opus_decode_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);
+    });
+}
+
+void Application::InputAudio() {
+    auto codec = Board::GetInstance().GetAudioCodec();
+    std::vector<int16_t> data;
+    if (!codec->InputData(data)) {
+        return;
+    }
+
+    if (codec->input_sample_rate() != 16000) {
+        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);
+        }
+    }
+
+#if CONFIG_USE_WAKE_WORD_DETECT
+    if (wake_word_detect_.IsDetectionRunning()) {
+        wake_word_detect_.Feed(data);
+    }
+#endif
+#if CONFIG_USE_AUDIO_PROCESSOR
+    if (audio_processor_.IsRunning()) {
+        audio_processor_.Input(data);
+    }
+#else
+    if (device_state_ == kDeviceStateListening) {
+        background_task_->Schedule([this, data = std::move(data)]() mutable {
+            opus_encoder_->Encode(std::move(data), [this](std::vector<uint8_t>&& opus) {
+                Schedule([this, opus = std::move(opus)]() {
+                    protocol_->SendAudio(opus);
+                });
+            });
+        });
+    }
+#endif
+}
+
+void Application::AbortSpeaking(AbortReason reason) {
+    ESP_LOGI(TAG, "Abort speaking");
+    aborted_ = true;
+    protocol_->SendAbortSpeaking(reason);
+}
+
+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 codec = board.GetAudioCodec();
+    auto display = board.GetDisplay();
+    auto led = board.GetLed();
+    led->OnStateChanged();
+    switch (state) {
+        case kDeviceStateUnknown:
+        case kDeviceStateIdle:
+            display->SetStatus(Lang::Strings::STANDBY);
+            display->SetEmotion("neutral");
+#if CONFIG_USE_AUDIO_PROCESSOR
+            audio_processor_.Stop();
+#endif
+#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", "");
+            break;
+        case kDeviceStateListening:
+            display->SetStatus(Lang::Strings::LISTENING);
+            display->SetEmotion("neutral");
+            ResetDecoder();
+            opus_encoder_->ResetState();
+#if CONFIG_USE_AUDIO_PROCESSOR
+            audio_processor_.Start();
+#endif
+#if CONFIG_USE_WAKE_WORD_DETECT
+            wake_word_detect_.StopDetection();
+#endif
+            UpdateIotStates();
+            if (previous_state == kDeviceStateSpeaking) {
+                // FIXME: Wait for the speaker to empty the buffer
+                vTaskDelay(pdMS_TO_TICKS(120));
+            }
+            break;
+        case kDeviceStateSpeaking:
+            display->SetStatus(Lang::Strings::SPEAKING);
+            ResetDecoder();
+            codec->EnableOutput(true);
+#if CONFIG_USE_AUDIO_PROCESSOR
+            audio_processor_.Stop();
+#endif
+#if CONFIG_USE_WAKE_WORD_DETECT
+            wake_word_detect_.StartDetection();
+#endif
+            break;
+        default:
+            // Do nothing
+            break;
+    }
+}
+
+void Application::SetDecodeSampleRate(int sample_rate) {
+    if (opus_decode_sample_rate_ == sample_rate) {
+        return;
+    }
+
+    opus_decode_sample_rate_ = sample_rate;
+    opus_decoder_.reset();
+    opus_decoder_ = std::make_unique<OpusDecoderWrapper>(opus_decode_sample_rate_, 1);
+
+    auto codec = Board::GetInstance().GetAudioCodec();
+    if (opus_decode_sample_rate_ != codec->output_sample_rate()) {
+        ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decode_sample_rate_, codec->output_sample_rate());
+        output_resampler_.Configure(opus_decode_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;
+}

+ 122 - 0
main/application.h

@@ -0,0 +1,122 @@
+#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 <opus_encoder.h>
+#include <opus_decoder.h>
+#include <opus_resampler.h>
+
+#include "protocol.h"
+#include "ota.h"
+#include "background_task.h"
+
+#if CONFIG_USE_WAKE_WORD_DETECT
+#include "wake_word_detect.h"
+#endif
+#if CONFIG_USE_AUDIO_PROCESSOR
+#include "audio_processor.h"
+#endif
+
+#define SCHEDULE_EVENT (1 << 0)
+#define AUDIO_INPUT_READY_EVENT (1 << 1)
+#define AUDIO_OUTPUT_READY_EVENT (1 << 2)
+
+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
+#if CONFIG_USE_AUDIO_PROCESSOR
+    AudioProcessor audio_processor_;
+#endif
+    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;
+    bool keep_listening_ = false;
+    bool aborted_ = false;
+    bool voice_detected_ = false;
+    int clock_ticks_ = 0;
+
+    // Audio encode / decode
+    BackgroundTask* background_task_ = nullptr;
+    std::chrono::steady_clock::time_point last_output_time_;
+    std::list<std::vector<uint8_t>> audio_decode_queue_;
+
+    std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
+    std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
+
+    int opus_decode_sample_rate_ = -1;
+    OpusResampler input_resampler_;
+    OpusResampler reference_resampler_;
+    OpusResampler output_resampler_;
+
+    void MainLoop();
+    void InputAudio();
+    void OutputAudio();
+    void ResetDecoder();
+    void SetDecodeSampleRate(int sample_rate);
+    void CheckNewVersion();
+    void ShowActivationCode();
+    void OnClockTimer();
+
+    //我添加的
+    std::string last_displayed_message_;
+};
+
+#endif // _APPLICATION_H_

BIN
main/assets/common/exclamation.p3


BIN
main/assets/common/low_battery.p3


BIN
main/assets/common/success.p3


BIN
main/assets/common/vibration.p3


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 52 - 0
main/assets/en-US/language.json

@@ -0,0 +1,52 @@
+{
+    "language": {
+        "type": "en-US"
+    },
+    "strings": {
+        "WARNING": "Warning",
+        "INFO": "Information",
+        "ERROR": "Error",
+        "VERSION": "Ver ",
+        "LOADING_PROTOCOL": "Loading Protocol...",
+        "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...",
+
+        "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"
+    }
+}

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 51 - 0
main/assets/ja-JP/language.json

@@ -0,0 +1,51 @@
+{
+    "language": {
+        "type": "ja-JP"
+    },
+    "strings": {
+        "WARNING": "警告",
+        "INFO": "情報",
+        "ERROR": "エラー",
+        "VERSION": "バージョン ",
+        "LOADING_PROTOCOL": "プロトコルを読み込み中...",
+        "INITIALIZING": "初期化中...",
+        "PIN_ERROR": "SIMカードを挿入してください",
+        "REG_ERROR": "ネットワークに接続できません。ネットワーク状態を確認してください",
+        "DETECTING_MODULE": "モジュールを検出中...",
+        "REGISTERING_NETWORK": "ネットワーク接続待機中...",
+
+        "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": "最大音量"
+    }
+}

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 51 - 0
main/assets/zh-CN/language.json

@@ -0,0 +1,51 @@
+{
+    "language": {
+        "type" :"zh-CN"
+    },
+    "strings": {
+        "WARNING":"警告",
+        "INFO":"信息",
+        "ERROR":"错误",
+        "VERSION": "版本 ",
+        "LOADING_PROTOCOL":"加载协议...",
+        "INITIALIZING":"正在初始化...",
+        "PIN_ERROR":"请插入 SIM 卡",
+        "REG_ERROR":"无法接入网络,请检查流量卡状态",
+        "DETECTING_MODULE":"检测模组...",
+        "REGISTERING_NETWORK":"等待网络...",
+
+        "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":"最大音量"
+    }
+}

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


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


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


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


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


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


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


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


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


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


Vissa filer visades inte eftersom för många filer har ändrats