commit 165219cee06ffd0ad1c0c297db41792e1d4a6830
Author: 0Xiao0 <511201264@qq.com>
Date: Mon Feb 2 14:29:15 2026 +0800
first commit
diff --git a/.clangd b/.clangd
new file mode 100644
index 0000000..437f255
--- /dev/null
+++ b/.clangd
@@ -0,0 +1,2 @@
+CompileFlags:
+ Remove: [-f*, -m*]
diff --git a/.github/ISSUE_TEMPLATE/01_build_install_bug.yml b/.github/ISSUE_TEMPLATE/01_build_install_bug.yml
new file mode 100644
index 0000000..d3489e2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/01_build_install_bug.yml
@@ -0,0 +1,103 @@
+name: Installation or build bug report
+description: Report installation or build bugs
+labels: ['bug']
+body:
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Answers checklist.
+ description: Before submitting a new issue, please follow the checklist and try to find the answer.
+ options:
+ - label: I have read the documentation [XiaoZhi AI Programming Guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb) and the issue is not addressed there.
+ required: true
+ - label: I have updated my branch (master or release) to the latest version and checked that the issue is present there.
+ required: true
+ - label: I have searched the issue tracker for a similar issue and not found a similar issue.
+ required: true
+ - type: input
+ id: xiaozhi_ai_version
+ attributes:
+ label: XiaoZhi AI version.
+ description: On which XiaoZhi AI version does this issue occur on? Run `git describe --tags` to find it.
+ placeholder: ex. v1.1.0-44-g140aab8
+ validations:
+ required: true
+ - type: dropdown
+ id: operating_system
+ attributes:
+ label: Operating System used.
+ multiple: false
+ options:
+ - Windows
+ - Linux
+ - macOS
+ validations:
+ required: true
+ - type: dropdown
+ id: build
+ attributes:
+ label: How did you build your project?
+ multiple: false
+ options:
+ - Command line with CMake
+ - Command line with idf.py
+ - CLion IDE
+ - VS Code IDE/Cursor
+ - Other (please specify in More Information)
+ validations:
+ required: true
+ - type: dropdown
+ id: windows_comand_line
+ attributes:
+ label: If you are using Windows, please specify command line type.
+ multiple: false
+ options:
+ - PowerShell
+ - CMD
+ validations:
+ required: false
+ - type: textarea
+ id: expected
+ attributes:
+ label: What is the expected behavior?
+ description: Please provide a clear and concise description of the expected behavior.
+ placeholder: I expected it to...
+ validations:
+ required: true
+ - type: textarea
+ id: actual
+ attributes:
+ label: What is the actual behavior?
+ description: Please describe actual behavior.
+ placeholder: Instead it...
+ validations:
+ required: true
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce.
+ description: 'How do you trigger this bug? Please walk us through it step by step. If this is build bug, please attach sdkconfig file (from your project folder). Please attach your code here.'
+ value: |
+ 1. Step
+ 2. Step
+ 3. Step
+ ...
+ validations:
+ required: true
+ - type: textarea
+ id: debug_logs
+ attributes:
+ label: Build or installation Logs.
+ description: Build or installation log goes here, should contain the backtrace, as well as the reset source if it is a crash.
+ placeholder: Your log goes here.
+ render: plain
+ validations:
+ required: false
+ - type: textarea
+ id: more-info
+ attributes:
+ label: More Information.
+ description: Do you have any other information from investigating this?
+ placeholder: ex. Any more.
+ validations:
+ required: false
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/02_runtime_bug.yml b/.github/ISSUE_TEMPLATE/02_runtime_bug.yml
new file mode 100644
index 0000000..60a62f8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/02_runtime_bug.yml
@@ -0,0 +1,115 @@
+name: Runtime bug report
+description: Report runtime bugs
+labels: ['bug']
+body:
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Answers checklist.
+ description: Before submitting a new issue, please follow the checklist and try to find the answer.
+ options:
+ - label: I have read the documentation [XiaoZhi AI Programming Guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb) and the issue is not addressed there.
+ required: true
+ - label: I have updated my firmware to the latest version and checked that the issue is present there.
+ required: true
+ - label: I have searched the issue tracker for a similar issue and not found a similar issue.
+ required: true
+ - type: input
+ id: xiaozhi_ai_firmware_version
+ attributes:
+ label: XiaoZhi AI firmware version.
+ description: On which firmware version does this issue occur on?
+ placeholder: ex. v1.2.1_bread-compact-wifi
+ validations:
+ required: true
+ - type: dropdown
+ id: operating_system
+ attributes:
+ label: Operating System used.
+ multiple: false
+ options:
+ - Windows
+ - Linux
+ - macOS
+ validations:
+ required: true
+ - type: dropdown
+ id: build
+ attributes:
+ label: How did you build your project?
+ multiple: false
+ options:
+ - Command line with CMake
+ - Command line with idf.py
+ - CLion IDE
+ - VS Code IDE/Cursor
+ - Other (please specify in More Information)
+ validations:
+ required: true
+ - type: dropdown
+ id: windows_comand_line
+ attributes:
+ label: If you are using Windows, please specify command line type.
+ multiple: false
+ options:
+ - PowerShell
+ - CMD
+ validations:
+ required: false
+ - type: dropdown
+ id: power_supply
+ attributes:
+ label: Power Supply used.
+ multiple: false
+ options:
+ - USB
+ - External 5V
+ - External 3.3V
+ - Battery
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: What is the expected behavior?
+ description: Please provide a clear and concise description of the expected behavior.
+ placeholder: I expected it to...
+ validations:
+ required: true
+ - type: textarea
+ id: actual
+ attributes:
+ label: What is the actual behavior?
+ description: Please describe actual behavior.
+ placeholder: Instead it...
+ validations:
+ required: true
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce.
+ description: 'How do you trigger this bug? Please walk us through it step by step. Please attach your code here.'
+ value: |
+ 1. Step
+ 2. Step
+ 3. Step
+ ...
+ validations:
+ required: true
+ - type: textarea
+ id: debug_logs
+ attributes:
+ label: Debug Logs.
+ description: Debug log goes here, should contain the backtrace, as well as the reset source if it is a crash.
+ placeholder: Your log goes here.
+ render: plain
+ validations:
+ required: false
+ - type: textarea
+ id: more-info
+ attributes:
+ label: More Information.
+ description: Do you have any other information from investigating this?
+ placeholder: ex. Any more.
+ validations:
+ required: false
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/03_feature_request.yml b/.github/ISSUE_TEMPLATE/03_feature_request.yml
new file mode 100644
index 0000000..79cb921
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/03_feature_request.yml
@@ -0,0 +1,34 @@
+name: Feature request
+description: Suggest an idea for this project.
+labels: ['enhancement']
+body:
+ - type: markdown
+ attributes:
+ value: |
+ * We welcome any ideas or feature requests! It’s helpful if you can explain exactly why the feature would be useful.
+ * There are usually some outstanding feature requests in the [existing issues list](https://github.com/78/xiaozhi-esp32/labels/enhancement), feel free to add comments to them.
+ * If you would like to contribute, please read the [contributions guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb).
+ - type: textarea
+ id: problem-related
+ attributes:
+ label: Is your feature request related to a problem?
+ description: Please provide a clear and concise description of what the problem is.
+ placeholder: ex. I'm always frustrated when ...
+ - type: textarea
+ id: solution
+ attributes:
+ label: Describe the solution you'd like.
+ description: Please provide a clear and concise description of what you want to happen.
+ placeholder: ex. When using XiaoZhi ...
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Describe alternatives you've considered.
+ description: Please provide a clear and concise description of any alternative solutions or features you've considered.
+ placeholder: ex. Choosing other approach wouldn't work, because ...
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional context.
+ description: Please add any other context or screenshots about the feature request here.
+ placeholder: ex. This would work only when ...
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..d663ce7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: true
+contact_links:
+ - name: 小智 AI 官方网站
+ url: https://xiaozhi.me/
+ about: 激活设备、配置 AI、声纹识别、声音克隆等应有尽有,DIY 属于你自己的小智
+ - name: 小智 AI 聊天机器人百科全书
+ url: https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb
+ about: 开发文档、硬件制作、烧录教程、FAQ尽在小智百科
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..f7f365e
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,106 @@
+name: Build Boards
+
+on:
+ push:
+ branches:
+ - main
+ - ci/* # for ci test
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ prepare:
+ name: Determine boards to build
+ runs-on: ubuntu-latest
+ outputs:
+ boards: ${{ steps.select.outputs.boards }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install jq
+ run: sudo apt-get update && sudo apt-get install -y jq
+
+ - id: list
+ name: Get all board list
+ run: |
+ echo "all_boards=$(python scripts/release.py --list-boards --json)" >> $GITHUB_OUTPUT
+
+ - id: select
+ name: Select boards based on changes
+ env:
+ ALL_BOARDS: ${{ steps.list.outputs.all_boards }}
+ run: |
+ EVENT_NAME="${{ github.event_name }}"
+
+ # For push to main branch, build all boards
+ if [[ "$EVENT_NAME" == "push" ]]; then
+ echo "boards=$ALL_BOARDS" >> $GITHUB_OUTPUT
+ exit 0
+ fi
+
+ # For pull_request
+ BASE_SHA="${{ github.event.pull_request.base.sha }}"
+ HEAD_SHA="${{ github.event.pull_request.head.sha }}"
+ echo "Base: $BASE_SHA, Head: $HEAD_SHA"
+
+ CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA || true)
+ echo "Changed files:\n$CHANGED"
+
+ NEED_ALL=0
+ declare -A AFFECTED
+ while IFS= read -r file; do
+ if [[ "$file" == main/* && "$file" != main/boards/* ]]; then
+ NEED_ALL=1
+ fi
+
+ if [[ "$file" == main/boards/* ]]; then
+ board=$(echo "$file" | cut -d '/' -f3)
+ AFFECTED[$board]=1
+ fi
+ done <<< "$CHANGED"
+
+ if [[ "$NEED_ALL" -eq 1 ]]; then
+ echo "boards=$ALL_BOARDS" >> $GITHUB_OUTPUT
+ else
+ if [[ ${#AFFECTED[@]} -eq 0 ]]; then
+ echo "boards=[]" >> $GITHUB_OUTPUT
+ else
+ JSON=$(printf '%s\n' "${!AFFECTED[@]}" | sort -u | jq -R -s -c 'split("\n")[:-1]')
+ echo "boards=$JSON" >> $GITHUB_OUTPUT
+ fi
+ fi
+
+ build:
+ name: Build ${{ matrix.board }}
+ needs: prepare
+ if: ${{ needs.prepare.outputs.boards != '[]' }}
+ strategy:
+ fail-fast: false # 单个 board 失败不影响其它 board
+ matrix:
+ board: ${{ fromJson(needs.prepare.outputs.boards) }}
+ runs-on: ubuntu-latest
+ container:
+ image: espressif/idf:release-v5.4
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Build current board
+ shell: bash
+ run: |
+ source $IDF_PATH/export.sh
+ python scripts/release.py ${{ matrix.board }}
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: xiaozhi_${{ matrix.board }}_${{ github.sha }}.bin
+ path: build/merged-binary.bin
+ if-no-files-found: error
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..62655f1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+tmp/
+components/
+managed_components/
+build/
+.vscode/
+.devcontainer/
+sdkconfig.old
+sdkconfig
+dependencies.lock
+.env
+releases/
+main/assets/lang_config.h
+main/mmap_generate_emoji.h
+.DS_Store
+.cache
+*.pyc
+*.bin
+mmap_generate_*.h
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..2f44b3c
--- /dev/null
+++ b/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 "2.0.1")
+
+# 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)
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e90b554
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2025 Shenzhen Xinzhi Future Technology Co., Ltd.
+Copyright (c) 2025 Project Contributors
+
+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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fcb7519
--- /dev/null
+++ b/README.md
@@ -0,0 +1,161 @@
+# An MCP-based Chatbot | 一个基于 MCP 的聊天机器人
+
+(中文 | [English](README_en.md) | [日本語](README_ja.md))
+
+## 视频
+
+👉 [人类:给 AI 装摄像头 vs AI:当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
+
+👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
+
+## 介绍
+
+这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,或用于商业用途。
+
+我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
+
+如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:1011329060
+
+### 基于 MCP 控制万物
+
+小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制。
+
+
+
+### 已实现功能
+
+- Wi-Fi / ML307 Cat.1 4G
+- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
+- 支持两种通信协议([Websocket](docs/websocket.md) 或 MQTT+UDP)
+- 采用 OPUS 音频编解码
+- 基于流式 ASR + LLM + TTS 架构的语音交互
+- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker)
+- OLED / LCD 显示屏,支持表情显示
+- 电量显示与电源管理
+- 支持多语言(中文、英文、日文)
+- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台
+- 通过设备端 MCP 实现设备控制(音量、灯光、电机、GPIO 等)
+- 通过云端 MCP 扩展大模型能力(智能家居控制、PC桌面操作、知识搜索、邮件收发等)
+
+## 硬件
+
+### 面包板手工制作实践
+
+详见飞书文档教程:
+
+👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
+
+面包板效果图如下:
+
+
+
+### 支持 70 多个开源硬件(仅展示部分)
+
+- 立创·实战派 ESP32-S3 开发板
+- 乐鑫 ESP32-S3-BOX3
+- M5Stack CoreS3
+- M5Stack AtomS3R + Echo Base
+- 神奇按钮 2.4
+- 微雪电子 ESP32-S3-Touch-AMOLED-1.8
+- LILYGO T-Circle-S3
+- 虾哥 Mini C3
+- 璀璨·AI 吊坠
+- 无名科技 Nologo-星智-1.54TFT
+- SenseCAP Watcher
+- ESP-HI 超低成本机器狗
+
+
+
+## 软件
+
+### 固件烧录
+
+新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。
+
+固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,个人用户注册账号可以免费使用 Qwen 实时模型。
+
+👉 [新手烧录固件教程](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
+
+### 开发环境
+
+- Cursor 或 VSCode
+- 安装 ESP-IDF 插件,选择 SDK 版本 5.4 或以上
+- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
+- 本项目使用 Google C++ 代码风格,提交代码时请确保符合规范
+
+### 开发者文档
+
+- [自定义开发板指南](main/boards/README.md) - 学习如何为小智 AI 创建自定义开发板
+- [MCP 协议物联网控制用法说明](docs/mcp-usage.md) - 了解如何通过 MCP 协议控制物联网设备
+- [MCP 协议交互流程](docs/mcp-protocol.md) - 设备端 MCP 协议的实现方式
+- [MQTT + UDP 混合通信协议文档](docs/mqtt-udp.md)
+- [一份详细的 WebSocket 通信协议文档](docs/websocket.md)
+
+## 大模型配置
+
+如果你已经拥有一个小智 AI 聊天机器人设备,并且已接入官方服务器,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。
+
+👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
+
+## 相关开源项目
+
+在个人电脑上部署服务器,可以参考以下第三方开源的项目:
+
+- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python 服务器
+- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java 服务器
+- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang 服务器
+
+使用小智通信协议的第三方客户端项目:
+
+- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python 客户端
+- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客户端
+- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百问科技提供的 Linux 客户端
+- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技的蓝牙芯片固件
+- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移远提供的 QuecPython 固件
+
+## Star History
+
+
+
+
+
+
+
+
diff --git a/README_en.md b/README_en.md
new file mode 100644
index 0000000..f6a6a49
--- /dev/null
+++ b/README_en.md
@@ -0,0 +1,157 @@
+# An MCP-based Chatbot
+
+(English | [中文](README.md) | [日本語](README_ja.md))
+
+## Video
+
+👉 [Human: Give AI a camera vs AI: Instantly finds out the owner hasn't washed hair for three days【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
+
+👉 [Handcraft your AI girlfriend, beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
+
+## Introduction
+
+This is an open-source ESP32 project, released under the MIT license, allowing anyone to use it for free, including for commercial purposes.
+
+We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices.
+
+If you have any ideas or suggestions, please feel free to raise Issues or join the QQ group: 1011329060
+
+### Control Everything with MCP
+
+As a voice interaction entry, the XiaoZhi AI chatbot leverages the AI capabilities of large models like Qwen / DeepSeek, and achieves multi-terminal control via the MCP protocol.
+
+
+
+### Features Implemented
+
+- Wi-Fi / ML307 Cat.1 4G
+- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
+- Supports two communication protocols ([Websocket](docs/websocket.md) or MQTT+UDP)
+- Uses OPUS audio codec
+- Voice interaction based on streaming ASR + LLM + TTS architecture
+- Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker)
+- OLED / LCD display, supports emoji display
+- Battery display and power management
+- Multi-language support (Chinese, English, Japanese)
+- Supports ESP32-C3, ESP32-S3, ESP32-P4 chip platforms
+- Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.)
+- Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.)
+
+## Hardware
+
+### Breadboard DIY Practice
+
+See the Feishu document tutorial:
+
+👉 ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
+
+Breadboard demo:
+
+
+
+### Supports 70+ Open Source Hardware (Partial List)
+
+- LiChuang ESP32-S3 Development Board
+- Espressif ESP32-S3-BOX3
+- M5Stack CoreS3
+- M5Stack AtomS3R + Echo Base
+- Magic Button 2.4
+- Waveshare ESP32-S3-Touch-AMOLED-1.8
+- LILYGO T-Circle-S3
+- XiaGe Mini C3
+- CuiCan AI Pendant
+- WMnologo-Xingzhi-1.54TFT
+- SenseCAP Watcher
+- ESP-HI Low Cost Robot Dog
+
+
+
+## Software
+
+### Firmware Flashing
+
+For beginners, it is recommended to 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. Personal users can register an account to use the Qwen real-time model for free.
+
+👉 [Beginner's Firmware Flashing Guide](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
+
+### Development Environment
+
+- Cursor or VSCode
+- Install ESP-IDF plugin, select SDK version 5.4 or above
+- Linux is better than Windows for faster compilation and fewer driver issues
+- This project uses Google C++ code style, please ensure compliance when submitting code
+
+### Developer Documentation
+
+- [Custom Board Guide](main/boards/README.md) - Learn how to create custom boards for XiaoZhi AI
+- [MCP Protocol IoT Control Usage](docs/mcp-usage.md) - Learn how to control IoT devices via MCP protocol
+- [MCP Protocol Interaction Flow](docs/mcp-protocol.md) - Device-side MCP protocol implementation
+- [A detailed WebSocket communication protocol document](docs/websocket.md)
+
+## Large Model Configuration
+
+If you already have a XiaoZhi AI chatbot device and have connected to the official server, you can log in to the [xiaozhi.me](https://xiaozhi.me) console for configuration.
+
+👉 [Backend Operation Video Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
+
+## Related Open Source Projects
+
+For server deployment on personal computers, refer to the following open-source projects:
+
+- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python server
+- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java server
+- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang server
+
+Other client projects using the XiaoZhi communication protocol:
+
+- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python client
+- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android client
+
+## Star History
+
+
+
+
+
+
+
+
diff --git a/README_ja.md b/README_ja.md
new file mode 100644
index 0000000..b5a2b65
--- /dev/null
+++ b/README_ja.md
@@ -0,0 +1,157 @@
+# MCP ベースのチャットボット
+
+(日本語 | [中文](README.md) | [English](README_en.md))
+
+## 動画
+
+👉 [人間:AIにカメラを装着 vs AI:その場で飼い主が3日間髪を洗っていないことを発見【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
+
+👉 [手作りでAIガールフレンドを作る、初心者入門チュートリアル【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
+
+## イントロダクション
+
+これはエビ兄さんがオープンソースで公開しているESP32プロジェクトで、MITライセンスのもと、誰でも無料で、商用利用も可能です。
+
+このプロジェクトを通じて、AIハードウェア開発を理解し、急速に進化する大規模言語モデルを実際のハードウェアデバイスに応用できるようになることを目指しています。
+
+ご意見やご提案があれば、いつでもIssueを提出するか、QQグループ:1011329060 にご参加ください。
+
+### MCPであらゆるものを制御
+
+シャオジーAIチャットボットは音声インタラクションの入口として、Qwen / DeepSeekなどの大規模モデルのAI能力を活用し、MCPプロトコルを通じてマルチエンド制御を実現します。
+
+
+
+### 実装済み機能
+
+- Wi-Fi / ML307 Cat.1 4G
+- オフライン音声ウェイクアップ [ESP-SR](https://github.com/espressif/esp-sr)
+- 2種類の通信プロトコルに対応([Websocket](docs/websocket.md) または MQTT+UDP)
+- OPUSオーディオコーデックを採用
+- ストリーミングASR + LLM + TTSアーキテクチャに基づく音声インタラクション
+- 話者認識、現在話している人を識別 [3D Speaker](https://github.com/modelscope/3D-Speaker)
+- OLED / LCDディスプレイ、表情表示対応
+- バッテリー表示と電源管理
+- 多言語対応(中国語、英語、日本語)
+- ESP32-C3、ESP32-S3、ESP32-P4チッププラットフォーム対応
+- デバイス側MCPによるデバイス制御(音量・明るさ調整、アクション制御など)
+- クラウド側MCPで大規模モデル能力を拡張(スマートホーム制御、PCデスクトップ操作、知識検索、メール送受信など)
+
+## ハードウェア
+
+### ブレッドボード手作り実践
+
+Feishuドキュメントチュートリアルをご覧ください:
+
+👉 [「シャオジーAIチャットボット百科事典」](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
+
+ブレッドボードのデモ:
+
+
+
+### 70種類以上のオープンソースハードウェアに対応(一部のみ表示)
+
+- 立創・実戦派 ESP32-S3 開発ボード
+- 楽鑫 ESP32-S3-BOX3
+- M5Stack CoreS3
+- M5Stack AtomS3R + Echo Base
+- マジックボタン2.4
+- 微雪電子 ESP32-S3-Touch-AMOLED-1.8
+- LILYGO T-Circle-S3
+- エビ兄さん Mini C3
+- CuiCan AIペンダント
+- 無名科技Nologo-星智-1.54TFT
+- SenseCAP Watcher
+- ESP-HI 超低コストロボット犬
+
+
+
+## ソフトウェア
+
+### ファームウェア書き込み
+
+初心者の方は、まず開発環境を構築せずに書き込み可能なファームウェアを使用することをおすすめします。
+
+ファームウェアはデフォルトで公式 [xiaozhi.me](https://xiaozhi.me) サーバーに接続します。個人ユーザーはアカウント登録でQwenリアルタイムモデルを無料で利用できます。
+
+👉 [初心者向けファームウェア書き込みガイド](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
+
+### 開発環境
+
+- Cursor または VSCode
+- ESP-IDFプラグインをインストールし、SDKバージョン5.4以上を選択
+- LinuxはWindowsよりも優れており、コンパイルが速く、ドライバの問題も少ない
+- 本プロジェクトはGoogle C++コードスタイルを採用、コード提出時は準拠を確認してください
+
+### 開発者ドキュメント
+
+- [カスタム開発ボードガイド](main/boards/README.md) - シャオジーAI用のカスタム開発ボード作成方法
+- [MCPプロトコルIoT制御使用法](docs/mcp-usage.md) - MCPプロトコルでIoTデバイスを制御する方法
+- [MCPプロトコルインタラクションフロー](docs/mcp-protocol.md) - デバイス側MCPプロトコルの実装方法
+- [詳細なWebSocket通信プロトコルドキュメント](docs/websocket.md)
+
+## 大規模モデル設定
+
+すでにシャオジーAIチャットボットデバイスをお持ちで、公式サーバーに接続済みの場合は、[xiaozhi.me](https://xiaozhi.me) コンソールで設定できます。
+
+👉 [バックエンド操作ビデオチュートリアル(旧インターフェース)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
+
+## 関連オープンソースプロジェクト
+
+個人PCでサーバーをデプロイする場合は、以下のオープンソースプロジェクトを参照してください:
+
+- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Pythonサーバー
+- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Javaサーバー
+- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golangサーバー
+
+シャオジー通信プロトコルを利用した他のクライアントプロジェクト:
+
+- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Pythonクライアント
+- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Androidクライアント
+
+## スター履歴
+
+
+
+
+
+
+
+
diff --git a/docs/mcp-based-graph.jpg b/docs/mcp-based-graph.jpg
new file mode 100644
index 0000000..af81cd2
Binary files /dev/null and b/docs/mcp-based-graph.jpg differ
diff --git a/docs/mcp-protocol.md b/docs/mcp-protocol.md
new file mode 100644
index 0000000..0c8ec90
--- /dev/null
+++ b/docs/mcp-protocol.md
@@ -0,0 +1,269 @@
+# MCP (Model Context Protocol) 交互流程
+
+NOTICE: AI 辅助生成, 在实现后台服务时, 请参照代码确认细节!!
+
+本项目中的 MCP 协议用于后台 API(MCP 客户端)与 ESP32 设备(MCP 服务器)之间的通信,以便后台能够发现和调用设备提供的功能(工具)。
+
+## 协议格式
+
+根据代码 (`main/protocols/protocol.cc`, `main/mcp_server.cc`),MCP 消息是封装在基础通信协议(如 WebSocket 或 MQTT)的消息体中的。其内部结构遵循 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) 规范。
+
+整体消息结构示例:
+
+```json
+{
+ "session_id": "...", // 会话 ID
+ "type": "mcp", // 消息类型,固定为 "mcp"
+ "payload": { // JSON-RPC 2.0 负载
+ "jsonrpc": "2.0",
+ "method": "...", // 方法名 (如 "initialize", "tools/list", "tools/call")
+ "params": { ... }, // 方法参数 (对于 request)
+ "id": ..., // 请求 ID (对于 request 和 response)
+ "result": { ... }, // 方法执行结果 (对于 success response)
+ "error": { ... } // 错误信息 (对于 error response)
+ }
+}
+```
+
+其中,`payload` 部分是标准的 JSON-RPC 2.0 消息:
+
+- `jsonrpc`: 固定的字符串 "2.0"。
+- `method`: 要调用的方法名称 (对于 Request)。
+- `params`: 方法的参数,一个结构化值,通常为对象 (对于 Request)。
+- `id`: 请求的标识符,客户端发送请求时提供,服务器响应时原样返回。用于匹配请求和响应。
+- `result`: 方法成功执行时的结果 (对于 Success Response)。
+- `error`: 方法执行失败时的错误信息 (对于 Error Response)。
+
+## 交互流程及发送时机
+
+MCP 的交互主要围绕客户端(后台 API)发现和调用设备上的“工具”(Tool)进行。
+
+1. **连接建立与能力通告**
+
+ - **时机:** 设备启动并成功连接到后台 API 后。
+ - **发送方:** 设备。
+ - **消息:** 设备发送基础协议的 "hello" 消息给后台 API,消息中包含设备支持的能力列表,例如通过支持 MCP 协议 (`"mcp": true`)。
+ - **示例 (非 MCP 负载,而是基础协议消息):**
+ ```json
+ {
+ "type": "hello",
+ "version": ...,
+ "features": {
+ "mcp": true,
+ ...
+ },
+ "transport": "websocket", // 或 "mqtt"
+ "audio_params": { ... },
+ "session_id": "..." // 设备收到服务器hello后可能设置
+ }
+ ```
+
+2. **初始化 MCP 会话**
+
+ - **时机:** 后台 API 收到设备 "hello" 消息,确认设备支持 MCP 后,通常作为 MCP 会话的第一个请求发送。
+ - **发送方:** 后台 API (客户端)。
+ - **方法:** `initialize`
+ - **消息 (MCP payload):**
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "method": "initialize",
+ "params": {
+ "capabilities": {
+ // 客户端能力,可选
+
+ // 摄像头视觉相关
+ "vision": {
+ "url": "...", //摄像头: 图片处理地址(必须是http地址, 不是websocket地址)
+ "token": "..." // url token
+ }
+
+ // ... 其他客户端能力
+ }
+ },
+ "id": 1 // 请求 ID
+ }
+ ```
+
+ - **设备响应时机:** 设备收到 `initialize` 请求并处理后。
+ - **设备响应消息 (MCP payload):**
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": 1, // 匹配请求 ID
+ "result": {
+ "protocolVersion": "2024-11-05",
+ "capabilities": {
+ "tools": {} // 这里的 tools 似乎不列出详细信息,需要 tools/list
+ },
+ "serverInfo": {
+ "name": "...", // 设备名称 (BOARD_NAME)
+ "version": "..." // 设备固件版本
+ }
+ }
+ }
+ ```
+
+3. **发现设备工具列表**
+
+ - **时机:** 后台 API 需要获取设备当前支持的具体功能(工具)列表及其调用方式时。
+ - **发送方:** 后台 API (客户端)。
+ - **方法:** `tools/list`
+ - **消息 (MCP payload):**
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "method": "tools/list",
+ "params": {
+ "cursor": "" // 用于分页,首次请求为空字符串
+ },
+ "id": 2 // 请求 ID
+ }
+ ```
+ - **设备响应时机:** 设备收到 `tools/list` 请求并生成工具列表后。
+ - **设备响应消息 (MCP payload):**
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": 2, // 匹配请求 ID
+ "result": {
+ "tools": [ // 工具对象列表
+ {
+ "name": "self.get_device_status",
+ "description": "...",
+ "inputSchema": { ... } // 参数 schema
+ },
+ {
+ "name": "self.audio_speaker.set_volume",
+ "description": "...",
+ "inputSchema": { ... } // 参数 schema
+ }
+ // ... 更多工具
+ ],
+ "nextCursor": "..." // 如果列表很大需要分页,这里会包含下一个请求的 cursor 值
+ }
+ }
+ ```
+ - **分页处理:** 如果 `nextCursor` 字段非空,客户端需要再次发送 `tools/list` 请求,并在 `params` 中带上这个 `cursor` 值以获取下一页工具。
+
+4. **调用设备工具**
+
+ - **时机:** 后台 API 需要执行设备上的某个具体功能时。
+ - **发送方:** 后台 API (客户端)。
+ - **方法:** `tools/call`
+ - **消息 (MCP payload):**
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "self.audio_speaker.set_volume", // 要调用的工具名称
+ "arguments": {
+ // 工具参数,对象格式
+ "volume": 50 // 参数名及其值
+ }
+ },
+ "id": 3 // 请求 ID
+ }
+ ```
+ - **设备响应时机:** 设备收到 `tools/call` 请求,执行相应的工具函数后。
+ - **设备成功响应消息 (MCP payload):**
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": 3, // 匹配请求 ID
+ "result": {
+ "content": [
+ // 工具执行结果内容
+ { "type": "text", "text": "true" } // 示例:set_volume 返回 bool
+ ],
+ "isError": false // 表示成功
+ }
+ }
+ ```
+ - **设备失败响应消息 (MCP payload):**
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": 3, // 匹配请求 ID
+ "error": {
+ "code": -32601, // JSON-RPC 错误码,例如 Method not found (-32601)
+ "message": "Unknown tool: self.non_existent_tool" // 错误描述
+ }
+ }
+ ```
+
+5. **设备主动发送消息 (Notifications)**
+ - **时机:** 设备内部发生需要通知后台 API 的事件时(例如,状态变化,虽然代码示例中没有明确的工具发送此类消息,但 `Application::SendMcpMessage` 的存在暗示了设备可能主动发送 MCP 消息)。
+ - **发送方:** 设备 (服务器)。
+ - **方法:** 可能是以 `notifications/` 开头的方法名,或者其他自定义方法。
+ - **消息 (MCP payload):** 遵循 JSON-RPC Notification 格式,没有 `id` 字段。
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "method": "notifications/state_changed", // 示例方法名
+ "params": {
+ "newState": "idle",
+ "oldState": "connecting"
+ }
+ // 没有 id 字段
+ }
+ ```
+ - **后台 API 处理:** 接收到 Notification 后,后台 API 进行相应的处理,但不回复。
+
+## 交互图
+
+下面是一个简化的交互序列图,展示了主要的 MCP 消息流程:
+
+```mermaid
+sequenceDiagram
+ participant Device as ESP32 Device
+ participant BackendAPI as 后台 API (Client)
+
+ Note over Device, BackendAPI: 建立 WebSocket / MQTT 连接
+
+ Device->>BackendAPI: Hello Message (包含 "mcp": true)
+
+ BackendAPI->>Device: MCP Initialize Request
+ Note over BackendAPI: method: initialize
+ Note over BackendAPI: params: { capabilities: ... }
+
+ Device->>BackendAPI: MCP Initialize Response
+ Note over Device: result: { protocolVersion: ..., serverInfo: ... }
+
+ BackendAPI->>Device: MCP Get Tools List Request
+ Note over BackendAPI: method: tools/list
+ Note over BackendAPI: params: { cursor: "" }
+
+ Device->>BackendAPI: MCP Get Tools List Response
+ Note over Device: result: { tools: [...], nextCursor: ... }
+
+ loop Optional Pagination
+ BackendAPI->>Device: MCP Get Tools List Request
+ Note over BackendAPI: method: tools/list
+ Note over BackendAPI: params: { cursor: "..." }
+ Device->>BackendAPI: MCP Get Tools List Response
+ Note over Device: result: { tools: [...], nextCursor: "" }
+ end
+
+ BackendAPI->>Device: MCP Call Tool Request
+ Note over BackendAPI: method: tools/call
+ Note over BackendAPI: params: { name: "...", arguments: { ... } }
+
+ alt Tool Call Successful
+ Device->>BackendAPI: MCP Tool Call Success Response
+ Note over Device: result: { content: [...], isError: false }
+ else Tool Call Failed
+ Device->>BackendAPI: MCP Tool Call Error Response
+ Note over Device: error: { code: ..., message: ... }
+ end
+
+ opt Device Notification
+ Device->>BackendAPI: MCP Notification
+ Note over Device: method: notifications/...
+ Note over Device: params: { ... }
+ end
+```
+
+这份文档概述了该项目中 MCP 协议的主要交互流程。具体的参数细节和工具功能需要参考 `main/mcp_server.cc` 中 `McpServer::AddCommonTools` 以及各个工具的实现。
diff --git a/docs/mcp-usage.md b/docs/mcp-usage.md
new file mode 100644
index 0000000..fa50a39
--- /dev/null
+++ b/docs/mcp-usage.md
@@ -0,0 +1,115 @@
+# MCP 协议物联网控制用法说明
+
+> 本文档介绍如何基于 MCP 协议实现 ESP32 设备的物联网控制。详细协议流程请参考 [`mcp-protocol.md`](./mcp-protocol.md)。
+
+## 简介
+
+MCP(Model Context Protocol)是新一代推荐用于物联网控制的协议,通过标准 JSON-RPC 2.0 格式在后台与设备间发现和调用"工具"(Tool),实现灵活的设备控制。
+
+## 典型使用流程
+
+1. 设备启动后通过基础协议(如 WebSocket/MQTT)与后台建立连接。
+2. 后台通过 MCP 协议的 `initialize` 方法初始化会话。
+3. 后台通过 `tools/list` 获取设备支持的所有工具(功能)及参数说明。
+4. 后台通过 `tools/call` 调用具体工具,实现对设备的控制。
+
+详细协议格式与交互请见 [`mcp-protocol.md`](./mcp-protocol.md)。
+
+## 设备端工具注册方法说明
+
+设备通过 `McpServer::AddTool` 方法注册可被后台调用的"工具"。其常用函数签名如下:
+
+```cpp
+void AddTool(
+ const std::string& name, // 工具名称,建议唯一且有层次感,如 self.dog.forward
+ const std::string& description, // 工具描述,简明说明功能,便于大模型理解
+ const PropertyList& properties, // 输入参数列表(可为空),支持类型:布尔、整数、字符串
+ std::function callback // 工具被调用时的回调实现
+);
+```
+- name:工具唯一标识,建议用"模块.功能"命名风格。
+- description:自然语言描述,便于 AI/用户理解。
+- properties:参数列表,支持类型有布尔、整数、字符串,可指定范围和默认值。
+- callback:收到调用请求时的实际执行逻辑,返回值可为 bool/int/string。
+
+## 典型注册示例(以 ESP-Hi 为例)
+
+```cpp
+void InitializeTools() {
+ auto& mcp_server = McpServer::GetInstance();
+ // 例1:无参数,控制机器人前进
+ mcp_server.AddTool("self.dog.forward", "机器人向前移动", PropertyList(), [this](const PropertyList&) -> ReturnValue {
+ servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
+ return true;
+ });
+ // 例2:带参数,设置灯光 RGB 颜色
+ mcp_server.AddTool("self.light.set_rgb", "设置RGB颜色", PropertyList({
+ Property("r", kPropertyTypeInteger, 0, 255),
+ Property("g", kPropertyTypeInteger, 0, 255),
+ Property("b", kPropertyTypeInteger, 0, 255)
+ }), [this](const PropertyList& properties) -> ReturnValue {
+ int r = properties["r"].value();
+ int g = properties["g"].value();
+ int b = properties["b"].value();
+ led_on_ = true;
+ SetLedColor(r, g, b);
+ return true;
+ });
+}
+```
+
+## 常见工具调用 JSON-RPC 示例
+
+### 1. 获取工具列表
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "tools/list",
+ "params": { "cursor": "" },
+ "id": 1
+}
+```
+
+### 2. 控制底盘前进
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "self.chassis.go_forward",
+ "arguments": {}
+ },
+ "id": 2
+}
+```
+
+### 3. 切换灯光模式
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "self.chassis.switch_light_mode",
+ "arguments": { "light_mode": 3 }
+ },
+ "id": 3
+}
+```
+
+### 4. 摄像头翻转
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "self.camera.set_camera_flipped",
+ "arguments": {}
+ },
+ "id": 4
+}
+```
+
+## 备注
+- 工具名称、参数及返回值请以设备端 `AddTool` 注册为准。
+- 推荐所有新项目统一采用 MCP 协议进行物联网控制。
+- 详细协议与进阶用法请查阅 [`mcp-protocol.md`](./mcp-protocol.md)。
\ No newline at end of file
diff --git a/docs/mqtt-udp.md b/docs/mqtt-udp.md
new file mode 100644
index 0000000..478e466
--- /dev/null
+++ b/docs/mqtt-udp.md
@@ -0,0 +1,393 @@
+# MQTT + UDP 混合通信协议文档
+
+基于代码实现整理的 MQTT + UDP 混合通信协议文档,概述设备端与服务器之间如何通过 MQTT 进行控制消息传输,通过 UDP 进行音频数据传输的交互方式。
+
+---
+
+## 1. 协议概览
+
+本协议采用混合传输方式:
+- **MQTT**:用于控制消息、状态同步、JSON 数据交换
+- **UDP**:用于实时音频数据传输,支持加密
+
+### 1.1 协议特点
+
+- **双通道设计**:控制与数据分离,确保实时性
+- **加密传输**:UDP 音频数据使用 AES-CTR 加密
+- **序列号保护**:防止数据包重放和乱序
+- **自动重连**:MQTT 连接断开时自动重连
+
+---
+
+## 2. 总体流程概览
+
+```mermaid
+sequenceDiagram
+ participant Device as ESP32 设备
+ participant MQTT as MQTT 服务器
+ participant UDP as UDP 服务器
+
+ Note over Device, UDP: 1. 建立 MQTT 连接
+ Device->>MQTT: MQTT Connect
+ MQTT->>Device: Connected
+
+ Note over Device, UDP: 2. 请求音频通道
+ Device->>MQTT: Hello Message (type: "hello", transport: "udp")
+ MQTT->>Device: Hello Response (UDP 连接信息 + 加密密钥)
+
+ Note over Device, UDP: 3. 建立 UDP 连接
+ Device->>UDP: UDP Connect
+ UDP->>Device: Connected
+
+ Note over Device, UDP: 4. 音频数据传输
+ loop 音频流传输
+ Device->>UDP: 加密音频数据 (Opus)
+ UDP->>Device: 加密音频数据 (Opus)
+ end
+
+ Note over Device, UDP: 5. 控制消息交换
+ par 控制消息
+ Device->>MQTT: Listen/TTS/MCP 消息
+ MQTT->>Device: STT/TTS/MCP 响应
+ end
+
+ Note over Device, UDP: 6. 关闭连接
+ Device->>MQTT: Goodbye Message
+ Device->>UDP: Disconnect
+```
+
+---
+
+## 3. MQTT 控制通道
+
+### 3.1 连接建立
+
+设备通过 MQTT 连接到服务器,连接参数包括:
+- **Endpoint**:MQTT 服务器地址和端口
+- **Client ID**:设备唯一标识符
+- **Username/Password**:认证凭据
+- **Keep Alive**:心跳间隔(默认240秒)
+
+### 3.2 Hello 消息交换
+
+#### 3.2.1 设备端发送 Hello
+
+```json
+{
+ "type": "hello",
+ "version": 3,
+ "transport": "udp",
+ "features": {
+ "mcp": true
+ },
+ "audio_params": {
+ "format": "opus",
+ "sample_rate": 16000,
+ "channels": 1,
+ "frame_duration": 60
+ }
+}
+```
+
+#### 3.2.2 服务器响应 Hello
+
+```json
+{
+ "type": "hello",
+ "transport": "udp",
+ "session_id": "xxx",
+ "audio_params": {
+ "format": "opus",
+ "sample_rate": 24000,
+ "channels": 1,
+ "frame_duration": 60
+ },
+ "udp": {
+ "server": "192.168.1.100",
+ "port": 8888,
+ "key": "0123456789ABCDEF0123456789ABCDEF",
+ "nonce": "0123456789ABCDEF0123456789ABCDEF"
+ }
+}
+```
+
+**字段说明:**
+- `udp.server`:UDP 服务器地址
+- `udp.port`:UDP 服务器端口
+- `udp.key`:AES 加密密钥(十六进制字符串)
+- `udp.nonce`:AES 加密随机数(十六进制字符串)
+
+### 3.3 JSON 消息类型
+
+#### 3.3.1 设备端→服务器
+
+1. **Listen 消息**
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "listen",
+ "state": "start",
+ "mode": "manual"
+ }
+ ```
+
+2. **Abort 消息**
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "abort",
+ "reason": "wake_word_detected"
+ }
+ ```
+
+3. **MCP 消息**
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "mcp",
+ "payload": {
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {...}
+ }
+ }
+ ```
+
+4. **Goodbye 消息**
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "goodbye"
+ }
+ ```
+
+#### 3.3.2 服务器→设备端
+
+支持的消息类型与 WebSocket 协议一致,包括:
+- **STT**:语音识别结果
+- **TTS**:语音合成控制
+- **LLM**:情感表达控制
+- **MCP**:物联网控制
+- **System**:系统控制
+- **Custom**:自定义消息(可选)
+
+---
+
+## 4. UDP 音频通道
+
+### 4.1 连接建立
+
+设备收到 MQTT Hello 响应后,使用其中的 UDP 连接信息建立音频通道:
+1. 解析 UDP 服务器地址和端口
+2. 解析加密密钥和随机数
+3. 初始化 AES-CTR 加密上下文
+4. 建立 UDP 连接
+
+### 4.2 音频数据格式
+
+#### 4.2.1 加密音频包结构
+
+```
+|type 1byte|flags 1byte|payload_len 2bytes|ssrc 4bytes|timestamp 4bytes|sequence 4bytes|
+|payload payload_len bytes|
+```
+
+**字段说明:**
+- `type`:数据包类型,固定为 0x01
+- `flags`:标志位,当前未使用
+- `payload_len`:负载长度(网络字节序)
+- `ssrc`:同步源标识符
+- `timestamp`:时间戳(网络字节序)
+- `sequence`:序列号(网络字节序)
+- `payload`:加密的 Opus 音频数据
+
+#### 4.2.2 加密算法
+
+使用 **AES-CTR** 模式加密:
+- **密钥**:128位,由服务器提供
+- **随机数**:128位,由服务器提供
+- **计数器**:包含时间戳和序列号信息
+
+### 4.3 序列号管理
+
+- **发送端**:`local_sequence_` 单调递增
+- **接收端**:`remote_sequence_` 验证连续性
+- **防重放**:拒绝序列号小于期望值的数据包
+- **容错处理**:允许轻微的序列号跳跃,记录警告
+
+### 4.4 错误处理
+
+1. **解密失败**:记录错误,丢弃数据包
+2. **序列号异常**:记录警告,但仍处理数据包
+3. **数据包格式错误**:记录错误,丢弃数据包
+
+---
+
+## 5. 状态管理
+
+### 5.1 连接状态
+
+```mermaid
+stateDiagram
+ direction TB
+ [*] --> Disconnected
+ Disconnected --> MqttConnecting: StartMqttClient()
+ MqttConnecting --> MqttConnected: MQTT Connected
+ MqttConnecting --> Disconnected: Connect Failed
+ MqttConnected --> RequestingChannel: OpenAudioChannel()
+ RequestingChannel --> ChannelOpened: Hello Exchange Success
+ RequestingChannel --> MqttConnected: Hello Timeout/Failed
+ ChannelOpened --> UdpConnected: UDP Connect Success
+ UdpConnected --> AudioStreaming: Start Audio Transfer
+ AudioStreaming --> UdpConnected: Stop Audio Transfer
+ UdpConnected --> ChannelOpened: UDP Disconnect
+ ChannelOpened --> MqttConnected: CloseAudioChannel()
+ MqttConnected --> Disconnected: MQTT Disconnect
+```
+
+### 5.2 状态检查
+
+设备通过以下条件判断音频通道是否可用:
+```cpp
+bool IsAudioChannelOpened() const {
+ return udp_ != nullptr && !error_occurred_ && !IsTimeout();
+}
+```
+
+---
+
+## 6. 配置参数
+
+### 6.1 MQTT 配置
+
+从设置中读取的配置项:
+- `endpoint`:MQTT 服务器地址
+- `client_id`:客户端标识符
+- `username`:用户名
+- `password`:密码
+- `keepalive`:心跳间隔(默认240秒)
+- `publish_topic`:发布主题
+
+### 6.2 音频参数
+
+- **格式**:Opus
+- **采样率**:16000 Hz(设备端)/ 24000 Hz(服务器端)
+- **声道数**:1(单声道)
+- **帧时长**:60ms
+
+---
+
+## 7. 错误处理与重连
+
+### 7.1 MQTT 重连机制
+
+- 连接失败时自动重试
+- 支持错误上报控制
+- 断线时触发清理流程
+
+### 7.2 UDP 连接管理
+
+- 连接失败时不自动重试
+- 依赖 MQTT 通道重新协商
+- 支持连接状态查询
+
+### 7.3 超时处理
+
+基类 `Protocol` 提供超时检测:
+- 默认超时时间:120 秒
+- 基于最后接收时间计算
+- 超时时自动标记为不可用
+
+---
+
+## 8. 安全考虑
+
+### 8.1 传输加密
+
+- **MQTT**:支持 TLS/SSL 加密(端口8883)
+- **UDP**:使用 AES-CTR 加密音频数据
+
+### 8.2 认证机制
+
+- **MQTT**:用户名/密码认证
+- **UDP**:通过 MQTT 通道分发密钥
+
+### 8.3 防重放攻击
+
+- 序列号单调递增
+- 拒绝过期数据包
+- 时间戳验证
+
+---
+
+## 9. 性能优化
+
+### 9.1 并发控制
+
+使用互斥锁保护 UDP 连接:
+```cpp
+std::lock_guard lock(channel_mutex_);
+```
+
+### 9.2 内存管理
+
+- 动态创建/销毁网络对象
+- 智能指针管理音频数据包
+- 及时释放加密上下文
+
+### 9.3 网络优化
+
+- UDP 连接复用
+- 数据包大小优化
+- 序列号连续性检查
+
+---
+
+## 10. 与 WebSocket 协议的比较
+
+| 特性 | MQTT + UDP | WebSocket |
+|------|------------|-----------|
+| 控制通道 | MQTT | WebSocket |
+| 音频通道 | UDP (加密) | WebSocket (二进制) |
+| 实时性 | 高 (UDP) | 中等 |
+| 可靠性 | 中等 | 高 |
+| 复杂度 | 高 | 低 |
+| 加密 | AES-CTR | TLS |
+| 防火墙友好度 | 低 | 高 |
+
+---
+
+## 11. 部署建议
+
+### 11.1 网络环境
+
+- 确保 UDP 端口可达
+- 配置防火墙规则
+- 考虑 NAT 穿透
+
+### 11.2 服务器配置
+
+- MQTT Broker 配置
+- UDP 服务器部署
+- 密钥管理系统
+
+### 11.3 监控指标
+
+- 连接成功率
+- 音频传输延迟
+- 数据包丢失率
+- 解密失败率
+
+---
+
+## 12. 总结
+
+MQTT + UDP 混合协议通过以下设计实现高效的音视频通信:
+
+- **分离式架构**:控制与数据通道分离,各司其职
+- **加密保护**:AES-CTR 确保音频数据安全传输
+- **序列化管理**:防止重放攻击和数据乱序
+- **自动恢复**:支持连接断开后的自动重连
+- **性能优化**:UDP 传输保证音频数据的实时性
+
+该协议适用于对实时性要求较高的语音交互场景,但需要在网络复杂度和传输性能之间做出权衡。
\ No newline at end of file
diff --git a/docs/v0/AtomMatrix-echo-base.jpg b/docs/v0/AtomMatrix-echo-base.jpg
new file mode 100644
index 0000000..979cf81
Binary files /dev/null and b/docs/v0/AtomMatrix-echo-base.jpg differ
diff --git a/docs/v0/ESP32-BreadBoard.jpg b/docs/v0/ESP32-BreadBoard.jpg
new file mode 100644
index 0000000..f7a6fd4
Binary files /dev/null and b/docs/v0/ESP32-BreadBoard.jpg differ
diff --git a/docs/v0/atoms3r-echo-base.jpg b/docs/v0/atoms3r-echo-base.jpg
new file mode 100644
index 0000000..961e72b
Binary files /dev/null and b/docs/v0/atoms3r-echo-base.jpg differ
diff --git a/docs/v0/esp32s3-box3.jpg b/docs/v0/esp32s3-box3.jpg
new file mode 100644
index 0000000..53c4b55
Binary files /dev/null and b/docs/v0/esp32s3-box3.jpg differ
diff --git a/docs/v0/lichuang-s3.jpg b/docs/v0/lichuang-s3.jpg
new file mode 100644
index 0000000..721e0a0
Binary files /dev/null and b/docs/v0/lichuang-s3.jpg differ
diff --git a/docs/v0/m5stack-cores3.jpg b/docs/v0/m5stack-cores3.jpg
new file mode 100644
index 0000000..b123f73
Binary files /dev/null and b/docs/v0/m5stack-cores3.jpg differ
diff --git a/docs/v0/magiclick-2p4.jpg b/docs/v0/magiclick-2p4.jpg
new file mode 100644
index 0000000..beffb3d
Binary files /dev/null and b/docs/v0/magiclick-2p4.jpg differ
diff --git a/docs/v0/waveshare-esp32-s3-touch-amoled-1.8.jpg b/docs/v0/waveshare-esp32-s3-touch-amoled-1.8.jpg
new file mode 100644
index 0000000..90f2744
Binary files /dev/null and b/docs/v0/waveshare-esp32-s3-touch-amoled-1.8.jpg differ
diff --git a/docs/v0/wiring.jpg b/docs/v0/wiring.jpg
new file mode 100644
index 0000000..764c170
Binary files /dev/null and b/docs/v0/wiring.jpg differ
diff --git a/docs/v1/atoms3r.jpg b/docs/v1/atoms3r.jpg
new file mode 100644
index 0000000..45cbb45
Binary files /dev/null and b/docs/v1/atoms3r.jpg differ
diff --git a/docs/v1/electron-bot.png b/docs/v1/electron-bot.png
new file mode 100644
index 0000000..4d00d6d
Binary files /dev/null and b/docs/v1/electron-bot.png differ
diff --git a/docs/v1/esp-hi.jpg b/docs/v1/esp-hi.jpg
new file mode 100644
index 0000000..d6fc714
Binary files /dev/null and b/docs/v1/esp-hi.jpg differ
diff --git a/docs/v1/esp-sparkbot.jpg b/docs/v1/esp-sparkbot.jpg
new file mode 100644
index 0000000..b738840
Binary files /dev/null and b/docs/v1/esp-sparkbot.jpg differ
diff --git a/docs/v1/espbox3.jpg b/docs/v1/espbox3.jpg
new file mode 100644
index 0000000..641d74b
Binary files /dev/null and b/docs/v1/espbox3.jpg differ
diff --git a/docs/v1/lichuang-s3.jpg b/docs/v1/lichuang-s3.jpg
new file mode 100644
index 0000000..a559070
Binary files /dev/null and b/docs/v1/lichuang-s3.jpg differ
diff --git a/docs/v1/lilygo-t-circle-s3.jpg b/docs/v1/lilygo-t-circle-s3.jpg
new file mode 100644
index 0000000..45985d8
Binary files /dev/null and b/docs/v1/lilygo-t-circle-s3.jpg differ
diff --git a/docs/v1/m5cores3.jpg b/docs/v1/m5cores3.jpg
new file mode 100644
index 0000000..6a30cef
Binary files /dev/null and b/docs/v1/m5cores3.jpg differ
diff --git a/docs/v1/magiclick.jpg b/docs/v1/magiclick.jpg
new file mode 100644
index 0000000..3c01463
Binary files /dev/null and b/docs/v1/magiclick.jpg differ
diff --git a/docs/v1/movecall-cuican-esp32s3.jpg b/docs/v1/movecall-cuican-esp32s3.jpg
new file mode 100644
index 0000000..ae70cfd
Binary files /dev/null and b/docs/v1/movecall-cuican-esp32s3.jpg differ
diff --git a/docs/v1/movecall-moji-esp32s3.jpg b/docs/v1/movecall-moji-esp32s3.jpg
new file mode 100644
index 0000000..dec4526
Binary files /dev/null and b/docs/v1/movecall-moji-esp32s3.jpg differ
diff --git a/docs/v1/otto-robot.png b/docs/v1/otto-robot.png
new file mode 100644
index 0000000..d61cbdc
Binary files /dev/null and b/docs/v1/otto-robot.png differ
diff --git a/docs/v1/sensecap_watcher.jpg b/docs/v1/sensecap_watcher.jpg
new file mode 100644
index 0000000..b1d7e4c
Binary files /dev/null and b/docs/v1/sensecap_watcher.jpg differ
diff --git a/docs/v1/waveshare.jpg b/docs/v1/waveshare.jpg
new file mode 100644
index 0000000..7dacf2f
Binary files /dev/null and b/docs/v1/waveshare.jpg differ
diff --git a/docs/v1/wiring2.jpg b/docs/v1/wiring2.jpg
new file mode 100644
index 0000000..f3a67ae
Binary files /dev/null and b/docs/v1/wiring2.jpg differ
diff --git a/docs/v1/wmnologo_xingzhi_0.96.jpg b/docs/v1/wmnologo_xingzhi_0.96.jpg
new file mode 100644
index 0000000..24369cc
Binary files /dev/null and b/docs/v1/wmnologo_xingzhi_0.96.jpg differ
diff --git a/docs/v1/wmnologo_xingzhi_1.54.jpg b/docs/v1/wmnologo_xingzhi_1.54.jpg
new file mode 100644
index 0000000..7456477
Binary files /dev/null and b/docs/v1/wmnologo_xingzhi_1.54.jpg differ
diff --git a/docs/v1/xmini-c3.jpg b/docs/v1/xmini-c3.jpg
new file mode 100644
index 0000000..f1ed8c2
Binary files /dev/null and b/docs/v1/xmini-c3.jpg differ
diff --git a/docs/websocket.md b/docs/websocket.md
new file mode 100644
index 0000000..f767114
--- /dev/null
+++ b/docs/websocket.md
@@ -0,0 +1,495 @@
+以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述设备端与服务器之间如何通过 WebSocket 进行交互。
+
+该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。
+
+---
+
+## 1. 总体流程概览
+
+1. **设备端初始化**
+ - 设备上电、初始化 `Application`:
+ - 初始化音频编解码器、显示屏、LED 等
+ - 连接网络
+ - 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol`)
+ - 进入主循环等待事件(音频输入、音频输出、调度任务等)。
+
+2. **建立 WebSocket 连接**
+ - 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()`:
+ - 根据配置获取 WebSocket URL
+ - 设置若干请求头(`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`)
+ - 调用 `Connect()` 与服务器建立 WebSocket 连接
+
+3. **设备端发送 "hello" 消息**
+ - 连接成功后,设备会发送一条 JSON 消息,示例结构如下:
+ ```json
+ {
+ "type": "hello",
+ "version": 1,
+ "features": {
+ "mcp": true
+ },
+ "transport": "websocket",
+ "audio_params": {
+ "format": "opus",
+ "sample_rate": 16000,
+ "channels": 1,
+ "frame_duration": 60
+ }
+ }
+ ```
+ - 其中 `features` 字段为可选,内容根据设备编译配置自动生成。例如:`"mcp": true` 表示支持 MCP 协议。
+ - `frame_duration` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms)。
+
+4. **服务器回复 "hello"**
+ - 设备等待服务器返回一条包含 `"type": "hello"` 的 JSON 消息,并检查 `"transport": "websocket"` 是否匹配。
+ - 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。
+ - 示例:
+ ```json
+ {
+ "type": "hello",
+ "transport": "websocket",
+ "session_id": "xxx",
+ "audio_params": {
+ "format": "opus",
+ "sample_rate": 24000,
+ "channels": 1,
+ "frame_duration": 60
+ }
+ }
+ ```
+ - 如果匹配,则认为服务器已就绪,标记音频通道打开成功。
+ - 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。
+
+5. **后续消息交互**
+ - 设备端和服务器端之间可发送两种主要类型的数据:
+ 1. **二进制音频数据**(Opus 编码)
+ 2. **文本 JSON 消息**(用于传输聊天状态、TTS/STT 事件、MCP 协议消息等)
+
+ - 在代码里,接收回调主要分为:
+ - `OnData(...)`:
+ - 当 `binary` 为 `true` 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。
+ - 当 `binary` 为 `false` 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理(如聊天、TTS、MCP 协议消息等)。
+
+ - 当服务器或网络出现断连,回调 `OnDisconnected()` 被触发:
+ - 设备会调用 `on_audio_channel_closed_()`,并最终回到空闲状态。
+
+6. **关闭 WebSocket 连接**
+ - 设备在需要结束语音会话时,会调用 `CloseAudioChannel()` 主动断开连接,并回到空闲状态。
+ - 或者如果服务器端主动断开,也会引发同样的回调流程。
+
+---
+
+## 2. 通用请求头
+
+在建立 WebSocket 连接时,代码示例中设置了以下请求头:
+
+- `Authorization`: 用于存放访问令牌,形如 `"Bearer "`
+- `Protocol-Version`: 协议版本号,与 hello 消息体内的 `version` 字段保持一致
+- `Device-Id`: 设备物理网卡 MAC 地址
+- `Client-Id`: 软件生成的 UUID(擦除 NVS 或重新烧录完整固件会重置)
+
+这些头会随着 WebSocket 握手一起发送到服务器,服务器可根据需求进行校验、认证等。
+
+---
+
+## 3. 二进制协议版本
+
+设备支持多种二进制协议版本,通过配置中的 `version` 字段指定:
+
+### 3.1 版本1(默认)
+直接发送 Opus 音频数据,无额外元数据。Websocket 协议会区分 text 与 binary。
+
+### 3.2 版本2
+使用 `BinaryProtocol2` 结构:
+```c
+struct BinaryProtocol2 {
+ uint16_t version; // 协议版本
+ uint16_t type; // 消息类型 (0: OPUS, 1: JSON)
+ uint32_t reserved; // 保留字段
+ uint32_t timestamp; // 时间戳(毫秒,用于服务器端AEC)
+ uint32_t payload_size; // 负载大小(字节)
+ uint8_t payload[]; // 负载数据
+} __attribute__((packed));
+```
+
+### 3.3 版本3
+使用 `BinaryProtocol3` 结构:
+```c
+struct BinaryProtocol3 {
+ uint8_t type; // 消息类型
+ uint8_t reserved; // 保留字段
+ uint16_t payload_size; // 负载大小
+ uint8_t payload[]; // 负载数据
+} __attribute__((packed));
+```
+
+---
+
+## 4. JSON 消息结构
+
+WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。
+
+### 4.1 设备端→服务器
+
+1. **Hello**
+ - 连接成功后,由设备端发送,告知服务器基本参数。
+ - 例:
+ ```json
+ {
+ "type": "hello",
+ "version": 1,
+ "features": {
+ "mcp": true
+ },
+ "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**
+ - 用于设备端向服务器告知检测到唤醒词。
+ - 在发送该消息之前,可提前发送唤醒词的 Opus 音频数据,用于服务器进行声纹检测。
+ - 例:
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "listen",
+ "state": "detect",
+ "text": "你好小明"
+ }
+ ```
+
+5. **MCP**
+ - 推荐用于物联网控制的新一代协议。所有设备能力发现、工具调用等均通过 type: "mcp" 的消息进行,payload 内部为标准 JSON-RPC 2.0(详见 [MCP 协议文档](./mcp-protocol.md))。
+
+ - **设备端到服务器发送 result 的例子:**
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "mcp",
+ "payload": {
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "content": [
+ { "type": "text", "text": "true" }
+ ],
+ "isError": false
+ }
+ }
+ }
+ ```
+
+---
+
+### 4.2 服务器→设备端
+
+1. **Hello**
+ - 服务器端返回的握手确认消息。
+ - 必须包含 `"type": "hello"` 和 `"transport": "websocket"`。
+ - 可能会带有 `audio_params`,表示服务器期望的音频参数,或与设备端对齐的配置。
+ - 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。
+ - 成功接收后设备端会设置事件标志,表示 WebSocket 通道就绪。
+
+2. **STT**
+ - `{"session_id": "xxx", "type": "stt", "text": "..."}`
+ - 表示服务器端识别到了用户语音。(例如语音转文本结果)
+ - 设备可能将此文本显示到屏幕上,后续再进入回答等流程。
+
+3. **LLM**
+ - `{"session_id": "xxx", "type": "llm", "emotion": "happy", "text": "😀"}`
+ - 服务器指示设备调整表情动画 / UI 表达。
+
+4. **TTS**
+ - `{"session_id": "xxx", "type": "tts", "state": "start"}`:服务器准备下发 TTS 音频,设备端进入 "speaking" 播放状态。
+ - `{"session_id": "xxx", "type": "tts", "state": "stop"}`:表示本次 TTS 结束。
+ - `{"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}`
+ - 让设备在界面上显示当前要播放或朗读的文本片段(例如用于显示给用户)。
+
+5. **MCP**
+ - 服务器通过 type: "mcp" 的消息下发物联网相关的控制指令或返回调用结果,payload 结构同上。
+
+ - **服务器到设备端发送 tools/call 的例子:**
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "mcp",
+ "payload": {
+ "jsonrpc": "2.0",
+ "method": "tools/call",
+ "params": {
+ "name": "self.light.set_rgb",
+ "arguments": { "r": 255, "g": 0, "b": 0 }
+ },
+ "id": 1
+ }
+ }
+ ```
+
+6. **System**
+ - 系统控制命令,常用于远程升级更新。
+ - 例:
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "system",
+ "command": "reboot"
+ }
+ ```
+ - 支持的命令:
+ - `"reboot"`:重启设备
+
+7. **Custom**(可选)
+ - 自定义消息,当 `CONFIG_RECEIVE_CUSTOM_MESSAGE` 启用时支持。
+ - 例:
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "custom",
+ "payload": {
+ "message": "自定义内容"
+ }
+ }
+ ```
+
+8. **音频数据:二进制帧**
+ - 当服务器发送音频二进制帧(Opus 编码)时,设备端解码并播放。
+ - 若设备端正在处于 "listening" (录音)状态,收到的音频帧会被忽略或清空以防冲突。
+
+---
+
+## 5. 音频编解码
+
+1. **设备端发送录音数据**
+ - 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。
+ - 根据协议版本,可能直接发送 Opus 数据(版本1)或使用带元数据的二进制协议(版本2/3)。
+
+2. **设备端播放收到的音频**
+ - 收到服务器的二进制帧时,同样认定是 Opus 数据。
+ - 设备端会进行解码,然后交由音频输出接口播放。
+ - 如果服务器的音频采样率与设备不一致,会在解码后再进行重采样。
+
+---
+
+## 6. 常见状态流转
+
+以下为常见设备端关键状态流转,与 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。
+
+### 自动模式状态流转图
+
+```mermaid
+stateDiagram
+ direction TB
+ [*] --> kDeviceStateUnknown
+ kDeviceStateUnknown --> kDeviceStateStarting:初始化
+ kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
+ kDeviceStateStarting --> kDeviceStateActivating:激活设备
+ kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本
+ kDeviceStateActivating --> kDeviceStateIdle:激活完成
+ kDeviceStateIdle --> kDeviceStateConnecting:开始连接
+ kDeviceStateConnecting --> kDeviceStateIdle:连接失败
+ kDeviceStateConnecting --> kDeviceStateListening:连接成功
+ kDeviceStateListening --> kDeviceStateSpeaking:开始说话
+ kDeviceStateSpeaking --> kDeviceStateListening:结束说话
+ kDeviceStateListening --> kDeviceStateIdle:手动终止
+ kDeviceStateSpeaking --> kDeviceStateIdle:自动终止
+```
+
+### 手动模式状态流转图
+
+```mermaid
+stateDiagram
+ direction TB
+ [*] --> kDeviceStateUnknown
+ kDeviceStateUnknown --> kDeviceStateStarting:初始化
+ kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
+ kDeviceStateStarting --> kDeviceStateActivating:激活设备
+ kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本
+ kDeviceStateActivating --> kDeviceStateIdle:激活完成
+ kDeviceStateIdle --> kDeviceStateConnecting:开始连接
+ kDeviceStateConnecting --> kDeviceStateIdle:连接失败
+ kDeviceStateConnecting --> kDeviceStateListening:连接成功
+ kDeviceStateIdle --> kDeviceStateListening:开始监听
+ kDeviceStateListening --> kDeviceStateIdle:停止监听
+ kDeviceStateIdle --> kDeviceStateSpeaking:开始说话
+ kDeviceStateSpeaking --> kDeviceStateIdle:结束说话
+```
+
+---
+
+## 7. 错误处理
+
+1. **连接失败**
+ - 如果 `Connect(url)` 返回失败或在等待服务器 "hello" 消息时超时,触发 `on_network_error_()` 回调。设备会提示"无法连接到服务"或类似错误信息。
+
+2. **服务器断开**
+ - 如果 WebSocket 异常断开,回调 `OnDisconnected()`:
+ - 设备回调 `on_audio_channel_closed_()`
+ - 切换到 Idle 或其他重试逻辑。
+
+---
+
+## 8. 其它注意事项
+
+1. **鉴权**
+ - 设备通过设置 `Authorization: Bearer ` 提供鉴权,服务器端需验证是否有效。
+ - 如果令牌过期或无效,服务器可拒绝握手或在后续断开。
+
+2. **会话控制**
+ - 代码中部分消息包含 `session_id`,用于区分独立的对话或操作。服务端可根据需要对不同会话做分离处理。
+
+3. **音频负载**
+ - 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。为了获得更好的音乐播放效果,服务器下行音频可能使用 24000 采样率。
+
+4. **协议版本配置**
+ - 通过设置中的 `version` 字段配置二进制协议版本(1、2 或 3)
+ - 版本1:直接发送 Opus 数据
+ - 版本2:使用带时间戳的二进制协议,适用于服务器端 AEC
+ - 版本3:使用简化的二进制协议
+
+5. **物联网控制推荐 MCP 协议**
+ - 设备与服务器之间的物联网能力发现、状态同步、控制指令等,建议全部通过 MCP 协议(type: "mcp")实现。原有的 type: "iot" 方案已废弃。
+ - MCP 协议可在 WebSocket、MQTT 等多种底层协议上传输,具备更好的扩展性和标准化能力。
+ - 详细用法请参考 [MCP 协议文档](./mcp-protocol.md) 及 [MCP 物联网控制用法](./mcp-usage.md)。
+
+6. **错误或异常 JSON**
+ - 当 JSON 中缺少必要字段,例如 `{"type": ...}`,设备端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。
+
+---
+
+## 9. 消息示例
+
+下面给出一个典型的双向消息示例(流程简化示意):
+
+1. **设备端 → 服务器**(握手)
+ ```json
+ {
+ "type": "hello",
+ "version": 1,
+ "features": {
+ "mcp": true
+ },
+ "transport": "websocket",
+ "audio_params": {
+ "format": "opus",
+ "sample_rate": 16000,
+ "channels": 1,
+ "frame_duration": 60
+ }
+ }
+ ```
+
+2. **服务器 → 设备端**(握手应答)
+ ```json
+ {
+ "type": "hello",
+ "transport": "websocket",
+ "session_id": "xxx",
+ "audio_params": {
+ "format": "opus",
+ "sample_rate": 16000
+ }
+ }
+ ```
+
+3. **设备端 → 服务器**(开始监听)
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "listen",
+ "state": "start",
+ "mode": "auto"
+ }
+ ```
+ 同时设备端开始发送二进制帧(Opus 数据)。
+
+4. **服务器 → 设备端**(ASR 结果)
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "stt",
+ "text": "用户说的话"
+ }
+ ```
+
+5. **服务器 → 设备端**(TTS开始)
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "tts",
+ "state": "start"
+ }
+ ```
+ 接着服务器发送二进制音频帧给设备端播放。
+
+6. **服务器 → 设备端**(TTS结束)
+ ```json
+ {
+ "session_id": "xxx",
+ "type": "tts",
+ "state": "stop"
+ }
+ ```
+ 设备端停止播放音频,若无更多指令,则回到空闲状态。
+
+---
+
+## 10. 总结
+
+本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧,完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、MCP 指令下发等。其核心特征:
+
+- **握手阶段**:发送 `"type":"hello"`,等待服务器返回。
+- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流,支持多种协议版本。
+- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、MCP、WakeWord、System、Custom 等。
+- **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。
+
+服务器与设备端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
new file mode 100644
index 0000000..44f0d9b
--- /dev/null
+++ b/main/CMakeLists.txt
@@ -0,0 +1,841 @@
+# Define default assets files (Absolute url starting with http or https is supported)
+set(ASSETS_URL_PREFIX "https://files.xiaozhi.me/assets/default/")
+
+set(ASSETS_PUHUI_COMMON_14_1 "${ASSETS_URL_PREFIX}none-font_puhui_common_14_1-none.bin")
+set(ASSETS_XIAOZHI_WAKENET_ONLY "${ASSETS_URL_PREFIX}wn9_nihaoxiaozhi_tts-none-none.bin")
+set(ASSETS_XIAOZHI_PUHUI_COMMON_14_1 "${ASSETS_URL_PREFIX}wn9_nihaoxiaozhi_tts-font_puhui_common_14_1-none.bin")
+set(ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32 "${ASSETS_URL_PREFIX}wn9_nihaoxiaozhi_tts-font_puhui_common_16_4-emojis_32.bin")
+set(ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64 "${ASSETS_URL_PREFIX}wn9_nihaoxiaozhi_tts-font_puhui_common_16_4-emojis_64.bin")
+set(ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64 "${ASSETS_URL_PREFIX}wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-emojis_64.bin")
+set(ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64 "${ASSETS_URL_PREFIX}wn9_nihaoxiaozhi_tts-font_puhui_common_30_4-emojis_64.bin")
+set(ASSETS_XIAOZHI_S_WAKENET_ONLY "${ASSETS_URL_PREFIX}wn9s_nihaoxiaozhi-none-none.bin")
+set(ASSETS_XIAOZHI_S_PUHUI_COMMON_14_1 "${ASSETS_URL_PREFIX}wn9s_nihaoxiaozhi-font_puhui_common_14_1-none.bin")
+set(ASSETS_XIAOZHI_S_PUHUI_COMMON_16_4_EMOJI_32 "${ASSETS_URL_PREFIX}wn9s_nihaoxiaozhi-font_puhui_common_16_4-emojis_32.bin")
+set(ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_32 "${ASSETS_URL_PREFIX}wn9s_nihaoxiaozhi-font_puhui_common_20_4-emojis_32.bin")
+set(ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_64 "${ASSETS_URL_PREFIX}wn9s_nihaoxiaozhi-font_puhui_common_20_4-emojis_64.bin")
+set(ASSETS_XIAOZHI_S_PUHUI_COMMON_30_4_EMOJI_64 "${ASSETS_URL_PREFIX}wn9s_nihaoxiaozhi-font_puhui_common_30_4-emojis_64.bin")
+
+# Embedded font files defined in `xiaozhi-fonts` component
+# Basic fonts include ASCII and about 600 characters used in assets/locales
+set(FONT_PUHUI_BASIC_14_1 font_puhui_basic_14_1)
+set(FONT_PUHUI_BASIC_16_4 font_puhui_basic_16_4)
+set(FONT_PUHUI_BASIC_20_4 font_puhui_basic_20_4)
+set(FONT_PUHUI_BASIC_30_4 font_puhui_basic_30_4)
+# Common fonts include about 7000 common characters generated with DeepSeek R1 tokenizer
+set(FONT_PUHUI_COMMON_14_1 font_puhui_14_1)
+set(FONT_PUHUI_COMMON_16_4 font_puhui_16_4)
+set(FONT_PUHUI_COMMON_20_4 font_puhui_20_4)
+set(FONT_PUHUI_COMMON_30_4 font_puhui_30_4)
+set(FONT_AWESOME_14_1 font_awesome_14_1)
+set(FONT_AWESOME_30_1 font_awesome_30_1)
+set(FONT_AWESOME_16_4 font_awesome_16_4)
+set(FONT_AWESOME_20_4 font_awesome_20_4)
+set(FONT_AWESOME_30_4 font_awesome_30_4)
+
+
+# Define source files
+set(SOURCES "audio/audio_codec.cc"
+ "audio/audio_service.cc"
+ "audio/codecs/no_audio_codec.cc"
+ "audio/codecs/box_audio_codec.cc"
+ "audio/codecs/es8311_audio_codec.cc"
+ "audio/codecs/es8374_audio_codec.cc"
+ "audio/codecs/es8388_audio_codec.cc"
+ "audio/codecs/es8389_audio_codec.cc"
+ "audio/codecs/dummy_audio_codec.cc"
+ "audio/processors/audio_debugger.cc"
+ "led/single_led.cc"
+ "led/circular_strip.cc"
+ "led/gpio_led.cc"
+ "display/display.cc"
+ "display/lcd_display.cc"
+ "display/oled_display.cc"
+ "display/lvgl_display/lvgl_display.cc"
+ "display/lvgl_display/emoji_collection.cc"
+ "display/lvgl_display/lvgl_theme.cc"
+ "display/lvgl_display/lvgl_font.cc"
+ "display/lvgl_display/lvgl_image.cc"
+ "display/lvgl_display/gif/lvgl_gif.cc"
+ "display/lvgl_display/gif/gifdec.c"
+ "protocols/protocol.cc"
+ "protocols/mqtt_protocol.cc"
+ "protocols/websocket_protocol.cc"
+ "mcp_server.cc"
+ "system_info.cc"
+ "application.cc"
+ "ota.cc"
+ "settings.cc"
+ "device_state_event.cc"
+ "assets.cc"
+ "main.cc"
+ )
+
+set(INCLUDE_DIRS "." "display" "display/lvgl_display" "audio" "protocols")
+
+# Add board common files
+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)
+
+# Set default LVGL_TEXT_FONT and LVGL_ICON_FONT
+set(LVGL_TEXT_FONT ${FONT_PUHUI_COMMON_14_1})
+set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+
+# Add board files according to BOARD_TYPE
+# Set default assets if the board uses partition table V2
+if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)
+ set(BOARD_TYPE "bread-compact-wifi")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_ML307)
+ set(BOARD_TYPE "bread-compact-ml307")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_14_1})
+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")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_COMMON_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+elseif(CONFIG_BOARD_TYPE_DF_K10)
+ set(BOARD_TYPE "df-k10")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_DF_S3_AI_CAM)
+ set(BOARD_TYPE "df-s3-ai-cam")
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_WAKENET_ONLY})
+elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
+ set(BOARD_TYPE "esp-box-3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP_BOX)
+ set(BOARD_TYPE "esp-box")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP_BOX_LITE)
+ set(BOARD_TYPE "esp-box-lite")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_1)
+ set(BOARD_TYPE "kevin-box-1")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_KEVIN_BOX_2)
+ set(BOARD_TYPE "kevin-box-2")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_KEVIN_C3)
+ set(BOARD_TYPE "kevin-c3")
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_WAKENET_ONLY})
+elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V3_DEV)
+ set(BOARD_TYPE "kevin-sp-v3-dev")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_KEVIN_SP_V4_DEV)
+ set(BOARD_TYPE "kevin-sp-v4-dev")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD)
+ set(BOARD_TYPE "kevin-yuying-313lcd")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV)
+ set(BOARD_TYPE "lichuang-dev")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_LICHUANG_C3_DEV)
+ set(BOARD_TYPE "lichuang-c3-dev")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_MAGICLICK_2P4)
+ set(BOARD_TYPE "magiclick-2p4")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_MAGICLICK_2P5)
+ set(BOARD_TYPE "magiclick-2p5")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3)
+ set(BOARD_TYPE "magiclick-c3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_MAGICLICK_C3_V2)
+ set(BOARD_TYPE "magiclick-c3-v2")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_S3)
+ set(BOARD_TYPE "m5stack-core-s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_TAB5)
+ set(BOARD_TYPE "m5stack-tab5")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ATOMS3_ECHO_BASE)
+ set(BOARD_TYPE "atoms3-echo-base")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_ATOMS3R_ECHO_BASE)
+ set(BOARD_TYPE "atoms3r-echo-base")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_ATOMS3R_CAM_M12_ECHO_BASE)
+ set(BOARD_TYPE "atoms3r-cam-m12-echo-base")
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_WAKENET_ONLY})
+elseif(CONFIG_BOARD_TYPE_ATOM_ECHOS3R)
+ set(BOARD_TYPE "atom-echos3r")
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_WAKENET_ONLY})
+elseif(CONFIG_BOARD_TYPE_ATOMMATRIX_ECHO_BASE)
+ set(BOARD_TYPE "atommatrix-echo-base")
+elseif(CONFIG_BOARD_TYPE_XMINI_C3_V3)
+ set(BOARD_TYPE "xmini-c3-v3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_XMINI_C3_4G)
+ set(BOARD_TYPE "xmini-c3-4g")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_XMINI_C3)
+ set(BOARD_TYPE "xmini-c3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_KORVO2_V3)
+ set(BOARD_TYPE "esp32s3-korvo2-v3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT)
+ set(BOARD_TYPE "esp-sparkbot")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3)
+ set(BOARD_TYPE "esp-spot-s3")
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_WAKENET_ONLY})
+elseif(CONFIG_BOARD_TYPE_ESP_HI)
+ set(BOARD_TYPE "esp-hi")
+elseif(CONFIG_BOARD_TYPE_ECHOEAR)
+ set(BOARD_TYPE "echoear")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_COMMON_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_AUDIO_BOARD)
+ set(BOARD_TYPE "waveshare-s3-audio-board")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8)
+ set(BOARD_TYPE "esp32-s3-touch-amoled-1.8")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06)
+ set(BOARD_TYPE "waveshare-s3-touch-amoled-2.06")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75)
+ set(BOARD_TYPE "waveshare-s3-touch-amoled-1.75")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85C)
+ set(BOARD_TYPE "esp32-s3-touch-lcd-1.85c")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85)
+ set(BOARD_TYPE "esp32-s3-touch-lcd-1.85")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_46)
+ set(BOARD_TYPE "esp32-s3-touch-lcd-1.46")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_3_5)
+ set(BOARD_TYPE "esp32-s3-touch-lcd-3.5")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_3_5B)
+ set(BOARD_TYPE "waveshare-s3-touch-lcd-3.5b")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_ESP32C6_LCD_1_69)
+ set(BOARD_TYPE "waveshare-c6-lcd-1.69")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32C6_Touch_AMOLED_1_43)
+ set(BOARD_TYPE "waveshare-c6-touch-amoled-1.43")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32P4_NANO)
+ set(BOARD_TYPE "waveshare-p4-nano")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B)
+ set(BOARD_TYPE "waveshare-p4-wifi6-touch-lcd-4b")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC)
+ set(BOARD_TYPE "waveshare-p4-wifi6-touch-lcd-xc")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD)
+ set(BOARD_TYPE "bread-compact-wifi-lcd")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_TUDOUZI)
+ set(BOARD_TYPE "tudouzi")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_LILYGO_T_CIRCLE_S3)
+ set(BOARD_TYPE "lilygo-t-circle-s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_0_V1_1)
+ set(BOARD_TYPE "lilygo-t-cameraplus-s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA)
+ set(BOARD_TYPE "lilygo-t-display-s3-pro-mvsrlora")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_MOVECALL_MOJI_ESP32S3)
+ set(BOARD_TYPE "movecall-moji-esp32s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_MOVECALL_CUICAN_ESP32S3)
+ set(BOARD_TYPE "movecall-cuican-esp32s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3)
+ set(BOARD_TYPE "atk-dnesp32s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX)
+ set(BOARD_TYPE "atk-dnesp32s3-box")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX0)
+ set(BOARD_TYPE "atk-dnesp32s3-box0")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI)
+ set(BOARD_TYPE "atk-dnesp32s3-box2-wifi")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3_BOX2_4G)
+ set(BOARD_TYPE "atk-dnesp32s3-box2-4g")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_WIFI)
+ set(BOARD_TYPE "atk-dnesp32s3m-wifi")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_ATK_DNESP32S3M_4G)
+ set(BOARD_TYPE "atk-dnesp32s3m-4g")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_DU_CHATX)
+ set(BOARD_TYPE "du-chatx")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_ESP32S3_Taiji_Pi)
+ set(BOARD_TYPE "taiji-pi-s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_85TFT_WIFI)
+ set(BOARD_TYPE "xingzhi-cube-0.85tft-wifi")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_85TFT_ML307)
+ set(BOARD_TYPE "xingzhi-cube-0.85tft-ml307")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_96OLED_WIFI)
+ set(BOARD_TYPE "xingzhi-cube-0.96oled-wifi")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_0_96OLED_ML307)
+ set(BOARD_TYPE "xingzhi-cube-0.96oled-ml307")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_14_1})
+elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_1_54TFT_WIFI)
+ set(BOARD_TYPE "xingzhi-cube-1.54tft-wifi")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_XINGZHI_Cube_1_54TFT_ML307)
+ set(BOARD_TYPE "xingzhi-cube-1.54tft-ml307")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_SENSECAP_WATCHER)
+ set(BOARD_TYPE "sensecap-watcher")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_DOIT_S3_AIBOX)
+ set(BOARD_TYPE "doit-s3-aibox")
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_WAKENET_ONLY})
+elseif(CONFIG_BOARD_TYPE_MIXGO_NOVA)
+ set(BOARD_TYPE "mixgo-nova")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_GENJUTECH_S3_1_54TFT)
+ set(BOARD_TYPE "genjutech-s3-1.54tft")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32_CGC)
+ set(BOARD_TYPE "esp32-cgc")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_COMMON_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+elseif(CONFIG_BOARD_TYPE_ESP32_CGC_144)
+ set(BOARD_TYPE "esp32-cgc-144")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_COMMON_14_1})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_14_1})
+elseif(CONFIG_BOARD_TYPE_ESP_S3_LCD_EV_Board)
+ set(BOARD_TYPE "esp-s3-lcd-ev-board")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP_S3_LCD_EV_Board_2)
+ set(BOARD_TYPE "esp-s3-lcd-ev-board-2")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_30_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_30_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_1_54TFT_WIFI)
+ set(BOARD_TYPE "zhengchen-1.54tft-wifi")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_MINSI_K08_DUAL)
+ set(BOARD_TYPE "minsi-k08-dual")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_1_54TFT_ML307)
+ set(BOARD_TYPE "zhengchen-1.54tft-ml307")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ESP32_S3_1_54_MUMA)
+ set(BOARD_TYPE "sp-esp32-s3-1.54-muma")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_ESP32_S3_1_28_BOX)
+ set(BOARD_TYPE "sp-esp32-s3-1.28-box")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_OTTO_ROBOT)
+ set(BOARD_TYPE "otto-robot")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_COMMON_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+elseif(CONFIG_BOARD_TYPE_ELECTRON_BOT)
+ set(BOARD_TYPE "electron-bot")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_COMMON_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_CAM)
+ set(BOARD_TYPE "bread-compact-wifi-s3cam")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_16_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_16_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_JIUCHUAN)
+ set(BOARD_TYPE "jiuchuan-s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_LABPLUS_MPYTHON_V3)
+ set(BOARD_TYPE "labplus-mpython-v3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_LABPLUS_LEDONG_V2)
+ set(BOARD_TYPE "labplus-ledong-v2")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_SURFER_C3_1_14TFT)
+ set(BOARD_TYPE "surfer-c3-1.14tft")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_32})
+elseif(CONFIG_BOARD_TYPE_YUNLIAO_S3)
+ set(BOARD_TYPE "yunliao-s3")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_shhk_CAM)
+ set(BOARD_TYPE "shhk-cam")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+elseif(CONFIG_BOARD_TYPE_ZHENGCHEN_CAM_ML307)
+ set(BOARD_TYPE "zhengchen-cam-ml307")
+ set(LVGL_TEXT_FONT ${FONT_PUHUI_BASIC_20_4})
+ set(LVGL_ICON_FONT ${FONT_AWESOME_20_4})
+ set(DEFAULT_ASSETS ${ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64})
+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})
+
+# Select audio processor according to Kconfig
+if(CONFIG_USE_AUDIO_PROCESSOR)
+ list(APPEND SOURCES "audio/processors/afe_audio_processor.cc")
+else()
+ list(APPEND SOURCES "audio/processors/no_audio_processor.cc")
+endif()
+if(CONFIG_USE_AFE_WAKE_WORD)
+ list(APPEND SOURCES "audio/wake_words/afe_wake_word.cc")
+elseif(CONFIG_USE_ESP_WAKE_WORD)
+ list(APPEND SOURCES "audio/wake_words/esp_wake_word.cc")
+elseif(CONFIG_USE_CUSTOM_WAKE_WORD)
+ list(APPEND SOURCES "audio/wake_words/custom_wake_word.cc")
+endif()
+
+# Select language directory according to 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")
+elseif(CONFIG_LANGUAGE_KO_KR)
+ set(LANG_DIR "ko-KR")
+elseif(CONFIG_LANGUAGE_VI_VN)
+ set(LANG_DIR "vi-VN")
+elseif(CONFIG_LANGUAGE_TH_TH)
+ set(LANG_DIR "th-TH")
+elseif(CONFIG_LANGUAGE_DE_DE)
+ set(LANG_DIR "de-DE")
+elseif(CONFIG_LANGUAGE_FR_FR)
+ set(LANG_DIR "fr-FR")
+elseif(CONFIG_LANGUAGE_ES_ES)
+ set(LANG_DIR "es-ES")
+elseif(CONFIG_LANGUAGE_IT_IT)
+ set(LANG_DIR "it-IT")
+elseif(CONFIG_LANGUAGE_RU_RU)
+ set(LANG_DIR "ru-RU")
+elseif(CONFIG_LANGUAGE_AR_SA)
+ set(LANG_DIR "ar-SA")
+elseif(CONFIG_LANGUAGE_HI_IN)
+ set(LANG_DIR "hi-IN")
+elseif(CONFIG_LANGUAGE_PT_PT)
+ set(LANG_DIR "pt-PT")
+elseif(CONFIG_LANGUAGE_PL_PL)
+ set(LANG_DIR "pl-PL")
+elseif(CONFIG_LANGUAGE_CS_CZ)
+ set(LANG_DIR "cs-CZ")
+elseif(CONFIG_LANGUAGE_FI_FI)
+ set(LANG_DIR "fi-FI")
+elseif(CONFIG_LANGUAGE_TR_TR)
+ set(LANG_DIR "tr-TR")
+elseif(CONFIG_LANGUAGE_ID_ID)
+ set(LANG_DIR "id-ID")
+elseif(CONFIG_LANGUAGE_UK_UA)
+ set(LANG_DIR "uk-UA")
+elseif(CONFIG_LANGUAGE_RO_RO)
+ set(LANG_DIR "ro-RO")
+endif()
+
+# Define generation path
+set(LANG_JSON "${CMAKE_CURRENT_SOURCE_DIR}/assets/locales/${LANG_DIR}/language.json")
+set(LANG_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/assets/lang_config.h")
+file(GLOB LANG_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/locales/${LANG_DIR}/*.ogg)
+file(GLOB COMMON_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/common/*.ogg)
+
+# If target chip is ESP32, exclude specific files to avoid build errors
+if(CONFIG_IDF_TARGET_ESP32)
+ list(REMOVE_ITEM SOURCES "audio/codecs/box_audio_codec.cc"
+ "audio/codecs/es8388_audio_codec.cc"
+ "audio/codecs/es8389_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
+ )
+
+# Use target_compile_definitions to define BOARD_TYPE, BOARD_NAME
+# If BOARD_NAME is empty, use 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}\"
+ PRIVATE DEFAULT_ASSETS=\"${DEFAULT_ASSETS}\" LVGL_TEXT_FONT=${LVGL_TEXT_FONT} LVGL_ICON_FONT=${LVGL_ICON_FONT}
+ )
+
+# Add generation rules
+add_custom_command(
+ OUTPUT ${LANG_HEADER}
+ COMMAND python ${PROJECT_DIR}/scripts/gen_lang.py
+ --language "${LANG_DIR}"
+ --output "${LANG_HEADER}"
+ DEPENDS
+ ${LANG_JSON}
+ ${PROJECT_DIR}/scripts/gen_lang.py
+ COMMENT "Generating ${LANG_DIR} language config"
+)
+
+# Force build generation dependencies
+add_custom_target(lang_header ALL
+ DEPENDS ${LANG_HEADER}
+)
+
+if(CONFIG_BOARD_TYPE_ESP_HI)
+set(URL "https://github.com/espressif2022/image_player/raw/main/test_apps/test_8bit")
+set(SPIFFS_DIR "${CMAKE_BINARY_DIR}/emoji")
+file(MAKE_DIRECTORY ${SPIFFS_DIR})
+
+# List all files to download
+set(FILES_TO_DOWNLOAD "")
+list(APPEND FILES_TO_DOWNLOAD "Anger_enter.aaf" "Anger_loop.aaf" "Anger_return.aaf")
+list(APPEND FILES_TO_DOWNLOAD "happy_enter.aaf" "happy_loop.aaf" "happ_return.aaf")
+list(APPEND FILES_TO_DOWNLOAD "sad_enter.aaf" "sad_loop.aaf" "sad_return.aaf")
+list(APPEND FILES_TO_DOWNLOAD "scorn_enter.aaf" "scorn_loop.aaf" "scorn_return.aaf")
+list(APPEND FILES_TO_DOWNLOAD "left_enter.aaf" "left_loop.aaf" "left_return.aaf")
+list(APPEND FILES_TO_DOWNLOAD "right_enter.aaf" "right_loop.aaf" "right_return.aaf")
+list(APPEND FILES_TO_DOWNLOAD "asking.aaf" "blink_once.aaf" "blink_quick.aaf")
+list(APPEND FILES_TO_DOWNLOAD "connecting.aaf" "panic_enter.aaf" "panic_loop.aaf")
+list(APPEND FILES_TO_DOWNLOAD "panic_return.aaf" "wake.aaf")
+
+foreach(FILENAME IN LISTS FILES_TO_DOWNLOAD)
+ set(REMOTE_FILE "${URL}/${FILENAME}")
+ set(LOCAL_FILE "${SPIFFS_DIR}/${FILENAME}")
+
+ # Check if local file exists
+ if(EXISTS ${LOCAL_FILE})
+ message(STATUS "File ${FILENAME} already exists, skipping download")
+ else()
+ message(STATUS "Downloading ${FILENAME}")
+ file(DOWNLOAD ${REMOTE_FILE} ${LOCAL_FILE}
+ STATUS DOWNLOAD_STATUS)
+ list(GET DOWNLOAD_STATUS 0 STATUS_CODE)
+ if(NOT STATUS_CODE EQUAL 0)
+ message(FATAL_ERROR "Failed to download ${FILENAME} from ${URL}")
+ endif()
+ endif()
+endforeach()
+
+spiffs_create_partition_assets(
+ assets_A
+ ${SPIFFS_DIR}
+ FLASH_IN_PROJECT
+ MMAP_FILE_SUPPORT_FORMAT ".aaf"
+)
+endif()
+
+if(CONFIG_BOARD_TYPE_ECHOEAR)
+
+idf_build_get_property(build_components BUILD_COMPONENTS)
+foreach(COMPONENT ${build_components})
+ if(COMPONENT MATCHES "esp_emote_gfx" OR COMPONENT MATCHES "espressif2022__esp_emote_gfx")
+ set(EMOTE_GFX_COMPONENT ${COMPONENT})
+ idf_component_get_property(EMOTE_GFX_COMPONENT_PATH ${EMOTE_GFX_COMPONENT} COMPONENT_DIR)
+ set(SPIFFS_DIR "${EMOTE_GFX_COMPONENT_PATH}/emoji_normal")
+ break()
+ endif()
+endforeach()
+
+spiffs_create_partition_assets(
+ assets_A
+ ${SPIFFS_DIR}
+ FLASH_IN_PROJECT
+ MMAP_FILE_SUPPORT_FORMAT ".aaf, ttf, bin"
+ IMPORT_INC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}
+)
+endif()
+
+# Font configuration validation function
+function(validate_font_config board_name text_font default_assets)
+ if(text_font)
+ # Check if DEFAULT_ASSETS contains font
+ if(default_assets AND default_assets MATCHES "font_")
+ # Rule 1: If DEFAULT_ASSETS uses font, LVGL_TEXT_FONT must be BASIC
+ if(NOT text_font MATCHES "basic")
+ message(FATAL_ERROR "Font config error for ${board_name}: DEFAULT_ASSETS contains COMMON font but LVGL_TEXT_FONT is not BASIC (${text_font})")
+ endif()
+ else()
+ # Rule 2: If no DEFAULT_ASSETS or DEFAULT_ASSETS doesn't contain font_, LVGL_TEXT_FONT must not be BASIC
+ if(text_font MATCHES "basic")
+ message(FATAL_ERROR "Font config error for ${board_name}: No DEFAULT_ASSETS with COMMON font but LVGL_TEXT_FONT is not COMMON (${text_font})")
+ endif()
+ endif()
+ # Pass validation
+ message(STATUS "Font config validation passed for ${board_name}: LVGL_TEXT_FONT=${text_font}, DEFAULT_ASSETS=${default_assets}")
+ endif()
+endfunction()
+
+# DEFAULT_ASSETS prefix validation function
+function(validate_default_assets_prefix board_name default_assets)
+ if(default_assets)
+ # Check for ESP32S3/P4 target - DEFAULT_ASSETS cannot start with "wn9s_"
+ if(CONFIG_IDF_TARGET_ESP32S3 OR CONFIG_IDF_TARGET_ESP32P4)
+ if(default_assets MATCHES "^wn9s_")
+ message(FATAL_ERROR "Assets config error for ${board_name}: DEFAULT_ASSETS cannot start with 'wn9s_' for ESP32S3 target (${default_assets})")
+ endif()
+ endif()
+
+ # Check for ESP32C3/C6 target - DEFAULT_ASSETS cannot start with "wn9_"
+ if(CONFIG_IDF_TARGET_ESP32C3 OR CONFIG_IDF_TARGET_ESP32C6)
+ if(default_assets MATCHES "^wn9_")
+ message(FATAL_ERROR "Assets config error for ${board_name}: DEFAULT_ASSETS cannot start with 'wn9_' for ESP32C3/C6 target (${default_assets})")
+ endif()
+ endif()
+
+ # Pass validation
+ message(STATUS "Assets prefix validation passed for ${board_name}: DEFAULT_ASSETS=${default_assets}")
+ endif()
+endfunction()
+
+# Global font configuration validation
+# This will validate the current board's font configuration
+if(LVGL_TEXT_FONT)
+ validate_font_config("${BOARD_TYPE}" "${LVGL_TEXT_FONT}" "${DEFAULT_ASSETS}")
+endif()
+
+# Global DEFAULT_ASSETS prefix validation
+# This will validate the current board's DEFAULT_ASSETS prefix configuration
+if(DEFAULT_ASSETS)
+ validate_default_assets_prefix("${BOARD_TYPE}" "${DEFAULT_ASSETS}")
+endif()
+
+# Function to get local assets file path (handles both URL and local file)
+function(get_assets_local_file assets_source assets_local_file_var)
+ # Check if it's a URL (starts with http:// or https://)
+ if(assets_source MATCHES "^https?://")
+ # It's a URL, download it
+ get_filename_component(ASSETS_FILENAME "${assets_source}" NAME)
+ set(ASSETS_LOCAL_FILE "${CMAKE_BINARY_DIR}/${ASSETS_FILENAME}")
+ set(ASSETS_TEMP_FILE "${CMAKE_BINARY_DIR}/${ASSETS_FILENAME}.tmp")
+
+ # Check if local file exists
+ if(EXISTS ${ASSETS_LOCAL_FILE})
+ message(STATUS "Assets file ${ASSETS_FILENAME} already exists, skipping download")
+ else()
+ message(STATUS "Downloading ${ASSETS_FILENAME}")
+
+ # Clean up any existing temp file
+ if(EXISTS ${ASSETS_TEMP_FILE})
+ file(REMOVE ${ASSETS_TEMP_FILE})
+ endif()
+
+ # Download to temporary file first
+ file(DOWNLOAD ${assets_source} ${ASSETS_TEMP_FILE}
+ STATUS DOWNLOAD_STATUS)
+ list(GET DOWNLOAD_STATUS 0 STATUS_CODE)
+ if(NOT STATUS_CODE EQUAL 0)
+ # Clean up temp file on failure
+ if(EXISTS ${ASSETS_TEMP_FILE})
+ file(REMOVE ${ASSETS_TEMP_FILE})
+ endif()
+ message(FATAL_ERROR "Failed to download ${ASSETS_FILENAME} from ${assets_source}")
+ endif()
+
+ # Move temp file to final location (atomic operation)
+ file(RENAME ${ASSETS_TEMP_FILE} ${ASSETS_LOCAL_FILE})
+ message(STATUS "Successfully downloaded ${ASSETS_FILENAME}")
+ endif()
+ else()
+ # It's a local file path
+ if(IS_ABSOLUTE "${assets_source}")
+ set(ASSETS_LOCAL_FILE "${assets_source}")
+ else()
+ set(ASSETS_LOCAL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/${assets_source}")
+ endif()
+
+ # Check if local file exists
+ if(NOT EXISTS ${ASSETS_LOCAL_FILE})
+ message(FATAL_ERROR "Assets file not found: ${ASSETS_LOCAL_FILE}")
+ endif()
+
+ message(STATUS "Using assets file: ${ASSETS_LOCAL_FILE}")
+ endif()
+
+ set(${assets_local_file_var} ${ASSETS_LOCAL_FILE} PARENT_SCOPE)
+endfunction()
+
+# Flash assets based on configuration
+if(CONFIG_FLASH_DEFAULT_ASSETS)
+ # Flash default assets
+ if(DEFAULT_ASSETS)
+ get_assets_local_file("${DEFAULT_ASSETS}" ASSETS_LOCAL_FILE)
+ esptool_py_flash_to_partition(flash "assets" "${ASSETS_LOCAL_FILE}")
+ message(STATUS "Default assets download and flash configured: ${DEFAULT_ASSETS} -> assets partition")
+ else()
+ message(WARNING "FLASH_DEFAULT_ASSETS is enabled but no DEFAULT_ASSETS is defined for board ${BOARD_TYPE}")
+ endif()
+elseif(CONFIG_FLASH_CUSTOM_ASSETS)
+ # Flash custom assets
+ get_assets_local_file("${CONFIG_CUSTOM_ASSETS_FILE}" ASSETS_LOCAL_FILE)
+ esptool_py_flash_to_partition(flash "assets" "${ASSETS_LOCAL_FILE}")
+ message(STATUS "Custom assets flash configured: ${ASSETS_LOCAL_FILE} -> assets partition")
+elseif(CONFIG_FLASH_NONE_ASSETS)
+ message(STATUS "Assets flashing disabled (FLASH_NONE_ASSETS)")
+endif()
diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild
new file mode 100644
index 0000000..c9671ef
--- /dev/null
+++ b/main/Kconfig.projbuild
@@ -0,0 +1,588 @@
+menu "Xiaozhi Assistant"
+
+config OTA_URL
+ string "Default OTA URL"
+ default "https://api.tenclass.net/xiaozhi/ota/"
+ help
+ The application will access this URL to check for new firmwares and server address.
+
+choice
+ prompt "Flash Assets"
+ default FLASH_NONE_ASSETS
+ help
+ Select the assets to flash.
+
+ config FLASH_NONE_ASSETS
+ bool "Do not flash assets"
+ config FLASH_DEFAULT_ASSETS
+ bool "Flash Default Assets"
+ config FLASH_CUSTOM_ASSETS
+ bool "Flash Custom Assets"
+endchoice
+
+config CUSTOM_ASSETS_FILE
+ depends on FLASH_CUSTOM_ASSETS
+ string "Custom Assets File"
+ default "assets.bin"
+ help
+ The custom assets file to flash.
+ It can be a local file relative to the project directory or a remote url.
+
+choice
+ prompt "Default Language"
+ 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"
+ config LANGUAGE_KO_KR
+ bool "Korean"
+ config LANGUAGE_VI_VN
+ bool "Vietnamese"
+ config LANGUAGE_TH_TH
+ bool "Thai"
+ config LANGUAGE_DE_DE
+ bool "German"
+ config LANGUAGE_FR_FR
+ bool "French"
+ config LANGUAGE_ES_ES
+ bool "Spanish"
+ config LANGUAGE_IT_IT
+ bool "Italian"
+ config LANGUAGE_RU_RU
+ bool "Russian"
+ config LANGUAGE_AR_SA
+ bool "Arabic"
+ config LANGUAGE_HI_IN
+ bool "Hindi"
+ config LANGUAGE_PT_PT
+ bool "Portuguese"
+ config LANGUAGE_PL_PL
+ bool "Polish"
+ config LANGUAGE_CS_CZ
+ bool "Czech"
+ config LANGUAGE_FI_FI
+ bool "Finnish"
+ config LANGUAGE_TR_TR
+ bool "Turkish"
+ config LANGUAGE_ID_ID
+ bool "Indonesian"
+ config LANGUAGE_UK_UA
+ bool "Ukrainian"
+ config LANGUAGE_RO_RO
+ bool "Romanian"
+endchoice
+
+choice BOARD_TYPE
+ prompt "Board Type"
+ default BOARD_TYPE_BREAD_COMPACT_WIFI
+ help
+ Board type. 开发板类型
+ config BOARD_TYPE_BREAD_COMPACT_WIFI
+ bool "面包板新版接线(WiFi)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_BREAD_COMPACT_WIFI_LCD
+ bool "面包板新版接线(WiFi)+ LCD"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_BREAD_COMPACT_WIFI_CAM
+ bool "面包板新版接线(WiFi)+ LCD + Camera"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_BREAD_COMPACT_ML307
+ bool "面包板新版接线(ML307 AT)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_BREAD_COMPACT_ESP32
+ bool "面包板(WiFi) ESP32 DevKit"
+ depends on IDF_TARGET_ESP32
+ config BOARD_TYPE_BREAD_COMPACT_ESP32_LCD
+ bool "面包板(WiFi+ LCD) ESP32 DevKit"
+ depends on IDF_TARGET_ESP32
+ config BOARD_TYPE_XMINI_C3_V3
+ bool "虾哥 Mini C3 V3"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_XMINI_C3_4G
+ bool "虾哥 Mini C3 4G"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_XMINI_C3
+ bool "虾哥 Mini C3"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_ESP32S3_KORVO2_V3
+ bool "ESP32S3_KORVO2_V3开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_SPARKBOT
+ bool "ESP-SparkBot开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_SPOT_S3
+ bool "ESP-Spot-S3"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_HI
+ bool "ESP-HI"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_ECHOEAR
+ bool "EchoEar"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_BOX_3
+ bool "ESP BOX 3"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_BOX
+ bool "ESP BOX"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_BOX_LITE
+ bool "ESP BOX Lite"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_KEVIN_BOX_1
+ bool "Kevin Box 1"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_KEVIN_BOX_2
+ bool "Kevin Box 2"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_KEVIN_C3
+ bool "Kevin C3"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_KEVIN_SP_V3_DEV
+ bool "Kevin SP V3开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_KEVIN_SP_V4_DEV
+ bool "Kevin SP V4开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32_CGC
+ bool "ESP32 CGC"
+ depends on IDF_TARGET_ESP32
+ config BOARD_TYPE_ESP32_CGC_144
+ bool "ESP32 CGC 144"
+ depends on IDF_TARGET_ESP32
+ config BOARD_TYPE_KEVIN_YUYING_313LCD
+ bool "鱼鹰科技3.13LCD开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_LICHUANG_DEV
+ bool "立创·实战派ESP32-S3开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_LICHUANG_C3_DEV
+ bool "立创·实战派ESP32-C3开发板"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_DF_K10
+ bool "DFRobot 行空板 k10"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_DF_S3_AI_CAM
+ bool "DFRobot ESP32-S3 AI智能摄像头模块"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_MAGICLICK_2P4
+ bool "神奇按钮 Magiclick_2.4"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_MAGICLICK_2P5
+ bool "神奇按钮 Magiclick_2.5"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_MAGICLICK_C3
+ bool "神奇按钮 Magiclick_C3"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_MAGICLICK_C3_V2
+ bool "神奇按钮 Magiclick_C3_v2"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_M5STACK_CORE_S3
+ bool "M5Stack CoreS3"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_M5STACK_CORE_TAB5
+ bool "M5Stack Tab5"
+ depends on IDF_TARGET_ESP32P4
+ config BOARD_TYPE_ATOMS3_ECHO_BASE
+ bool "AtomS3 + Echo Base"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATOMS3R_ECHO_BASE
+ bool "AtomS3R + Echo Base"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATOMS3R_CAM_M12_ECHO_BASE
+ bool "AtomS3R CAM/M12 + Echo Base"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATOM_ECHOS3R
+ bool "AtomEchoS3R"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATOMMATRIX_ECHO_BASE
+ bool "AtomMatrix + Echo Base"
+ depends on IDF_TARGET_ESP32
+ config BOARD_TYPE_ESP32S3_AUDIO_BOARD
+ bool "Waveshare ESP32-S3-Audio-Board"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8
+ bool "Waveshare ESP32-S3-Touch-AMOLED-1.8"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06
+ bool "Waveshare ESP32-S3-Touch-AMOLED-2.06"
+ config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75
+ bool "Waveshare ESP32-S3-Touch-AMOLED-1.75"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32S3_Touch_LCD_1_85C
+ bool "Waveshare ESP32-S3-Touch-LCD-1.85C"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32S3_Touch_LCD_1_85
+ bool "Waveshare ESP32-S3-Touch-LCD-1.85"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32S3_Touch_LCD_1_46
+ bool "Waveshare ESP32-S3-Touch-LCD-1.46"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32C6_LCD_1_69
+ bool "Waveshare ESP32-C6-LCD-1.69"
+ depends on IDF_TARGET_ESP32C6
+ config BOARD_TYPE_ESP32C6_Touch_AMOLED_1_43
+ bool "Waveshare ESP32-C6-Touch-AMOLOED-1.43"
+ depends on IDF_TARGET_ESP32C6
+ config BOARD_TYPE_ESP32S3_Touch_LCD_3_5
+ bool "Waveshare ESP32-S3-Touch-LCD-3.5"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32S3_Touch_LCD_3_5B
+ bool "Waveshare ESP32-S3-Touch-LCD-3.5B"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32P4_NANO
+ bool "Waveshare ESP32-P4-NANO"
+ depends on IDF_TARGET_ESP32P4
+ config BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B
+ bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-4B"
+ depends on IDF_TARGET_ESP32P4
+ config BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC
+ bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-3.4C or ESP32-P4-WIFI6-Touch-LCD-4C"
+ depends on IDF_TARGET_ESP32P4
+ config BOARD_TYPE_TUDOUZI
+ bool "土豆子"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_LILYGO_T_CIRCLE_S3
+ bool "LILYGO T-Circle-S3"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_0_V1_1
+ bool "LILYGO T-CameraPlus-S3_V1_0_V1_1"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_LILYGO_T_CAMERAPLUS_S3_V1_2
+ bool "LILYGO T-CameraPlus-S3_V1_2"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA
+ bool "LILYGO T-Display-S3-Pro-MVSRLora"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_LILYGO_T_DISPLAY_S3_PRO_MVSRLORA_NO_BATTERY
+ bool "LILYGO T-Display-S3-Pro-MVSRLora_No_Battery"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_MOVECALL_MOJI_ESP32S3
+ bool "Movecall Moji 小智AI衍生版"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_MOVECALL_CUICAN_ESP32S3
+ bool "Movecall CuiCan 璀璨·AI吊坠"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATK_DNESP32S3
+ bool "正点原子DNESP32S3开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATK_DNESP32S3_BOX
+ bool "正点原子DNESP32S3-BOX"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATK_DNESP32S3_BOX0
+ bool "正点原子DNESP32S3-BOX0"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATK_DNESP32S3_BOX2_WIFI
+ bool "正点原子DNESP32S3-BOX2-WIFI"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATK_DNESP32S3_BOX2_4G
+ bool "正点原子DNESP32S3-BOX2-4G"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATK_DNESP32S3M_WIFI
+ bool "正点原子DNESP32S3M-WIFI"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ATK_DNESP32S3M_4G
+ bool "正点原子DNESP32S3M-4G"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_DU_CHATX
+ bool "嘟嘟开发板CHATX(wifi)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32S3_Taiji_Pi
+ bool "太极小派esp32s3"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_XINGZHI_Cube_0_85TFT_WIFI
+ bool "无名科技星智0.85(WIFI)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_XINGZHI_Cube_0_85TFT_ML307
+ bool "无名科技星智0.85(ML307)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_XINGZHI_Cube_0_96OLED_WIFI
+ bool "无名科技星智0.96(WIFI)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_XINGZHI_Cube_0_96OLED_ML307
+ bool "无名科技星智0.96(ML307)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_XINGZHI_Cube_1_54TFT_WIFI
+ bool "无名科技星智1.54(WIFI)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_XINGZHI_Cube_1_54TFT_ML307
+ bool "无名科技星智1.54(ML307)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_SENSECAP_WATCHER
+ bool "SenseCAP Watcher"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_DOIT_S3_AIBOX
+ bool "四博智联AI陪伴盒子"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_MIXGO_NOVA
+ bool "元控·青春"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_GENJUTECH_S3_1_54TFT
+ bool "亘具科技1.54(s3)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_S3_LCD_EV_Board
+ bool "乐鑫ESP S3 LCD EV Board开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP_S3_LCD_EV_Board_2
+ bool "乐鑫ESP S3 LCD EV Board 2开发板"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_shhk_CAM
+ bool "幻鲲科技AI Camera"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ZHENGCHEN_1_54TFT_WIFI
+ bool "征辰科技1.54(WIFI)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ZHENGCHEN_1_54TFT_ML307
+ bool "征辰科技1.54(ML307)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ZHENGCHEN_CAM_ML307
+ bool "征辰科技AI Camera(ML307)"
+ config BOARD_TYPE_MINSI_K08_DUAL
+ bool "敏思科技K08(DUAL)"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32_S3_1_54_MUMA
+ bool "Spotpear ESP32-S3-1.54-MUMA"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_ESP32_S3_1_28_BOX
+ bool "Spotpear ESP32-S3-1.28-BOX"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_OTTO_ROBOT
+ bool "ottoRobot"
+ depends on IDF_TARGET_ESP32S3
+ select LV_USE_GIF
+ select LV_GIF_CACHE_DECODE_DATA
+ config BOARD_TYPE_ELECTRON_BOT
+ bool "electronBot"
+ depends on IDF_TARGET_ESP32S3
+ select LV_USE_GIF
+ select LV_GIF_CACHE_DECODE_DATA
+ config BOARD_TYPE_JIUCHUAN
+ bool "九川智能"
+ config BOARD_TYPE_LABPLUS_MPYTHON_V3
+ bool "labplus mpython_v3 board"
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_LABPLUS_LEDONG_V2
+ bool "labplus ledong_v2 board"
+ depends on IDF_TARGET_ESP32S3
+ depends on IDF_TARGET_ESP32S3
+ config BOARD_TYPE_SURFER_C3_1_14TFT
+ bool "Surfer-C3-1-14TFT"
+ depends on IDF_TARGET_ESP32C3
+ config BOARD_TYPE_YUNLIAO_S3
+ bool "小智云聊-S3"
+ depends on IDF_TARGET_ESP32S3
+endchoice
+
+choice ESP_S3_LCD_EV_Board_Version_TYPE
+ depends on BOARD_TYPE_ESP_S3_LCD_EV_Board
+ prompt "EV_BOARD Type"
+ default ESP_S3_LCD_EV_Board_1p4
+ help
+ 开发板硬件版本型号选择
+ config ESP_S3_LCD_EV_Board_1p4
+ bool "乐鑫ESP32_S3_LCD_EV_Board-MB_V1.4"
+ config ESP_S3_LCD_EV_Board_1p5
+ bool "乐鑫ESP32_S3_LCD_EV_Board-MB_V1.5"
+endchoice
+
+choice DISPLAY_OLED_TYPE
+ depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307 || BOARD_TYPE_BREAD_COMPACT_ESP32
+ prompt "OLED Type"
+ default OLED_SSD1306_128X32
+ help
+ OLED 屏幕类型选择
+ config OLED_SSD1306_128X32
+ bool "SSD1306, 分辨率128*32"
+ config OLED_SSD1306_128X64
+ bool "SSD1306, 分辨率128*64"
+ config OLED_SH1106_128X64
+ bool "SH1106, 分辨率128*64"
+endchoice
+
+choice DISPLAY_LCD_TYPE
+ depends on BOARD_TYPE_BREAD_COMPACT_WIFI_LCD || BOARD_TYPE_BREAD_COMPACT_ESP32_LCD || BOARD_TYPE_ESP32_CGC || BOARD_TYPE_ESP32P4_NANO || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_BREAD_COMPACT_WIFI_CAM
+ prompt "LCD Type"
+ default LCD_ST7789_240X320
+ help
+ 屏幕类型选择
+ config LCD_ST7789_240X320
+ bool "ST7789, 分辨率240*320, IPS"
+ config LCD_ST7789_240X320_NO_IPS
+ bool "ST7789, 分辨率240*320, 非IPS"
+ config LCD_ST7789_170X320
+ bool "ST7789, 分辨率170*320"
+ config LCD_ST7789_172X320
+ bool "ST7789, 分辨率172*320"
+ config LCD_ST7789_240X280
+ bool "ST7789, 分辨率240*280"
+ config LCD_ST7789_240X240
+ bool "ST7789, 分辨率240*240"
+ config LCD_ST7789_240X240_7PIN
+ bool "ST7789, 分辨率240*240, 7PIN"
+ config LCD_ST7789_240X135
+ bool "ST7789, 分辨率240*135"
+ config LCD_ST7735_128X160
+ bool "ST7735, 分辨率128*160"
+ config LCD_ST7735_128X128
+ bool "ST7735, 分辨率128*128"
+ config LCD_ST7796_320X480
+ bool "ST7796, 分辨率320*480 IPS"
+ config LCD_ST7796_320X480_NO_IPS
+ bool "ST7796, 分辨率320*480, 非IPS"
+ config LCD_ILI9341_240X320
+ bool "ILI9341, 分辨率240*320"
+ config LCD_ILI9341_240X320_NO_IPS
+ bool "ILI9341, 分辨率240*320, 非IPS"
+ config LCD_GC9A01_240X240
+ bool "GC9A01, 分辨率240*240, 圆屏"
+ config LCD_TYPE_800_1280_10_1_INCH
+ bool "Waveshare 101M-8001280-IPS-CT-K Display"
+ config LCD_TYPE_800_1280_10_1_INCH_A
+ bool "Waveshare 10.1-DSI-TOUCH-A Display"
+ config LCD_TYPE_800_800_3_4_INCH
+ bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-3.4C with 800*800 3.4inch round display"
+ config LCD_TYPE_720_720_4_INCH
+ bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-4C with 720*720 4inch round display"
+ config LCD_CUSTOM
+ bool "自定义屏幕参数"
+endchoice
+
+choice DISPLAY_ESP32S3_KORVO2_V3
+ depends on BOARD_TYPE_ESP32S3_KORVO2_V3
+ prompt "ESP32S3_KORVO2_V3 LCD Type"
+ default ESP32S3_KORVO2_V3_LCD_ST7789
+ help
+ 屏幕类型选择
+ config ESP32S3_KORVO2_V3_LCD_ST7789
+ bool "ST7789, 分辨率240*280"
+ config ESP32S3_KORVO2_V3_LCD_ILI9341
+ bool "ILI9341, 分辨率240*320"
+endchoice
+
+choice DISPLAY_ESP32S3_AUDIO_BOARD
+ depends on BOARD_TYPE_ESP32S3_AUDIO_BOARD
+ prompt "ESP32S3_AUDIO_BOARD LCD Type"
+ default AUDIO_BOARD_LCD_JD9853
+ help
+ 屏幕类型选择
+ config AUDIO_BOARD_LCD_JD9853
+ bool "JD9853, 分辨率320*172"
+ config AUDIO_BOARD_LCD_ST7789
+ bool "ST7789, 分辨率240*320"
+endchoice
+
+config USE_WECHAT_MESSAGE_STYLE
+ bool "Enable WeChat Message Style"
+ default n
+ help
+ 使用微信聊天界面风格
+
+config USE_ESP_WAKE_WORD
+ bool "Enable Wake Word Detection (without AFE)"
+ default n
+ depends on IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C5 || IDF_TARGET_ESP32C6 || (IDF_TARGET_ESP32 && SPIRAM)
+ help
+ 支持 ESP32 C3、ESP32 C5 与 ESP32 C6,增加ESP32支持(需要开启PSRAM)
+
+config USE_AFE_WAKE_WORD
+ bool "Enable Wake Word Detection (AFE)"
+ default y
+ depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
+ help
+ 需要 ESP32 S3 与 PSRAM 支持
+
+config USE_CUSTOM_WAKE_WORD
+ bool "Enable Custom Wake Word Detection"
+ default n
+ depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM && (!USE_AFE_WAKE_WORD)
+ help
+ 需要 ESP32 S3 与 PSRAM 支持
+
+config CUSTOM_WAKE_WORD
+ string "Custom Wake Word"
+ default "xiao tu dou"
+ depends on USE_CUSTOM_WAKE_WORD
+ help
+ 自定义唤醒词,中文用拼音表示,每个字之间用空格隔开
+
+config CUSTOM_WAKE_WORD_DISPLAY
+ string "Custom Wake Word Display"
+ default "小土豆"
+ depends on USE_CUSTOM_WAKE_WORD
+ help
+ 唤醒后发送给服务器的问候语
+
+config CUSTOM_WAKE_WORD_THRESHOLD
+ int "Custom Wake Word Threshold (%)"
+ default 20
+ range 1 99
+ depends on USE_CUSTOM_WAKE_WORD
+ help
+ 自定义唤醒词阈值,范围1-99,越小越敏感,默认10
+
+config USE_AUDIO_PROCESSOR
+ bool "Enable Audio Noise Reduction"
+ default y
+ depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
+ help
+ 需要 ESP32 S3 与 PSRAM 支持
+
+config USE_DEVICE_AEC
+ bool "Enable Device-Side AEC"
+ default n
+ depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 || BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3 || BOARD_TYPE_ZHENGCHEN_CAM || BOARD_TYPE_ZHENGCHEN_CAM_ML307)
+ help
+ 因为性能不够,不建议和微信聊天界面风格同时开启
+
+config USE_SERVER_AEC
+ bool "Enable Server-Side AEC (Unstable)"
+ default n
+ depends on USE_AUDIO_PROCESSOR
+ help
+ 启用服务器端 AEC,需要服务器支持
+
+config USE_AUDIO_DEBUGGER
+ bool "Enable Audio Debugger"
+ default n
+ help
+ 启用音频调试功能,通过UDP发送音频数据
+
+config USE_ACOUSTIC_WIFI_PROVISIONING
+ bool "Enable Acoustic WiFi Provisioning"
+ default n
+ help
+ 启用声波配网功能,使用音频信号传输 WiFi 配置数据
+
+config AUDIO_DEBUG_UDP_SERVER
+ string "Audio Debug UDP Server Address"
+ default "192.168.2.100:8000"
+ depends on USE_AUDIO_DEBUGGER
+ help
+ UDP服务器地址,格式: IP:PORT,用于接收音频调试数据
+
+config RECEIVE_CUSTOM_MESSAGE
+ bool "Enable Custom Message Reception"
+ default n
+ help
+ 启用接收自定义消息功能,允许设备接收来自服务器的自定义消息(最好通过 MQTT 协议)
+
+choice I2S_TYPE_TAIJIPI_S3
+ depends on BOARD_TYPE_ESP32S3_Taiji_Pi
+ prompt "taiji-pi-S3 I2S Type"
+ default TAIJIPAI_I2S_TYPE_STD
+ help
+ I2S 类型选择
+ config TAIJIPAI_I2S_TYPE_STD
+ bool "I2S Type STD"
+ config TAIJIPAI_I2S_TYPE_PDM
+ bool "I2S Type PDM"
+endchoice
+
+endmenu
diff --git a/main/application.cc b/main/application.cc
new file mode 100644
index 0000000..6373c44
--- /dev/null
+++ b/main/application.cc
@@ -0,0 +1,883 @@
+#include "application.h"
+#include "board.h"
+#include "display.h"
+#include "system_info.h"
+#include "audio_codec.h"
+#include "mqtt_protocol.h"
+#include "websocket_protocol.h"
+#include "assets/lang_config.h"
+#include "mcp_server.h"
+#include "assets.h"
+#include "settings.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define TAG "Application"
+
+
+static const char* const STATE_STRINGS[] = {
+ "unknown",
+ "starting",
+ "configuring",
+ "idle",
+ "connecting",
+ "listening",
+ "speaking",
+ "upgrading",
+ "activating",
+ "audio_testing",
+ "fatal_error",
+ "invalid_state"
+};
+
+Application::Application() {
+ event_group_ = xEventGroupCreate();
+
+#if CONFIG_USE_DEVICE_AEC && CONFIG_USE_SERVER_AEC
+#error "CONFIG_USE_DEVICE_AEC and CONFIG_USE_SERVER_AEC cannot be enabled at the same time"
+#elif CONFIG_USE_DEVICE_AEC
+ aec_mode_ = kAecOnDeviceSide;
+#elif CONFIG_USE_SERVER_AEC
+ aec_mode_ = kAecOnServerSide;
+#else
+ aec_mode_ = kAecOff;
+#endif
+
+ esp_timer_create_args_t clock_timer_args = {
+ .callback = [](void* arg) {
+ Application* app = (Application*)arg;
+ xEventGroupSetBits(app->event_group_, MAIN_EVENT_CLOCK_TICK);
+ },
+ .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_);
+ }
+ vEventGroupDelete(event_group_);
+}
+
+void Application::CheckAssetsVersion() {
+ auto& board = Board::GetInstance();
+ auto display = board.GetDisplay();
+ auto assets = board.GetAssets();
+ if (!assets) {
+ ESP_LOGE(TAG, "Assets is not set for board %s", BOARD_NAME);
+ return;
+ }
+
+ if (!assets->partition_valid()) {
+ ESP_LOGE(TAG, "Assets partition is not valid for board %s", BOARD_NAME);
+ return;
+ }
+
+ Settings settings("assets", true);
+ // Check if there is a new assets need to be downloaded
+ std::string download_url = settings.GetString("download_url");
+ if (!download_url.empty()) {
+ settings.EraseKey("download_url");
+ }
+ if (download_url.empty() && !assets->checksum_valid()) {
+ download_url = assets->default_assets_url();
+ }
+
+ if (!download_url.empty()) {
+ char message[256];
+ snprintf(message, sizeof(message), Lang::Strings::FOUND_NEW_ASSETS, download_url.c_str());
+ Alert(Lang::Strings::LOADING_ASSETS, message, "cloud_arrow_down", Lang::Sounds::OGG_UPGRADE);
+
+ // Wait for the audio service to be idle for 3 seconds
+ vTaskDelay(pdMS_TO_TICKS(3000));
+ SetDeviceState(kDeviceStateUpgrading);
+ board.SetPowerSaveMode(false);
+ display->SetChatMessage("system", Lang::Strings::PLEASE_WAIT);
+
+ bool success = assets->Download(download_url, [display](int progress, size_t speed) -> void {
+ std::thread([display, progress, speed]() {
+ char buffer[32];
+ snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
+ display->SetChatMessage("system", buffer);
+ }).detach();
+ });
+
+ board.SetPowerSaveMode(true);
+ vTaskDelay(pdMS_TO_TICKS(1000));
+
+ if (!success) {
+ Alert(Lang::Strings::ERROR, Lang::Strings::DOWNLOAD_ASSETS_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION);
+ vTaskDelay(pdMS_TO_TICKS(2000));
+ return;
+ }
+ }
+
+ // Apply assets
+ assets->Apply();
+ display->SetChatMessage("system", "");
+ display->SetEmotion("microchip_ai");
+}
+
+void Application::CheckNewVersion(Ota& ota) {
+ const int MAX_RETRY = 10;
+ int retry_count = 0;
+ int retry_delay = 10; // 初始重试延迟为10秒
+
+ auto& board = Board::GetInstance();
+ while (true) {
+ SetDeviceState(kDeviceStateActivating);
+ auto display = board.GetDisplay();
+ display->SetStatus(Lang::Strings::CHECKING_NEW_VERSION);
+
+ if (!ota.CheckVersion()) {
+ retry_count++;
+ if (retry_count >= MAX_RETRY) {
+ ESP_LOGE(TAG, "Too many retries, exit version check");
+ return;
+ }
+
+ char buffer[256];
+ snprintf(buffer, sizeof(buffer), Lang::Strings::CHECK_NEW_VERSION_FAILED, retry_delay, ota.GetCheckVersionUrl().c_str());
+ Alert(Lang::Strings::ERROR, buffer, "cloud_slash", Lang::Sounds::OGG_EXCLAMATION);
+
+ ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", retry_delay, retry_count, MAX_RETRY);
+ for (int i = 0; i < retry_delay; i++) {
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ if (device_state_ == kDeviceStateIdle) {
+ break;
+ }
+ }
+ retry_delay *= 2; // 每次重试后延迟时间翻倍
+ continue;
+ }
+ retry_count = 0;
+ retry_delay = 10; // 重置重试延迟时间
+
+ if (ota.HasNewVersion()) {
+ if (UpgradeFirmware(ota)) {
+ return; // This line will never be reached after reboot
+ }
+ // If upgrade failed, continue to normal operation (don't break, just fall through)
+ }
+
+ // No new version, mark the current version as valid
+ ota.MarkCurrentVersionValid();
+ if (!ota.HasActivationCode() && !ota.HasActivationChallenge()) {
+ xEventGroupSetBits(event_group_, MAIN_EVENT_CHECK_NEW_VERSION_DONE);
+ // Exit the loop if done checking new version
+ break;
+ }
+
+ display->SetStatus(Lang::Strings::ACTIVATION);
+ // Activation code is shown to the user and waiting for the user to input
+ if (ota.HasActivationCode()) {
+ ShowActivationCode(ota.GetActivationCode(), ota.GetActivationMessage());
+ }
+
+ // This will block the loop until the activation is done or timeout
+ for (int i = 0; i < 10; ++i) {
+ ESP_LOGI(TAG, "Activating... %d/%d", i + 1, 10);
+ esp_err_t err = ota.Activate();
+ if (err == ESP_OK) {
+ xEventGroupSetBits(event_group_, MAIN_EVENT_CHECK_NEW_VERSION_DONE);
+ break;
+ } else if (err == ESP_ERR_TIMEOUT) {
+ vTaskDelay(pdMS_TO_TICKS(3000));
+ } else {
+ vTaskDelay(pdMS_TO_TICKS(10000));
+ }
+ if (device_state_ == kDeviceStateIdle) {
+ break;
+ }
+ }
+ }
+}
+
+void Application::ShowActivationCode(const std::string& code, const std::string& message) {
+ struct digit_sound {
+ char digit;
+ const std::string_view& sound;
+ };
+ static const std::array digit_sounds{{
+ digit_sound{'0', Lang::Sounds::OGG_0},
+ digit_sound{'1', Lang::Sounds::OGG_1},
+ digit_sound{'2', Lang::Sounds::OGG_2},
+ digit_sound{'3', Lang::Sounds::OGG_3},
+ digit_sound{'4', Lang::Sounds::OGG_4},
+ digit_sound{'5', Lang::Sounds::OGG_5},
+ digit_sound{'6', Lang::Sounds::OGG_6},
+ digit_sound{'7', Lang::Sounds::OGG_7},
+ digit_sound{'8', Lang::Sounds::OGG_8},
+ digit_sound{'9', Lang::Sounds::OGG_9}
+ }};
+
+ // This sentence uses 9KB of SRAM, so we need to wait for it to finish
+ Alert(Lang::Strings::ACTIVATION, message.c_str(), "link", Lang::Sounds::OGG_ACTIVATION);
+
+ for (const auto& digit : code) {
+ auto it = std::find_if(digit_sounds.begin(), digit_sounds.end(),
+ [digit](const digit_sound& ds) { return ds.digit == digit; });
+ if (it != digit_sounds.end()) {
+ audio_service_.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", emotion, status, message);
+ auto display = Board::GetInstance().GetDisplay();
+ display->SetStatus(status);
+ display->SetEmotion(emotion);
+ display->SetChatMessage("system", message);
+ if (!sound.empty()) {
+ audio_service_.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::ToggleChatState() {
+ if (device_state_ == kDeviceStateActivating) {
+ SetDeviceState(kDeviceStateIdle);
+ return;
+ } else if (device_state_ == kDeviceStateWifiConfiguring) {
+ audio_service_.EnableAudioTesting(true);
+ SetDeviceState(kDeviceStateAudioTesting);
+ return;
+ } else if (device_state_ == kDeviceStateAudioTesting) {
+ audio_service_.EnableAudioTesting(false);
+ SetDeviceState(kDeviceStateWifiConfiguring);
+ return;
+ }
+
+ if (!protocol_) {
+ ESP_LOGE(TAG, "Protocol not initialized");
+ return;
+ }
+
+ if (device_state_ == kDeviceStateIdle) {
+ Schedule([this]() {
+ if (!protocol_->IsAudioChannelOpened()) {
+ SetDeviceState(kDeviceStateConnecting);
+ if (!protocol_->OpenAudioChannel()) {
+ return;
+ }
+ }
+
+ SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
+ });
+ } 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;
+ } else if (device_state_ == kDeviceStateWifiConfiguring) {
+ audio_service_.EnableAudioTesting(true);
+ SetDeviceState(kDeviceStateAudioTesting);
+ return;
+ }
+
+ if (!protocol_) {
+ ESP_LOGE(TAG, "Protocol not initialized");
+ return;
+ }
+
+ if (device_state_ == kDeviceStateIdle) {
+ Schedule([this]() {
+ if (!protocol_->IsAudioChannelOpened()) {
+ SetDeviceState(kDeviceStateConnecting);
+ if (!protocol_->OpenAudioChannel()) {
+ return;
+ }
+ }
+
+ SetListeningMode(kListeningModeManualStop);
+ });
+ } else if (device_state_ == kDeviceStateSpeaking) {
+ Schedule([this]() {
+ AbortSpeaking(kAbortReasonNone);
+ SetListeningMode(kListeningModeManualStop);
+ });
+ }
+}
+
+void Application::StopListening() {
+ if (device_state_ == kDeviceStateAudioTesting) {
+ audio_service_.EnableAudioTesting(false);
+ SetDeviceState(kDeviceStateWifiConfiguring);
+ return;
+ }
+
+ const std::array valid_states = {
+ kDeviceStateListening,
+ kDeviceStateSpeaking,
+ kDeviceStateIdle,
+ };
+ // If not valid, do nothing
+ if (std::find(valid_states.begin(), valid_states.end(), device_state_) == valid_states.end()) {
+ return;
+ }
+
+ Schedule([this]() {
+ if (device_state_ == kDeviceStateListening) {
+ protocol_->SendStopListening();
+ SetDeviceState(kDeviceStateIdle);
+ }
+ });
+}
+
+void Application::Start() {
+ auto& board = Board::GetInstance();
+ SetDeviceState(kDeviceStateStarting);
+
+ /* Setup the display */
+ auto display = board.GetDisplay();
+
+ /* Setup the audio service */
+ auto codec = board.GetAudioCodec();
+ audio_service_.Initialize(codec);
+ audio_service_.Start();
+
+ AudioServiceCallbacks callbacks;
+ callbacks.on_send_queue_available = [this]() {
+ xEventGroupSetBits(event_group_, MAIN_EVENT_SEND_AUDIO);
+ };
+ callbacks.on_wake_word_detected = [this](const std::string& wake_word) {
+ xEventGroupSetBits(event_group_, MAIN_EVENT_WAKE_WORD_DETECTED);
+ };
+ callbacks.on_vad_change = [this](bool speaking) {
+ xEventGroupSetBits(event_group_, MAIN_EVENT_VAD_CHANGE);
+ };
+ audio_service_.SetCallbacks(callbacks);
+
+ /* Start the clock timer to update the status bar */
+ esp_timer_start_periodic(clock_timer_handle_, 1000000);
+
+ /* Wait for the network to be ready */
+ board.StartNetwork();
+
+ // Update the status bar immediately to show the network state
+ display->UpdateStatusBar(true);
+
+ // Check for new assets version
+ CheckAssetsVersion();
+
+ // Check for new firmware version or get the MQTT broker address
+ Ota ota;
+ CheckNewVersion(ota);
+
+ // Initialize the protocol
+ display->SetStatus(Lang::Strings::LOADING_PROTOCOL);
+
+ // Add MCP common tools before initializing the protocol
+ auto& mcp_server = McpServer::GetInstance();
+ mcp_server.AddCommonTools();
+ mcp_server.AddUserOnlyTools();
+
+ if (ota.HasMqttConfig()) {
+ protocol_ = std::make_unique();
+ } else if (ota.HasWebsocketConfig()) {
+ protocol_ = std::make_unique();
+ } else {
+ ESP_LOGW(TAG, "No protocol specified in the OTA config, using MQTT");
+ protocol_ = std::make_unique();
+ }
+
+ protocol_->OnConnected([this]() {
+ DismissAlert();
+ });
+
+ protocol_->OnNetworkError([this](const std::string& message) {
+ last_error_message_ = message;
+ xEventGroupSetBits(event_group_, MAIN_EVENT_ERROR);
+ });
+ protocol_->OnIncomingAudio([this](std::unique_ptr packet) {
+ if (device_state_ == kDeviceStateSpeaking) {
+ audio_service_.PushPacketToDecodeQueue(std::move(packet));
+ }
+ });
+ protocol_->OnAudioChannelOpened([this, codec, &board]() {
+ board.SetPowerSaveMode(false);
+ if (protocol_->server_sample_rate() != codec->output_sample_rate()) {
+ ESP_LOGW(TAG, "Server sample rate %d does not match device output sample rate %d, resampling may cause distortion",
+ protocol_->server_sample_rate(), codec->output_sample_rate());
+ }
+ });
+ 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) {
+ if (listening_mode_ == kListeningModeManualStop) {
+ SetDeviceState(kDeviceStateIdle);
+ } else {
+ SetDeviceState(kDeviceStateListening);
+ }
+ }
+ });
+ } else if (strcmp(state->valuestring, "sentence_start") == 0) {
+ auto text = cJSON_GetObjectItem(root, "text");
+ if (cJSON_IsString(text)) {
+ ESP_LOGI(TAG, "<< %s", text->valuestring);
+ Schedule([this, display, message = std::string(text->valuestring)]() {
+ display->SetChatMessage("assistant", message.c_str());
+ });
+ }
+ }
+ } else if (strcmp(type->valuestring, "stt") == 0) {
+ auto text = cJSON_GetObjectItem(root, "text");
+ if (cJSON_IsString(text)) {
+ 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 (cJSON_IsString(emotion)) {
+ Schedule([this, display, emotion_str = std::string(emotion->valuestring)]() {
+ display->SetEmotion(emotion_str.c_str());
+ });
+ }
+ } else if (strcmp(type->valuestring, "mcp") == 0) {
+ auto payload = cJSON_GetObjectItem(root, "payload");
+ if (cJSON_IsObject(payload)) {
+ McpServer::GetInstance().ParseMessage(payload);
+ }
+ } else if (strcmp(type->valuestring, "system") == 0) {
+ auto command = cJSON_GetObjectItem(root, "command");
+ if (cJSON_IsString(command)) {
+ ESP_LOGI(TAG, "System command: %s", command->valuestring);
+ if (strcmp(command->valuestring, "reboot") == 0) {
+ // Do a reboot if user requests a OTA update
+ Schedule([this]() {
+ Reboot();
+ });
+ } else {
+ ESP_LOGW(TAG, "Unknown system command: %s", command->valuestring);
+ }
+ }
+ } else if (strcmp(type->valuestring, "alert") == 0) {
+ auto status = cJSON_GetObjectItem(root, "status");
+ auto message = cJSON_GetObjectItem(root, "message");
+ auto emotion = cJSON_GetObjectItem(root, "emotion");
+ if (cJSON_IsString(status) && cJSON_IsString(message) && cJSON_IsString(emotion)) {
+ Alert(status->valuestring, message->valuestring, emotion->valuestring, Lang::Sounds::OGG_VIBRATION);
+ } else {
+ ESP_LOGW(TAG, "Alert command requires status, message and emotion");
+ }
+#if CONFIG_RECEIVE_CUSTOM_MESSAGE
+ } else if (strcmp(type->valuestring, "custom") == 0) {
+ auto payload = cJSON_GetObjectItem(root, "payload");
+ ESP_LOGI(TAG, "Received custom message: %s", cJSON_PrintUnformatted(root));
+ if (cJSON_IsObject(payload)) {
+ Schedule([this, display, payload_str = std::string(cJSON_PrintUnformatted(payload))]() {
+ display->SetChatMessage("system", payload_str.c_str());
+ });
+ } else {
+ ESP_LOGW(TAG, "Invalid custom message format: missing payload");
+ }
+#endif
+ } else {
+ ESP_LOGW(TAG, "Unknown message type: %s", type->valuestring);
+ }
+ });
+ bool protocol_started = protocol_->Start();
+
+ SystemInfo::PrintHeapStats();
+ SetDeviceState(kDeviceStateIdle);
+
+ has_server_time_ = ota.HasServerTime();
+ if (protocol_started) {
+ std::string message = std::string(Lang::Strings::VERSION) + ota.GetCurrentVersion();
+ display->ShowNotification(message.c_str());
+ display->SetChatMessage("system", "");
+ // Play the success sound to indicate the device is ready
+ audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS);
+ }
+
+ // Start the main event loop task with priority 3
+ xTaskCreate([](void* arg) {
+ ((Application*)arg)->MainEventLoop();
+ vTaskDelete(NULL);
+ }, "main_event_loop", 2048 * 4, this, 3, &main_event_loop_task_handle_);
+}
+
+// Add a async task to MainLoop
+void Application::Schedule(std::function callback) {
+ {
+ std::lock_guard lock(mutex_);
+ main_tasks_.push_back(std::move(callback));
+ }
+ xEventGroupSetBits(event_group_, MAIN_EVENT_SCHEDULE);
+}
+
+// The Main Event Loop controls the chat state and websocket connection
+// If other tasks need to access the websocket or chat state,
+// they should use Schedule to call this function
+void Application::MainEventLoop() {
+ while (true) {
+ auto bits = xEventGroupWaitBits(event_group_, MAIN_EVENT_SCHEDULE |
+ MAIN_EVENT_SEND_AUDIO |
+ MAIN_EVENT_WAKE_WORD_DETECTED |
+ MAIN_EVENT_VAD_CHANGE |
+ MAIN_EVENT_CLOCK_TICK |
+ MAIN_EVENT_ERROR, pdTRUE, pdFALSE, portMAX_DELAY);
+
+ if (bits & MAIN_EVENT_ERROR) {
+ SetDeviceState(kDeviceStateIdle);
+ Alert(Lang::Strings::ERROR, last_error_message_.c_str(), "circle_xmark", Lang::Sounds::OGG_EXCLAMATION);
+ }
+
+ if (bits & MAIN_EVENT_SEND_AUDIO) {
+ while (auto packet = audio_service_.PopPacketFromSendQueue()) {
+ if (protocol_ && !protocol_->SendAudio(std::move(packet))) {
+ break;
+ }
+ }
+ }
+
+ if (bits & MAIN_EVENT_WAKE_WORD_DETECTED) {
+ OnWakeWordDetected();
+ }
+
+ if (bits & MAIN_EVENT_VAD_CHANGE) {
+ if (device_state_ == kDeviceStateListening) {
+ auto led = Board::GetInstance().GetLed();
+ led->OnStateChanged();
+ }
+ }
+
+ if (bits & MAIN_EVENT_SCHEDULE) {
+ std::unique_lock lock(mutex_);
+ auto tasks = std::move(main_tasks_);
+ lock.unlock();
+ for (auto& task : tasks) {
+ task();
+ }
+ }
+
+ if (bits & MAIN_EVENT_CLOCK_TICK) {
+ clock_ticks_++;
+ auto display = Board::GetInstance().GetDisplay();
+ display->UpdateStatusBar();
+
+ // Print the debug info every 10 seconds
+ if (clock_ticks_ % 10 == 0) {
+ // SystemInfo::PrintTaskCpuUsage(pdMS_TO_TICKS(1000));
+ // SystemInfo::PrintTaskList();
+ SystemInfo::PrintHeapStats();
+ }
+ }
+ }
+}
+
+void Application::OnWakeWordDetected() {
+ if (!protocol_) {
+ return;
+ }
+
+ if (device_state_ == kDeviceStateIdle) {
+ audio_service_.EncodeWakeWord();
+
+ if (!protocol_->IsAudioChannelOpened()) {
+ SetDeviceState(kDeviceStateConnecting);
+ if (!protocol_->OpenAudioChannel()) {
+ audio_service_.EnableWakeWordDetection(true);
+ return;
+ }
+ }
+
+ auto wake_word = audio_service_.GetLastWakeWord();
+ ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
+#if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD
+ // Encode and send the wake word data to the server
+ while (auto packet = audio_service_.PopWakeWordPacket()) {
+ protocol_->SendAudio(std::move(packet));
+ }
+ // Set the chat state to wake word detected
+ protocol_->SendWakeWordDetected(wake_word);
+ SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
+#else
+ SetListeningMode(aec_mode_ == kAecOff ? kListeningModeAutoStop : kListeningModeRealtime);
+ // Play the pop up sound to indicate the wake word is detected
+ audio_service_.PlaySound(Lang::Sounds::OGG_POPUP);
+#endif
+ } else if (device_state_ == kDeviceStateSpeaking) {
+ AbortSpeaking(kAbortReasonWakeWordDetected);
+ } else if (device_state_ == kDeviceStateActivating) {
+ SetDeviceState(kDeviceStateIdle);
+ }
+}
+
+void Application::AbortSpeaking(AbortReason reason) {
+ ESP_LOGI(TAG, "Abort speaking");
+ aborted_ = true;
+ if (protocol_) {
+ protocol_->SendAbortSpeaking(reason);
+ }
+}
+
+void Application::SetListeningMode(ListeningMode mode) {
+ listening_mode_ = mode;
+ SetDeviceState(kDeviceStateListening);
+}
+
+void Application::SetDeviceState(DeviceState state) {
+ if (device_state_ == state) {
+ return;
+ }
+
+ clock_ticks_ = 0;
+ auto previous_state = device_state_;
+ device_state_ = state;
+ ESP_LOGI(TAG, "STATE: %s", STATE_STRINGS[device_state_]);
+
+ // Send the state change event
+ DeviceStateEventManager::GetInstance().PostStateChangeEvent(previous_state, state);
+
+ auto& board = Board::GetInstance();
+ auto display = board.GetDisplay();
+ auto led = board.GetLed();
+ led->OnStateChanged();
+ switch (state) {
+ case kDeviceStateUnknown:
+ case kDeviceStateIdle:
+ display->SetStatus(Lang::Strings::STANDBY);
+ display->SetEmotion("neutral");
+ audio_service_.EnableVoiceProcessing(false);
+ audio_service_.EnableWakeWordDetection(true);
+ 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");
+
+ // Make sure the audio processor is running
+ if (!audio_service_.IsAudioProcessorRunning()) {
+ // Send the start listening command
+ protocol_->SendStartListening(listening_mode_);
+ audio_service_.EnableVoiceProcessing(true);
+ audio_service_.EnableWakeWordDetection(false);
+ }
+ break;
+ case kDeviceStateSpeaking:
+ display->SetStatus(Lang::Strings::SPEAKING);
+
+ if (listening_mode_ != kListeningModeRealtime) {
+ audio_service_.EnableVoiceProcessing(false);
+ // Only AFE wake word can be detected in speaking mode
+#if CONFIG_USE_AFE_WAKE_WORD
+ audio_service_.EnableWakeWordDetection(true);
+#else
+ audio_service_.EnableWakeWordDetection(false);
+#endif
+ }
+ audio_service_.ResetDecoder();
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+}
+
+void Application::Reboot() {
+ ESP_LOGI(TAG, "Rebooting...");
+ // Disconnect the audio channel
+ if (protocol_ && protocol_->IsAudioChannelOpened()) {
+ protocol_->CloseAudioChannel();
+ }
+ protocol_.reset();
+ audio_service_.Stop();
+
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ esp_restart();
+}
+
+bool Application::UpgradeFirmware(Ota& ota, const std::string& url) {
+ auto& board = Board::GetInstance();
+ auto display = board.GetDisplay();
+
+ // Use provided URL or get from OTA object
+ std::string upgrade_url = url.empty() ? ota.GetFirmwareUrl() : url;
+ std::string version_info = url.empty() ? ota.GetFirmwareVersion() : "(Manual upgrade)";
+
+ // Close audio channel if it's open
+ if (protocol_ && protocol_->IsAudioChannelOpened()) {
+ ESP_LOGI(TAG, "Closing audio channel before firmware upgrade");
+ protocol_->CloseAudioChannel();
+ }
+ ESP_LOGI(TAG, "Starting firmware upgrade from URL: %s", upgrade_url.c_str());
+
+ Alert(Lang::Strings::OTA_UPGRADE, Lang::Strings::UPGRADING, "download", Lang::Sounds::OGG_UPGRADE);
+ vTaskDelay(pdMS_TO_TICKS(3000));
+
+ SetDeviceState(kDeviceStateUpgrading);
+
+ std::string message = std::string(Lang::Strings::NEW_VERSION) + version_info;
+ display->SetChatMessage("system", message.c_str());
+
+ board.SetPowerSaveMode(false);
+ audio_service_.Stop();
+ vTaskDelay(pdMS_TO_TICKS(1000));
+
+ bool upgrade_success = ota.StartUpgradeFromUrl(upgrade_url, [display](int progress, size_t speed) {
+ std::thread([display, progress, speed]() {
+ char buffer[32];
+ snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
+ display->SetChatMessage("system", buffer);
+ }).detach();
+ });
+
+ if (!upgrade_success) {
+ // Upgrade failed, restart audio service and continue running
+ ESP_LOGE(TAG, "Firmware upgrade failed, restarting audio service and continuing operation...");
+ audio_service_.Start(); // Restart audio service
+ board.SetPowerSaveMode(true); // Restore power save mode
+ Alert(Lang::Strings::ERROR, Lang::Strings::UPGRADE_FAILED, "circle_xmark", Lang::Sounds::OGG_EXCLAMATION);
+ vTaskDelay(pdMS_TO_TICKS(3000));
+ return false;
+ } else {
+ // Upgrade success, reboot immediately
+ ESP_LOGI(TAG, "Firmware upgrade successful, rebooting...");
+ display->SetChatMessage("system", "Upgrade successful, rebooting...");
+ vTaskDelay(pdMS_TO_TICKS(1000)); // Brief pause to show message
+ Reboot();
+ return true;
+ }
+}
+
+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;
+ }
+
+ if (!audio_service_.IsIdle()) {
+ return false;
+ }
+
+ // Now it is safe to enter sleep mode
+ return true;
+}
+
+void Application::SendMcpMessage(const std::string& payload) {
+ if (protocol_ == nullptr) {
+ return;
+ }
+
+ // Make sure you are using main thread to send MCP message
+ if (xTaskGetCurrentTaskHandle() == main_event_loop_task_handle_) {
+ ESP_LOGI(TAG, "Send MCP message in main thread");
+ protocol_->SendMcpMessage(payload);
+ } else {
+ ESP_LOGI(TAG, "Send MCP message in sub thread");
+ Schedule([this, payload = std::move(payload)]() {
+ protocol_->SendMcpMessage(payload);
+ });
+ }
+}
+
+void Application::SetAecMode(AecMode mode) {
+ aec_mode_ = mode;
+ Schedule([this]() {
+ auto& board = Board::GetInstance();
+ auto display = board.GetDisplay();
+ switch (aec_mode_) {
+ case kAecOff:
+ audio_service_.EnableDeviceAec(false);
+ display->ShowNotification(Lang::Strings::RTC_MODE_OFF);
+ break;
+ case kAecOnServerSide:
+ audio_service_.EnableDeviceAec(false);
+ display->ShowNotification(Lang::Strings::RTC_MODE_ON);
+ break;
+ case kAecOnDeviceSide:
+ audio_service_.EnableDeviceAec(true);
+ display->ShowNotification(Lang::Strings::RTC_MODE_ON);
+ break;
+ }
+
+ // If the AEC mode is changed, close the audio channel
+ if (protocol_ && protocol_->IsAudioChannelOpened()) {
+ protocol_->CloseAudioChannel();
+ }
+ });
+}
+
+void Application::PlaySound(const std::string_view& sound) {
+ audio_service_.PlaySound(sound);
+}
\ No newline at end of file
diff --git a/main/application.h b/main/application.h
new file mode 100644
index 0000000..bff4e9e
--- /dev/null
+++ b/main/application.h
@@ -0,0 +1,110 @@
+#ifndef _APPLICATION_H_
+#define _APPLICATION_H_
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "protocol.h"
+#include "ota.h"
+#include "audio_service.h"
+#include "device_state_event.h"
+
+
+#define MAIN_EVENT_SCHEDULE (1 << 0)
+#define MAIN_EVENT_SEND_AUDIO (1 << 1)
+#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
+#define MAIN_EVENT_VAD_CHANGE (1 << 3)
+#define MAIN_EVENT_ERROR (1 << 4)
+#define MAIN_EVENT_CHECK_NEW_VERSION_DONE (1 << 5)
+#define MAIN_EVENT_CLOCK_TICK (1 << 6)
+
+
+enum AecMode {
+ kAecOff,
+ kAecOnDeviceSide,
+ kAecOnServerSide,
+};
+
+class Application {
+public:
+ static Application& GetInstance() {
+ static Application instance;
+ return instance;
+ }
+ // 删除拷贝构造函数和赋值运算符
+ Application(const Application&) = delete;
+ Application& operator=(const Application&) = delete;
+
+ void Start();
+ void MainEventLoop();
+ DeviceState GetDeviceState() const { return device_state_; }
+ bool IsVoiceDetected() const { return audio_service_.IsVoiceDetected(); }
+ void Schedule(std::function 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 Reboot();
+ void WakeWordInvoke(const std::string& wake_word);
+ bool UpgradeFirmware(Ota& ota, const std::string& url = "");
+ bool CanEnterSleepMode();
+ void SendMcpMessage(const std::string& payload);
+ void SetAecMode(AecMode mode);
+ AecMode GetAecMode() const { return aec_mode_; }
+ void PlaySound(const std::string_view& sound);
+ AudioService& GetAudioService() { return audio_service_; }
+
+private:
+ Application();
+ ~Application();
+
+ std::mutex mutex_;
+ std::deque> main_tasks_;
+ std::unique_ptr protocol_;
+ EventGroupHandle_t event_group_ = nullptr;
+ esp_timer_handle_t clock_timer_handle_ = nullptr;
+ volatile DeviceState device_state_ = kDeviceStateUnknown;
+ ListeningMode listening_mode_ = kListeningModeAutoStop;
+ AecMode aec_mode_ = kAecOff;
+ std::string last_error_message_;
+ AudioService audio_service_;
+
+ bool has_server_time_ = false;
+ bool aborted_ = false;
+ int clock_ticks_ = 0;
+ TaskHandle_t check_new_version_task_handle_ = nullptr;
+ TaskHandle_t main_event_loop_task_handle_ = nullptr;
+
+ void OnWakeWordDetected();
+ void CheckNewVersion(Ota& ota);
+ void CheckAssetsVersion();
+ void ShowActivationCode(const std::string& code, const std::string& message);
+ void SetListeningMode(ListeningMode mode);
+};
+
+
+class TaskPriorityReset {
+public:
+ TaskPriorityReset(BaseType_t priority) {
+ original_priority_ = uxTaskPriorityGet(NULL);
+ vTaskPrioritySet(NULL, priority);
+ }
+ ~TaskPriorityReset() {
+ vTaskPrioritySet(NULL, original_priority_);
+ }
+
+private:
+ BaseType_t original_priority_;
+};
+
+#endif // _APPLICATION_H_
diff --git a/main/assets.cc b/main/assets.cc
new file mode 100644
index 0000000..48d790f
--- /dev/null
+++ b/main/assets.cc
@@ -0,0 +1,406 @@
+#include "assets.h"
+#include "board.h"
+#include "display.h"
+#include "application.h"
+#include "lvgl_theme.h"
+
+#include
+#include
+#include
+#include
+
+
+#define TAG "Assets"
+
+struct mmap_assets_table {
+ char asset_name[32]; /*!< Name of the asset */
+ uint32_t asset_size; /*!< Size of the asset */
+ uint32_t asset_offset; /*!< Offset of the asset */
+ uint16_t asset_width; /*!< Width of the asset */
+ uint16_t asset_height; /*!< Height of the asset */
+};
+
+
+Assets::Assets(std::string default_assets_url) {
+ if (default_assets_url.find("http") == 0) {
+ default_assets_url_ = default_assets_url;
+ } else {
+ ESP_LOGE(TAG, "The default assets url is not a http url: %s", default_assets_url.c_str());
+ }
+
+ // Initialize the partition
+ InitializePartition();
+}
+
+Assets::~Assets() {
+ if (mmap_handle_ != 0) {
+ esp_partition_munmap(mmap_handle_);
+ }
+}
+
+uint32_t Assets::CalculateChecksum(const char* data, uint32_t length) {
+ uint32_t checksum = 0;
+ for (uint32_t i = 0; i < length; i++) {
+ checksum += data[i];
+ }
+ return checksum & 0xFFFF;
+}
+
+bool Assets::InitializePartition() {
+ partition_valid_ = false;
+ checksum_valid_ = false;
+ assets_.clear();
+
+ partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, "assets");
+ if (partition_ == nullptr) {
+ ESP_LOGI(TAG, "No assets partition found");
+ return false;
+ }
+
+ int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
+ uint32_t storage_size = free_pages * 64 * 1024;
+ ESP_LOGI(TAG, "The storage free size is %ld KB", storage_size / 1024);
+ ESP_LOGI(TAG, "The partition size is %ld KB", partition_->size / 1024);
+ if (storage_size < partition_->size) {
+ ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, partition_->size / 1024);
+ return false;
+ }
+
+ esp_err_t err = esp_partition_mmap(partition_, 0, partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err));
+ return false;
+ }
+
+ partition_valid_ = true;
+
+ uint32_t stored_files = *(uint32_t*)(mmap_root_ + 0);
+ uint32_t stored_chksum = *(uint32_t*)(mmap_root_ + 4);
+ uint32_t stored_len = *(uint32_t*)(mmap_root_ + 8);
+
+ if (stored_len > partition_->size - 12) {
+ ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, partition_->size);
+ return false;
+ }
+
+ auto start_time = esp_timer_get_time();
+ uint32_t calculated_checksum = CalculateChecksum(mmap_root_ + 12, stored_len);
+ auto end_time = esp_timer_get_time();
+ ESP_LOGI(TAG, "The checksum calculation time is %d ms", int((end_time - start_time) / 1000));
+
+ if (calculated_checksum != stored_chksum) {
+ ESP_LOGE(TAG, "The calculated checksum (0x%lx) does not match the stored checksum (0x%lx)", calculated_checksum, stored_chksum);
+ return false;
+ }
+
+ checksum_valid_ = true;
+
+ for (uint32_t i = 0; i < stored_files; i++) {
+ auto item = (const mmap_assets_table*)(mmap_root_ + 12 + i * sizeof(mmap_assets_table));
+ auto asset = Asset{
+ .size = static_cast(item->asset_size),
+ .offset = static_cast(12 + sizeof(mmap_assets_table) * stored_files + item->asset_offset)
+ };
+ assets_[item->asset_name] = asset;
+ }
+ return checksum_valid_;
+}
+
+bool Assets::Apply() {
+ void* ptr = nullptr;
+ size_t size = 0;
+ if (!GetAssetData("index.json", ptr, size)) {
+ ESP_LOGE(TAG, "The index.json file is not found");
+ return false;
+ }
+ cJSON* root = cJSON_ParseWithLength(static_cast(ptr), size);
+ if (root == nullptr) {
+ ESP_LOGE(TAG, "The index.json file is not valid");
+ return false;
+ }
+
+ cJSON* version = cJSON_GetObjectItem(root, "version");
+ if (cJSON_IsNumber(version)) {
+ if (version->valuedouble > 1) {
+ ESP_LOGE(TAG, "The assets version %d is not supported, please upgrade the firmware", version->valueint);
+ return false;
+ }
+ }
+
+ cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels");
+ if (cJSON_IsString(srmodels)) {
+ std::string srmodels_file = srmodels->valuestring;
+ if (GetAssetData(srmodels_file, ptr, size)) {
+ if (models_list_ != nullptr) {
+ esp_srmodel_deinit(models_list_);
+ models_list_ = nullptr;
+ }
+ models_list_ = srmodel_load(static_cast(ptr));
+ if (models_list_ != nullptr) {
+ auto& app = Application::GetInstance();
+ app.GetAudioService().SetModelsList(models_list_);
+ } else {
+ ESP_LOGE(TAG, "Failed to load srmodels.bin");
+ }
+ } else {
+ ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str());
+ }
+ }
+
+#ifdef HAVE_LVGL
+ auto& theme_manager = LvglThemeManager::GetInstance();
+ auto light_theme = theme_manager.GetTheme("light");
+ auto dark_theme = theme_manager.GetTheme("dark");
+
+ cJSON* font = cJSON_GetObjectItem(root, "text_font");
+ if (cJSON_IsString(font)) {
+ std::string fonts_text_file = font->valuestring;
+ if (GetAssetData(fonts_text_file, ptr, size)) {
+ auto text_font = std::make_shared(ptr);
+ if (text_font->font() == nullptr) {
+ ESP_LOGE(TAG, "Failed to load fonts.bin");
+ return false;
+ }
+ if (light_theme != nullptr) {
+ light_theme->set_text_font(text_font);
+ }
+ if (dark_theme != nullptr) {
+ dark_theme->set_text_font(text_font);
+ }
+ } else {
+ ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
+ }
+ }
+
+ cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
+ if (cJSON_IsArray(emoji_collection)) {
+ auto custom_emoji_collection = std::make_shared();
+ int emoji_count = cJSON_GetArraySize(emoji_collection);
+ for (int i = 0; i < emoji_count; i++) {
+ cJSON* emoji = cJSON_GetArrayItem(emoji_collection, i);
+ if (cJSON_IsObject(emoji)) {
+ cJSON* name = cJSON_GetObjectItem(emoji, "name");
+ cJSON* file = cJSON_GetObjectItem(emoji, "file");
+ if (cJSON_IsString(name) && cJSON_IsString(file)) {
+ if (!GetAssetData(file->valuestring, ptr, size)) {
+ ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring);
+ continue;
+ }
+ custom_emoji_collection->AddEmoji(name->valuestring, new LvglRawImage(ptr, size));
+ }
+ }
+ }
+ if (light_theme != nullptr) {
+ light_theme->set_emoji_collection(custom_emoji_collection);
+ }
+ if (dark_theme != nullptr) {
+ dark_theme->set_emoji_collection(custom_emoji_collection);
+ }
+ }
+
+ cJSON* skin = cJSON_GetObjectItem(root, "skin");
+ if (cJSON_IsObject(skin)) {
+ cJSON* light_skin = cJSON_GetObjectItem(skin, "light");
+ if (cJSON_IsObject(light_skin) && light_theme != nullptr) {
+ cJSON* text_color = cJSON_GetObjectItem(light_skin, "text_color");
+ cJSON* background_color = cJSON_GetObjectItem(light_skin, "background_color");
+ cJSON* background_image = cJSON_GetObjectItem(light_skin, "background_image");
+ if (cJSON_IsString(text_color)) {
+ light_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring));
+ }
+ if (cJSON_IsString(background_color)) {
+ light_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring));
+ light_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring));
+ }
+ if (cJSON_IsString(background_image)) {
+ if (!GetAssetData(background_image->valuestring, ptr, size)) {
+ ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
+ return false;
+ }
+ auto background_image = std::make_shared(ptr);
+ light_theme->set_background_image(background_image);
+ }
+ }
+ cJSON* dark_skin = cJSON_GetObjectItem(skin, "dark");
+ if (cJSON_IsObject(dark_skin) && dark_theme != nullptr) {
+ cJSON* text_color = cJSON_GetObjectItem(dark_skin, "text_color");
+ cJSON* background_color = cJSON_GetObjectItem(dark_skin, "background_color");
+ cJSON* background_image = cJSON_GetObjectItem(dark_skin, "background_image");
+ if (cJSON_IsString(text_color)) {
+ dark_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring));
+ }
+ if (cJSON_IsString(background_color)) {
+ dark_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring));
+ dark_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring));
+ }
+ if (cJSON_IsString(background_image)) {
+ if (!GetAssetData(background_image->valuestring, ptr, size)) {
+ ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
+ return false;
+ }
+ auto background_image = std::make_shared(ptr);
+ dark_theme->set_background_image(background_image);
+ }
+ }
+ }
+#endif
+
+ auto display = Board::GetInstance().GetDisplay();
+ ESP_LOGI(TAG, "Refreshing display theme...");
+
+ auto current_theme = display->GetTheme();
+ if (current_theme != nullptr) {
+ display->SetTheme(current_theme);
+ }
+ cJSON_Delete(root);
+ return true;
+}
+
+bool Assets::Download(std::string url, std::function progress_callback) {
+ ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str());
+
+ // 取消当前资源分区的内存映射
+ if (mmap_handle_ != 0) {
+ esp_partition_munmap(mmap_handle_);
+ mmap_handle_ = 0;
+ mmap_root_ = nullptr;
+ }
+ checksum_valid_ = false;
+ assets_.clear();
+
+ // 下载新的资源文件
+ auto network = Board::GetInstance().GetNetwork();
+ auto http = network->CreateHttp(0);
+
+ if (!http->Open("GET", url)) {
+ ESP_LOGE(TAG, "Failed to open HTTP connection");
+ return false;
+ }
+
+ if (http->GetStatusCode() != 200) {
+ ESP_LOGE(TAG, "Failed to get assets, status code: %d", http->GetStatusCode());
+ return false;
+ }
+
+ size_t content_length = http->GetBodyLength();
+ if (content_length == 0) {
+ ESP_LOGE(TAG, "Failed to get content length");
+ return false;
+ }
+
+ if (content_length > partition_->size) {
+ ESP_LOGE(TAG, "Assets file size (%u) is larger than partition size (%lu)", content_length, partition_->size);
+ return false;
+ }
+
+ // 定义扇区大小为4KB(ESP32的标准扇区大小)
+ const size_t SECTOR_SIZE = esp_partition_get_main_flash_sector_size();
+
+ // 计算需要擦除的扇区数量
+ size_t sectors_to_erase = (content_length + SECTOR_SIZE - 1) / SECTOR_SIZE; // 向上取整
+ size_t total_erase_size = sectors_to_erase * SECTOR_SIZE;
+
+ ESP_LOGI(TAG, "Sector size: %u, content length: %u, sectors to erase: %u, total erase size: %u",
+ SECTOR_SIZE, content_length, sectors_to_erase, total_erase_size);
+
+ // 写入新的资源文件到分区,一边erase一边写入
+ char buffer[512];
+ size_t total_written = 0;
+ size_t recent_written = 0;
+ size_t current_sector = 0;
+ auto last_calc_time = esp_timer_get_time();
+
+ while (true) {
+ int ret = http->Read(buffer, sizeof(buffer));
+ if (ret < 0) {
+ ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret));
+ return false;
+ }
+
+ if (ret == 0) {
+ break;
+ }
+
+ // 检查是否需要擦除新的扇区
+ size_t write_end_offset = total_written + ret;
+ size_t needed_sectors = (write_end_offset + SECTOR_SIZE - 1) / SECTOR_SIZE;
+
+ // 擦除需要的新扇区
+ while (current_sector < needed_sectors) {
+ size_t sector_start = current_sector * SECTOR_SIZE;
+ size_t sector_end = (current_sector + 1) * SECTOR_SIZE;
+
+ // 确保擦除范围不超过分区大小
+ if (sector_end > partition_->size) {
+ ESP_LOGE(TAG, "Sector end (%u) exceeds partition size (%lu)", sector_end, partition_->size);
+ return false;
+ }
+
+ ESP_LOGD(TAG, "Erasing sector %u (offset: %u, size: %u)", current_sector, sector_start, SECTOR_SIZE);
+ esp_err_t err = esp_partition_erase_range(partition_, sector_start, SECTOR_SIZE);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", current_sector, sector_start, esp_err_to_name(err));
+ return false;
+ }
+
+ current_sector++;
+ }
+
+ // 写入数据到分区
+ esp_err_t err = esp_partition_write(partition_, total_written, buffer, ret);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "Failed to write to assets partition at offset %u: %s", total_written, esp_err_to_name(err));
+ return false;
+ }
+
+ total_written += ret;
+ recent_written += ret;
+
+ // 计算进度和速度
+ if (esp_timer_get_time() - last_calc_time >= 1000000 || total_written == content_length || ret == 0) {
+ size_t progress = total_written * 100 / content_length;
+ size_t speed = recent_written; // 每秒的字节数
+ ESP_LOGI(TAG, "Progress: %u%% (%u/%u), Speed: %u B/s, Sectors erased: %u",
+ progress, total_written, content_length, speed, current_sector);
+ if (progress_callback) {
+ progress_callback(progress, speed);
+ }
+ last_calc_time = esp_timer_get_time();
+ recent_written = 0; // 重置最近写入的字节数
+ }
+ }
+
+ http->Close();
+
+ if (total_written != content_length) {
+ ESP_LOGE(TAG, "Downloaded size (%u) does not match expected size (%u)", total_written, content_length);
+ return false;
+ }
+
+ ESP_LOGI(TAG, "Assets download completed, total written: %u bytes, total sectors erased: %u",
+ total_written, current_sector);
+
+ // 重新初始化资源分区
+ if (!InitializePartition()) {
+ ESP_LOGE(TAG, "Failed to re-initialize assets partition");
+ return false;
+ }
+
+ return true;
+}
+
+bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) {
+ auto asset = assets_.find(name);
+ if (asset == assets_.end()) {
+ return false;
+ }
+ auto data = (const char*)(mmap_root_ + asset->second.offset);
+ if (data[0] != 'Z' || data[1] != 'Z') {
+ ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]);
+ return false;
+ }
+
+ ptr = static_cast(const_cast(data + 2));
+ size = asset->second.size;
+ return true;
+}
diff --git a/main/assets.h b/main/assets.h
new file mode 100644
index 0000000..ff9a59e
--- /dev/null
+++ b/main/assets.h
@@ -0,0 +1,65 @@
+#ifndef ASSETS_H
+#define ASSETS_H
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+// All combinations of wakenet_model, text_font, emoji_collection can be found from the following url:
+// https://github.com/78/xiaozhi-fonts/releases/tag/assets
+
+#define ASSETS_PUHUI_COMMON_14_1 "none-font_puhui_common_14_1-none.bin"
+#define ASSETS_XIAOZHI_WAKENET "wn9_nihaoxiaozhi_tts-none-none.bin"
+#define ASSETS_XIAOZHI_WAKENET_SMALL "wn9s_nihaoxiaozhi-none-none.bin"
+#define ASSETS_XIAOZHI_PUHUI_COMMON_14_1 "wn9_nihaoxiaozhi_tts-font_puhui_common_14_1-none.bin"
+#define ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32 "wn9_nihaoxiaozhi_tts-font_puhui_common_16_4-emojis_32.bin"
+#define ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64 "wn9_nihaoxiaozhi_tts-font_puhui_common_16_4-emojis_64.bin"
+#define ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64 "wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-emojis_64.bin"
+#define ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64 "wn9_nihaoxiaozhi_tts-font_puhui_common_30_4-emojis_64.bin"
+#define ASSETS_XIAOZHI_S_PUHUI_COMMON_14_1 "wn9s_nihaoxiaozhi-font_puhui_common_14_1-none.bin"
+#define ASSETS_XIAOZHI_S_PUHUI_COMMON_16_4_EMOJI_32 "wn9s_nihaoxiaozhi-font_puhui_common_16_4-emojis_32.bin"
+#define ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_32 "wn9s_nihaoxiaozhi-font_puhui_common_20_4-emojis_32.bin"
+#define ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_64 "wn9s_nihaoxiaozhi-font_puhui_common_20_4-emojis_64.bin"
+#define ASSETS_XIAOZHI_S_PUHUI_COMMON_30_4_EMOJI_64 "wn9s_nihaoxiaozhi-font_puhui_common_30_4-emojis_64.bin"
+
+struct Asset {
+ size_t size;
+ size_t offset;
+};
+
+class Assets {
+public:
+ Assets(std::string default_assets_url);
+ ~Assets();
+
+ bool Download(std::string url, std::function progress_callback);
+ bool Apply();
+
+ inline bool partition_valid() const { return partition_valid_; }
+ inline bool checksum_valid() const { return checksum_valid_; }
+ inline std::string default_assets_url() const { return default_assets_url_; }
+
+private:
+ Assets(const Assets&) = delete;
+ Assets& operator=(const Assets&) = delete;
+
+ bool InitializePartition();
+ uint32_t CalculateChecksum(const char* data, uint32_t length);
+ bool GetAssetData(const std::string& name, void*& ptr, size_t& size);
+
+ const esp_partition_t* partition_ = nullptr;
+ esp_partition_mmap_handle_t mmap_handle_ = 0;
+ const char* mmap_root_ = nullptr;
+ bool partition_valid_ = false;
+ bool checksum_valid_ = false;
+ std::string default_assets_url_;
+ srmodel_list_t* models_list_ = nullptr;
+ std::map assets_;
+};
+
+#endif
diff --git a/main/assets/common/exclamation.ogg b/main/assets/common/exclamation.ogg
new file mode 100644
index 0000000..9a4cf58
Binary files /dev/null and b/main/assets/common/exclamation.ogg differ
diff --git a/main/assets/common/low_battery.ogg b/main/assets/common/low_battery.ogg
new file mode 100644
index 0000000..968b3ee
Binary files /dev/null and b/main/assets/common/low_battery.ogg differ
diff --git a/main/assets/common/popup.ogg b/main/assets/common/popup.ogg
new file mode 100644
index 0000000..ea73b6f
Binary files /dev/null and b/main/assets/common/popup.ogg differ
diff --git a/main/assets/common/success.ogg b/main/assets/common/success.ogg
new file mode 100644
index 0000000..ced1902
Binary files /dev/null and b/main/assets/common/success.ogg differ
diff --git a/main/assets/common/vibration.ogg b/main/assets/common/vibration.ogg
new file mode 100644
index 0000000..0076436
Binary files /dev/null and b/main/assets/common/vibration.ogg differ
diff --git a/main/assets/locales/ar-SA/0.ogg b/main/assets/locales/ar-SA/0.ogg
new file mode 100644
index 0000000..c656582
Binary files /dev/null and b/main/assets/locales/ar-SA/0.ogg differ
diff --git a/main/assets/locales/ar-SA/1.ogg b/main/assets/locales/ar-SA/1.ogg
new file mode 100644
index 0000000..dca3e06
Binary files /dev/null and b/main/assets/locales/ar-SA/1.ogg differ
diff --git a/main/assets/locales/ar-SA/2.ogg b/main/assets/locales/ar-SA/2.ogg
new file mode 100644
index 0000000..6d1f4d7
Binary files /dev/null and b/main/assets/locales/ar-SA/2.ogg differ
diff --git a/main/assets/locales/ar-SA/3.ogg b/main/assets/locales/ar-SA/3.ogg
new file mode 100644
index 0000000..12667d9
Binary files /dev/null and b/main/assets/locales/ar-SA/3.ogg differ
diff --git a/main/assets/locales/ar-SA/4.ogg b/main/assets/locales/ar-SA/4.ogg
new file mode 100644
index 0000000..e12dd7a
Binary files /dev/null and b/main/assets/locales/ar-SA/4.ogg differ
diff --git a/main/assets/locales/ar-SA/5.ogg b/main/assets/locales/ar-SA/5.ogg
new file mode 100644
index 0000000..7dae30d
Binary files /dev/null and b/main/assets/locales/ar-SA/5.ogg differ
diff --git a/main/assets/locales/ar-SA/6.ogg b/main/assets/locales/ar-SA/6.ogg
new file mode 100644
index 0000000..697278b
Binary files /dev/null and b/main/assets/locales/ar-SA/6.ogg differ
diff --git a/main/assets/locales/ar-SA/7.ogg b/main/assets/locales/ar-SA/7.ogg
new file mode 100644
index 0000000..f83701d
Binary files /dev/null and b/main/assets/locales/ar-SA/7.ogg differ
diff --git a/main/assets/locales/ar-SA/8.ogg b/main/assets/locales/ar-SA/8.ogg
new file mode 100644
index 0000000..bf3d2a1
Binary files /dev/null and b/main/assets/locales/ar-SA/8.ogg differ
diff --git a/main/assets/locales/ar-SA/9.ogg b/main/assets/locales/ar-SA/9.ogg
new file mode 100644
index 0000000..6d62228
Binary files /dev/null and b/main/assets/locales/ar-SA/9.ogg differ
diff --git a/main/assets/locales/ar-SA/activation.ogg b/main/assets/locales/ar-SA/activation.ogg
new file mode 100644
index 0000000..17aa008
Binary files /dev/null and b/main/assets/locales/ar-SA/activation.ogg differ
diff --git a/main/assets/locales/ar-SA/err_pin.ogg b/main/assets/locales/ar-SA/err_pin.ogg
new file mode 100644
index 0000000..8c624f5
Binary files /dev/null and b/main/assets/locales/ar-SA/err_pin.ogg differ
diff --git a/main/assets/locales/ar-SA/err_reg.ogg b/main/assets/locales/ar-SA/err_reg.ogg
new file mode 100644
index 0000000..b540a47
Binary files /dev/null and b/main/assets/locales/ar-SA/err_reg.ogg differ
diff --git a/main/assets/locales/ar-SA/language.json b/main/assets/locales/ar-SA/language.json
new file mode 100644
index 0000000..88263e4
--- /dev/null
+++ b/main/assets/locales/ar-SA/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "ar-SA"
+ },
+ "strings": {
+ "WARNING": "تحذير",
+ "INFO": "معلومات",
+ "ERROR": "خطأ",
+ "VERSION": "الإصدار ",
+ "LOADING_PROTOCOL": "الاتصال بالخادم...",
+ "INITIALIZING": "التهيئة...",
+ "PIN_ERROR": "يرجى إدخال بطاقة SIM",
+ "REG_ERROR": "لا يمكن الوصول إلى الشبكة، يرجى التحقق من حالة بطاقة البيانات",
+ "DETECTING_MODULE": "اكتشاف الوحدة...",
+ "REGISTERING_NETWORK": "انتظار الشبكة...",
+ "CHECKING_NEW_VERSION": "فحص الإصدار الجديد...",
+ "CHECK_NEW_VERSION_FAILED": "فشل فحص الإصدار الجديد، سيتم المحاولة خلال %d ثانية: %s",
+ "SWITCH_TO_WIFI_NETWORK": "التبديل إلى Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "التبديل إلى 4G...",
+ "STANDBY": "في الانتظار",
+ "CONNECT_TO": "الاتصال بـ ",
+ "CONNECTING": "جاري الاتصال...",
+ "CONNECTED_TO": "متصل بـ ",
+ "LISTENING": "الاستماع...",
+ "SPEAKING": "التحدث...",
+ "SERVER_NOT_FOUND": "البحث عن خدمة متاحة",
+ "SERVER_NOT_CONNECTED": "لا يمكن الاتصال بالخدمة، يرجى المحاولة لاحقاً",
+ "SERVER_TIMEOUT": "انتهت مهلة الاستجابة",
+ "SERVER_ERROR": "فشل الإرسال، يرجى التحقق من الشبكة",
+ "CONNECT_TO_HOTSPOT": "اتصل الهاتف بنقطة الاتصال ",
+ "ACCESS_VIA_BROWSER": "،الوصول عبر المتصفح ",
+ "WIFI_CONFIG_MODE": "وضع تكوين الشبكة",
+ "ENTERING_WIFI_CONFIG_MODE": "الدخول في وضع تكوين الشبكة...",
+ "SCANNING_WIFI": "فحص Wi-Fi...",
+ "NEW_VERSION": "إصدار جديد ",
+ "OTA_UPGRADE": "تحديث OTA",
+ "UPGRADING": "تحديث النظام...",
+ "UPGRADE_FAILED": "فشل التحديث",
+ "ACTIVATION": "تفعيل الجهاز",
+ "BATTERY_LOW": "البطارية منخفضة",
+ "BATTERY_CHARGING": "جاري الشحن",
+ "BATTERY_FULL": "البطارية ممتلئة",
+ "BATTERY_NEED_CHARGE": "البطارية منخفضة، يرجى الشحن",
+ "VOLUME": "الصوت ",
+ "MUTED": "صامت",
+ "MAX_VOLUME": "أقصى صوت",
+ "RTC_MODE_OFF": "AEC مُوقف",
+ "RTC_MODE_ON": "AEC مُشغل",
+ "DOWNLOAD_ASSETS_FAILED": "فشل في تنزيل الموارد",
+ "LOADING_ASSETS": "جاري تحميل الموارد...",
+ "PLEASE_WAIT": "يرجى الانتظار...",
+ "FOUND_NEW_ASSETS": "تم العثور على موارد جديدة: %s",
+ "HELLO_MY_FRIEND": "مرحباً، صديقي!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/ar-SA/upgrade.ogg b/main/assets/locales/ar-SA/upgrade.ogg
new file mode 100644
index 0000000..f60d374
Binary files /dev/null and b/main/assets/locales/ar-SA/upgrade.ogg differ
diff --git a/main/assets/locales/ar-SA/welcome.ogg b/main/assets/locales/ar-SA/welcome.ogg
new file mode 100644
index 0000000..77028ad
Binary files /dev/null and b/main/assets/locales/ar-SA/welcome.ogg differ
diff --git a/main/assets/locales/ar-SA/wificonfig.ogg b/main/assets/locales/ar-SA/wificonfig.ogg
new file mode 100644
index 0000000..b82f7a8
Binary files /dev/null and b/main/assets/locales/ar-SA/wificonfig.ogg differ
diff --git a/main/assets/locales/cs-CZ/0.ogg b/main/assets/locales/cs-CZ/0.ogg
new file mode 100644
index 0000000..96891e6
Binary files /dev/null and b/main/assets/locales/cs-CZ/0.ogg differ
diff --git a/main/assets/locales/cs-CZ/1.ogg b/main/assets/locales/cs-CZ/1.ogg
new file mode 100644
index 0000000..a12d638
Binary files /dev/null and b/main/assets/locales/cs-CZ/1.ogg differ
diff --git a/main/assets/locales/cs-CZ/2.ogg b/main/assets/locales/cs-CZ/2.ogg
new file mode 100644
index 0000000..a18d97e
Binary files /dev/null and b/main/assets/locales/cs-CZ/2.ogg differ
diff --git a/main/assets/locales/cs-CZ/3.ogg b/main/assets/locales/cs-CZ/3.ogg
new file mode 100644
index 0000000..a2d9d3d
Binary files /dev/null and b/main/assets/locales/cs-CZ/3.ogg differ
diff --git a/main/assets/locales/cs-CZ/4.ogg b/main/assets/locales/cs-CZ/4.ogg
new file mode 100644
index 0000000..76646ed
Binary files /dev/null and b/main/assets/locales/cs-CZ/4.ogg differ
diff --git a/main/assets/locales/cs-CZ/5.ogg b/main/assets/locales/cs-CZ/5.ogg
new file mode 100644
index 0000000..2d9ab25
Binary files /dev/null and b/main/assets/locales/cs-CZ/5.ogg differ
diff --git a/main/assets/locales/cs-CZ/6.ogg b/main/assets/locales/cs-CZ/6.ogg
new file mode 100644
index 0000000..571ff30
Binary files /dev/null and b/main/assets/locales/cs-CZ/6.ogg differ
diff --git a/main/assets/locales/cs-CZ/7.ogg b/main/assets/locales/cs-CZ/7.ogg
new file mode 100644
index 0000000..e8f0f94
Binary files /dev/null and b/main/assets/locales/cs-CZ/7.ogg differ
diff --git a/main/assets/locales/cs-CZ/8.ogg b/main/assets/locales/cs-CZ/8.ogg
new file mode 100644
index 0000000..0f72a58
Binary files /dev/null and b/main/assets/locales/cs-CZ/8.ogg differ
diff --git a/main/assets/locales/cs-CZ/9.ogg b/main/assets/locales/cs-CZ/9.ogg
new file mode 100644
index 0000000..c102667
Binary files /dev/null and b/main/assets/locales/cs-CZ/9.ogg differ
diff --git a/main/assets/locales/cs-CZ/activation.ogg b/main/assets/locales/cs-CZ/activation.ogg
new file mode 100644
index 0000000..b4334a4
Binary files /dev/null and b/main/assets/locales/cs-CZ/activation.ogg differ
diff --git a/main/assets/locales/cs-CZ/err_pin.ogg b/main/assets/locales/cs-CZ/err_pin.ogg
new file mode 100644
index 0000000..07818a1
Binary files /dev/null and b/main/assets/locales/cs-CZ/err_pin.ogg differ
diff --git a/main/assets/locales/cs-CZ/err_reg.ogg b/main/assets/locales/cs-CZ/err_reg.ogg
new file mode 100644
index 0000000..6d4752e
Binary files /dev/null and b/main/assets/locales/cs-CZ/err_reg.ogg differ
diff --git a/main/assets/locales/cs-CZ/language.json b/main/assets/locales/cs-CZ/language.json
new file mode 100644
index 0000000..1e18a5c
--- /dev/null
+++ b/main/assets/locales/cs-CZ/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "cs-CZ"
+ },
+ "strings": {
+ "WARNING": "Varování",
+ "INFO": "Informace",
+ "ERROR": "Chyba",
+ "VERSION": "Verze ",
+ "LOADING_PROTOCOL": "Připojování k serveru...",
+ "INITIALIZING": "Inicializace...",
+ "PIN_ERROR": "Prosím vložte SIM kartu",
+ "REG_ERROR": "Nelze se připojit k síti, zkontrolujte stav datové karty",
+ "DETECTING_MODULE": "Detekce modulu...",
+ "REGISTERING_NETWORK": "Čekání na síť...",
+ "CHECKING_NEW_VERSION": "Kontrola nové verze...",
+ "CHECK_NEW_VERSION_FAILED": "Kontrola nové verze selhala, opakování za %d sekund: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Přepínání na Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Přepínání na 4G...",
+ "STANDBY": "Pohotovost",
+ "CONNECT_TO": "Připojit k ",
+ "CONNECTING": "Připojování...",
+ "CONNECTED_TO": "Připojeno k ",
+ "LISTENING": "Naslouchání...",
+ "SPEAKING": "Mluvení...",
+ "SERVER_NOT_FOUND": "Hledání dostupné služby",
+ "SERVER_NOT_CONNECTED": "Nelze se připojit ke službě, zkuste to později",
+ "SERVER_TIMEOUT": "Čas odpovědi vypršel",
+ "SERVER_ERROR": "Odeslání selhalo, zkontrolujte síť",
+ "CONNECT_TO_HOTSPOT": "Připojte telefon k hotspotu ",
+ "ACCESS_VIA_BROWSER": ",přístup přes prohlížeč ",
+ "WIFI_CONFIG_MODE": "Režim konfigurace sítě",
+ "ENTERING_WIFI_CONFIG_MODE": "Vstup do režimu konfigurace sítě...",
+ "SCANNING_WIFI": "Skenování Wi-Fi...",
+ "NEW_VERSION": "Nová verze ",
+ "OTA_UPGRADE": "OTA upgrade",
+ "UPGRADING": "Aktualizace systému...",
+ "UPGRADE_FAILED": "Upgrade selhal",
+ "ACTIVATION": "Aktivace zařízení",
+ "BATTERY_LOW": "Slabá baterie",
+ "BATTERY_CHARGING": "Nabíjení",
+ "BATTERY_FULL": "Baterie plná",
+ "BATTERY_NEED_CHARGE": "Slabá baterie, prosím nabijte",
+ "VOLUME": "Hlasitost ",
+ "MUTED": "Ztlumeno",
+ "MAX_VOLUME": "Maximální hlasitost",
+ "RTC_MODE_OFF": "AEC vypnuto",
+ "RTC_MODE_ON": "AEC zapnuto",
+ "DOWNLOAD_ASSETS_FAILED": "Nepodařilo se stáhnout prostředky",
+ "LOADING_ASSETS": "Načítání prostředků...",
+ "PLEASE_WAIT": "Prosím čekejte...",
+ "FOUND_NEW_ASSETS": "Nalezeny nové prostředky: %s",
+ "HELLO_MY_FRIEND": "Ahoj, můj příteli!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/cs-CZ/upgrade.ogg b/main/assets/locales/cs-CZ/upgrade.ogg
new file mode 100644
index 0000000..e29edc5
Binary files /dev/null and b/main/assets/locales/cs-CZ/upgrade.ogg differ
diff --git a/main/assets/locales/cs-CZ/welcome.ogg b/main/assets/locales/cs-CZ/welcome.ogg
new file mode 100644
index 0000000..7a0625e
Binary files /dev/null and b/main/assets/locales/cs-CZ/welcome.ogg differ
diff --git a/main/assets/locales/cs-CZ/wificonfig.ogg b/main/assets/locales/cs-CZ/wificonfig.ogg
new file mode 100644
index 0000000..bc945a9
Binary files /dev/null and b/main/assets/locales/cs-CZ/wificonfig.ogg differ
diff --git a/main/assets/locales/de-DE/0.ogg b/main/assets/locales/de-DE/0.ogg
new file mode 100644
index 0000000..a3cdb23
Binary files /dev/null and b/main/assets/locales/de-DE/0.ogg differ
diff --git a/main/assets/locales/de-DE/1.ogg b/main/assets/locales/de-DE/1.ogg
new file mode 100644
index 0000000..fb4fd93
Binary files /dev/null and b/main/assets/locales/de-DE/1.ogg differ
diff --git a/main/assets/locales/de-DE/2.ogg b/main/assets/locales/de-DE/2.ogg
new file mode 100644
index 0000000..fb09b60
Binary files /dev/null and b/main/assets/locales/de-DE/2.ogg differ
diff --git a/main/assets/locales/de-DE/3.ogg b/main/assets/locales/de-DE/3.ogg
new file mode 100644
index 0000000..d3f7549
Binary files /dev/null and b/main/assets/locales/de-DE/3.ogg differ
diff --git a/main/assets/locales/de-DE/4.ogg b/main/assets/locales/de-DE/4.ogg
new file mode 100644
index 0000000..46db0ce
Binary files /dev/null and b/main/assets/locales/de-DE/4.ogg differ
diff --git a/main/assets/locales/de-DE/5.ogg b/main/assets/locales/de-DE/5.ogg
new file mode 100644
index 0000000..3ceee0f
Binary files /dev/null and b/main/assets/locales/de-DE/5.ogg differ
diff --git a/main/assets/locales/de-DE/6.ogg b/main/assets/locales/de-DE/6.ogg
new file mode 100644
index 0000000..8942550
Binary files /dev/null and b/main/assets/locales/de-DE/6.ogg differ
diff --git a/main/assets/locales/de-DE/7.ogg b/main/assets/locales/de-DE/7.ogg
new file mode 100644
index 0000000..3b6957d
Binary files /dev/null and b/main/assets/locales/de-DE/7.ogg differ
diff --git a/main/assets/locales/de-DE/8.ogg b/main/assets/locales/de-DE/8.ogg
new file mode 100644
index 0000000..421f018
Binary files /dev/null and b/main/assets/locales/de-DE/8.ogg differ
diff --git a/main/assets/locales/de-DE/9.ogg b/main/assets/locales/de-DE/9.ogg
new file mode 100644
index 0000000..40b7876
Binary files /dev/null and b/main/assets/locales/de-DE/9.ogg differ
diff --git a/main/assets/locales/de-DE/activation.ogg b/main/assets/locales/de-DE/activation.ogg
new file mode 100644
index 0000000..3dfccbd
Binary files /dev/null and b/main/assets/locales/de-DE/activation.ogg differ
diff --git a/main/assets/locales/de-DE/err_pin.ogg b/main/assets/locales/de-DE/err_pin.ogg
new file mode 100644
index 0000000..8d2eef9
Binary files /dev/null and b/main/assets/locales/de-DE/err_pin.ogg differ
diff --git a/main/assets/locales/de-DE/err_reg.ogg b/main/assets/locales/de-DE/err_reg.ogg
new file mode 100644
index 0000000..430b876
Binary files /dev/null and b/main/assets/locales/de-DE/err_reg.ogg differ
diff --git a/main/assets/locales/de-DE/language.json b/main/assets/locales/de-DE/language.json
new file mode 100644
index 0000000..724e267
--- /dev/null
+++ b/main/assets/locales/de-DE/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "de-DE"
+ },
+ "strings": {
+ "WARNING": "Warnung",
+ "INFO": "Information",
+ "ERROR": "Fehler",
+ "VERSION": "Version ",
+ "LOADING_PROTOCOL": "Verbindung zum Server...",
+ "INITIALIZING": "Initialisierung...",
+ "PIN_ERROR": "Bitte SIM-Karte einlegen",
+ "REG_ERROR": "Netzwerkverbindung fehlgeschlagen, bitte Datenkartenstatus prüfen",
+ "DETECTING_MODULE": "Modul erkennen...",
+ "REGISTERING_NETWORK": "Auf Netzwerk warten...",
+ "CHECKING_NEW_VERSION": "Neue Version prüfen...",
+ "CHECK_NEW_VERSION_FAILED": "Neue Version prüfen fehlgeschlagen, Wiederholung in %d Sekunden: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Zu Wi-Fi wechseln...",
+ "SWITCH_TO_4G_NETWORK": "Zu 4G wechseln...",
+ "STANDBY": "Bereitschaft",
+ "CONNECT_TO": "Verbinden zu ",
+ "CONNECTING": "Verbindung wird hergestellt...",
+ "CONNECTED_TO": "Verbunden mit ",
+ "LISTENING": "Zuhören...",
+ "SPEAKING": "Sprechen...",
+ "SERVER_NOT_FOUND": "Verfügbaren Service suchen",
+ "SERVER_NOT_CONNECTED": "Service-Verbindung fehlgeschlagen, bitte später versuchen",
+ "SERVER_TIMEOUT": "Antwort-Timeout",
+ "SERVER_ERROR": "Senden fehlgeschlagen, bitte Netzwerk prüfen",
+ "CONNECT_TO_HOTSPOT": "Handy mit Hotspot verbinden ",
+ "ACCESS_VIA_BROWSER": ",Browser öffnen ",
+ "WIFI_CONFIG_MODE": "Netzwerkkonfigurationsmodus",
+ "ENTERING_WIFI_CONFIG_MODE": "Netzwerkkonfigurationsmodus eingeben...",
+ "SCANNING_WIFI": "Wi-Fi scannen...",
+ "NEW_VERSION": "Neue Version ",
+ "OTA_UPGRADE": "OTA-Upgrade",
+ "UPGRADING": "System wird aktualisiert...",
+ "UPGRADE_FAILED": "Upgrade fehlgeschlagen",
+ "ACTIVATION": "Gerät aktivieren",
+ "BATTERY_LOW": "Niedriger Batteriestand",
+ "BATTERY_CHARGING": "Wird geladen",
+ "BATTERY_FULL": "Batterie voll",
+ "BATTERY_NEED_CHARGE": "Niedriger Batteriestand, bitte aufladen",
+ "VOLUME": "Lautstärke ",
+ "MUTED": "Stummgeschaltet",
+ "MAX_VOLUME": "Maximale Lautstärke",
+ "RTC_MODE_OFF": "AEC aus",
+ "RTC_MODE_ON": "AEC ein",
+ "DOWNLOAD_ASSETS_FAILED": "Fehler beim Herunterladen der Ressourcen",
+ "LOADING_ASSETS": "Ressourcen werden geladen...",
+ "PLEASE_WAIT": "Bitte warten...",
+ "FOUND_NEW_ASSETS": "Neue Ressourcen gefunden: %s",
+ "HELLO_MY_FRIEND": "Hallo, mein Freund!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/de-DE/upgrade.ogg b/main/assets/locales/de-DE/upgrade.ogg
new file mode 100644
index 0000000..716d7cb
Binary files /dev/null and b/main/assets/locales/de-DE/upgrade.ogg differ
diff --git a/main/assets/locales/de-DE/welcome.ogg b/main/assets/locales/de-DE/welcome.ogg
new file mode 100644
index 0000000..69271ce
Binary files /dev/null and b/main/assets/locales/de-DE/welcome.ogg differ
diff --git a/main/assets/locales/de-DE/wificonfig.ogg b/main/assets/locales/de-DE/wificonfig.ogg
new file mode 100644
index 0000000..a3d753d
Binary files /dev/null and b/main/assets/locales/de-DE/wificonfig.ogg differ
diff --git a/main/assets/locales/en-US/0.ogg b/main/assets/locales/en-US/0.ogg
new file mode 100644
index 0000000..51dff41
Binary files /dev/null and b/main/assets/locales/en-US/0.ogg differ
diff --git a/main/assets/locales/en-US/1.ogg b/main/assets/locales/en-US/1.ogg
new file mode 100644
index 0000000..814753e
Binary files /dev/null and b/main/assets/locales/en-US/1.ogg differ
diff --git a/main/assets/locales/en-US/2.ogg b/main/assets/locales/en-US/2.ogg
new file mode 100644
index 0000000..420a994
Binary files /dev/null and b/main/assets/locales/en-US/2.ogg differ
diff --git a/main/assets/locales/en-US/3.ogg b/main/assets/locales/en-US/3.ogg
new file mode 100644
index 0000000..1d3fa8a
Binary files /dev/null and b/main/assets/locales/en-US/3.ogg differ
diff --git a/main/assets/locales/en-US/4.ogg b/main/assets/locales/en-US/4.ogg
new file mode 100644
index 0000000..8e364ad
Binary files /dev/null and b/main/assets/locales/en-US/4.ogg differ
diff --git a/main/assets/locales/en-US/5.ogg b/main/assets/locales/en-US/5.ogg
new file mode 100644
index 0000000..5be437d
Binary files /dev/null and b/main/assets/locales/en-US/5.ogg differ
diff --git a/main/assets/locales/en-US/6.ogg b/main/assets/locales/en-US/6.ogg
new file mode 100644
index 0000000..e398409
Binary files /dev/null and b/main/assets/locales/en-US/6.ogg differ
diff --git a/main/assets/locales/en-US/7.ogg b/main/assets/locales/en-US/7.ogg
new file mode 100644
index 0000000..879aebd
Binary files /dev/null and b/main/assets/locales/en-US/7.ogg differ
diff --git a/main/assets/locales/en-US/8.ogg b/main/assets/locales/en-US/8.ogg
new file mode 100644
index 0000000..0e9efd2
Binary files /dev/null and b/main/assets/locales/en-US/8.ogg differ
diff --git a/main/assets/locales/en-US/9.ogg b/main/assets/locales/en-US/9.ogg
new file mode 100644
index 0000000..434c320
Binary files /dev/null and b/main/assets/locales/en-US/9.ogg differ
diff --git a/main/assets/locales/en-US/activation.ogg b/main/assets/locales/en-US/activation.ogg
new file mode 100644
index 0000000..0e9b843
Binary files /dev/null and b/main/assets/locales/en-US/activation.ogg differ
diff --git a/main/assets/locales/en-US/err_pin.ogg b/main/assets/locales/en-US/err_pin.ogg
new file mode 100644
index 0000000..c52fdf4
Binary files /dev/null and b/main/assets/locales/en-US/err_pin.ogg differ
diff --git a/main/assets/locales/en-US/err_reg.ogg b/main/assets/locales/en-US/err_reg.ogg
new file mode 100644
index 0000000..95593f4
Binary files /dev/null and b/main/assets/locales/en-US/err_reg.ogg differ
diff --git a/main/assets/locales/en-US/language.json b/main/assets/locales/en-US/language.json
new file mode 100644
index 0000000..1758d34
--- /dev/null
+++ b/main/assets/locales/en-US/language.json
@@ -0,0 +1,56 @@
+{
+ "language": {
+ "type": "en-US"
+ },
+ "strings": {
+ "WARNING": "Warning",
+ "INFO": "Information",
+ "ERROR": "Error",
+ "VERSION": "Ver ",
+ "LOADING_PROTOCOL": "Logging in...",
+ "INITIALIZING": "Initializing...",
+ "PIN_ERROR": "Please insert SIM card",
+ "REG_ERROR": "Unable to access network, please check SIM card status",
+ "DETECTING_MODULE": "Detecting module...",
+ "REGISTERING_NETWORK": "Waiting for network...",
+ "CHECKING_NEW_VERSION": "Checking for new version...",
+ "CHECK_NEW_VERSION_FAILED": "Check for new version failed, will retry in %d seconds: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Switching to Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Switching to 4G...",
+ "STANDBY": "Standby",
+ "CONNECT_TO": "Connect to ",
+ "CONNECTING": "Connecting...",
+ "CONNECTION_SUCCESSFUL": "Connection Successful",
+ "CONNECTED_TO": "Connected to ",
+ "LISTENING": "Listening...",
+ "SPEAKING": "Speaking...",
+ "SERVER_NOT_FOUND": "Looking for available service",
+ "SERVER_NOT_CONNECTED": "Unable to connect to service, please try again later",
+ "SERVER_TIMEOUT": "Waiting for response timeout",
+ "SERVER_ERROR": "Sending failed, please check the network",
+ "CONNECT_TO_HOTSPOT": "Hotspot: ",
+ "ACCESS_VIA_BROWSER": " Config URL: ",
+ "WIFI_CONFIG_MODE": "Wi-Fi Configuration Mode",
+ "ENTERING_WIFI_CONFIG_MODE": "Entering Wi-Fi configuration mode...",
+ "SCANNING_WIFI": "Scanning Wi-Fi...",
+ "NEW_VERSION": "New version ",
+ "OTA_UPGRADE": "OTA Upgrade",
+ "UPGRADING": "System is upgrading...",
+ "UPGRADE_FAILED": "Upgrade failed",
+ "ACTIVATION": "Activation",
+ "BATTERY_LOW": "Low battery",
+ "BATTERY_CHARGING": "Charging",
+ "BATTERY_FULL": "Battery full",
+ "BATTERY_NEED_CHARGE": "Low battery, please charge",
+ "VOLUME": "Volume ",
+ "MUTED": "Muted",
+ "MAX_VOLUME": "Max volume",
+ "RTC_MODE_OFF": "AEC Off",
+ "RTC_MODE_ON": "AEC On",
+ "PLEASE_WAIT": "Please wait...",
+ "FOUND_NEW_ASSETS": "Found new assets: %s",
+ "DOWNLOAD_ASSETS_FAILED": "Failed to download assets",
+ "LOADING_ASSETS": "Loading assets...",
+ "HELLO_MY_FRIEND": "Hello, my friend!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/en-US/upgrade.ogg b/main/assets/locales/en-US/upgrade.ogg
new file mode 100644
index 0000000..f27ec7a
Binary files /dev/null and b/main/assets/locales/en-US/upgrade.ogg differ
diff --git a/main/assets/locales/en-US/welcome.ogg b/main/assets/locales/en-US/welcome.ogg
new file mode 100644
index 0000000..4d4d8b9
Binary files /dev/null and b/main/assets/locales/en-US/welcome.ogg differ
diff --git a/main/assets/locales/en-US/wificonfig.ogg b/main/assets/locales/en-US/wificonfig.ogg
new file mode 100644
index 0000000..f4ed3d4
Binary files /dev/null and b/main/assets/locales/en-US/wificonfig.ogg differ
diff --git a/main/assets/locales/es-ES/0.ogg b/main/assets/locales/es-ES/0.ogg
new file mode 100644
index 0000000..98fbba2
Binary files /dev/null and b/main/assets/locales/es-ES/0.ogg differ
diff --git a/main/assets/locales/es-ES/1.ogg b/main/assets/locales/es-ES/1.ogg
new file mode 100644
index 0000000..2c2f4fb
Binary files /dev/null and b/main/assets/locales/es-ES/1.ogg differ
diff --git a/main/assets/locales/es-ES/2.ogg b/main/assets/locales/es-ES/2.ogg
new file mode 100644
index 0000000..8e962b9
Binary files /dev/null and b/main/assets/locales/es-ES/2.ogg differ
diff --git a/main/assets/locales/es-ES/3.ogg b/main/assets/locales/es-ES/3.ogg
new file mode 100644
index 0000000..6b6ba47
Binary files /dev/null and b/main/assets/locales/es-ES/3.ogg differ
diff --git a/main/assets/locales/es-ES/4.ogg b/main/assets/locales/es-ES/4.ogg
new file mode 100644
index 0000000..ae5909c
Binary files /dev/null and b/main/assets/locales/es-ES/4.ogg differ
diff --git a/main/assets/locales/es-ES/5.ogg b/main/assets/locales/es-ES/5.ogg
new file mode 100644
index 0000000..a8a4ca3
Binary files /dev/null and b/main/assets/locales/es-ES/5.ogg differ
diff --git a/main/assets/locales/es-ES/6.ogg b/main/assets/locales/es-ES/6.ogg
new file mode 100644
index 0000000..bcc46c0
Binary files /dev/null and b/main/assets/locales/es-ES/6.ogg differ
diff --git a/main/assets/locales/es-ES/7.ogg b/main/assets/locales/es-ES/7.ogg
new file mode 100644
index 0000000..09e7df4
Binary files /dev/null and b/main/assets/locales/es-ES/7.ogg differ
diff --git a/main/assets/locales/es-ES/8.ogg b/main/assets/locales/es-ES/8.ogg
new file mode 100644
index 0000000..e06e023
Binary files /dev/null and b/main/assets/locales/es-ES/8.ogg differ
diff --git a/main/assets/locales/es-ES/9.ogg b/main/assets/locales/es-ES/9.ogg
new file mode 100644
index 0000000..4108458
Binary files /dev/null and b/main/assets/locales/es-ES/9.ogg differ
diff --git a/main/assets/locales/es-ES/activation.ogg b/main/assets/locales/es-ES/activation.ogg
new file mode 100644
index 0000000..8d50682
Binary files /dev/null and b/main/assets/locales/es-ES/activation.ogg differ
diff --git a/main/assets/locales/es-ES/err_pin.ogg b/main/assets/locales/es-ES/err_pin.ogg
new file mode 100644
index 0000000..b3e55f8
Binary files /dev/null and b/main/assets/locales/es-ES/err_pin.ogg differ
diff --git a/main/assets/locales/es-ES/err_reg.ogg b/main/assets/locales/es-ES/err_reg.ogg
new file mode 100644
index 0000000..509451e
Binary files /dev/null and b/main/assets/locales/es-ES/err_reg.ogg differ
diff --git a/main/assets/locales/es-ES/language.json b/main/assets/locales/es-ES/language.json
new file mode 100644
index 0000000..e7f349b
--- /dev/null
+++ b/main/assets/locales/es-ES/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "es-ES"
+ },
+ "strings": {
+ "WARNING": "Advertencia",
+ "INFO": "Información",
+ "ERROR": "Error",
+ "VERSION": "Versión ",
+ "LOADING_PROTOCOL": "Conectando al servidor...",
+ "INITIALIZING": "Inicializando...",
+ "PIN_ERROR": "Por favor inserte la tarjeta SIM",
+ "REG_ERROR": "No se puede acceder a la red, verifique el estado de la tarjeta de datos",
+ "DETECTING_MODULE": "Detectando módulo...",
+ "REGISTERING_NETWORK": "Esperando red...",
+ "CHECKING_NEW_VERSION": "Verificando nueva versión...",
+ "CHECK_NEW_VERSION_FAILED": "Error al verificar nueva versión, reintentando en %d segundos: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Cambiando a Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Cambiando a 4G...",
+ "STANDBY": "En espera",
+ "CONNECT_TO": "Conectar a ",
+ "CONNECTING": "Conectando...",
+ "CONNECTED_TO": "Conectado a ",
+ "LISTENING": "Escuchando...",
+ "SPEAKING": "Hablando...",
+ "SERVER_NOT_FOUND": "Buscando servicio disponible",
+ "SERVER_NOT_CONNECTED": "No se puede conectar al servicio, inténtelo más tarde",
+ "SERVER_TIMEOUT": "Tiempo de espera agotado",
+ "SERVER_ERROR": "Error de envío, verifique la red",
+ "CONNECT_TO_HOTSPOT": "Conectar teléfono al punto de acceso ",
+ "ACCESS_VIA_BROWSER": ",acceder mediante navegador ",
+ "WIFI_CONFIG_MODE": "Modo configuración de red",
+ "ENTERING_WIFI_CONFIG_MODE": "Entrando en modo configuración de red...",
+ "SCANNING_WIFI": "Escaneando Wi-Fi...",
+ "NEW_VERSION": "Nueva versión ",
+ "OTA_UPGRADE": "Actualización OTA",
+ "UPGRADING": "Actualizando sistema...",
+ "UPGRADE_FAILED": "Actualización fallida",
+ "ACTIVATION": "Activación del dispositivo",
+ "BATTERY_LOW": "Batería baja",
+ "BATTERY_CHARGING": "Cargando",
+ "BATTERY_FULL": "Batería llena",
+ "BATTERY_NEED_CHARGE": "Batería baja, por favor cargar",
+ "VOLUME": "Volumen ",
+ "MUTED": "Silenciado",
+ "MAX_VOLUME": "Volumen máximo",
+ "RTC_MODE_OFF": "AEC desactivado",
+ "RTC_MODE_ON": "AEC activado",
+ "DOWNLOAD_ASSETS_FAILED": "Error al descargar recursos",
+ "LOADING_ASSETS": "Cargando recursos...",
+ "PLEASE_WAIT": "Por favor espere...",
+ "FOUND_NEW_ASSETS": "Encontrados nuevos recursos: %s",
+ "HELLO_MY_FRIEND": "¡Hola, mi amigo!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/es-ES/upgrade.ogg b/main/assets/locales/es-ES/upgrade.ogg
new file mode 100644
index 0000000..c29826c
Binary files /dev/null and b/main/assets/locales/es-ES/upgrade.ogg differ
diff --git a/main/assets/locales/es-ES/welcome.ogg b/main/assets/locales/es-ES/welcome.ogg
new file mode 100644
index 0000000..a506db5
Binary files /dev/null and b/main/assets/locales/es-ES/welcome.ogg differ
diff --git a/main/assets/locales/es-ES/wificonfig.ogg b/main/assets/locales/es-ES/wificonfig.ogg
new file mode 100644
index 0000000..11755fa
Binary files /dev/null and b/main/assets/locales/es-ES/wificonfig.ogg differ
diff --git a/main/assets/locales/fi-FI/0.ogg b/main/assets/locales/fi-FI/0.ogg
new file mode 100644
index 0000000..d368787
Binary files /dev/null and b/main/assets/locales/fi-FI/0.ogg differ
diff --git a/main/assets/locales/fi-FI/1.ogg b/main/assets/locales/fi-FI/1.ogg
new file mode 100644
index 0000000..f496dd6
Binary files /dev/null and b/main/assets/locales/fi-FI/1.ogg differ
diff --git a/main/assets/locales/fi-FI/2.ogg b/main/assets/locales/fi-FI/2.ogg
new file mode 100644
index 0000000..96a2529
Binary files /dev/null and b/main/assets/locales/fi-FI/2.ogg differ
diff --git a/main/assets/locales/fi-FI/3.ogg b/main/assets/locales/fi-FI/3.ogg
new file mode 100644
index 0000000..1fcdcf2
Binary files /dev/null and b/main/assets/locales/fi-FI/3.ogg differ
diff --git a/main/assets/locales/fi-FI/4.ogg b/main/assets/locales/fi-FI/4.ogg
new file mode 100644
index 0000000..6ba9ded
Binary files /dev/null and b/main/assets/locales/fi-FI/4.ogg differ
diff --git a/main/assets/locales/fi-FI/5.ogg b/main/assets/locales/fi-FI/5.ogg
new file mode 100644
index 0000000..b94d4a2
Binary files /dev/null and b/main/assets/locales/fi-FI/5.ogg differ
diff --git a/main/assets/locales/fi-FI/6.ogg b/main/assets/locales/fi-FI/6.ogg
new file mode 100644
index 0000000..ffe8276
Binary files /dev/null and b/main/assets/locales/fi-FI/6.ogg differ
diff --git a/main/assets/locales/fi-FI/7.ogg b/main/assets/locales/fi-FI/7.ogg
new file mode 100644
index 0000000..ed3197a
Binary files /dev/null and b/main/assets/locales/fi-FI/7.ogg differ
diff --git a/main/assets/locales/fi-FI/8.ogg b/main/assets/locales/fi-FI/8.ogg
new file mode 100644
index 0000000..7f81a62
Binary files /dev/null and b/main/assets/locales/fi-FI/8.ogg differ
diff --git a/main/assets/locales/fi-FI/9.ogg b/main/assets/locales/fi-FI/9.ogg
new file mode 100644
index 0000000..aaedd05
Binary files /dev/null and b/main/assets/locales/fi-FI/9.ogg differ
diff --git a/main/assets/locales/fi-FI/activation.ogg b/main/assets/locales/fi-FI/activation.ogg
new file mode 100644
index 0000000..6347745
Binary files /dev/null and b/main/assets/locales/fi-FI/activation.ogg differ
diff --git a/main/assets/locales/fi-FI/err_pin.ogg b/main/assets/locales/fi-FI/err_pin.ogg
new file mode 100644
index 0000000..8c6eb51
Binary files /dev/null and b/main/assets/locales/fi-FI/err_pin.ogg differ
diff --git a/main/assets/locales/fi-FI/err_reg.ogg b/main/assets/locales/fi-FI/err_reg.ogg
new file mode 100644
index 0000000..32561bf
Binary files /dev/null and b/main/assets/locales/fi-FI/err_reg.ogg differ
diff --git a/main/assets/locales/fi-FI/language.json b/main/assets/locales/fi-FI/language.json
new file mode 100644
index 0000000..2326ee4
--- /dev/null
+++ b/main/assets/locales/fi-FI/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "fi-FI"
+ },
+ "strings": {
+ "WARNING": "Varoitus",
+ "INFO": "Tieto",
+ "ERROR": "Virhe",
+ "VERSION": "Versio ",
+ "LOADING_PROTOCOL": "Yhdistetään palvelimeen...",
+ "INITIALIZING": "Alustetaan...",
+ "PIN_ERROR": "Ole hyvä ja aseta SIM-kortti",
+ "REG_ERROR": "Ei voi muodostaa yhteyttä verkkoon, tarkista datakortin tila",
+ "DETECTING_MODULE": "Tunnistetaan moduuli...",
+ "REGISTERING_NETWORK": "Odotetaan verkkoa...",
+ "CHECKING_NEW_VERSION": "Tarkistetaan uutta versiota...",
+ "CHECK_NEW_VERSION_FAILED": "Uuden version tarkistus epäonnistui, yritetään uudelleen %d sekunnin kuluttua: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Vaihdetaan Wi-Fi:hin...",
+ "SWITCH_TO_4G_NETWORK": "Vaihdetaan 4G:hen...",
+ "STANDBY": "Valmiustila",
+ "CONNECT_TO": "Yhdistä ",
+ "CONNECTING": "Yhdistetään...",
+ "CONNECTED_TO": "Yhdistetty ",
+ "LISTENING": "Kuunnellaan...",
+ "SPEAKING": "Puhutaan...",
+ "SERVER_NOT_FOUND": "Etsitään käytettävissä olevaa palvelua",
+ "SERVER_NOT_CONNECTED": "Ei voi yhdistää palveluun, yritä myöhemmin",
+ "SERVER_TIMEOUT": "Vastauksen aikakatkaisu",
+ "SERVER_ERROR": "Lähetys epäonnistui, tarkista verkko",
+ "CONNECT_TO_HOTSPOT": "Yhdistä puhelin hotspottiin ",
+ "ACCESS_VIA_BROWSER": ",pääsy selaimen kautta ",
+ "WIFI_CONFIG_MODE": "Verkon konfigurointitila",
+ "ENTERING_WIFI_CONFIG_MODE": "Siirrytään verkon konfigurointitilaan...",
+ "SCANNING_WIFI": "Skannataan Wi-Fi...",
+ "NEW_VERSION": "Uusi versio ",
+ "OTA_UPGRADE": "OTA-päivitys",
+ "UPGRADING": "Päivitetään järjestelmää...",
+ "UPGRADE_FAILED": "Päivitys epäonnistui",
+ "ACTIVATION": "Laitteen aktivointi",
+ "BATTERY_LOW": "Akku vähissä",
+ "BATTERY_CHARGING": "Ladataan",
+ "BATTERY_FULL": "Akku täynnä",
+ "BATTERY_NEED_CHARGE": "Akku vähissä, ole hyvä ja lataa",
+ "VOLUME": "Äänenvoimakkuus ",
+ "MUTED": "Mykistetty",
+ "MAX_VOLUME": "Maksimi äänenvoimakkuus",
+ "RTC_MODE_OFF": "AEC pois päältä",
+ "RTC_MODE_ON": "AEC päällä",
+ "DOWNLOAD_ASSETS_FAILED": "Resurssien lataaminen epäonnistui",
+ "LOADING_ASSETS": "Ladataan resursseja...",
+ "PLEASE_WAIT": "Odota hetki...",
+ "FOUND_NEW_ASSETS": "Löydetty uusia resursseja: %s",
+ "HELLO_MY_FRIEND": "Hei, ystäväni!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/fi-FI/upgrade.ogg b/main/assets/locales/fi-FI/upgrade.ogg
new file mode 100644
index 0000000..5ff02a6
Binary files /dev/null and b/main/assets/locales/fi-FI/upgrade.ogg differ
diff --git a/main/assets/locales/fi-FI/welcome.ogg b/main/assets/locales/fi-FI/welcome.ogg
new file mode 100644
index 0000000..1430372
Binary files /dev/null and b/main/assets/locales/fi-FI/welcome.ogg differ
diff --git a/main/assets/locales/fi-FI/wificonfig.ogg b/main/assets/locales/fi-FI/wificonfig.ogg
new file mode 100644
index 0000000..deb233a
Binary files /dev/null and b/main/assets/locales/fi-FI/wificonfig.ogg differ
diff --git a/main/assets/locales/fr-FR/0.ogg b/main/assets/locales/fr-FR/0.ogg
new file mode 100644
index 0000000..79b8faf
Binary files /dev/null and b/main/assets/locales/fr-FR/0.ogg differ
diff --git a/main/assets/locales/fr-FR/1.ogg b/main/assets/locales/fr-FR/1.ogg
new file mode 100644
index 0000000..e517324
Binary files /dev/null and b/main/assets/locales/fr-FR/1.ogg differ
diff --git a/main/assets/locales/fr-FR/2.ogg b/main/assets/locales/fr-FR/2.ogg
new file mode 100644
index 0000000..311697c
Binary files /dev/null and b/main/assets/locales/fr-FR/2.ogg differ
diff --git a/main/assets/locales/fr-FR/3.ogg b/main/assets/locales/fr-FR/3.ogg
new file mode 100644
index 0000000..5b4acc1
Binary files /dev/null and b/main/assets/locales/fr-FR/3.ogg differ
diff --git a/main/assets/locales/fr-FR/4.ogg b/main/assets/locales/fr-FR/4.ogg
new file mode 100644
index 0000000..832aeab
Binary files /dev/null and b/main/assets/locales/fr-FR/4.ogg differ
diff --git a/main/assets/locales/fr-FR/5.ogg b/main/assets/locales/fr-FR/5.ogg
new file mode 100644
index 0000000..ddcce17
Binary files /dev/null and b/main/assets/locales/fr-FR/5.ogg differ
diff --git a/main/assets/locales/fr-FR/6.ogg b/main/assets/locales/fr-FR/6.ogg
new file mode 100644
index 0000000..aced771
Binary files /dev/null and b/main/assets/locales/fr-FR/6.ogg differ
diff --git a/main/assets/locales/fr-FR/7.ogg b/main/assets/locales/fr-FR/7.ogg
new file mode 100644
index 0000000..293d1c0
Binary files /dev/null and b/main/assets/locales/fr-FR/7.ogg differ
diff --git a/main/assets/locales/fr-FR/8.ogg b/main/assets/locales/fr-FR/8.ogg
new file mode 100644
index 0000000..acec112
Binary files /dev/null and b/main/assets/locales/fr-FR/8.ogg differ
diff --git a/main/assets/locales/fr-FR/9.ogg b/main/assets/locales/fr-FR/9.ogg
new file mode 100644
index 0000000..fe27acd
Binary files /dev/null and b/main/assets/locales/fr-FR/9.ogg differ
diff --git a/main/assets/locales/fr-FR/activation.ogg b/main/assets/locales/fr-FR/activation.ogg
new file mode 100644
index 0000000..b48f215
Binary files /dev/null and b/main/assets/locales/fr-FR/activation.ogg differ
diff --git a/main/assets/locales/fr-FR/err_pin.ogg b/main/assets/locales/fr-FR/err_pin.ogg
new file mode 100644
index 0000000..1a3d84b
Binary files /dev/null and b/main/assets/locales/fr-FR/err_pin.ogg differ
diff --git a/main/assets/locales/fr-FR/err_reg.ogg b/main/assets/locales/fr-FR/err_reg.ogg
new file mode 100644
index 0000000..c3256c5
Binary files /dev/null and b/main/assets/locales/fr-FR/err_reg.ogg differ
diff --git a/main/assets/locales/fr-FR/language.json b/main/assets/locales/fr-FR/language.json
new file mode 100644
index 0000000..244cade
--- /dev/null
+++ b/main/assets/locales/fr-FR/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "fr-FR"
+ },
+ "strings": {
+ "WARNING": "Avertissement",
+ "INFO": "Information",
+ "ERROR": "Erreur",
+ "VERSION": "Version ",
+ "LOADING_PROTOCOL": "Connexion au serveur...",
+ "INITIALIZING": "Initialisation...",
+ "PIN_ERROR": "Veuillez insérer la carte SIM",
+ "REG_ERROR": "Impossible d'accéder au réseau, veuillez vérifier l'état de la carte de données",
+ "DETECTING_MODULE": "Détection du module...",
+ "REGISTERING_NETWORK": "En attente du réseau...",
+ "CHECKING_NEW_VERSION": "Vérification de nouvelle version...",
+ "CHECK_NEW_VERSION_FAILED": "Échec de vérification de nouvelle version, nouvelle tentative dans %d secondes : %s",
+ "SWITCH_TO_WIFI_NETWORK": "Basculer vers Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Basculer vers 4G...",
+ "STANDBY": "En attente",
+ "CONNECT_TO": "Se connecter à ",
+ "CONNECTING": "Connexion en cours...",
+ "CONNECTED_TO": "Connecté à ",
+ "LISTENING": "Écoute...",
+ "SPEAKING": "Parole...",
+ "SERVER_NOT_FOUND": "Recherche d'un service disponible",
+ "SERVER_NOT_CONNECTED": "Impossible de se connecter au service, veuillez réessayer plus tard",
+ "SERVER_TIMEOUT": "Délai d'attente de réponse",
+ "SERVER_ERROR": "Échec d'envoi, veuillez vérifier le réseau",
+ "CONNECT_TO_HOTSPOT": "Connecter le téléphone au point d'accès ",
+ "ACCESS_VIA_BROWSER": ",accéder via le navigateur ",
+ "WIFI_CONFIG_MODE": "Mode configuration réseau",
+ "ENTERING_WIFI_CONFIG_MODE": "Entrer en mode configuration réseau...",
+ "SCANNING_WIFI": "Scan Wi-Fi...",
+ "NEW_VERSION": "Nouvelle version ",
+ "OTA_UPGRADE": "Mise à jour OTA",
+ "UPGRADING": "Mise à jour du système...",
+ "UPGRADE_FAILED": "Échec de mise à jour",
+ "ACTIVATION": "Activation de l'appareil",
+ "BATTERY_LOW": "Batterie faible",
+ "BATTERY_CHARGING": "En charge",
+ "BATTERY_FULL": "Batterie pleine",
+ "BATTERY_NEED_CHARGE": "Batterie faible, veuillez charger",
+ "VOLUME": "Volume ",
+ "MUTED": "Muet",
+ "MAX_VOLUME": "Volume maximum",
+ "RTC_MODE_OFF": "AEC désactivé",
+ "RTC_MODE_ON": "AEC activé",
+ "DOWNLOAD_ASSETS_FAILED": "Échec du téléchargement des ressources",
+ "LOADING_ASSETS": "Chargement des ressources...",
+ "PLEASE_WAIT": "Veuillez patienter...",
+ "FOUND_NEW_ASSETS": "Nouvelles ressources trouvées: %s",
+ "HELLO_MY_FRIEND": "Bonjour, mon ami !"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/fr-FR/upgrade.ogg b/main/assets/locales/fr-FR/upgrade.ogg
new file mode 100644
index 0000000..b6ac1f9
Binary files /dev/null and b/main/assets/locales/fr-FR/upgrade.ogg differ
diff --git a/main/assets/locales/fr-FR/welcome.ogg b/main/assets/locales/fr-FR/welcome.ogg
new file mode 100644
index 0000000..4492de7
Binary files /dev/null and b/main/assets/locales/fr-FR/welcome.ogg differ
diff --git a/main/assets/locales/fr-FR/wificonfig.ogg b/main/assets/locales/fr-FR/wificonfig.ogg
new file mode 100644
index 0000000..9e2f99b
Binary files /dev/null and b/main/assets/locales/fr-FR/wificonfig.ogg differ
diff --git a/main/assets/locales/hi-IN/0.ogg b/main/assets/locales/hi-IN/0.ogg
new file mode 100644
index 0000000..e531348
Binary files /dev/null and b/main/assets/locales/hi-IN/0.ogg differ
diff --git a/main/assets/locales/hi-IN/1.ogg b/main/assets/locales/hi-IN/1.ogg
new file mode 100644
index 0000000..cb2f9d5
Binary files /dev/null and b/main/assets/locales/hi-IN/1.ogg differ
diff --git a/main/assets/locales/hi-IN/2.ogg b/main/assets/locales/hi-IN/2.ogg
new file mode 100644
index 0000000..2244d45
Binary files /dev/null and b/main/assets/locales/hi-IN/2.ogg differ
diff --git a/main/assets/locales/hi-IN/3.ogg b/main/assets/locales/hi-IN/3.ogg
new file mode 100644
index 0000000..63fde6d
Binary files /dev/null and b/main/assets/locales/hi-IN/3.ogg differ
diff --git a/main/assets/locales/hi-IN/4.ogg b/main/assets/locales/hi-IN/4.ogg
new file mode 100644
index 0000000..76caee9
Binary files /dev/null and b/main/assets/locales/hi-IN/4.ogg differ
diff --git a/main/assets/locales/hi-IN/5.ogg b/main/assets/locales/hi-IN/5.ogg
new file mode 100644
index 0000000..c96a386
Binary files /dev/null and b/main/assets/locales/hi-IN/5.ogg differ
diff --git a/main/assets/locales/hi-IN/6.ogg b/main/assets/locales/hi-IN/6.ogg
new file mode 100644
index 0000000..88f7643
Binary files /dev/null and b/main/assets/locales/hi-IN/6.ogg differ
diff --git a/main/assets/locales/hi-IN/7.ogg b/main/assets/locales/hi-IN/7.ogg
new file mode 100644
index 0000000..3a997d4
Binary files /dev/null and b/main/assets/locales/hi-IN/7.ogg differ
diff --git a/main/assets/locales/hi-IN/8.ogg b/main/assets/locales/hi-IN/8.ogg
new file mode 100644
index 0000000..56f125e
Binary files /dev/null and b/main/assets/locales/hi-IN/8.ogg differ
diff --git a/main/assets/locales/hi-IN/9.ogg b/main/assets/locales/hi-IN/9.ogg
new file mode 100644
index 0000000..25e64c0
Binary files /dev/null and b/main/assets/locales/hi-IN/9.ogg differ
diff --git a/main/assets/locales/hi-IN/activation.ogg b/main/assets/locales/hi-IN/activation.ogg
new file mode 100644
index 0000000..aa262df
Binary files /dev/null and b/main/assets/locales/hi-IN/activation.ogg differ
diff --git a/main/assets/locales/hi-IN/err_pin.ogg b/main/assets/locales/hi-IN/err_pin.ogg
new file mode 100644
index 0000000..00669fa
Binary files /dev/null and b/main/assets/locales/hi-IN/err_pin.ogg differ
diff --git a/main/assets/locales/hi-IN/err_reg.ogg b/main/assets/locales/hi-IN/err_reg.ogg
new file mode 100644
index 0000000..6194739
Binary files /dev/null and b/main/assets/locales/hi-IN/err_reg.ogg differ
diff --git a/main/assets/locales/hi-IN/language.json b/main/assets/locales/hi-IN/language.json
new file mode 100644
index 0000000..732176e
--- /dev/null
+++ b/main/assets/locales/hi-IN/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "hi-IN"
+ },
+ "strings": {
+ "WARNING": "चेतावनी",
+ "INFO": "जानकारी",
+ "ERROR": "त्रुटि",
+ "VERSION": "संस्करण ",
+ "LOADING_PROTOCOL": "सर्वर से कनेक्ट हो रहे हैं...",
+ "INITIALIZING": "आरंभीकरण...",
+ "PIN_ERROR": "कृपया सिम कार्ड डालें",
+ "REG_ERROR": "नेटवर्क तक पहुंच नहीं हो सकती, कृपया डेटा कार्ड स्थिति जांचें",
+ "DETECTING_MODULE": "मॉड्यूल का पता लगाया जा रहा है...",
+ "REGISTERING_NETWORK": "नेटवर्क की प्रतीक्षा...",
+ "CHECKING_NEW_VERSION": "नया संस्करण जाँच रहे हैं...",
+ "CHECK_NEW_VERSION_FAILED": "नया संस्करण जाँचना असफल, %d सेकंड में पुनः प्रयास: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Wi-Fi पर स्विच कर रहे हैं...",
+ "SWITCH_TO_4G_NETWORK": "4G पर स्विच कर रहे हैं...",
+ "STANDBY": "स्टैंडबाय",
+ "CONNECT_TO": "कनेक्ट करें ",
+ "CONNECTING": "कनेक्ट हो रहे हैं...",
+ "CONNECTED_TO": "कनेक्ट हो गए ",
+ "LISTENING": "सुन रहे हैं...",
+ "SPEAKING": "बोल रहे हैं...",
+ "SERVER_NOT_FOUND": "उपलब्ध सेवा खोज रहे हैं",
+ "SERVER_NOT_CONNECTED": "सेवा से कनेक्ट नहीं हो सकते, कृपया बाद में कोशिश करें",
+ "SERVER_TIMEOUT": "प्रतिक्रिया का समय समाप्त",
+ "SERVER_ERROR": "भेजना असफल, कृपया नेटवर्क जांचें",
+ "CONNECT_TO_HOTSPOT": "फोन को हॉटस्पॉट से कनेक्ट करें ",
+ "ACCESS_VIA_BROWSER": ",ब्राउज़र के माध्यम से पहुंचें ",
+ "WIFI_CONFIG_MODE": "नेटवर्क कॉन्फ़िगरेशन मोड",
+ "ENTERING_WIFI_CONFIG_MODE": "नेटवर्क कॉन्फ़िगरेशन मोड में प्रवेश...",
+ "SCANNING_WIFI": "Wi-Fi स्कैन कर रहे हैं...",
+ "NEW_VERSION": "नया संस्करण ",
+ "OTA_UPGRADE": "OTA अपग्रेड",
+ "UPGRADING": "सिस्टम अपग्रेड हो रहा है...",
+ "UPGRADE_FAILED": "अपग्रेड असफल",
+ "ACTIVATION": "डिवाइस सक्रियण",
+ "BATTERY_LOW": "बैटरी कम",
+ "BATTERY_CHARGING": "चार्ज हो रही है",
+ "BATTERY_FULL": "बैटरी फुल",
+ "BATTERY_NEED_CHARGE": "बैटरी कम है, कृपया चार्ज करें",
+ "VOLUME": "आवाज़ ",
+ "MUTED": "म्यूट",
+ "MAX_VOLUME": "अधिकतम आवाज़",
+ "RTC_MODE_OFF": "AEC बंद",
+ "RTC_MODE_ON": "AEC चालू",
+ "DOWNLOAD_ASSETS_FAILED": "संसाधन डाउनलोड करने में विफल",
+ "LOADING_ASSETS": "संसाधन लोड हो रहे हैं...",
+ "PLEASE_WAIT": "कृपया प्रतीक्षा करें...",
+ "FOUND_NEW_ASSETS": "नए संसाधन मिले: %s",
+ "HELLO_MY_FRIEND": "नमस्ते, मेरे दोस्त!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/hi-IN/upgrade.ogg b/main/assets/locales/hi-IN/upgrade.ogg
new file mode 100644
index 0000000..8de0693
Binary files /dev/null and b/main/assets/locales/hi-IN/upgrade.ogg differ
diff --git a/main/assets/locales/hi-IN/welcome.ogg b/main/assets/locales/hi-IN/welcome.ogg
new file mode 100644
index 0000000..3352a57
Binary files /dev/null and b/main/assets/locales/hi-IN/welcome.ogg differ
diff --git a/main/assets/locales/hi-IN/wificonfig.ogg b/main/assets/locales/hi-IN/wificonfig.ogg
new file mode 100644
index 0000000..5a9853f
Binary files /dev/null and b/main/assets/locales/hi-IN/wificonfig.ogg differ
diff --git a/main/assets/locales/id-ID/0.ogg b/main/assets/locales/id-ID/0.ogg
new file mode 100644
index 0000000..1d8dea3
Binary files /dev/null and b/main/assets/locales/id-ID/0.ogg differ
diff --git a/main/assets/locales/id-ID/1.ogg b/main/assets/locales/id-ID/1.ogg
new file mode 100644
index 0000000..cd8d7cc
Binary files /dev/null and b/main/assets/locales/id-ID/1.ogg differ
diff --git a/main/assets/locales/id-ID/2.ogg b/main/assets/locales/id-ID/2.ogg
new file mode 100644
index 0000000..69cdce8
Binary files /dev/null and b/main/assets/locales/id-ID/2.ogg differ
diff --git a/main/assets/locales/id-ID/3.ogg b/main/assets/locales/id-ID/3.ogg
new file mode 100644
index 0000000..2730364
Binary files /dev/null and b/main/assets/locales/id-ID/3.ogg differ
diff --git a/main/assets/locales/id-ID/4.ogg b/main/assets/locales/id-ID/4.ogg
new file mode 100644
index 0000000..f26010f
Binary files /dev/null and b/main/assets/locales/id-ID/4.ogg differ
diff --git a/main/assets/locales/id-ID/5.ogg b/main/assets/locales/id-ID/5.ogg
new file mode 100644
index 0000000..67b41f7
Binary files /dev/null and b/main/assets/locales/id-ID/5.ogg differ
diff --git a/main/assets/locales/id-ID/6.ogg b/main/assets/locales/id-ID/6.ogg
new file mode 100644
index 0000000..48f0343
Binary files /dev/null and b/main/assets/locales/id-ID/6.ogg differ
diff --git a/main/assets/locales/id-ID/7.ogg b/main/assets/locales/id-ID/7.ogg
new file mode 100644
index 0000000..c56df9f
Binary files /dev/null and b/main/assets/locales/id-ID/7.ogg differ
diff --git a/main/assets/locales/id-ID/8.ogg b/main/assets/locales/id-ID/8.ogg
new file mode 100644
index 0000000..cb9e15b
Binary files /dev/null and b/main/assets/locales/id-ID/8.ogg differ
diff --git a/main/assets/locales/id-ID/9.ogg b/main/assets/locales/id-ID/9.ogg
new file mode 100644
index 0000000..e088c86
Binary files /dev/null and b/main/assets/locales/id-ID/9.ogg differ
diff --git a/main/assets/locales/id-ID/activation.ogg b/main/assets/locales/id-ID/activation.ogg
new file mode 100644
index 0000000..a468767
Binary files /dev/null and b/main/assets/locales/id-ID/activation.ogg differ
diff --git a/main/assets/locales/id-ID/err_pin.ogg b/main/assets/locales/id-ID/err_pin.ogg
new file mode 100644
index 0000000..419ff8a
Binary files /dev/null and b/main/assets/locales/id-ID/err_pin.ogg differ
diff --git a/main/assets/locales/id-ID/err_reg.ogg b/main/assets/locales/id-ID/err_reg.ogg
new file mode 100644
index 0000000..d756ddf
Binary files /dev/null and b/main/assets/locales/id-ID/err_reg.ogg differ
diff --git a/main/assets/locales/id-ID/language.json b/main/assets/locales/id-ID/language.json
new file mode 100644
index 0000000..11224a6
--- /dev/null
+++ b/main/assets/locales/id-ID/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "id-ID"
+ },
+ "strings": {
+ "WARNING": "Peringatan",
+ "INFO": "Informasi",
+ "ERROR": "Kesalahan",
+ "VERSION": "Versi ",
+ "LOADING_PROTOCOL": "Menghubungkan ke server...",
+ "INITIALIZING": "Menginisialisasi...",
+ "PIN_ERROR": "Silakan masukkan kartu SIM",
+ "REG_ERROR": "Tidak dapat mengakses jaringan, periksa status kartu data",
+ "DETECTING_MODULE": "Mendeteksi modul...",
+ "REGISTERING_NETWORK": "Menunggu jaringan...",
+ "CHECKING_NEW_VERSION": "Memeriksa versi baru...",
+ "CHECK_NEW_VERSION_FAILED": "Pemeriksaan versi baru gagal, mencoba lagi dalam %d detik: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Beralih ke Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Beralih ke 4G...",
+ "STANDBY": "Siaga",
+ "CONNECT_TO": "Hubungkan ke ",
+ "CONNECTING": "Menghubungkan...",
+ "CONNECTED_TO": "Terhubung ke ",
+ "LISTENING": "Mendengarkan...",
+ "SPEAKING": "Berbicara...",
+ "SERVER_NOT_FOUND": "Mencari layanan yang tersedia",
+ "SERVER_NOT_CONNECTED": "Tidak dapat terhubung ke layanan, coba lagi nanti",
+ "SERVER_TIMEOUT": "Waktu respons habis",
+ "SERVER_ERROR": "Pengiriman gagal, periksa jaringan",
+ "CONNECT_TO_HOTSPOT": "Hubungkan ponsel ke hotspot ",
+ "ACCESS_VIA_BROWSER": ",akses melalui browser ",
+ "WIFI_CONFIG_MODE": "Mode konfigurasi jaringan",
+ "ENTERING_WIFI_CONFIG_MODE": "Memasuki mode konfigurasi jaringan...",
+ "SCANNING_WIFI": "Memindai Wi-Fi...",
+ "NEW_VERSION": "Versi baru ",
+ "OTA_UPGRADE": "Pembaruan OTA",
+ "UPGRADING": "Memperbarui sistem...",
+ "UPGRADE_FAILED": "Pembaruan gagal",
+ "ACTIVATION": "Aktivasi perangkat",
+ "BATTERY_LOW": "Baterai lemah",
+ "BATTERY_CHARGING": "Mengisi",
+ "BATTERY_FULL": "Baterai penuh",
+ "BATTERY_NEED_CHARGE": "Baterai lemah, silakan isi",
+ "VOLUME": "Volume ",
+ "MUTED": "Bisu",
+ "MAX_VOLUME": "Volume maksimum",
+ "RTC_MODE_OFF": "AEC mati",
+ "RTC_MODE_ON": "AEC nyala",
+ "DOWNLOAD_ASSETS_FAILED": "Gagal mengunduh aset",
+ "LOADING_ASSETS": "Memuat aset...",
+ "PLEASE_WAIT": "Mohon tunggu...",
+ "FOUND_NEW_ASSETS": "Ditemukan aset baru: %s",
+ "HELLO_MY_FRIEND": "Halo, teman saya!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/id-ID/upgrade.ogg b/main/assets/locales/id-ID/upgrade.ogg
new file mode 100644
index 0000000..cc2557a
Binary files /dev/null and b/main/assets/locales/id-ID/upgrade.ogg differ
diff --git a/main/assets/locales/id-ID/welcome.ogg b/main/assets/locales/id-ID/welcome.ogg
new file mode 100644
index 0000000..57beac6
Binary files /dev/null and b/main/assets/locales/id-ID/welcome.ogg differ
diff --git a/main/assets/locales/id-ID/wificonfig.ogg b/main/assets/locales/id-ID/wificonfig.ogg
new file mode 100644
index 0000000..477acdd
Binary files /dev/null and b/main/assets/locales/id-ID/wificonfig.ogg differ
diff --git a/main/assets/locales/it-IT/0.ogg b/main/assets/locales/it-IT/0.ogg
new file mode 100644
index 0000000..ae426e3
Binary files /dev/null and b/main/assets/locales/it-IT/0.ogg differ
diff --git a/main/assets/locales/it-IT/1.ogg b/main/assets/locales/it-IT/1.ogg
new file mode 100644
index 0000000..7c97b52
Binary files /dev/null and b/main/assets/locales/it-IT/1.ogg differ
diff --git a/main/assets/locales/it-IT/2.ogg b/main/assets/locales/it-IT/2.ogg
new file mode 100644
index 0000000..3d7995a
Binary files /dev/null and b/main/assets/locales/it-IT/2.ogg differ
diff --git a/main/assets/locales/it-IT/3.ogg b/main/assets/locales/it-IT/3.ogg
new file mode 100644
index 0000000..b22ad1e
Binary files /dev/null and b/main/assets/locales/it-IT/3.ogg differ
diff --git a/main/assets/locales/it-IT/4.ogg b/main/assets/locales/it-IT/4.ogg
new file mode 100644
index 0000000..ef01a46
Binary files /dev/null and b/main/assets/locales/it-IT/4.ogg differ
diff --git a/main/assets/locales/it-IT/5.ogg b/main/assets/locales/it-IT/5.ogg
new file mode 100644
index 0000000..d240b88
Binary files /dev/null and b/main/assets/locales/it-IT/5.ogg differ
diff --git a/main/assets/locales/it-IT/6.ogg b/main/assets/locales/it-IT/6.ogg
new file mode 100644
index 0000000..c52a2d7
Binary files /dev/null and b/main/assets/locales/it-IT/6.ogg differ
diff --git a/main/assets/locales/it-IT/7.ogg b/main/assets/locales/it-IT/7.ogg
new file mode 100644
index 0000000..dce632c
Binary files /dev/null and b/main/assets/locales/it-IT/7.ogg differ
diff --git a/main/assets/locales/it-IT/8.ogg b/main/assets/locales/it-IT/8.ogg
new file mode 100644
index 0000000..acd19e8
Binary files /dev/null and b/main/assets/locales/it-IT/8.ogg differ
diff --git a/main/assets/locales/it-IT/9.ogg b/main/assets/locales/it-IT/9.ogg
new file mode 100644
index 0000000..807c226
Binary files /dev/null and b/main/assets/locales/it-IT/9.ogg differ
diff --git a/main/assets/locales/it-IT/activation.ogg b/main/assets/locales/it-IT/activation.ogg
new file mode 100644
index 0000000..6381325
Binary files /dev/null and b/main/assets/locales/it-IT/activation.ogg differ
diff --git a/main/assets/locales/it-IT/err_pin.ogg b/main/assets/locales/it-IT/err_pin.ogg
new file mode 100644
index 0000000..3fb7107
Binary files /dev/null and b/main/assets/locales/it-IT/err_pin.ogg differ
diff --git a/main/assets/locales/it-IT/err_reg.ogg b/main/assets/locales/it-IT/err_reg.ogg
new file mode 100644
index 0000000..3bbe56e
Binary files /dev/null and b/main/assets/locales/it-IT/err_reg.ogg differ
diff --git a/main/assets/locales/it-IT/language.json b/main/assets/locales/it-IT/language.json
new file mode 100644
index 0000000..54cc9bc
--- /dev/null
+++ b/main/assets/locales/it-IT/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "it-IT"
+ },
+ "strings": {
+ "WARNING": "Avviso",
+ "INFO": "Informazione",
+ "ERROR": "Errore",
+ "VERSION": "Versione ",
+ "LOADING_PROTOCOL": "Connessione al server...",
+ "INITIALIZING": "Inizializzazione...",
+ "PIN_ERROR": "Inserire la scheda SIM",
+ "REG_ERROR": "Impossibile accedere alla rete, controllare lo stato della scheda dati",
+ "DETECTING_MODULE": "Rilevamento modulo...",
+ "REGISTERING_NETWORK": "In attesa della rete...",
+ "CHECKING_NEW_VERSION": "Controllo nuova versione...",
+ "CHECK_NEW_VERSION_FAILED": "Controllo nuova versione fallito, riprovo tra %d secondi: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Passaggio a Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Passaggio a 4G...",
+ "STANDBY": "In attesa",
+ "CONNECT_TO": "Connetti a ",
+ "CONNECTING": "Connessione...",
+ "CONNECTED_TO": "Connesso a ",
+ "LISTENING": "In ascolto...",
+ "SPEAKING": "Parlando...",
+ "SERVER_NOT_FOUND": "Ricerca servizio disponibile",
+ "SERVER_NOT_CONNECTED": "Impossibile connettersi al servizio, riprovare più tardi",
+ "SERVER_TIMEOUT": "Timeout risposta",
+ "SERVER_ERROR": "Invio fallito, controllare la rete",
+ "CONNECT_TO_HOTSPOT": "Connetti telefono al hotspot ",
+ "ACCESS_VIA_BROWSER": ",accedi tramite browser ",
+ "WIFI_CONFIG_MODE": "Modalità configurazione rete",
+ "ENTERING_WIFI_CONFIG_MODE": "Entrata in modalità configurazione rete...",
+ "SCANNING_WIFI": "Scansione Wi-Fi...",
+ "NEW_VERSION": "Nuova versione ",
+ "OTA_UPGRADE": "Aggiornamento OTA",
+ "UPGRADING": "Aggiornamento sistema...",
+ "UPGRADE_FAILED": "Aggiornamento fallito",
+ "ACTIVATION": "Attivazione dispositivo",
+ "BATTERY_LOW": "Batteria scarica",
+ "BATTERY_CHARGING": "In carica",
+ "BATTERY_FULL": "Batteria piena",
+ "BATTERY_NEED_CHARGE": "Batteria scarica, ricaricare",
+ "VOLUME": "Volume ",
+ "MUTED": "Silenziato",
+ "MAX_VOLUME": "Volume massimo",
+ "RTC_MODE_OFF": "AEC disattivato",
+ "RTC_MODE_ON": "AEC attivato",
+ "DOWNLOAD_ASSETS_FAILED": "Impossibile scaricare le risorse",
+ "LOADING_ASSETS": "Caricamento risorse...",
+ "PLEASE_WAIT": "Attendere prego...",
+ "FOUND_NEW_ASSETS": "Trovate nuove risorse: %s",
+ "HELLO_MY_FRIEND": "Ciao, amico mio!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/it-IT/upgrade.ogg b/main/assets/locales/it-IT/upgrade.ogg
new file mode 100644
index 0000000..79c0048
Binary files /dev/null and b/main/assets/locales/it-IT/upgrade.ogg differ
diff --git a/main/assets/locales/it-IT/welcome.ogg b/main/assets/locales/it-IT/welcome.ogg
new file mode 100644
index 0000000..702def3
Binary files /dev/null and b/main/assets/locales/it-IT/welcome.ogg differ
diff --git a/main/assets/locales/it-IT/wificonfig.ogg b/main/assets/locales/it-IT/wificonfig.ogg
new file mode 100644
index 0000000..7aae624
Binary files /dev/null and b/main/assets/locales/it-IT/wificonfig.ogg differ
diff --git a/main/assets/locales/ja-JP/0.ogg b/main/assets/locales/ja-JP/0.ogg
new file mode 100644
index 0000000..5b24d6a
Binary files /dev/null and b/main/assets/locales/ja-JP/0.ogg differ
diff --git a/main/assets/locales/ja-JP/1.ogg b/main/assets/locales/ja-JP/1.ogg
new file mode 100644
index 0000000..e31933d
Binary files /dev/null and b/main/assets/locales/ja-JP/1.ogg differ
diff --git a/main/assets/locales/ja-JP/2.ogg b/main/assets/locales/ja-JP/2.ogg
new file mode 100644
index 0000000..f75a934
Binary files /dev/null and b/main/assets/locales/ja-JP/2.ogg differ
diff --git a/main/assets/locales/ja-JP/3.ogg b/main/assets/locales/ja-JP/3.ogg
new file mode 100644
index 0000000..e414b59
Binary files /dev/null and b/main/assets/locales/ja-JP/3.ogg differ
diff --git a/main/assets/locales/ja-JP/4.ogg b/main/assets/locales/ja-JP/4.ogg
new file mode 100644
index 0000000..a977c97
Binary files /dev/null and b/main/assets/locales/ja-JP/4.ogg differ
diff --git a/main/assets/locales/ja-JP/5.ogg b/main/assets/locales/ja-JP/5.ogg
new file mode 100644
index 0000000..52ccf1b
Binary files /dev/null and b/main/assets/locales/ja-JP/5.ogg differ
diff --git a/main/assets/locales/ja-JP/6.ogg b/main/assets/locales/ja-JP/6.ogg
new file mode 100644
index 0000000..361f2e6
Binary files /dev/null and b/main/assets/locales/ja-JP/6.ogg differ
diff --git a/main/assets/locales/ja-JP/7.ogg b/main/assets/locales/ja-JP/7.ogg
new file mode 100644
index 0000000..e98be57
Binary files /dev/null and b/main/assets/locales/ja-JP/7.ogg differ
diff --git a/main/assets/locales/ja-JP/8.ogg b/main/assets/locales/ja-JP/8.ogg
new file mode 100644
index 0000000..2f33874
Binary files /dev/null and b/main/assets/locales/ja-JP/8.ogg differ
diff --git a/main/assets/locales/ja-JP/9.ogg b/main/assets/locales/ja-JP/9.ogg
new file mode 100644
index 0000000..c150a18
Binary files /dev/null and b/main/assets/locales/ja-JP/9.ogg differ
diff --git a/main/assets/locales/ja-JP/activation.ogg b/main/assets/locales/ja-JP/activation.ogg
new file mode 100644
index 0000000..995a489
Binary files /dev/null and b/main/assets/locales/ja-JP/activation.ogg differ
diff --git a/main/assets/locales/ja-JP/err_pin.ogg b/main/assets/locales/ja-JP/err_pin.ogg
new file mode 100644
index 0000000..120bd6c
Binary files /dev/null and b/main/assets/locales/ja-JP/err_pin.ogg differ
diff --git a/main/assets/locales/ja-JP/err_reg.ogg b/main/assets/locales/ja-JP/err_reg.ogg
new file mode 100644
index 0000000..27ccc93
Binary files /dev/null and b/main/assets/locales/ja-JP/err_reg.ogg differ
diff --git a/main/assets/locales/ja-JP/language.json b/main/assets/locales/ja-JP/language.json
new file mode 100644
index 0000000..1c2bd82
--- /dev/null
+++ b/main/assets/locales/ja-JP/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "ja-JP"
+ },
+ "strings": {
+ "WARNING": "警告",
+ "INFO": "情報",
+ "ERROR": "エラー",
+ "VERSION": "バージョン ",
+ "LOADING_PROTOCOL": "サーバーにログイン中...",
+ "INITIALIZING": "初期化中...",
+ "PIN_ERROR": "SIMカードを挿入してください",
+ "REG_ERROR": "ネットワークに接続できません。ネットワーク状態を確認してください",
+ "DETECTING_MODULE": "モジュールを検出中...",
+ "REGISTERING_NETWORK": "ネットワーク接続待機中...",
+ "CHECKING_NEW_VERSION": "新しいバージョンを確認中...",
+ "CHECK_NEW_VERSION_FAILED": "更新確認に失敗しました。%d 秒後に再試行します: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Wi-Fiに切り替え中...",
+ "SWITCH_TO_4G_NETWORK": "4Gに切り替え中...",
+ "STANDBY": "待機中",
+ "CONNECT_TO": "接続先 ",
+ "CONNECTING": "接続中...",
+ "CONNECTED_TO": "接続完了 ",
+ "LISTENING": "リスニング中...",
+ "SPEAKING": "話しています...",
+ "SERVER_NOT_FOUND": "利用可能なサーバーを探しています",
+ "SERVER_NOT_CONNECTED": "サーバーに接続できません。後でもう一度お試しください",
+ "SERVER_TIMEOUT": "応答待機時間が終了しました",
+ "SERVER_ERROR": "送信に失敗しました。ネットワークを確認してください",
+ "CONNECT_TO_HOTSPOT": "スマートフォンをWi-Fi ",
+ "ACCESS_VIA_BROWSER": " に接続し、ブラウザでアクセスしてください ",
+ "WIFI_CONFIG_MODE": "ネットワーク設定モード",
+ "ENTERING_WIFI_CONFIG_MODE": "ネットワーク設定中...",
+ "SCANNING_WIFI": "Wi-Fiをスキャン中...",
+ "NEW_VERSION": "新しいバージョン ",
+ "OTA_UPGRADE": "OTAアップグレード",
+ "UPGRADING": "システムをアップグレード中...",
+ "UPGRADE_FAILED": "アップグレード失敗",
+ "ACTIVATION": "デバイスをアクティベート",
+ "BATTERY_LOW": "バッテリーが少なくなっています",
+ "BATTERY_CHARGING": "充電中",
+ "BATTERY_FULL": "バッテリー満タン",
+ "BATTERY_NEED_CHARGE": "バッテリーが低下しています。充電してください",
+ "VOLUME": "音量 ",
+ "MUTED": "ミュートされています",
+ "MAX_VOLUME": "最大音量",
+ "RTC_MODE_OFF": "AEC 無効",
+ "RTC_MODE_ON": "AEC 有効",
+ "DOWNLOAD_ASSETS_FAILED": "アセットのダウンロードに失敗しました",
+ "LOADING_ASSETS": "アセットを読み込み中...",
+ "PLEASE_WAIT": "お待ちください...",
+ "FOUND_NEW_ASSETS": "新しいアセットが見つかりました: %s",
+ "HELLO_MY_FRIEND": "こんにちは、友達!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/ja-JP/upgrade.ogg b/main/assets/locales/ja-JP/upgrade.ogg
new file mode 100644
index 0000000..1994026
Binary files /dev/null and b/main/assets/locales/ja-JP/upgrade.ogg differ
diff --git a/main/assets/locales/ja-JP/welcome.ogg b/main/assets/locales/ja-JP/welcome.ogg
new file mode 100644
index 0000000..f2a4f17
Binary files /dev/null and b/main/assets/locales/ja-JP/welcome.ogg differ
diff --git a/main/assets/locales/ja-JP/wificonfig.ogg b/main/assets/locales/ja-JP/wificonfig.ogg
new file mode 100644
index 0000000..16fbf94
Binary files /dev/null and b/main/assets/locales/ja-JP/wificonfig.ogg differ
diff --git a/main/assets/locales/ko-KR/0.ogg b/main/assets/locales/ko-KR/0.ogg
new file mode 100644
index 0000000..7c29fe5
Binary files /dev/null and b/main/assets/locales/ko-KR/0.ogg differ
diff --git a/main/assets/locales/ko-KR/1.ogg b/main/assets/locales/ko-KR/1.ogg
new file mode 100644
index 0000000..031d5d8
Binary files /dev/null and b/main/assets/locales/ko-KR/1.ogg differ
diff --git a/main/assets/locales/ko-KR/2.ogg b/main/assets/locales/ko-KR/2.ogg
new file mode 100644
index 0000000..7a12499
Binary files /dev/null and b/main/assets/locales/ko-KR/2.ogg differ
diff --git a/main/assets/locales/ko-KR/3.ogg b/main/assets/locales/ko-KR/3.ogg
new file mode 100644
index 0000000..c3b7a52
Binary files /dev/null and b/main/assets/locales/ko-KR/3.ogg differ
diff --git a/main/assets/locales/ko-KR/4.ogg b/main/assets/locales/ko-KR/4.ogg
new file mode 100644
index 0000000..79f515e
Binary files /dev/null and b/main/assets/locales/ko-KR/4.ogg differ
diff --git a/main/assets/locales/ko-KR/5.ogg b/main/assets/locales/ko-KR/5.ogg
new file mode 100644
index 0000000..2cc2c65
Binary files /dev/null and b/main/assets/locales/ko-KR/5.ogg differ
diff --git a/main/assets/locales/ko-KR/6.ogg b/main/assets/locales/ko-KR/6.ogg
new file mode 100644
index 0000000..84653eb
Binary files /dev/null and b/main/assets/locales/ko-KR/6.ogg differ
diff --git a/main/assets/locales/ko-KR/7.ogg b/main/assets/locales/ko-KR/7.ogg
new file mode 100644
index 0000000..e3e6515
Binary files /dev/null and b/main/assets/locales/ko-KR/7.ogg differ
diff --git a/main/assets/locales/ko-KR/8.ogg b/main/assets/locales/ko-KR/8.ogg
new file mode 100644
index 0000000..b9b7607
Binary files /dev/null and b/main/assets/locales/ko-KR/8.ogg differ
diff --git a/main/assets/locales/ko-KR/9.ogg b/main/assets/locales/ko-KR/9.ogg
new file mode 100644
index 0000000..9060b21
Binary files /dev/null and b/main/assets/locales/ko-KR/9.ogg differ
diff --git a/main/assets/locales/ko-KR/activation.ogg b/main/assets/locales/ko-KR/activation.ogg
new file mode 100644
index 0000000..1af58af
Binary files /dev/null and b/main/assets/locales/ko-KR/activation.ogg differ
diff --git a/main/assets/locales/ko-KR/err_pin.ogg b/main/assets/locales/ko-KR/err_pin.ogg
new file mode 100644
index 0000000..508e926
Binary files /dev/null and b/main/assets/locales/ko-KR/err_pin.ogg differ
diff --git a/main/assets/locales/ko-KR/err_reg.ogg b/main/assets/locales/ko-KR/err_reg.ogg
new file mode 100644
index 0000000..68561fe
Binary files /dev/null and b/main/assets/locales/ko-KR/err_reg.ogg differ
diff --git a/main/assets/locales/ko-KR/language.json b/main/assets/locales/ko-KR/language.json
new file mode 100644
index 0000000..1d65bfb
--- /dev/null
+++ b/main/assets/locales/ko-KR/language.json
@@ -0,0 +1,56 @@
+{
+ "language": {
+ "type": "ko-KR"
+ },
+ "strings": {
+ "WARNING": "경고",
+ "INFO": "정보",
+ "ERROR": "오류",
+ "VERSION": "버전 ",
+ "LOADING_PROTOCOL": "로그인 중...",
+ "INITIALIZING": "초기화 중...",
+ "PIN_ERROR": "SIM 카드를 삽입하세요",
+ "REG_ERROR": "네트워크에 접속할 수 없습니다. SIM 카드 상태를 확인하세요",
+ "DETECTING_MODULE": "모듈 감지 중...",
+ "REGISTERING_NETWORK": "네트워크 대기 중...",
+ "CHECKING_NEW_VERSION": "새 버전 확인 중...",
+ "CHECK_NEW_VERSION_FAILED": "새 버전 확인에 실패했습니다. %d초 후에 다시 시도합니다: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Wi-Fi로 전환 중...",
+ "SWITCH_TO_4G_NETWORK": "4G로 전환 중...",
+ "STANDBY": "대기",
+ "CONNECT_TO": "연결 대상: ",
+ "CONNECTING": "연결 중...",
+ "CONNECTION_SUCCESSFUL": "연결 성공",
+ "CONNECTED_TO": "연결됨: ",
+ "LISTENING": "듣는 중...",
+ "SPEAKING": "말하는 중...",
+ "SERVER_NOT_FOUND": "사용 가능한 서비스를 찾는 중",
+ "SERVER_NOT_CONNECTED": "서비스에 연결할 수 없습니다. 나중에 다시 시도하세요",
+ "SERVER_TIMEOUT": "응답 대기 시간 초과",
+ "SERVER_ERROR": "전송 실패, 네트워크를 확인하세요",
+ "CONNECT_TO_HOTSPOT": "핫스팟: ",
+ "ACCESS_VIA_BROWSER": " 설정 URL: ",
+ "WIFI_CONFIG_MODE": "Wi-Fi 설정 모드",
+ "ENTERING_WIFI_CONFIG_MODE": "Wi-Fi 설정 모드 진입 중...",
+ "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": "최대 볼륨",
+ "RTC_MODE_OFF": "AEC 끄기",
+ "RTC_MODE_ON": "AEC 켜기",
+ "DOWNLOAD_ASSETS_FAILED": "에셋 다운로드 실패",
+ "LOADING_ASSETS": "에셋 로딩 중...",
+ "PLEASE_WAIT": "잠시 기다려 주세요...",
+ "FOUND_NEW_ASSETS": "새로운 에셋을 발견했습니다: %s",
+ "HELLO_MY_FRIEND": "안녕하세요, 친구!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/ko-KR/upgrade.ogg b/main/assets/locales/ko-KR/upgrade.ogg
new file mode 100644
index 0000000..0656a78
Binary files /dev/null and b/main/assets/locales/ko-KR/upgrade.ogg differ
diff --git a/main/assets/locales/ko-KR/welcome.ogg b/main/assets/locales/ko-KR/welcome.ogg
new file mode 100644
index 0000000..59621f4
Binary files /dev/null and b/main/assets/locales/ko-KR/welcome.ogg differ
diff --git a/main/assets/locales/ko-KR/wificonfig.ogg b/main/assets/locales/ko-KR/wificonfig.ogg
new file mode 100644
index 0000000..e1610af
Binary files /dev/null and b/main/assets/locales/ko-KR/wificonfig.ogg differ
diff --git a/main/assets/locales/pl-PL/0.ogg b/main/assets/locales/pl-PL/0.ogg
new file mode 100644
index 0000000..7351d96
Binary files /dev/null and b/main/assets/locales/pl-PL/0.ogg differ
diff --git a/main/assets/locales/pl-PL/1.ogg b/main/assets/locales/pl-PL/1.ogg
new file mode 100644
index 0000000..342c9cc
Binary files /dev/null and b/main/assets/locales/pl-PL/1.ogg differ
diff --git a/main/assets/locales/pl-PL/2.ogg b/main/assets/locales/pl-PL/2.ogg
new file mode 100644
index 0000000..8fa7c87
Binary files /dev/null and b/main/assets/locales/pl-PL/2.ogg differ
diff --git a/main/assets/locales/pl-PL/3.ogg b/main/assets/locales/pl-PL/3.ogg
new file mode 100644
index 0000000..b656411
Binary files /dev/null and b/main/assets/locales/pl-PL/3.ogg differ
diff --git a/main/assets/locales/pl-PL/4.ogg b/main/assets/locales/pl-PL/4.ogg
new file mode 100644
index 0000000..ea49088
Binary files /dev/null and b/main/assets/locales/pl-PL/4.ogg differ
diff --git a/main/assets/locales/pl-PL/5.ogg b/main/assets/locales/pl-PL/5.ogg
new file mode 100644
index 0000000..8d4b3b9
Binary files /dev/null and b/main/assets/locales/pl-PL/5.ogg differ
diff --git a/main/assets/locales/pl-PL/6.ogg b/main/assets/locales/pl-PL/6.ogg
new file mode 100644
index 0000000..7c8fefd
Binary files /dev/null and b/main/assets/locales/pl-PL/6.ogg differ
diff --git a/main/assets/locales/pl-PL/7.ogg b/main/assets/locales/pl-PL/7.ogg
new file mode 100644
index 0000000..7139591
Binary files /dev/null and b/main/assets/locales/pl-PL/7.ogg differ
diff --git a/main/assets/locales/pl-PL/8.ogg b/main/assets/locales/pl-PL/8.ogg
new file mode 100644
index 0000000..8e2dd56
Binary files /dev/null and b/main/assets/locales/pl-PL/8.ogg differ
diff --git a/main/assets/locales/pl-PL/9.ogg b/main/assets/locales/pl-PL/9.ogg
new file mode 100644
index 0000000..b88ab87
Binary files /dev/null and b/main/assets/locales/pl-PL/9.ogg differ
diff --git a/main/assets/locales/pl-PL/activation.ogg b/main/assets/locales/pl-PL/activation.ogg
new file mode 100644
index 0000000..7c1ff9d
Binary files /dev/null and b/main/assets/locales/pl-PL/activation.ogg differ
diff --git a/main/assets/locales/pl-PL/err_pin.ogg b/main/assets/locales/pl-PL/err_pin.ogg
new file mode 100644
index 0000000..4d35f3c
Binary files /dev/null and b/main/assets/locales/pl-PL/err_pin.ogg differ
diff --git a/main/assets/locales/pl-PL/err_reg.ogg b/main/assets/locales/pl-PL/err_reg.ogg
new file mode 100644
index 0000000..35d1452
Binary files /dev/null and b/main/assets/locales/pl-PL/err_reg.ogg differ
diff --git a/main/assets/locales/pl-PL/language.json b/main/assets/locales/pl-PL/language.json
new file mode 100644
index 0000000..b54c292
--- /dev/null
+++ b/main/assets/locales/pl-PL/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "pl-PL"
+ },
+ "strings": {
+ "WARNING": "Ostrzeżenie",
+ "INFO": "Informacja",
+ "ERROR": "Błąd",
+ "VERSION": "Wersja ",
+ "LOADING_PROTOCOL": "Łączenie z serwerem...",
+ "INITIALIZING": "Inicjalizacja...",
+ "PIN_ERROR": "Proszę włożyć kartę SIM",
+ "REG_ERROR": "Nie można uzyskać dostępu do sieci, sprawdź stan karty danych",
+ "DETECTING_MODULE": "Wykrywanie modułu...",
+ "REGISTERING_NETWORK": "Oczekiwanie na sieć...",
+ "CHECKING_NEW_VERSION": "Sprawdzanie nowej wersji...",
+ "CHECK_NEW_VERSION_FAILED": "Sprawdzanie nowej wersji nie powiodło się, ponowna próba za %d sekund: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Przełączanie na Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Przełączanie na 4G...",
+ "STANDBY": "Gotowość",
+ "CONNECT_TO": "Połącz z ",
+ "CONNECTING": "Łączenie...",
+ "CONNECTED_TO": "Połączono z ",
+ "LISTENING": "Słuchanie...",
+ "SPEAKING": "Mówienie...",
+ "SERVER_NOT_FOUND": "Szukanie dostępnej usługi",
+ "SERVER_NOT_CONNECTED": "Nie można połączyć się z usługą, spróbuj ponownie później",
+ "SERVER_TIMEOUT": "Przekroczono czas oczekiwania na odpowiedź",
+ "SERVER_ERROR": "Wysyłanie nie powiodło się, sprawdź sieć",
+ "CONNECT_TO_HOTSPOT": "Podłącz telefon do hotspotu ",
+ "ACCESS_VIA_BROWSER": ",dostęp przez przeglądarkę ",
+ "WIFI_CONFIG_MODE": "Tryb konfiguracji sieci",
+ "ENTERING_WIFI_CONFIG_MODE": "Wchodzenie w tryb konfiguracji sieci...",
+ "SCANNING_WIFI": "Skanowanie Wi-Fi...",
+ "NEW_VERSION": "Nowa wersja ",
+ "OTA_UPGRADE": "Aktualizacja OTA",
+ "UPGRADING": "Aktualizacja systemu...",
+ "UPGRADE_FAILED": "Aktualizacja nie powiodła się",
+ "ACTIVATION": "Aktywacja urządzenia",
+ "BATTERY_LOW": "Niski poziom baterii",
+ "BATTERY_CHARGING": "Ładowanie",
+ "BATTERY_FULL": "Bateria pełna",
+ "BATTERY_NEED_CHARGE": "Niski poziom baterii, proszę naładować",
+ "VOLUME": "Głośność ",
+ "MUTED": "Wyciszony",
+ "MAX_VOLUME": "Maksymalna głośność",
+ "RTC_MODE_OFF": "AEC wyłączony",
+ "RTC_MODE_ON": "AEC włączony",
+ "DOWNLOAD_ASSETS_FAILED": "Nie udało się pobrać zasobów",
+ "LOADING_ASSETS": "Ładowanie zasobów...",
+ "PLEASE_WAIT": "Proszę czekać...",
+ "FOUND_NEW_ASSETS": "Znaleziono nowe zasoby: %s",
+ "HELLO_MY_FRIEND": "Cześć, mój przyjacielu!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/pl-PL/upgrade.ogg b/main/assets/locales/pl-PL/upgrade.ogg
new file mode 100644
index 0000000..041eee8
Binary files /dev/null and b/main/assets/locales/pl-PL/upgrade.ogg differ
diff --git a/main/assets/locales/pl-PL/welcome.ogg b/main/assets/locales/pl-PL/welcome.ogg
new file mode 100644
index 0000000..140db7d
Binary files /dev/null and b/main/assets/locales/pl-PL/welcome.ogg differ
diff --git a/main/assets/locales/pl-PL/wificonfig.ogg b/main/assets/locales/pl-PL/wificonfig.ogg
new file mode 100644
index 0000000..0d2222f
Binary files /dev/null and b/main/assets/locales/pl-PL/wificonfig.ogg differ
diff --git a/main/assets/locales/pt-PT/0.ogg b/main/assets/locales/pt-PT/0.ogg
new file mode 100644
index 0000000..80c9f8b
Binary files /dev/null and b/main/assets/locales/pt-PT/0.ogg differ
diff --git a/main/assets/locales/pt-PT/1.ogg b/main/assets/locales/pt-PT/1.ogg
new file mode 100644
index 0000000..4299bc8
Binary files /dev/null and b/main/assets/locales/pt-PT/1.ogg differ
diff --git a/main/assets/locales/pt-PT/2.ogg b/main/assets/locales/pt-PT/2.ogg
new file mode 100644
index 0000000..fa67197
Binary files /dev/null and b/main/assets/locales/pt-PT/2.ogg differ
diff --git a/main/assets/locales/pt-PT/3.ogg b/main/assets/locales/pt-PT/3.ogg
new file mode 100644
index 0000000..da2b131
Binary files /dev/null and b/main/assets/locales/pt-PT/3.ogg differ
diff --git a/main/assets/locales/pt-PT/4.ogg b/main/assets/locales/pt-PT/4.ogg
new file mode 100644
index 0000000..2c593f6
Binary files /dev/null and b/main/assets/locales/pt-PT/4.ogg differ
diff --git a/main/assets/locales/pt-PT/5.ogg b/main/assets/locales/pt-PT/5.ogg
new file mode 100644
index 0000000..62d670d
Binary files /dev/null and b/main/assets/locales/pt-PT/5.ogg differ
diff --git a/main/assets/locales/pt-PT/6.ogg b/main/assets/locales/pt-PT/6.ogg
new file mode 100644
index 0000000..32ea27b
Binary files /dev/null and b/main/assets/locales/pt-PT/6.ogg differ
diff --git a/main/assets/locales/pt-PT/7.ogg b/main/assets/locales/pt-PT/7.ogg
new file mode 100644
index 0000000..c8542ed
Binary files /dev/null and b/main/assets/locales/pt-PT/7.ogg differ
diff --git a/main/assets/locales/pt-PT/8.ogg b/main/assets/locales/pt-PT/8.ogg
new file mode 100644
index 0000000..8e54738
Binary files /dev/null and b/main/assets/locales/pt-PT/8.ogg differ
diff --git a/main/assets/locales/pt-PT/9.ogg b/main/assets/locales/pt-PT/9.ogg
new file mode 100644
index 0000000..e97c8fc
Binary files /dev/null and b/main/assets/locales/pt-PT/9.ogg differ
diff --git a/main/assets/locales/pt-PT/activation.ogg b/main/assets/locales/pt-PT/activation.ogg
new file mode 100644
index 0000000..1c980ac
Binary files /dev/null and b/main/assets/locales/pt-PT/activation.ogg differ
diff --git a/main/assets/locales/pt-PT/err_pin.ogg b/main/assets/locales/pt-PT/err_pin.ogg
new file mode 100644
index 0000000..ceb2223
Binary files /dev/null and b/main/assets/locales/pt-PT/err_pin.ogg differ
diff --git a/main/assets/locales/pt-PT/err_reg.ogg b/main/assets/locales/pt-PT/err_reg.ogg
new file mode 100644
index 0000000..2fa4350
Binary files /dev/null and b/main/assets/locales/pt-PT/err_reg.ogg differ
diff --git a/main/assets/locales/pt-PT/language.json b/main/assets/locales/pt-PT/language.json
new file mode 100644
index 0000000..da0e0e2
--- /dev/null
+++ b/main/assets/locales/pt-PT/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "pt-PT"
+ },
+ "strings": {
+ "WARNING": "Aviso",
+ "INFO": "Informação",
+ "ERROR": "Erro",
+ "VERSION": "Versão ",
+ "LOADING_PROTOCOL": "Ligando ao servidor...",
+ "INITIALIZING": "A inicializar...",
+ "PIN_ERROR": "Por favor insira o cartão SIM",
+ "REG_ERROR": "Não é possível aceder à rede, verifique o estado do cartão de dados",
+ "DETECTING_MODULE": "A detectar módulo...",
+ "REGISTERING_NETWORK": "À espera da rede...",
+ "CHECKING_NEW_VERSION": "A verificar nova versão...",
+ "CHECK_NEW_VERSION_FAILED": "Falha na verificação de nova versão, nova tentativa em %d segundos: %s",
+ "SWITCH_TO_WIFI_NETWORK": "A mudar para Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "A mudar para 4G...",
+ "STANDBY": "Em espera",
+ "CONNECT_TO": "Ligar a ",
+ "CONNECTING": "A ligar...",
+ "CONNECTED_TO": "Ligado a ",
+ "LISTENING": "A escutar...",
+ "SPEAKING": "A falar...",
+ "SERVER_NOT_FOUND": "A procurar serviço disponível",
+ "SERVER_NOT_CONNECTED": "Não é possível ligar ao serviço, tente mais tarde",
+ "SERVER_TIMEOUT": "Tempo limite de resposta",
+ "SERVER_ERROR": "Falha no envio, verifique a rede",
+ "CONNECT_TO_HOTSPOT": "Ligue o telefone ao hotspot ",
+ "ACCESS_VIA_BROWSER": ",aceder através do navegador ",
+ "WIFI_CONFIG_MODE": "Modo de configuração de rede",
+ "ENTERING_WIFI_CONFIG_MODE": "A entrar no modo de configuração de rede...",
+ "SCANNING_WIFI": "A procurar Wi-Fi...",
+ "NEW_VERSION": "Nova versão ",
+ "OTA_UPGRADE": "Atualização OTA",
+ "UPGRADING": "A atualizar sistema...",
+ "UPGRADE_FAILED": "Atualização falhada",
+ "ACTIVATION": "Ativação do dispositivo",
+ "BATTERY_LOW": "Bateria fraca",
+ "BATTERY_CHARGING": "A carregar",
+ "BATTERY_FULL": "Bateria cheia",
+ "BATTERY_NEED_CHARGE": "Bateria fraca, por favor carregue",
+ "VOLUME": "Volume ",
+ "MUTED": "Silenciado",
+ "MAX_VOLUME": "Volume máximo",
+ "RTC_MODE_OFF": "AEC desligado",
+ "RTC_MODE_ON": "AEC ligado",
+ "DOWNLOAD_ASSETS_FAILED": "Falha ao descarregar recursos",
+ "LOADING_ASSETS": "A carregar recursos...",
+ "PLEASE_WAIT": "Por favor aguarde...",
+ "FOUND_NEW_ASSETS": "Encontrados novos recursos: %s",
+ "HELLO_MY_FRIEND": "Olá, meu amigo!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/pt-PT/upgrade.ogg b/main/assets/locales/pt-PT/upgrade.ogg
new file mode 100644
index 0000000..76bac60
Binary files /dev/null and b/main/assets/locales/pt-PT/upgrade.ogg differ
diff --git a/main/assets/locales/pt-PT/welcome.ogg b/main/assets/locales/pt-PT/welcome.ogg
new file mode 100644
index 0000000..74daf23
Binary files /dev/null and b/main/assets/locales/pt-PT/welcome.ogg differ
diff --git a/main/assets/locales/pt-PT/wificonfig.ogg b/main/assets/locales/pt-PT/wificonfig.ogg
new file mode 100644
index 0000000..784939f
Binary files /dev/null and b/main/assets/locales/pt-PT/wificonfig.ogg differ
diff --git a/main/assets/locales/ro-RO/0.ogg b/main/assets/locales/ro-RO/0.ogg
new file mode 100644
index 0000000..67fd181
Binary files /dev/null and b/main/assets/locales/ro-RO/0.ogg differ
diff --git a/main/assets/locales/ro-RO/1.ogg b/main/assets/locales/ro-RO/1.ogg
new file mode 100644
index 0000000..291be66
Binary files /dev/null and b/main/assets/locales/ro-RO/1.ogg differ
diff --git a/main/assets/locales/ro-RO/2.ogg b/main/assets/locales/ro-RO/2.ogg
new file mode 100644
index 0000000..54caf95
Binary files /dev/null and b/main/assets/locales/ro-RO/2.ogg differ
diff --git a/main/assets/locales/ro-RO/3.ogg b/main/assets/locales/ro-RO/3.ogg
new file mode 100644
index 0000000..b93d5c1
Binary files /dev/null and b/main/assets/locales/ro-RO/3.ogg differ
diff --git a/main/assets/locales/ro-RO/4.ogg b/main/assets/locales/ro-RO/4.ogg
new file mode 100644
index 0000000..3feff21
Binary files /dev/null and b/main/assets/locales/ro-RO/4.ogg differ
diff --git a/main/assets/locales/ro-RO/5.ogg b/main/assets/locales/ro-RO/5.ogg
new file mode 100644
index 0000000..fe56930
Binary files /dev/null and b/main/assets/locales/ro-RO/5.ogg differ
diff --git a/main/assets/locales/ro-RO/6.ogg b/main/assets/locales/ro-RO/6.ogg
new file mode 100644
index 0000000..ed8aee9
Binary files /dev/null and b/main/assets/locales/ro-RO/6.ogg differ
diff --git a/main/assets/locales/ro-RO/7.ogg b/main/assets/locales/ro-RO/7.ogg
new file mode 100644
index 0000000..9ce02c0
Binary files /dev/null and b/main/assets/locales/ro-RO/7.ogg differ
diff --git a/main/assets/locales/ro-RO/8.ogg b/main/assets/locales/ro-RO/8.ogg
new file mode 100644
index 0000000..c5d3184
Binary files /dev/null and b/main/assets/locales/ro-RO/8.ogg differ
diff --git a/main/assets/locales/ro-RO/9.ogg b/main/assets/locales/ro-RO/9.ogg
new file mode 100644
index 0000000..1d156c7
Binary files /dev/null and b/main/assets/locales/ro-RO/9.ogg differ
diff --git a/main/assets/locales/ro-RO/activation.ogg b/main/assets/locales/ro-RO/activation.ogg
new file mode 100644
index 0000000..d2d91fb
Binary files /dev/null and b/main/assets/locales/ro-RO/activation.ogg differ
diff --git a/main/assets/locales/ro-RO/err_pin.ogg b/main/assets/locales/ro-RO/err_pin.ogg
new file mode 100644
index 0000000..202000c
Binary files /dev/null and b/main/assets/locales/ro-RO/err_pin.ogg differ
diff --git a/main/assets/locales/ro-RO/err_reg.ogg b/main/assets/locales/ro-RO/err_reg.ogg
new file mode 100644
index 0000000..eafcabc
Binary files /dev/null and b/main/assets/locales/ro-RO/err_reg.ogg differ
diff --git a/main/assets/locales/ro-RO/language.json b/main/assets/locales/ro-RO/language.json
new file mode 100644
index 0000000..d8ec774
--- /dev/null
+++ b/main/assets/locales/ro-RO/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "ro-RO"
+ },
+ "strings": {
+ "WARNING": "Avertisment",
+ "INFO": "Informație",
+ "ERROR": "Eroare",
+ "VERSION": "Versiune ",
+ "LOADING_PROTOCOL": "Se conectează la server...",
+ "INITIALIZING": "Se inițializează...",
+ "PIN_ERROR": "Vă rugăm să introduceți cardul SIM",
+ "REG_ERROR": "Nu se poate accesa rețeaua, verificați starea cardului de date",
+ "DETECTING_MODULE": "Se detectează modulul...",
+ "REGISTERING_NETWORK": "Se așteaptă rețeaua...",
+ "CHECKING_NEW_VERSION": "Se verifică versiunea nouă...",
+ "CHECK_NEW_VERSION_FAILED": "Verificarea versiunii noi a eșuat, se reîncearcă în %d secunde: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Se comută la Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Se comută la 4G...",
+ "STANDBY": "În așteptare",
+ "CONNECT_TO": "Conectare la ",
+ "CONNECTING": "Se conectează...",
+ "CONNECTED_TO": "Conectat la ",
+ "LISTENING": "Se ascultă...",
+ "SPEAKING": "Se vorbește...",
+ "SERVER_NOT_FOUND": "Se caută serviciul disponibil",
+ "SERVER_NOT_CONNECTED": "Nu se poate conecta la serviciu, încercați mai târziu",
+ "SERVER_TIMEOUT": "Timpul de răspuns a expirat",
+ "SERVER_ERROR": "Trimiterea a eșuat, verificați rețeaua",
+ "CONNECT_TO_HOTSPOT": "Conectați telefonul la hotspot ",
+ "ACCESS_VIA_BROWSER": ",accesați prin browser ",
+ "WIFI_CONFIG_MODE": "Modul de configurare rețea",
+ "ENTERING_WIFI_CONFIG_MODE": "Se intră în modul de configurare rețea...",
+ "SCANNING_WIFI": "Se scanează Wi-Fi...",
+ "NEW_VERSION": "Versiune nouă ",
+ "OTA_UPGRADE": "Actualizare OTA",
+ "UPGRADING": "Se actualizează sistemul...",
+ "UPGRADE_FAILED": "Actualizarea a eșuat",
+ "ACTIVATION": "Activarea dispozitivului",
+ "BATTERY_LOW": "Baterie scăzută",
+ "BATTERY_CHARGING": "Se încarcă",
+ "BATTERY_FULL": "Baterie plină",
+ "BATTERY_NEED_CHARGE": "Baterie scăzută, vă rugăm să încărcați",
+ "VOLUME": "Volum ",
+ "MUTED": "Silențios",
+ "MAX_VOLUME": "Volum maxim",
+ "RTC_MODE_OFF": "AEC oprit",
+ "RTC_MODE_ON": "AEC pornit",
+ "DOWNLOAD_ASSETS_FAILED": "Eșec la descărcarea resurselor",
+ "LOADING_ASSETS": "Se încarcă resursele...",
+ "PLEASE_WAIT": "Vă rugăm să așteptați...",
+ "FOUND_NEW_ASSETS": "S-au găsit resurse noi: %s",
+ "HELLO_MY_FRIEND": "Salut, prietenul meu!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/ro-RO/upgrade.ogg b/main/assets/locales/ro-RO/upgrade.ogg
new file mode 100644
index 0000000..f4b5618
Binary files /dev/null and b/main/assets/locales/ro-RO/upgrade.ogg differ
diff --git a/main/assets/locales/ro-RO/welcome.ogg b/main/assets/locales/ro-RO/welcome.ogg
new file mode 100644
index 0000000..673f3d8
Binary files /dev/null and b/main/assets/locales/ro-RO/welcome.ogg differ
diff --git a/main/assets/locales/ro-RO/wificonfig.ogg b/main/assets/locales/ro-RO/wificonfig.ogg
new file mode 100644
index 0000000..d536cdc
Binary files /dev/null and b/main/assets/locales/ro-RO/wificonfig.ogg differ
diff --git a/main/assets/locales/ru-RU/0.ogg b/main/assets/locales/ru-RU/0.ogg
new file mode 100644
index 0000000..4c091eb
Binary files /dev/null and b/main/assets/locales/ru-RU/0.ogg differ
diff --git a/main/assets/locales/ru-RU/1.ogg b/main/assets/locales/ru-RU/1.ogg
new file mode 100644
index 0000000..8a6e69b
Binary files /dev/null and b/main/assets/locales/ru-RU/1.ogg differ
diff --git a/main/assets/locales/ru-RU/2.ogg b/main/assets/locales/ru-RU/2.ogg
new file mode 100644
index 0000000..d4b29cf
Binary files /dev/null and b/main/assets/locales/ru-RU/2.ogg differ
diff --git a/main/assets/locales/ru-RU/3.ogg b/main/assets/locales/ru-RU/3.ogg
new file mode 100644
index 0000000..6b2498b
Binary files /dev/null and b/main/assets/locales/ru-RU/3.ogg differ
diff --git a/main/assets/locales/ru-RU/4.ogg b/main/assets/locales/ru-RU/4.ogg
new file mode 100644
index 0000000..2aa1343
Binary files /dev/null and b/main/assets/locales/ru-RU/4.ogg differ
diff --git a/main/assets/locales/ru-RU/5.ogg b/main/assets/locales/ru-RU/5.ogg
new file mode 100644
index 0000000..768081c
Binary files /dev/null and b/main/assets/locales/ru-RU/5.ogg differ
diff --git a/main/assets/locales/ru-RU/6.ogg b/main/assets/locales/ru-RU/6.ogg
new file mode 100644
index 0000000..0a6e23d
Binary files /dev/null and b/main/assets/locales/ru-RU/6.ogg differ
diff --git a/main/assets/locales/ru-RU/7.ogg b/main/assets/locales/ru-RU/7.ogg
new file mode 100644
index 0000000..060be36
Binary files /dev/null and b/main/assets/locales/ru-RU/7.ogg differ
diff --git a/main/assets/locales/ru-RU/8.ogg b/main/assets/locales/ru-RU/8.ogg
new file mode 100644
index 0000000..30093ae
Binary files /dev/null and b/main/assets/locales/ru-RU/8.ogg differ
diff --git a/main/assets/locales/ru-RU/9.ogg b/main/assets/locales/ru-RU/9.ogg
new file mode 100644
index 0000000..2e07653
Binary files /dev/null and b/main/assets/locales/ru-RU/9.ogg differ
diff --git a/main/assets/locales/ru-RU/activation.ogg b/main/assets/locales/ru-RU/activation.ogg
new file mode 100644
index 0000000..050b12f
Binary files /dev/null and b/main/assets/locales/ru-RU/activation.ogg differ
diff --git a/main/assets/locales/ru-RU/err_pin.ogg b/main/assets/locales/ru-RU/err_pin.ogg
new file mode 100644
index 0000000..e9f0b16
Binary files /dev/null and b/main/assets/locales/ru-RU/err_pin.ogg differ
diff --git a/main/assets/locales/ru-RU/err_reg.ogg b/main/assets/locales/ru-RU/err_reg.ogg
new file mode 100644
index 0000000..8be3b18
Binary files /dev/null and b/main/assets/locales/ru-RU/err_reg.ogg differ
diff --git a/main/assets/locales/ru-RU/language.json b/main/assets/locales/ru-RU/language.json
new file mode 100644
index 0000000..ebac606
--- /dev/null
+++ b/main/assets/locales/ru-RU/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "ru-RU"
+ },
+ "strings": {
+ "WARNING": "Предупреждение",
+ "INFO": "Информация",
+ "ERROR": "Ошибка",
+ "VERSION": "Версия ",
+ "LOADING_PROTOCOL": "Подключение к серверу...",
+ "INITIALIZING": "Инициализация...",
+ "PIN_ERROR": "Пожалуйста, вставьте SIM-карту",
+ "REG_ERROR": "Невозможно подключиться к сети, проверьте состояние карты данных",
+ "DETECTING_MODULE": "Обнаружение модуля...",
+ "REGISTERING_NETWORK": "Ожидание сети...",
+ "CHECKING_NEW_VERSION": "Проверка новой версии...",
+ "CHECK_NEW_VERSION_FAILED": "Ошибка проверки новой версии, повтор через %d секунд: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Переключение на Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Переключение на 4G...",
+ "STANDBY": "Ожидание",
+ "CONNECT_TO": "Подключение к ",
+ "CONNECTING": "Подключение...",
+ "CONNECTED_TO": "Подключено к ",
+ "LISTENING": "Прослушивание...",
+ "SPEAKING": "Говорение...",
+ "SERVER_NOT_FOUND": "Поиск доступного сервиса",
+ "SERVER_NOT_CONNECTED": "Невозможно подключиться к сервису, попробуйте позже",
+ "SERVER_TIMEOUT": "Тайм-аут ответа",
+ "SERVER_ERROR": "Ошибка отправки, проверьте сеть",
+ "CONNECT_TO_HOTSPOT": "Подключите телефон к точке доступа ",
+ "ACCESS_VIA_BROWSER": ",доступ через браузер ",
+ "WIFI_CONFIG_MODE": "Режим настройки сети",
+ "ENTERING_WIFI_CONFIG_MODE": "Вход в режим настройки сети...",
+ "SCANNING_WIFI": "Сканирование Wi-Fi...",
+ "NEW_VERSION": "Новая версия ",
+ "OTA_UPGRADE": "Обновление OTA",
+ "UPGRADING": "Обновление системы...",
+ "UPGRADE_FAILED": "Обновление не удалось",
+ "ACTIVATION": "Активация устройства",
+ "BATTERY_LOW": "Низкий заряд батареи",
+ "BATTERY_CHARGING": "Зарядка",
+ "BATTERY_FULL": "Батарея полная",
+ "BATTERY_NEED_CHARGE": "Низкий заряд, пожалуйста, зарядите",
+ "VOLUME": "Громкость ",
+ "MUTED": "Звук отключен",
+ "MAX_VOLUME": "Максимальная громкость",
+ "RTC_MODE_OFF": "AEC выключен",
+ "RTC_MODE_ON": "AEC включен",
+ "DOWNLOAD_ASSETS_FAILED": "Не удалось загрузить ресурсы",
+ "LOADING_ASSETS": "Загрузка ресурсов...",
+ "PLEASE_WAIT": "Пожалуйста, подождите...",
+ "FOUND_NEW_ASSETS": "Найдены новые ресурсы: %s",
+ "HELLO_MY_FRIEND": "Привет, мой друг!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/ru-RU/upgrade.ogg b/main/assets/locales/ru-RU/upgrade.ogg
new file mode 100644
index 0000000..fdb02d4
Binary files /dev/null and b/main/assets/locales/ru-RU/upgrade.ogg differ
diff --git a/main/assets/locales/ru-RU/welcome.ogg b/main/assets/locales/ru-RU/welcome.ogg
new file mode 100644
index 0000000..116aa37
Binary files /dev/null and b/main/assets/locales/ru-RU/welcome.ogg differ
diff --git a/main/assets/locales/ru-RU/wificonfig.ogg b/main/assets/locales/ru-RU/wificonfig.ogg
new file mode 100644
index 0000000..7185680
Binary files /dev/null and b/main/assets/locales/ru-RU/wificonfig.ogg differ
diff --git a/main/assets/locales/th-TH/0.ogg b/main/assets/locales/th-TH/0.ogg
new file mode 100644
index 0000000..15e2544
Binary files /dev/null and b/main/assets/locales/th-TH/0.ogg differ
diff --git a/main/assets/locales/th-TH/1.ogg b/main/assets/locales/th-TH/1.ogg
new file mode 100644
index 0000000..87753b3
Binary files /dev/null and b/main/assets/locales/th-TH/1.ogg differ
diff --git a/main/assets/locales/th-TH/2.ogg b/main/assets/locales/th-TH/2.ogg
new file mode 100644
index 0000000..057a6f3
Binary files /dev/null and b/main/assets/locales/th-TH/2.ogg differ
diff --git a/main/assets/locales/th-TH/3.ogg b/main/assets/locales/th-TH/3.ogg
new file mode 100644
index 0000000..104d51b
Binary files /dev/null and b/main/assets/locales/th-TH/3.ogg differ
diff --git a/main/assets/locales/th-TH/4.ogg b/main/assets/locales/th-TH/4.ogg
new file mode 100644
index 0000000..a35befc
Binary files /dev/null and b/main/assets/locales/th-TH/4.ogg differ
diff --git a/main/assets/locales/th-TH/5.ogg b/main/assets/locales/th-TH/5.ogg
new file mode 100644
index 0000000..8eb3b5b
Binary files /dev/null and b/main/assets/locales/th-TH/5.ogg differ
diff --git a/main/assets/locales/th-TH/6.ogg b/main/assets/locales/th-TH/6.ogg
new file mode 100644
index 0000000..b8cbbe0
Binary files /dev/null and b/main/assets/locales/th-TH/6.ogg differ
diff --git a/main/assets/locales/th-TH/7.ogg b/main/assets/locales/th-TH/7.ogg
new file mode 100644
index 0000000..e93c9a4
Binary files /dev/null and b/main/assets/locales/th-TH/7.ogg differ
diff --git a/main/assets/locales/th-TH/8.ogg b/main/assets/locales/th-TH/8.ogg
new file mode 100644
index 0000000..becde0e
Binary files /dev/null and b/main/assets/locales/th-TH/8.ogg differ
diff --git a/main/assets/locales/th-TH/9.ogg b/main/assets/locales/th-TH/9.ogg
new file mode 100644
index 0000000..319ef67
Binary files /dev/null and b/main/assets/locales/th-TH/9.ogg differ
diff --git a/main/assets/locales/th-TH/activation.ogg b/main/assets/locales/th-TH/activation.ogg
new file mode 100644
index 0000000..983e4d8
Binary files /dev/null and b/main/assets/locales/th-TH/activation.ogg differ
diff --git a/main/assets/locales/th-TH/err_pin.ogg b/main/assets/locales/th-TH/err_pin.ogg
new file mode 100644
index 0000000..059cc8f
Binary files /dev/null and b/main/assets/locales/th-TH/err_pin.ogg differ
diff --git a/main/assets/locales/th-TH/err_reg.ogg b/main/assets/locales/th-TH/err_reg.ogg
new file mode 100644
index 0000000..042c47f
Binary files /dev/null and b/main/assets/locales/th-TH/err_reg.ogg differ
diff --git a/main/assets/locales/th-TH/language.json b/main/assets/locales/th-TH/language.json
new file mode 100644
index 0000000..e97fed1
--- /dev/null
+++ b/main/assets/locales/th-TH/language.json
@@ -0,0 +1,56 @@
+{
+ "language": {
+ "type": "th-TH"
+ },
+ "strings": {
+ "WARNING": "คำเตือน",
+ "INFO": "ข้อมูล",
+ "ERROR": "ข้อผิดพลาด",
+ "VERSION": "เวอร์ชัน ",
+ "LOADING_PROTOCOL": "กำลังเข้าสู่ระบบ...",
+ "INITIALIZING": "กำลังเริ่มต้นระบบ...",
+ "PIN_ERROR": "กรุณาใส่ซิมการ์ด",
+ "REG_ERROR": "ไม่สามารถเข้าถึงเครือข่ายได้ กรุณาตรวจสอบสถานะซิมการ์ด",
+ "DETECTING_MODULE": "กำลังตรวจจับโมดูล...",
+ "REGISTERING_NETWORK": "กำลังรอเครือข่าย...",
+ "CHECKING_NEW_VERSION": "กำลังตรวจสอบเวอร์ชันใหม่...",
+ "CHECK_NEW_VERSION_FAILED": "การตรวจสอบเวอร์ชันใหม่ล้มเหลว จะลองใหม่ใน %d วินาที: %s",
+ "SWITCH_TO_WIFI_NETWORK": "กำลังเปลี่ยนเป็น Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "กำลังเปลี่ยนเป็น 4G...",
+ "STANDBY": "พร้อม",
+ "CONNECT_TO": "เชื่อมต่อกับ ",
+ "CONNECTING": "กำลังเชื่อมต่อ...",
+ "CONNECTION_SUCCESSFUL": "เชื่อมต่อสำเร็จ",
+ "CONNECTED_TO": "เชื่อมต่อกับ ",
+ "LISTENING": "กำลังฟัง...",
+ "SPEAKING": "กำลังพูด...",
+ "SERVER_NOT_FOUND": "กำลังค้นหาบริการที่ใช้งานได้",
+ "SERVER_NOT_CONNECTED": "ไม่สามารถเชื่อมต่อกับบริการได้ กรุณาลองใหม่ในภายหลัง",
+ "SERVER_TIMEOUT": "หมดเวลารอการตอบกลับ",
+ "SERVER_ERROR": "การส่งข้อมูลล้มเหลว กรุณาตรวจสอบเครือข่าย",
+ "CONNECT_TO_HOTSPOT": "ฮอตสปอต: ",
+ "ACCESS_VIA_BROWSER": " URL การตั้งค่า: ",
+ "WIFI_CONFIG_MODE": "โหมดการตั้งค่า Wi-Fi",
+ "ENTERING_WIFI_CONFIG_MODE": "กำลังเข้าสู่โหมดการตั้งค่า Wi-Fi...",
+ "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": "เสียงสูงสุด",
+ "RTC_MODE_OFF": "ปิด AEC",
+ "RTC_MODE_ON": "เปิด AEC",
+ "DOWNLOAD_ASSETS_FAILED": "ดาวน์โหลดทรัพยากรล้มเหลว",
+ "LOADING_ASSETS": "กำลังโหลดทรัพยากร...",
+ "PLEASE_WAIT": "กรุณารอสักครู่...",
+ "FOUND_NEW_ASSETS": "พบทรัพยากรใหม่: %s",
+ "HELLO_MY_FRIEND": "สวัสดี เพื่อนของฉัน!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/th-TH/upgrade.ogg b/main/assets/locales/th-TH/upgrade.ogg
new file mode 100644
index 0000000..e024638
Binary files /dev/null and b/main/assets/locales/th-TH/upgrade.ogg differ
diff --git a/main/assets/locales/th-TH/welcome.ogg b/main/assets/locales/th-TH/welcome.ogg
new file mode 100644
index 0000000..0b32b80
Binary files /dev/null and b/main/assets/locales/th-TH/welcome.ogg differ
diff --git a/main/assets/locales/th-TH/wificonfig.ogg b/main/assets/locales/th-TH/wificonfig.ogg
new file mode 100644
index 0000000..984d217
Binary files /dev/null and b/main/assets/locales/th-TH/wificonfig.ogg differ
diff --git a/main/assets/locales/tr-TR/0.ogg b/main/assets/locales/tr-TR/0.ogg
new file mode 100644
index 0000000..9ab9285
Binary files /dev/null and b/main/assets/locales/tr-TR/0.ogg differ
diff --git a/main/assets/locales/tr-TR/1.ogg b/main/assets/locales/tr-TR/1.ogg
new file mode 100644
index 0000000..131d774
Binary files /dev/null and b/main/assets/locales/tr-TR/1.ogg differ
diff --git a/main/assets/locales/tr-TR/2.ogg b/main/assets/locales/tr-TR/2.ogg
new file mode 100644
index 0000000..5e80f6e
Binary files /dev/null and b/main/assets/locales/tr-TR/2.ogg differ
diff --git a/main/assets/locales/tr-TR/3.ogg b/main/assets/locales/tr-TR/3.ogg
new file mode 100644
index 0000000..bdc0550
Binary files /dev/null and b/main/assets/locales/tr-TR/3.ogg differ
diff --git a/main/assets/locales/tr-TR/4.ogg b/main/assets/locales/tr-TR/4.ogg
new file mode 100644
index 0000000..fd39d2d
Binary files /dev/null and b/main/assets/locales/tr-TR/4.ogg differ
diff --git a/main/assets/locales/tr-TR/5.ogg b/main/assets/locales/tr-TR/5.ogg
new file mode 100644
index 0000000..30d093e
Binary files /dev/null and b/main/assets/locales/tr-TR/5.ogg differ
diff --git a/main/assets/locales/tr-TR/6.ogg b/main/assets/locales/tr-TR/6.ogg
new file mode 100644
index 0000000..95a6f6d
Binary files /dev/null and b/main/assets/locales/tr-TR/6.ogg differ
diff --git a/main/assets/locales/tr-TR/7.ogg b/main/assets/locales/tr-TR/7.ogg
new file mode 100644
index 0000000..9ba5ebc
Binary files /dev/null and b/main/assets/locales/tr-TR/7.ogg differ
diff --git a/main/assets/locales/tr-TR/8.ogg b/main/assets/locales/tr-TR/8.ogg
new file mode 100644
index 0000000..9a4d8c8
Binary files /dev/null and b/main/assets/locales/tr-TR/8.ogg differ
diff --git a/main/assets/locales/tr-TR/9.ogg b/main/assets/locales/tr-TR/9.ogg
new file mode 100644
index 0000000..af30078
Binary files /dev/null and b/main/assets/locales/tr-TR/9.ogg differ
diff --git a/main/assets/locales/tr-TR/activation.ogg b/main/assets/locales/tr-TR/activation.ogg
new file mode 100644
index 0000000..f8bf34e
Binary files /dev/null and b/main/assets/locales/tr-TR/activation.ogg differ
diff --git a/main/assets/locales/tr-TR/err_pin.ogg b/main/assets/locales/tr-TR/err_pin.ogg
new file mode 100644
index 0000000..6e5831e
Binary files /dev/null and b/main/assets/locales/tr-TR/err_pin.ogg differ
diff --git a/main/assets/locales/tr-TR/err_reg.ogg b/main/assets/locales/tr-TR/err_reg.ogg
new file mode 100644
index 0000000..91fc7ff
Binary files /dev/null and b/main/assets/locales/tr-TR/err_reg.ogg differ
diff --git a/main/assets/locales/tr-TR/language.json b/main/assets/locales/tr-TR/language.json
new file mode 100644
index 0000000..4d0f70f
--- /dev/null
+++ b/main/assets/locales/tr-TR/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "tr-TR"
+ },
+ "strings": {
+ "WARNING": "Uyarı",
+ "INFO": "Bilgi",
+ "ERROR": "Hata",
+ "VERSION": "Sürüm ",
+ "LOADING_PROTOCOL": "Sunucuya bağlanıyor...",
+ "INITIALIZING": "Başlatılıyor...",
+ "PIN_ERROR": "Lütfen SIM kartı takın",
+ "REG_ERROR": "Ağa erişilemiyor, veri kartı durumunu kontrol edin",
+ "DETECTING_MODULE": "Modül algılanıyor...",
+ "REGISTERING_NETWORK": "Ağ bekleniyor...",
+ "CHECKING_NEW_VERSION": "Yeni sürüm kontrol ediliyor...",
+ "CHECK_NEW_VERSION_FAILED": "Yeni sürüm kontrolü başarısız, %d saniye sonra tekrar denenecek: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Wi-Fi'ye geçiliyor...",
+ "SWITCH_TO_4G_NETWORK": "4G'ye geçiliyor...",
+ "STANDBY": "Bekleme",
+ "CONNECT_TO": "Bağlan ",
+ "CONNECTING": "Bağlanıyor...",
+ "CONNECTED_TO": "Bağlandı ",
+ "LISTENING": "Dinleniyor...",
+ "SPEAKING": "Konuşuluyor...",
+ "SERVER_NOT_FOUND": "Mevcut hizmet aranıyor",
+ "SERVER_NOT_CONNECTED": "Hizmete bağlanılamıyor, lütfen daha sonra deneyin",
+ "SERVER_TIMEOUT": "Yanıt zaman aşımı",
+ "SERVER_ERROR": "Gönderme başarısız, ağı kontrol edin",
+ "CONNECT_TO_HOTSPOT": "Telefonu hotspot'a bağlayın ",
+ "ACCESS_VIA_BROWSER": ",tarayıcı üzerinden erişin ",
+ "WIFI_CONFIG_MODE": "Ağ yapılandırma modu",
+ "ENTERING_WIFI_CONFIG_MODE": "Ağ yapılandırma moduna giriliyor...",
+ "SCANNING_WIFI": "Wi-Fi taranıyor...",
+ "NEW_VERSION": "Yeni sürüm ",
+ "OTA_UPGRADE": "OTA güncelleme",
+ "UPGRADING": "Sistem güncelleniyor...",
+ "UPGRADE_FAILED": "Güncelleme başarısız",
+ "ACTIVATION": "Cihaz aktivasyonu",
+ "BATTERY_LOW": "Pil düşük",
+ "BATTERY_CHARGING": "Şarj oluyor",
+ "BATTERY_FULL": "Pil dolu",
+ "BATTERY_NEED_CHARGE": "Pil düşük, lütfen şarj edin",
+ "VOLUME": "Ses ",
+ "MUTED": "Sessiz",
+ "MAX_VOLUME": "Maksimum ses",
+ "RTC_MODE_OFF": "AEC kapalı",
+ "RTC_MODE_ON": "AEC açık",
+ "DOWNLOAD_ASSETS_FAILED": "Varlıklar indirilemedi",
+ "LOADING_ASSETS": "Varlıklar yükleniyor...",
+ "PLEASE_WAIT": "Lütfen bekleyin...",
+ "FOUND_NEW_ASSETS": "Yeni varlıklar bulundu: %s",
+ "HELLO_MY_FRIEND": "Merhaba, arkadaşım!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/tr-TR/upgrade.ogg b/main/assets/locales/tr-TR/upgrade.ogg
new file mode 100644
index 0000000..57215bd
Binary files /dev/null and b/main/assets/locales/tr-TR/upgrade.ogg differ
diff --git a/main/assets/locales/tr-TR/welcome.ogg b/main/assets/locales/tr-TR/welcome.ogg
new file mode 100644
index 0000000..fedd21c
Binary files /dev/null and b/main/assets/locales/tr-TR/welcome.ogg differ
diff --git a/main/assets/locales/tr-TR/wificonfig.ogg b/main/assets/locales/tr-TR/wificonfig.ogg
new file mode 100644
index 0000000..c898adc
Binary files /dev/null and b/main/assets/locales/tr-TR/wificonfig.ogg differ
diff --git a/main/assets/locales/uk-UA/0.ogg b/main/assets/locales/uk-UA/0.ogg
new file mode 100644
index 0000000..f4ea62c
Binary files /dev/null and b/main/assets/locales/uk-UA/0.ogg differ
diff --git a/main/assets/locales/uk-UA/1.ogg b/main/assets/locales/uk-UA/1.ogg
new file mode 100644
index 0000000..94bfed5
Binary files /dev/null and b/main/assets/locales/uk-UA/1.ogg differ
diff --git a/main/assets/locales/uk-UA/2.ogg b/main/assets/locales/uk-UA/2.ogg
new file mode 100644
index 0000000..e6226d5
Binary files /dev/null and b/main/assets/locales/uk-UA/2.ogg differ
diff --git a/main/assets/locales/uk-UA/3.ogg b/main/assets/locales/uk-UA/3.ogg
new file mode 100644
index 0000000..50f7faa
Binary files /dev/null and b/main/assets/locales/uk-UA/3.ogg differ
diff --git a/main/assets/locales/uk-UA/4.ogg b/main/assets/locales/uk-UA/4.ogg
new file mode 100644
index 0000000..d3f9d7f
Binary files /dev/null and b/main/assets/locales/uk-UA/4.ogg differ
diff --git a/main/assets/locales/uk-UA/5.ogg b/main/assets/locales/uk-UA/5.ogg
new file mode 100644
index 0000000..712b0a0
Binary files /dev/null and b/main/assets/locales/uk-UA/5.ogg differ
diff --git a/main/assets/locales/uk-UA/6.ogg b/main/assets/locales/uk-UA/6.ogg
new file mode 100644
index 0000000..7a01d5f
Binary files /dev/null and b/main/assets/locales/uk-UA/6.ogg differ
diff --git a/main/assets/locales/uk-UA/7.ogg b/main/assets/locales/uk-UA/7.ogg
new file mode 100644
index 0000000..4f3d5b2
Binary files /dev/null and b/main/assets/locales/uk-UA/7.ogg differ
diff --git a/main/assets/locales/uk-UA/8.ogg b/main/assets/locales/uk-UA/8.ogg
new file mode 100644
index 0000000..9577bc7
Binary files /dev/null and b/main/assets/locales/uk-UA/8.ogg differ
diff --git a/main/assets/locales/uk-UA/9.ogg b/main/assets/locales/uk-UA/9.ogg
new file mode 100644
index 0000000..dbaf9d6
Binary files /dev/null and b/main/assets/locales/uk-UA/9.ogg differ
diff --git a/main/assets/locales/uk-UA/activation.ogg b/main/assets/locales/uk-UA/activation.ogg
new file mode 100644
index 0000000..446e020
Binary files /dev/null and b/main/assets/locales/uk-UA/activation.ogg differ
diff --git a/main/assets/locales/uk-UA/err_pin.ogg b/main/assets/locales/uk-UA/err_pin.ogg
new file mode 100644
index 0000000..dd7d402
Binary files /dev/null and b/main/assets/locales/uk-UA/err_pin.ogg differ
diff --git a/main/assets/locales/uk-UA/err_reg.ogg b/main/assets/locales/uk-UA/err_reg.ogg
new file mode 100644
index 0000000..ffe1a4c
Binary files /dev/null and b/main/assets/locales/uk-UA/err_reg.ogg differ
diff --git a/main/assets/locales/uk-UA/language.json b/main/assets/locales/uk-UA/language.json
new file mode 100644
index 0000000..d844a38
--- /dev/null
+++ b/main/assets/locales/uk-UA/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "uk-UA"
+ },
+ "strings": {
+ "WARNING": "Попередження",
+ "INFO": "Інформація",
+ "ERROR": "Помилка",
+ "VERSION": "Версія ",
+ "LOADING_PROTOCOL": "Підключення до сервера...",
+ "INITIALIZING": "Ініціалізація...",
+ "PIN_ERROR": "Будь ласка, вставте SIM-карту",
+ "REG_ERROR": "Неможливо отримати доступ до мережі, перевірте стан карти даних",
+ "DETECTING_MODULE": "Виявлення модуля...",
+ "REGISTERING_NETWORK": "Очікування мережі...",
+ "CHECKING_NEW_VERSION": "Перевірка нової версії...",
+ "CHECK_NEW_VERSION_FAILED": "Перевірка нової версії не вдалася, повтор через %d секунд: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Перемикання на Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Перемикання на 4G...",
+ "STANDBY": "Очікування",
+ "CONNECT_TO": "Підключитися до ",
+ "CONNECTING": "Підключення...",
+ "CONNECTED_TO": "Підключено до ",
+ "LISTENING": "Прослуховування...",
+ "SPEAKING": "Говоріння...",
+ "SERVER_NOT_FOUND": "Пошук доступного сервісу",
+ "SERVER_NOT_CONNECTED": "Неможливо підключитися до сервісу, спробуйте пізніше",
+ "SERVER_TIMEOUT": "Час очікування відповіді",
+ "SERVER_ERROR": "Помилка відправки, перевірте мережу",
+ "CONNECT_TO_HOTSPOT": "Підключіть телефон до точки доступу ",
+ "ACCESS_VIA_BROWSER": ",доступ через браузер ",
+ "WIFI_CONFIG_MODE": "Режим налаштування мережі",
+ "ENTERING_WIFI_CONFIG_MODE": "Вхід у режим налаштування мережі...",
+ "SCANNING_WIFI": "Сканування Wi-Fi...",
+ "NEW_VERSION": "Нова версія ",
+ "OTA_UPGRADE": "Оновлення OTA",
+ "UPGRADING": "Оновлення системи...",
+ "UPGRADE_FAILED": "Оновлення не вдалося",
+ "ACTIVATION": "Активація пристрою",
+ "BATTERY_LOW": "Низький заряд батареї",
+ "BATTERY_CHARGING": "Зарядка",
+ "BATTERY_FULL": "Батарея повна",
+ "BATTERY_NEED_CHARGE": "Низький заряд, будь ласка, зарядіть",
+ "VOLUME": "Гучність ",
+ "MUTED": "Звук вимкнено",
+ "MAX_VOLUME": "Максимальна гучність",
+ "RTC_MODE_OFF": "AEC вимкнено",
+ "RTC_MODE_ON": "AEC увімкнено",
+ "DOWNLOAD_ASSETS_FAILED": "Не вдалося завантажити ресурси",
+ "LOADING_ASSETS": "Завантаження ресурсів...",
+ "PLEASE_WAIT": "Будь ласка, зачекайте...",
+ "FOUND_NEW_ASSETS": "Знайдено нові ресурси: %s",
+ "HELLO_MY_FRIEND": "Привіт, мій друже!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/uk-UA/upgrade.ogg b/main/assets/locales/uk-UA/upgrade.ogg
new file mode 100644
index 0000000..5a41be8
Binary files /dev/null and b/main/assets/locales/uk-UA/upgrade.ogg differ
diff --git a/main/assets/locales/uk-UA/welcome.ogg b/main/assets/locales/uk-UA/welcome.ogg
new file mode 100644
index 0000000..e04ca00
Binary files /dev/null and b/main/assets/locales/uk-UA/welcome.ogg differ
diff --git a/main/assets/locales/uk-UA/wificonfig.ogg b/main/assets/locales/uk-UA/wificonfig.ogg
new file mode 100644
index 0000000..c1d5d90
Binary files /dev/null and b/main/assets/locales/uk-UA/wificonfig.ogg differ
diff --git a/main/assets/locales/vi-VN/0.ogg b/main/assets/locales/vi-VN/0.ogg
new file mode 100644
index 0000000..c6c0418
Binary files /dev/null and b/main/assets/locales/vi-VN/0.ogg differ
diff --git a/main/assets/locales/vi-VN/1.ogg b/main/assets/locales/vi-VN/1.ogg
new file mode 100644
index 0000000..4774b06
Binary files /dev/null and b/main/assets/locales/vi-VN/1.ogg differ
diff --git a/main/assets/locales/vi-VN/2.ogg b/main/assets/locales/vi-VN/2.ogg
new file mode 100644
index 0000000..592e9a4
Binary files /dev/null and b/main/assets/locales/vi-VN/2.ogg differ
diff --git a/main/assets/locales/vi-VN/3.ogg b/main/assets/locales/vi-VN/3.ogg
new file mode 100644
index 0000000..6825973
Binary files /dev/null and b/main/assets/locales/vi-VN/3.ogg differ
diff --git a/main/assets/locales/vi-VN/4.ogg b/main/assets/locales/vi-VN/4.ogg
new file mode 100644
index 0000000..983e48a
Binary files /dev/null and b/main/assets/locales/vi-VN/4.ogg differ
diff --git a/main/assets/locales/vi-VN/5.ogg b/main/assets/locales/vi-VN/5.ogg
new file mode 100644
index 0000000..fd950a0
Binary files /dev/null and b/main/assets/locales/vi-VN/5.ogg differ
diff --git a/main/assets/locales/vi-VN/6.ogg b/main/assets/locales/vi-VN/6.ogg
new file mode 100644
index 0000000..8158cfc
Binary files /dev/null and b/main/assets/locales/vi-VN/6.ogg differ
diff --git a/main/assets/locales/vi-VN/7.ogg b/main/assets/locales/vi-VN/7.ogg
new file mode 100644
index 0000000..ca03b3e
Binary files /dev/null and b/main/assets/locales/vi-VN/7.ogg differ
diff --git a/main/assets/locales/vi-VN/8.ogg b/main/assets/locales/vi-VN/8.ogg
new file mode 100644
index 0000000..d359674
Binary files /dev/null and b/main/assets/locales/vi-VN/8.ogg differ
diff --git a/main/assets/locales/vi-VN/9.ogg b/main/assets/locales/vi-VN/9.ogg
new file mode 100644
index 0000000..6c6dd0b
Binary files /dev/null and b/main/assets/locales/vi-VN/9.ogg differ
diff --git a/main/assets/locales/vi-VN/activation.ogg b/main/assets/locales/vi-VN/activation.ogg
new file mode 100644
index 0000000..a3871b8
Binary files /dev/null and b/main/assets/locales/vi-VN/activation.ogg differ
diff --git a/main/assets/locales/vi-VN/err_pin.ogg b/main/assets/locales/vi-VN/err_pin.ogg
new file mode 100644
index 0000000..16e42a0
Binary files /dev/null and b/main/assets/locales/vi-VN/err_pin.ogg differ
diff --git a/main/assets/locales/vi-VN/err_reg.ogg b/main/assets/locales/vi-VN/err_reg.ogg
new file mode 100644
index 0000000..00b6bab
Binary files /dev/null and b/main/assets/locales/vi-VN/err_reg.ogg differ
diff --git a/main/assets/locales/vi-VN/language.json b/main/assets/locales/vi-VN/language.json
new file mode 100644
index 0000000..e010b7b
--- /dev/null
+++ b/main/assets/locales/vi-VN/language.json
@@ -0,0 +1,56 @@
+{
+ "language": {
+ "type": "vi-VN"
+ },
+ "strings": {
+ "WARNING": "Cảnh báo",
+ "INFO": "Thông tin",
+ "ERROR": "Lỗi",
+ "VERSION": "Phiên bản ",
+ "LOADING_PROTOCOL": "Đang đăng nhập...",
+ "INITIALIZING": "Đang khởi tạo...",
+ "PIN_ERROR": "Vui lòng cắm thẻ SIM",
+ "REG_ERROR": "Không thể truy cập mạng, vui lòng kiểm tra trạng thái thẻ SIM",
+ "DETECTING_MODULE": "Đang phát hiện module...",
+ "REGISTERING_NETWORK": "Đang chờ mạng...",
+ "CHECKING_NEW_VERSION": "Đang kiểm tra phiên bản mới...",
+ "CHECK_NEW_VERSION_FAILED": "Kiểm tra phiên bản mới thất bại, sẽ thử lại sau %d giây: %s",
+ "SWITCH_TO_WIFI_NETWORK": "Đang chuyển sang Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "Đang chuyển sang 4G...",
+ "STANDBY": "Chờ",
+ "CONNECT_TO": "Kết nối đến ",
+ "CONNECTING": "Đang kết nối...",
+ "CONNECTION_SUCCESSFUL": "Kết nối thành công",
+ "CONNECTED_TO": "Đã kết nối đến ",
+ "LISTENING": "Đang lắng nghe...",
+ "SPEAKING": "Đang nói...",
+ "SERVER_NOT_FOUND": "Đang tìm dịch vụ khả dụng",
+ "SERVER_NOT_CONNECTED": "Không thể kết nối đến dịch vụ, vui lòng thử lại sau",
+ "SERVER_TIMEOUT": "Hết thời gian chờ phản hồi",
+ "SERVER_ERROR": "Gửi thất bại, vui lòng kiểm tra mạng",
+ "CONNECT_TO_HOTSPOT": "Điểm phát sóng: ",
+ "ACCESS_VIA_BROWSER": " URL cấu hình: ",
+ "WIFI_CONFIG_MODE": "Chế độ cấu hình Wi-Fi",
+ "ENTERING_WIFI_CONFIG_MODE": "Đang vào chế độ cấu hình Wi-Fi...",
+ "SCANNING_WIFI": "Đang quét Wi-Fi...",
+ "NEW_VERSION": "Phiên bản mới ",
+ "OTA_UPGRADE": "Nâng cấp OTA",
+ "UPGRADING": "Hệ thống đang nâng cấp...",
+ "UPGRADE_FAILED": "Nâng cấp thất bại",
+ "ACTIVATION": "Kích hoạt",
+ "BATTERY_LOW": "Pin yếu",
+ "BATTERY_CHARGING": "Đang sạc",
+ "BATTERY_FULL": "Pin đầy",
+ "BATTERY_NEED_CHARGE": "Pin yếu, vui lòng sạc",
+ "VOLUME": "Âm lượng ",
+ "MUTED": "Tắt tiếng",
+ "MAX_VOLUME": "Âm lượng tối đa",
+ "RTC_MODE_OFF": "Tắt AEC",
+ "RTC_MODE_ON": "Bật AEC",
+ "DOWNLOAD_ASSETS_FAILED": "Tải xuống tài nguyên thất bại",
+ "LOADING_ASSETS": "Đang tải tài nguyên...",
+ "PLEASE_WAIT": "Vui lòng đợi...",
+ "FOUND_NEW_ASSETS": "Tìm thấy tài nguyên mới: %s",
+ "HELLO_MY_FRIEND": "Xin chào, bạn của tôi!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/vi-VN/upgrade.ogg b/main/assets/locales/vi-VN/upgrade.ogg
new file mode 100644
index 0000000..1d41e98
Binary files /dev/null and b/main/assets/locales/vi-VN/upgrade.ogg differ
diff --git a/main/assets/locales/vi-VN/welcome.ogg b/main/assets/locales/vi-VN/welcome.ogg
new file mode 100644
index 0000000..ae12cc3
Binary files /dev/null and b/main/assets/locales/vi-VN/welcome.ogg differ
diff --git a/main/assets/locales/vi-VN/wificonfig.ogg b/main/assets/locales/vi-VN/wificonfig.ogg
new file mode 100644
index 0000000..08c3cb5
Binary files /dev/null and b/main/assets/locales/vi-VN/wificonfig.ogg differ
diff --git a/main/assets/locales/zh-CN/0.ogg b/main/assets/locales/zh-CN/0.ogg
new file mode 100644
index 0000000..0cb1247
Binary files /dev/null and b/main/assets/locales/zh-CN/0.ogg differ
diff --git a/main/assets/locales/zh-CN/1.ogg b/main/assets/locales/zh-CN/1.ogg
new file mode 100644
index 0000000..858af34
Binary files /dev/null and b/main/assets/locales/zh-CN/1.ogg differ
diff --git a/main/assets/locales/zh-CN/2.ogg b/main/assets/locales/zh-CN/2.ogg
new file mode 100644
index 0000000..72f53aa
Binary files /dev/null and b/main/assets/locales/zh-CN/2.ogg differ
diff --git a/main/assets/locales/zh-CN/3.ogg b/main/assets/locales/zh-CN/3.ogg
new file mode 100644
index 0000000..848af11
Binary files /dev/null and b/main/assets/locales/zh-CN/3.ogg differ
diff --git a/main/assets/locales/zh-CN/4.ogg b/main/assets/locales/zh-CN/4.ogg
new file mode 100644
index 0000000..39b3eee
Binary files /dev/null and b/main/assets/locales/zh-CN/4.ogg differ
diff --git a/main/assets/locales/zh-CN/5.ogg b/main/assets/locales/zh-CN/5.ogg
new file mode 100644
index 0000000..9230358
Binary files /dev/null and b/main/assets/locales/zh-CN/5.ogg differ
diff --git a/main/assets/locales/zh-CN/6.ogg b/main/assets/locales/zh-CN/6.ogg
new file mode 100644
index 0000000..9ecb574
Binary files /dev/null and b/main/assets/locales/zh-CN/6.ogg differ
diff --git a/main/assets/locales/zh-CN/7.ogg b/main/assets/locales/zh-CN/7.ogg
new file mode 100644
index 0000000..6348799
Binary files /dev/null and b/main/assets/locales/zh-CN/7.ogg differ
diff --git a/main/assets/locales/zh-CN/8.ogg b/main/assets/locales/zh-CN/8.ogg
new file mode 100644
index 0000000..67fc7a9
Binary files /dev/null and b/main/assets/locales/zh-CN/8.ogg differ
diff --git a/main/assets/locales/zh-CN/9.ogg b/main/assets/locales/zh-CN/9.ogg
new file mode 100644
index 0000000..a7769d9
Binary files /dev/null and b/main/assets/locales/zh-CN/9.ogg differ
diff --git a/main/assets/locales/zh-CN/activation.ogg b/main/assets/locales/zh-CN/activation.ogg
new file mode 100644
index 0000000..33291af
Binary files /dev/null and b/main/assets/locales/zh-CN/activation.ogg differ
diff --git a/main/assets/locales/zh-CN/err_pin.ogg b/main/assets/locales/zh-CN/err_pin.ogg
new file mode 100644
index 0000000..e455244
Binary files /dev/null and b/main/assets/locales/zh-CN/err_pin.ogg differ
diff --git a/main/assets/locales/zh-CN/err_reg.ogg b/main/assets/locales/zh-CN/err_reg.ogg
new file mode 100644
index 0000000..a3456f1
Binary files /dev/null and b/main/assets/locales/zh-CN/err_reg.ogg differ
diff --git a/main/assets/locales/zh-CN/language.json b/main/assets/locales/zh-CN/language.json
new file mode 100644
index 0000000..9eb619d
--- /dev/null
+++ b/main/assets/locales/zh-CN/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "zh-CN"
+ },
+ "strings": {
+ "WARNING": "警告",
+ "INFO": "信息",
+ "ERROR": "错误",
+ "VERSION": "版本 ",
+ "LOADING_PROTOCOL": "登录服务器...",
+ "INITIALIZING": "正在初始化...",
+ "PIN_ERROR": "请插入 SIM 卡",
+ "REG_ERROR": "无法接入网络,请检查流量卡状态",
+ "DETECTING_MODULE": "检测模组...",
+ "REGISTERING_NETWORK": "等待网络...",
+ "CHECKING_NEW_VERSION": "检查新版本...",
+ "CHECK_NEW_VERSION_FAILED": "检查新版本失败,将在 %d 秒后重试:%s",
+ "SWITCH_TO_WIFI_NETWORK": "切换到 Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "切换到 4G...",
+ "STANDBY": "待命",
+ "CONNECT_TO": "连接 ",
+ "CONNECTING": "连接中...",
+ "CONNECTED_TO": "已连接 ",
+ "LISTENING": "聆听中...",
+ "SPEAKING": "说话中...",
+ "SERVER_NOT_FOUND": "正在寻找可用服务",
+ "SERVER_NOT_CONNECTED": "无法连接服务,请稍后再试",
+ "SERVER_TIMEOUT": "等待响应超时",
+ "SERVER_ERROR": "发送失败,请检查网络",
+ "CONNECT_TO_HOTSPOT": "手机连接热点 ",
+ "ACCESS_VIA_BROWSER": ",浏览器访问 ",
+ "WIFI_CONFIG_MODE": "配网模式",
+ "ENTERING_WIFI_CONFIG_MODE": "进入配网模式...",
+ "SCANNING_WIFI": "扫描 Wi-Fi...",
+ "NEW_VERSION": "新版本 ",
+ "OTA_UPGRADE": "OTA 升级",
+ "UPGRADING": "正在升级系统...",
+ "UPGRADE_FAILED": "升级失败",
+ "ACTIVATION": "激活设备",
+ "BATTERY_LOW": "电量不足",
+ "BATTERY_CHARGING": "正在充电",
+ "BATTERY_FULL": "电量已满",
+ "BATTERY_NEED_CHARGE": "电量低,请充电",
+ "VOLUME": "音量 ",
+ "MUTED": "已静音",
+ "MAX_VOLUME": "最大音量",
+ "RTC_MODE_OFF": "AEC 关闭",
+ "RTC_MODE_ON": "AEC 开启",
+ "DOWNLOAD_ASSETS_FAILED": "下载资源失败",
+ "LOADING_ASSETS": "加载资源...",
+ "PLEASE_WAIT": "请稍候...",
+ "FOUND_NEW_ASSETS": "发现新资源: %s",
+ "HELLO_MY_FRIEND": "你好,我的朋友!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/zh-CN/upgrade.ogg b/main/assets/locales/zh-CN/upgrade.ogg
new file mode 100644
index 0000000..1feb87b
Binary files /dev/null and b/main/assets/locales/zh-CN/upgrade.ogg differ
diff --git a/main/assets/locales/zh-CN/welcome.ogg b/main/assets/locales/zh-CN/welcome.ogg
new file mode 100644
index 0000000..b2eeb4f
Binary files /dev/null and b/main/assets/locales/zh-CN/welcome.ogg differ
diff --git a/main/assets/locales/zh-CN/wificonfig.ogg b/main/assets/locales/zh-CN/wificonfig.ogg
new file mode 100644
index 0000000..7ef88e2
Binary files /dev/null and b/main/assets/locales/zh-CN/wificonfig.ogg differ
diff --git a/main/assets/locales/zh-TW/0.ogg b/main/assets/locales/zh-TW/0.ogg
new file mode 100644
index 0000000..0cb1247
Binary files /dev/null and b/main/assets/locales/zh-TW/0.ogg differ
diff --git a/main/assets/locales/zh-TW/1.ogg b/main/assets/locales/zh-TW/1.ogg
new file mode 100644
index 0000000..858af34
Binary files /dev/null and b/main/assets/locales/zh-TW/1.ogg differ
diff --git a/main/assets/locales/zh-TW/2.ogg b/main/assets/locales/zh-TW/2.ogg
new file mode 100644
index 0000000..72f53aa
Binary files /dev/null and b/main/assets/locales/zh-TW/2.ogg differ
diff --git a/main/assets/locales/zh-TW/3.ogg b/main/assets/locales/zh-TW/3.ogg
new file mode 100644
index 0000000..848af11
Binary files /dev/null and b/main/assets/locales/zh-TW/3.ogg differ
diff --git a/main/assets/locales/zh-TW/4.ogg b/main/assets/locales/zh-TW/4.ogg
new file mode 100644
index 0000000..39b3eee
Binary files /dev/null and b/main/assets/locales/zh-TW/4.ogg differ
diff --git a/main/assets/locales/zh-TW/5.ogg b/main/assets/locales/zh-TW/5.ogg
new file mode 100644
index 0000000..9230358
Binary files /dev/null and b/main/assets/locales/zh-TW/5.ogg differ
diff --git a/main/assets/locales/zh-TW/6.ogg b/main/assets/locales/zh-TW/6.ogg
new file mode 100644
index 0000000..9ecb574
Binary files /dev/null and b/main/assets/locales/zh-TW/6.ogg differ
diff --git a/main/assets/locales/zh-TW/7.ogg b/main/assets/locales/zh-TW/7.ogg
new file mode 100644
index 0000000..6348799
Binary files /dev/null and b/main/assets/locales/zh-TW/7.ogg differ
diff --git a/main/assets/locales/zh-TW/8.ogg b/main/assets/locales/zh-TW/8.ogg
new file mode 100644
index 0000000..67fc7a9
Binary files /dev/null and b/main/assets/locales/zh-TW/8.ogg differ
diff --git a/main/assets/locales/zh-TW/9.ogg b/main/assets/locales/zh-TW/9.ogg
new file mode 100644
index 0000000..a7769d9
Binary files /dev/null and b/main/assets/locales/zh-TW/9.ogg differ
diff --git a/main/assets/locales/zh-TW/activation.ogg b/main/assets/locales/zh-TW/activation.ogg
new file mode 100644
index 0000000..33291af
Binary files /dev/null and b/main/assets/locales/zh-TW/activation.ogg differ
diff --git a/main/assets/locales/zh-TW/err_pin.ogg b/main/assets/locales/zh-TW/err_pin.ogg
new file mode 100644
index 0000000..e455244
Binary files /dev/null and b/main/assets/locales/zh-TW/err_pin.ogg differ
diff --git a/main/assets/locales/zh-TW/err_reg.ogg b/main/assets/locales/zh-TW/err_reg.ogg
new file mode 100644
index 0000000..a3456f1
Binary files /dev/null and b/main/assets/locales/zh-TW/err_reg.ogg differ
diff --git a/main/assets/locales/zh-TW/language.json b/main/assets/locales/zh-TW/language.json
new file mode 100644
index 0000000..f035b36
--- /dev/null
+++ b/main/assets/locales/zh-TW/language.json
@@ -0,0 +1,55 @@
+{
+ "language": {
+ "type": "zh-TW"
+ },
+ "strings": {
+ "WARNING": "警告",
+ "INFO": "資訊",
+ "ERROR": "錯誤",
+ "VERSION": "版本 ",
+ "LOADING_PROTOCOL": "登入伺服器...",
+ "INITIALIZING": "正在初始化...",
+ "PIN_ERROR": "請插入 SIM 卡",
+ "REG_ERROR": "無法接入網絡,請檢查網路狀態",
+ "DETECTING_MODULE": "檢測模組...",
+ "REGISTERING_NETWORK": "等待網絡...",
+ "CHECKING_NEW_VERSION": "檢查新版本...",
+ "CHECK_NEW_VERSION_FAILED": "檢查新版本失敗,將在 %d 秒後重試:%s",
+ "SWITCH_TO_WIFI_NETWORK": "切換到 Wi-Fi...",
+ "SWITCH_TO_4G_NETWORK": "切換到 4G...",
+ "STANDBY": "待命",
+ "CONNECT_TO": "連接 ",
+ "CONNECTING": "連接中...",
+ "CONNECTED_TO": "已連接 ",
+ "LISTENING": "聆聽中...",
+ "SPEAKING": "說話中...",
+ "SERVER_NOT_FOUND": "正在尋找可用服務",
+ "SERVER_NOT_CONNECTED": "無法連接服務,請稍後再試",
+ "SERVER_TIMEOUT": "等待響應超時",
+ "SERVER_ERROR": "發送失敗,請檢查網絡",
+ "CONNECT_TO_HOTSPOT": "手機連接WiFi ",
+ "ACCESS_VIA_BROWSER": ",瀏覽器訪問 ",
+ "WIFI_CONFIG_MODE": "網路設定模式",
+ "ENTERING_WIFI_CONFIG_MODE": "正在設定網路...",
+ "SCANNING_WIFI": "掃描 Wi-Fi...",
+ "NEW_VERSION": "新版本 ",
+ "OTA_UPGRADE": "OTA 升級",
+ "UPGRADING": "正在升級系統...",
+ "UPGRADE_FAILED": "升級失敗",
+ "ACTIVATION": "啟用設備",
+ "BATTERY_LOW": "電量不足",
+ "BATTERY_CHARGING": "正在充電",
+ "BATTERY_FULL": "電量已滿",
+ "BATTERY_NEED_CHARGE": "電量低,請充電",
+ "VOLUME": "音量 ",
+ "MUTED": "已靜音",
+ "MAX_VOLUME": "最大音量",
+ "RTC_MODE_OFF": "AEC 關閉",
+ "RTC_MODE_ON": "AEC 開啟",
+ "DOWNLOAD_ASSETS_FAILED": "下載資源失敗",
+ "LOADING_ASSETS": "載入資源...",
+ "PLEASE_WAIT": "請稍候...",
+ "FOUND_NEW_ASSETS": "發現新資源: %s",
+ "HELLO_MY_FRIEND": "你好,我的朋友!"
+ }
+}
\ No newline at end of file
diff --git a/main/assets/locales/zh-TW/upgrade.ogg b/main/assets/locales/zh-TW/upgrade.ogg
new file mode 100644
index 0000000..1feb87b
Binary files /dev/null and b/main/assets/locales/zh-TW/upgrade.ogg differ
diff --git a/main/assets/locales/zh-TW/welcome.ogg b/main/assets/locales/zh-TW/welcome.ogg
new file mode 100644
index 0000000..b2eeb4f
Binary files /dev/null and b/main/assets/locales/zh-TW/welcome.ogg differ
diff --git a/main/assets/locales/zh-TW/wificonfig.ogg b/main/assets/locales/zh-TW/wificonfig.ogg
new file mode 100644
index 0000000..7ef88e2
Binary files /dev/null and b/main/assets/locales/zh-TW/wificonfig.ogg differ
diff --git a/main/audio/README.md b/main/audio/README.md
new file mode 100644
index 0000000..6159f9a
--- /dev/null
+++ b/main/audio/README.md
@@ -0,0 +1,88 @@
+# Audio Service Architecture
+
+The audio service is a core component responsible for managing all audio-related functionalities, including capturing audio from the microphone, processing it, encoding/decoding, and playing back audio through the speaker. It is designed to be modular and efficient, running its main operations in dedicated FreeRTOS tasks to ensure real-time performance.
+
+## Key Components
+
+- **`AudioService`**: The central orchestrator. It initializes and manages all other audio components, tasks, and data queues.
+- **`AudioCodec`**: A hardware abstraction layer (HAL) for the physical audio codec chip. It handles the raw I2S communication for audio input and output.
+- **`AudioProcessor`**: Performs real-time audio processing on the microphone input stream. This typically includes Acoustic Echo Cancellation (AEC), noise suppression, and Voice Activity Detection (VAD). `AfeAudioProcessor` is the default implementation, utilizing the ESP-ADF Audio Front-End.
+- **`WakeWord`**: Detects keywords (e.g., "你好,小智", "Hi, ESP") from the audio stream. It runs independently from the main audio processor until a wake word is detected.
+- **`OpusEncoderWrapper` / `OpusDecoderWrapper`**: Manages the encoding of PCM audio to the Opus format and decoding Opus packets back to PCM. Opus is used for its high compression and low latency, making it ideal for voice streaming.
+- **`OpusResampler`**: A utility to convert audio streams between different sample rates (e.g., resampling from the codec's native sample rate to the required 16kHz for processing).
+
+## Threading Model
+
+The service operates on three primary tasks to handle the different stages of the audio pipeline concurrently:
+
+1. **`AudioInputTask`**: Solely responsible for reading raw PCM data from the `AudioCodec`. It then feeds this data to either the `WakeWord` engine or the `AudioProcessor` based on the current state.
+2. **`AudioOutputTask`**: Responsible for playing audio. It retrieves decoded PCM data from the `audio_playback_queue_` and sends it to the `AudioCodec` to be played on the speaker.
+3. **`OpusCodecTask`**: A worker task that handles both encoding and decoding. It fetches raw audio from `audio_encode_queue_`, encodes it into Opus packets, and places them in the `audio_send_queue_`. Concurrently, it fetches Opus packets from `audio_decode_queue_`, decodes them into PCM, and places the result in the `audio_playback_queue_`.
+
+## Data Flow
+
+There are two primary data flows: audio input (uplink) and audio output (downlink).
+
+### 1. Audio Input (Uplink) Flow
+
+This flow captures audio from the microphone, processes it, encodes it, and prepares it for sending to a server.
+
+```mermaid
+graph TD
+ subgraph Device
+ Mic[("Microphone")] -->|I2S| Codec(AudioCodec)
+
+ subgraph AudioInputTask
+ Codec -->|Raw PCM| Read(ReadAudioData)
+ Read -->|16kHz PCM| Processor(AudioProcessor)
+ end
+
+ subgraph OpusCodecTask
+ Processor -->|Clean PCM| EncodeQueue(audio_encode_queue_)
+ EncodeQueue --> Encoder(OpusEncoder)
+ Encoder -->|Opus Packet| SendQueue(audio_send_queue_)
+ end
+
+ SendQueue --> |"PopPacketFromSendQueue()"| App(Application Layer)
+ end
+
+ App -->|Network| Server((Cloud Server))
+```
+
+- The `AudioInputTask` continuously reads raw PCM data from the `AudioCodec`.
+- This data is fed into an `AudioProcessor` for cleaning (AEC, VAD).
+- The processed PCM data is pushed into the `audio_encode_queue_`.
+- The `OpusCodecTask` picks up the PCM data, encodes it into Opus format, and pushes the resulting packet to the `audio_send_queue_`.
+- The application can then retrieve these Opus packets and send them over the network.
+
+### 2. Audio Output (Downlink) Flow
+
+This flow receives encoded audio data, decodes it, and plays it on the speaker.
+
+```mermaid
+graph TD
+ Server((Cloud Server)) -->|Network| App(Application Layer)
+
+ subgraph Device
+ App -->|"PushPacketToDecodeQueue()"| DecodeQueue(audio_decode_queue_)
+
+ subgraph OpusCodecTask
+ DecodeQueue -->|Opus Packet| Decoder(OpusDecoder)
+ Decoder -->|PCM| PlaybackQueue(audio_playback_queue_)
+ end
+
+ subgraph AudioOutputTask
+ PlaybackQueue -->|PCM| Codec(AudioCodec)
+ end
+
+ Codec -->|I2S| Speaker[("Speaker")]
+ end
+```
+
+- The application receives Opus packets from the network and pushes them into the `audio_decode_queue_`.
+- The `OpusCodecTask` retrieves these packets, decodes them back into PCM data, and pushes the data to the `audio_playback_queue_`.
+- The `AudioOutputTask` takes the PCM data from the queue and sends it to the `AudioCodec` for playback.
+
+## Power Management
+
+To conserve energy, the audio codec's input (ADC) and output (DAC) channels are automatically disabled after a period of inactivity (`AUDIO_POWER_TIMEOUT_MS`). A timer (`audio_power_timer_`) periodically checks for activity and manages the power state. The channels are automatically re-enabled when new audio needs to be captured or played.
\ No newline at end of file
diff --git a/main/audio/audio_codec.cc b/main/audio/audio_codec.cc
new file mode 100644
index 0000000..cef6180
--- /dev/null
+++ b/main/audio/audio_codec.cc
@@ -0,0 +1,72 @@
+#include "audio_codec.h"
+#include "board.h"
+#include "settings.h"
+
+#include
+#include
+#include
+
+#define TAG "AudioCodec"
+
+AudioCodec::AudioCodec() {
+}
+
+AudioCodec::~AudioCodec() {
+}
+
+void AudioCodec::OutputData(std::vector& data) {
+ Write(data.data(), data.size());
+}
+
+bool AudioCodec::InputData(std::vector& data) {
+ int samples = Read(data.data(), data.size());
+ if (samples > 0) {
+ return true;
+ }
+ return false;
+}
+
+void AudioCodec::Start() {
+ Settings settings("audio", false);
+ output_volume_ = settings.GetInt("output_volume", output_volume_);
+ if (output_volume_ <= 0) {
+ ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_);
+ output_volume_ = 10;
+ }
+
+ if (tx_handle_ != nullptr) {
+ ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
+ }
+
+ if (rx_handle_ != nullptr) {
+ ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));
+ }
+
+ EnableInput(true);
+ EnableOutput(true);
+ ESP_LOGI(TAG, "Audio codec started");
+}
+
+void AudioCodec::SetOutputVolume(int volume) {
+ output_volume_ = volume;
+ ESP_LOGI(TAG, "Set output volume to %d", output_volume_);
+
+ Settings settings("audio", true);
+ settings.SetInt("output_volume", output_volume_);
+}
+
+void AudioCodec::EnableInput(bool enable) {
+ if (enable == input_enabled_) {
+ return;
+ }
+ input_enabled_ = enable;
+ ESP_LOGI(TAG, "Set input enable to %s", enable ? "true" : "false");
+}
+
+void AudioCodec::EnableOutput(bool enable) {
+ if (enable == output_enabled_) {
+ return;
+ }
+ output_enabled_ = enable;
+ ESP_LOGI(TAG, "Set output enable to %s", enable ? "true" : "false");
+}
diff --git a/main/audio/audio_codec.h b/main/audio/audio_codec.h
new file mode 100644
index 0000000..fb56106
--- /dev/null
+++ b/main/audio/audio_codec.h
@@ -0,0 +1,59 @@
+#ifndef _AUDIO_CODEC_H
+#define _AUDIO_CODEC_H
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "board.h"
+
+#define AUDIO_CODEC_DMA_DESC_NUM 6
+#define AUDIO_CODEC_DMA_FRAME_NUM 240
+#define AUDIO_CODEC_DEFAULT_MIC_GAIN 30.0
+
+class AudioCodec {
+public:
+ AudioCodec();
+ virtual ~AudioCodec();
+
+ virtual void SetOutputVolume(int volume);
+ virtual void EnableInput(bool enable);
+ virtual void EnableOutput(bool enable);
+
+ virtual void OutputData(std::vector& data);
+ virtual bool InputData(std::vector& data);
+ virtual void Start();
+
+ inline bool duplex() const { return duplex_; }
+ inline bool input_reference() const { return input_reference_; }
+ inline int input_sample_rate() const { return input_sample_rate_; }
+ inline int output_sample_rate() const { return output_sample_rate_; }
+ inline int input_channels() const { return input_channels_; }
+ inline int output_channels() const { return output_channels_; }
+ inline int output_volume() const { return output_volume_; }
+ inline bool input_enabled() const { return input_enabled_; }
+ inline bool output_enabled() const { return output_enabled_; }
+
+protected:
+ i2s_chan_handle_t tx_handle_ = nullptr;
+ i2s_chan_handle_t rx_handle_ = nullptr;
+
+ bool duplex_ = false;
+ bool input_reference_ = false;
+ bool input_enabled_ = false;
+ bool output_enabled_ = false;
+ int input_sample_rate_ = 0;
+ int output_sample_rate_ = 0;
+ int input_channels_ = 1;
+ int output_channels_ = 1;
+ int output_volume_ = 70;
+
+ virtual int Read(int16_t* dest, int samples) = 0;
+ virtual int Write(const int16_t* data, int samples) = 0;
+};
+
+#endif // _AUDIO_CODEC_H
diff --git a/main/audio/audio_processor.h b/main/audio/audio_processor.h
new file mode 100644
index 0000000..543c7ae
--- /dev/null
+++ b/main/audio/audio_processor.h
@@ -0,0 +1,26 @@
+#ifndef AUDIO_PROCESSOR_H
+#define AUDIO_PROCESSOR_H
+
+#include
+#include
+#include
+
+#include
+#include "audio_codec.h"
+
+class AudioProcessor {
+public:
+ virtual ~AudioProcessor() = default;
+
+ virtual void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) = 0;
+ virtual void Feed(std::vector&& data) = 0;
+ virtual void Start() = 0;
+ virtual void Stop() = 0;
+ virtual bool IsRunning() = 0;
+ virtual void OnOutput(std::function&& data)> callback) = 0;
+ virtual void OnVadStateChange(std::function callback) = 0;
+ virtual size_t GetFeedSize() = 0;
+ virtual void EnableDeviceAec(bool enable) = 0;
+};
+
+#endif
diff --git a/main/audio/audio_service.cc b/main/audio/audio_service.cc
new file mode 100644
index 0000000..7cb8dcb
--- /dev/null
+++ b/main/audio/audio_service.cc
@@ -0,0 +1,673 @@
+#include "audio_service.h"
+#include
+#include
+
+#if CONFIG_USE_AUDIO_PROCESSOR
+#include "processors/afe_audio_processor.h"
+#else
+#include "processors/no_audio_processor.h"
+#endif
+
+#if CONFIG_USE_AFE_WAKE_WORD
+#include "wake_words/afe_wake_word.h"
+#elif CONFIG_USE_ESP_WAKE_WORD
+#include "wake_words/esp_wake_word.h"
+#elif CONFIG_USE_CUSTOM_WAKE_WORD
+#include "wake_words/custom_wake_word.h"
+#endif
+
+#define TAG "AudioService"
+
+
+AudioService::AudioService() {
+ event_group_ = xEventGroupCreate();
+}
+
+AudioService::~AudioService() {
+ if (event_group_ != nullptr) {
+ vEventGroupDelete(event_group_);
+ }
+}
+
+
+void AudioService::Initialize(AudioCodec* codec) {
+ codec_ = codec;
+ codec_->Start();
+
+ /* Setup the audio codec */
+ opus_decoder_ = std::make_unique(codec->output_sample_rate(), 1, OPUS_FRAME_DURATION_MS);
+ opus_encoder_ = std::make_unique(16000, 1, OPUS_FRAME_DURATION_MS);
+ opus_encoder_->SetComplexity(0);
+
+ if (codec->input_sample_rate() != 16000) {
+ input_resampler_.Configure(codec->input_sample_rate(), 16000);
+ reference_resampler_.Configure(codec->input_sample_rate(), 16000);
+ }
+
+#if CONFIG_USE_AUDIO_PROCESSOR
+ audio_processor_ = std::make_unique();
+#else
+ audio_processor_ = std::make_unique();
+#endif
+
+#if CONFIG_USE_AFE_WAKE_WORD
+ wake_word_ = std::make_unique();
+#elif CONFIG_USE_ESP_WAKE_WORD
+ wake_word_ = std::make_unique();
+#elif CONFIG_USE_CUSTOM_WAKE_WORD
+ wake_word_ = std::make_unique();
+#else
+ wake_word_ = nullptr;
+#endif
+
+ audio_processor_->OnOutput([this](std::vector&& data) {
+ PushTaskToEncodeQueue(kAudioTaskTypeEncodeToSendQueue, std::move(data));
+ });
+
+ audio_processor_->OnVadStateChange([this](bool speaking) {
+ voice_detected_ = speaking;
+ if (callbacks_.on_vad_change) {
+ callbacks_.on_vad_change(speaking);
+ }
+ });
+
+ if (wake_word_) {
+ wake_word_->OnWakeWordDetected([this](const std::string& wake_word) {
+ if (callbacks_.on_wake_word_detected) {
+ callbacks_.on_wake_word_detected(wake_word);
+ }
+ });
+ }
+
+ esp_timer_create_args_t audio_power_timer_args = {
+ .callback = [](void* arg) {
+ AudioService* audio_service = (AudioService*)arg;
+ audio_service->CheckAndUpdateAudioPowerState();
+ },
+ .arg = this,
+ .dispatch_method = ESP_TIMER_TASK,
+ .name = "audio_power_timer",
+ .skip_unhandled_events = true,
+ };
+ esp_timer_create(&audio_power_timer_args, &audio_power_timer_);
+}
+
+void AudioService::Start() {
+ service_stopped_ = false;
+ xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING | AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING);
+
+ esp_timer_start_periodic(audio_power_timer_, 1000000);
+
+#if CONFIG_USE_AUDIO_PROCESSOR
+ /* Start the audio input task */
+ xTaskCreate([](void* arg) {
+ AudioService* audio_service = (AudioService*)arg;
+ audio_service->AudioInputTask();
+ vTaskDelete(NULL);
+ }, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_);
+
+ /* Start the audio output task */
+ xTaskCreate([](void* arg) {
+ AudioService* audio_service = (AudioService*)arg;
+ audio_service->AudioOutputTask();
+ vTaskDelete(NULL);
+ }, "audio_output", 2048 * 2, this, 4, &audio_output_task_handle_);
+#else
+ /* Start the audio input task */
+ xTaskCreate([](void* arg) {
+ AudioService* audio_service = (AudioService*)arg;
+ audio_service->AudioInputTask();
+ vTaskDelete(NULL);
+ }, "audio_input", 2048 * 2, this, 8, &audio_input_task_handle_);
+
+ /* Start the audio output task */
+ xTaskCreate([](void* arg) {
+ AudioService* audio_service = (AudioService*)arg;
+ audio_service->AudioOutputTask();
+ vTaskDelete(NULL);
+ }, "audio_output", 2048, this, 4, &audio_output_task_handle_);
+#endif
+
+ /* Start the opus codec task */
+ xTaskCreate([](void* arg) {
+ AudioService* audio_service = (AudioService*)arg;
+ audio_service->OpusCodecTask();
+ vTaskDelete(NULL);
+ }, "opus_codec", 2048 * 13, this, 2, &opus_codec_task_handle_);
+}
+
+void AudioService::Stop() {
+ esp_timer_stop(audio_power_timer_);
+ service_stopped_ = true;
+ xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING |
+ AS_EVENT_WAKE_WORD_RUNNING |
+ AS_EVENT_AUDIO_PROCESSOR_RUNNING);
+
+ std::lock_guard lock(audio_queue_mutex_);
+ audio_encode_queue_.clear();
+ audio_decode_queue_.clear();
+ audio_playback_queue_.clear();
+ audio_testing_queue_.clear();
+ audio_queue_cv_.notify_all();
+}
+
+bool AudioService::ReadAudioData(std::vector& data, int sample_rate, int samples) {
+ if (!codec_->input_enabled()) {
+ esp_timer_stop(audio_power_timer_);
+ esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000);
+ codec_->EnableInput(true);
+ }
+
+ if (codec_->input_sample_rate() != sample_rate) {
+ data.resize(samples * codec_->input_sample_rate() / sample_rate * codec_->input_channels());
+ if (!codec_->InputData(data)) {
+ return false;
+ }
+ if (codec_->input_channels() == 2) {
+ auto mic_channel = std::vector(data.size() / 2);
+ auto reference_channel = std::vector(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(input_resampler_.GetOutputSamples(mic_channel.size()));
+ auto resampled_reference = std::vector(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(input_resampler_.GetOutputSamples(data.size()));
+ input_resampler_.Process(data.data(), data.size(), resampled.data());
+ data = std::move(resampled);
+ }
+ } else {
+ data.resize(samples * codec_->input_channels());
+ if (!codec_->InputData(data)) {
+ return false;
+ }
+ }
+
+ /* Update the last input time */
+ last_input_time_ = std::chrono::steady_clock::now();
+ debug_statistics_.input_count++;
+
+#if CONFIG_USE_AUDIO_DEBUGGER
+ // 音频调试:发送原始音频数据
+ if (audio_debugger_ == nullptr) {
+ audio_debugger_ = std::make_unique();
+ }
+ audio_debugger_->Feed(data);
+#endif
+
+ return true;
+}
+
+void AudioService::AudioInputTask() {
+ while (true) {
+ EventBits_t bits = xEventGroupWaitBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING |
+ AS_EVENT_WAKE_WORD_RUNNING | AS_EVENT_AUDIO_PROCESSOR_RUNNING,
+ pdFALSE, pdFALSE, portMAX_DELAY);
+
+ if (service_stopped_) {
+ break;
+ }
+ if (audio_input_need_warmup_) {
+ audio_input_need_warmup_ = false;
+ vTaskDelay(pdMS_TO_TICKS(120));
+ continue;
+ }
+
+ /* Used for audio testing in NetworkConfiguring mode by clicking the BOOT button */
+ if (bits & AS_EVENT_AUDIO_TESTING_RUNNING) {
+ if (audio_testing_queue_.size() >= AUDIO_TESTING_MAX_DURATION_MS / OPUS_FRAME_DURATION_MS) {
+ ESP_LOGW(TAG, "Audio testing queue is full, stopping audio testing");
+ EnableAudioTesting(false);
+ continue;
+ }
+ std::vector data;
+ int samples = OPUS_FRAME_DURATION_MS * 16000 / 1000;
+ if (ReadAudioData(data, 16000, samples)) {
+ // If input channels is 2, we need to fetch the left channel data
+ if (codec_->input_channels() == 2) {
+ auto mono_data = std::vector(data.size() / 2);
+ for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
+ mono_data[i] = data[j];
+ }
+ data = std::move(mono_data);
+ }
+ PushTaskToEncodeQueue(kAudioTaskTypeEncodeToTestingQueue, std::move(data));
+ continue;
+ }
+ }
+
+ /* Feed the wake word */
+ if (bits & AS_EVENT_WAKE_WORD_RUNNING) {
+ std::vector data;
+ int samples = wake_word_->GetFeedSize();
+ if (samples > 0) {
+ if (ReadAudioData(data, 16000, samples)) {
+ wake_word_->Feed(data);
+ continue;
+ }
+ }
+ }
+
+ /* Feed the audio processor */
+ if (bits & AS_EVENT_AUDIO_PROCESSOR_RUNNING) {
+ std::vector data;
+ int samples = audio_processor_->GetFeedSize();
+ if (samples > 0) {
+ if (ReadAudioData(data, 16000, samples)) {
+ audio_processor_->Feed(std::move(data));
+ continue;
+ }
+ }
+ }
+
+ ESP_LOGE(TAG, "Should not be here, bits: %lx", bits);
+ break;
+ }
+
+ ESP_LOGW(TAG, "Audio input task stopped");
+}
+
+void AudioService::AudioOutputTask() {
+ while (true) {
+ std::unique_lock lock(audio_queue_mutex_);
+ audio_queue_cv_.wait(lock, [this]() { return !audio_playback_queue_.empty() || service_stopped_; });
+ if (service_stopped_) {
+ break;
+ }
+
+ auto task = std::move(audio_playback_queue_.front());
+ audio_playback_queue_.pop_front();
+ audio_queue_cv_.notify_all();
+ lock.unlock();
+
+ if (!codec_->output_enabled()) {
+ esp_timer_stop(audio_power_timer_);
+ esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000);
+ codec_->EnableOutput(true);
+ }
+ codec_->OutputData(task->pcm);
+
+ /* Update the last output time */
+ last_output_time_ = std::chrono::steady_clock::now();
+ debug_statistics_.playback_count++;
+
+#if CONFIG_USE_SERVER_AEC
+ /* Record the timestamp for server AEC */
+ if (task->timestamp > 0) {
+ lock.lock();
+ timestamp_queue_.push_back(task->timestamp);
+ }
+#endif
+ }
+
+ ESP_LOGW(TAG, "Audio output task stopped");
+}
+
+void AudioService::OpusCodecTask() {
+ while (true) {
+ std::unique_lock lock(audio_queue_mutex_);
+ audio_queue_cv_.wait(lock, [this]() {
+ return service_stopped_ ||
+ (!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) ||
+ (!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE);
+ });
+ if (service_stopped_) {
+ break;
+ }
+
+ /* Decode the audio from decode queue */
+ if (!audio_decode_queue_.empty() && audio_playback_queue_.size() < MAX_PLAYBACK_TASKS_IN_QUEUE) {
+ auto packet = std::move(audio_decode_queue_.front());
+ audio_decode_queue_.pop_front();
+ audio_queue_cv_.notify_all();
+ lock.unlock();
+
+ auto task = std::make_unique();
+ task->type = kAudioTaskTypeDecodeToPlaybackQueue;
+ task->timestamp = packet->timestamp;
+
+ SetDecodeSampleRate(packet->sample_rate, packet->frame_duration);
+ if (opus_decoder_->Decode(std::move(packet->payload), task->pcm)) {
+ // Resample if the sample rate is different
+ if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {
+ int target_size = output_resampler_.GetOutputSamples(task->pcm.size());
+ std::vector resampled(target_size);
+ output_resampler_.Process(task->pcm.data(), task->pcm.size(), resampled.data());
+ task->pcm = std::move(resampled);
+ }
+
+ lock.lock();
+ audio_playback_queue_.push_back(std::move(task));
+ audio_queue_cv_.notify_all();
+ } else {
+ ESP_LOGE(TAG, "Failed to decode audio");
+ lock.lock();
+ }
+ debug_statistics_.decode_count++;
+ }
+
+ /* Encode the audio to send queue */
+ if (!audio_encode_queue_.empty() && audio_send_queue_.size() < MAX_SEND_PACKETS_IN_QUEUE) {
+ auto task = std::move(audio_encode_queue_.front());
+ audio_encode_queue_.pop_front();
+ audio_queue_cv_.notify_all();
+ lock.unlock();
+
+ auto packet = std::make_unique();
+ packet->frame_duration = OPUS_FRAME_DURATION_MS;
+ packet->sample_rate = 16000;
+ packet->timestamp = task->timestamp;
+ if (!opus_encoder_->Encode(std::move(task->pcm), packet->payload)) {
+ ESP_LOGE(TAG, "Failed to encode audio");
+ continue;
+ }
+
+ if (task->type == kAudioTaskTypeEncodeToSendQueue) {
+ {
+ std::lock_guard lock(audio_queue_mutex_);
+ audio_send_queue_.push_back(std::move(packet));
+ }
+ if (callbacks_.on_send_queue_available) {
+ callbacks_.on_send_queue_available();
+ }
+ } else if (task->type == kAudioTaskTypeEncodeToTestingQueue) {
+ std::lock_guard lock(audio_queue_mutex_);
+ audio_testing_queue_.push_back(std::move(packet));
+ }
+ debug_statistics_.encode_count++;
+ lock.lock();
+ }
+ }
+
+ ESP_LOGW(TAG, "Opus codec task stopped");
+}
+
+void AudioService::SetDecodeSampleRate(int sample_rate, int frame_duration) {
+ if (opus_decoder_->sample_rate() == sample_rate && opus_decoder_->duration_ms() == frame_duration) {
+ return;
+ }
+
+ opus_decoder_.reset();
+ opus_decoder_ = std::make_unique(sample_rate, 1, frame_duration);
+
+ auto codec = Board::GetInstance().GetAudioCodec();
+ if (opus_decoder_->sample_rate() != codec->output_sample_rate()) {
+ ESP_LOGI(TAG, "Resampling audio from %d to %d", opus_decoder_->sample_rate(), codec->output_sample_rate());
+ output_resampler_.Configure(opus_decoder_->sample_rate(), codec->output_sample_rate());
+ }
+}
+
+void AudioService::PushTaskToEncodeQueue(AudioTaskType type, std::vector&& pcm) {
+ auto task = std::make_unique();
+ task->type = type;
+ task->pcm = std::move(pcm);
+
+ /* Push the task to the encode queue */
+ std::unique_lock lock(audio_queue_mutex_);
+
+ /* If the task is to send queue, we need to set the timestamp */
+ if (type == kAudioTaskTypeEncodeToSendQueue && !timestamp_queue_.empty()) {
+ if (timestamp_queue_.size() <= MAX_TIMESTAMPS_IN_QUEUE) {
+ task->timestamp = timestamp_queue_.front();
+ } else {
+ ESP_LOGW(TAG, "Timestamp queue (%u) is full, dropping timestamp", timestamp_queue_.size());
+ }
+ timestamp_queue_.pop_front();
+ }
+
+ audio_queue_cv_.wait(lock, [this]() { return audio_encode_queue_.size() < MAX_ENCODE_TASKS_IN_QUEUE; });
+ audio_encode_queue_.push_back(std::move(task));
+ audio_queue_cv_.notify_all();
+}
+
+bool AudioService::PushPacketToDecodeQueue(std::unique_ptr packet, bool wait) {
+ std::unique_lock lock(audio_queue_mutex_);
+ if (audio_decode_queue_.size() >= MAX_DECODE_PACKETS_IN_QUEUE) {
+ if (wait) {
+ audio_queue_cv_.wait(lock, [this]() { return audio_decode_queue_.size() < MAX_DECODE_PACKETS_IN_QUEUE; });
+ } else {
+ return false;
+ }
+ }
+ audio_decode_queue_.push_back(std::move(packet));
+ audio_queue_cv_.notify_all();
+ return true;
+}
+
+std::unique_ptr AudioService::PopPacketFromSendQueue() {
+ std::lock_guard lock(audio_queue_mutex_);
+ if (audio_send_queue_.empty()) {
+ return nullptr;
+ }
+ auto packet = std::move(audio_send_queue_.front());
+ audio_send_queue_.pop_front();
+ audio_queue_cv_.notify_all();
+ return packet;
+}
+
+void AudioService::EncodeWakeWord() {
+ if (wake_word_) {
+ wake_word_->EncodeWakeWordData();
+ }
+}
+
+const std::string& AudioService::GetLastWakeWord() const {
+ return wake_word_->GetLastDetectedWakeWord();
+}
+
+std::unique_ptr AudioService::PopWakeWordPacket() {
+ auto packet = std::make_unique();
+ if (wake_word_->GetWakeWordOpus(packet->payload)) {
+ return packet;
+ }
+ return nullptr;
+}
+
+void AudioService::EnableWakeWordDetection(bool enable) {
+ if (!wake_word_) {
+ return;
+ }
+
+ ESP_LOGD(TAG, "%s wake word detection", enable ? "Enabling" : "Disabling");
+ if (enable) {
+ if (!wake_word_initialized_) {
+ if (!wake_word_->Initialize(codec_, models_list_)) {
+ ESP_LOGE(TAG, "Failed to initialize wake word");
+ return;
+ }
+ wake_word_initialized_ = true;
+ }
+ wake_word_->Start();
+ xEventGroupSetBits(event_group_, AS_EVENT_WAKE_WORD_RUNNING);
+ } else {
+ wake_word_->Stop();
+ xEventGroupClearBits(event_group_, AS_EVENT_WAKE_WORD_RUNNING);
+ }
+}
+
+void AudioService::EnableVoiceProcessing(bool enable) {
+ ESP_LOGD(TAG, "%s voice processing", enable ? "Enabling" : "Disabling");
+ if (enable) {
+ if (!audio_processor_initialized_) {
+ audio_processor_->Initialize(codec_, OPUS_FRAME_DURATION_MS, models_list_);
+ audio_processor_initialized_ = true;
+ }
+
+ /* We should make sure no audio is playing */
+ ResetDecoder();
+ audio_input_need_warmup_ = true;
+ audio_processor_->Start();
+ xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_PROCESSOR_RUNNING);
+ } else {
+ audio_processor_->Stop();
+ xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_PROCESSOR_RUNNING);
+ }
+}
+
+void AudioService::EnableAudioTesting(bool enable) {
+ ESP_LOGI(TAG, "%s audio testing", enable ? "Enabling" : "Disabling");
+ if (enable) {
+ xEventGroupSetBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING);
+ } else {
+ xEventGroupClearBits(event_group_, AS_EVENT_AUDIO_TESTING_RUNNING);
+ /* Copy audio_testing_queue_ to audio_decode_queue_ */
+ std::lock_guard lock(audio_queue_mutex_);
+ audio_decode_queue_ = std::move(audio_testing_queue_);
+ audio_queue_cv_.notify_all();
+ }
+}
+
+void AudioService::EnableDeviceAec(bool enable) {
+ ESP_LOGI(TAG, "%s device AEC", enable ? "Enabling" : "Disabling");
+ if (!audio_processor_initialized_) {
+ audio_processor_->Initialize(codec_, OPUS_FRAME_DURATION_MS, models_list_);
+ audio_processor_initialized_ = true;
+ }
+
+ audio_processor_->EnableDeviceAec(enable);
+}
+
+void AudioService::SetCallbacks(AudioServiceCallbacks& callbacks) {
+ callbacks_ = callbacks;
+}
+
+void AudioService::PlaySound(const std::string_view& ogg) {
+ if (!codec_->output_enabled()) {
+ esp_timer_stop(audio_power_timer_);
+ esp_timer_start_periodic(audio_power_timer_, AUDIO_POWER_CHECK_INTERVAL_MS * 1000);
+ codec_->EnableOutput(true);
+ }
+
+ const uint8_t* buf = reinterpret_cast(ogg.data());
+ size_t size = ogg.size();
+ size_t offset = 0;
+
+ auto find_page = [&](size_t start)->size_t {
+ for (size_t i = start; i + 4 <= size; ++i) {
+ if (buf[i] == 'O' && buf[i+1] == 'g' && buf[i+2] == 'g' && buf[i+3] == 'S') return i;
+ }
+ return static_cast(-1);
+ };
+
+ bool seen_head = false;
+ bool seen_tags = false;
+ int sample_rate = 16000; // 默认值
+
+ while (true) {
+ size_t pos = find_page(offset);
+ if (pos == static_cast(-1)) break;
+ offset = pos;
+ if (offset + 27 > size) break;
+
+ const uint8_t* page = buf + offset;
+ uint8_t page_segments = page[26];
+ size_t seg_table_off = offset + 27;
+ if (seg_table_off + page_segments > size) break;
+
+ size_t body_size = 0;
+ for (size_t i = 0; i < page_segments; ++i) body_size += page[27 + i];
+
+ size_t body_off = seg_table_off + page_segments;
+ if (body_off + body_size > size) break;
+
+ // Parse packets using lacing
+ size_t cur = body_off;
+ size_t seg_idx = 0;
+ while (seg_idx < page_segments) {
+ size_t pkt_len = 0;
+ size_t pkt_start = cur;
+ bool continued = false;
+ do {
+ uint8_t l = page[27 + seg_idx++];
+ pkt_len += l;
+ cur += l;
+ continued = (l == 255);
+ } while (continued && seg_idx < page_segments);
+
+ if (pkt_len == 0) continue;
+ const uint8_t* pkt_ptr = buf + pkt_start;
+
+ if (!seen_head) {
+ // 解析OpusHead包
+ if (pkt_len >= 19 && std::memcmp(pkt_ptr, "OpusHead", 8) == 0) {
+ seen_head = true;
+
+ // OpusHead结构:[0-7] "OpusHead", [8] version, [9] channel_count, [10-11] pre_skip
+ // [12-15] input_sample_rate, [16-17] output_gain, [18] mapping_family
+ if (pkt_len >= 12) {
+ uint8_t version = pkt_ptr[8];
+ uint8_t channel_count = pkt_ptr[9];
+
+ if (pkt_len >= 16) {
+ // 读取输入采样率 (little-endian)
+ sample_rate = pkt_ptr[12] | (pkt_ptr[13] << 8) |
+ (pkt_ptr[14] << 16) | (pkt_ptr[15] << 24);
+ ESP_LOGI(TAG, "OpusHead: version=%d, channels=%d, sample_rate=%d",
+ version, channel_count, sample_rate);
+ }
+ }
+ }
+ continue;
+ }
+ if (!seen_tags) {
+ // Expect OpusTags in second packet
+ if (pkt_len >= 8 && std::memcmp(pkt_ptr, "OpusTags", 8) == 0) {
+ seen_tags = true;
+ }
+ continue;
+ }
+
+ // Audio packet (Opus)
+ auto packet = std::make_unique();
+ packet->sample_rate = sample_rate;
+ packet->frame_duration = 60;
+ packet->payload.resize(pkt_len);
+ std::memcpy(packet->payload.data(), pkt_ptr, pkt_len);
+ PushPacketToDecodeQueue(std::move(packet), true);
+ }
+
+ offset = body_off + body_size;
+ }
+}
+
+bool AudioService::IsIdle() {
+ std::lock_guard lock(audio_queue_mutex_);
+ return audio_encode_queue_.empty() && audio_decode_queue_.empty() && audio_playback_queue_.empty() && audio_testing_queue_.empty();
+}
+
+void AudioService::ResetDecoder() {
+ std::lock_guard lock(audio_queue_mutex_);
+ opus_decoder_->ResetState();
+ timestamp_queue_.clear();
+ audio_decode_queue_.clear();
+ audio_playback_queue_.clear();
+ audio_testing_queue_.clear();
+ audio_queue_cv_.notify_all();
+}
+
+void AudioService::CheckAndUpdateAudioPowerState() {
+ auto now = std::chrono::steady_clock::now();
+ auto input_elapsed = std::chrono::duration_cast(now - last_input_time_).count();
+ auto output_elapsed = std::chrono::duration_cast(now - last_output_time_).count();
+ if (input_elapsed > AUDIO_POWER_TIMEOUT_MS && codec_->input_enabled()) {
+ codec_->EnableInput(false);
+ }
+ if (output_elapsed > AUDIO_POWER_TIMEOUT_MS && codec_->output_enabled()) {
+ codec_->EnableOutput(false);
+ }
+ if (!codec_->input_enabled() && !codec_->output_enabled()) {
+ esp_timer_stop(audio_power_timer_);
+ }
+}
+
+void AudioService::SetModelsList(srmodel_list_t* models_list) {
+ models_list_ = models_list;
+}
\ No newline at end of file
diff --git a/main/audio/audio_service.h b/main/audio/audio_service.h
new file mode 100644
index 0000000..7ecf9eb
--- /dev/null
+++ b/main/audio/audio_service.h
@@ -0,0 +1,160 @@
+#ifndef AUDIO_SERVICE_H
+#define AUDIO_SERVICE_H
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "audio_codec.h"
+#include "audio_processor.h"
+#include "processors/audio_debugger.h"
+#include "wake_word.h"
+#include "protocol.h"
+
+
+/*
+ * There are two types of audio data flow:
+ * 1. (MIC) -> [Processors] -> {Encode Queue} -> [Opus Encoder] -> {Send Queue} -> (Server)
+ * 2. (Server) -> {Decode Queue} -> [Opus Decoder] -> {Playback Queue} -> (Speaker)
+ *
+ * We use one task for MIC / Speaker / Processors, and one task for Opus Encoder / Opus Decoder.
+ *
+ * Decode Queue and Send Queue are the main queues, because Opus packets are quite smaller than PCM packets.
+ *
+ */
+
+#define OPUS_FRAME_DURATION_MS 60
+#define MAX_ENCODE_TASKS_IN_QUEUE 2
+#define MAX_PLAYBACK_TASKS_IN_QUEUE 2
+#define MAX_DECODE_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS)
+#define MAX_SEND_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS)
+#define AUDIO_TESTING_MAX_DURATION_MS 10000
+#define MAX_TIMESTAMPS_IN_QUEUE 3
+
+#define AUDIO_POWER_TIMEOUT_MS 15000
+#define AUDIO_POWER_CHECK_INTERVAL_MS 1000
+
+
+#define AS_EVENT_AUDIO_TESTING_RUNNING (1 << 0)
+#define AS_EVENT_WAKE_WORD_RUNNING (1 << 1)
+#define AS_EVENT_AUDIO_PROCESSOR_RUNNING (1 << 2)
+#define AS_EVENT_PLAYBACK_NOT_EMPTY (1 << 3)
+
+struct AudioServiceCallbacks {
+ std::function on_send_queue_available;
+ std::function on_wake_word_detected;
+ std::function on_vad_change;
+ std::function on_audio_testing_queue_full;
+};
+
+
+enum AudioTaskType {
+ kAudioTaskTypeEncodeToSendQueue,
+ kAudioTaskTypeEncodeToTestingQueue,
+ kAudioTaskTypeDecodeToPlaybackQueue,
+};
+
+struct AudioTask {
+ AudioTaskType type;
+ std::vector pcm;
+ uint32_t timestamp;
+};
+
+struct DebugStatistics {
+ uint32_t input_count = 0;
+ uint32_t decode_count = 0;
+ uint32_t encode_count = 0;
+ uint32_t playback_count = 0;
+};
+
+class AudioService {
+public:
+ AudioService();
+ ~AudioService();
+
+ void Initialize(AudioCodec* codec);
+ void Start();
+ void Stop();
+ void EncodeWakeWord();
+ std::unique_ptr PopWakeWordPacket();
+ const std::string& GetLastWakeWord() const;
+ bool IsVoiceDetected() const { return voice_detected_; }
+ bool IsIdle();
+ bool IsWakeWordRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_WAKE_WORD_RUNNING; }
+ bool IsAudioProcessorRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_AUDIO_PROCESSOR_RUNNING; }
+
+ void EnableWakeWordDetection(bool enable);
+ void EnableVoiceProcessing(bool enable);
+ void EnableAudioTesting(bool enable);
+ void EnableDeviceAec(bool enable);
+
+ void SetCallbacks(AudioServiceCallbacks& callbacks);
+
+ bool PushPacketToDecodeQueue(std::unique_ptr packet, bool wait = false);
+ std::unique_ptr PopPacketFromSendQueue();
+ void PlaySound(const std::string_view& sound);
+ bool ReadAudioData(std::vector& data, int sample_rate, int samples);
+ void ResetDecoder();
+ void SetModelsList(srmodel_list_t* models_list);
+
+private:
+ AudioCodec* codec_ = nullptr;
+ AudioServiceCallbacks callbacks_;
+ std::unique_ptr audio_processor_;
+ std::unique_ptr wake_word_;
+ std::unique_ptr audio_debugger_;
+ std::unique_ptr opus_encoder_;
+ std::unique_ptr opus_decoder_;
+ OpusResampler input_resampler_;
+ OpusResampler reference_resampler_;
+ OpusResampler output_resampler_;
+ DebugStatistics debug_statistics_;
+ srmodel_list_t* models_list_ = nullptr;
+
+ EventGroupHandle_t event_group_;
+
+ // Audio encode / decode
+ TaskHandle_t audio_input_task_handle_ = nullptr;
+ TaskHandle_t audio_output_task_handle_ = nullptr;
+ TaskHandle_t opus_codec_task_handle_ = nullptr;
+ std::mutex audio_queue_mutex_;
+ std::condition_variable audio_queue_cv_;
+ std::deque> audio_decode_queue_;
+ std::deque> audio_send_queue_;
+ std::deque> audio_testing_queue_;
+ std::deque> audio_encode_queue_;
+ std::deque> audio_playback_queue_;
+ // For server AEC
+ std::deque timestamp_queue_;
+
+ bool wake_word_initialized_ = false;
+ bool audio_processor_initialized_ = false;
+ bool voice_detected_ = false;
+ bool service_stopped_ = true;
+ bool audio_input_need_warmup_ = false;
+
+ esp_timer_handle_t audio_power_timer_ = nullptr;
+ std::chrono::steady_clock::time_point last_input_time_;
+ std::chrono::steady_clock::time_point last_output_time_;
+
+ void AudioInputTask();
+ void AudioOutputTask();
+ void OpusCodecTask();
+ void PushTaskToEncodeQueue(AudioTaskType type, std::vector&& pcm);
+ void SetDecodeSampleRate(int sample_rate, int frame_duration);
+ void CheckAndUpdateAudioPowerState();
+};
+
+#endif
\ No newline at end of file
diff --git a/main/audio/codecs/box_audio_codec.cc b/main/audio/codecs/box_audio_codec.cc
new file mode 100644
index 0000000..a4a3eee
--- /dev/null
+++ b/main/audio/codecs/box_audio_codec.cc
@@ -0,0 +1,244 @@
+#include "box_audio_codec.h"
+
+#include
+#include
+#include
+
+#define TAG "BoxAudioCodec"
+
+BoxAudioCodec::BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference) {
+ duplex_ = true; // 是否双工
+ input_reference_ = input_reference; // 是否使用参考输入,实现回声消除
+ input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+
+ CreateDuplexChannels(mclk, bclk, ws, dout, din);
+
+ // Do initialize of related interface: data_if, ctrl_if and gpio_if
+ audio_codec_i2s_cfg_t i2s_cfg = {
+ .port = I2S_NUM_0,
+ .rx_handle = rx_handle_,
+ .tx_handle = tx_handle_,
+ };
+ data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
+ assert(data_if_ != NULL);
+
+ // Output
+ audio_codec_i2c_cfg_t i2c_cfg = {
+ .port = (i2c_port_t)1,
+ .addr = es8311_addr,
+ .bus_handle = i2c_master_handle,
+ };
+ out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
+ assert(out_ctrl_if_ != NULL);
+
+ gpio_if_ = audio_codec_new_gpio();
+ assert(gpio_if_ != NULL);
+
+ es8311_codec_cfg_t es8311_cfg = {};
+ es8311_cfg.ctrl_if = out_ctrl_if_;
+ es8311_cfg.gpio_if = gpio_if_;
+ es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC;
+ es8311_cfg.pa_pin = pa_pin;
+ es8311_cfg.use_mclk = true;
+ es8311_cfg.hw_gain.pa_voltage = 5.0;
+ es8311_cfg.hw_gain.codec_dac_voltage = 3.3;
+ out_codec_if_ = es8311_codec_new(&es8311_cfg);
+ assert(out_codec_if_ != NULL);
+
+ esp_codec_dev_cfg_t dev_cfg = {
+ .dev_type = ESP_CODEC_DEV_TYPE_OUT,
+ .codec_if = out_codec_if_,
+ .data_if = data_if_,
+ };
+ output_dev_ = esp_codec_dev_new(&dev_cfg);
+ assert(output_dev_ != NULL);
+
+ // Input
+ i2c_cfg.addr = es7210_addr;
+ in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
+ assert(in_ctrl_if_ != NULL);
+
+ es7210_codec_cfg_t es7210_cfg = {};
+ es7210_cfg.ctrl_if = in_ctrl_if_;
+ es7210_cfg.mic_selected = ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4;
+ in_codec_if_ = es7210_codec_new(&es7210_cfg);
+ assert(in_codec_if_ != NULL);
+
+ dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
+ dev_cfg.codec_if = in_codec_if_;
+ input_dev_ = esp_codec_dev_new(&dev_cfg);
+ assert(input_dev_ != NULL);
+
+ ESP_LOGI(TAG, "BoxAudioDevice initialized");
+}
+
+BoxAudioCodec::~BoxAudioCodec() {
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ esp_codec_dev_delete(output_dev_);
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ esp_codec_dev_delete(input_dev_);
+
+ audio_codec_delete_codec_if(in_codec_if_);
+ audio_codec_delete_ctrl_if(in_ctrl_if_);
+ audio_codec_delete_codec_if(out_codec_if_);
+ audio_codec_delete_ctrl_if(out_ctrl_if_);
+ audio_codec_delete_gpio_if(gpio_if_);
+ audio_codec_delete_data_if(data_if_);
+}
+
+void BoxAudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
+ assert(input_sample_rate_ == output_sample_rate_);
+
+ i2s_chan_config_t chan_cfg = {
+ .id = I2S_NUM_0,
+ .role = I2S_ROLE_MASTER,
+ .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
+ .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
+ .auto_clear_after_cb = true,
+ .auto_clear_before_cb = false,
+ .intr_priority = 0,
+ };
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
+
+ i2s_std_config_t std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .ext_clk_freq_hz = 0,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_STEREO,
+ .slot_mask = I2S_STD_SLOT_BOTH,
+ .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .ws_pol = false,
+ .bit_shift = true,
+ .left_align = true,
+ .big_endian = false,
+ .bit_order_lsb = false
+ },
+ .gpio_cfg = {
+ .mclk = mclk,
+ .bclk = bclk,
+ .ws = ws,
+ .dout = dout,
+ .din = I2S_GPIO_UNUSED,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+
+ i2s_tdm_config_t tdm_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)input_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .ext_clk_freq_hz = 0,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256,
+ .bclk_div = 8,
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_STEREO,
+ .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3),
+ .ws_width = I2S_TDM_AUTO_WS_WIDTH,
+ .ws_pol = false,
+ .bit_shift = true,
+ .left_align = false,
+ .big_endian = false,
+ .bit_order_lsb = false,
+ .skip_mask = false,
+ .total_slot = I2S_TDM_AUTO_SLOT_NUM
+ },
+ .gpio_cfg = {
+ .mclk = mclk,
+ .bclk = bclk,
+ .ws = ws,
+ .dout = I2S_GPIO_UNUSED,
+ .din = din,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
+ ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg));
+ ESP_LOGI(TAG, "Duplex channels created");
+}
+
+void BoxAudioCodec::SetOutputVolume(int volume) {
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
+ AudioCodec::SetOutputVolume(volume);
+}
+
+void BoxAudioCodec::EnableInput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == input_enabled_) {
+ return;
+ }
+ if (enable) {
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = 4,
+ .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
+ .sample_rate = (uint32_t)output_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ if (input_reference_) {
+ fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1);
+ }
+ ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
+ ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), AUDIO_CODEC_DEFAULT_MIC_GAIN));
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ }
+ AudioCodec::EnableInput(enable);
+}
+
+void BoxAudioCodec::EnableOutput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == output_enabled_) {
+ return;
+ }
+ if (enable) {
+ // Play 16bit 1 channel
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = 1,
+ .channel_mask = 0,
+ .sample_rate = (uint32_t)output_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ }
+ AudioCodec::EnableOutput(enable);
+}
+
+int BoxAudioCodec::Read(int16_t* dest, int samples) {
+ if (input_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
+
+int BoxAudioCodec::Write(const int16_t* data, int samples) {
+ if (output_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
\ No newline at end of file
diff --git a/main/audio/codecs/box_audio_codec.h b/main/audio/codecs/box_audio_codec.h
new file mode 100644
index 0000000..cb7d389
--- /dev/null
+++ b/main/audio/codecs/box_audio_codec.h
@@ -0,0 +1,40 @@
+#ifndef _BOX_AUDIO_CODEC_H
+#define _BOX_AUDIO_CODEC_H
+
+#include "audio_codec.h"
+
+#include
+#include
+#include
+
+
+class BoxAudioCodec : public AudioCodec {
+private:
+ const audio_codec_data_if_t* data_if_ = nullptr;
+ const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr;
+ const audio_codec_if_t* out_codec_if_ = nullptr;
+ const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr;
+ const audio_codec_if_t* in_codec_if_ = nullptr;
+ const audio_codec_gpio_if_t* gpio_if_ = nullptr;
+
+ esp_codec_dev_handle_t output_dev_ = nullptr;
+ esp_codec_dev_handle_t input_dev_ = nullptr;
+ std::mutex data_if_mutex_;
+
+ void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
+
+ virtual int Read(int16_t* dest, int samples) override;
+ virtual int Write(const int16_t* data, int samples) override;
+
+public:
+ BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference);
+ virtual ~BoxAudioCodec();
+
+ virtual void SetOutputVolume(int volume) override;
+ virtual void EnableInput(bool enable) override;
+ virtual void EnableOutput(bool enable) override;
+};
+
+#endif // _BOX_AUDIO_CODEC_H
diff --git a/main/audio/codecs/dummy_audio_codec.cc b/main/audio/codecs/dummy_audio_codec.cc
new file mode 100644
index 0000000..5f646b3
--- /dev/null
+++ b/main/audio/codecs/dummy_audio_codec.cc
@@ -0,0 +1,20 @@
+#include "dummy_audio_codec.h"
+
+DummyAudioCodec::DummyAudioCodec(int input_sample_rate, int output_sample_rate) {
+ duplex_ = true;
+ input_reference_ = false;
+ input_channels_ = 1;
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+}
+
+DummyAudioCodec::~DummyAudioCodec() {
+}
+
+int DummyAudioCodec::Read(int16_t* dest, int samples) {
+ return 0;
+}
+
+int DummyAudioCodec::Write(const int16_t* data, int samples) {
+ return 0;
+}
diff --git a/main/audio/codecs/dummy_audio_codec.h b/main/audio/codecs/dummy_audio_codec.h
new file mode 100644
index 0000000..158f140
--- /dev/null
+++ b/main/audio/codecs/dummy_audio_codec.h
@@ -0,0 +1,16 @@
+#ifndef _DUMMY_AUDIO_CODEC_H
+#define _DUMMY_AUDIO_CODEC_H
+
+#include "audio_codec.h"
+
+class DummyAudioCodec : public AudioCodec {
+private:
+ virtual int Read(int16_t* dest, int samples) override;
+ virtual int Write(const int16_t* data, int samples) override;
+
+public:
+ DummyAudioCodec(int input_sample_rate, int output_sample_rate);
+ virtual ~DummyAudioCodec();
+};
+
+#endif // _DUMMY_AUDIO_CODEC_H
\ No newline at end of file
diff --git a/main/audio/codecs/es8311_audio_codec.cc b/main/audio/codecs/es8311_audio_codec.cc
new file mode 100644
index 0000000..33c8c9d
--- /dev/null
+++ b/main/audio/codecs/es8311_audio_codec.cc
@@ -0,0 +1,187 @@
+#include "es8311_audio_codec.h"
+
+#include
+
+#define TAG "Es8311AudioCodec"
+
+Es8311AudioCodec::Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk, bool pa_inverted) {
+ duplex_ = true; // 是否双工
+ input_reference_ = false; // 是否使用参考输入,实现回声消除
+ input_channels_ = 1; // 输入通道数
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+ pa_pin_ = pa_pin;
+ pa_inverted_ = pa_inverted;
+
+ assert(input_sample_rate_ == output_sample_rate_);
+ CreateDuplexChannels(mclk, bclk, ws, dout, din);
+
+ // Do initialize of related interface: data_if, ctrl_if and gpio_if
+ audio_codec_i2s_cfg_t i2s_cfg = {
+ .port = I2S_NUM_0,
+ .rx_handle = rx_handle_,
+ .tx_handle = tx_handle_,
+ };
+ data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
+ assert(data_if_ != NULL);
+
+ // Output
+ audio_codec_i2c_cfg_t i2c_cfg = {
+ .port = i2c_port,
+ .addr = es8311_addr,
+ .bus_handle = i2c_master_handle,
+ };
+ ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
+ assert(ctrl_if_ != NULL);
+
+ gpio_if_ = audio_codec_new_gpio();
+ assert(gpio_if_ != NULL);
+
+ es8311_codec_cfg_t es8311_cfg = {};
+ es8311_cfg.ctrl_if = ctrl_if_;
+ es8311_cfg.gpio_if = gpio_if_;
+ es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
+ es8311_cfg.pa_pin = pa_pin;
+ es8311_cfg.use_mclk = use_mclk;
+ es8311_cfg.hw_gain.pa_voltage = 5.0;
+ es8311_cfg.hw_gain.codec_dac_voltage = 3.3;
+ es8311_cfg.pa_reverted = pa_inverted_;
+ codec_if_ = es8311_codec_new(&es8311_cfg);
+ assert(codec_if_ != NULL);
+
+ ESP_LOGI(TAG, "Es8311AudioCodec initialized");
+}
+
+Es8311AudioCodec::~Es8311AudioCodec() {
+ esp_codec_dev_delete(dev_);
+
+ audio_codec_delete_codec_if(codec_if_);
+ audio_codec_delete_ctrl_if(ctrl_if_);
+ audio_codec_delete_gpio_if(gpio_if_);
+ audio_codec_delete_data_if(data_if_);
+}
+
+void Es8311AudioCodec::UpdateDeviceState() {
+ if ((input_enabled_ || output_enabled_) && dev_ == nullptr) {
+ esp_codec_dev_cfg_t dev_cfg = {
+ .dev_type = ESP_CODEC_DEV_TYPE_IN_OUT,
+ .codec_if = codec_if_,
+ .data_if = data_if_,
+ };
+ dev_ = esp_codec_dev_new(&dev_cfg);
+ assert(dev_ != NULL);
+
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = 1,
+ .channel_mask = 0,
+ .sample_rate = (uint32_t)input_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ ESP_ERROR_CHECK(esp_codec_dev_open(dev_, &fs));
+ ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(dev_, AUDIO_CODEC_DEFAULT_MIC_GAIN));
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(dev_, output_volume_));
+ } else if (!input_enabled_ && !output_enabled_ && dev_ != nullptr) {
+ esp_codec_dev_close(dev_);
+ dev_ = nullptr;
+ }
+ if (pa_pin_ != GPIO_NUM_NC) {
+ int level = output_enabled_ ? 1 : 0;
+ gpio_set_level(pa_pin_, pa_inverted_ ? !level : level);
+ }
+}
+
+void Es8311AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
+ assert(input_sample_rate_ == output_sample_rate_);
+
+ i2s_chan_config_t chan_cfg = {
+ .id = I2S_NUM_0,
+ .role = I2S_ROLE_MASTER,
+ .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
+ .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
+ .auto_clear_after_cb = true,
+ .auto_clear_before_cb = false,
+ .intr_priority = 0,
+ };
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
+
+ i2s_std_config_t std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256,
+ #ifdef I2S_HW_VERSION_2
+ .ext_clk_freq_hz = 0,
+ #endif
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_STEREO,
+ .slot_mask = I2S_STD_SLOT_BOTH,
+ .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .ws_pol = false,
+ .bit_shift = true,
+ #ifdef I2S_HW_VERSION_2
+ .left_align = true,
+ .big_endian = false,
+ .bit_order_lsb = false
+ #endif
+ },
+ .gpio_cfg = {
+ .mclk = mclk,
+ .bclk = bclk,
+ .ws = ws,
+ .dout = dout,
+ .din = din,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
+ ESP_LOGI(TAG, "Duplex channels created");
+}
+
+void Es8311AudioCodec::SetOutputVolume(int volume) {
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(dev_, volume));
+ AudioCodec::SetOutputVolume(volume);
+}
+
+void Es8311AudioCodec::EnableInput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == input_enabled_) {
+ return;
+ }
+ AudioCodec::EnableInput(enable);
+ UpdateDeviceState();
+}
+
+void Es8311AudioCodec::EnableOutput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == output_enabled_) {
+ return;
+ }
+ AudioCodec::EnableOutput(enable);
+ UpdateDeviceState();
+}
+
+int Es8311AudioCodec::Read(int16_t* dest, int samples) {
+ if (input_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(dev_, (void*)dest, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
+
+int Es8311AudioCodec::Write(const int16_t* data, int samples) {
+ if (output_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(dev_, (void*)data, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
\ No newline at end of file
diff --git a/main/audio/codecs/es8311_audio_codec.h b/main/audio/codecs/es8311_audio_codec.h
new file mode 100644
index 0000000..dd0e639
--- /dev/null
+++ b/main/audio/codecs/es8311_audio_codec.h
@@ -0,0 +1,42 @@
+#ifndef _ES8311_AUDIO_CODEC_H
+#define _ES8311_AUDIO_CODEC_H
+
+#include "audio_codec.h"
+
+#include
+#include
+#include
+#include
+#include
+
+
+class Es8311AudioCodec : public AudioCodec {
+private:
+ const audio_codec_data_if_t* data_if_ = nullptr;
+ const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
+ const audio_codec_if_t* codec_if_ = nullptr;
+ const audio_codec_gpio_if_t* gpio_if_ = nullptr;
+
+ esp_codec_dev_handle_t dev_ = nullptr;
+ gpio_num_t pa_pin_ = GPIO_NUM_NC;
+ bool pa_inverted_ = false;
+ std::mutex data_if_mutex_;
+
+ void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
+ void UpdateDeviceState();
+
+ virtual int Read(int16_t* dest, int samples) override;
+ virtual int Write(const int16_t* data, int samples) override;
+
+public:
+ Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk = true, bool pa_inverted = false);
+ virtual ~Es8311AudioCodec();
+
+ virtual void SetOutputVolume(int volume) override;
+ virtual void EnableInput(bool enable) override;
+ virtual void EnableOutput(bool enable) override;
+};
+
+#endif // _ES8311_AUDIO_CODEC_H
\ No newline at end of file
diff --git a/main/audio/codecs/es8374_audio_codec.cc b/main/audio/codecs/es8374_audio_codec.cc
new file mode 100644
index 0000000..11dea58
--- /dev/null
+++ b/main/audio/codecs/es8374_audio_codec.cc
@@ -0,0 +1,196 @@
+#include "es8374_audio_codec.h"
+
+#include
+
+#define TAG "Es8374AudioCodec"
+
+Es8374AudioCodec::Es8374AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8374_addr, bool use_mclk) {
+ duplex_ = true; // 是否双工
+ input_reference_ = false; // 是否使用参考输入,实现回声消除
+ input_channels_ = 1; // 输入通道数
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+ pa_pin_ = pa_pin;
+ CreateDuplexChannels(mclk, bclk, ws, dout, din);
+
+ // Do initialize of related interface: data_if, ctrl_if and gpio_if
+ audio_codec_i2s_cfg_t i2s_cfg = {
+ .port = I2S_NUM_0,
+ .rx_handle = rx_handle_,
+ .tx_handle = tx_handle_,
+ };
+ data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
+ assert(data_if_ != NULL);
+
+ // Output
+ audio_codec_i2c_cfg_t i2c_cfg = {
+ .port = i2c_port,
+ .addr = es8374_addr,
+ .bus_handle = i2c_master_handle,
+ };
+ ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
+ assert(ctrl_if_ != NULL);
+
+ gpio_if_ = audio_codec_new_gpio();
+ assert(gpio_if_ != NULL);
+
+ es8374_codec_cfg_t es8374_cfg = {};
+ es8374_cfg.ctrl_if = ctrl_if_;
+ es8374_cfg.gpio_if = gpio_if_;
+ es8374_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
+ es8374_cfg.pa_pin = pa_pin;
+ codec_if_ = es8374_codec_new(&es8374_cfg);
+ assert(codec_if_ != NULL);
+
+ esp_codec_dev_cfg_t dev_cfg = {
+ .dev_type = ESP_CODEC_DEV_TYPE_OUT,
+ .codec_if = codec_if_,
+ .data_if = data_if_,
+ };
+ output_dev_ = esp_codec_dev_new(&dev_cfg);
+ assert(output_dev_ != NULL);
+ dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
+ input_dev_ = esp_codec_dev_new(&dev_cfg);
+ assert(input_dev_ != NULL);
+ esp_codec_set_disable_when_closed(output_dev_, false);
+ esp_codec_set_disable_when_closed(input_dev_, false);
+ ESP_LOGI(TAG, "Es8374AudioCodec initialized");
+}
+
+Es8374AudioCodec::~Es8374AudioCodec() {
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ esp_codec_dev_delete(output_dev_);
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ esp_codec_dev_delete(input_dev_);
+
+ audio_codec_delete_codec_if(codec_if_);
+ audio_codec_delete_ctrl_if(ctrl_if_);
+ audio_codec_delete_gpio_if(gpio_if_);
+ audio_codec_delete_data_if(data_if_);
+}
+
+void Es8374AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
+ assert(input_sample_rate_ == output_sample_rate_);
+
+ i2s_chan_config_t chan_cfg = {
+ .id = I2S_NUM_0,
+ .role = I2S_ROLE_MASTER,
+ .dma_desc_num = 6,
+ .dma_frame_num = 240,
+ .auto_clear_after_cb = true,
+ .auto_clear_before_cb = false,
+ .intr_priority = 0,
+ };
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
+
+ i2s_std_config_t std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256,
+ #ifdef I2S_HW_VERSION_2
+ .ext_clk_freq_hz = 0,
+ #endif
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_STEREO,
+ .slot_mask = I2S_STD_SLOT_BOTH,
+ .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .ws_pol = false,
+ .bit_shift = true,
+ #ifdef I2S_HW_VERSION_2
+ .left_align = true,
+ .big_endian = false,
+ .bit_order_lsb = false
+ #endif
+ },
+ .gpio_cfg = {
+ .mclk = mclk,
+ .bclk = bclk,
+ .ws = ws,
+ .dout = dout,
+ .din = din,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
+ ESP_LOGI(TAG, "Duplex channels created");
+}
+
+void Es8374AudioCodec::SetOutputVolume(int volume) {
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
+ AudioCodec::SetOutputVolume(volume);
+}
+
+void Es8374AudioCodec::EnableInput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == input_enabled_) {
+ return;
+ }
+ if (enable) {
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = 1,
+ .channel_mask = 0,
+ .sample_rate = (uint32_t)input_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
+ ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, AUDIO_CODEC_DEFAULT_MIC_GAIN));
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ }
+ AudioCodec::EnableInput(enable);
+}
+
+void Es8374AudioCodec::EnableOutput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == output_enabled_) {
+ return;
+ }
+ if (enable) {
+ // Play 16bit 1 channel
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = 1,
+ .channel_mask = 0,
+ .sample_rate = (uint32_t)output_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
+ if (pa_pin_ != GPIO_NUM_NC) {
+ gpio_set_level(pa_pin_, 1);
+ }
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ if (pa_pin_ != GPIO_NUM_NC) {
+ gpio_set_level(pa_pin_, 0);
+ }
+ }
+ AudioCodec::EnableOutput(enable);
+}
+
+int Es8374AudioCodec::Read(int16_t* dest, int samples) {
+ if (input_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
+
+int Es8374AudioCodec::Write(const int16_t* data, int samples) {
+ if (output_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
\ No newline at end of file
diff --git a/main/audio/codecs/es8374_audio_codec.h b/main/audio/codecs/es8374_audio_codec.h
new file mode 100644
index 0000000..7533c22
--- /dev/null
+++ b/main/audio/codecs/es8374_audio_codec.h
@@ -0,0 +1,41 @@
+#ifndef _ES8374_AUDIO_CODEC_H
+#define _ES8374_AUDIO_CODEC_H
+
+#include "audio_codec.h"
+
+#include
+#include
+#include
+#include
+#include
+
+
+class Es8374AudioCodec : public AudioCodec {
+private:
+ const audio_codec_data_if_t* data_if_ = nullptr;
+ const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
+ const audio_codec_if_t* codec_if_ = nullptr;
+ const audio_codec_gpio_if_t* gpio_if_ = nullptr;
+
+ esp_codec_dev_handle_t output_dev_ = nullptr;
+ esp_codec_dev_handle_t input_dev_ = nullptr;
+ gpio_num_t pa_pin_ = GPIO_NUM_NC;
+ std::mutex data_if_mutex_;
+
+ void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
+
+ virtual int Read(int16_t* dest, int samples) override;
+ virtual int Write(const int16_t* data, int samples) override;
+
+public:
+ Es8374AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8374_addr, bool use_mclk = true);
+ virtual ~Es8374AudioCodec();
+
+ virtual void SetOutputVolume(int volume) override;
+ virtual void EnableInput(bool enable) override;
+ virtual void EnableOutput(bool enable) override;
+};
+
+#endif // _ES8374_AUDIO_CODEC_H
\ No newline at end of file
diff --git a/main/audio/codecs/es8388_audio_codec.cc b/main/audio/codecs/es8388_audio_codec.cc
new file mode 100644
index 0000000..3fd327b
--- /dev/null
+++ b/main/audio/codecs/es8388_audio_codec.cc
@@ -0,0 +1,219 @@
+#include "es8388_audio_codec.h"
+
+#include
+
+#define TAG "Es8388AudioCodec"
+
+Es8388AudioCodec::Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference) {
+ duplex_ = true; // 是否双工
+ input_reference_ = input_reference; // 是否使用参考输入,实现回声消除
+ input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+ pa_pin_ = pa_pin;
+ CreateDuplexChannels(mclk, bclk, ws, dout, din);
+
+ // Do initialize of related interface: data_if, ctrl_if and gpio_if
+ audio_codec_i2s_cfg_t i2s_cfg = {
+ .port = I2S_NUM_0,
+ .rx_handle = rx_handle_,
+ .tx_handle = tx_handle_,
+ };
+ data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
+ assert(data_if_ != NULL);
+
+ // Output
+ audio_codec_i2c_cfg_t i2c_cfg = {
+ .port = i2c_port,
+ .addr = es8388_addr,
+ .bus_handle = i2c_master_handle,
+ };
+ ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
+ assert(ctrl_if_ != NULL);
+
+ gpio_if_ = audio_codec_new_gpio();
+ assert(gpio_if_ != NULL);
+
+ es8388_codec_cfg_t es8388_cfg = {};
+ es8388_cfg.ctrl_if = ctrl_if_;
+ es8388_cfg.gpio_if = gpio_if_;
+ es8388_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
+ es8388_cfg.master_mode = true;
+ es8388_cfg.pa_pin = pa_pin;
+ es8388_cfg.pa_reverted = false;
+ es8388_cfg.hw_gain.pa_voltage = 5.0;
+ es8388_cfg.hw_gain.codec_dac_voltage = 3.3;
+ codec_if_ = es8388_codec_new(&es8388_cfg);
+ assert(codec_if_ != NULL);
+
+ esp_codec_dev_cfg_t outdev_cfg = {
+ .dev_type = ESP_CODEC_DEV_TYPE_OUT,
+ .codec_if = codec_if_,
+ .data_if = data_if_,
+ };
+ output_dev_ = esp_codec_dev_new(&outdev_cfg);
+ assert(output_dev_ != NULL);
+
+ esp_codec_dev_cfg_t indev_cfg = {
+ .dev_type = ESP_CODEC_DEV_TYPE_IN,
+ .codec_if = codec_if_,
+ .data_if = data_if_,
+ };
+ input_dev_ = esp_codec_dev_new(&indev_cfg);
+ assert(input_dev_ != NULL);
+ esp_codec_set_disable_when_closed(output_dev_, false);
+ esp_codec_set_disable_when_closed(input_dev_, false);
+ ESP_LOGI(TAG, "Es8388AudioCodec initialized");
+}
+
+Es8388AudioCodec::~Es8388AudioCodec() {
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ esp_codec_dev_delete(output_dev_);
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ esp_codec_dev_delete(input_dev_);
+
+ audio_codec_delete_codec_if(codec_if_);
+ audio_codec_delete_ctrl_if(ctrl_if_);
+ audio_codec_delete_gpio_if(gpio_if_);
+ audio_codec_delete_data_if(data_if_);
+}
+
+void Es8388AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din){
+ assert(input_sample_rate_ == output_sample_rate_);
+
+ i2s_chan_config_t chan_cfg = {
+ .id = I2S_NUM_0,
+ .role = I2S_ROLE_MASTER,
+ .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
+ .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
+ .auto_clear_after_cb = true,
+ .auto_clear_before_cb = false,
+ .intr_priority = 0,
+ };
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
+
+ i2s_std_config_t std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .ext_clk_freq_hz = 0,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_STEREO,
+ .slot_mask = I2S_STD_SLOT_BOTH,
+ .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .ws_pol = false,
+ .bit_shift = true,
+ .left_align = true,
+ .big_endian = false,
+ .bit_order_lsb = false
+ },
+ .gpio_cfg = {
+ .mclk = mclk,
+ .bclk = bclk,
+ .ws = ws,
+ .dout = dout,
+ .din = din,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
+ ESP_LOGI(TAG, "Duplex channels created");
+}
+
+void Es8388AudioCodec::SetOutputVolume(int volume) {
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
+ AudioCodec::SetOutputVolume(volume);
+}
+
+void Es8388AudioCodec::EnableInput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == input_enabled_) {
+ return;
+ }
+ if (enable) {
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = (uint8_t) input_channels_,
+ .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
+ .sample_rate = (uint32_t)input_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ if (input_reference_) {
+ fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1);
+ }
+ ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
+ if (input_reference_) {
+ uint8_t gain = (11 << 4) + 0;
+ ctrl_if_->write_reg(ctrl_if_, 0x09, 1, &gain, 1);
+ }else{
+ ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0));
+ }
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ }
+ AudioCodec::EnableInput(enable);
+}
+
+void Es8388AudioCodec::EnableOutput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == output_enabled_) {
+ return;
+ }
+ if (enable) {
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = 1,
+ .channel_mask = 0,
+ .sample_rate = (uint32_t)output_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
+
+ // Set analog output volume to 0dB, default is -45dB
+ uint8_t reg_val = 30; // 0dB
+ if(input_reference_){
+ reg_val = 27;
+ }
+ uint8_t regs[] = { 46, 47, 48, 49 }; // HP_LVOL, HP_RVOL, SPK_LVOL, SPK_RVOL
+ for (uint8_t reg : regs) {
+ ctrl_if_->write_reg(ctrl_if_, reg, 1, ®_val, 1);
+ }
+
+ if (pa_pin_ != GPIO_NUM_NC) {
+ gpio_set_level(pa_pin_, 1);
+ }
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ if (pa_pin_ != GPIO_NUM_NC) {
+ gpio_set_level(pa_pin_, 0);
+ }
+ }
+ AudioCodec::EnableOutput(enable);
+}
+
+int Es8388AudioCodec::Read(int16_t* dest, int samples) {
+ if (input_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
+
+int Es8388AudioCodec::Write(const int16_t* data, int samples) {
+ if (output_enabled_ && output_dev_ && data != nullptr) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
diff --git a/main/audio/codecs/es8388_audio_codec.h b/main/audio/codecs/es8388_audio_codec.h
new file mode 100644
index 0000000..316dfce
--- /dev/null
+++ b/main/audio/codecs/es8388_audio_codec.h
@@ -0,0 +1,40 @@
+#ifndef _ES8388_AUDIO_CODEC_H
+#define _ES8388_AUDIO_CODEC_H
+
+#include "audio_codec.h"
+
+#include
+#include
+#include
+#include
+
+
+class Es8388AudioCodec : public AudioCodec {
+private:
+ const audio_codec_data_if_t* data_if_ = nullptr;
+ const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
+ const audio_codec_if_t* codec_if_ = nullptr;
+ const audio_codec_gpio_if_t* gpio_if_ = nullptr;
+
+ esp_codec_dev_handle_t output_dev_ = nullptr;
+ esp_codec_dev_handle_t input_dev_ = nullptr;
+ gpio_num_t pa_pin_ = GPIO_NUM_NC;
+ std::mutex data_if_mutex_;
+
+ void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
+
+ virtual int Read(int16_t* dest, int samples) override;
+ virtual int Write(const int16_t* data, int samples) override;
+
+public:
+ Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference = false);
+ virtual ~Es8388AudioCodec();
+
+ virtual void SetOutputVolume(int volume) override;
+ virtual void EnableInput(bool enable) override;
+ virtual void EnableOutput(bool enable) override;
+};
+
+#endif // _ES8388_AUDIO_CODEC_H
diff --git a/main/audio/codecs/es8389_audio_codec.cc b/main/audio/codecs/es8389_audio_codec.cc
new file mode 100644
index 0000000..ece34b9
--- /dev/null
+++ b/main/audio/codecs/es8389_audio_codec.cc
@@ -0,0 +1,203 @@
+#include "es8389_audio_codec.h"
+
+#include
+
+static const char TAG[] = "Es8389AudioCodec";
+
+Es8389AudioCodec::Es8389AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8389_addr, bool use_mclk) {
+ duplex_ = true; // 是否双工
+ input_reference_ = false; // 是否使用参考输入,实现回声消除
+ input_channels_ = 1; // 输入通道数
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+ pa_pin_ = pa_pin;
+ CreateDuplexChannels(mclk, bclk, ws, dout, din);
+
+ // Do initialize of related interface: data_if, ctrl_if and gpio_if
+ audio_codec_i2s_cfg_t i2s_cfg = {
+ .port = I2S_NUM_0,
+ .rx_handle = rx_handle_,
+ .tx_handle = tx_handle_,
+ };
+ data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
+ assert(data_if_ != NULL);
+
+ // Output
+ audio_codec_i2c_cfg_t i2c_cfg = {
+ .port = i2c_port,
+ .addr = es8389_addr,
+ .bus_handle = i2c_master_handle,
+ };
+ ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
+ assert(ctrl_if_ != NULL);
+
+ gpio_if_ = audio_codec_new_gpio();
+ assert(gpio_if_ != NULL);
+
+ es8389_codec_cfg_t es8389_cfg = {};
+ es8389_cfg.ctrl_if = ctrl_if_;
+ es8389_cfg.gpio_if = gpio_if_;
+ es8389_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
+ es8389_cfg.pa_pin = pa_pin;
+ es8389_cfg.use_mclk = use_mclk;
+ es8389_cfg.hw_gain.pa_voltage = 5.0;
+ es8389_cfg.hw_gain.codec_dac_voltage = 3.3;
+ codec_if_ = es8389_codec_new(&es8389_cfg);
+
+ assert(codec_if_ != NULL);
+
+ esp_codec_dev_cfg_t outdev_cfg = {
+ .dev_type = ESP_CODEC_DEV_TYPE_OUT,
+ .codec_if = codec_if_,
+ .data_if = data_if_,
+ };
+ output_dev_ = esp_codec_dev_new(&outdev_cfg);
+ assert(output_dev_ != NULL);
+
+ esp_codec_dev_cfg_t indev_cfg = {
+ .dev_type = ESP_CODEC_DEV_TYPE_IN,
+ .codec_if = codec_if_,
+ .data_if = data_if_,
+ };
+ input_dev_ = esp_codec_dev_new(&indev_cfg);
+ assert(input_dev_ != NULL);
+ esp_codec_set_disable_when_closed(output_dev_, false);
+ esp_codec_set_disable_when_closed(input_dev_, false);
+ ESP_LOGI(TAG, "Es8389AudioCodec initialized");
+}
+
+Es8389AudioCodec::~Es8389AudioCodec() {
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ esp_codec_dev_delete(output_dev_);
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ esp_codec_dev_delete(input_dev_);
+
+ audio_codec_delete_codec_if(codec_if_);
+ audio_codec_delete_ctrl_if(ctrl_if_);
+ audio_codec_delete_gpio_if(gpio_if_);
+ audio_codec_delete_data_if(data_if_);
+}
+
+void Es8389AudioCodec::CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
+ assert(input_sample_rate_ == output_sample_rate_);
+
+ i2s_chan_config_t chan_cfg = {
+ .id = I2S_NUM_0,
+ .role = I2S_ROLE_MASTER,
+ .dma_desc_num = 6,
+ .dma_frame_num = 240,
+ .auto_clear_after_cb = true,
+ .auto_clear_before_cb = false,
+ .intr_priority = 0,
+ };
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
+
+ i2s_std_config_t std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256,
+#ifdef I2S_HW_VERSION_2
+ .ext_clk_freq_hz = 0,
+#endif
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_STEREO,
+ .slot_mask = I2S_STD_SLOT_BOTH,
+ .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
+ .ws_pol = false,
+ .bit_shift = true,
+ .left_align = true,
+ .big_endian = false,
+ .bit_order_lsb = false
+ },
+ .gpio_cfg = {
+ .mclk = mclk,
+ .bclk = bclk,
+ .ws = ws,
+ .dout = dout,
+ .din = din,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
+ ESP_LOGI(TAG, "Duplex channels created");
+}
+
+void Es8389AudioCodec::SetOutputVolume(int volume) {
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
+ AudioCodec::SetOutputVolume(volume);
+}
+
+void Es8389AudioCodec::EnableInput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == input_enabled_) {
+ return;
+ }
+ if (enable) {
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = 1,
+ .channel_mask = 0,
+ .sample_rate = (uint32_t)input_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
+ ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 40.0));
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
+ }
+ AudioCodec::EnableInput(enable);
+}
+
+void Es8389AudioCodec::EnableOutput(bool enable) {
+ std::lock_guard lock(data_if_mutex_);
+ if (enable == output_enabled_) {
+ return;
+ }
+ if (enable) {
+ // Play 16bit 1 channel
+ esp_codec_dev_sample_info_t fs = {
+ .bits_per_sample = 16,
+ .channel = 1,
+ .channel_mask = 0,
+ .sample_rate = (uint32_t)output_sample_rate_,
+ .mclk_multiple = 0,
+ };
+ ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
+ ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
+ if (pa_pin_ != GPIO_NUM_NC) {
+ gpio_set_level(pa_pin_, 1);
+ }
+ } else {
+ ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
+ if (pa_pin_ != GPIO_NUM_NC) {
+ gpio_set_level(pa_pin_, 0);
+ }
+ }
+ AudioCodec::EnableOutput(enable);
+}
+
+int Es8389AudioCodec::Read(int16_t* dest, int samples) {
+ if (input_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
+
+int Es8389AudioCodec::Write(const int16_t* data, int samples) {
+ if (output_enabled_) {
+ ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
+ }
+ return samples;
+}
\ No newline at end of file
diff --git a/main/audio/codecs/es8389_audio_codec.h b/main/audio/codecs/es8389_audio_codec.h
new file mode 100644
index 0000000..b55b427
--- /dev/null
+++ b/main/audio/codecs/es8389_audio_codec.h
@@ -0,0 +1,40 @@
+#ifndef _ES8389_AUDIO_CODEC_H
+#define _ES8389_AUDIO_CODEC_H
+
+#include "audio_codec.h"
+
+#include
+#include
+#include
+#include
+#include
+
+class Es8389AudioCodec : public AudioCodec {
+private:
+ const audio_codec_data_if_t* data_if_ = nullptr;
+ const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
+ const audio_codec_if_t* codec_if_ = nullptr;
+ const audio_codec_gpio_if_t* gpio_if_ = nullptr;
+
+ esp_codec_dev_handle_t output_dev_ = nullptr;
+ esp_codec_dev_handle_t input_dev_ = nullptr;
+ gpio_num_t pa_pin_ = GPIO_NUM_NC;
+ std::mutex data_if_mutex_;
+
+ void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
+
+ virtual int Read(int16_t* dest, int samples) override;
+ virtual int Write(const int16_t* data, int samples) override;
+
+public:
+ Es8389AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
+ gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
+ gpio_num_t pa_pin, uint8_t es8389_addr, bool use_mclk = true);
+ virtual ~Es8389AudioCodec();
+
+ virtual void SetOutputVolume(int volume) override;
+ virtual void EnableInput(bool enable) override;
+ virtual void EnableOutput(bool enable) override;
+};
+
+#endif // _ES8389_AUDIO_CODEC_H
diff --git a/main/audio/codecs/no_audio_codec.cc b/main/audio/codecs/no_audio_codec.cc
new file mode 100644
index 0000000..92fe3be
--- /dev/null
+++ b/main/audio/codecs/no_audio_codec.cc
@@ -0,0 +1,332 @@
+#include "no_audio_codec.h"
+
+#include
+#include
+#include
+
+#define TAG "NoAudioCodec"
+
+NoAudioCodec::~NoAudioCodec() {
+ if (rx_handle_ != nullptr) {
+ ESP_ERROR_CHECK(i2s_channel_disable(rx_handle_));
+ }
+ if (tx_handle_ != nullptr) {
+ ESP_ERROR_CHECK(i2s_channel_disable(tx_handle_));
+ }
+}
+
+NoAudioCodecDuplex::NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
+ duplex_ = true;
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+
+ i2s_chan_config_t chan_cfg = {
+ .id = I2S_NUM_0,
+ .role = I2S_ROLE_MASTER,
+ .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
+ .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
+ .auto_clear_after_cb = true,
+ .auto_clear_before_cb = false,
+ .intr_priority = 0,
+ };
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
+
+ i2s_std_config_t std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256,
+ #ifdef I2S_HW_VERSION_2
+ .ext_clk_freq_hz = 0,
+ #endif
+
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_MONO,
+ .slot_mask = I2S_STD_SLOT_LEFT,
+ .ws_width = I2S_DATA_BIT_WIDTH_32BIT,
+ .ws_pol = false,
+ .bit_shift = true,
+ #ifdef I2S_HW_VERSION_2
+ .left_align = true,
+ .big_endian = false,
+ .bit_order_lsb = false
+ #endif
+
+ },
+ .gpio_cfg = {
+ .mclk = I2S_GPIO_UNUSED,
+ .bclk = bclk,
+ .ws = ws,
+ .dout = dout,
+ .din = din,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
+ ESP_LOGI(TAG, "Duplex channels created");
+}
+
+
+NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din) {
+ duplex_ = false;
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+
+ // Create a new channel for speaker
+ i2s_chan_config_t chan_cfg = {
+ .id = (i2s_port_t)0,
+ .role = I2S_ROLE_MASTER,
+ .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
+ .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
+ .auto_clear_after_cb = true,
+ .auto_clear_before_cb = false,
+ .intr_priority = 0,
+ };
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr));
+
+ i2s_std_config_t std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256,
+ #ifdef I2S_HW_VERSION_2
+ .ext_clk_freq_hz = 0,
+ #endif
+
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_MONO,
+ .slot_mask = I2S_STD_SLOT_LEFT,
+ .ws_width = I2S_DATA_BIT_WIDTH_32BIT,
+ .ws_pol = false,
+ .bit_shift = true,
+ #ifdef I2S_HW_VERSION_2
+ .left_align = true,
+ .big_endian = false,
+ .bit_order_lsb = false
+ #endif
+
+ },
+ .gpio_cfg = {
+ .mclk = I2S_GPIO_UNUSED,
+ .bclk = spk_bclk,
+ .ws = spk_ws,
+ .dout = spk_dout,
+ .din = I2S_GPIO_UNUSED,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
+
+ // Create a new channel for MIC
+ chan_cfg.id = (i2s_port_t)1;
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_));
+ std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
+ std_cfg.gpio_cfg.bclk = mic_sck;
+ std_cfg.gpio_cfg.ws = mic_ws;
+ std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
+ std_cfg.gpio_cfg.din = mic_din;
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
+ ESP_LOGI(TAG, "Simplex channels created");
+}
+
+NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din, i2s_std_slot_mask_t mic_slot_mask){
+ duplex_ = false;
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+
+ // Create a new channel for speaker
+ i2s_chan_config_t chan_cfg = {
+ .id = (i2s_port_t)0,
+ .role = I2S_ROLE_MASTER,
+ .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
+ .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
+ .auto_clear_after_cb = true,
+ .auto_clear_before_cb = false,
+ .intr_priority = 0,
+ };
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr));
+
+ i2s_std_config_t std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256,
+ #ifdef I2S_HW_VERSION_2
+ .ext_clk_freq_hz = 0,
+ #endif
+
+ },
+ .slot_cfg = {
+ .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
+ .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
+ .slot_mode = I2S_SLOT_MODE_MONO,
+ .slot_mask = spk_slot_mask,
+ .ws_width = I2S_DATA_BIT_WIDTH_32BIT,
+ .ws_pol = false,
+ .bit_shift = true,
+ #ifdef I2S_HW_VERSION_2
+ .left_align = true,
+ .big_endian = false,
+ .bit_order_lsb = false
+ #endif
+
+ },
+ .gpio_cfg = {
+ .mclk = I2S_GPIO_UNUSED,
+ .bclk = spk_bclk,
+ .ws = spk_ws,
+ .dout = spk_dout,
+ .din = I2S_GPIO_UNUSED,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false
+ }
+ }
+ };
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
+
+ // Create a new channel for MIC
+ chan_cfg.id = (i2s_port_t)1;
+ ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_));
+ std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
+ std_cfg.slot_cfg.slot_mask = mic_slot_mask;
+ std_cfg.gpio_cfg.bclk = mic_sck;
+ std_cfg.gpio_cfg.ws = mic_ws;
+ std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
+ std_cfg.gpio_cfg.din = mic_din;
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
+ ESP_LOGI(TAG, "Simplex channels created");
+}
+
+NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din) {
+ duplex_ = false;
+ input_sample_rate_ = input_sample_rate;
+ output_sample_rate_ = output_sample_rate;
+
+ // Create a new channel for speaker
+ i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)1, I2S_ROLE_MASTER);
+ tx_chan_cfg.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM;
+ tx_chan_cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM;
+ tx_chan_cfg.auto_clear_after_cb = true;
+ tx_chan_cfg.auto_clear_before_cb = false;
+ tx_chan_cfg.intr_priority = 0;
+ ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle_, NULL));
+
+
+ i2s_std_config_t tx_std_cfg = {
+ .clk_cfg = {
+ .sample_rate_hz = (uint32_t)output_sample_rate_,
+ .clk_src = I2S_CLK_SRC_DEFAULT,
+ .mclk_multiple = I2S_MCLK_MULTIPLE_256,
+ #ifdef I2S_HW_VERSION_2
+ .ext_clk_freq_hz = 0,
+ #endif
+
+ },
+ .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
+ .gpio_cfg = {
+ .mclk = I2S_GPIO_UNUSED,
+ .bclk = spk_bclk,
+ .ws = spk_ws,
+ .dout = spk_dout,
+ .din = I2S_GPIO_UNUSED,
+ .invert_flags = {
+ .mclk_inv = false,
+ .bclk_inv = false,
+ .ws_inv = false,
+ },
+ },
+ };
+ ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &tx_std_cfg));
+#if SOC_I2S_SUPPORTS_PDM_RX
+ // Create a new channel for MIC in PDM mode
+ i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER);
+ ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_));
+ i2s_pdm_rx_config_t pdm_rx_cfg = {
+ .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_),
+ /* The data bit-width of PDM mode is fixed to 16 */
+ .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
+ .gpio_cfg = {
+ .clk = mic_sck,
+ .din = mic_din,
+
+ .invert_flags = {
+ .clk_inv = false,
+ },
+ },
+ };
+ ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle_, &pdm_rx_cfg));
+#else
+ ESP_LOGE(TAG, "PDM is not supported");
+#endif
+ ESP_LOGI(TAG, "Simplex channels created");
+}
+
+int NoAudioCodec::Write(const int16_t* data, int samples) {
+ std::lock_guard lock(data_if_mutex_);
+ std::vector buffer(samples);
+
+ // output_volume_: 0-100
+ // volume_factor_: 0-65536
+ int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536;
+ for (int i = 0; i < samples; i++) {
+ int64_t temp = int64_t(data[i]) * volume_factor; // 使用 int64_t 进行乘法运算
+ if (temp > INT32_MAX) {
+ buffer[i] = INT32_MAX;
+ } else if (temp < INT32_MIN) {
+ buffer[i] = INT32_MIN;
+ } else {
+ buffer[i] = static_cast(temp);
+ }
+ }
+
+ size_t bytes_written;
+ ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY));
+ return bytes_written / sizeof(int32_t);
+}
+
+int NoAudioCodec::Read(int16_t* dest, int samples) {
+ size_t bytes_read;
+
+ std::vector bit32_buffer(samples);
+ if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
+ ESP_LOGE(TAG, "Read Failed!");
+ return 0;
+ }
+
+ samples = bytes_read / sizeof(int32_t);
+ for (int i = 0; i < samples; i++) {
+ int32_t value = bit32_buffer[i] >> 12;
+ dest[i] = (value > INT16_MAX) ? INT16_MAX : (value < -INT16_MAX) ? -INT16_MAX : (int16_t)value;
+ }
+ return samples;
+}
+
+int NoAudioCodecSimplexPdm::Read(int16_t* dest, int samples) {
+ size_t bytes_read;
+
+ // PDM 解调后的数据位宽为 16 位,直接读取到目标缓冲区
+ if (i2s_channel_read(rx_handle_, dest, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
+ ESP_LOGE(TAG, "Read Failed!");
+ return 0;
+ }
+
+ // 计算实际读取的样本数
+ return bytes_read / sizeof(int16_t);
+}
diff --git a/main/audio/codecs/no_audio_codec.h b/main/audio/codecs/no_audio_codec.h
new file mode 100644
index 0000000..c1eedc6
--- /dev/null
+++ b/main/audio/codecs/no_audio_codec.h
@@ -0,0 +1,38 @@
+#ifndef _NO_AUDIO_CODEC_H
+#define _NO_AUDIO_CODEC_H
+
+#include "audio_codec.h"
+
+#include
+#include
+#include
+
+class NoAudioCodec : public AudioCodec {
+protected:
+ std::mutex data_if_mutex_;
+
+ virtual int Write(const int16_t* data, int samples) override;
+ virtual int Read(int16_t* dest, int samples) override;
+
+public:
+ virtual ~NoAudioCodec();
+};
+
+class NoAudioCodecDuplex : public NoAudioCodec {
+public:
+ NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
+};
+
+class NoAudioCodecSimplex : public NoAudioCodec {
+public:
+ NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din);
+ NoAudioCodecSimplex(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_ws, gpio_num_t mic_din, i2s_std_slot_mask_t mic_slot_mask);
+};
+
+class NoAudioCodecSimplexPdm : public NoAudioCodec {
+public:
+ NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din);
+ int Read(int16_t* dest, int samples);
+};
+
+#endif // _NO_AUDIO_CODEC_H
diff --git a/main/audio/processors/afe_audio_processor.cc b/main/audio/processors/afe_audio_processor.cc
new file mode 100644
index 0000000..a9145be
--- /dev/null
+++ b/main/audio/processors/afe_audio_processor.cc
@@ -0,0 +1,189 @@
+#include "afe_audio_processor.h"
+#include
+
+#define PROCESSOR_RUNNING 0x01
+
+#define TAG "AfeAudioProcessor"
+
+AfeAudioProcessor::AfeAudioProcessor()
+ : afe_data_(nullptr) {
+ event_group_ = xEventGroupCreate();
+}
+
+void AfeAudioProcessor::Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) {
+ codec_ = codec;
+ frame_samples_ = frame_duration_ms * 16000 / 1000;
+
+ // Pre-allocate output buffer capacity
+ output_buffer_.reserve(frame_samples_);
+
+ int ref_num = codec_->input_reference() ? 1 : 0;
+
+ std::string input_format;
+ for (int i = 0; i < codec_->input_channels() - ref_num; i++) {
+ input_format.push_back('M');
+ }
+ for (int i = 0; i < ref_num; i++) {
+ input_format.push_back('R');
+ }
+
+ srmodel_list_t *models;
+ if (models_list == nullptr) {
+ models = esp_srmodel_init("model");
+ } else {
+ models = models_list;
+ }
+
+ char* ns_model_name = esp_srmodel_filter(models, ESP_NSNET_PREFIX, NULL);
+ char* vad_model_name = esp_srmodel_filter(models, ESP_VADN_PREFIX, NULL);
+
+ afe_config_t* afe_config = afe_config_init(input_format.c_str(), NULL, AFE_TYPE_VC, AFE_MODE_HIGH_PERF);
+ afe_config->aec_mode = AEC_MODE_VOIP_HIGH_PERF;
+ afe_config->vad_mode = VAD_MODE_0;
+ afe_config->vad_min_noise_ms = 100;
+ if (vad_model_name != nullptr) {
+ afe_config->vad_model_name = vad_model_name;
+ }
+
+ if (ns_model_name != nullptr) {
+ afe_config->ns_init = true;
+ afe_config->ns_model_name = ns_model_name;
+ afe_config->afe_ns_mode = AFE_NS_MODE_NET;
+ } else {
+ afe_config->ns_init = false;
+ }
+
+ afe_config->afe_perferred_core = 1;
+ afe_config->afe_perferred_priority = 1;
+ afe_config->agc_init = false;
+ afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM;
+
+#ifdef CONFIG_USE_DEVICE_AEC
+ afe_config->aec_init = true;
+ afe_config->vad_init = false;
+#else
+ afe_config->aec_init = false;
+ afe_config->vad_init = true;
+#endif
+
+ afe_iface_ = esp_afe_handle_from_config(afe_config);
+ afe_data_ = afe_iface_->create_from_config(afe_config);
+
+ xTaskCreate([](void* arg) {
+ auto this_ = (AfeAudioProcessor*)arg;
+ this_->AudioProcessorTask();
+ vTaskDelete(NULL);
+ }, "audio_communication", 4096, this, 3, NULL);
+}
+
+AfeAudioProcessor::~AfeAudioProcessor() {
+ if (afe_data_ != nullptr) {
+ afe_iface_->destroy(afe_data_);
+ }
+ vEventGroupDelete(event_group_);
+}
+
+size_t AfeAudioProcessor::GetFeedSize() {
+ if (afe_data_ == nullptr) {
+ return 0;
+ }
+ return afe_iface_->get_feed_chunksize(afe_data_);
+}
+
+void AfeAudioProcessor::Feed(std::vector&& data) {
+ if (afe_data_ == nullptr) {
+ return;
+ }
+ afe_iface_->feed(afe_data_, data.data());
+}
+
+void AfeAudioProcessor::Start() {
+ xEventGroupSetBits(event_group_, PROCESSOR_RUNNING);
+}
+
+void AfeAudioProcessor::Stop() {
+ xEventGroupClearBits(event_group_, PROCESSOR_RUNNING);
+ if (afe_data_ != nullptr) {
+ afe_iface_->reset_buffer(afe_data_);
+ }
+}
+
+bool AfeAudioProcessor::IsRunning() {
+ return xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING;
+}
+
+void AfeAudioProcessor::OnOutput(std::function&& data)> callback) {
+ output_callback_ = callback;
+}
+
+void AfeAudioProcessor::OnVadStateChange(std::function callback) {
+ vad_state_change_callback_ = callback;
+}
+
+void AfeAudioProcessor::AudioProcessorTask() {
+ auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_);
+ auto feed_size = afe_iface_->get_feed_chunksize(afe_data_);
+ ESP_LOGI(TAG, "Audio communication task started, feed size: %d fetch size: %d",
+ feed_size, fetch_size);
+
+ while (true) {
+ xEventGroupWaitBits(event_group_, PROCESSOR_RUNNING, pdFALSE, pdTRUE, portMAX_DELAY);
+
+ auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
+ if ((xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING) == 0) {
+ continue;
+ }
+ if (res == nullptr || res->ret_value == ESP_FAIL) {
+ if (res != nullptr) {
+ ESP_LOGI(TAG, "Error code: %d", res->ret_value);
+ }
+ continue;
+ }
+
+ // VAD state change
+ if (vad_state_change_callback_) {
+ if (res->vad_state == VAD_SPEECH && !is_speaking_) {
+ is_speaking_ = true;
+ vad_state_change_callback_(true);
+ } else if (res->vad_state == VAD_SILENCE && is_speaking_) {
+ is_speaking_ = false;
+ vad_state_change_callback_(false);
+ }
+ }
+
+ if (output_callback_) {
+ size_t samples = res->data_size / sizeof(int16_t);
+
+ // Add data to buffer
+ output_buffer_.insert(output_buffer_.end(), res->data, res->data + samples);
+
+ // Output complete frames when buffer has enough data
+ while (output_buffer_.size() >= frame_samples_) {
+ if (output_buffer_.size() == frame_samples_) {
+ // If buffer size equals frame size, move the entire buffer
+ output_callback_(std::move(output_buffer_));
+ output_buffer_.clear();
+ output_buffer_.reserve(frame_samples_);
+ } else {
+ // If buffer size exceeds frame size, copy one frame and remove it
+ output_callback_(std::vector(output_buffer_.begin(), output_buffer_.begin() + frame_samples_));
+ output_buffer_.erase(output_buffer_.begin(), output_buffer_.begin() + frame_samples_);
+ }
+ }
+ }
+ }
+}
+
+void AfeAudioProcessor::EnableDeviceAec(bool enable) {
+ if (enable) {
+#if CONFIG_USE_DEVICE_AEC
+ afe_iface_->disable_vad(afe_data_);
+ afe_iface_->enable_aec(afe_data_);
+#else
+ ESP_LOGE(TAG, "Device AEC is not supported");
+#endif
+ } else {
+ afe_iface_->disable_aec(afe_data_);
+ afe_iface_->enable_vad(afe_data_);
+ }
+}
diff --git a/main/audio/processors/afe_audio_processor.h b/main/audio/processors/afe_audio_processor.h
new file mode 100644
index 0000000..38b1dad
--- /dev/null
+++ b/main/audio/processors/afe_audio_processor.h
@@ -0,0 +1,45 @@
+#ifndef AFE_AUDIO_PROCESSOR_H
+#define AFE_AUDIO_PROCESSOR_H
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "audio_processor.h"
+#include "audio_codec.h"
+
+class AfeAudioProcessor : public AudioProcessor {
+public:
+ AfeAudioProcessor();
+ ~AfeAudioProcessor();
+
+ void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) override;
+ void Feed(std::vector&& data) override;
+ void Start() override;
+ void Stop() override;
+ bool IsRunning() override;
+ void OnOutput(std::function&& data)> callback) override;
+ void OnVadStateChange(std::function callback) override;
+ size_t GetFeedSize() override;
+ void EnableDeviceAec(bool enable) override;
+
+private:
+ EventGroupHandle_t event_group_ = nullptr;
+ esp_afe_sr_iface_t* afe_iface_ = nullptr;
+ esp_afe_sr_data_t* afe_data_ = nullptr;
+ std::function&& data)> output_callback_;
+ std::function vad_state_change_callback_;
+ AudioCodec* codec_ = nullptr;
+ int frame_samples_ = 0;
+ bool is_speaking_ = false;
+ std::vector output_buffer_;
+
+ void AudioProcessorTask();
+};
+
+#endif
\ No newline at end of file
diff --git a/main/audio/processors/audio_debugger.cc b/main/audio/processors/audio_debugger.cc
new file mode 100644
index 0000000..630057c
--- /dev/null
+++ b/main/audio/processors/audio_debugger.cc
@@ -0,0 +1,68 @@
+#include "audio_debugger.h"
+#include "sdkconfig.h"
+
+#if CONFIG_USE_AUDIO_DEBUGGER
+#include
+#include
+#include
+#include
+#include
+#include
+#endif
+
+#define TAG "AudioDebugger"
+
+
+AudioDebugger::AudioDebugger() {
+#if CONFIG_USE_AUDIO_DEBUGGER
+ udp_sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
+ if (udp_sockfd_ >= 0) {
+ // 解析配置的服务器地址 "IP:PORT"
+ std::string server_addr = CONFIG_AUDIO_DEBUG_UDP_SERVER;
+ size_t colon_pos = server_addr.find(':');
+
+ if (colon_pos != std::string::npos) {
+ std::string ip = server_addr.substr(0, colon_pos);
+ int port = std::stoi(server_addr.substr(colon_pos + 1));
+
+ memset(&udp_server_addr_, 0, sizeof(udp_server_addr_));
+ udp_server_addr_.sin_family = AF_INET;
+ udp_server_addr_.sin_port = htons(port);
+ inet_pton(AF_INET, ip.c_str(), &udp_server_addr_.sin_addr);
+
+ ESP_LOGI(TAG, "Initialized server address: %s", CONFIG_AUDIO_DEBUG_UDP_SERVER);
+ } else {
+ ESP_LOGW(TAG, "Invalid server address: %s, should be IP:PORT", CONFIG_AUDIO_DEBUG_UDP_SERVER);
+ close(udp_sockfd_);
+ udp_sockfd_ = -1;
+ }
+ } else {
+ ESP_LOGW(TAG, "Failed to create UDP socket: %d", errno);
+ }
+#endif
+}
+
+AudioDebugger::~AudioDebugger() {
+#if CONFIG_USE_AUDIO_DEBUGGER
+ if (udp_sockfd_ >= 0) {
+ close(udp_sockfd_);
+ ESP_LOGI(TAG, "Closed UDP socket");
+ }
+#endif
+}
+
+void AudioDebugger::Feed(const std::vector& data) {
+#if CONFIG_USE_AUDIO_DEBUGGER
+ if (udp_sockfd_ >= 0) {
+ ssize_t sent = sendto(udp_sockfd_, data.data(), data.size() * sizeof(int16_t), 0,
+ (struct sockaddr*)&udp_server_addr_, sizeof(udp_server_addr_));
+ if (sent < 0) {
+ ESP_LOGW(TAG, "Failed to send audio data to %s: %d", CONFIG_AUDIO_DEBUG_UDP_SERVER, errno);
+ } else {
+ ESP_LOGD(TAG, "Sent %d bytes audio data to %s", sent, CONFIG_AUDIO_DEBUG_UDP_SERVER);
+ }
+ }
+#endif
+}
+
+
\ No newline at end of file
diff --git a/main/audio/processors/audio_debugger.h b/main/audio/processors/audio_debugger.h
new file mode 100644
index 0000000..a81336c
--- /dev/null
+++ b/main/audio/processors/audio_debugger.h
@@ -0,0 +1,22 @@
+#ifndef AUDIO_DEBUGGER_H
+#define AUDIO_DEBUGGER_H
+
+#include
+#include
+
+#include
+#include
+
+class AudioDebugger {
+public:
+ AudioDebugger();
+ ~AudioDebugger();
+
+ void Feed(const std::vector& data);
+
+private:
+ int udp_sockfd_ = -1;
+ struct sockaddr_in udp_server_addr_;
+};
+
+#endif
\ No newline at end of file
diff --git a/main/audio/processors/no_audio_processor.cc b/main/audio/processors/no_audio_processor.cc
new file mode 100644
index 0000000..bac5bad
--- /dev/null
+++ b/main/audio/processors/no_audio_processor.cc
@@ -0,0 +1,59 @@
+#include "no_audio_processor.h"
+#include
+
+#define TAG "NoAudioProcessor"
+
+void NoAudioProcessor::Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) {
+ codec_ = codec;
+ frame_samples_ = frame_duration_ms * 16000 / 1000;
+}
+
+void NoAudioProcessor::Feed(std::vector&& data) {
+ if (!is_running_ || !output_callback_) {
+ return;
+ }
+
+ if (codec_->input_channels() == 2) {
+ // If input channels is 2, we need to fetch the left channel data
+ auto mono_data = std::vector(data.size() / 2);
+ for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
+ mono_data[i] = data[j];
+ }
+ output_callback_(std::move(mono_data));
+ } else {
+ output_callback_(std::move(data));
+ }
+}
+
+void NoAudioProcessor::Start() {
+ is_running_ = true;
+}
+
+void NoAudioProcessor::Stop() {
+ is_running_ = false;
+}
+
+bool NoAudioProcessor::IsRunning() {
+ return is_running_;
+}
+
+void NoAudioProcessor::OnOutput(std::function&& data)> callback) {
+ output_callback_ = callback;
+}
+
+void NoAudioProcessor::OnVadStateChange(std::function callback) {
+ vad_state_change_callback_ = callback;
+}
+
+size_t NoAudioProcessor::GetFeedSize() {
+ if (!codec_) {
+ return 0;
+ }
+ return frame_samples_;
+}
+
+void NoAudioProcessor::EnableDeviceAec(bool enable) {
+ if (enable) {
+ ESP_LOGE(TAG, "Device AEC is not supported");
+ }
+}
diff --git a/main/audio/processors/no_audio_processor.h b/main/audio/processors/no_audio_processor.h
new file mode 100644
index 0000000..d326d50
--- /dev/null
+++ b/main/audio/processors/no_audio_processor.h
@@ -0,0 +1,33 @@
+#ifndef DUMMY_AUDIO_PROCESSOR_H
+#define DUMMY_AUDIO_PROCESSOR_H
+
+#include
+#include
+
+#include "audio_processor.h"
+#include "audio_codec.h"
+
+class NoAudioProcessor : public AudioProcessor {
+public:
+ NoAudioProcessor() = default;
+ ~NoAudioProcessor() = default;
+
+ void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) override;
+ void Feed(std::vector&& data) override;
+ void Start() override;
+ void Stop() override;
+ bool IsRunning() override;
+ void OnOutput(std::function&& data)> callback) override;
+ void OnVadStateChange(std::function callback) override;
+ size_t GetFeedSize() override;
+ void EnableDeviceAec(bool enable) override;
+
+private:
+ AudioCodec* codec_ = nullptr;
+ int frame_samples_ = 0;
+ std::function&& data)> output_callback_;
+ std::function vad_state_change_callback_;
+ bool is_running_ = false;
+};
+
+#endif
\ No newline at end of file
diff --git a/main/audio/wake_word.h b/main/audio/wake_word.h
new file mode 100644
index 0000000..9b8986a
--- /dev/null
+++ b/main/audio/wake_word.h
@@ -0,0 +1,26 @@
+#ifndef WAKE_WORD_H
+#define WAKE_WORD_H
+
+#include
+#include
+#include
+
+#include
+#include "audio_codec.h"
+
+class WakeWord {
+public:
+ virtual ~WakeWord() = default;
+
+ virtual bool Initialize(AudioCodec* codec, srmodel_list_t* models_list) = 0;
+ virtual void Feed(const std::vector