Upgrade Playlist Features

This commit is contained in:
2025-12-09 17:20:01 +08:00
parent 577990de69
commit 8bd2780688
683 changed files with 91812 additions and 81260 deletions

2
.clangd Normal file
View File

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

View File

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

View File

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

View File

@@ -1,34 +1,34 @@
name: Feature request name: Feature request
description: Suggest an idea for this project. description: Suggest an idea for this project.
labels: ['enhancement'] labels: ['enhancement']
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
* We welcome any ideas or feature requests! Its helpful if you can explain exactly why the feature would be useful. * We welcome any ideas or feature requests! Its 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. * 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). * If you would like to contribute, please read the [contributions guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb).
- type: textarea - type: textarea
id: problem-related id: problem-related
attributes: attributes:
label: Is your feature request related to a problem? label: Is your feature request related to a problem?
description: Please provide a clear and concise description of what the problem is. description: Please provide a clear and concise description of what the problem is.
placeholder: ex. I'm always frustrated when ... placeholder: ex. I'm always frustrated when ...
- type: textarea - type: textarea
id: solution id: solution
attributes: attributes:
label: Describe the solution you'd like. label: Describe the solution you'd like.
description: Please provide a clear and concise description of what you want to happen. description: Please provide a clear and concise description of what you want to happen.
placeholder: ex. When using XiaoZhi ... placeholder: ex. When using XiaoZhi ...
- type: textarea - type: textarea
id: alternatives id: alternatives
attributes: attributes:
label: Describe alternatives you've considered. label: Describe alternatives you've considered.
description: Please provide a clear and concise description of any alternative solutions or features 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 ... placeholder: ex. Choosing other approach wouldn't work, because ...
- type: textarea - type: textarea
id: context id: context
attributes: attributes:
label: Additional context. label: Additional context.
description: Please add any other context or screenshots about the feature request here. description: Please add any other context or screenshots about the feature request here.
placeholder: ex. This would work only when ... placeholder: ex. This would work only when ...

View File

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

111
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,111 @@
name: Build Boards
on:
push:
branches:
- main
- ci/* # for ci test
pull_request:
branches:
- main
permissions:
contents: read
jobs:
prepare:
name: Determine variants to build
runs-on: ubuntu-latest
outputs:
variants: ${{ steps.select.outputs.variants }}
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 variant list
run: |
echo "all_variants=$(python scripts/release.py --list-boards --json)" >> $GITHUB_OUTPUT
- id: select
name: Select variants based on changes
env:
ALL_VARIANTS: ${{ steps.list.outputs.all_variants }}
run: |
EVENT_NAME="${{ github.event_name }}"
# push 到 main 分支,编译全部变体
if [[ "$EVENT_NAME" == "push" ]]; then
echo "variants=$ALL_VARIANTS" >> $GITHUB_OUTPUT
exit 0
fi
# 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 -e "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/common/* ]]; 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 "variants=$ALL_VARIANTS" >> $GITHUB_OUTPUT
else
if [[ ${#AFFECTED[@]} -eq 0 ]]; then
echo "variants=[]" >> $GITHUB_OUTPUT
else
BOARDS_JSON=$(printf '%s\n' "${!AFFECTED[@]}" | sort -u | jq -R -s -c 'split("\n")[:-1]')
FILTERED=$(echo "$ALL_VARIANTS" | jq -c --argjson boards "$BOARDS_JSON" 'map(select(.board as $b | $boards | index($b)))')
echo "variants=$FILTERED" >> $GITHUB_OUTPUT
fi
fi
build:
name: Build ${{ matrix.name }}
needs: prepare
if: ${{ needs.prepare.outputs.variants != '[]' }}
strategy:
fail-fast: false # 单个变体失败不影响其它变体
matrix:
include: ${{ fromJson(needs.prepare.outputs.variants) }}
runs-on: ubuntu-latest
container:
image: espressif/idf:release-v5.4
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build current variant
shell: bash
run: |
source $IDF_PATH/export.sh
python scripts/release.py ${{ matrix.board }} --name ${{ matrix.name }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: xiaozhi_${{ matrix.name }}_${{ github.sha }}.bin
path: build/merged-binary.bin
if-no-files-found: error

18
.gitignore vendored Normal file
View File

@@ -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

30
CMakeLists.txt Executable file → Normal file
View File

@@ -1,13 +1,17 @@
# For more information about build system see # For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html # 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 # The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly # CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
set(PROJECT_VER "2.0.1") set(PROJECT_VER "2.0.3")
# Add this line to disable the specific warning # Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers) add_compile_options(-Wno-missing-field-initializers)
include($ENV{IDF_PATH}/tools/cmake/project.cmake) # Fix Windows command line length limit
project(xiaozhi) set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(xiaozhi)

44
LICENSE
View File

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

389
README.md
View File

@@ -1,218 +1,171 @@
# 超级小智-ESP32 # An MCP-based Chatbot | 一个基于 MCP 的聊天机器人
(中文 | English(编写中) | 日本語(编写中)
(中文 | [English](README_en.md) | [日本語](README_ja.md)
基于 https://github.com/78/xiaozhi-esp32 改良的船新版本
## 视频
## 💡介绍
这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,或用于商业用途。 👉 [人类:给 AI 装摄像头 vs AI当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。 👉 [手工打造你的 AI 女友新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群暂无Telegram群暂无。 ## 介绍
项目主要贡献者小霜霜Meow(抖音、B站UP)、空白泡泡糖果B站UP硅灵造物科技B站UP 这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,或用于商业用途。
项目其它贡献者:[@zhubinsheng](https://github.com/zhubinsheng) 我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
贡献者说明:引入部分其它贡献者在其它项目上的代码,并进行了部分修改。 如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群1011329060
音乐服务器、相关源码提供者(为爱发电)小霜霜Meow 项目其它贡献者:@zhubinsheng
音乐服务器源码请见 https://github.com/IntelligentlyEverything/MeowMusicServer 贡献者说明:引入部分其它贡献者在其它项目上的代码,并进行了部分修改。
### ❕注意事项 音乐服务器相关源码提供者(为爱发电)小霜霜Meow
1. 如果小智说找不到歌曲怎么办?
进入[小智后台](https://xiaozhi.me/),找到对应设备,修改角色配置 感谢群友 cz 提供音乐服务器
- 选择 DeepSeekV3 大语言模型
- 在人物介绍中填入 音乐服务器源码请见 https://github.com/IntelligentlyEverything/MeowMusicServer
- 收到音乐相关的需求时,只使用 MPC tool self.music.play_song 工具,同时禁止使用 search_music 功能。
### 基于 MCP 控制万物
2. 内置API调用失败怎么办
请查看具体错误代码后加入QQ群865754861或电报群 http://t.me/MeowMusicServer 给出错误代码和日志,等待我们修复 小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制
### ⚙️已支持硬件芯片系列 ![通过MCP控制万物](docs/mcp-based-graph.jpg)
- ESP32 ### 已实现功能
- ESP32-S3
- ESP32-C3 - Wi-Fi / ML307 Cat.1 4G
- ESP32-C6 - 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
- ESP32-P4 - 支持两种通信协议([Websocket](docs/websocket.md) 或 MQTT+UDP
- 采用 OPUS 音频编解码
❕大部分硬件由于没有进行完整测试可能会存在一些问题属于正常现象具体可提交issues进行反馈。 - 基于流式 ASR + LLM + TTS 架构的语音交互
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker)
### 项目改动范围 - OLED / LCD 显示屏,支持表情显示
新增: - 电量显示与电源管理
- main/schedule_manager.h - 支持多语言(中文、英文、日文)
- main/schedule_manager.cc - 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台
- main/audio/timer_manager.h - 通过设备端 MCP 实现设备控制音量、灯光、电机、GPIO 等)
- main/audio/timer_manager.cc - 通过云端 MCP 扩展大模型能力智能家居控制、PC桌面操作、知识搜索、邮件收发等
- main/boards/common/music.h
- main/boards/common/esp32_music.h ## 硬件
- main/boards/common/esp32_music.cc
- main/display/esplog_display.h ### 面包板手工制作实践
- main/display/esplog_display.cc
- main/protocols/sleep_music_protocol.h 详见飞书文档教程:
- main/protocols/sleep_music_protocol.cc
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
修改:
- main/audio/codecs/no_audio_codec.h 面包板效果图如下:
- main/audio/codecs/no_audio_codec.cc
- main/audio/audio_codec.h ![面包板效果图](docs/v1/wiring2.jpg)
- main/audio/audio_codec.cc
- main/audio/audio_service.h ### 支持 70 多个开源硬件(仅展示部分)
- main/audio/audio_service.cc
- main/boards/common/board.h - <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a>
- main/boards/common/board.cc - <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a>
- main/display/display.h - <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- main/display/display.cc - <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
- main/display/lcd_display.h - <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
- main/display/lcd_display.cc - <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a>
- main/application.h - <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- main/application.cc - <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a>
- main/idf_component.yml - <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">璀璨·AI 吊坠</a>
- main/mcp_server.cc - <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="无名科技Nologo-星智-1.54">无名科技 Nologo-星智-1.54TFT</a>
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
### 基于 MCP 控制万物 - <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI 超低成本机器狗">ESP-HI 超低成本机器狗</a>
小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制。
<div style="display: flex; justify-content: space-between;">
![通过MCP控制万物](docs/mcp-based-graph.jpg) <a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板">
<img src="docs/v1/lichuang-s3.jpg" width="240" />
### 已实现功能 </a>
<a href="docs/v1/espbox3.jpg" target="_blank" title="乐鑫 ESP32-S3-BOX3">
- Wi-Fi / ML307 Cat.1 4G <img src="docs/v1/espbox3.jpg" width="240" />
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr) </a>
- 支持两种通信协议([Websocket](docs/websocket.md) 或 MQTT+UDP <a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
- 采用 OPUS 音频编解码 <img src="docs/v1/m5cores3.jpg" width="240" />
- 基于流式 ASR + LLM + TTS 架构的语音交互 </a>
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker) <a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
- OLED / LCD 显示屏,支持表情显示 <img src="docs/v1/atoms3r.jpg" width="240" />
- 电量显示与电源管理 </a>
- 支持多语言(中文、英文、日文) <a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4">
- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台 <img src="docs/v1/magiclick.jpg" width="240" />
- 通过设备端 MCP 实现设备控制音量、灯光、电机、GPIO 等) </a>
- 通过云端 MCP 扩展大模型能力智能家居控制、PC桌面操作、知识搜索、邮件收发等 <a href="docs/v1/waveshare.jpg" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">
本项目新增功能: <img src="docs/v1/waveshare.jpg" width="240" />
- 新增音乐播放功能,支持播放本地音乐(开发中,敬请期待)、云端音乐(完善中)。 </a>
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
## 硬件 <img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
</a>
### 面包板手工制作实践 <a href="docs/v1/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
<img src="docs/v1/xmini-c3.jpg" width="240" />
详见飞书文档教程: </a>
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) <img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
</a>
面包板效果图如下: <a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="无名科技Nologo-星智-1.54">
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
![面包板效果图](docs/v1/wiring2.jpg) </a>
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
### 支持 70 多个开源硬件(仅展示部分) <img src="docs/v1/sensecap_watcher.jpg" width="240" />
</a>
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a> <a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI 超低成本机器狗">
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a> <img src="docs/v1/esp-hi.jpg" width="240" />
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a> </a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a> </div>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a> ## 软件
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a> ### 固件烧录
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">璀璨·AI 吊坠</a>
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="无名科技Nologo-星智-1.54">无名科技 Nologo-星智-1.54TFT</a> 新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI 超低成本机器狗">ESP-HI 超低成本机器狗</a> 固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,个人用户注册账号可以免费使用 Qwen 实时模型。
<div style="display: flex; justify-content: space-between;"> 👉 [新手烧录固件教程](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板">
<img src="docs/v1/lichuang-s3.jpg" width="240" /> ### 开发环境
</a>
<a href="docs/v1/espbox3.jpg" target="_blank" title="乐鑫 ESP32-S3-BOX3"> - Cursor 或 VSCode
<img src="docs/v1/espbox3.jpg" width="240" /> - 安装 ESP-IDF 插件,选择 SDK 版本 5.4 或以上
</a> - Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3"> - 本项目使用 Google C++ 代码风格,提交代码时请确保符合规范
<img src="docs/v1/m5cores3.jpg" width="240" />
</a> ### 开发者文档
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
<img src="docs/v1/atoms3r.jpg" width="240" /> - [自定义开发板指南](main/boards/README.md) - 学习如何为小智 AI 创建自定义开发板
</a> - [MCP 协议物联网控制用法说明](docs/mcp-usage.md) - 了解如何通过 MCP 协议控制物联网设备
<a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4"> - [MCP 协议交互流程](docs/mcp-protocol.md) - 设备端 MCP 协议的实现方式
<img src="docs/v1/magiclick.jpg" width="240" /> - [MQTT + UDP 混合通信协议文档](docs/mqtt-udp.md)
</a> - [一份详细的 WebSocket 通信协议文档](docs/websocket.md)
<a href="docs/v1/waveshare.jpg" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">
<img src="docs/v1/waveshare.jpg" width="240" /> ## 大模型配置
</a>
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3"> 如果你已经拥有一个小智 AI 聊天机器人设备,并且已接入官方服务器,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
</a> 👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
<img src="docs/v1/xmini-c3.jpg" width="240" /> ## 相关开源项目
</a>
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan"> 在个人电脑上部署服务器,可以参考以下第三方开源的项目:
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
</a> - [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python 服务器
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="无名科技Nologo-星智-1.54"> - [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java 服务器
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" /> - [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang 服务器
</a>
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher"> 使用小智通信协议的第三方客户端项目:
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
</a> - [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python 客户端
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI 超低成本机器狗"> - [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客户端
<img src="docs/v1/esp-hi.jpg" width="240" /> - [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百问科技提供的 Linux 客户端
</a> - [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技的蓝牙芯片固件
</div> - [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移远提供的 QuecPython 固件
## 软件 ## Star History
### 固件烧录 <a href="https://star-history.com/#78/xiaozhi-esp32&Date">
<picture>
新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。 <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,个人用户注册账号可以免费使用 Qwen 实时模型。 <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
</picture>
👉 [新手烧录固件教程](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) </a>
### 开发环境
- 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
<a href="https://star-history.com/#IntelligentlyEverything/xiaozhi-esp32&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=IntelligentlyEverything/xiaozhi-esp32&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=IntelligentlyEverything/xiaozhi-esp32&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=IntelligentlyEverything/xiaozhi-esp32&type=Date" />
</picture>
</a>

157
README_en.md Normal file
View File

@@ -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.
![Control everything via MCP](docs/mcp-based-graph.jpg)
### 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:
![Breadboard Demo](docs/v1/wiring2.jpg)
### Supports 70+ Open Source Hardware (Partial List)
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="Magic Button 2.4">Magic Button 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a>
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AI Pendant</a>
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="WMnologo-Xingzhi-1.54">WMnologo-Xingzhi-1.54TFT</a>
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI Low Cost Robot Dog">ESP-HI Low Cost Robot Dog</a>
<div style="display: flex; justify-content: space-between;">
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
<img src="docs/v1/lichuang-s3.jpg" width="240" />
</a>
<a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
<img src="docs/v1/espbox3.jpg" width="240" />
</a>
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
<img src="docs/v1/m5cores3.jpg" width="240" />
</a>
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
<img src="docs/v1/atoms3r.jpg" width="240" />
</a>
<a href="docs/v1/magiclick.jpg" target="_blank" title="Magic Button 2.4">
<img src="docs/v1/magiclick.jpg" width="240" />
</a>
<a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
<img src="docs/v1/waveshare.jpg" width="240" />
</a>
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
</a>
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="XiaGe Mini C3">
<img src="docs/v1/xmini-c3.jpg" width="240" />
</a>
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
</a>
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="WMnologo-Xingzhi-1.54">
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
</a>
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
</a>
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI Low Cost Robot Dog">
<img src="docs/v1/esp-hi.jpg" width="240" />
</a>
</div>
## 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
<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
</picture>
</a>

157
README_ja.md Normal file
View File

@@ -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プロトコルを通じてマルチエンド制御を実現します。
![MCPであらゆるものを制御](docs/mcp-based-graph.jpg)
### 実装済み機能
- 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)
ブレッドボードのデモ:
![ブレッドボードデモ](docs/v1/wiring2.jpg)
### 70種類以上のオープンソースハードウェアに対応一部のみ表示
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立創・実戦派 ESP32-S3 開発ボード">立創・実戦派 ESP32-S3 開発ボード</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="楽鑫 ESP32-S3-BOX3">楽鑫 ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="マジックボタン2.4">マジックボタン2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪電子 ESP32-S3-Touch-AMOLED-1.8">微雪電子 ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="エビ兄さん Mini C3">エビ兄さん Mini C3</a>
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AIペンダント</a>
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="無名科技Nologo-星智-1.54">無名科技Nologo-星智-1.54TFT</a>
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI 超低コストロボット犬">ESP-HI 超低コストロボット犬</a>
<div style="display: flex; justify-content: space-between;">
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立創・実戦派 ESP32-S3 開発ボード">
<img src="docs/v1/lichuang-s3.jpg" width="240" />
</a>
<a href="docs/v1/espbox3.jpg" target="_blank" title="楽鑫 ESP32-S3-BOX3">
<img src="docs/v1/espbox3.jpg" width="240" />
</a>
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
<img src="docs/v1/m5cores3.jpg" width="240" />
</a>
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
<img src="docs/v1/atoms3r.jpg" width="240" />
</a>
<a href="docs/v1/magiclick.jpg" target="_blank" title="マジックボタン2.4">
<img src="docs/v1/magiclick.jpg" width="240" />
</a>
<a href="docs/v1/waveshare.jpg" target="_blank" title="微雪電子 ESP32-S3-Touch-AMOLED-1.8">
<img src="docs/v1/waveshare.jpg" width="240" />
</a>
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
</a>
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="エビ兄さん Mini C3">
<img src="docs/v1/xmini-c3.jpg" width="240" />
</a>
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
</a>
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="無名科技Nologo-星智-1.54">
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
</a>
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
</a>
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI 超低コストロボット犬">
<img src="docs/v1/esp-hi.jpg" width="240" />
</a>
</div>
## ソフトウェア
### ファームウェア書き込み
初心者の方は、まず開発環境を構築せずに書き込み可能なファームウェアを使用することをおすすめします。
ファームウェアはデフォルトで公式 [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クライアント
## スター履歴
<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
</picture>
</a>

228
SERVER_CONFIG_GUIDE.md Normal file
View File

@@ -0,0 +1,228 @@
# ESP32服务器地址配置指南
## 📍 **配置文件位置**
```
xiaozhi-esp32_music/main/server_config.h
```
## 🔧 **如何修改服务器地址**
### **步骤1查看您的服务器IP地址**
#### **Windows系统**
打开命令提示符CMD输入
```cmd
ipconfig
```
查找"IPv4 地址",例如:`192.168.1.100`
#### **Linux/Mac系统**
打开终端,输入:
```bash
ifconfig
# 或
ip addr
```
查找局域网IP地址。
---
### **步骤2编辑配置文件**
打开文件:
```
d:\esp32-music-server\Meow\MeowEmbeddedMusicServer\xiaozhi-esp32_music\xiaozhi-esp32_music\main\server_config.h
```
找到这一行:
```cpp
#define MUSIC_SERVER_URL "http://192.168.1.100:2233"
```
`192.168.1.100` 替换为您的服务器IP地址。
---
## 🌐 **配置示例**
### **示例1本地局域网推荐**
服务器和ESP32在同一个WiFi网络中
```cpp
#define MUSIC_SERVER_URL "http://192.168.1.100:2233"
```
**优点**
- ✅ 速度快
- ✅ 延迟低
- ✅ 不需要公网IP
---
### **示例2公网IP**
如果您有公网IP或使用花生壳等内网穿透
```cpp
#define MUSIC_SERVER_URL "http://123.45.67.89:2233"
```
**注意**
- ⚠️ 确保路由器端口转发2233端口
- ⚠️ 注意服务器安全
---
### **示例3域名**
如果您有域名:
```cpp
#define MUSIC_SERVER_URL "http://your-music-server.com:2233"
```
**注意**
- ⚠️ 确保域名解析正确
- ⚠️ 如果使用HTTPS改为`https://`
---
### **示例4使用原作者在线服务器**
```cpp
#define MUSIC_SERVER_URL "http://http-embedded-music.miao-lab.top:2233"
```
**说明**
- ✅ 无需自己搭建服务器
- ⚠️ 依赖外部服务可用性
- ⚠️ 无法使用设备绑定等个性化功能
---
## 🔍 **如何测试服务器地址是否正确**
### **方法1浏览器测试**
在浏览器中访问:
```
http://您的服务器IP:2233
```
应该看到Meow Music的Web界面。
---
### **方法2curl测试**
```bash
curl http://您的服务器IP:2233/api/search?song=江南
```
应该返回JSON格式的搜索结果。
---
## 📝 **完整配置检查清单**
- [ ] 确认服务器正在运行(`go run .`
- [ ] 确认服务器端口是2233
- [ ] 确认ESP32和服务器在同一网络或有公网连接
- [ ] 修改`server_config.h`中的IP地址
- [ ] 保存文件
- [ ] 重新编译ESP32固件`idf.py build`
- [ ] 烧录到ESP32`idf.py flash`
- [ ] 测试连接
---
## 🐛 **常见问题**
### **问题1ESP32无法连接服务器**
**现象**
```
[Esp32Music] Failed to connect to music API
```
**排查**
1. 检查服务器是否运行
2. 检查IP地址是否正确
3. 检查ESP32是否连接WiFi
4. Ping服务器IP测试网络连通性
---
### **问题2地址写错了**
**现象**
```
[Esp32Music] HTTP GET failed with status code: 404
```
**解决**
- 检查URL格式是否正确
- 确保有`http://`前缀
- 确保端口号是`:2233`
---
### **问题3防火墙阻止**
**现象**
- ESP32无法连接
- 但浏览器可以访问
**解决Windows**
```
控制面板 → Windows防火墙 → 允许应用通过防火墙
→ 找到Go程序 → 允许专用和公用网络
```
---
## 🎯 **推荐配置**
### **开发测试阶段**
使用局域网IP
```cpp
#define MUSIC_SERVER_URL "http://192.168.1.100:2233"
```
### **生产环境**
使用域名:
```cpp
#define MUSIC_SERVER_URL "http://music.your-domain.com:2233"
```
---
## 💡 **高级技巧**
### **使用环境变量(未来功能)**
可以考虑在ESP32端添加NVS配置通过Web界面修改服务器地址无需重新编译。
### **mDNS服务发现未来功能**
可以使用mDNS实现服务器自动发现
```
http://meow-music.local:2233
```
---
## 📞 **技术支持**
如有问题,请加入:
**喵波音律QQ交流群865754861**
---
**配置完成后,记得重新编译并烧录固件!** 🚀

BIN
combine.exe Executable file

Binary file not shown.

View File

@@ -1,269 +1,269 @@
# MCP (Model Context Protocol) 交互流程 # MCP (Model Context Protocol) 交互流程
NOTICE: AI 辅助生成, 在实现后台服务时, 请参照代码确认细节!! NOTICE: AI 辅助生成, 在实现后台服务时, 请参照代码确认细节!!
本项目中的 MCP 协议用于后台 APIMCP 客户端)与 ESP32 设备MCP 服务器)之间的通信,以便后台能够发现和调用设备提供的功能(工具)。 本项目中的 MCP 协议用于后台 APIMCP 客户端)与 ESP32 设备MCP 服务器)之间的通信,以便后台能够发现和调用设备提供的功能(工具)。
## 协议格式 ## 协议格式
根据代码 (`main/protocols/protocol.cc`, `main/mcp_server.cc`)MCP 消息是封装在基础通信协议(如 WebSocket 或 MQTT的消息体中的。其内部结构遵循 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) 规范。 根据代码 (`main/protocols/protocol.cc`, `main/mcp_server.cc`)MCP 消息是封装在基础通信协议(如 WebSocket 或 MQTT的消息体中的。其内部结构遵循 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) 规范。
整体消息结构示例: 整体消息结构示例:
```json ```json
{ {
"session_id": "...", // 会话 ID "session_id": "...", // 会话 ID
"type": "mcp", // 消息类型,固定为 "mcp" "type": "mcp", // 消息类型,固定为 "mcp"
"payload": { // JSON-RPC 2.0 负载 "payload": { // JSON-RPC 2.0 负载
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "...", // 方法名 (如 "initialize", "tools/list", "tools/call") "method": "...", // 方法名 (如 "initialize", "tools/list", "tools/call")
"params": { ... }, // 方法参数 (对于 request) "params": { ... }, // 方法参数 (对于 request)
"id": ..., // 请求 ID (对于 request 和 response) "id": ..., // 请求 ID (对于 request 和 response)
"result": { ... }, // 方法执行结果 (对于 success response) "result": { ... }, // 方法执行结果 (对于 success response)
"error": { ... } // 错误信息 (对于 error response) "error": { ... } // 错误信息 (对于 error response)
} }
} }
``` ```
其中,`payload` 部分是标准的 JSON-RPC 2.0 消息: 其中,`payload` 部分是标准的 JSON-RPC 2.0 消息:
- `jsonrpc`: 固定的字符串 "2.0"。 - `jsonrpc`: 固定的字符串 "2.0"。
- `method`: 要调用的方法名称 (对于 Request)。 - `method`: 要调用的方法名称 (对于 Request)。
- `params`: 方法的参数,一个结构化值,通常为对象 (对于 Request)。 - `params`: 方法的参数,一个结构化值,通常为对象 (对于 Request)。
- `id`: 请求的标识符,客户端发送请求时提供,服务器响应时原样返回。用于匹配请求和响应。 - `id`: 请求的标识符,客户端发送请求时提供,服务器响应时原样返回。用于匹配请求和响应。
- `result`: 方法成功执行时的结果 (对于 Success Response)。 - `result`: 方法成功执行时的结果 (对于 Success Response)。
- `error`: 方法执行失败时的错误信息 (对于 Error Response)。 - `error`: 方法执行失败时的错误信息 (对于 Error Response)。
## 交互流程及发送时机 ## 交互流程及发送时机
MCP 的交互主要围绕客户端(后台 API发现和调用设备上的“工具”Tool进行。 MCP 的交互主要围绕客户端(后台 API发现和调用设备上的“工具”Tool进行。
1. **连接建立与能力通告** 1. **连接建立与能力通告**
- **时机:** 设备启动并成功连接到后台 API 后。 - **时机:** 设备启动并成功连接到后台 API 后。
- **发送方:** 设备。 - **发送方:** 设备。
- **消息:** 设备发送基础协议的 "hello" 消息给后台 API消息中包含设备支持的能力列表例如通过支持 MCP 协议 (`"mcp": true`)。 - **消息:** 设备发送基础协议的 "hello" 消息给后台 API消息中包含设备支持的能力列表例如通过支持 MCP 协议 (`"mcp": true`)。
- **示例 (非 MCP 负载,而是基础协议消息):** - **示例 (非 MCP 负载,而是基础协议消息):**
```json ```json
{ {
"type": "hello", "type": "hello",
"version": ..., "version": ...,
"features": { "features": {
"mcp": true, "mcp": true,
... ...
}, },
"transport": "websocket", // 或 "mqtt" "transport": "websocket", // 或 "mqtt"
"audio_params": { ... }, "audio_params": { ... },
"session_id": "..." // 设备收到服务器hello后可能设置 "session_id": "..." // 设备收到服务器hello后可能设置
} }
``` ```
2. **初始化 MCP 会话** 2. **初始化 MCP 会话**
- **时机:** 后台 API 收到设备 "hello" 消息,确认设备支持 MCP 后,通常作为 MCP 会话的第一个请求发送。 - **时机:** 后台 API 收到设备 "hello" 消息,确认设备支持 MCP 后,通常作为 MCP 会话的第一个请求发送。
- **发送方:** 后台 API (客户端)。 - **发送方:** 后台 API (客户端)。
- **方法:** `initialize` - **方法:** `initialize`
- **消息 (MCP payload):** - **消息 (MCP payload):**
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "initialize", "method": "initialize",
"params": { "params": {
"capabilities": { "capabilities": {
// 客户端能力,可选 // 客户端能力,可选
// 摄像头视觉相关 // 摄像头视觉相关
"vision": { "vision": {
"url": "...", //摄像头: 图片处理地址(必须是http地址, 不是websocket地址) "url": "...", //摄像头: 图片处理地址(必须是http地址, 不是websocket地址)
"token": "..." // url token "token": "..." // url token
} }
// ... 其他客户端能力 // ... 其他客户端能力
} }
}, },
"id": 1 // 请求 ID "id": 1 // 请求 ID
} }
``` ```
- **设备响应时机:** 设备收到 `initialize` 请求并处理后。 - **设备响应时机:** 设备收到 `initialize` 请求并处理后。
- **设备响应消息 (MCP payload):** - **设备响应消息 (MCP payload):**
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 1, // 匹配请求 ID "id": 1, // 匹配请求 ID
"result": { "result": {
"protocolVersion": "2024-11-05", "protocolVersion": "2024-11-05",
"capabilities": { "capabilities": {
"tools": {} // 这里的 tools 似乎不列出详细信息,需要 tools/list "tools": {} // 这里的 tools 似乎不列出详细信息,需要 tools/list
}, },
"serverInfo": { "serverInfo": {
"name": "...", // 设备名称 (BOARD_NAME) "name": "...", // 设备名称 (BOARD_NAME)
"version": "..." // 设备固件版本 "version": "..." // 设备固件版本
} }
} }
} }
``` ```
3. **发现设备工具列表** 3. **发现设备工具列表**
- **时机:** 后台 API 需要获取设备当前支持的具体功能(工具)列表及其调用方式时。 - **时机:** 后台 API 需要获取设备当前支持的具体功能(工具)列表及其调用方式时。
- **发送方:** 后台 API (客户端)。 - **发送方:** 后台 API (客户端)。
- **方法:** `tools/list` - **方法:** `tools/list`
- **消息 (MCP payload):** - **消息 (MCP payload):**
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "tools/list", "method": "tools/list",
"params": { "params": {
"cursor": "" // 用于分页,首次请求为空字符串 "cursor": "" // 用于分页,首次请求为空字符串
}, },
"id": 2 // 请求 ID "id": 2 // 请求 ID
} }
``` ```
- **设备响应时机:** 设备收到 `tools/list` 请求并生成工具列表后。 - **设备响应时机:** 设备收到 `tools/list` 请求并生成工具列表后。
- **设备响应消息 (MCP payload):** - **设备响应消息 (MCP payload):**
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 2, // 匹配请求 ID "id": 2, // 匹配请求 ID
"result": { "result": {
"tools": [ // 工具对象列表 "tools": [ // 工具对象列表
{ {
"name": "self.get_device_status", "name": "self.get_device_status",
"description": "...", "description": "...",
"inputSchema": { ... } // 参数 schema "inputSchema": { ... } // 参数 schema
}, },
{ {
"name": "self.audio_speaker.set_volume", "name": "self.audio_speaker.set_volume",
"description": "...", "description": "...",
"inputSchema": { ... } // 参数 schema "inputSchema": { ... } // 参数 schema
} }
// ... 更多工具 // ... 更多工具
], ],
"nextCursor": "..." // 如果列表很大需要分页,这里会包含下一个请求的 cursor 值 "nextCursor": "..." // 如果列表很大需要分页,这里会包含下一个请求的 cursor 值
} }
} }
``` ```
- **分页处理:** 如果 `nextCursor` 字段非空,客户端需要再次发送 `tools/list` 请求,并在 `params` 中带上这个 `cursor` 值以获取下一页工具。 - **分页处理:** 如果 `nextCursor` 字段非空,客户端需要再次发送 `tools/list` 请求,并在 `params` 中带上这个 `cursor` 值以获取下一页工具。
4. **调用设备工具** 4. **调用设备工具**
- **时机:** 后台 API 需要执行设备上的某个具体功能时。 - **时机:** 后台 API 需要执行设备上的某个具体功能时。
- **发送方:** 后台 API (客户端)。 - **发送方:** 后台 API (客户端)。
- **方法:** `tools/call` - **方法:** `tools/call`
- **消息 (MCP payload):** - **消息 (MCP payload):**
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "tools/call", "method": "tools/call",
"params": { "params": {
"name": "self.audio_speaker.set_volume", // 要调用的工具名称 "name": "self.audio_speaker.set_volume", // 要调用的工具名称
"arguments": { "arguments": {
// 工具参数,对象格式 // 工具参数,对象格式
"volume": 50 // 参数名及其值 "volume": 50 // 参数名及其值
} }
}, },
"id": 3 // 请求 ID "id": 3 // 请求 ID
} }
``` ```
- **设备响应时机:** 设备收到 `tools/call` 请求,执行相应的工具函数后。 - **设备响应时机:** 设备收到 `tools/call` 请求,执行相应的工具函数后。
- **设备成功响应消息 (MCP payload):** - **设备成功响应消息 (MCP payload):**
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 3, // 匹配请求 ID "id": 3, // 匹配请求 ID
"result": { "result": {
"content": [ "content": [
// 工具执行结果内容 // 工具执行结果内容
{ "type": "text", "text": "true" } // 示例set_volume 返回 bool { "type": "text", "text": "true" } // 示例set_volume 返回 bool
], ],
"isError": false // 表示成功 "isError": false // 表示成功
} }
} }
``` ```
- **设备失败响应消息 (MCP payload):** - **设备失败响应消息 (MCP payload):**
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 3, // 匹配请求 ID "id": 3, // 匹配请求 ID
"error": { "error": {
"code": -32601, // JSON-RPC 错误码,例如 Method not found (-32601) "code": -32601, // JSON-RPC 错误码,例如 Method not found (-32601)
"message": "Unknown tool: self.non_existent_tool" // 错误描述 "message": "Unknown tool: self.non_existent_tool" // 错误描述
} }
} }
``` ```
5. **设备主动发送消息 (Notifications)** 5. **设备主动发送消息 (Notifications)**
- **时机:** 设备内部发生需要通知后台 API 的事件时(例如,状态变化,虽然代码示例中没有明确的工具发送此类消息,但 `Application::SendMcpMessage` 的存在暗示了设备可能主动发送 MCP 消息)。 - **时机:** 设备内部发生需要通知后台 API 的事件时(例如,状态变化,虽然代码示例中没有明确的工具发送此类消息,但 `Application::SendMcpMessage` 的存在暗示了设备可能主动发送 MCP 消息)。
- **发送方:** 设备 (服务器)。 - **发送方:** 设备 (服务器)。
- **方法:** 可能是以 `notifications/` 开头的方法名,或者其他自定义方法。 - **方法:** 可能是以 `notifications/` 开头的方法名,或者其他自定义方法。
- **消息 (MCP payload):** 遵循 JSON-RPC Notification 格式,没有 `id` 字段。 - **消息 (MCP payload):** 遵循 JSON-RPC Notification 格式,没有 `id` 字段。
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "notifications/state_changed", // 示例方法名 "method": "notifications/state_changed", // 示例方法名
"params": { "params": {
"newState": "idle", "newState": "idle",
"oldState": "connecting" "oldState": "connecting"
} }
// 没有 id 字段 // 没有 id 字段
} }
``` ```
- **后台 API 处理:** 接收到 Notification 后,后台 API 进行相应的处理,但不回复。 - **后台 API 处理:** 接收到 Notification 后,后台 API 进行相应的处理,但不回复。
## 交互图 ## 交互图
下面是一个简化的交互序列图,展示了主要的 MCP 消息流程: 下面是一个简化的交互序列图,展示了主要的 MCP 消息流程:
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant Device as ESP32 Device participant Device as ESP32 Device
participant BackendAPI as 后台 API (Client) participant BackendAPI as 后台 API (Client)
Note over Device, BackendAPI: 建立 WebSocket / MQTT 连接 Note over Device, BackendAPI: 建立 WebSocket / MQTT 连接
Device->>BackendAPI: Hello Message (包含 "mcp": true) Device->>BackendAPI: Hello Message (包含 "mcp": true)
BackendAPI->>Device: MCP Initialize Request BackendAPI->>Device: MCP Initialize Request
Note over BackendAPI: method: initialize Note over BackendAPI: method: initialize
Note over BackendAPI: params: { capabilities: ... } Note over BackendAPI: params: { capabilities: ... }
Device->>BackendAPI: MCP Initialize Response Device->>BackendAPI: MCP Initialize Response
Note over Device: result: { protocolVersion: ..., serverInfo: ... } Note over Device: result: { protocolVersion: ..., serverInfo: ... }
BackendAPI->>Device: MCP Get Tools List Request BackendAPI->>Device: MCP Get Tools List Request
Note over BackendAPI: method: tools/list Note over BackendAPI: method: tools/list
Note over BackendAPI: params: { cursor: "" } Note over BackendAPI: params: { cursor: "" }
Device->>BackendAPI: MCP Get Tools List Response Device->>BackendAPI: MCP Get Tools List Response
Note over Device: result: { tools: [...], nextCursor: ... } Note over Device: result: { tools: [...], nextCursor: ... }
loop Optional Pagination loop Optional Pagination
BackendAPI->>Device: MCP Get Tools List Request BackendAPI->>Device: MCP Get Tools List Request
Note over BackendAPI: method: tools/list Note over BackendAPI: method: tools/list
Note over BackendAPI: params: { cursor: "..." } Note over BackendAPI: params: { cursor: "..." }
Device->>BackendAPI: MCP Get Tools List Response Device->>BackendAPI: MCP Get Tools List Response
Note over Device: result: { tools: [...], nextCursor: "" } Note over Device: result: { tools: [...], nextCursor: "" }
end end
BackendAPI->>Device: MCP Call Tool Request BackendAPI->>Device: MCP Call Tool Request
Note over BackendAPI: method: tools/call Note over BackendAPI: method: tools/call
Note over BackendAPI: params: { name: "...", arguments: { ... } } Note over BackendAPI: params: { name: "...", arguments: { ... } }
alt Tool Call Successful alt Tool Call Successful
Device->>BackendAPI: MCP Tool Call Success Response Device->>BackendAPI: MCP Tool Call Success Response
Note over Device: result: { content: [...], isError: false } Note over Device: result: { content: [...], isError: false }
else Tool Call Failed else Tool Call Failed
Device->>BackendAPI: MCP Tool Call Error Response Device->>BackendAPI: MCP Tool Call Error Response
Note over Device: error: { code: ..., message: ... } Note over Device: error: { code: ..., message: ... }
end end
opt Device Notification opt Device Notification
Device->>BackendAPI: MCP Notification Device->>BackendAPI: MCP Notification
Note over Device: method: notifications/... Note over Device: method: notifications/...
Note over Device: params: { ... } Note over Device: params: { ... }
end end
``` ```
这份文档概述了该项目中 MCP 协议的主要交互流程。具体的参数细节和工具功能需要参考 `main/mcp_server.cc` 中 `McpServer::AddCommonTools` 以及各个工具的实现。 这份文档概述了该项目中 MCP 协议的主要交互流程。具体的参数细节和工具功能需要参考 `main/mcp_server.cc` 中 `McpServer::AddCommonTools` 以及各个工具的实现。

View File

@@ -1,115 +1,115 @@
# MCP 协议物联网控制用法说明 # MCP 协议物联网控制用法说明
> 本文档介绍如何基于 MCP 协议实现 ESP32 设备的物联网控制。详细协议流程请参考 [`mcp-protocol.md`](./mcp-protocol.md)。 > 本文档介绍如何基于 MCP 协议实现 ESP32 设备的物联网控制。详细协议流程请参考 [`mcp-protocol.md`](./mcp-protocol.md)。
## 简介 ## 简介
MCPModel Context Protocol是新一代推荐用于物联网控制的协议通过标准 JSON-RPC 2.0 格式在后台与设备间发现和调用"工具"Tool实现灵活的设备控制。 MCPModel Context Protocol是新一代推荐用于物联网控制的协议通过标准 JSON-RPC 2.0 格式在后台与设备间发现和调用"工具"Tool实现灵活的设备控制。
## 典型使用流程 ## 典型使用流程
1. 设备启动后通过基础协议(如 WebSocket/MQTT与后台建立连接。 1. 设备启动后通过基础协议(如 WebSocket/MQTT与后台建立连接。
2. 后台通过 MCP 协议的 `initialize` 方法初始化会话。 2. 后台通过 MCP 协议的 `initialize` 方法初始化会话。
3. 后台通过 `tools/list` 获取设备支持的所有工具(功能)及参数说明。 3. 后台通过 `tools/list` 获取设备支持的所有工具(功能)及参数说明。
4. 后台通过 `tools/call` 调用具体工具,实现对设备的控制。 4. 后台通过 `tools/call` 调用具体工具,实现对设备的控制。
详细协议格式与交互请见 [`mcp-protocol.md`](./mcp-protocol.md)。 详细协议格式与交互请见 [`mcp-protocol.md`](./mcp-protocol.md)。
## 设备端工具注册方法说明 ## 设备端工具注册方法说明
设备通过 `McpServer::AddTool` 方法注册可被后台调用的"工具"。其常用函数签名如下: 设备通过 `McpServer::AddTool` 方法注册可被后台调用的"工具"。其常用函数签名如下:
```cpp ```cpp
void AddTool( void AddTool(
const std::string& name, // 工具名称,建议唯一且有层次感,如 self.dog.forward const std::string& name, // 工具名称,建议唯一且有层次感,如 self.dog.forward
const std::string& description, // 工具描述,简明说明功能,便于大模型理解 const std::string& description, // 工具描述,简明说明功能,便于大模型理解
const PropertyList& properties, // 输入参数列表(可为空),支持类型:布尔、整数、字符串 const PropertyList& properties, // 输入参数列表(可为空),支持类型:布尔、整数、字符串
std::function<ReturnValue(const PropertyList&)> callback // 工具被调用时的回调实现 std::function<ReturnValue(const PropertyList&)> callback // 工具被调用时的回调实现
); );
``` ```
- name工具唯一标识建议用"模块.功能"命名风格。 - name工具唯一标识建议用"模块.功能"命名风格。
- description自然语言描述便于 AI/用户理解。 - description自然语言描述便于 AI/用户理解。
- properties参数列表支持类型有布尔、整数、字符串可指定范围和默认值。 - properties参数列表支持类型有布尔、整数、字符串可指定范围和默认值。
- callback收到调用请求时的实际执行逻辑返回值可为 bool/int/string。 - callback收到调用请求时的实际执行逻辑返回值可为 bool/int/string。
## 典型注册示例(以 ESP-Hi 为例) ## 典型注册示例(以 ESP-Hi 为例)
```cpp ```cpp
void InitializeTools() { void InitializeTools() {
auto& mcp_server = McpServer::GetInstance(); auto& mcp_server = McpServer::GetInstance();
// 例1无参数控制机器人前进 // 例1无参数控制机器人前进
mcp_server.AddTool("self.dog.forward", "机器人向前移动", PropertyList(), [this](const PropertyList&) -> ReturnValue { mcp_server.AddTool("self.dog.forward", "机器人向前移动", PropertyList(), [this](const PropertyList&) -> ReturnValue {
servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL); servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
return true; return true;
}); });
// 例2带参数设置灯光 RGB 颜色 // 例2带参数设置灯光 RGB 颜色
mcp_server.AddTool("self.light.set_rgb", "设置RGB颜色", PropertyList({ mcp_server.AddTool("self.light.set_rgb", "设置RGB颜色", PropertyList({
Property("r", kPropertyTypeInteger, 0, 255), Property("r", kPropertyTypeInteger, 0, 255),
Property("g", kPropertyTypeInteger, 0, 255), Property("g", kPropertyTypeInteger, 0, 255),
Property("b", kPropertyTypeInteger, 0, 255) Property("b", kPropertyTypeInteger, 0, 255)
}), [this](const PropertyList& properties) -> ReturnValue { }), [this](const PropertyList& properties) -> ReturnValue {
int r = properties["r"].value<int>(); int r = properties["r"].value<int>();
int g = properties["g"].value<int>(); int g = properties["g"].value<int>();
int b = properties["b"].value<int>(); int b = properties["b"].value<int>();
led_on_ = true; led_on_ = true;
SetLedColor(r, g, b); SetLedColor(r, g, b);
return true; return true;
}); });
} }
``` ```
## 常见工具调用 JSON-RPC 示例 ## 常见工具调用 JSON-RPC 示例
### 1. 获取工具列表 ### 1. 获取工具列表
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "tools/list", "method": "tools/list",
"params": { "cursor": "" }, "params": { "cursor": "" },
"id": 1 "id": 1
} }
``` ```
### 2. 控制底盘前进 ### 2. 控制底盘前进
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "tools/call", "method": "tools/call",
"params": { "params": {
"name": "self.chassis.go_forward", "name": "self.chassis.go_forward",
"arguments": {} "arguments": {}
}, },
"id": 2 "id": 2
} }
``` ```
### 3. 切换灯光模式 ### 3. 切换灯光模式
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "tools/call", "method": "tools/call",
"params": { "params": {
"name": "self.chassis.switch_light_mode", "name": "self.chassis.switch_light_mode",
"arguments": { "light_mode": 3 } "arguments": { "light_mode": 3 }
}, },
"id": 3 "id": 3
} }
``` ```
### 4. 摄像头翻转 ### 4. 摄像头翻转
```json ```json
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "tools/call", "method": "tools/call",
"params": { "params": {
"name": "self.camera.set_camera_flipped", "name": "self.camera.set_camera_flipped",
"arguments": {} "arguments": {}
}, },
"id": 4 "id": 4
} }
``` ```
## 备注 ## 备注
- 工具名称、参数及返回值请以设备端 `AddTool` 注册为准。 - 工具名称、参数及返回值请以设备端 `AddTool` 注册为准。
- 推荐所有新项目统一采用 MCP 协议进行物联网控制。 - 推荐所有新项目统一采用 MCP 协议进行物联网控制。
- 详细协议与进阶用法请查阅 [`mcp-protocol.md`](./mcp-protocol.md)。 - 详细协议与进阶用法请查阅 [`mcp-protocol.md`](./mcp-protocol.md)。

View File

@@ -1,393 +1,393 @@
# MQTT + UDP 混合通信协议文档 # MQTT + UDP 混合通信协议文档
基于代码实现整理的 MQTT + UDP 混合通信协议文档,概述设备端与服务器之间如何通过 MQTT 进行控制消息传输,通过 UDP 进行音频数据传输的交互方式。 基于代码实现整理的 MQTT + UDP 混合通信协议文档,概述设备端与服务器之间如何通过 MQTT 进行控制消息传输,通过 UDP 进行音频数据传输的交互方式。
--- ---
## 1. 协议概览 ## 1. 协议概览
本协议采用混合传输方式: 本协议采用混合传输方式:
- **MQTT**用于控制消息、状态同步、JSON 数据交换 - **MQTT**用于控制消息、状态同步、JSON 数据交换
- **UDP**:用于实时音频数据传输,支持加密 - **UDP**:用于实时音频数据传输,支持加密
### 1.1 协议特点 ### 1.1 协议特点
- **双通道设计**:控制与数据分离,确保实时性 - **双通道设计**:控制与数据分离,确保实时性
- **加密传输**UDP 音频数据使用 AES-CTR 加密 - **加密传输**UDP 音频数据使用 AES-CTR 加密
- **序列号保护**:防止数据包重放和乱序 - **序列号保护**:防止数据包重放和乱序
- **自动重连**MQTT 连接断开时自动重连 - **自动重连**MQTT 连接断开时自动重连
--- ---
## 2. 总体流程概览 ## 2. 总体流程概览
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant Device as ESP32 设备 participant Device as ESP32 设备
participant MQTT as MQTT 服务器 participant MQTT as MQTT 服务器
participant UDP as UDP 服务器 participant UDP as UDP 服务器
Note over Device, UDP: 1. 建立 MQTT 连接 Note over Device, UDP: 1. 建立 MQTT 连接
Device->>MQTT: MQTT Connect Device->>MQTT: MQTT Connect
MQTT->>Device: Connected MQTT->>Device: Connected
Note over Device, UDP: 2. 请求音频通道 Note over Device, UDP: 2. 请求音频通道
Device->>MQTT: Hello Message (type: "hello", transport: "udp") Device->>MQTT: Hello Message (type: "hello", transport: "udp")
MQTT->>Device: Hello Response (UDP 连接信息 + 加密密钥) MQTT->>Device: Hello Response (UDP 连接信息 + 加密密钥)
Note over Device, UDP: 3. 建立 UDP 连接 Note over Device, UDP: 3. 建立 UDP 连接
Device->>UDP: UDP Connect Device->>UDP: UDP Connect
UDP->>Device: Connected UDP->>Device: Connected
Note over Device, UDP: 4. 音频数据传输 Note over Device, UDP: 4. 音频数据传输
loop 音频流传输 loop 音频流传输
Device->>UDP: 加密音频数据 (Opus) Device->>UDP: 加密音频数据 (Opus)
UDP->>Device: 加密音频数据 (Opus) UDP->>Device: 加密音频数据 (Opus)
end end
Note over Device, UDP: 5. 控制消息交换 Note over Device, UDP: 5. 控制消息交换
par 控制消息 par 控制消息
Device->>MQTT: Listen/TTS/MCP 消息 Device->>MQTT: Listen/TTS/MCP 消息
MQTT->>Device: STT/TTS/MCP 响应 MQTT->>Device: STT/TTS/MCP 响应
end end
Note over Device, UDP: 6. 关闭连接 Note over Device, UDP: 6. 关闭连接
Device->>MQTT: Goodbye Message Device->>MQTT: Goodbye Message
Device->>UDP: Disconnect Device->>UDP: Disconnect
``` ```
--- ---
## 3. MQTT 控制通道 ## 3. MQTT 控制通道
### 3.1 连接建立 ### 3.1 连接建立
设备通过 MQTT 连接到服务器,连接参数包括: 设备通过 MQTT 连接到服务器,连接参数包括:
- **Endpoint**MQTT 服务器地址和端口 - **Endpoint**MQTT 服务器地址和端口
- **Client ID**:设备唯一标识符 - **Client ID**:设备唯一标识符
- **Username/Password**:认证凭据 - **Username/Password**:认证凭据
- **Keep Alive**心跳间隔默认240秒 - **Keep Alive**心跳间隔默认240秒
### 3.2 Hello 消息交换 ### 3.2 Hello 消息交换
#### 3.2.1 设备端发送 Hello #### 3.2.1 设备端发送 Hello
```json ```json
{ {
"type": "hello", "type": "hello",
"version": 3, "version": 3,
"transport": "udp", "transport": "udp",
"features": { "features": {
"mcp": true "mcp": true
}, },
"audio_params": { "audio_params": {
"format": "opus", "format": "opus",
"sample_rate": 16000, "sample_rate": 16000,
"channels": 1, "channels": 1,
"frame_duration": 60 "frame_duration": 60
} }
} }
``` ```
#### 3.2.2 服务器响应 Hello #### 3.2.2 服务器响应 Hello
```json ```json
{ {
"type": "hello", "type": "hello",
"transport": "udp", "transport": "udp",
"session_id": "xxx", "session_id": "xxx",
"audio_params": { "audio_params": {
"format": "opus", "format": "opus",
"sample_rate": 24000, "sample_rate": 24000,
"channels": 1, "channels": 1,
"frame_duration": 60 "frame_duration": 60
}, },
"udp": { "udp": {
"server": "192.168.1.100", "server": "192.168.1.100",
"port": 8888, "port": 8888,
"key": "0123456789ABCDEF0123456789ABCDEF", "key": "0123456789ABCDEF0123456789ABCDEF",
"nonce": "0123456789ABCDEF0123456789ABCDEF" "nonce": "0123456789ABCDEF0123456789ABCDEF"
} }
} }
``` ```
**字段说明:** **字段说明:**
- `udp.server`UDP 服务器地址 - `udp.server`UDP 服务器地址
- `udp.port`UDP 服务器端口 - `udp.port`UDP 服务器端口
- `udp.key`AES 加密密钥(十六进制字符串) - `udp.key`AES 加密密钥(十六进制字符串)
- `udp.nonce`AES 加密随机数(十六进制字符串) - `udp.nonce`AES 加密随机数(十六进制字符串)
### 3.3 JSON 消息类型 ### 3.3 JSON 消息类型
#### 3.3.1 设备端→服务器 #### 3.3.1 设备端→服务器
1. **Listen 消息** 1. **Listen 消息**
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "listen", "type": "listen",
"state": "start", "state": "start",
"mode": "manual" "mode": "manual"
} }
``` ```
2. **Abort 消息** 2. **Abort 消息**
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "abort", "type": "abort",
"reason": "wake_word_detected" "reason": "wake_word_detected"
} }
``` ```
3. **MCP 消息** 3. **MCP 消息**
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "mcp", "type": "mcp",
"payload": { "payload": {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 1, "id": 1,
"result": {...} "result": {...}
} }
} }
``` ```
4. **Goodbye 消息** 4. **Goodbye 消息**
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "goodbye" "type": "goodbye"
} }
``` ```
#### 3.3.2 服务器→设备端 #### 3.3.2 服务器→设备端
支持的消息类型与 WebSocket 协议一致,包括: 支持的消息类型与 WebSocket 协议一致,包括:
- **STT**:语音识别结果 - **STT**:语音识别结果
- **TTS**:语音合成控制 - **TTS**:语音合成控制
- **LLM**:情感表达控制 - **LLM**:情感表达控制
- **MCP**:物联网控制 - **MCP**:物联网控制
- **System**:系统控制 - **System**:系统控制
- **Custom**:自定义消息(可选) - **Custom**:自定义消息(可选)
--- ---
## 4. UDP 音频通道 ## 4. UDP 音频通道
### 4.1 连接建立 ### 4.1 连接建立
设备收到 MQTT Hello 响应后,使用其中的 UDP 连接信息建立音频通道: 设备收到 MQTT Hello 响应后,使用其中的 UDP 连接信息建立音频通道:
1. 解析 UDP 服务器地址和端口 1. 解析 UDP 服务器地址和端口
2. 解析加密密钥和随机数 2. 解析加密密钥和随机数
3. 初始化 AES-CTR 加密上下文 3. 初始化 AES-CTR 加密上下文
4. 建立 UDP 连接 4. 建立 UDP 连接
### 4.2 音频数据格式 ### 4.2 音频数据格式
#### 4.2.1 加密音频包结构 #### 4.2.1 加密音频包结构
``` ```
|type 1byte|flags 1byte|payload_len 2bytes|ssrc 4bytes|timestamp 4bytes|sequence 4bytes| |type 1byte|flags 1byte|payload_len 2bytes|ssrc 4bytes|timestamp 4bytes|sequence 4bytes|
|payload payload_len bytes| |payload payload_len bytes|
``` ```
**字段说明:** **字段说明:**
- `type`:数据包类型,固定为 0x01 - `type`:数据包类型,固定为 0x01
- `flags`:标志位,当前未使用 - `flags`:标志位,当前未使用
- `payload_len`:负载长度(网络字节序) - `payload_len`:负载长度(网络字节序)
- `ssrc`:同步源标识符 - `ssrc`:同步源标识符
- `timestamp`:时间戳(网络字节序) - `timestamp`:时间戳(网络字节序)
- `sequence`:序列号(网络字节序) - `sequence`:序列号(网络字节序)
- `payload`:加密的 Opus 音频数据 - `payload`:加密的 Opus 音频数据
#### 4.2.2 加密算法 #### 4.2.2 加密算法
使用 **AES-CTR** 模式加密: 使用 **AES-CTR** 模式加密:
- **密钥**128位由服务器提供 - **密钥**128位由服务器提供
- **随机数**128位由服务器提供 - **随机数**128位由服务器提供
- **计数器**:包含时间戳和序列号信息 - **计数器**:包含时间戳和序列号信息
### 4.3 序列号管理 ### 4.3 序列号管理
- **发送端**`local_sequence_` 单调递增 - **发送端**`local_sequence_` 单调递增
- **接收端**`remote_sequence_` 验证连续性 - **接收端**`remote_sequence_` 验证连续性
- **防重放**:拒绝序列号小于期望值的数据包 - **防重放**:拒绝序列号小于期望值的数据包
- **容错处理**:允许轻微的序列号跳跃,记录警告 - **容错处理**:允许轻微的序列号跳跃,记录警告
### 4.4 错误处理 ### 4.4 错误处理
1. **解密失败**:记录错误,丢弃数据包 1. **解密失败**:记录错误,丢弃数据包
2. **序列号异常**:记录警告,但仍处理数据包 2. **序列号异常**:记录警告,但仍处理数据包
3. **数据包格式错误**:记录错误,丢弃数据包 3. **数据包格式错误**:记录错误,丢弃数据包
--- ---
## 5. 状态管理 ## 5. 状态管理
### 5.1 连接状态 ### 5.1 连接状态
```mermaid ```mermaid
stateDiagram stateDiagram
direction TB direction TB
[*] --> Disconnected [*] --> Disconnected
Disconnected --> MqttConnecting: StartMqttClient() Disconnected --> MqttConnecting: StartMqttClient()
MqttConnecting --> MqttConnected: MQTT Connected MqttConnecting --> MqttConnected: MQTT Connected
MqttConnecting --> Disconnected: Connect Failed MqttConnecting --> Disconnected: Connect Failed
MqttConnected --> RequestingChannel: OpenAudioChannel() MqttConnected --> RequestingChannel: OpenAudioChannel()
RequestingChannel --> ChannelOpened: Hello Exchange Success RequestingChannel --> ChannelOpened: Hello Exchange Success
RequestingChannel --> MqttConnected: Hello Timeout/Failed RequestingChannel --> MqttConnected: Hello Timeout/Failed
ChannelOpened --> UdpConnected: UDP Connect Success ChannelOpened --> UdpConnected: UDP Connect Success
UdpConnected --> AudioStreaming: Start Audio Transfer UdpConnected --> AudioStreaming: Start Audio Transfer
AudioStreaming --> UdpConnected: Stop Audio Transfer AudioStreaming --> UdpConnected: Stop Audio Transfer
UdpConnected --> ChannelOpened: UDP Disconnect UdpConnected --> ChannelOpened: UDP Disconnect
ChannelOpened --> MqttConnected: CloseAudioChannel() ChannelOpened --> MqttConnected: CloseAudioChannel()
MqttConnected --> Disconnected: MQTT Disconnect MqttConnected --> Disconnected: MQTT Disconnect
``` ```
### 5.2 状态检查 ### 5.2 状态检查
设备通过以下条件判断音频通道是否可用: 设备通过以下条件判断音频通道是否可用:
```cpp ```cpp
bool IsAudioChannelOpened() const { bool IsAudioChannelOpened() const {
return udp_ != nullptr && !error_occurred_ && !IsTimeout(); return udp_ != nullptr && !error_occurred_ && !IsTimeout();
} }
``` ```
--- ---
## 6. 配置参数 ## 6. 配置参数
### 6.1 MQTT 配置 ### 6.1 MQTT 配置
从设置中读取的配置项: 从设置中读取的配置项:
- `endpoint`MQTT 服务器地址 - `endpoint`MQTT 服务器地址
- `client_id`:客户端标识符 - `client_id`:客户端标识符
- `username`:用户名 - `username`:用户名
- `password`:密码 - `password`:密码
- `keepalive`心跳间隔默认240秒 - `keepalive`心跳间隔默认240秒
- `publish_topic`:发布主题 - `publish_topic`:发布主题
### 6.2 音频参数 ### 6.2 音频参数
- **格式**Opus - **格式**Opus
- **采样率**16000 Hz设备端/ 24000 Hz服务器端 - **采样率**16000 Hz设备端/ 24000 Hz服务器端
- **声道数**1单声道 - **声道数**1单声道
- **帧时长**60ms - **帧时长**60ms
--- ---
## 7. 错误处理与重连 ## 7. 错误处理与重连
### 7.1 MQTT 重连机制 ### 7.1 MQTT 重连机制
- 连接失败时自动重试 - 连接失败时自动重试
- 支持错误上报控制 - 支持错误上报控制
- 断线时触发清理流程 - 断线时触发清理流程
### 7.2 UDP 连接管理 ### 7.2 UDP 连接管理
- 连接失败时不自动重试 - 连接失败时不自动重试
- 依赖 MQTT 通道重新协商 - 依赖 MQTT 通道重新协商
- 支持连接状态查询 - 支持连接状态查询
### 7.3 超时处理 ### 7.3 超时处理
基类 `Protocol` 提供超时检测: 基类 `Protocol` 提供超时检测:
- 默认超时时间120 秒 - 默认超时时间120 秒
- 基于最后接收时间计算 - 基于最后接收时间计算
- 超时时自动标记为不可用 - 超时时自动标记为不可用
--- ---
## 8. 安全考虑 ## 8. 安全考虑
### 8.1 传输加密 ### 8.1 传输加密
- **MQTT**:支持 TLS/SSL 加密端口8883 - **MQTT**:支持 TLS/SSL 加密端口8883
- **UDP**:使用 AES-CTR 加密音频数据 - **UDP**:使用 AES-CTR 加密音频数据
### 8.2 认证机制 ### 8.2 认证机制
- **MQTT**:用户名/密码认证 - **MQTT**:用户名/密码认证
- **UDP**:通过 MQTT 通道分发密钥 - **UDP**:通过 MQTT 通道分发密钥
### 8.3 防重放攻击 ### 8.3 防重放攻击
- 序列号单调递增 - 序列号单调递增
- 拒绝过期数据包 - 拒绝过期数据包
- 时间戳验证 - 时间戳验证
--- ---
## 9. 性能优化 ## 9. 性能优化
### 9.1 并发控制 ### 9.1 并发控制
使用互斥锁保护 UDP 连接: 使用互斥锁保护 UDP 连接:
```cpp ```cpp
std::lock_guard<std::mutex> lock(channel_mutex_); std::lock_guard<std::mutex> lock(channel_mutex_);
``` ```
### 9.2 内存管理 ### 9.2 内存管理
- 动态创建/销毁网络对象 - 动态创建/销毁网络对象
- 智能指针管理音频数据包 - 智能指针管理音频数据包
- 及时释放加密上下文 - 及时释放加密上下文
### 9.3 网络优化 ### 9.3 网络优化
- UDP 连接复用 - UDP 连接复用
- 数据包大小优化 - 数据包大小优化
- 序列号连续性检查 - 序列号连续性检查
--- ---
## 10. 与 WebSocket 协议的比较 ## 10. 与 WebSocket 协议的比较
| 特性 | MQTT + UDP | WebSocket | | 特性 | MQTT + UDP | WebSocket |
|------|------------|-----------| |------|------------|-----------|
| 控制通道 | MQTT | WebSocket | | 控制通道 | MQTT | WebSocket |
| 音频通道 | UDP (加密) | WebSocket (二进制) | | 音频通道 | UDP (加密) | WebSocket (二进制) |
| 实时性 | 高 (UDP) | 中等 | | 实时性 | 高 (UDP) | 中等 |
| 可靠性 | 中等 | 高 | | 可靠性 | 中等 | 高 |
| 复杂度 | 高 | 低 | | 复杂度 | 高 | 低 |
| 加密 | AES-CTR | TLS | | 加密 | AES-CTR | TLS |
| 防火墙友好度 | 低 | 高 | | 防火墙友好度 | 低 | 高 |
--- ---
## 11. 部署建议 ## 11. 部署建议
### 11.1 网络环境 ### 11.1 网络环境
- 确保 UDP 端口可达 - 确保 UDP 端口可达
- 配置防火墙规则 - 配置防火墙规则
- 考虑 NAT 穿透 - 考虑 NAT 穿透
### 11.2 服务器配置 ### 11.2 服务器配置
- MQTT Broker 配置 - MQTT Broker 配置
- UDP 服务器部署 - UDP 服务器部署
- 密钥管理系统 - 密钥管理系统
### 11.3 监控指标 ### 11.3 监控指标
- 连接成功率 - 连接成功率
- 音频传输延迟 - 音频传输延迟
- 数据包丢失率 - 数据包丢失率
- 解密失败率 - 解密失败率
--- ---
## 12. 总结 ## 12. 总结
MQTT + UDP 混合协议通过以下设计实现高效的音视频通信: MQTT + UDP 混合协议通过以下设计实现高效的音视频通信:
- **分离式架构**:控制与数据通道分离,各司其职 - **分离式架构**:控制与数据通道分离,各司其职
- **加密保护**AES-CTR 确保音频数据安全传输 - **加密保护**AES-CTR 确保音频数据安全传输
- **序列化管理**:防止重放攻击和数据乱序 - **序列化管理**:防止重放攻击和数据乱序
- **自动恢复**:支持连接断开后的自动重连 - **自动恢复**:支持连接断开后的自动重连
- **性能优化**UDP 传输保证音频数据的实时性 - **性能优化**UDP 传输保证音频数据的实时性
该协议适用于对实时性要求较高的语音交互场景,但需要在网络复杂度和传输性能之间做出权衡。 该协议适用于对实时性要求较高的语音交互场景,但需要在网络复杂度和传输性能之间做出权衡。

0
docs/v0/atoms3r-echo-base.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

0
docs/v1/movecall-cuican-esp32s3.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -1,495 +1,495 @@
以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述设备端与服务器之间如何通过 WebSocket 进行交互。 以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述设备端与服务器之间如何通过 WebSocket 进行交互。
该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。 该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。
--- ---
## 1. 总体流程概览 ## 1. 总体流程概览
1. **设备端初始化** 1. **设备端初始化**
- 设备上电、初始化 `Application` - 设备上电、初始化 `Application`
- 初始化音频编解码器、显示屏、LED 等 - 初始化音频编解码器、显示屏、LED 等
- 连接网络 - 连接网络
- 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol` - 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol`
- 进入主循环等待事件(音频输入、音频输出、调度任务等)。 - 进入主循环等待事件(音频输入、音频输出、调度任务等)。
2. **建立 WebSocket 连接** 2. **建立 WebSocket 连接**
- 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()` - 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()`
- 根据配置获取 WebSocket URL - 根据配置获取 WebSocket URL
- 设置若干请求头(`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id` - 设置若干请求头(`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`
- 调用 `Connect()` 与服务器建立 WebSocket 连接 - 调用 `Connect()` 与服务器建立 WebSocket 连接
3. **设备端发送 "hello" 消息** 3. **设备端发送 "hello" 消息**
- 连接成功后,设备会发送一条 JSON 消息,示例结构如下: - 连接成功后,设备会发送一条 JSON 消息,示例结构如下:
```json ```json
{ {
"type": "hello", "type": "hello",
"version": 1, "version": 1,
"features": { "features": {
"mcp": true "mcp": true
}, },
"transport": "websocket", "transport": "websocket",
"audio_params": { "audio_params": {
"format": "opus", "format": "opus",
"sample_rate": 16000, "sample_rate": 16000,
"channels": 1, "channels": 1,
"frame_duration": 60 "frame_duration": 60
} }
} }
``` ```
- 其中 `features` 字段为可选,内容根据设备编译配置自动生成。例如:`"mcp": true` 表示支持 MCP 协议。 - 其中 `features` 字段为可选,内容根据设备编译配置自动生成。例如:`"mcp": true` 表示支持 MCP 协议。
- `frame_duration` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms - `frame_duration` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms
4. **服务器回复 "hello"** 4. **服务器回复 "hello"**
- 设备等待服务器返回一条包含 `"type": "hello"` 的 JSON 消息,并检查 `"transport": "websocket"` 是否匹配。 - 设备等待服务器返回一条包含 `"type": "hello"` 的 JSON 消息,并检查 `"transport": "websocket"` 是否匹配。
- 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。 - 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。
- 示例: - 示例:
```json ```json
{ {
"type": "hello", "type": "hello",
"transport": "websocket", "transport": "websocket",
"session_id": "xxx", "session_id": "xxx",
"audio_params": { "audio_params": {
"format": "opus", "format": "opus",
"sample_rate": 24000, "sample_rate": 24000,
"channels": 1, "channels": 1,
"frame_duration": 60 "frame_duration": 60
} }
} }
``` ```
- 如果匹配,则认为服务器已就绪,标记音频通道打开成功。 - 如果匹配,则认为服务器已就绪,标记音频通道打开成功。
- 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。 - 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。
5. **后续消息交互** 5. **后续消息交互**
- 设备端和服务器端之间可发送两种主要类型的数据: - 设备端和服务器端之间可发送两种主要类型的数据:
1. **二进制音频数据**Opus 编码) 1. **二进制音频数据**Opus 编码)
2. **文本 JSON 消息**用于传输聊天状态、TTS/STT 事件、MCP 协议消息等) 2. **文本 JSON 消息**用于传输聊天状态、TTS/STT 事件、MCP 协议消息等)
- 在代码里,接收回调主要分为: - 在代码里,接收回调主要分为:
- `OnData(...)`: - `OnData(...)`:
- 当 `binary` 为 `true` 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。 - 当 `binary` 为 `true` 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。
- 当 `binary` 为 `false` 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理如聊天、TTS、MCP 协议消息等)。 - 当 `binary` 为 `false` 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理如聊天、TTS、MCP 协议消息等)。
- 当服务器或网络出现断连,回调 `OnDisconnected()` 被触发: - 当服务器或网络出现断连,回调 `OnDisconnected()` 被触发:
- 设备会调用 `on_audio_channel_closed_()`,并最终回到空闲状态。 - 设备会调用 `on_audio_channel_closed_()`,并最终回到空闲状态。
6. **关闭 WebSocket 连接** 6. **关闭 WebSocket 连接**
- 设备在需要结束语音会话时,会调用 `CloseAudioChannel()` 主动断开连接,并回到空闲状态。 - 设备在需要结束语音会话时,会调用 `CloseAudioChannel()` 主动断开连接,并回到空闲状态。
- 或者如果服务器端主动断开,也会引发同样的回调流程。 - 或者如果服务器端主动断开,也会引发同样的回调流程。
--- ---
## 2. 通用请求头 ## 2. 通用请求头
在建立 WebSocket 连接时,代码示例中设置了以下请求头: 在建立 WebSocket 连接时,代码示例中设置了以下请求头:
- `Authorization`: 用于存放访问令牌,形如 `"Bearer <token>"` - `Authorization`: 用于存放访问令牌,形如 `"Bearer <token>"`
- `Protocol-Version`: 协议版本号,与 hello 消息体内的 `version` 字段保持一致 - `Protocol-Version`: 协议版本号,与 hello 消息体内的 `version` 字段保持一致
- `Device-Id`: 设备物理网卡 MAC 地址 - `Device-Id`: 设备物理网卡 MAC 地址
- `Client-Id`: 软件生成的 UUID擦除 NVS 或重新烧录完整固件会重置) - `Client-Id`: 软件生成的 UUID擦除 NVS 或重新烧录完整固件会重置)
这些头会随着 WebSocket 握手一起发送到服务器,服务器可根据需求进行校验、认证等。 这些头会随着 WebSocket 握手一起发送到服务器,服务器可根据需求进行校验、认证等。
--- ---
## 3. 二进制协议版本 ## 3. 二进制协议版本
设备支持多种二进制协议版本,通过配置中的 `version` 字段指定: 设备支持多种二进制协议版本,通过配置中的 `version` 字段指定:
### 3.1 版本1默认 ### 3.1 版本1默认
直接发送 Opus 音频数据无额外元数据。Websocket 协议会区分 text 与 binary。 直接发送 Opus 音频数据无额外元数据。Websocket 协议会区分 text 与 binary。
### 3.2 版本2 ### 3.2 版本2
使用 `BinaryProtocol2` 结构: 使用 `BinaryProtocol2` 结构:
```c ```c
struct BinaryProtocol2 { struct BinaryProtocol2 {
uint16_t version; // 协议版本 uint16_t version; // 协议版本
uint16_t type; // 消息类型 (0: OPUS, 1: JSON) uint16_t type; // 消息类型 (0: OPUS, 1: JSON)
uint32_t reserved; // 保留字段 uint32_t reserved; // 保留字段
uint32_t timestamp; // 时间戳毫秒用于服务器端AEC uint32_t timestamp; // 时间戳毫秒用于服务器端AEC
uint32_t payload_size; // 负载大小(字节) uint32_t payload_size; // 负载大小(字节)
uint8_t payload[]; // 负载数据 uint8_t payload[]; // 负载数据
} __attribute__((packed)); } __attribute__((packed));
``` ```
### 3.3 版本3 ### 3.3 版本3
使用 `BinaryProtocol3` 结构: 使用 `BinaryProtocol3` 结构:
```c ```c
struct BinaryProtocol3 { struct BinaryProtocol3 {
uint8_t type; // 消息类型 uint8_t type; // 消息类型
uint8_t reserved; // 保留字段 uint8_t reserved; // 保留字段
uint16_t payload_size; // 负载大小 uint16_t payload_size; // 负载大小
uint8_t payload[]; // 负载数据 uint8_t payload[]; // 负载数据
} __attribute__((packed)); } __attribute__((packed));
``` ```
--- ---
## 4. JSON 消息结构 ## 4. JSON 消息结构
WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。 WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。
### 4.1 设备端→服务器 ### 4.1 设备端→服务器
1. **Hello** 1. **Hello**
- 连接成功后,由设备端发送,告知服务器基本参数。 - 连接成功后,由设备端发送,告知服务器基本参数。
- 例: - 例:
```json ```json
{ {
"type": "hello", "type": "hello",
"version": 1, "version": 1,
"features": { "features": {
"mcp": true "mcp": true
}, },
"transport": "websocket", "transport": "websocket",
"audio_params": { "audio_params": {
"format": "opus", "format": "opus",
"sample_rate": 16000, "sample_rate": 16000,
"channels": 1, "channels": 1,
"frame_duration": 60 "frame_duration": 60
} }
} }
``` ```
2. **Listen** 2. **Listen**
- 表示设备端开始或停止录音监听。 - 表示设备端开始或停止录音监听。
- 常见字段: - 常见字段:
- `"session_id"`:会话标识 - `"session_id"`:会话标识
- `"type": "listen"` - `"type": "listen"`
- `"state"``"start"`, `"stop"`, `"detect"`(唤醒检测已触发) - `"state"``"start"`, `"stop"`, `"detect"`(唤醒检测已触发)
- `"mode"``"auto"`, `"manual"` 或 `"realtime"`,表示识别模式。 - `"mode"``"auto"`, `"manual"` 或 `"realtime"`,表示识别模式。
- 例:开始监听 - 例:开始监听
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "listen", "type": "listen",
"state": "start", "state": "start",
"mode": "manual" "mode": "manual"
} }
``` ```
3. **Abort** 3. **Abort**
- 终止当前说话TTS 播放)或语音通道。 - 终止当前说话TTS 播放)或语音通道。
- 例: - 例:
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "abort", "type": "abort",
"reason": "wake_word_detected" "reason": "wake_word_detected"
} }
``` ```
- `reason` 值可为 `"wake_word_detected"` 或其他。 - `reason` 值可为 `"wake_word_detected"` 或其他。
4. **Wake Word Detected** 4. **Wake Word Detected**
- 用于设备端向服务器告知检测到唤醒词。 - 用于设备端向服务器告知检测到唤醒词。
- 在发送该消息之前,可提前发送唤醒词的 Opus 音频数据,用于服务器进行声纹检测。 - 在发送该消息之前,可提前发送唤醒词的 Opus 音频数据,用于服务器进行声纹检测。
- 例: - 例:
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "listen", "type": "listen",
"state": "detect", "state": "detect",
"text": "你好小明" "text": "你好小明"
} }
``` ```
5. **MCP** 5. **MCP**
- 推荐用于物联网控制的新一代协议。所有设备能力发现、工具调用等均通过 type: "mcp" 的消息进行payload 内部为标准 JSON-RPC 2.0(详见 [MCP 协议文档](./mcp-protocol.md))。 - 推荐用于物联网控制的新一代协议。所有设备能力发现、工具调用等均通过 type: "mcp" 的消息进行payload 内部为标准 JSON-RPC 2.0(详见 [MCP 协议文档](./mcp-protocol.md))。
- **设备端到服务器发送 result 的例子:** - **设备端到服务器发送 result 的例子:**
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "mcp", "type": "mcp",
"payload": { "payload": {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 1, "id": 1,
"result": { "result": {
"content": [ "content": [
{ "type": "text", "text": "true" } { "type": "text", "text": "true" }
], ],
"isError": false "isError": false
} }
} }
} }
``` ```
--- ---
### 4.2 服务器→设备端 ### 4.2 服务器→设备端
1. **Hello** 1. **Hello**
- 服务器端返回的握手确认消息。 - 服务器端返回的握手确认消息。
- 必须包含 `"type": "hello"` 和 `"transport": "websocket"`。 - 必须包含 `"type": "hello"` 和 `"transport": "websocket"`。
- 可能会带有 `audio_params`,表示服务器期望的音频参数,或与设备端对齐的配置。 - 可能会带有 `audio_params`,表示服务器期望的音频参数,或与设备端对齐的配置。
- 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。 - 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。
- 成功接收后设备端会设置事件标志,表示 WebSocket 通道就绪。 - 成功接收后设备端会设置事件标志,表示 WebSocket 通道就绪。
2. **STT** 2. **STT**
- `{"session_id": "xxx", "type": "stt", "text": "..."}` - `{"session_id": "xxx", "type": "stt", "text": "..."}`
- 表示服务器端识别到了用户语音。(例如语音转文本结果) - 表示服务器端识别到了用户语音。(例如语音转文本结果)
- 设备可能将此文本显示到屏幕上,后续再进入回答等流程。 - 设备可能将此文本显示到屏幕上,后续再进入回答等流程。
3. **LLM** 3. **LLM**
- `{"session_id": "xxx", "type": "llm", "emotion": "happy", "text": "😀"}` - `{"session_id": "xxx", "type": "llm", "emotion": "happy", "text": "😀"}`
- 服务器指示设备调整表情动画 / UI 表达。 - 服务器指示设备调整表情动画 / UI 表达。
4. **TTS** 4. **TTS**
- `{"session_id": "xxx", "type": "tts", "state": "start"}`:服务器准备下发 TTS 音频,设备端进入 "speaking" 播放状态。 - `{"session_id": "xxx", "type": "tts", "state": "start"}`:服务器准备下发 TTS 音频,设备端进入 "speaking" 播放状态。
- `{"session_id": "xxx", "type": "tts", "state": "stop"}`:表示本次 TTS 结束。 - `{"session_id": "xxx", "type": "tts", "state": "stop"}`:表示本次 TTS 结束。
- `{"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}` - `{"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}`
- 让设备在界面上显示当前要播放或朗读的文本片段(例如用于显示给用户)。 - 让设备在界面上显示当前要播放或朗读的文本片段(例如用于显示给用户)。
5. **MCP** 5. **MCP**
- 服务器通过 type: "mcp" 的消息下发物联网相关的控制指令或返回调用结果payload 结构同上。 - 服务器通过 type: "mcp" 的消息下发物联网相关的控制指令或返回调用结果payload 结构同上。
- **服务器到设备端发送 tools/call 的例子:** - **服务器到设备端发送 tools/call 的例子:**
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "mcp", "type": "mcp",
"payload": { "payload": {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "tools/call", "method": "tools/call",
"params": { "params": {
"name": "self.light.set_rgb", "name": "self.light.set_rgb",
"arguments": { "r": 255, "g": 0, "b": 0 } "arguments": { "r": 255, "g": 0, "b": 0 }
}, },
"id": 1 "id": 1
} }
} }
``` ```
6. **System** 6. **System**
- 系统控制命令,常用于远程升级更新。 - 系统控制命令,常用于远程升级更新。
- 例: - 例:
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "system", "type": "system",
"command": "reboot" "command": "reboot"
} }
``` ```
- 支持的命令: - 支持的命令:
- `"reboot"`:重启设备 - `"reboot"`:重启设备
7. **Custom**(可选) 7. **Custom**(可选)
- 自定义消息,当 `CONFIG_RECEIVE_CUSTOM_MESSAGE` 启用时支持。 - 自定义消息,当 `CONFIG_RECEIVE_CUSTOM_MESSAGE` 启用时支持。
- 例: - 例:
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "custom", "type": "custom",
"payload": { "payload": {
"message": "自定义内容" "message": "自定义内容"
} }
} }
``` ```
8. **音频数据:二进制帧** 8. **音频数据:二进制帧**
- 当服务器发送音频二进制帧Opus 编码)时,设备端解码并播放。 - 当服务器发送音频二进制帧Opus 编码)时,设备端解码并播放。
- 若设备端正在处于 "listening" (录音)状态,收到的音频帧会被忽略或清空以防冲突。 - 若设备端正在处于 "listening" (录音)状态,收到的音频帧会被忽略或清空以防冲突。
--- ---
## 5. 音频编解码 ## 5. 音频编解码
1. **设备端发送录音数据** 1. **设备端发送录音数据**
- 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。 - 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。
- 根据协议版本,可能直接发送 Opus 数据版本1或使用带元数据的二进制协议版本2/3 - 根据协议版本,可能直接发送 Opus 数据版本1或使用带元数据的二进制协议版本2/3
2. **设备端播放收到的音频** 2. **设备端播放收到的音频**
- 收到服务器的二进制帧时,同样认定是 Opus 数据。 - 收到服务器的二进制帧时,同样认定是 Opus 数据。
- 设备端会进行解码,然后交由音频输出接口播放。 - 设备端会进行解码,然后交由音频输出接口播放。
- 如果服务器的音频采样率与设备不一致,会在解码后再进行重采样。 - 如果服务器的音频采样率与设备不一致,会在解码后再进行重采样。
--- ---
## 6. 常见状态流转 ## 6. 常见状态流转
以下为常见设备端关键状态流转,与 WebSocket 消息对应: 以下为常见设备端关键状态流转,与 WebSocket 消息对应:
1. **Idle** → **Connecting** 1. **Idle** → **Connecting**
- 用户触发或唤醒后,设备调用 `OpenAudioChannel()` → 建立 WebSocket 连接 → 发送 `"type":"hello"`。 - 用户触发或唤醒后,设备调用 `OpenAudioChannel()` → 建立 WebSocket 连接 → 发送 `"type":"hello"`。
2. **Connecting** → **Listening** 2. **Connecting** → **Listening**
- 成功建立连接后,若继续执行 `SendStartListening(...)`,则进入录音状态。此时设备会持续编码麦克风数据并发送到服务器。 - 成功建立连接后,若继续执行 `SendStartListening(...)`,则进入录音状态。此时设备会持续编码麦克风数据并发送到服务器。
3. **Listening** → **Speaking** 3. **Listening** → **Speaking**
- 收到服务器 TTS Start 消息 (`{"type":"tts","state":"start"}`) → 停止录音并播放接收到的音频。 - 收到服务器 TTS Start 消息 (`{"type":"tts","state":"start"}`) → 停止录音并播放接收到的音频。
4. **Speaking** → **Idle** 4. **Speaking** → **Idle**
- 服务器 TTS Stop (`{"type":"tts","state":"stop"}`) → 音频播放结束。若未继续进入自动监听,则返回 Idle如果配置了自动循环则再度进入 Listening。 - 服务器 TTS Stop (`{"type":"tts","state":"stop"}`) → 音频播放结束。若未继续进入自动监听,则返回 Idle如果配置了自动循环则再度进入 Listening。
5. **Listening** / **Speaking** → **Idle**(遇到异常或主动中断) 5. **Listening** / **Speaking** → **Idle**(遇到异常或主动中断)
- 调用 `SendAbortSpeaking(...)` 或 `CloseAudioChannel()` → 中断会话 → 关闭 WebSocket → 状态回到 Idle。 - 调用 `SendAbortSpeaking(...)` 或 `CloseAudioChannel()` → 中断会话 → 关闭 WebSocket → 状态回到 Idle。
### 自动模式状态流转图 ### 自动模式状态流转图
```mermaid ```mermaid
stateDiagram stateDiagram
direction TB direction TB
[*] --> kDeviceStateUnknown [*] --> kDeviceStateUnknown
kDeviceStateUnknown --> kDeviceStateStarting:初始化 kDeviceStateUnknown --> kDeviceStateStarting:初始化
kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
kDeviceStateStarting --> kDeviceStateActivating:激活设备 kDeviceStateStarting --> kDeviceStateActivating:激活设备
kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本 kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本
kDeviceStateActivating --> kDeviceStateIdle:激活完成 kDeviceStateActivating --> kDeviceStateIdle:激活完成
kDeviceStateIdle --> kDeviceStateConnecting:开始连接 kDeviceStateIdle --> kDeviceStateConnecting:开始连接
kDeviceStateConnecting --> kDeviceStateIdle:连接失败 kDeviceStateConnecting --> kDeviceStateIdle:连接失败
kDeviceStateConnecting --> kDeviceStateListening:连接成功 kDeviceStateConnecting --> kDeviceStateListening:连接成功
kDeviceStateListening --> kDeviceStateSpeaking:开始说话 kDeviceStateListening --> kDeviceStateSpeaking:开始说话
kDeviceStateSpeaking --> kDeviceStateListening:结束说话 kDeviceStateSpeaking --> kDeviceStateListening:结束说话
kDeviceStateListening --> kDeviceStateIdle:手动终止 kDeviceStateListening --> kDeviceStateIdle:手动终止
kDeviceStateSpeaking --> kDeviceStateIdle:自动终止 kDeviceStateSpeaking --> kDeviceStateIdle:自动终止
``` ```
### 手动模式状态流转图 ### 手动模式状态流转图
```mermaid ```mermaid
stateDiagram stateDiagram
direction TB direction TB
[*] --> kDeviceStateUnknown [*] --> kDeviceStateUnknown
kDeviceStateUnknown --> kDeviceStateStarting:初始化 kDeviceStateUnknown --> kDeviceStateStarting:初始化
kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
kDeviceStateStarting --> kDeviceStateActivating:激活设备 kDeviceStateStarting --> kDeviceStateActivating:激活设备
kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本 kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本
kDeviceStateActivating --> kDeviceStateIdle:激活完成 kDeviceStateActivating --> kDeviceStateIdle:激活完成
kDeviceStateIdle --> kDeviceStateConnecting:开始连接 kDeviceStateIdle --> kDeviceStateConnecting:开始连接
kDeviceStateConnecting --> kDeviceStateIdle:连接失败 kDeviceStateConnecting --> kDeviceStateIdle:连接失败
kDeviceStateConnecting --> kDeviceStateListening:连接成功 kDeviceStateConnecting --> kDeviceStateListening:连接成功
kDeviceStateIdle --> kDeviceStateListening:开始监听 kDeviceStateIdle --> kDeviceStateListening:开始监听
kDeviceStateListening --> kDeviceStateIdle:停止监听 kDeviceStateListening --> kDeviceStateIdle:停止监听
kDeviceStateIdle --> kDeviceStateSpeaking:开始说话 kDeviceStateIdle --> kDeviceStateSpeaking:开始说话
kDeviceStateSpeaking --> kDeviceStateIdle:结束说话 kDeviceStateSpeaking --> kDeviceStateIdle:结束说话
``` ```
--- ---
## 7. 错误处理 ## 7. 错误处理
1. **连接失败** 1. **连接失败**
- 如果 `Connect(url)` 返回失败或在等待服务器 "hello" 消息时超时,触发 `on_network_error_()` 回调。设备会提示"无法连接到服务"或类似错误信息。 - 如果 `Connect(url)` 返回失败或在等待服务器 "hello" 消息时超时,触发 `on_network_error_()` 回调。设备会提示"无法连接到服务"或类似错误信息。
2. **服务器断开** 2. **服务器断开**
- 如果 WebSocket 异常断开,回调 `OnDisconnected()` - 如果 WebSocket 异常断开,回调 `OnDisconnected()`
- 设备回调 `on_audio_channel_closed_()` - 设备回调 `on_audio_channel_closed_()`
- 切换到 Idle 或其他重试逻辑。 - 切换到 Idle 或其他重试逻辑。
--- ---
## 8. 其它注意事项 ## 8. 其它注意事项
1. **鉴权** 1. **鉴权**
- 设备通过设置 `Authorization: Bearer <token>` 提供鉴权,服务器端需验证是否有效。 - 设备通过设置 `Authorization: Bearer <token>` 提供鉴权,服务器端需验证是否有效。
- 如果令牌过期或无效,服务器可拒绝握手或在后续断开。 - 如果令牌过期或无效,服务器可拒绝握手或在后续断开。
2. **会话控制** 2. **会话控制**
- 代码中部分消息包含 `session_id`,用于区分独立的对话或操作。服务端可根据需要对不同会话做分离处理。 - 代码中部分消息包含 `session_id`,用于区分独立的对话或操作。服务端可根据需要对不同会话做分离处理。
3. **音频负载** 3. **音频负载**
- 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。为了获得更好的音乐播放效果服务器下行音频可能使用 24000 采样率。 - 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。为了获得更好的音乐播放效果服务器下行音频可能使用 24000 采样率。
4. **协议版本配置** 4. **协议版本配置**
- 通过设置中的 `version` 字段配置二进制协议版本1、2 或 3 - 通过设置中的 `version` 字段配置二进制协议版本1、2 或 3
- 版本1直接发送 Opus 数据 - 版本1直接发送 Opus 数据
- 版本2使用带时间戳的二进制协议适用于服务器端 AEC - 版本2使用带时间戳的二进制协议适用于服务器端 AEC
- 版本3使用简化的二进制协议 - 版本3使用简化的二进制协议
5. **物联网控制推荐 MCP 协议** 5. **物联网控制推荐 MCP 协议**
- 设备与服务器之间的物联网能力发现、状态同步、控制指令等,建议全部通过 MCP 协议type: "mcp")实现。原有的 type: "iot" 方案已废弃。 - 设备与服务器之间的物联网能力发现、状态同步、控制指令等,建议全部通过 MCP 协议type: "mcp")实现。原有的 type: "iot" 方案已废弃。
- MCP 协议可在 WebSocket、MQTT 等多种底层协议上传输,具备更好的扩展性和标准化能力。 - MCP 协议可在 WebSocket、MQTT 等多种底层协议上传输,具备更好的扩展性和标准化能力。
- 详细用法请参考 [MCP 协议文档](./mcp-protocol.md) 及 [MCP 物联网控制用法](./mcp-usage.md)。 - 详细用法请参考 [MCP 协议文档](./mcp-protocol.md) 及 [MCP 物联网控制用法](./mcp-usage.md)。
6. **错误或异常 JSON** 6. **错误或异常 JSON**
- 当 JSON 中缺少必要字段,例如 `{"type": ...}`,设备端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。 - 当 JSON 中缺少必要字段,例如 `{"type": ...}`,设备端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。
--- ---
## 9. 消息示例 ## 9. 消息示例
下面给出一个典型的双向消息示例(流程简化示意): 下面给出一个典型的双向消息示例(流程简化示意):
1. **设备端 → 服务器**(握手) 1. **设备端 → 服务器**(握手)
```json ```json
{ {
"type": "hello", "type": "hello",
"version": 1, "version": 1,
"features": { "features": {
"mcp": true "mcp": true
}, },
"transport": "websocket", "transport": "websocket",
"audio_params": { "audio_params": {
"format": "opus", "format": "opus",
"sample_rate": 16000, "sample_rate": 16000,
"channels": 1, "channels": 1,
"frame_duration": 60 "frame_duration": 60
} }
} }
``` ```
2. **服务器 → 设备端**(握手应答) 2. **服务器 → 设备端**(握手应答)
```json ```json
{ {
"type": "hello", "type": "hello",
"transport": "websocket", "transport": "websocket",
"session_id": "xxx", "session_id": "xxx",
"audio_params": { "audio_params": {
"format": "opus", "format": "opus",
"sample_rate": 16000 "sample_rate": 16000
} }
} }
``` ```
3. **设备端 → 服务器**(开始监听) 3. **设备端 → 服务器**(开始监听)
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "listen", "type": "listen",
"state": "start", "state": "start",
"mode": "auto" "mode": "auto"
} }
``` ```
同时设备端开始发送二进制帧Opus 数据)。 同时设备端开始发送二进制帧Opus 数据)。
4. **服务器 → 设备端**ASR 结果) 4. **服务器 → 设备端**ASR 结果)
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "stt", "type": "stt",
"text": "用户说的话" "text": "用户说的话"
} }
``` ```
5. **服务器 → 设备端**TTS开始 5. **服务器 → 设备端**TTS开始
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "tts", "type": "tts",
"state": "start" "state": "start"
} }
``` ```
接着服务器发送二进制音频帧给设备端播放。 接着服务器发送二进制音频帧给设备端播放。
6. **服务器 → 设备端**TTS结束 6. **服务器 → 设备端**TTS结束
```json ```json
{ {
"session_id": "xxx", "session_id": "xxx",
"type": "tts", "type": "tts",
"state": "stop" "state": "stop"
} }
``` ```
设备端停止播放音频,若无更多指令,则回到空闲状态。 设备端停止播放音频,若无更多指令,则回到空闲状态。
--- ---
## 10. 总结 ## 10. 总结
本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、MCP 指令下发等。其核心特征: 本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、MCP 指令下发等。其核心特征:
- **握手阶段**:发送 `"type":"hello"`,等待服务器返回。 - **握手阶段**:发送 `"type":"hello"`,等待服务器返回。
- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流,支持多种协议版本。 - **音频通道**:采用 Opus 编码的二进制帧双向传输语音流,支持多种协议版本。
- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、MCP、WakeWord、System、Custom 等。 - **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、MCP、WakeWord、System、Custom 等。
- **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。 - **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。
服务器与设备端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。 服务器与设备端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。

BIN
esptool.exe Executable file

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

671
main/alarm_manager.cc Normal file
View File

@@ -0,0 +1,671 @@
#include "alarm_manager.h"
#include <esp_log.h>
#include <esp_timer.h>
#include <cJSON.h>
#include <time.h>
#include <sys/time.h>
#include <sstream>
#include <iomanip>
#include <algorithm>
#define TAG "AlarmManager"
#define ALARM_SETTINGS_NAMESPACE "alarms"
AlarmManager::AlarmManager()
: initialized_(false), next_alarm_id_(1),
default_snooze_minutes_(5), default_max_snooze_count_(3) {
}
AlarmManager::~AlarmManager() {
Cleanup();
}
void AlarmManager::Initialize() {
if (initialized_) {
return;
}
ESP_LOGI(TAG, "Initializing Alarm Manager");
// 初始化设置存储
settings_ = std::make_unique<Settings>(ALARM_SETTINGS_NAMESPACE, true);
// 从存储中加载闹钟
LoadAlarmsFromStorage();
// 获取下一个闹钟ID
next_alarm_id_ = settings_->GetInt("next_id", 1);
initialized_ = true;
ESP_LOGI(TAG, "Alarm Manager initialized with %d alarms", alarms_.size());
}
void AlarmManager::Cleanup() {
if (!initialized_) {
return;
}
ESP_LOGI(TAG, "Cleaning up Alarm Manager");
// 停止所有活动的闹钟
StopAllActiveAlarms();
// 保存数据
SaveAlarmsToStorage();
// 清理资源
{
std::lock_guard<std::mutex> lock(alarms_mutex_);
alarms_.clear();
}
settings_.reset();
initialized_ = false;
}
int AlarmManager::AddAlarm(int hour, int minute, AlarmRepeatMode repeat_mode,
const std::string& label, const std::string& music_name) {
if (!initialized_) {
ESP_LOGE(TAG, "AlarmManager not initialized");
return -1;
}
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
ESP_LOGE(TAG, "Invalid time: %02d:%02d", hour, minute);
return -1;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
auto alarm = std::make_unique<AlarmItem>();
alarm->id = GetNextAlarmId();
alarm->hour = hour;
alarm->minute = minute;
alarm->repeat_mode = repeat_mode;
alarm->label = label;
alarm->music_name = music_name;
alarm->status = kAlarmEnabled;
alarm->snooze_minutes = default_snooze_minutes_;
alarm->max_snooze_count = default_max_snooze_count_;
// 设置星期掩码
switch (repeat_mode) {
case kAlarmDaily:
alarm->weekdays_mask = 0b1111111; // 每天
break;
case kAlarmWeekdays:
alarm->weekdays_mask = 0b0111110; // 周一到周五
break;
case kAlarmWeekends:
alarm->weekdays_mask = 0b1000001; // 周六周日
break;
default:
alarm->weekdays_mask = 0; // 一次性或自定义
break;
}
int alarm_id = alarm->id;
alarms_.push_back(std::move(alarm));
// 保存到存储
SaveAlarmToStorage(*alarms_.back());
settings_->SetInt("next_id", next_alarm_id_);
ESP_LOGI(TAG, "Added alarm %d: %02d:%02d, repeat=%d, label='%s', music='%s'",
alarm_id, hour, minute, repeat_mode, label.c_str(), music_name.c_str());
return alarm_id;
}
bool AlarmManager::RemoveAlarm(int alarm_id) {
if (!initialized_) {
return false;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
auto it = std::find_if(alarms_.begin(), alarms_.end(),
[alarm_id](const auto& alarm) { return alarm->id == alarm_id; });
if (it != alarms_.end()) {
ESP_LOGI(TAG, "Removing alarm %d", alarm_id);
alarms_.erase(it);
RemoveAlarmFromStorage(alarm_id);
return true;
}
ESP_LOGW(TAG, "Alarm %d not found for removal", alarm_id);
return false;
}
bool AlarmManager::EnableAlarm(int alarm_id, bool enabled) {
if (!initialized_) {
return false;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
auto it = std::find_if(alarms_.begin(), alarms_.end(),
[alarm_id](const auto& alarm) { return alarm->id == alarm_id; });
if (it != alarms_.end()) {
(*it)->status = enabled ? kAlarmEnabled : kAlarmDisabled;
SaveAlarmToStorage(**it);
ESP_LOGI(TAG, "Alarm %d %s", alarm_id, enabled ? "enabled" : "disabled");
return true;
}
return false;
}
bool AlarmManager::ModifyAlarm(int alarm_id, int hour, int minute, AlarmRepeatMode repeat_mode,
const std::string& label, const std::string& music_name) {
if (!initialized_) {
return false;
}
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
ESP_LOGE(TAG, "Invalid time: %02d:%02d", hour, minute);
return false;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
auto it = std::find_if(alarms_.begin(), alarms_.end(),
[alarm_id](const auto& alarm) { return alarm->id == alarm_id; });
if (it != alarms_.end()) {
(*it)->hour = hour;
(*it)->minute = minute;
(*it)->repeat_mode = repeat_mode;
(*it)->label = label;
(*it)->music_name = music_name;
// 重新设置星期掩码
switch (repeat_mode) {
case kAlarmDaily:
(*it)->weekdays_mask = 0b1111111;
break;
case kAlarmWeekdays:
(*it)->weekdays_mask = 0b0111110;
break;
case kAlarmWeekends:
(*it)->weekdays_mask = 0b1000001;
break;
default:
break;
}
SaveAlarmToStorage(**it);
ESP_LOGI(TAG, "Modified alarm %d: %02d:%02d", alarm_id, hour, minute);
return true;
}
return false;
}
std::vector<AlarmItem> AlarmManager::GetAllAlarms() const {
if (!initialized_) {
return {};
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
std::vector<AlarmItem> result;
for (const auto& alarm : alarms_) {
result.push_back(*alarm);
}
return result;
}
AlarmItem* AlarmManager::GetAlarm(int alarm_id) {
if (!initialized_) {
return nullptr;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
auto it = std::find_if(alarms_.begin(), alarms_.end(),
[alarm_id](const auto& alarm) { return alarm->id == alarm_id; });
return (it != alarms_.end()) ? it->get() : nullptr;
}
std::vector<AlarmItem> AlarmManager::GetActiveAlarms() const {
if (!initialized_) {
return {};
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
std::vector<AlarmItem> result;
for (const auto& alarm : alarms_) {
if (alarm->status == kAlarmTriggered || alarm->status == kAlarmSnoozed) {
result.push_back(*alarm);
}
}
return result;
}
std::string AlarmManager::GetNextAlarmInfo() const {
if (!initialized_) {
return "闹钟管理器未初始化";
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
// 查找下一个要触发的闹钟
AlarmItem* next_alarm = nullptr;
int64_t current_time = GetCurrentTimeInMinutes();
int current_weekday = GetCurrentWeekday();
int64_t min_time_diff = 24 * 60 * 7; // 一周的分钟数
for (const auto& alarm : alarms_) {
if (alarm->status != kAlarmEnabled) {
continue;
}
// 计算今天和明天的时间差
int64_t alarm_time = alarm->hour * 60 + alarm->minute;
for (int day_offset = 0; day_offset < 7; day_offset++) {
int check_weekday = (current_weekday + day_offset) % 7;
if (day_offset == 0 && alarm_time <= current_time) {
continue; // 今天已经过了
}
if (alarm->repeat_mode == kAlarmOnce && day_offset > 0) {
continue; // 一次性闹钟只检查今天
}
if (IsWeekdayActive(*alarm, check_weekday)) {
int64_t time_diff = day_offset * 24 * 60 + alarm_time - current_time;
if (day_offset == 0) {
time_diff = alarm_time - current_time;
}
if (time_diff < min_time_diff) {
min_time_diff = time_diff;
next_alarm = alarm.get();
}
break;
}
}
}
if (!next_alarm) {
return "无活动闹钟";
}
// 格式化下一个闹钟信息
std::ostringstream oss;
oss << "下个闹钟: " << FormatTime(next_alarm->hour, next_alarm->minute);
if (min_time_diff < 24 * 60) {
int hours = min_time_diff / 60;
int minutes = min_time_diff % 60;
if (hours > 0) {
oss << " (" << hours << "小时" << minutes << "分钟后)";
} else {
oss << " (" << minutes << "分钟后)";
}
} else {
int days = min_time_diff / (24 * 60);
oss << " (" << days << "天后)";
}
if (!next_alarm->label.empty()) {
oss << " - " << next_alarm->label;
}
return oss.str();
}
bool AlarmManager::SnoozeAlarm(int alarm_id) {
if (!initialized_) {
return false;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
auto it = std::find_if(alarms_.begin(), alarms_.end(),
[alarm_id](const auto& alarm) { return alarm->id == alarm_id; });
if (it != alarms_.end() && (*it)->status == kAlarmTriggered) {
if ((*it)->snooze_count < (*it)->max_snooze_count) {
(*it)->status = kAlarmSnoozed;
(*it)->snooze_count++;
(*it)->next_snooze_time = esp_timer_get_time() / 1000000 + (*it)->snooze_minutes * 60;
ESP_LOGI(TAG, "Snoozed alarm %d for %d minutes (count: %d/%d)",
alarm_id, (*it)->snooze_minutes, (*it)->snooze_count, (*it)->max_snooze_count);
if (on_alarm_snoozed_) {
on_alarm_snoozed_(**it);
}
return true;
} else {
ESP_LOGI(TAG, "Alarm %d exceeded max snooze count, stopping", alarm_id);
StopAlarm(alarm_id);
}
}
return false;
}
bool AlarmManager::StopAlarm(int alarm_id) {
if (!initialized_) {
return false;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
auto it = std::find_if(alarms_.begin(), alarms_.end(),
[alarm_id](const auto& alarm) { return alarm->id == alarm_id; });
if (it != alarms_.end() &&
((*it)->status == kAlarmTriggered || (*it)->status == kAlarmSnoozed)) {
AlarmStatus old_status = (*it)->status;
(*it)->status = ((*it)->repeat_mode == kAlarmOnce) ? kAlarmDisabled : kAlarmEnabled;
(*it)->snooze_count = 0;
(*it)->next_snooze_time = 0;
ESP_LOGI(TAG, "Stopped alarm %d", alarm_id);
if (on_alarm_stopped_) {
on_alarm_stopped_(**it);
}
return true;
}
return false;
}
void AlarmManager::StopAllActiveAlarms() {
if (!initialized_) {
return;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
for (auto& alarm : alarms_) {
if (alarm->status == kAlarmTriggered || alarm->status == kAlarmSnoozed) {
alarm->status = (alarm->repeat_mode == kAlarmOnce) ? kAlarmDisabled : kAlarmEnabled;
alarm->snooze_count = 0;
alarm->next_snooze_time = 0;
if (on_alarm_stopped_) {
on_alarm_stopped_(*alarm);
}
}
}
ESP_LOGI(TAG, "Stopped all active alarms");
}
void AlarmManager::CheckAlarms() {
if (!initialized_) {
return;
}
int64_t current_time_seconds = esp_timer_get_time() / 1000000;
int64_t current_time_minutes = GetCurrentTimeInMinutes();
std::lock_guard<std::mutex> lock(alarms_mutex_);
for (auto& alarm : alarms_) {
// 检查贪睡闹钟
if (alarm->status == kAlarmSnoozed &&
current_time_seconds >= alarm->next_snooze_time) {
alarm->status = kAlarmTriggered;
alarm->next_snooze_time = 0;
ESP_LOGI(TAG, "Snooze ended for alarm %d, triggering again", alarm->id);
if (on_alarm_triggered_) {
on_alarm_triggered_(*alarm);
}
continue;
}
// 检查正常闹钟触发
if (alarm->status != kAlarmEnabled) {
continue;
}
int64_t alarm_time_minutes = alarm->hour * 60 + alarm->minute;
// 检查是否是触发时间(精确到分钟)
if (alarm_time_minutes != current_time_minutes) {
continue;
}
// 检查是否应该在今天触发
if (!ShouldTriggerToday(*alarm)) {
continue;
}
// 防止重复触发(同一分钟内)
int64_t current_time_in_seconds = esp_timer_get_time() / 1000000;
if (alarm->last_triggered_time > 0 &&
(current_time_in_seconds - alarm->last_triggered_time) < 60) {
continue;
}
// 触发闹钟
alarm->status = kAlarmTriggered;
alarm->last_triggered_time = current_time_in_seconds;
alarm->snooze_count = 0;
ESP_LOGI(TAG, "Triggering alarm %d: %02d:%02d - %s",
alarm->id, alarm->hour, alarm->minute, alarm->label.c_str());
if (on_alarm_triggered_) {
on_alarm_triggered_(*alarm);
}
}
}
void AlarmManager::SetAlarmTriggeredCallback(AlarmTriggeredCallback callback) {
on_alarm_triggered_ = callback;
}
void AlarmManager::SetAlarmSnoozeCallback(AlarmSnoozeCallback callback) {
on_alarm_snoozed_ = callback;
}
void AlarmManager::SetAlarmStopCallback(AlarmStopCallback callback) {
on_alarm_stopped_ = callback;
}
void AlarmManager::SetDefaultSnoozeMinutes(int minutes) {
default_snooze_minutes_ = std::max(1, std::min(60, minutes));
}
void AlarmManager::SetDefaultMaxSnoozeCount(int count) {
default_max_snooze_count_ = std::max(0, std::min(10, count));
}
// 静态工具方法
std::string AlarmManager::FormatTime(int hour, int minute) {
std::ostringstream oss;
oss << std::setfill('0') << std::setw(2) << hour
<< ":" << std::setfill('0') << std::setw(2) << minute;
return oss.str();
}
std::string AlarmManager::FormatAlarmTime(const AlarmItem& alarm) {
std::ostringstream oss;
oss << FormatTime(alarm.hour, alarm.minute);
switch (alarm.repeat_mode) {
case kAlarmOnce:
oss << " (一次)";
break;
case kAlarmDaily:
oss << " (每日)";
break;
case kAlarmWeekdays:
oss << " (工作日)";
break;
case kAlarmWeekends:
oss << " (周末)";
break;
case kAlarmCustom:
oss << " (自定义)";
break;
}
return oss.str();
}
bool AlarmManager::IsWeekdayActive(const AlarmItem& alarm, int weekday) {
if (alarm.repeat_mode == kAlarmOnce) {
return true; // 一次性闹钟在任何一天都可以触发
}
return (alarm.weekdays_mask & (1 << weekday)) != 0;
}
// 私有方法实现
void AlarmManager::LoadAlarmsFromStorage() {
std::lock_guard<std::mutex> lock(alarms_mutex_);
alarms_.clear();
// 读取闹钟数量
int alarm_count = settings_->GetInt("count", 0);
ESP_LOGI(TAG, "Loading %d alarms from storage", alarm_count);
for (int i = 0; i < alarm_count; i++) {
std::string alarm_key = "alarm_" + std::to_string(i);
std::string alarm_json = settings_->GetString(alarm_key);
if (alarm_json.empty()) {
continue;
}
cJSON* json = cJSON_Parse(alarm_json.c_str());
if (!json) {
ESP_LOGW(TAG, "Failed to parse alarm JSON: %s", alarm_key.c_str());
continue;
}
auto alarm = std::make_unique<AlarmItem>();
// 解析JSON数据
if (auto id = cJSON_GetObjectItem(json, "id")) alarm->id = id->valueint;
if (auto hour = cJSON_GetObjectItem(json, "hour")) alarm->hour = hour->valueint;
if (auto minute = cJSON_GetObjectItem(json, "minute")) alarm->minute = minute->valueint;
if (auto repeat = cJSON_GetObjectItem(json, "repeat")) alarm->repeat_mode = (AlarmRepeatMode)repeat->valueint;
if (auto mask = cJSON_GetObjectItem(json, "weekdays")) alarm->weekdays_mask = mask->valueint;
if (auto status = cJSON_GetObjectItem(json, "status")) alarm->status = (AlarmStatus)status->valueint;
if (auto label = cJSON_GetObjectItem(json, "label")) alarm->label = label->valuestring;
if (auto music = cJSON_GetObjectItem(json, "music")) alarm->music_name = music->valuestring;
if (auto snooze_min = cJSON_GetObjectItem(json, "snooze_minutes")) alarm->snooze_minutes = snooze_min->valueint;
if (auto max_snooze = cJSON_GetObjectItem(json, "max_snooze")) alarm->max_snooze_count = max_snooze->valueint;
// 重置运行时状态
alarm->snooze_count = 0;
alarm->last_triggered_time = 0;
alarm->next_snooze_time = 0;
if (alarm->status == kAlarmTriggered || alarm->status == kAlarmSnoozed) {
alarm->status = kAlarmEnabled;
}
alarms_.push_back(std::move(alarm));
cJSON_Delete(json);
}
ESP_LOGI(TAG, "Loaded %d alarms successfully", alarms_.size());
}
void AlarmManager::SaveAlarmsToStorage() {
if (!settings_) {
return;
}
std::lock_guard<std::mutex> lock(alarms_mutex_);
// 保存闹钟数量
settings_->SetInt("count", alarms_.size());
// 保存每个闹钟
for (size_t i = 0; i < alarms_.size(); i++) {
std::string alarm_key = "alarm_" + std::to_string(i);
SaveAlarmToStorage(*alarms_[i]);
}
ESP_LOGI(TAG, "Saved %d alarms to storage", alarms_.size());
}
void AlarmManager::SaveAlarmToStorage(const AlarmItem& alarm) {
if (!settings_) {
return;
}
cJSON* json = cJSON_CreateObject();
cJSON_AddNumberToObject(json, "id", alarm.id);
cJSON_AddNumberToObject(json, "hour", alarm.hour);
cJSON_AddNumberToObject(json, "minute", alarm.minute);
cJSON_AddNumberToObject(json, "repeat", alarm.repeat_mode);
cJSON_AddNumberToObject(json, "weekdays", alarm.weekdays_mask);
cJSON_AddNumberToObject(json, "status", alarm.status);
cJSON_AddStringToObject(json, "label", alarm.label.c_str());
cJSON_AddStringToObject(json, "music", alarm.music_name.c_str());
cJSON_AddNumberToObject(json, "snooze_minutes", alarm.snooze_minutes);
cJSON_AddNumberToObject(json, "max_snooze", alarm.max_snooze_count);
char* json_string = cJSON_PrintUnformatted(json);
// 找到这个闹钟在数组中的位置
auto it = std::find_if(alarms_.begin(), alarms_.end(),
[&alarm](const auto& a) { return a->id == alarm.id; });
if (it != alarms_.end()) {
size_t index = std::distance(alarms_.begin(), it);
std::string alarm_key = "alarm_" + std::to_string(index);
settings_->SetString(alarm_key, json_string);
}
cJSON_free(json_string);
cJSON_Delete(json);
}
void AlarmManager::RemoveAlarmFromStorage(int alarm_id) {
// 重新保存所有闹钟(简单实现)
SaveAlarmsToStorage();
}
int AlarmManager::GetNextAlarmId() {
return next_alarm_id_++;
}
bool AlarmManager::ShouldTriggerToday(const AlarmItem& alarm) const {
if (alarm.repeat_mode == kAlarmOnce) {
return true;
}
int weekday = GetCurrentWeekday();
return IsWeekdayActive(alarm, weekday);
}
int64_t AlarmManager::GetCurrentTimeInMinutes() const {
time_t now;
time(&now);
struct tm* timeinfo = localtime(&now);
return timeinfo->tm_hour * 60 + timeinfo->tm_min;
}
int AlarmManager::GetCurrentWeekday() const {
time_t now;
time(&now);
struct tm* timeinfo = localtime(&now);
return timeinfo->tm_wday; // 0=周日, 1=周一, ..., 6=周六
}

140
main/alarm_manager.h Normal file
View File

@@ -0,0 +1,140 @@
#ifndef ALARM_MANAGER_H
#define ALARM_MANAGER_H
#include <string>
#include <vector>
#include <memory>
#include <functional>
#include <esp_timer.h>
#include "settings.h"
// 闹钟重复模式
enum AlarmRepeatMode {
kAlarmOnce = 0, // 一次性闹钟
kAlarmDaily = 1, // 每日重复
kAlarmWeekdays = 2, // 工作日(周一到周五)
kAlarmWeekends = 3, // 周末(周六周日)
kAlarmCustom = 4 // 自定义星期(使用weekdays_mask)
};
// 闹钟状态
enum AlarmStatus {
kAlarmEnabled = 0, // 启用
kAlarmDisabled = 1, // 禁用
kAlarmTriggered = 2, // 已触发(等待贪睡或关闭)
kAlarmSnoozed = 3 // 贪睡中
};
// 闹钟项结构
struct AlarmItem {
int id; // 闹钟ID
int hour; // 小时 (0-23)
int minute; // 分钟 (0-59)
AlarmRepeatMode repeat_mode; // 重复模式
uint8_t weekdays_mask; // 星期掩码 (bit0=周日, bit1=周一, ..., bit6=周六)
AlarmStatus status; // 闹钟状态
std::string label; // 闹钟标签/备注
std::string music_name; // 指定的音乐名称(空则使用默认铃声)
int snooze_count; // 当前贪睡次数
int max_snooze_count; // 最大贪睡次数 (默认3次)
int snooze_minutes; // 贪睡间隔(分钟默认5分钟)
int64_t last_triggered_time; // 上次触发时间戳(避免重复触发)
int64_t next_snooze_time; // 下次贪睡时间戳
AlarmItem() : id(0), hour(0), minute(0), repeat_mode(kAlarmOnce),
weekdays_mask(0), status(kAlarmEnabled), label(""),
music_name(""), snooze_count(0), max_snooze_count(3),
snooze_minutes(5), last_triggered_time(0), next_snooze_time(0) {}
};
// 闹钟触发回调类型
using AlarmTriggeredCallback = std::function<void(const AlarmItem& alarm)>;
using AlarmSnoozeCallback = std::function<void(const AlarmItem& alarm)>;
using AlarmStopCallback = std::function<void(const AlarmItem& alarm)>;
class AlarmManager {
public:
static AlarmManager& GetInstance() {
static AlarmManager instance;
return instance;
}
// 删除拷贝构造函数和赋值运算符
AlarmManager(const AlarmManager&) = delete;
AlarmManager& operator=(const AlarmManager&) = delete;
// 初始化和清理
void Initialize();
void Cleanup();
// 闹钟管理
int AddAlarm(int hour, int minute, AlarmRepeatMode repeat_mode = kAlarmOnce,
const std::string& label = "", const std::string& music_name = "");
bool RemoveAlarm(int alarm_id);
bool EnableAlarm(int alarm_id, bool enabled = true);
bool ModifyAlarm(int alarm_id, int hour, int minute, AlarmRepeatMode repeat_mode = kAlarmOnce,
const std::string& label = "", const std::string& music_name = "");
// 查询功能
std::vector<AlarmItem> GetAllAlarms() const;
AlarmItem* GetAlarm(int alarm_id);
std::vector<AlarmItem> GetActiveAlarms() const;
std::string GetNextAlarmInfo() const; // 获取下一个闹钟的信息字符串
// 贪睡和停止
bool SnoozeAlarm(int alarm_id);
bool StopAlarm(int alarm_id);
void StopAllActiveAlarms();
// 时间检查 (由Application的CLOCK_TICK调用)
void CheckAlarms();
// 回调设置
void SetAlarmTriggeredCallback(AlarmTriggeredCallback callback);
void SetAlarmSnoozeCallback(AlarmSnoozeCallback callback);
void SetAlarmStopCallback(AlarmStopCallback callback);
// 配置设置
void SetDefaultSnoozeMinutes(int minutes);
void SetDefaultMaxSnoozeCount(int count);
// 时间工具
static std::string FormatTime(int hour, int minute);
static std::string FormatAlarmTime(const AlarmItem& alarm);
static bool IsWeekdayActive(const AlarmItem& alarm, int weekday); // 0=周日, 1=周一, ..., 6=周六
private:
AlarmManager();
~AlarmManager();
// 内部方法
void LoadAlarmsFromStorage();
void SaveAlarmsToStorage();
void SaveAlarmToStorage(const AlarmItem& alarm);
void RemoveAlarmFromStorage(int alarm_id);
int GetNextAlarmId();
bool ShouldTriggerToday(const AlarmItem& alarm) const;
int64_t GetCurrentTimeInMinutes() const; // 获取当前时间的分钟数(从午夜开始)
int GetCurrentWeekday() const; // 获取当前星期几
// 成员变量
std::vector<std::unique_ptr<AlarmItem>> alarms_;
std::unique_ptr<Settings> settings_;
bool initialized_;
int next_alarm_id_;
// 配置
int default_snooze_minutes_;
int default_max_snooze_count_;
// 回调函数
AlarmTriggeredCallback on_alarm_triggered_;
AlarmSnoozeCallback on_alarm_snoozed_;
AlarmStopCallback on_alarm_stopped_;
// 互斥锁保护
mutable std::mutex alarms_mutex_;
};
#endif // ALARM_MANAGER_H

File diff suppressed because it is too large Load Diff

View File

@@ -1,111 +1,129 @@
#ifndef _APPLICATION_H_ #ifndef _APPLICATION_H_
#define _APPLICATION_H_ #define _APPLICATION_H_
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h> #include <freertos/event_groups.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <esp_timer.h> #include <esp_timer.h>
#include <string> #include <string>
#include <mutex> #include <mutex>
#include <deque> #include <deque>
#include <memory> #include <memory>
#include "protocol.h" #include "protocol.h"
#include "ota.h" #include "ota.h"
#include "audio_service.h" #include "audio_service.h"
#include "device_state_event.h" #include "device_state_event.h"
#include "alarm_manager.h"
#define MAIN_EVENT_SCHEDULE (1 << 0)
#define MAIN_EVENT_SEND_AUDIO (1 << 1) #define MAIN_EVENT_SCHEDULE (1 << 0)
#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2) #define MAIN_EVENT_SEND_AUDIO (1 << 1)
#define MAIN_EVENT_VAD_CHANGE (1 << 3) #define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
#define MAIN_EVENT_ERROR (1 << 4) #define MAIN_EVENT_VAD_CHANGE (1 << 3)
#define MAIN_EVENT_CHECK_NEW_VERSION_DONE (1 << 5) #define MAIN_EVENT_ERROR (1 << 4)
#define MAIN_EVENT_CLOCK_TICK (1 << 6) #define MAIN_EVENT_CHECK_NEW_VERSION_DONE (1 << 5)
#define MAIN_EVENT_CLOCK_TICK (1 << 6)
enum AecMode {
kAecOff, enum AecMode {
kAecOnDeviceSide, kAecOff,
kAecOnServerSide, kAecOnDeviceSide,
}; kAecOnServerSide,
};
class Application {
public: class Application {
static Application& GetInstance() { public:
static Application instance; static Application& GetInstance() {
return instance; static Application instance;
} return instance;
// 删除拷贝构造函数和赋值运算符 }
Application(const Application&) = delete; // 删除拷贝构造函数和赋值运算符
Application& operator=(const Application&) = delete; Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
void Start();
void MainEventLoop(); void Start();
DeviceState GetDeviceState() const { return device_state_; } void MainEventLoop();
bool IsVoiceDetected() const { return audio_service_.IsVoiceDetected(); } DeviceState GetDeviceState() const { return device_state_; }
void Schedule(std::function<void()> callback); bool IsVoiceDetected() const { return audio_service_.IsVoiceDetected(); }
void SetDeviceState(DeviceState state); void Schedule(std::function<void()> callback);
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = ""); void SetDeviceState(DeviceState state);
void DismissAlert(); void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");
void AbortSpeaking(AbortReason reason); void DismissAlert();
void ToggleChatState(); void AbortSpeaking(AbortReason reason);
void StartListening(); void ToggleChatState();
void StopListening(); void StartListening();
void Reboot(); void StopListening();
void WakeWordInvoke(const std::string& wake_word); void Reboot();
bool UpgradeFirmware(Ota& ota, const std::string& url = ""); void WakeWordInvoke(const std::string& wake_word);
bool CanEnterSleepMode(); bool UpgradeFirmware(Ota& ota, const std::string& url = "");
void SendMcpMessage(const std::string& payload); bool CanEnterSleepMode();
void SetAecMode(AecMode mode); void SendMcpMessage(const std::string& payload);
AecMode GetAecMode() const { return aec_mode_; } void SetAecMode(AecMode mode);
void PlaySound(const std::string_view& sound); AecMode GetAecMode() const { return aec_mode_; }
AudioService& GetAudioService() { return audio_service_; } void PlaySound(const std::string_view& sound);
void AddAudioData(AudioStreamPacket&& packet); // 新增:接收外部音频数据(如音乐播放)
void AddAudioData(AudioStreamPacket&& packet);
private: AudioService& GetAudioService() { return audio_service_; }
Application();
~Application(); // 闹钟功能
AlarmManager& GetAlarmManager() { return AlarmManager::GetInstance(); }
std::mutex mutex_; std::vector<std::string> GetDefaultAlarmMusicList() const;
std::deque<std::function<void()>> main_tasks_;
std::unique_ptr<Protocol> protocol_; private:
EventGroupHandle_t event_group_ = nullptr; Application();
esp_timer_handle_t clock_timer_handle_ = nullptr; ~Application();
volatile DeviceState device_state_ = kDeviceStateUnknown;
ListeningMode listening_mode_ = kListeningModeAutoStop; std::mutex mutex_;
AecMode aec_mode_ = kAecOff; std::deque<std::function<void()>> main_tasks_;
std::string last_error_message_; std::unique_ptr<Protocol> protocol_;
AudioService audio_service_; EventGroupHandle_t event_group_ = nullptr;
esp_timer_handle_t clock_timer_handle_ = nullptr;
bool has_server_time_ = false; volatile DeviceState device_state_ = kDeviceStateUnknown;
bool aborted_ = false; ListeningMode listening_mode_ = kListeningModeAutoStop;
int clock_ticks_ = 0; AecMode aec_mode_ = kAecOff;
TaskHandle_t check_new_version_task_handle_ = nullptr; std::string last_error_message_;
TaskHandle_t main_event_loop_task_handle_ = nullptr; AudioService audio_service_;
void OnWakeWordDetected(); bool has_server_time_ = false;
void CheckNewVersion(Ota& ota); bool aborted_ = false;
void CheckAssetsVersion(); int clock_ticks_ = 0;
void ShowActivationCode(const std::string& code, const std::string& message); TaskHandle_t check_new_version_task_handle_ = nullptr;
void SetListeningMode(ListeningMode mode); TaskHandle_t main_event_loop_task_handle_ = nullptr;
};
void OnWakeWordDetected();
void CheckNewVersion(Ota& ota);
class TaskPriorityReset { void CheckAssetsVersion();
public: void ShowActivationCode(const std::string& code, const std::string& message);
TaskPriorityReset(BaseType_t priority) { void SetListeningMode(ListeningMode mode);
original_priority_ = uxTaskPriorityGet(NULL);
vTaskPrioritySet(NULL, priority); // 闹钟相关私有方法
} void OnAlarmTriggered(const AlarmItem& alarm);
~TaskPriorityReset() { void OnAlarmSnoozed(const AlarmItem& alarm);
vTaskPrioritySet(NULL, original_priority_); void OnAlarmStopped(const AlarmItem& alarm);
}
// 音乐进度跟踪相关
private: void UpdateMusicProgress();
BaseType_t original_priority_; std::string current_music_name_;
}; int64_t music_start_time_ms_ = 0; // 音乐开始播放的时间戳
int music_duration_seconds_ = 180; // 默认歌曲长度3分钟
#endif // _APPLICATION_H_ bool is_music_playing_ = false;
};
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_

View File

@@ -1,406 +1,517 @@
#include "assets.h" #include "assets.h"
#include "board.h" #include "board.h"
#include "display.h" #include "display.h"
#include "application.h" #include "application.h"
#include "lvgl_theme.h" #include "lvgl_theme.h"
#include "emote_display.h"
#include <esp_log.h>
#include <spi_flash_mmap.h> #include <esp_log.h>
#include <esp_timer.h> #include <spi_flash_mmap.h>
#include <cbin_font.h> #include <esp_timer.h>
#include <cbin_font.h>
#define TAG "Assets"
#define TAG "Assets"
struct mmap_assets_table {
char asset_name[32]; /*!< Name of the asset */ struct mmap_assets_table {
uint32_t asset_size; /*!< Size of the asset */ char asset_name[32]; /*!< Name of the asset */
uint32_t asset_offset; /*!< Offset of the asset */ uint32_t asset_size; /*!< Size of the asset */
uint16_t asset_width; /*!< Width of the asset */ uint32_t asset_offset; /*!< Offset of the asset */
uint16_t asset_height; /*!< Height 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) { Assets::Assets() {
default_assets_url_ = default_assets_url; // Initialize the partition
} else { InitializePartition();
ESP_LOGE(TAG, "The default assets url is not a http url: %s", default_assets_url.c_str()); }
}
Assets::~Assets() {
// Initialize the partition if (mmap_handle_ != 0) {
InitializePartition(); esp_partition_munmap(mmap_handle_);
} }
}
Assets::~Assets() {
if (mmap_handle_ != 0) { uint32_t Assets::CalculateChecksum(const char* data, uint32_t length) {
esp_partition_munmap(mmap_handle_); uint32_t checksum = 0;
} for (uint32_t i = 0; i < length; i++) {
} checksum += data[i];
}
uint32_t Assets::CalculateChecksum(const char* data, uint32_t length) { return checksum & 0xFFFF;
uint32_t checksum = 0; }
for (uint32_t i = 0; i < length; i++) {
checksum += data[i]; bool Assets::InitializePartition() {
} partition_valid_ = false;
return checksum & 0xFFFF; checksum_valid_ = false;
} assets_.clear();
bool Assets::InitializePartition() { partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, "assets");
partition_valid_ = false; if (partition_ == nullptr) {
checksum_valid_ = false; ESP_LOGI(TAG, "No assets partition found");
assets_.clear(); return false;
}
partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, "assets");
if (partition_ == nullptr) { int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
ESP_LOGI(TAG, "No assets partition found"); uint32_t storage_size = free_pages * 64 * 1024;
return false; 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) {
int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA); ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, partition_->size / 1024);
uint32_t storage_size = free_pages * 64 * 1024; return false;
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_err_t err = esp_partition_mmap(partition_, 0, partition_->size, ESP_PARTITION_MMAP_DATA, (const void**)&mmap_root_, &mmap_handle_);
ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB", storage_size / 1024, partition_->size / 1024); if (err != ESP_OK) {
return false; ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err));
} 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) { partition_valid_ = true;
ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err));
return false; 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);
partition_valid_ = true;
if (stored_len > partition_->size - 12) {
uint32_t stored_files = *(uint32_t*)(mmap_root_ + 0); ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, partition_->size);
uint32_t stored_chksum = *(uint32_t*)(mmap_root_ + 4); return false;
uint32_t stored_len = *(uint32_t*)(mmap_root_ + 8); }
if (stored_len > partition_->size - 12) { auto start_time = esp_timer_get_time();
ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12", stored_len, partition_->size); uint32_t calculated_checksum = CalculateChecksum(mmap_root_ + 12, stored_len);
return false; auto end_time = esp_timer_get_time();
} ESP_LOGI(TAG, "The checksum calculation time is %d ms", int((end_time - start_time) / 1000));
auto start_time = esp_timer_get_time(); if (calculated_checksum != stored_chksum) {
uint32_t calculated_checksum = CalculateChecksum(mmap_root_ + 12, stored_len); ESP_LOGE(TAG, "The calculated checksum (0x%lx) does not match the stored checksum (0x%lx)", calculated_checksum, stored_chksum);
auto end_time = esp_timer_get_time(); return false;
ESP_LOGI(TAG, "The checksum calculation time is %d ms", int((end_time - start_time) / 1000)); }
if (calculated_checksum != stored_chksum) { checksum_valid_ = true;
ESP_LOGE(TAG, "The calculated checksum (0x%lx) does not match the stored checksum (0x%lx)", calculated_checksum, stored_chksum);
return false; 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{
checksum_valid_ = true; .size = static_cast<size_t>(item->asset_size),
.offset = static_cast<size_t>(12 + sizeof(mmap_assets_table) * stored_files + item->asset_offset)
for (uint32_t i = 0; i < stored_files; i++) { };
auto item = (const mmap_assets_table*)(mmap_root_ + 12 + i * sizeof(mmap_assets_table)); assets_[item->asset_name] = asset;
auto asset = Asset{ }
.size = static_cast<size_t>(item->asset_size), return checksum_valid_;
.offset = static_cast<size_t>(12 + sizeof(mmap_assets_table) * stored_files + item->asset_offset) }
};
assets_[item->asset_name] = asset; bool Assets::Apply() {
} void* ptr = nullptr;
return checksum_valid_; size_t size = 0;
} if (!GetAssetData("index.json", ptr, size)) {
ESP_LOGE(TAG, "The index.json file is not found");
bool Assets::Apply() { return false;
void* ptr = nullptr; }
size_t size = 0;
if (!GetAssetData("index.json", ptr, size)) { cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
ESP_LOGE(TAG, "The index.json file is not found"); if (root == nullptr) {
return false; ESP_LOGE(TAG, "The index.json file is not valid");
} return false;
cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size); }
if (root == nullptr) {
ESP_LOGE(TAG, "The index.json file is not valid"); cJSON* version = cJSON_GetObjectItem(root, "version");
return false; if (cJSON_IsNumber(version)) {
} if (version->valuedouble > 1) {
ESP_LOGE(TAG, "The assets version %d is not supported, please upgrade the firmware", version->valueint);
cJSON* version = cJSON_GetObjectItem(root, "version"); return false;
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)) {
cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels"); if (models_list_ != nullptr) {
if (cJSON_IsString(srmodels)) { esp_srmodel_deinit(models_list_);
std::string srmodels_file = srmodels->valuestring; models_list_ = nullptr;
if (GetAssetData(srmodels_file, ptr, size)) { }
if (models_list_ != nullptr) { models_list_ = srmodel_load(static_cast<uint8_t*>(ptr));
esp_srmodel_deinit(models_list_); if (models_list_ != nullptr) {
models_list_ = nullptr; auto& app = Application::GetInstance();
} app.GetAudioService().SetModelsList(models_list_);
models_list_ = srmodel_load(static_cast<uint8_t*>(ptr)); } else {
if (models_list_ != nullptr) { ESP_LOGE(TAG, "Failed to load srmodels.bin");
auto& app = Application::GetInstance(); }
app.GetAudioService().SetModelsList(models_list_); } else {
} else { ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str());
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");
#ifdef HAVE_LVGL
auto& theme_manager = LvglThemeManager::GetInstance(); cJSON* font = cJSON_GetObjectItem(root, "text_font");
auto light_theme = theme_manager.GetTheme("light"); if (cJSON_IsString(font)) {
auto dark_theme = theme_manager.GetTheme("dark"); std::string fonts_text_file = font->valuestring;
if (GetAssetData(fonts_text_file, ptr, size)) {
cJSON* font = cJSON_GetObjectItem(root, "text_font"); auto text_font = std::make_shared<LvglCBinFont>(ptr);
if (cJSON_IsString(font)) { if (text_font->font() == nullptr) {
std::string fonts_text_file = font->valuestring; ESP_LOGE(TAG, "Failed to load fonts.bin");
if (GetAssetData(fonts_text_file, ptr, size)) { return false;
auto text_font = std::make_shared<LvglCBinFont>(ptr); }
if (text_font->font() == nullptr) { if (light_theme != nullptr) {
ESP_LOGE(TAG, "Failed to load fonts.bin"); light_theme->set_text_font(text_font);
return false; }
} if (dark_theme != nullptr) {
if (light_theme != nullptr) { dark_theme->set_text_font(text_font);
light_theme->set_text_font(text_font); }
} } else {
if (dark_theme != nullptr) { ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
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<EmojiCollection>();
int emoji_count = cJSON_GetArraySize(emoji_collection);
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection"); for (int i = 0; i < emoji_count; i++) {
if (cJSON_IsArray(emoji_collection)) { cJSON* emoji = cJSON_GetArrayItem(emoji_collection, i);
auto custom_emoji_collection = std::make_shared<EmojiCollection>(); if (cJSON_IsObject(emoji)) {
int emoji_count = cJSON_GetArraySize(emoji_collection); cJSON* name = cJSON_GetObjectItem(emoji, "name");
for (int i = 0; i < emoji_count; i++) { cJSON* file = cJSON_GetObjectItem(emoji, "file");
cJSON* emoji = cJSON_GetArrayItem(emoji_collection, i); cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf");
if (cJSON_IsObject(emoji)) { if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL== eaf)) {
cJSON* name = cJSON_GetObjectItem(emoji, "name"); if (!GetAssetData(file->valuestring, ptr, size)) {
cJSON* file = cJSON_GetObjectItem(emoji, "file"); ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring);
if (cJSON_IsString(name) && cJSON_IsString(file)) { continue;
if (!GetAssetData(file->valuestring, ptr, size)) { }
ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring); custom_emoji_collection->AddEmoji(name->valuestring, new LvglRawImage(ptr, size));
continue; }
} }
custom_emoji_collection->AddEmoji(name->valuestring, new LvglRawImage(ptr, size)); }
} if (light_theme != nullptr) {
} light_theme->set_emoji_collection(custom_emoji_collection);
} }
if (light_theme != nullptr) { if (dark_theme != nullptr) {
light_theme->set_emoji_collection(custom_emoji_collection); dark_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");
cJSON* skin = cJSON_GetObjectItem(root, "skin"); if (cJSON_IsObject(light_skin) && light_theme != nullptr) {
if (cJSON_IsObject(skin)) { cJSON* text_color = cJSON_GetObjectItem(light_skin, "text_color");
cJSON* light_skin = cJSON_GetObjectItem(skin, "light"); cJSON* background_color = cJSON_GetObjectItem(light_skin, "background_color");
if (cJSON_IsObject(light_skin) && light_theme != nullptr) { cJSON* background_image = cJSON_GetObjectItem(light_skin, "background_image");
cJSON* text_color = cJSON_GetObjectItem(light_skin, "text_color"); if (cJSON_IsString(text_color)) {
cJSON* background_color = cJSON_GetObjectItem(light_skin, "background_color"); light_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring));
cJSON* background_image = cJSON_GetObjectItem(light_skin, "background_image"); }
if (cJSON_IsString(text_color)) { if (cJSON_IsString(background_color)) {
light_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring)); 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_color)) { }
light_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring)); if (cJSON_IsString(background_image)) {
light_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring)); if (!GetAssetData(background_image->valuestring, ptr, size)) {
} ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
if (cJSON_IsString(background_image)) { return false;
if (!GetAssetData(background_image->valuestring, ptr, size)) { }
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring); auto background_image = std::make_shared<LvglCBinImage>(ptr);
return false; light_theme->set_background_image(background_image);
} }
auto background_image = std::make_shared<LvglCBinImage>(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* dark_skin = cJSON_GetObjectItem(skin, "dark"); cJSON* background_color = cJSON_GetObjectItem(dark_skin, "background_color");
if (cJSON_IsObject(dark_skin) && dark_theme != nullptr) { cJSON* background_image = cJSON_GetObjectItem(dark_skin, "background_image");
cJSON* text_color = cJSON_GetObjectItem(dark_skin, "text_color"); if (cJSON_IsString(text_color)) {
cJSON* background_color = cJSON_GetObjectItem(dark_skin, "background_color"); dark_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring));
cJSON* background_image = cJSON_GetObjectItem(dark_skin, "background_image"); }
if (cJSON_IsString(text_color)) { if (cJSON_IsString(background_color)) {
dark_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring)); 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_color)) { }
dark_theme->set_background_color(LvglTheme::ParseColor(background_color->valuestring)); if (cJSON_IsString(background_image)) {
dark_theme->set_chat_background_color(LvglTheme::ParseColor(background_color->valuestring)); if (!GetAssetData(background_image->valuestring, ptr, size)) {
} ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring);
if (cJSON_IsString(background_image)) { return false;
if (!GetAssetData(background_image->valuestring, ptr, size)) { }
ESP_LOGE(TAG, "The background image file %s is not found", background_image->valuestring); auto background_image = std::make_shared<LvglCBinImage>(ptr);
return false; dark_theme->set_background_image(background_image);
} }
auto background_image = std::make_shared<LvglCBinImage>(ptr); }
dark_theme->set_background_image(background_image); }
}
} auto display = Board::GetInstance().GetDisplay();
} ESP_LOGI(TAG, "Refreshing display theme...");
#endif
auto current_theme = display->GetTheme();
auto display = Board::GetInstance().GetDisplay(); if (current_theme != nullptr) {
ESP_LOGI(TAG, "Refreshing display theme..."); display->SetTheme(current_theme);
}
auto current_theme = display->GetTheme(); #elif defined(CONFIG_USE_EMOTE_MESSAGE_STYLE)
if (current_theme != nullptr) { auto &board = Board::GetInstance();
display->SetTheme(current_theme); auto display = board.GetDisplay();
} auto emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
cJSON_Delete(root);
return true; cJSON* font = cJSON_GetObjectItem(root, "text_font");
} if (cJSON_IsString(font)) {
std::string fonts_text_file = font->valuestring;
bool Assets::Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback) { if (GetAssetData(fonts_text_file, ptr, size)) {
ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str()); auto text_font = std::make_shared<LvglCBinFont>(ptr);
if (text_font->font() == nullptr) {
// 取消当前资源分区的内存映射 ESP_LOGE(TAG, "Failed to load fonts.bin");
if (mmap_handle_ != 0) { return false;
esp_partition_munmap(mmap_handle_); }
mmap_handle_ = 0;
mmap_root_ = nullptr; if (emote_display) {
} emote_display->AddTextFont(text_font);
checksum_valid_ = false; }
assets_.clear(); } else {
ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
// 下载新的资源文件 }
auto network = Board::GetInstance().GetNetwork(); }
auto http = network->CreateHttp(0);
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
if (!http->Open("GET", url)) { if (cJSON_IsArray(emoji_collection)) {
ESP_LOGE(TAG, "Failed to open HTTP connection"); int emoji_count = cJSON_GetArraySize(emoji_collection);
return false; if (emote_display) {
} for (int i = 0; i < emoji_count; i++) {
cJSON* icon = cJSON_GetArrayItem(emoji_collection, i);
if (http->GetStatusCode() != 200) { if (cJSON_IsObject(icon)) {
ESP_LOGE(TAG, "Failed to get assets, status code: %d", http->GetStatusCode()); cJSON* name = cJSON_GetObjectItem(icon, "name");
return false; cJSON* file = cJSON_GetObjectItem(icon, "file");
}
if (cJSON_IsString(name) && cJSON_IsString(file)) {
size_t content_length = http->GetBodyLength(); if (GetAssetData(file->valuestring, ptr, size)) {
if (content_length == 0) { cJSON* eaf = cJSON_GetObjectItem(icon, "eaf");
ESP_LOGE(TAG, "Failed to get content length"); bool lack_value = false;
return false; bool loop_value = false;
} int fps_value = 0;
if (content_length > partition_->size) { if (cJSON_IsObject(eaf)) {
ESP_LOGE(TAG, "Assets file size (%u) is larger than partition size (%lu)", content_length, partition_->size); cJSON* lack = cJSON_GetObjectItem(eaf, "lack");
return false; cJSON* loop = cJSON_GetObjectItem(eaf, "loop");
} cJSON* fps = cJSON_GetObjectItem(eaf, "fps");
// 定义扇区大小为4KBESP32的标准扇区大小 lack_value = lack ? cJSON_IsTrue(lack) : false;
const size_t SECTOR_SIZE = esp_partition_get_main_flash_sector_size(); loop_value = loop ? cJSON_IsTrue(loop) : false;
fps_value = fps ? fps->valueint : 0;
// 计算需要擦除的扇区数量
size_t sectors_to_erase = (content_length + SECTOR_SIZE - 1) / SECTOR_SIZE; // 向上取整 emote_display->AddEmojiData(name->valuestring, ptr, size,
size_t total_erase_size = sectors_to_erase * SECTOR_SIZE; static_cast<uint8_t>(fps_value),
loop_value, lack_value);
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);
} else {
// 写入新的资源文件到分区一边erase一边写入 ESP_LOGE(TAG, "Emoji \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
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)); cJSON* icon_collection = cJSON_GetObjectItem(root, "icon_collection");
if (ret < 0) { if (cJSON_IsArray(icon_collection)) {
ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret)); if (emote_display) {
return false; int icon_count = cJSON_GetArraySize(icon_collection);
} for (int i = 0; i < icon_count; i++) {
cJSON* icon = cJSON_GetArrayItem(icon_collection, i);
if (ret == 0) { if (cJSON_IsObject(icon)) {
break; cJSON* name = cJSON_GetObjectItem(icon, "name");
} cJSON* file = cJSON_GetObjectItem(icon, "file");
// 检查是否需要擦除新的扇区 if (cJSON_IsString(name) && cJSON_IsString(file)) {
size_t write_end_offset = total_written + ret; if (GetAssetData(file->valuestring, ptr, size)) {
size_t needed_sectors = (write_end_offset + SECTOR_SIZE - 1) / SECTOR_SIZE; emote_display->AddIconData(name->valuestring, ptr, size);
} else {
// 擦除需要的新扇区 ESP_LOGE(TAG, "Icon \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
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; cJSON* layout_json = cJSON_GetObjectItem(root, "layout");
} if (cJSON_IsArray(layout_json)) {
int layout_count = cJSON_GetArraySize(layout_json);
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); for (int i = 0; i < layout_count; i++) {
if (err != ESP_OK) { cJSON* layout_item = cJSON_GetArrayItem(layout_json, i);
ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", current_sector, sector_start, esp_err_to_name(err)); if (cJSON_IsObject(layout_item)) {
return false; cJSON* name = cJSON_GetObjectItem(layout_item, "name");
} cJSON* align = cJSON_GetObjectItem(layout_item, "align");
cJSON* x = cJSON_GetObjectItem(layout_item, "x");
current_sector++; cJSON* y = cJSON_GetObjectItem(layout_item, "y");
} cJSON* width = cJSON_GetObjectItem(layout_item, "width");
cJSON* height = cJSON_GetObjectItem(layout_item, "height");
// 写入数据到分区
esp_err_t err = esp_partition_write(partition_, total_written, buffer, ret); if (cJSON_IsString(name) && cJSON_IsString(align) && cJSON_IsNumber(x) && cJSON_IsNumber(y)) {
if (err != ESP_OK) { int width_val = cJSON_IsNumber(width) ? width->valueint : 0;
ESP_LOGE(TAG, "Failed to write to assets partition at offset %u: %s", total_written, esp_err_to_name(err)); int height_val = cJSON_IsNumber(height) ? height->valueint : 0;
return false;
} if (emote_display) {
emote_display->AddLayoutData(name->valuestring, align->valuestring,
total_written += ret; x->valueint, y->valueint, width_val, height_val);
recent_written += ret; }
} else {
// 计算进度和速度 ESP_LOGW(TAG, "Invalid layout item %d: missing required fields", i);
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); #endif
if (progress_callback) {
progress_callback(progress, speed); cJSON_Delete(root);
} return true;
last_calc_time = esp_timer_get_time(); }
recent_written = 0; // 重置最近写入的字节数
} bool Assets::Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback) {
} ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str());
http->Close(); // 取消当前资源分区的内存映射
if (mmap_handle_ != 0) {
if (total_written != content_length) { esp_partition_munmap(mmap_handle_);
ESP_LOGE(TAG, "Downloaded size (%u) does not match expected size (%u)", total_written, content_length); mmap_handle_ = 0;
return false; mmap_root_ = nullptr;
} }
checksum_valid_ = false;
ESP_LOGI(TAG, "Assets download completed, total written: %u bytes, total sectors erased: %u", assets_.clear();
total_written, current_sector);
// 下载新的资源文件
// 重新初始化资源分区 auto network = Board::GetInstance().GetNetwork();
if (!InitializePartition()) { auto http = network->CreateHttp(0);
ESP_LOGE(TAG, "Failed to re-initialize assets partition");
return false; if (!http->Open("GET", url)) {
} ESP_LOGE(TAG, "Failed to open HTTP connection");
return false;
return true; }
}
if (http->GetStatusCode() != 200) {
bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) { ESP_LOGE(TAG, "Failed to get assets, status code: %d", http->GetStatusCode());
auto asset = assets_.find(name); return false;
if (asset == assets_.end()) { }
return false;
} size_t content_length = http->GetBodyLength();
auto data = (const char*)(mmap_root_ + asset->second.offset); if (content_length == 0) {
if (data[0] != 'Z' || data[1] != 'Z') { ESP_LOGE(TAG, "Failed to get content length");
ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0], data[1]); return false;
return false; }
}
if (content_length > partition_->size) {
ptr = static_cast<void*>(const_cast<char*>(data + 2)); ESP_LOGE(TAG, "Assets file size (%u) is larger than partition size (%lu)", content_length, partition_->size);
size = asset->second.size; return false;
return true; }
}
// 定义扇区大小为4KBESP32的标准扇区大小
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<void*>(const_cast<char*>(data + 2));
size = asset->second.size;
return true;
}

View File

@@ -1,65 +1,52 @@
#ifndef ASSETS_H #ifndef ASSETS_H
#define ASSETS_H #define ASSETS_H
#include <map> #include <map>
#include <string> #include <string>
#include <functional> #include <functional>
#include <cJSON.h> #include <cJSON.h>
#include <esp_partition.h> #include <esp_partition.h>
#include <model_path.h> #include <model_path.h>
// All combinations of wakenet_model, text_font, emoji_collection can be found from the following url: struct Asset {
// https://github.com/78/xiaozhi-fonts/releases/tag/assets size_t size;
size_t offset;
#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" class Assets {
#define ASSETS_XIAOZHI_PUHUI_COMMON_14_1 "wn9_nihaoxiaozhi_tts-font_puhui_common_14_1-none.bin" public:
#define ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32 "wn9_nihaoxiaozhi_tts-font_puhui_common_16_4-emojis_32.bin" static Assets& GetInstance() {
#define ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64 "wn9_nihaoxiaozhi_tts-font_puhui_common_16_4-emojis_64.bin" static Assets instance;
#define ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64 "wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-emojis_64.bin" return instance;
#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" ~Assets();
#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" bool Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback);
#define ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_64 "wn9s_nihaoxiaozhi-font_puhui_common_20_4-emojis_64.bin" bool Apply();
#define ASSETS_XIAOZHI_S_PUHUI_COMMON_30_4_EMOJI_64 "wn9s_nihaoxiaozhi-font_puhui_common_30_4-emojis_64.bin" bool GetAssetData(const std::string& name, void*& ptr, size_t& size);
struct Asset { inline bool partition_valid() const { return partition_valid_; }
size_t size; inline bool checksum_valid() const { return checksum_valid_; }
size_t offset; inline std::string default_assets_url() const { return default_assets_url_; }
};
private:
class Assets { Assets();
public: Assets(const Assets&) = delete;
Assets(std::string default_assets_url); Assets& operator=(const Assets&) = delete;
~Assets();
bool InitializePartition();
bool Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback); uint32_t CalculateChecksum(const char* data, uint32_t length);
bool Apply();
const esp_partition_t* partition_ = nullptr;
inline bool partition_valid() const { return partition_valid_; } esp_partition_mmap_handle_t mmap_handle_ = 0;
inline bool checksum_valid() const { return checksum_valid_; } const char* mmap_root_ = nullptr;
inline std::string default_assets_url() const { return default_assets_url_; } bool partition_valid_ = false;
bool checksum_valid_ = false;
private: std::string default_assets_url_;
Assets(const Assets&) = delete; srmodel_list_t* models_list_ = nullptr;
Assets& operator=(const Assets&) = delete; std::map<std::string, Asset> assets_;
};
bool InitializePartition();
uint32_t CalculateChecksum(const char* data, uint32_t length); #endif
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<std::string, Asset> assets_;
};
#endif

View File

@@ -1,218 +1,218 @@
// Auto-generated language config // Auto-generated language config
// Language: zh-CN with en-US fallback // Language: en-US with en-US fallback
#pragma once #pragma once
#include <string_view> #include <string_view>
#ifndef zh_cn #ifndef en_us
#define zh_cn // 預設語言 #define en_us // 預設語言
#endif #endif
namespace Lang { namespace Lang {
// 语言元数据 // 语言元数据
constexpr const char* CODE = "zh-CN"; constexpr const char* CODE = "en-US";
// 字符串资源 (en-US as fallback for missing keys) // 字符串资源 (en-US as fallback for missing keys)
namespace Strings { namespace Strings {
constexpr const char* ACCESS_VIA_BROWSER = ",浏览器访问 "; constexpr const char* ACCESS_VIA_BROWSER = " Config URL: ";
constexpr const char* ACTIVATION = "激活设备"; constexpr const char* ACTIVATION = "Activation";
constexpr const char* BATTERY_CHARGING = "正在充电"; constexpr const char* BATTERY_CHARGING = "Charging";
constexpr const char* BATTERY_FULL = "电量已满"; constexpr const char* BATTERY_FULL = "Battery full";
constexpr const char* BATTERY_LOW = "电量不足"; constexpr const char* BATTERY_LOW = "Low battery";
constexpr const char* BATTERY_NEED_CHARGE = "电量低,请充电"; constexpr const char* BATTERY_NEED_CHARGE = "Low battery, please charge";
constexpr const char* CHECKING_NEW_VERSION = "检查新版本..."; constexpr const char* CHECKING_NEW_VERSION = "Checking for new version...";
constexpr const char* CHECK_NEW_VERSION_FAILED = "检查新版本失败,将在 %d 秒后重试:%s"; constexpr const char* CHECK_NEW_VERSION_FAILED = "Check for new version failed, will retry in %d seconds: %s";
constexpr const char* CONNECTED_TO = "已连接 "; constexpr const char* CONNECTED_TO = "Connected to ";
constexpr const char* CONNECTING = "连接中..."; constexpr const char* CONNECTING = "Connecting...";
constexpr const char* CONNECTION_SUCCESSFUL = "Connection Successful"; constexpr const char* CONNECTION_SUCCESSFUL = "Connection Successful";
constexpr const char* CONNECT_TO = "连接 "; constexpr const char* CONNECT_TO = "Connect to ";
constexpr const char* CONNECT_TO_HOTSPOT = "手机连接热点 "; constexpr const char* CONNECT_TO_HOTSPOT = "Hotspot: ";
constexpr const char* DETECTING_MODULE = "检测模组..."; constexpr const char* DETECTING_MODULE = "Detecting module...";
constexpr const char* DOWNLOAD_ASSETS_FAILED = "下载资源失败"; constexpr const char* DOWNLOAD_ASSETS_FAILED = "Failed to download assets";
constexpr const char* ENTERING_WIFI_CONFIG_MODE = "进入配网模式..."; constexpr const char* ENTERING_WIFI_CONFIG_MODE = "Entering Wi-Fi configuration mode...";
constexpr const char* ERROR = "错误"; constexpr const char* ERROR = "Error";
constexpr const char* FOUND_NEW_ASSETS = "发现新资源: %s"; constexpr const char* FOUND_NEW_ASSETS = "Found new assets: %s";
constexpr const char* HELLO_MY_FRIEND = "你好,我的朋友!"; constexpr const char* HELLO_MY_FRIEND = "Hello, my friend!";
constexpr const char* INFO = "信息"; constexpr const char* INFO = "Information";
constexpr const char* INITIALIZING = "正在初始化..."; constexpr const char* INITIALIZING = "Initializing...";
constexpr const char* LISTENING = "聆听中..."; constexpr const char* LISTENING = "Listening...";
constexpr const char* LOADING_ASSETS = "加载资源..."; constexpr const char* LOADING_ASSETS = "Loading assets...";
constexpr const char* LOADING_PROTOCOL = "登录服务器..."; constexpr const char* LOADING_PROTOCOL = "Logging in...";
constexpr const char* MAX_VOLUME = "最大音量"; constexpr const char* MAX_VOLUME = "Max volume";
constexpr const char* MUTED = "已静音"; constexpr const char* MUTED = "Muted";
constexpr const char* NEW_VERSION = "新版本 "; constexpr const char* NEW_VERSION = "New version ";
constexpr const char* OTA_UPGRADE = "OTA 升级"; constexpr const char* OTA_UPGRADE = "OTA Upgrade";
constexpr const char* PIN_ERROR = "请插入 SIM "; constexpr const char* PIN_ERROR = "Please insert SIM card";
constexpr const char* PLEASE_WAIT = "请稍候..."; constexpr const char* PLEASE_WAIT = "Please wait...";
constexpr const char* REGISTERING_NETWORK = "等待网络..."; constexpr const char* REGISTERING_NETWORK = "Waiting for network...";
constexpr const char* REG_ERROR = "无法接入网络,请检查流量卡状态"; constexpr const char* REG_ERROR = "Unable to access network, please check SIM card status";
constexpr const char* RTC_MODE_OFF = "AEC 关闭"; constexpr const char* RTC_MODE_OFF = "AEC Off";
constexpr const char* RTC_MODE_ON = "AEC 开启"; constexpr const char* RTC_MODE_ON = "AEC On";
constexpr const char* SCANNING_WIFI = "扫描 Wi-Fi..."; constexpr const char* SCANNING_WIFI = "Scanning Wi-Fi...";
constexpr const char* SERVER_ERROR = "发送失败,请检查网络"; constexpr const char* SERVER_ERROR = "Sending failed, please check the network";
constexpr const char* SERVER_NOT_CONNECTED = "无法连接服务,请稍后再试"; constexpr const char* SERVER_NOT_CONNECTED = "Unable to connect to service, please try again later";
constexpr const char* SERVER_NOT_FOUND = "正在寻找可用服务"; constexpr const char* SERVER_NOT_FOUND = "Looking for available service";
constexpr const char* SERVER_TIMEOUT = "等待响应超时"; constexpr const char* SERVER_TIMEOUT = "Waiting for response timeout";
constexpr const char* SPEAKING = "说话中..."; constexpr const char* SPEAKING = "Speaking...";
constexpr const char* STANDBY = "待命"; constexpr const char* STANDBY = "Standby";
constexpr const char* SWITCH_TO_4G_NETWORK = "切换到 4G..."; constexpr const char* SWITCH_TO_4G_NETWORK = "Switching to 4G...";
constexpr const char* SWITCH_TO_WIFI_NETWORK = "切换到 Wi-Fi..."; constexpr const char* SWITCH_TO_WIFI_NETWORK = "Switching to Wi-Fi...";
constexpr const char* UPGRADE_FAILED = "升级失败"; constexpr const char* UPGRADE_FAILED = "Upgrade failed";
constexpr const char* UPGRADING = "正在升级系统..."; constexpr const char* UPGRADING = "System is upgrading...";
constexpr const char* VERSION = "版本 "; constexpr const char* VERSION = "Ver ";
constexpr const char* VOLUME = "音量 "; constexpr const char* VOLUME = "Volume ";
constexpr const char* WARNING = "警告"; constexpr const char* WARNING = "Warning";
constexpr const char* WIFI_CONFIG_MODE = "配网模式"; constexpr const char* WIFI_CONFIG_MODE = "Wi-Fi Configuration Mode";
} }
// 音效资源 (en-US as fallback for missing audio files) // 音效资源 (en-US as fallback for missing audio files)
namespace Sounds { namespace Sounds {
extern const char ogg_0_start[] asm("_binary_0_ogg_start"); extern const char ogg_0_start[] asm("_binary_0_ogg_start");
extern const char ogg_0_end[] asm("_binary_0_ogg_end"); extern const char ogg_0_end[] asm("_binary_0_ogg_end");
static const std::string_view OGG_0 { static const std::string_view OGG_0 {
static_cast<const char*>(ogg_0_start), static_cast<const char*>(ogg_0_start),
static_cast<size_t>(ogg_0_end - ogg_0_start) static_cast<size_t>(ogg_0_end - ogg_0_start)
}; };
extern const char ogg_1_start[] asm("_binary_1_ogg_start"); extern const char ogg_1_start[] asm("_binary_1_ogg_start");
extern const char ogg_1_end[] asm("_binary_1_ogg_end"); extern const char ogg_1_end[] asm("_binary_1_ogg_end");
static const std::string_view OGG_1 { static const std::string_view OGG_1 {
static_cast<const char*>(ogg_1_start), static_cast<const char*>(ogg_1_start),
static_cast<size_t>(ogg_1_end - ogg_1_start) static_cast<size_t>(ogg_1_end - ogg_1_start)
}; };
extern const char ogg_2_start[] asm("_binary_2_ogg_start"); extern const char ogg_2_start[] asm("_binary_2_ogg_start");
extern const char ogg_2_end[] asm("_binary_2_ogg_end"); extern const char ogg_2_end[] asm("_binary_2_ogg_end");
static const std::string_view OGG_2 { static const std::string_view OGG_2 {
static_cast<const char*>(ogg_2_start), static_cast<const char*>(ogg_2_start),
static_cast<size_t>(ogg_2_end - ogg_2_start) static_cast<size_t>(ogg_2_end - ogg_2_start)
}; };
extern const char ogg_3_start[] asm("_binary_3_ogg_start"); extern const char ogg_3_start[] asm("_binary_3_ogg_start");
extern const char ogg_3_end[] asm("_binary_3_ogg_end"); extern const char ogg_3_end[] asm("_binary_3_ogg_end");
static const std::string_view OGG_3 { static const std::string_view OGG_3 {
static_cast<const char*>(ogg_3_start), static_cast<const char*>(ogg_3_start),
static_cast<size_t>(ogg_3_end - ogg_3_start) static_cast<size_t>(ogg_3_end - ogg_3_start)
}; };
extern const char ogg_4_start[] asm("_binary_4_ogg_start"); extern const char ogg_4_start[] asm("_binary_4_ogg_start");
extern const char ogg_4_end[] asm("_binary_4_ogg_end"); extern const char ogg_4_end[] asm("_binary_4_ogg_end");
static const std::string_view OGG_4 { static const std::string_view OGG_4 {
static_cast<const char*>(ogg_4_start), static_cast<const char*>(ogg_4_start),
static_cast<size_t>(ogg_4_end - ogg_4_start) static_cast<size_t>(ogg_4_end - ogg_4_start)
}; };
extern const char ogg_5_start[] asm("_binary_5_ogg_start"); extern const char ogg_5_start[] asm("_binary_5_ogg_start");
extern const char ogg_5_end[] asm("_binary_5_ogg_end"); extern const char ogg_5_end[] asm("_binary_5_ogg_end");
static const std::string_view OGG_5 { static const std::string_view OGG_5 {
static_cast<const char*>(ogg_5_start), static_cast<const char*>(ogg_5_start),
static_cast<size_t>(ogg_5_end - ogg_5_start) static_cast<size_t>(ogg_5_end - ogg_5_start)
}; };
extern const char ogg_6_start[] asm("_binary_6_ogg_start"); extern const char ogg_6_start[] asm("_binary_6_ogg_start");
extern const char ogg_6_end[] asm("_binary_6_ogg_end"); extern const char ogg_6_end[] asm("_binary_6_ogg_end");
static const std::string_view OGG_6 { static const std::string_view OGG_6 {
static_cast<const char*>(ogg_6_start), static_cast<const char*>(ogg_6_start),
static_cast<size_t>(ogg_6_end - ogg_6_start) static_cast<size_t>(ogg_6_end - ogg_6_start)
}; };
extern const char ogg_7_start[] asm("_binary_7_ogg_start"); extern const char ogg_7_start[] asm("_binary_7_ogg_start");
extern const char ogg_7_end[] asm("_binary_7_ogg_end"); extern const char ogg_7_end[] asm("_binary_7_ogg_end");
static const std::string_view OGG_7 { static const std::string_view OGG_7 {
static_cast<const char*>(ogg_7_start), static_cast<const char*>(ogg_7_start),
static_cast<size_t>(ogg_7_end - ogg_7_start) static_cast<size_t>(ogg_7_end - ogg_7_start)
}; };
extern const char ogg_8_start[] asm("_binary_8_ogg_start"); extern const char ogg_8_start[] asm("_binary_8_ogg_start");
extern const char ogg_8_end[] asm("_binary_8_ogg_end"); extern const char ogg_8_end[] asm("_binary_8_ogg_end");
static const std::string_view OGG_8 { static const std::string_view OGG_8 {
static_cast<const char*>(ogg_8_start), static_cast<const char*>(ogg_8_start),
static_cast<size_t>(ogg_8_end - ogg_8_start) static_cast<size_t>(ogg_8_end - ogg_8_start)
}; };
extern const char ogg_9_start[] asm("_binary_9_ogg_start"); extern const char ogg_9_start[] asm("_binary_9_ogg_start");
extern const char ogg_9_end[] asm("_binary_9_ogg_end"); extern const char ogg_9_end[] asm("_binary_9_ogg_end");
static const std::string_view OGG_9 { static const std::string_view OGG_9 {
static_cast<const char*>(ogg_9_start), static_cast<const char*>(ogg_9_start),
static_cast<size_t>(ogg_9_end - ogg_9_start) static_cast<size_t>(ogg_9_end - ogg_9_start)
}; };
extern const char ogg_activation_start[] asm("_binary_activation_ogg_start"); extern const char ogg_activation_start[] asm("_binary_activation_ogg_start");
extern const char ogg_activation_end[] asm("_binary_activation_ogg_end"); extern const char ogg_activation_end[] asm("_binary_activation_ogg_end");
static const std::string_view OGG_ACTIVATION { static const std::string_view OGG_ACTIVATION {
static_cast<const char*>(ogg_activation_start), static_cast<const char*>(ogg_activation_start),
static_cast<size_t>(ogg_activation_end - ogg_activation_start) static_cast<size_t>(ogg_activation_end - ogg_activation_start)
}; };
extern const char ogg_err_pin_start[] asm("_binary_err_pin_ogg_start"); extern const char ogg_err_pin_start[] asm("_binary_err_pin_ogg_start");
extern const char ogg_err_pin_end[] asm("_binary_err_pin_ogg_end"); extern const char ogg_err_pin_end[] asm("_binary_err_pin_ogg_end");
static const std::string_view OGG_ERR_PIN { static const std::string_view OGG_ERR_PIN {
static_cast<const char*>(ogg_err_pin_start), static_cast<const char*>(ogg_err_pin_start),
static_cast<size_t>(ogg_err_pin_end - ogg_err_pin_start) static_cast<size_t>(ogg_err_pin_end - ogg_err_pin_start)
}; };
extern const char ogg_err_reg_start[] asm("_binary_err_reg_ogg_start"); extern const char ogg_err_reg_start[] asm("_binary_err_reg_ogg_start");
extern const char ogg_err_reg_end[] asm("_binary_err_reg_ogg_end"); extern const char ogg_err_reg_end[] asm("_binary_err_reg_ogg_end");
static const std::string_view OGG_ERR_REG { static const std::string_view OGG_ERR_REG {
static_cast<const char*>(ogg_err_reg_start), static_cast<const char*>(ogg_err_reg_start),
static_cast<size_t>(ogg_err_reg_end - ogg_err_reg_start) static_cast<size_t>(ogg_err_reg_end - ogg_err_reg_start)
}; };
extern const char ogg_exclamation_start[] asm("_binary_exclamation_ogg_start"); extern const char ogg_exclamation_start[] asm("_binary_exclamation_ogg_start");
extern const char ogg_exclamation_end[] asm("_binary_exclamation_ogg_end"); extern const char ogg_exclamation_end[] asm("_binary_exclamation_ogg_end");
static const std::string_view OGG_EXCLAMATION { static const std::string_view OGG_EXCLAMATION {
static_cast<const char*>(ogg_exclamation_start), static_cast<const char*>(ogg_exclamation_start),
static_cast<size_t>(ogg_exclamation_end - ogg_exclamation_start) static_cast<size_t>(ogg_exclamation_end - ogg_exclamation_start)
}; };
extern const char ogg_low_battery_start[] asm("_binary_low_battery_ogg_start"); extern const char ogg_low_battery_start[] asm("_binary_low_battery_ogg_start");
extern const char ogg_low_battery_end[] asm("_binary_low_battery_ogg_end"); extern const char ogg_low_battery_end[] asm("_binary_low_battery_ogg_end");
static const std::string_view OGG_LOW_BATTERY { static const std::string_view OGG_LOW_BATTERY {
static_cast<const char*>(ogg_low_battery_start), static_cast<const char*>(ogg_low_battery_start),
static_cast<size_t>(ogg_low_battery_end - ogg_low_battery_start) static_cast<size_t>(ogg_low_battery_end - ogg_low_battery_start)
}; };
extern const char ogg_popup_start[] asm("_binary_popup_ogg_start"); extern const char ogg_popup_start[] asm("_binary_popup_ogg_start");
extern const char ogg_popup_end[] asm("_binary_popup_ogg_end"); extern const char ogg_popup_end[] asm("_binary_popup_ogg_end");
static const std::string_view OGG_POPUP { static const std::string_view OGG_POPUP {
static_cast<const char*>(ogg_popup_start), static_cast<const char*>(ogg_popup_start),
static_cast<size_t>(ogg_popup_end - ogg_popup_start) static_cast<size_t>(ogg_popup_end - ogg_popup_start)
}; };
extern const char ogg_success_start[] asm("_binary_success_ogg_start"); extern const char ogg_success_start[] asm("_binary_success_ogg_start");
extern const char ogg_success_end[] asm("_binary_success_ogg_end"); extern const char ogg_success_end[] asm("_binary_success_ogg_end");
static const std::string_view OGG_SUCCESS { static const std::string_view OGG_SUCCESS {
static_cast<const char*>(ogg_success_start), static_cast<const char*>(ogg_success_start),
static_cast<size_t>(ogg_success_end - ogg_success_start) static_cast<size_t>(ogg_success_end - ogg_success_start)
}; };
extern const char ogg_upgrade_start[] asm("_binary_upgrade_ogg_start"); extern const char ogg_upgrade_start[] asm("_binary_upgrade_ogg_start");
extern const char ogg_upgrade_end[] asm("_binary_upgrade_ogg_end"); extern const char ogg_upgrade_end[] asm("_binary_upgrade_ogg_end");
static const std::string_view OGG_UPGRADE { static const std::string_view OGG_UPGRADE {
static_cast<const char*>(ogg_upgrade_start), static_cast<const char*>(ogg_upgrade_start),
static_cast<size_t>(ogg_upgrade_end - ogg_upgrade_start) static_cast<size_t>(ogg_upgrade_end - ogg_upgrade_start)
}; };
extern const char ogg_vibration_start[] asm("_binary_vibration_ogg_start"); extern const char ogg_vibration_start[] asm("_binary_vibration_ogg_start");
extern const char ogg_vibration_end[] asm("_binary_vibration_ogg_end"); extern const char ogg_vibration_end[] asm("_binary_vibration_ogg_end");
static const std::string_view OGG_VIBRATION { static const std::string_view OGG_VIBRATION {
static_cast<const char*>(ogg_vibration_start), static_cast<const char*>(ogg_vibration_start),
static_cast<size_t>(ogg_vibration_end - ogg_vibration_start) static_cast<size_t>(ogg_vibration_end - ogg_vibration_start)
}; };
extern const char ogg_welcome_start[] asm("_binary_welcome_ogg_start"); extern const char ogg_welcome_start[] asm("_binary_welcome_ogg_start");
extern const char ogg_welcome_end[] asm("_binary_welcome_ogg_end"); extern const char ogg_welcome_end[] asm("_binary_welcome_ogg_end");
static const std::string_view OGG_WELCOME { static const std::string_view OGG_WELCOME {
static_cast<const char*>(ogg_welcome_start), static_cast<const char*>(ogg_welcome_start),
static_cast<size_t>(ogg_welcome_end - ogg_welcome_start) static_cast<size_t>(ogg_welcome_end - ogg_welcome_start)
}; };
extern const char ogg_wificonfig_start[] asm("_binary_wificonfig_ogg_start"); extern const char ogg_wificonfig_start[] asm("_binary_wificonfig_ogg_start");
extern const char ogg_wificonfig_end[] asm("_binary_wificonfig_ogg_end"); extern const char ogg_wificonfig_end[] asm("_binary_wificonfig_ogg_end");
static const std::string_view OGG_WIFICONFIG { static const std::string_view OGG_WIFICONFIG {
static_cast<const char*>(ogg_wificonfig_start), static_cast<const char*>(ogg_wificonfig_start),
static_cast<size_t>(ogg_wificonfig_end - ogg_wificonfig_start) static_cast<size_t>(ogg_wificonfig_end - ogg_wificonfig_start)
}; };
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "ar-SA" "type": "ar-SA"
}, },
"strings": { "strings": {
"WARNING": "تحذير", "WARNING": "تحذير",
"INFO": "معلومات", "INFO": "معلومات",
"ERROR": "خطأ", "ERROR": "خطأ",
"VERSION": "الإصدار ", "VERSION": "الإصدار ",
"LOADING_PROTOCOL": "الاتصال بالخادم...", "LOADING_PROTOCOL": "الاتصال بالخادم...",
"INITIALIZING": "التهيئة...", "INITIALIZING": "التهيئة...",
"PIN_ERROR": "يرجى إدخال بطاقة SIM", "PIN_ERROR": "يرجى إدخال بطاقة SIM",
"REG_ERROR": "لا يمكن الوصول إلى الشبكة، يرجى التحقق من حالة بطاقة البيانات", "REG_ERROR": "لا يمكن الوصول إلى الشبكة، يرجى التحقق من حالة بطاقة البيانات",
"DETECTING_MODULE": "اكتشاف الوحدة...", "DETECTING_MODULE": "اكتشاف الوحدة...",
"REGISTERING_NETWORK": "انتظار الشبكة...", "REGISTERING_NETWORK": "انتظار الشبكة...",
"CHECKING_NEW_VERSION": "فحص الإصدار الجديد...", "CHECKING_NEW_VERSION": "فحص الإصدار الجديد...",
"CHECK_NEW_VERSION_FAILED": "فشل فحص الإصدار الجديد، سيتم المحاولة خلال %d ثانية: %s", "CHECK_NEW_VERSION_FAILED": "فشل فحص الإصدار الجديد، سيتم المحاولة خلال %d ثانية: %s",
"SWITCH_TO_WIFI_NETWORK": "التبديل إلى Wi-Fi...", "SWITCH_TO_WIFI_NETWORK": "التبديل إلى Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "التبديل إلى 4G...", "SWITCH_TO_4G_NETWORK": "التبديل إلى 4G...",
"STANDBY": "في الانتظار", "STANDBY": "في الانتظار",
"CONNECT_TO": "الاتصال بـ ", "CONNECT_TO": "الاتصال بـ ",
"CONNECTING": "جاري الاتصال...", "CONNECTING": "جاري الاتصال...",
"CONNECTED_TO": "متصل بـ ", "CONNECTED_TO": "متصل بـ ",
"LISTENING": "الاستماع...", "LISTENING": "الاستماع...",
"SPEAKING": "التحدث...", "SPEAKING": "التحدث...",
"SERVER_NOT_FOUND": "البحث عن خدمة متاحة", "SERVER_NOT_FOUND": "البحث عن خدمة متاحة",
"SERVER_NOT_CONNECTED": "لا يمكن الاتصال بالخدمة، يرجى المحاولة لاحقاً", "SERVER_NOT_CONNECTED": "لا يمكن الاتصال بالخدمة، يرجى المحاولة لاحقاً",
"SERVER_TIMEOUT": "انتهت مهلة الاستجابة", "SERVER_TIMEOUT": "انتهت مهلة الاستجابة",
"SERVER_ERROR": "فشل الإرسال، يرجى التحقق من الشبكة", "SERVER_ERROR": "فشل الإرسال، يرجى التحقق من الشبكة",
"CONNECT_TO_HOTSPOT": "اتصل الهاتف بنقطة الاتصال ", "CONNECT_TO_HOTSPOT": "اتصل الهاتف بنقطة الاتصال ",
"ACCESS_VIA_BROWSER": "،الوصول عبر المتصفح ", "ACCESS_VIA_BROWSER": "،الوصول عبر المتصفح ",
"WIFI_CONFIG_MODE": "وضع تكوين الشبكة", "WIFI_CONFIG_MODE": "وضع تكوين الشبكة",
"ENTERING_WIFI_CONFIG_MODE": "الدخول في وضع تكوين الشبكة...", "ENTERING_WIFI_CONFIG_MODE": "الدخول في وضع تكوين الشبكة...",
"SCANNING_WIFI": "فحص Wi-Fi...", "SCANNING_WIFI": "فحص Wi-Fi...",
"NEW_VERSION": "إصدار جديد ", "NEW_VERSION": "إصدار جديد ",
"OTA_UPGRADE": "تحديث OTA", "OTA_UPGRADE": "تحديث OTA",
"UPGRADING": "تحديث النظام...", "UPGRADING": "تحديث النظام...",
"UPGRADE_FAILED": "فشل التحديث", "UPGRADE_FAILED": "فشل التحديث",
"ACTIVATION": "تفعيل الجهاز", "ACTIVATION": "تفعيل الجهاز",
"BATTERY_LOW": "البطارية منخفضة", "BATTERY_LOW": "البطارية منخفضة",
"BATTERY_CHARGING": "جاري الشحن", "BATTERY_CHARGING": "جاري الشحن",
"BATTERY_FULL": "البطارية ممتلئة", "BATTERY_FULL": "البطارية ممتلئة",
"BATTERY_NEED_CHARGE": "البطارية منخفضة، يرجى الشحن", "BATTERY_NEED_CHARGE": "البطارية منخفضة، يرجى الشحن",
"VOLUME": "الصوت ", "VOLUME": "الصوت ",
"MUTED": "صامت", "MUTED": "صامت",
"MAX_VOLUME": "أقصى صوت", "MAX_VOLUME": "أقصى صوت",
"RTC_MODE_OFF": "AEC مُوقف", "RTC_MODE_OFF": "AEC مُوقف",
"RTC_MODE_ON": "AEC مُشغل", "RTC_MODE_ON": "AEC مُشغل",
"DOWNLOAD_ASSETS_FAILED": "فشل في تنزيل الموارد", "DOWNLOAD_ASSETS_FAILED": "فشل في تنزيل الموارد",
"LOADING_ASSETS": "جاري تحميل الموارد...", "LOADING_ASSETS": "جاري تحميل الموارد...",
"PLEASE_WAIT": "يرجى الانتظار...", "PLEASE_WAIT": "يرجى الانتظار...",
"FOUND_NEW_ASSETS": "تم العثور على موارد جديدة: %s", "FOUND_NEW_ASSETS": "تم العثور على موارد جديدة: %s",
"HELLO_MY_FRIEND": "مرحباً، صديقي!" "HELLO_MY_FRIEND": "مرحباً، صديقي!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "cs-CZ" "type": "cs-CZ"
}, },
"strings": { "strings": {
"WARNING": "Varování", "WARNING": "Varování",
"INFO": "Informace", "INFO": "Informace",
"ERROR": "Chyba", "ERROR": "Chyba",
"VERSION": "Verze ", "VERSION": "Verze ",
"LOADING_PROTOCOL": "Připojování k serveru...", "LOADING_PROTOCOL": "Připojování k serveru...",
"INITIALIZING": "Inicializace...", "INITIALIZING": "Inicializace...",
"PIN_ERROR": "Prosím vložte SIM kartu", "PIN_ERROR": "Prosím vložte SIM kartu",
"REG_ERROR": "Nelze se připojit k síti, zkontrolujte stav datové karty", "REG_ERROR": "Nelze se připojit k síti, zkontrolujte stav datové karty",
"DETECTING_MODULE": "Detekce modulu...", "DETECTING_MODULE": "Detekce modulu...",
"REGISTERING_NETWORK": "Čekání na síť...", "REGISTERING_NETWORK": "Čekání na síť...",
"CHECKING_NEW_VERSION": "Kontrola nové verze...", "CHECKING_NEW_VERSION": "Kontrola nové verze...",
"CHECK_NEW_VERSION_FAILED": "Kontrola nové verze selhala, opakování za %d sekund: %s", "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_WIFI_NETWORK": "Přepínání na Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Přepínání na 4G...", "SWITCH_TO_4G_NETWORK": "Přepínání na 4G...",
"STANDBY": "Pohotovost", "STANDBY": "Pohotovost",
"CONNECT_TO": "Připojit k ", "CONNECT_TO": "Připojit k ",
"CONNECTING": "Připojování...", "CONNECTING": "Připojování...",
"CONNECTED_TO": "Připojeno k ", "CONNECTED_TO": "Připojeno k ",
"LISTENING": "Naslouchání...", "LISTENING": "Naslouchání...",
"SPEAKING": "Mluvení...", "SPEAKING": "Mluvení...",
"SERVER_NOT_FOUND": "Hledání dostupné služby", "SERVER_NOT_FOUND": "Hledání dostupné služby",
"SERVER_NOT_CONNECTED": "Nelze se připojit ke službě, zkuste to později", "SERVER_NOT_CONNECTED": "Nelze se připojit ke službě, zkuste to později",
"SERVER_TIMEOUT": "Čas odpovědi vypršel", "SERVER_TIMEOUT": "Čas odpovědi vypršel",
"SERVER_ERROR": "Odeslání selhalo, zkontrolujte síť", "SERVER_ERROR": "Odeslání selhalo, zkontrolujte síť",
"CONNECT_TO_HOTSPOT": "Připojte telefon k hotspotu ", "CONNECT_TO_HOTSPOT": "Připojte telefon k hotspotu ",
"ACCESS_VIA_BROWSER": "přístup přes prohlížeč ", "ACCESS_VIA_BROWSER": "přístup přes prohlížeč ",
"WIFI_CONFIG_MODE": "Režim konfigurace sítě", "WIFI_CONFIG_MODE": "Režim konfigurace sítě",
"ENTERING_WIFI_CONFIG_MODE": "Vstup do režimu konfigurace sítě...", "ENTERING_WIFI_CONFIG_MODE": "Vstup do režimu konfigurace sítě...",
"SCANNING_WIFI": "Skenování Wi-Fi...", "SCANNING_WIFI": "Skenování Wi-Fi...",
"NEW_VERSION": "Nová verze ", "NEW_VERSION": "Nová verze ",
"OTA_UPGRADE": "OTA upgrade", "OTA_UPGRADE": "OTA upgrade",
"UPGRADING": "Aktualizace systému...", "UPGRADING": "Aktualizace systému...",
"UPGRADE_FAILED": "Upgrade selhal", "UPGRADE_FAILED": "Upgrade selhal",
"ACTIVATION": "Aktivace zařízení", "ACTIVATION": "Aktivace zařízení",
"BATTERY_LOW": "Slabá baterie", "BATTERY_LOW": "Slabá baterie",
"BATTERY_CHARGING": "Nabíjení", "BATTERY_CHARGING": "Nabíjení",
"BATTERY_FULL": "Baterie plná", "BATTERY_FULL": "Baterie plná",
"BATTERY_NEED_CHARGE": "Slabá baterie, prosím nabijte", "BATTERY_NEED_CHARGE": "Slabá baterie, prosím nabijte",
"VOLUME": "Hlasitost ", "VOLUME": "Hlasitost ",
"MUTED": "Ztlumeno", "MUTED": "Ztlumeno",
"MAX_VOLUME": "Maximální hlasitost", "MAX_VOLUME": "Maximální hlasitost",
"RTC_MODE_OFF": "AEC vypnuto", "RTC_MODE_OFF": "AEC vypnuto",
"RTC_MODE_ON": "AEC zapnuto", "RTC_MODE_ON": "AEC zapnuto",
"DOWNLOAD_ASSETS_FAILED": "Nepodařilo se stáhnout prostředky", "DOWNLOAD_ASSETS_FAILED": "Nepodařilo se stáhnout prostředky",
"LOADING_ASSETS": "Načítání prostředků...", "LOADING_ASSETS": "Načítání prostředků...",
"PLEASE_WAIT": "Prosím čekejte...", "PLEASE_WAIT": "Prosím čekejte...",
"FOUND_NEW_ASSETS": "Nalezeny nové prostředky: %s", "FOUND_NEW_ASSETS": "Nalezeny nové prostředky: %s",
"HELLO_MY_FRIEND": "Ahoj, můj příteli!" "HELLO_MY_FRIEND": "Ahoj, můj příteli!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "de-DE" "type": "de-DE"
}, },
"strings": { "strings": {
"WARNING": "Warnung", "WARNING": "Warnung",
"INFO": "Information", "INFO": "Information",
"ERROR": "Fehler", "ERROR": "Fehler",
"VERSION": "Version ", "VERSION": "Version ",
"LOADING_PROTOCOL": "Verbindung zum Server...", "LOADING_PROTOCOL": "Verbindung zum Server...",
"INITIALIZING": "Initialisierung...", "INITIALIZING": "Initialisierung...",
"PIN_ERROR": "Bitte SIM-Karte einlegen", "PIN_ERROR": "Bitte SIM-Karte einlegen",
"REG_ERROR": "Netzwerkverbindung fehlgeschlagen, bitte Datenkartenstatus prüfen", "REG_ERROR": "Netzwerkverbindung fehlgeschlagen, bitte Datenkartenstatus prüfen",
"DETECTING_MODULE": "Modul erkennen...", "DETECTING_MODULE": "Modul erkennen...",
"REGISTERING_NETWORK": "Auf Netzwerk warten...", "REGISTERING_NETWORK": "Auf Netzwerk warten...",
"CHECKING_NEW_VERSION": "Neue Version prüfen...", "CHECKING_NEW_VERSION": "Neue Version prüfen...",
"CHECK_NEW_VERSION_FAILED": "Neue Version prüfen fehlgeschlagen, Wiederholung in %d Sekunden: %s", "CHECK_NEW_VERSION_FAILED": "Neue Version prüfen fehlgeschlagen, Wiederholung in %d Sekunden: %s",
"SWITCH_TO_WIFI_NETWORK": "Zu Wi-Fi wechseln...", "SWITCH_TO_WIFI_NETWORK": "Zu Wi-Fi wechseln...",
"SWITCH_TO_4G_NETWORK": "Zu 4G wechseln...", "SWITCH_TO_4G_NETWORK": "Zu 4G wechseln...",
"STANDBY": "Bereitschaft", "STANDBY": "Bereitschaft",
"CONNECT_TO": "Verbinden zu ", "CONNECT_TO": "Verbinden zu ",
"CONNECTING": "Verbindung wird hergestellt...", "CONNECTING": "Verbindung wird hergestellt...",
"CONNECTED_TO": "Verbunden mit ", "CONNECTED_TO": "Verbunden mit ",
"LISTENING": "Zuhören...", "LISTENING": "Zuhören...",
"SPEAKING": "Sprechen...", "SPEAKING": "Sprechen...",
"SERVER_NOT_FOUND": "Verfügbaren Service suchen", "SERVER_NOT_FOUND": "Verfügbaren Service suchen",
"SERVER_NOT_CONNECTED": "Service-Verbindung fehlgeschlagen, bitte später versuchen", "SERVER_NOT_CONNECTED": "Service-Verbindung fehlgeschlagen, bitte später versuchen",
"SERVER_TIMEOUT": "Antwort-Timeout", "SERVER_TIMEOUT": "Antwort-Timeout",
"SERVER_ERROR": "Senden fehlgeschlagen, bitte Netzwerk prüfen", "SERVER_ERROR": "Senden fehlgeschlagen, bitte Netzwerk prüfen",
"CONNECT_TO_HOTSPOT": "Handy mit Hotspot verbinden ", "CONNECT_TO_HOTSPOT": "Handy mit Hotspot verbinden ",
"ACCESS_VIA_BROWSER": "Browser öffnen ", "ACCESS_VIA_BROWSER": "Browser öffnen ",
"WIFI_CONFIG_MODE": "Netzwerkkonfigurationsmodus", "WIFI_CONFIG_MODE": "Netzwerkkonfigurationsmodus",
"ENTERING_WIFI_CONFIG_MODE": "Netzwerkkonfigurationsmodus eingeben...", "ENTERING_WIFI_CONFIG_MODE": "Netzwerkkonfigurationsmodus eingeben...",
"SCANNING_WIFI": "Wi-Fi scannen...", "SCANNING_WIFI": "Wi-Fi scannen...",
"NEW_VERSION": "Neue Version ", "NEW_VERSION": "Neue Version ",
"OTA_UPGRADE": "OTA-Upgrade", "OTA_UPGRADE": "OTA-Upgrade",
"UPGRADING": "System wird aktualisiert...", "UPGRADING": "System wird aktualisiert...",
"UPGRADE_FAILED": "Upgrade fehlgeschlagen", "UPGRADE_FAILED": "Upgrade fehlgeschlagen",
"ACTIVATION": "Gerät aktivieren", "ACTIVATION": "Gerät aktivieren",
"BATTERY_LOW": "Niedriger Batteriestand", "BATTERY_LOW": "Niedriger Batteriestand",
"BATTERY_CHARGING": "Wird geladen", "BATTERY_CHARGING": "Wird geladen",
"BATTERY_FULL": "Batterie voll", "BATTERY_FULL": "Batterie voll",
"BATTERY_NEED_CHARGE": "Niedriger Batteriestand, bitte aufladen", "BATTERY_NEED_CHARGE": "Niedriger Batteriestand, bitte aufladen",
"VOLUME": "Lautstärke ", "VOLUME": "Lautstärke ",
"MUTED": "Stummgeschaltet", "MUTED": "Stummgeschaltet",
"MAX_VOLUME": "Maximale Lautstärke", "MAX_VOLUME": "Maximale Lautstärke",
"RTC_MODE_OFF": "AEC aus", "RTC_MODE_OFF": "AEC aus",
"RTC_MODE_ON": "AEC ein", "RTC_MODE_ON": "AEC ein",
"DOWNLOAD_ASSETS_FAILED": "Fehler beim Herunterladen der Ressourcen", "DOWNLOAD_ASSETS_FAILED": "Fehler beim Herunterladen der Ressourcen",
"LOADING_ASSETS": "Ressourcen werden geladen...", "LOADING_ASSETS": "Ressourcen werden geladen...",
"PLEASE_WAIT": "Bitte warten...", "PLEASE_WAIT": "Bitte warten...",
"FOUND_NEW_ASSETS": "Neue Ressourcen gefunden: %s", "FOUND_NEW_ASSETS": "Neue Ressourcen gefunden: %s",
"HELLO_MY_FRIEND": "Hallo, mein Freund!" "HELLO_MY_FRIEND": "Hallo, mein Freund!"
} }
} }

View File

@@ -1,56 +1,56 @@
{ {
"language": { "language": {
"type": "en-US" "type": "en-US"
}, },
"strings": { "strings": {
"WARNING": "Warning", "WARNING": "Warning",
"INFO": "Information", "INFO": "Information",
"ERROR": "Error", "ERROR": "Error",
"VERSION": "Ver ", "VERSION": "Ver ",
"LOADING_PROTOCOL": "Logging in...", "LOADING_PROTOCOL": "Logging in...",
"INITIALIZING": "Initializing...", "INITIALIZING": "Initializing...",
"PIN_ERROR": "Please insert SIM card", "PIN_ERROR": "Please insert SIM card",
"REG_ERROR": "Unable to access network, please check SIM card status", "REG_ERROR": "Unable to access network, please check SIM card status",
"DETECTING_MODULE": "Detecting module...", "DETECTING_MODULE": "Detecting module...",
"REGISTERING_NETWORK": "Waiting for network...", "REGISTERING_NETWORK": "Waiting for network...",
"CHECKING_NEW_VERSION": "Checking for new version...", "CHECKING_NEW_VERSION": "Checking for new version...",
"CHECK_NEW_VERSION_FAILED": "Check for new version failed, will retry in %d seconds: %s", "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_WIFI_NETWORK": "Switching to Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Switching to 4G...", "SWITCH_TO_4G_NETWORK": "Switching to 4G...",
"STANDBY": "Standby", "STANDBY": "Standby",
"CONNECT_TO": "Connect to ", "CONNECT_TO": "Connect to ",
"CONNECTING": "Connecting...", "CONNECTING": "Connecting...",
"CONNECTION_SUCCESSFUL": "Connection Successful", "CONNECTION_SUCCESSFUL": "Connection Successful",
"CONNECTED_TO": "Connected to ", "CONNECTED_TO": "Connected to ",
"LISTENING": "Listening...", "LISTENING": "Listening...",
"SPEAKING": "Speaking...", "SPEAKING": "Speaking...",
"SERVER_NOT_FOUND": "Looking for available service", "SERVER_NOT_FOUND": "Looking for available service",
"SERVER_NOT_CONNECTED": "Unable to connect to service, please try again later", "SERVER_NOT_CONNECTED": "Unable to connect to service, please try again later",
"SERVER_TIMEOUT": "Waiting for response timeout", "SERVER_TIMEOUT": "Waiting for response timeout",
"SERVER_ERROR": "Sending failed, please check the network", "SERVER_ERROR": "Sending failed, please check the network",
"CONNECT_TO_HOTSPOT": "Hotspot: ", "CONNECT_TO_HOTSPOT": "Hotspot: ",
"ACCESS_VIA_BROWSER": " Config URL: ", "ACCESS_VIA_BROWSER": " Config URL: ",
"WIFI_CONFIG_MODE": "Wi-Fi Configuration Mode", "WIFI_CONFIG_MODE": "Wi-Fi Configuration Mode",
"ENTERING_WIFI_CONFIG_MODE": "Entering Wi-Fi configuration mode...", "ENTERING_WIFI_CONFIG_MODE": "Entering Wi-Fi configuration mode...",
"SCANNING_WIFI": "Scanning Wi-Fi...", "SCANNING_WIFI": "Scanning Wi-Fi...",
"NEW_VERSION": "New version ", "NEW_VERSION": "New version ",
"OTA_UPGRADE": "OTA Upgrade", "OTA_UPGRADE": "OTA Upgrade",
"UPGRADING": "System is upgrading...", "UPGRADING": "System is upgrading...",
"UPGRADE_FAILED": "Upgrade failed", "UPGRADE_FAILED": "Upgrade failed",
"ACTIVATION": "Activation", "ACTIVATION": "Activation",
"BATTERY_LOW": "Low battery", "BATTERY_LOW": "Low battery",
"BATTERY_CHARGING": "Charging", "BATTERY_CHARGING": "Charging",
"BATTERY_FULL": "Battery full", "BATTERY_FULL": "Battery full",
"BATTERY_NEED_CHARGE": "Low battery, please charge", "BATTERY_NEED_CHARGE": "Low battery, please charge",
"VOLUME": "Volume ", "VOLUME": "Volume ",
"MUTED": "Muted", "MUTED": "Muted",
"MAX_VOLUME": "Max volume", "MAX_VOLUME": "Max volume",
"RTC_MODE_OFF": "AEC Off", "RTC_MODE_OFF": "AEC Off",
"RTC_MODE_ON": "AEC On", "RTC_MODE_ON": "AEC On",
"PLEASE_WAIT": "Please wait...", "PLEASE_WAIT": "Please wait...",
"FOUND_NEW_ASSETS": "Found new assets: %s", "FOUND_NEW_ASSETS": "Found new assets: %s",
"DOWNLOAD_ASSETS_FAILED": "Failed to download assets", "DOWNLOAD_ASSETS_FAILED": "Failed to download assets",
"LOADING_ASSETS": "Loading assets...", "LOADING_ASSETS": "Loading assets...",
"HELLO_MY_FRIEND": "Hello, my friend!" "HELLO_MY_FRIEND": "Hello, my friend!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "es-ES" "type": "es-ES"
}, },
"strings": { "strings": {
"WARNING": "Advertencia", "WARNING": "Advertencia",
"INFO": "Información", "INFO": "Información",
"ERROR": "Error", "ERROR": "Error",
"VERSION": "Versión ", "VERSION": "Versión ",
"LOADING_PROTOCOL": "Conectando al servidor...", "LOADING_PROTOCOL": "Conectando al servidor...",
"INITIALIZING": "Inicializando...", "INITIALIZING": "Inicializando...",
"PIN_ERROR": "Por favor inserte la tarjeta SIM", "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", "REG_ERROR": "No se puede acceder a la red, verifique el estado de la tarjeta de datos",
"DETECTING_MODULE": "Detectando módulo...", "DETECTING_MODULE": "Detectando módulo...",
"REGISTERING_NETWORK": "Esperando red...", "REGISTERING_NETWORK": "Esperando red...",
"CHECKING_NEW_VERSION": "Verificando nueva versión...", "CHECKING_NEW_VERSION": "Verificando nueva versión...",
"CHECK_NEW_VERSION_FAILED": "Error al verificar nueva versión, reintentando en %d segundos: %s", "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_WIFI_NETWORK": "Cambiando a Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Cambiando a 4G...", "SWITCH_TO_4G_NETWORK": "Cambiando a 4G...",
"STANDBY": "En espera", "STANDBY": "En espera",
"CONNECT_TO": "Conectar a ", "CONNECT_TO": "Conectar a ",
"CONNECTING": "Conectando...", "CONNECTING": "Conectando...",
"CONNECTED_TO": "Conectado a ", "CONNECTED_TO": "Conectado a ",
"LISTENING": "Escuchando...", "LISTENING": "Escuchando...",
"SPEAKING": "Hablando...", "SPEAKING": "Hablando...",
"SERVER_NOT_FOUND": "Buscando servicio disponible", "SERVER_NOT_FOUND": "Buscando servicio disponible",
"SERVER_NOT_CONNECTED": "No se puede conectar al servicio, inténtelo más tarde", "SERVER_NOT_CONNECTED": "No se puede conectar al servicio, inténtelo más tarde",
"SERVER_TIMEOUT": "Tiempo de espera agotado", "SERVER_TIMEOUT": "Tiempo de espera agotado",
"SERVER_ERROR": "Error de envío, verifique la red", "SERVER_ERROR": "Error de envío, verifique la red",
"CONNECT_TO_HOTSPOT": "Conectar teléfono al punto de acceso ", "CONNECT_TO_HOTSPOT": "Conectar teléfono al punto de acceso ",
"ACCESS_VIA_BROWSER": "acceder mediante navegador ", "ACCESS_VIA_BROWSER": "acceder mediante navegador ",
"WIFI_CONFIG_MODE": "Modo configuración de red", "WIFI_CONFIG_MODE": "Modo configuración de red",
"ENTERING_WIFI_CONFIG_MODE": "Entrando en modo configuración de red...", "ENTERING_WIFI_CONFIG_MODE": "Entrando en modo configuración de red...",
"SCANNING_WIFI": "Escaneando Wi-Fi...", "SCANNING_WIFI": "Escaneando Wi-Fi...",
"NEW_VERSION": "Nueva versión ", "NEW_VERSION": "Nueva versión ",
"OTA_UPGRADE": "Actualización OTA", "OTA_UPGRADE": "Actualización OTA",
"UPGRADING": "Actualizando sistema...", "UPGRADING": "Actualizando sistema...",
"UPGRADE_FAILED": "Actualización fallida", "UPGRADE_FAILED": "Actualización fallida",
"ACTIVATION": "Activación del dispositivo", "ACTIVATION": "Activación del dispositivo",
"BATTERY_LOW": "Batería baja", "BATTERY_LOW": "Batería baja",
"BATTERY_CHARGING": "Cargando", "BATTERY_CHARGING": "Cargando",
"BATTERY_FULL": "Batería llena", "BATTERY_FULL": "Batería llena",
"BATTERY_NEED_CHARGE": "Batería baja, por favor cargar", "BATTERY_NEED_CHARGE": "Batería baja, por favor cargar",
"VOLUME": "Volumen ", "VOLUME": "Volumen ",
"MUTED": "Silenciado", "MUTED": "Silenciado",
"MAX_VOLUME": "Volumen máximo", "MAX_VOLUME": "Volumen máximo",
"RTC_MODE_OFF": "AEC desactivado", "RTC_MODE_OFF": "AEC desactivado",
"RTC_MODE_ON": "AEC activado", "RTC_MODE_ON": "AEC activado",
"DOWNLOAD_ASSETS_FAILED": "Error al descargar recursos", "DOWNLOAD_ASSETS_FAILED": "Error al descargar recursos",
"LOADING_ASSETS": "Cargando recursos...", "LOADING_ASSETS": "Cargando recursos...",
"PLEASE_WAIT": "Por favor espere...", "PLEASE_WAIT": "Por favor espere...",
"FOUND_NEW_ASSETS": "Encontrados nuevos recursos: %s", "FOUND_NEW_ASSETS": "Encontrados nuevos recursos: %s",
"HELLO_MY_FRIEND": "¡Hola, mi amigo!" "HELLO_MY_FRIEND": "¡Hola, mi amigo!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "fi-FI" "type": "fi-FI"
}, },
"strings": { "strings": {
"WARNING": "Varoitus", "WARNING": "Varoitus",
"INFO": "Tieto", "INFO": "Tieto",
"ERROR": "Virhe", "ERROR": "Virhe",
"VERSION": "Versio ", "VERSION": "Versio ",
"LOADING_PROTOCOL": "Yhdistetään palvelimeen...", "LOADING_PROTOCOL": "Yhdistetään palvelimeen...",
"INITIALIZING": "Alustetaan...", "INITIALIZING": "Alustetaan...",
"PIN_ERROR": "Ole hyvä ja aseta SIM-kortti", "PIN_ERROR": "Ole hyvä ja aseta SIM-kortti",
"REG_ERROR": "Ei voi muodostaa yhteyttä verkkoon, tarkista datakortin tila", "REG_ERROR": "Ei voi muodostaa yhteyttä verkkoon, tarkista datakortin tila",
"DETECTING_MODULE": "Tunnistetaan moduuli...", "DETECTING_MODULE": "Tunnistetaan moduuli...",
"REGISTERING_NETWORK": "Odotetaan verkkoa...", "REGISTERING_NETWORK": "Odotetaan verkkoa...",
"CHECKING_NEW_VERSION": "Tarkistetaan uutta versiota...", "CHECKING_NEW_VERSION": "Tarkistetaan uutta versiota...",
"CHECK_NEW_VERSION_FAILED": "Uuden version tarkistus epäonnistui, yritetään uudelleen %d sekunnin kuluttua: %s", "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_WIFI_NETWORK": "Vaihdetaan Wi-Fi:hin...",
"SWITCH_TO_4G_NETWORK": "Vaihdetaan 4G:hen...", "SWITCH_TO_4G_NETWORK": "Vaihdetaan 4G:hen...",
"STANDBY": "Valmiustila", "STANDBY": "Valmiustila",
"CONNECT_TO": "Yhdistä ", "CONNECT_TO": "Yhdistä ",
"CONNECTING": "Yhdistetään...", "CONNECTING": "Yhdistetään...",
"CONNECTED_TO": "Yhdistetty ", "CONNECTED_TO": "Yhdistetty ",
"LISTENING": "Kuunnellaan...", "LISTENING": "Kuunnellaan...",
"SPEAKING": "Puhutaan...", "SPEAKING": "Puhutaan...",
"SERVER_NOT_FOUND": "Etsitään käytettävissä olevaa palvelua", "SERVER_NOT_FOUND": "Etsitään käytettävissä olevaa palvelua",
"SERVER_NOT_CONNECTED": "Ei voi yhdistää palveluun, yritä myöhemmin", "SERVER_NOT_CONNECTED": "Ei voi yhdistää palveluun, yritä myöhemmin",
"SERVER_TIMEOUT": "Vastauksen aikakatkaisu", "SERVER_TIMEOUT": "Vastauksen aikakatkaisu",
"SERVER_ERROR": "Lähetys epäonnistui, tarkista verkko", "SERVER_ERROR": "Lähetys epäonnistui, tarkista verkko",
"CONNECT_TO_HOTSPOT": "Yhdistä puhelin hotspottiin ", "CONNECT_TO_HOTSPOT": "Yhdistä puhelin hotspottiin ",
"ACCESS_VIA_BROWSER": "pääsy selaimen kautta ", "ACCESS_VIA_BROWSER": "pääsy selaimen kautta ",
"WIFI_CONFIG_MODE": "Verkon konfigurointitila", "WIFI_CONFIG_MODE": "Verkon konfigurointitila",
"ENTERING_WIFI_CONFIG_MODE": "Siirrytään verkon konfigurointitilaan...", "ENTERING_WIFI_CONFIG_MODE": "Siirrytään verkon konfigurointitilaan...",
"SCANNING_WIFI": "Skannataan Wi-Fi...", "SCANNING_WIFI": "Skannataan Wi-Fi...",
"NEW_VERSION": "Uusi versio ", "NEW_VERSION": "Uusi versio ",
"OTA_UPGRADE": "OTA-päivitys", "OTA_UPGRADE": "OTA-päivitys",
"UPGRADING": "Päivitetään järjestelmää...", "UPGRADING": "Päivitetään järjestelmää...",
"UPGRADE_FAILED": "Päivitys epäonnistui", "UPGRADE_FAILED": "Päivitys epäonnistui",
"ACTIVATION": "Laitteen aktivointi", "ACTIVATION": "Laitteen aktivointi",
"BATTERY_LOW": "Akku vähissä", "BATTERY_LOW": "Akku vähissä",
"BATTERY_CHARGING": "Ladataan", "BATTERY_CHARGING": "Ladataan",
"BATTERY_FULL": "Akku täynnä", "BATTERY_FULL": "Akku täynnä",
"BATTERY_NEED_CHARGE": "Akku vähissä, ole hyvä ja lataa", "BATTERY_NEED_CHARGE": "Akku vähissä, ole hyvä ja lataa",
"VOLUME": "Äänenvoimakkuus ", "VOLUME": "Äänenvoimakkuus ",
"MUTED": "Mykistetty", "MUTED": "Mykistetty",
"MAX_VOLUME": "Maksimi äänenvoimakkuus", "MAX_VOLUME": "Maksimi äänenvoimakkuus",
"RTC_MODE_OFF": "AEC pois päältä", "RTC_MODE_OFF": "AEC pois päältä",
"RTC_MODE_ON": "AEC päällä", "RTC_MODE_ON": "AEC päällä",
"DOWNLOAD_ASSETS_FAILED": "Resurssien lataaminen epäonnistui", "DOWNLOAD_ASSETS_FAILED": "Resurssien lataaminen epäonnistui",
"LOADING_ASSETS": "Ladataan resursseja...", "LOADING_ASSETS": "Ladataan resursseja...",
"PLEASE_WAIT": "Odota hetki...", "PLEASE_WAIT": "Odota hetki...",
"FOUND_NEW_ASSETS": "Löydetty uusia resursseja: %s", "FOUND_NEW_ASSETS": "Löydetty uusia resursseja: %s",
"HELLO_MY_FRIEND": "Hei, ystäväni!" "HELLO_MY_FRIEND": "Hei, ystäväni!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "fr-FR" "type": "fr-FR"
}, },
"strings": { "strings": {
"WARNING": "Avertissement", "WARNING": "Avertissement",
"INFO": "Information", "INFO": "Information",
"ERROR": "Erreur", "ERROR": "Erreur",
"VERSION": "Version ", "VERSION": "Version ",
"LOADING_PROTOCOL": "Connexion au serveur...", "LOADING_PROTOCOL": "Connexion au serveur...",
"INITIALIZING": "Initialisation...", "INITIALIZING": "Initialisation...",
"PIN_ERROR": "Veuillez insérer la carte SIM", "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", "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...", "DETECTING_MODULE": "Détection du module...",
"REGISTERING_NETWORK": "En attente du réseau...", "REGISTERING_NETWORK": "En attente du réseau...",
"CHECKING_NEW_VERSION": "Vérification de nouvelle version...", "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", "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_WIFI_NETWORK": "Basculer vers Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Basculer vers 4G...", "SWITCH_TO_4G_NETWORK": "Basculer vers 4G...",
"STANDBY": "En attente", "STANDBY": "En attente",
"CONNECT_TO": "Se connecter à ", "CONNECT_TO": "Se connecter à ",
"CONNECTING": "Connexion en cours...", "CONNECTING": "Connexion en cours...",
"CONNECTED_TO": "Connecté à ", "CONNECTED_TO": "Connecté à ",
"LISTENING": "Écoute...", "LISTENING": "Écoute...",
"SPEAKING": "Parole...", "SPEAKING": "Parole...",
"SERVER_NOT_FOUND": "Recherche d'un service disponible", "SERVER_NOT_FOUND": "Recherche d'un service disponible",
"SERVER_NOT_CONNECTED": "Impossible de se connecter au service, veuillez réessayer plus tard", "SERVER_NOT_CONNECTED": "Impossible de se connecter au service, veuillez réessayer plus tard",
"SERVER_TIMEOUT": "Délai d'attente de réponse", "SERVER_TIMEOUT": "Délai d'attente de réponse",
"SERVER_ERROR": "Échec d'envoi, veuillez vérifier le réseau", "SERVER_ERROR": "Échec d'envoi, veuillez vérifier le réseau",
"CONNECT_TO_HOTSPOT": "Connecter le téléphone au point d'accès ", "CONNECT_TO_HOTSPOT": "Connecter le téléphone au point d'accès ",
"ACCESS_VIA_BROWSER": "accéder via le navigateur ", "ACCESS_VIA_BROWSER": "accéder via le navigateur ",
"WIFI_CONFIG_MODE": "Mode configuration réseau", "WIFI_CONFIG_MODE": "Mode configuration réseau",
"ENTERING_WIFI_CONFIG_MODE": "Entrer en mode configuration réseau...", "ENTERING_WIFI_CONFIG_MODE": "Entrer en mode configuration réseau...",
"SCANNING_WIFI": "Scan Wi-Fi...", "SCANNING_WIFI": "Scan Wi-Fi...",
"NEW_VERSION": "Nouvelle version ", "NEW_VERSION": "Nouvelle version ",
"OTA_UPGRADE": "Mise à jour OTA", "OTA_UPGRADE": "Mise à jour OTA",
"UPGRADING": "Mise à jour du système...", "UPGRADING": "Mise à jour du système...",
"UPGRADE_FAILED": "Échec de mise à jour", "UPGRADE_FAILED": "Échec de mise à jour",
"ACTIVATION": "Activation de l'appareil", "ACTIVATION": "Activation de l'appareil",
"BATTERY_LOW": "Batterie faible", "BATTERY_LOW": "Batterie faible",
"BATTERY_CHARGING": "En charge", "BATTERY_CHARGING": "En charge",
"BATTERY_FULL": "Batterie pleine", "BATTERY_FULL": "Batterie pleine",
"BATTERY_NEED_CHARGE": "Batterie faible, veuillez charger", "BATTERY_NEED_CHARGE": "Batterie faible, veuillez charger",
"VOLUME": "Volume ", "VOLUME": "Volume ",
"MUTED": "Muet", "MUTED": "Muet",
"MAX_VOLUME": "Volume maximum", "MAX_VOLUME": "Volume maximum",
"RTC_MODE_OFF": "AEC désactivé", "RTC_MODE_OFF": "AEC désactivé",
"RTC_MODE_ON": "AEC activé", "RTC_MODE_ON": "AEC activé",
"DOWNLOAD_ASSETS_FAILED": "Échec du téléchargement des ressources", "DOWNLOAD_ASSETS_FAILED": "Échec du téléchargement des ressources",
"LOADING_ASSETS": "Chargement des ressources...", "LOADING_ASSETS": "Chargement des ressources...",
"PLEASE_WAIT": "Veuillez patienter...", "PLEASE_WAIT": "Veuillez patienter...",
"FOUND_NEW_ASSETS": "Nouvelles ressources trouvées: %s", "FOUND_NEW_ASSETS": "Nouvelles ressources trouvées: %s",
"HELLO_MY_FRIEND": "Bonjour, mon ami !" "HELLO_MY_FRIEND": "Bonjour, mon ami !"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "hi-IN" "type": "hi-IN"
}, },
"strings": { "strings": {
"WARNING": "चेतावनी", "WARNING": "चेतावनी",
"INFO": "जानकारी", "INFO": "जानकारी",
"ERROR": "त्रुटि", "ERROR": "त्रुटि",
"VERSION": "संस्करण ", "VERSION": "संस्करण ",
"LOADING_PROTOCOL": "सर्वर से कनेक्ट हो रहे हैं...", "LOADING_PROTOCOL": "सर्वर से कनेक्ट हो रहे हैं...",
"INITIALIZING": "आरंभीकरण...", "INITIALIZING": "आरंभीकरण...",
"PIN_ERROR": "कृपया सिम कार्ड डालें", "PIN_ERROR": "कृपया सिम कार्ड डालें",
"REG_ERROR": "नेटवर्क तक पहुंच नहीं हो सकती, कृपया डेटा कार्ड स्थिति जांचें", "REG_ERROR": "नेटवर्क तक पहुंच नहीं हो सकती, कृपया डेटा कार्ड स्थिति जांचें",
"DETECTING_MODULE": "मॉड्यूल का पता लगाया जा रहा है...", "DETECTING_MODULE": "मॉड्यूल का पता लगाया जा रहा है...",
"REGISTERING_NETWORK": "नेटवर्क की प्रतीक्षा...", "REGISTERING_NETWORK": "नेटवर्क की प्रतीक्षा...",
"CHECKING_NEW_VERSION": "नया संस्करण जाँच रहे हैं...", "CHECKING_NEW_VERSION": "नया संस्करण जाँच रहे हैं...",
"CHECK_NEW_VERSION_FAILED": "नया संस्करण जाँचना असफल, %d सेकंड में पुनः प्रयास: %s", "CHECK_NEW_VERSION_FAILED": "नया संस्करण जाँचना असफल, %d सेकंड में पुनः प्रयास: %s",
"SWITCH_TO_WIFI_NETWORK": "Wi-Fi पर स्विच कर रहे हैं...", "SWITCH_TO_WIFI_NETWORK": "Wi-Fi पर स्विच कर रहे हैं...",
"SWITCH_TO_4G_NETWORK": "4G पर स्विच कर रहे हैं...", "SWITCH_TO_4G_NETWORK": "4G पर स्विच कर रहे हैं...",
"STANDBY": "स्टैंडबाय", "STANDBY": "स्टैंडबाय",
"CONNECT_TO": "कनेक्ट करें ", "CONNECT_TO": "कनेक्ट करें ",
"CONNECTING": "कनेक्ट हो रहे हैं...", "CONNECTING": "कनेक्ट हो रहे हैं...",
"CONNECTED_TO": "कनेक्ट हो गए ", "CONNECTED_TO": "कनेक्ट हो गए ",
"LISTENING": "सुन रहे हैं...", "LISTENING": "सुन रहे हैं...",
"SPEAKING": "बोल रहे हैं...", "SPEAKING": "बोल रहे हैं...",
"SERVER_NOT_FOUND": "उपलब्ध सेवा खोज रहे हैं", "SERVER_NOT_FOUND": "उपलब्ध सेवा खोज रहे हैं",
"SERVER_NOT_CONNECTED": "सेवा से कनेक्ट नहीं हो सकते, कृपया बाद में कोशिश करें", "SERVER_NOT_CONNECTED": "सेवा से कनेक्ट नहीं हो सकते, कृपया बाद में कोशिश करें",
"SERVER_TIMEOUT": "प्रतिक्रिया का समय समाप्त", "SERVER_TIMEOUT": "प्रतिक्रिया का समय समाप्त",
"SERVER_ERROR": "भेजना असफल, कृपया नेटवर्क जांचें", "SERVER_ERROR": "भेजना असफल, कृपया नेटवर्क जांचें",
"CONNECT_TO_HOTSPOT": "फोन को हॉटस्पॉट से कनेक्ट करें ", "CONNECT_TO_HOTSPOT": "फोन को हॉटस्पॉट से कनेक्ट करें ",
"ACCESS_VIA_BROWSER": ",ब्राउज़र के माध्यम से पहुंचें ", "ACCESS_VIA_BROWSER": ",ब्राउज़र के माध्यम से पहुंचें ",
"WIFI_CONFIG_MODE": "नेटवर्क कॉन्फ़िगरेशन मोड", "WIFI_CONFIG_MODE": "नेटवर्क कॉन्फ़िगरेशन मोड",
"ENTERING_WIFI_CONFIG_MODE": "नेटवर्क कॉन्फ़िगरेशन मोड में प्रवेश...", "ENTERING_WIFI_CONFIG_MODE": "नेटवर्क कॉन्फ़िगरेशन मोड में प्रवेश...",
"SCANNING_WIFI": "Wi-Fi स्कैन कर रहे हैं...", "SCANNING_WIFI": "Wi-Fi स्कैन कर रहे हैं...",
"NEW_VERSION": "नया संस्करण ", "NEW_VERSION": "नया संस्करण ",
"OTA_UPGRADE": "OTA अपग्रेड", "OTA_UPGRADE": "OTA अपग्रेड",
"UPGRADING": "सिस्टम अपग्रेड हो रहा है...", "UPGRADING": "सिस्टम अपग्रेड हो रहा है...",
"UPGRADE_FAILED": "अपग्रेड असफल", "UPGRADE_FAILED": "अपग्रेड असफल",
"ACTIVATION": "डिवाइस सक्रियण", "ACTIVATION": "डिवाइस सक्रियण",
"BATTERY_LOW": "बैटरी कम", "BATTERY_LOW": "बैटरी कम",
"BATTERY_CHARGING": "चार्ज हो रही है", "BATTERY_CHARGING": "चार्ज हो रही है",
"BATTERY_FULL": "बैटरी फुल", "BATTERY_FULL": "बैटरी फुल",
"BATTERY_NEED_CHARGE": "बैटरी कम है, कृपया चार्ज करें", "BATTERY_NEED_CHARGE": "बैटरी कम है, कृपया चार्ज करें",
"VOLUME": "आवाज़ ", "VOLUME": "आवाज़ ",
"MUTED": "म्यूट", "MUTED": "म्यूट",
"MAX_VOLUME": "अधिकतम आवाज़", "MAX_VOLUME": "अधिकतम आवाज़",
"RTC_MODE_OFF": "AEC बंद", "RTC_MODE_OFF": "AEC बंद",
"RTC_MODE_ON": "AEC चालू", "RTC_MODE_ON": "AEC चालू",
"DOWNLOAD_ASSETS_FAILED": "संसाधन डाउनलोड करने में विफल", "DOWNLOAD_ASSETS_FAILED": "संसाधन डाउनलोड करने में विफल",
"LOADING_ASSETS": "संसाधन लोड हो रहे हैं...", "LOADING_ASSETS": "संसाधन लोड हो रहे हैं...",
"PLEASE_WAIT": "कृपया प्रतीक्षा करें...", "PLEASE_WAIT": "कृपया प्रतीक्षा करें...",
"FOUND_NEW_ASSETS": "नए संसाधन मिले: %s", "FOUND_NEW_ASSETS": "नए संसाधन मिले: %s",
"HELLO_MY_FRIEND": "नमस्ते, मेरे दोस्त!" "HELLO_MY_FRIEND": "नमस्ते, मेरे दोस्त!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "id-ID" "type": "id-ID"
}, },
"strings": { "strings": {
"WARNING": "Peringatan", "WARNING": "Peringatan",
"INFO": "Informasi", "INFO": "Informasi",
"ERROR": "Kesalahan", "ERROR": "Kesalahan",
"VERSION": "Versi ", "VERSION": "Versi ",
"LOADING_PROTOCOL": "Menghubungkan ke server...", "LOADING_PROTOCOL": "Menghubungkan ke server...",
"INITIALIZING": "Menginisialisasi...", "INITIALIZING": "Menginisialisasi...",
"PIN_ERROR": "Silakan masukkan kartu SIM", "PIN_ERROR": "Silakan masukkan kartu SIM",
"REG_ERROR": "Tidak dapat mengakses jaringan, periksa status kartu data", "REG_ERROR": "Tidak dapat mengakses jaringan, periksa status kartu data",
"DETECTING_MODULE": "Mendeteksi modul...", "DETECTING_MODULE": "Mendeteksi modul...",
"REGISTERING_NETWORK": "Menunggu jaringan...", "REGISTERING_NETWORK": "Menunggu jaringan...",
"CHECKING_NEW_VERSION": "Memeriksa versi baru...", "CHECKING_NEW_VERSION": "Memeriksa versi baru...",
"CHECK_NEW_VERSION_FAILED": "Pemeriksaan versi baru gagal, mencoba lagi dalam %d detik: %s", "CHECK_NEW_VERSION_FAILED": "Pemeriksaan versi baru gagal, mencoba lagi dalam %d detik: %s",
"SWITCH_TO_WIFI_NETWORK": "Beralih ke Wi-Fi...", "SWITCH_TO_WIFI_NETWORK": "Beralih ke Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Beralih ke 4G...", "SWITCH_TO_4G_NETWORK": "Beralih ke 4G...",
"STANDBY": "Siaga", "STANDBY": "Siaga",
"CONNECT_TO": "Hubungkan ke ", "CONNECT_TO": "Hubungkan ke ",
"CONNECTING": "Menghubungkan...", "CONNECTING": "Menghubungkan...",
"CONNECTED_TO": "Terhubung ke ", "CONNECTED_TO": "Terhubung ke ",
"LISTENING": "Mendengarkan...", "LISTENING": "Mendengarkan...",
"SPEAKING": "Berbicara...", "SPEAKING": "Berbicara...",
"SERVER_NOT_FOUND": "Mencari layanan yang tersedia", "SERVER_NOT_FOUND": "Mencari layanan yang tersedia",
"SERVER_NOT_CONNECTED": "Tidak dapat terhubung ke layanan, coba lagi nanti", "SERVER_NOT_CONNECTED": "Tidak dapat terhubung ke layanan, coba lagi nanti",
"SERVER_TIMEOUT": "Waktu respons habis", "SERVER_TIMEOUT": "Waktu respons habis",
"SERVER_ERROR": "Pengiriman gagal, periksa jaringan", "SERVER_ERROR": "Pengiriman gagal, periksa jaringan",
"CONNECT_TO_HOTSPOT": "Hubungkan ponsel ke hotspot ", "CONNECT_TO_HOTSPOT": "Hubungkan ponsel ke hotspot ",
"ACCESS_VIA_BROWSER": "akses melalui browser ", "ACCESS_VIA_BROWSER": "akses melalui browser ",
"WIFI_CONFIG_MODE": "Mode konfigurasi jaringan", "WIFI_CONFIG_MODE": "Mode konfigurasi jaringan",
"ENTERING_WIFI_CONFIG_MODE": "Memasuki mode konfigurasi jaringan...", "ENTERING_WIFI_CONFIG_MODE": "Memasuki mode konfigurasi jaringan...",
"SCANNING_WIFI": "Memindai Wi-Fi...", "SCANNING_WIFI": "Memindai Wi-Fi...",
"NEW_VERSION": "Versi baru ", "NEW_VERSION": "Versi baru ",
"OTA_UPGRADE": "Pembaruan OTA", "OTA_UPGRADE": "Pembaruan OTA",
"UPGRADING": "Memperbarui sistem...", "UPGRADING": "Memperbarui sistem...",
"UPGRADE_FAILED": "Pembaruan gagal", "UPGRADE_FAILED": "Pembaruan gagal",
"ACTIVATION": "Aktivasi perangkat", "ACTIVATION": "Aktivasi perangkat",
"BATTERY_LOW": "Baterai lemah", "BATTERY_LOW": "Baterai lemah",
"BATTERY_CHARGING": "Mengisi", "BATTERY_CHARGING": "Mengisi",
"BATTERY_FULL": "Baterai penuh", "BATTERY_FULL": "Baterai penuh",
"BATTERY_NEED_CHARGE": "Baterai lemah, silakan isi", "BATTERY_NEED_CHARGE": "Baterai lemah, silakan isi",
"VOLUME": "Volume ", "VOLUME": "Volume ",
"MUTED": "Bisu", "MUTED": "Bisu",
"MAX_VOLUME": "Volume maksimum", "MAX_VOLUME": "Volume maksimum",
"RTC_MODE_OFF": "AEC mati", "RTC_MODE_OFF": "AEC mati",
"RTC_MODE_ON": "AEC nyala", "RTC_MODE_ON": "AEC nyala",
"DOWNLOAD_ASSETS_FAILED": "Gagal mengunduh aset", "DOWNLOAD_ASSETS_FAILED": "Gagal mengunduh aset",
"LOADING_ASSETS": "Memuat aset...", "LOADING_ASSETS": "Memuat aset...",
"PLEASE_WAIT": "Mohon tunggu...", "PLEASE_WAIT": "Mohon tunggu...",
"FOUND_NEW_ASSETS": "Ditemukan aset baru: %s", "FOUND_NEW_ASSETS": "Ditemukan aset baru: %s",
"HELLO_MY_FRIEND": "Halo, teman saya!" "HELLO_MY_FRIEND": "Halo, teman saya!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "it-IT" "type": "it-IT"
}, },
"strings": { "strings": {
"WARNING": "Avviso", "WARNING": "Avviso",
"INFO": "Informazione", "INFO": "Informazione",
"ERROR": "Errore", "ERROR": "Errore",
"VERSION": "Versione ", "VERSION": "Versione ",
"LOADING_PROTOCOL": "Connessione al server...", "LOADING_PROTOCOL": "Connessione al server...",
"INITIALIZING": "Inizializzazione...", "INITIALIZING": "Inizializzazione...",
"PIN_ERROR": "Inserire la scheda SIM", "PIN_ERROR": "Inserire la scheda SIM",
"REG_ERROR": "Impossibile accedere alla rete, controllare lo stato della scheda dati", "REG_ERROR": "Impossibile accedere alla rete, controllare lo stato della scheda dati",
"DETECTING_MODULE": "Rilevamento modulo...", "DETECTING_MODULE": "Rilevamento modulo...",
"REGISTERING_NETWORK": "In attesa della rete...", "REGISTERING_NETWORK": "In attesa della rete...",
"CHECKING_NEW_VERSION": "Controllo nuova versione...", "CHECKING_NEW_VERSION": "Controllo nuova versione...",
"CHECK_NEW_VERSION_FAILED": "Controllo nuova versione fallito, riprovo tra %d secondi: %s", "CHECK_NEW_VERSION_FAILED": "Controllo nuova versione fallito, riprovo tra %d secondi: %s",
"SWITCH_TO_WIFI_NETWORK": "Passaggio a Wi-Fi...", "SWITCH_TO_WIFI_NETWORK": "Passaggio a Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Passaggio a 4G...", "SWITCH_TO_4G_NETWORK": "Passaggio a 4G...",
"STANDBY": "In attesa", "STANDBY": "In attesa",
"CONNECT_TO": "Connetti a ", "CONNECT_TO": "Connetti a ",
"CONNECTING": "Connessione...", "CONNECTING": "Connessione...",
"CONNECTED_TO": "Connesso a ", "CONNECTED_TO": "Connesso a ",
"LISTENING": "In ascolto...", "LISTENING": "In ascolto...",
"SPEAKING": "Parlando...", "SPEAKING": "Parlando...",
"SERVER_NOT_FOUND": "Ricerca servizio disponibile", "SERVER_NOT_FOUND": "Ricerca servizio disponibile",
"SERVER_NOT_CONNECTED": "Impossibile connettersi al servizio, riprovare più tardi", "SERVER_NOT_CONNECTED": "Impossibile connettersi al servizio, riprovare più tardi",
"SERVER_TIMEOUT": "Timeout risposta", "SERVER_TIMEOUT": "Timeout risposta",
"SERVER_ERROR": "Invio fallito, controllare la rete", "SERVER_ERROR": "Invio fallito, controllare la rete",
"CONNECT_TO_HOTSPOT": "Connetti telefono al hotspot ", "CONNECT_TO_HOTSPOT": "Connetti telefono al hotspot ",
"ACCESS_VIA_BROWSER": "accedi tramite browser ", "ACCESS_VIA_BROWSER": "accedi tramite browser ",
"WIFI_CONFIG_MODE": "Modalità configurazione rete", "WIFI_CONFIG_MODE": "Modalità configurazione rete",
"ENTERING_WIFI_CONFIG_MODE": "Entrata in modalità configurazione rete...", "ENTERING_WIFI_CONFIG_MODE": "Entrata in modalità configurazione rete...",
"SCANNING_WIFI": "Scansione Wi-Fi...", "SCANNING_WIFI": "Scansione Wi-Fi...",
"NEW_VERSION": "Nuova versione ", "NEW_VERSION": "Nuova versione ",
"OTA_UPGRADE": "Aggiornamento OTA", "OTA_UPGRADE": "Aggiornamento OTA",
"UPGRADING": "Aggiornamento sistema...", "UPGRADING": "Aggiornamento sistema...",
"UPGRADE_FAILED": "Aggiornamento fallito", "UPGRADE_FAILED": "Aggiornamento fallito",
"ACTIVATION": "Attivazione dispositivo", "ACTIVATION": "Attivazione dispositivo",
"BATTERY_LOW": "Batteria scarica", "BATTERY_LOW": "Batteria scarica",
"BATTERY_CHARGING": "In carica", "BATTERY_CHARGING": "In carica",
"BATTERY_FULL": "Batteria piena", "BATTERY_FULL": "Batteria piena",
"BATTERY_NEED_CHARGE": "Batteria scarica, ricaricare", "BATTERY_NEED_CHARGE": "Batteria scarica, ricaricare",
"VOLUME": "Volume ", "VOLUME": "Volume ",
"MUTED": "Silenziato", "MUTED": "Silenziato",
"MAX_VOLUME": "Volume massimo", "MAX_VOLUME": "Volume massimo",
"RTC_MODE_OFF": "AEC disattivato", "RTC_MODE_OFF": "AEC disattivato",
"RTC_MODE_ON": "AEC attivato", "RTC_MODE_ON": "AEC attivato",
"DOWNLOAD_ASSETS_FAILED": "Impossibile scaricare le risorse", "DOWNLOAD_ASSETS_FAILED": "Impossibile scaricare le risorse",
"LOADING_ASSETS": "Caricamento risorse...", "LOADING_ASSETS": "Caricamento risorse...",
"PLEASE_WAIT": "Attendere prego...", "PLEASE_WAIT": "Attendere prego...",
"FOUND_NEW_ASSETS": "Trovate nuove risorse: %s", "FOUND_NEW_ASSETS": "Trovate nuove risorse: %s",
"HELLO_MY_FRIEND": "Ciao, amico mio!" "HELLO_MY_FRIEND": "Ciao, amico mio!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "ja-JP" "type": "ja-JP"
}, },
"strings": { "strings": {
"WARNING": "警告", "WARNING": "警告",
"INFO": "情報", "INFO": "情報",
"ERROR": "エラー", "ERROR": "エラー",
"VERSION": "バージョン ", "VERSION": "バージョン ",
"LOADING_PROTOCOL": "サーバーにログイン中...", "LOADING_PROTOCOL": "サーバーにログイン中...",
"INITIALIZING": "初期化中...", "INITIALIZING": "初期化中...",
"PIN_ERROR": "SIMカードを挿入してください", "PIN_ERROR": "SIMカードを挿入してください",
"REG_ERROR": "ネットワークに接続できません。ネットワーク状態を確認してください", "REG_ERROR": "ネットワークに接続できません。ネットワーク状態を確認してください",
"DETECTING_MODULE": "モジュールを検出中...", "DETECTING_MODULE": "モジュールを検出中...",
"REGISTERING_NETWORK": "ネットワーク接続待機中...", "REGISTERING_NETWORK": "ネットワーク接続待機中...",
"CHECKING_NEW_VERSION": "新しいバージョンを確認中...", "CHECKING_NEW_VERSION": "新しいバージョンを確認中...",
"CHECK_NEW_VERSION_FAILED": "更新確認に失敗しました。%d 秒後に再試行します: %s", "CHECK_NEW_VERSION_FAILED": "更新確認に失敗しました。%d 秒後に再試行します: %s",
"SWITCH_TO_WIFI_NETWORK": "Wi-Fiに切り替え中...", "SWITCH_TO_WIFI_NETWORK": "Wi-Fiに切り替え中...",
"SWITCH_TO_4G_NETWORK": "4Gに切り替え中...", "SWITCH_TO_4G_NETWORK": "4Gに切り替え中...",
"STANDBY": "待機中", "STANDBY": "待機中",
"CONNECT_TO": "接続先 ", "CONNECT_TO": "接続先 ",
"CONNECTING": "接続中...", "CONNECTING": "接続中...",
"CONNECTED_TO": "接続完了 ", "CONNECTED_TO": "接続完了 ",
"LISTENING": "リスニング中...", "LISTENING": "リスニング中...",
"SPEAKING": "話しています...", "SPEAKING": "話しています...",
"SERVER_NOT_FOUND": "利用可能なサーバーを探しています", "SERVER_NOT_FOUND": "利用可能なサーバーを探しています",
"SERVER_NOT_CONNECTED": "サーバーに接続できません。後でもう一度お試しください", "SERVER_NOT_CONNECTED": "サーバーに接続できません。後でもう一度お試しください",
"SERVER_TIMEOUT": "応答待機時間が終了しました", "SERVER_TIMEOUT": "応答待機時間が終了しました",
"SERVER_ERROR": "送信に失敗しました。ネットワークを確認してください", "SERVER_ERROR": "送信に失敗しました。ネットワークを確認してください",
"CONNECT_TO_HOTSPOT": "スマートフォンをWi-Fi ", "CONNECT_TO_HOTSPOT": "スマートフォンをWi-Fi ",
"ACCESS_VIA_BROWSER": " に接続し、ブラウザでアクセスしてください ", "ACCESS_VIA_BROWSER": " に接続し、ブラウザでアクセスしてください ",
"WIFI_CONFIG_MODE": "ネットワーク設定モード", "WIFI_CONFIG_MODE": "ネットワーク設定モード",
"ENTERING_WIFI_CONFIG_MODE": "ネットワーク設定中...", "ENTERING_WIFI_CONFIG_MODE": "ネットワーク設定中...",
"SCANNING_WIFI": "Wi-Fiをスキャン中...", "SCANNING_WIFI": "Wi-Fiをスキャン中...",
"NEW_VERSION": "新しいバージョン ", "NEW_VERSION": "新しいバージョン ",
"OTA_UPGRADE": "OTAアップグレード", "OTA_UPGRADE": "OTAアップグレード",
"UPGRADING": "システムをアップグレード中...", "UPGRADING": "システムをアップグレード中...",
"UPGRADE_FAILED": "アップグレード失敗", "UPGRADE_FAILED": "アップグレード失敗",
"ACTIVATION": "デバイスをアクティベート", "ACTIVATION": "デバイスをアクティベート",
"BATTERY_LOW": "バッテリーが少なくなっています", "BATTERY_LOW": "バッテリーが少なくなっています",
"BATTERY_CHARGING": "充電中", "BATTERY_CHARGING": "充電中",
"BATTERY_FULL": "バッテリー満タン", "BATTERY_FULL": "バッテリー満タン",
"BATTERY_NEED_CHARGE": "バッテリーが低下しています。充電してください", "BATTERY_NEED_CHARGE": "バッテリーが低下しています。充電してください",
"VOLUME": "音量 ", "VOLUME": "音量 ",
"MUTED": "ミュートされています", "MUTED": "ミュートされています",
"MAX_VOLUME": "最大音量", "MAX_VOLUME": "最大音量",
"RTC_MODE_OFF": "AEC 無効", "RTC_MODE_OFF": "AEC 無効",
"RTC_MODE_ON": "AEC 有効", "RTC_MODE_ON": "AEC 有効",
"DOWNLOAD_ASSETS_FAILED": "アセットのダウンロードに失敗しました", "DOWNLOAD_ASSETS_FAILED": "アセットのダウンロードに失敗しました",
"LOADING_ASSETS": "アセットを読み込み中...", "LOADING_ASSETS": "アセットを読み込み中...",
"PLEASE_WAIT": "お待ちください...", "PLEASE_WAIT": "お待ちください...",
"FOUND_NEW_ASSETS": "新しいアセットが見つかりました: %s", "FOUND_NEW_ASSETS": "新しいアセットが見つかりました: %s",
"HELLO_MY_FRIEND": "こんにちは、友達!" "HELLO_MY_FRIEND": "こんにちは、友達!"
} }
} }

View File

@@ -1,56 +1,56 @@
{ {
"language": { "language": {
"type": "ko-KR" "type": "ko-KR"
}, },
"strings": { "strings": {
"WARNING": "경고", "WARNING": "경고",
"INFO": "정보", "INFO": "정보",
"ERROR": "오류", "ERROR": "오류",
"VERSION": "버전 ", "VERSION": "버전 ",
"LOADING_PROTOCOL": "로그인 중...", "LOADING_PROTOCOL": "로그인 중...",
"INITIALIZING": "초기화 중...", "INITIALIZING": "초기화 중...",
"PIN_ERROR": "SIM 카드를 삽입하세요", "PIN_ERROR": "SIM 카드를 삽입하세요",
"REG_ERROR": "네트워크에 접속할 수 없습니다. SIM 카드 상태를 확인하세요", "REG_ERROR": "네트워크에 접속할 수 없습니다. SIM 카드 상태를 확인하세요",
"DETECTING_MODULE": "모듈 감지 중...", "DETECTING_MODULE": "모듈 감지 중...",
"REGISTERING_NETWORK": "네트워크 대기 중...", "REGISTERING_NETWORK": "네트워크 대기 중...",
"CHECKING_NEW_VERSION": "새 버전 확인 중...", "CHECKING_NEW_VERSION": "새 버전 확인 중...",
"CHECK_NEW_VERSION_FAILED": "새 버전 확인에 실패했습니다. %d초 후에 다시 시도합니다: %s", "CHECK_NEW_VERSION_FAILED": "새 버전 확인에 실패했습니다. %d초 후에 다시 시도합니다: %s",
"SWITCH_TO_WIFI_NETWORK": "Wi-Fi로 전환 중...", "SWITCH_TO_WIFI_NETWORK": "Wi-Fi로 전환 중...",
"SWITCH_TO_4G_NETWORK": "4G로 전환 중...", "SWITCH_TO_4G_NETWORK": "4G로 전환 중...",
"STANDBY": "대기", "STANDBY": "대기",
"CONNECT_TO": "연결 대상: ", "CONNECT_TO": "연결 대상: ",
"CONNECTING": "연결 중...", "CONNECTING": "연결 중...",
"CONNECTION_SUCCESSFUL": "연결 성공", "CONNECTION_SUCCESSFUL": "연결 성공",
"CONNECTED_TO": "연결됨: ", "CONNECTED_TO": "연결됨: ",
"LISTENING": "듣는 중...", "LISTENING": "듣는 중...",
"SPEAKING": "말하는 중...", "SPEAKING": "말하는 중...",
"SERVER_NOT_FOUND": "사용 가능한 서비스를 찾는 중", "SERVER_NOT_FOUND": "사용 가능한 서비스를 찾는 중",
"SERVER_NOT_CONNECTED": "서비스에 연결할 수 없습니다. 나중에 다시 시도하세요", "SERVER_NOT_CONNECTED": "서비스에 연결할 수 없습니다. 나중에 다시 시도하세요",
"SERVER_TIMEOUT": "응답 대기 시간 초과", "SERVER_TIMEOUT": "응답 대기 시간 초과",
"SERVER_ERROR": "전송 실패, 네트워크를 확인하세요", "SERVER_ERROR": "전송 실패, 네트워크를 확인하세요",
"CONNECT_TO_HOTSPOT": "핫스팟: ", "CONNECT_TO_HOTSPOT": "핫스팟: ",
"ACCESS_VIA_BROWSER": " 설정 URL: ", "ACCESS_VIA_BROWSER": " 설정 URL: ",
"WIFI_CONFIG_MODE": "Wi-Fi 설정 모드", "WIFI_CONFIG_MODE": "Wi-Fi 설정 모드",
"ENTERING_WIFI_CONFIG_MODE": "Wi-Fi 설정 모드 진입 중...", "ENTERING_WIFI_CONFIG_MODE": "Wi-Fi 설정 모드 진입 중...",
"SCANNING_WIFI": "Wi-Fi 스캔 중...", "SCANNING_WIFI": "Wi-Fi 스캔 중...",
"NEW_VERSION": "새 버전 ", "NEW_VERSION": "새 버전 ",
"OTA_UPGRADE": "OTA 업그레이드", "OTA_UPGRADE": "OTA 업그레이드",
"UPGRADING": "시스템 업그레이드 중...", "UPGRADING": "시스템 업그레이드 중...",
"UPGRADE_FAILED": "업그레이드 실패", "UPGRADE_FAILED": "업그레이드 실패",
"ACTIVATION": "활성화", "ACTIVATION": "활성화",
"BATTERY_LOW": "배터리 부족", "BATTERY_LOW": "배터리 부족",
"BATTERY_CHARGING": "충전 중", "BATTERY_CHARGING": "충전 중",
"BATTERY_FULL": "배터리 완충", "BATTERY_FULL": "배터리 완충",
"BATTERY_NEED_CHARGE": "배터리 부족, 충전하세요", "BATTERY_NEED_CHARGE": "배터리 부족, 충전하세요",
"VOLUME": "볼륨 ", "VOLUME": "볼륨 ",
"MUTED": "음소거", "MUTED": "음소거",
"MAX_VOLUME": "최대 볼륨", "MAX_VOLUME": "최대 볼륨",
"RTC_MODE_OFF": "AEC 끄기", "RTC_MODE_OFF": "AEC 끄기",
"RTC_MODE_ON": "AEC 켜기", "RTC_MODE_ON": "AEC 켜기",
"DOWNLOAD_ASSETS_FAILED": "에셋 다운로드 실패", "DOWNLOAD_ASSETS_FAILED": "에셋 다운로드 실패",
"LOADING_ASSETS": "에셋 로딩 중...", "LOADING_ASSETS": "에셋 로딩 중...",
"PLEASE_WAIT": "잠시 기다려 주세요...", "PLEASE_WAIT": "잠시 기다려 주세요...",
"FOUND_NEW_ASSETS": "새로운 에셋을 발견했습니다: %s", "FOUND_NEW_ASSETS": "새로운 에셋을 발견했습니다: %s",
"HELLO_MY_FRIEND": "안녕하세요, 친구!" "HELLO_MY_FRIEND": "안녕하세요, 친구!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "pl-PL" "type": "pl-PL"
}, },
"strings": { "strings": {
"WARNING": "Ostrzeżenie", "WARNING": "Ostrzeżenie",
"INFO": "Informacja", "INFO": "Informacja",
"ERROR": "Błąd", "ERROR": "Błąd",
"VERSION": "Wersja ", "VERSION": "Wersja ",
"LOADING_PROTOCOL": "Łączenie z serwerem...", "LOADING_PROTOCOL": "Łączenie z serwerem...",
"INITIALIZING": "Inicjalizacja...", "INITIALIZING": "Inicjalizacja...",
"PIN_ERROR": "Proszę włożyć kartę SIM", "PIN_ERROR": "Proszę włożyć kartę SIM",
"REG_ERROR": "Nie można uzyskać dostępu do sieci, sprawdź stan karty danych", "REG_ERROR": "Nie można uzyskać dostępu do sieci, sprawdź stan karty danych",
"DETECTING_MODULE": "Wykrywanie modułu...", "DETECTING_MODULE": "Wykrywanie modułu...",
"REGISTERING_NETWORK": "Oczekiwanie na sieć...", "REGISTERING_NETWORK": "Oczekiwanie na sieć...",
"CHECKING_NEW_VERSION": "Sprawdzanie nowej wersji...", "CHECKING_NEW_VERSION": "Sprawdzanie nowej wersji...",
"CHECK_NEW_VERSION_FAILED": "Sprawdzanie nowej wersji nie powiodło się, ponowna próba za %d sekund: %s", "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_WIFI_NETWORK": "Przełączanie na Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Przełączanie na 4G...", "SWITCH_TO_4G_NETWORK": "Przełączanie na 4G...",
"STANDBY": "Gotowość", "STANDBY": "Gotowość",
"CONNECT_TO": "Połącz z ", "CONNECT_TO": "Połącz z ",
"CONNECTING": "Łączenie...", "CONNECTING": "Łączenie...",
"CONNECTED_TO": "Połączono z ", "CONNECTED_TO": "Połączono z ",
"LISTENING": "Słuchanie...", "LISTENING": "Słuchanie...",
"SPEAKING": "Mówienie...", "SPEAKING": "Mówienie...",
"SERVER_NOT_FOUND": "Szukanie dostępnej usługi", "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_NOT_CONNECTED": "Nie można połączyć się z usługą, spróbuj ponownie później",
"SERVER_TIMEOUT": "Przekroczono czas oczekiwania na odpowiedź", "SERVER_TIMEOUT": "Przekroczono czas oczekiwania na odpowiedź",
"SERVER_ERROR": "Wysyłanie nie powiodło się, sprawdź sieć", "SERVER_ERROR": "Wysyłanie nie powiodło się, sprawdź sieć",
"CONNECT_TO_HOTSPOT": "Podłącz telefon do hotspotu ", "CONNECT_TO_HOTSPOT": "Podłącz telefon do hotspotu ",
"ACCESS_VIA_BROWSER": "dostęp przez przeglądarkę ", "ACCESS_VIA_BROWSER": "dostęp przez przeglądarkę ",
"WIFI_CONFIG_MODE": "Tryb konfiguracji sieci", "WIFI_CONFIG_MODE": "Tryb konfiguracji sieci",
"ENTERING_WIFI_CONFIG_MODE": "Wchodzenie w tryb konfiguracji sieci...", "ENTERING_WIFI_CONFIG_MODE": "Wchodzenie w tryb konfiguracji sieci...",
"SCANNING_WIFI": "Skanowanie Wi-Fi...", "SCANNING_WIFI": "Skanowanie Wi-Fi...",
"NEW_VERSION": "Nowa wersja ", "NEW_VERSION": "Nowa wersja ",
"OTA_UPGRADE": "Aktualizacja OTA", "OTA_UPGRADE": "Aktualizacja OTA",
"UPGRADING": "Aktualizacja systemu...", "UPGRADING": "Aktualizacja systemu...",
"UPGRADE_FAILED": "Aktualizacja nie powiodła się", "UPGRADE_FAILED": "Aktualizacja nie powiodła się",
"ACTIVATION": "Aktywacja urządzenia", "ACTIVATION": "Aktywacja urządzenia",
"BATTERY_LOW": "Niski poziom baterii", "BATTERY_LOW": "Niski poziom baterii",
"BATTERY_CHARGING": "Ładowanie", "BATTERY_CHARGING": "Ładowanie",
"BATTERY_FULL": "Bateria pełna", "BATTERY_FULL": "Bateria pełna",
"BATTERY_NEED_CHARGE": "Niski poziom baterii, proszę naładować", "BATTERY_NEED_CHARGE": "Niski poziom baterii, proszę naładować",
"VOLUME": "Głośność ", "VOLUME": "Głośność ",
"MUTED": "Wyciszony", "MUTED": "Wyciszony",
"MAX_VOLUME": "Maksymalna głośność", "MAX_VOLUME": "Maksymalna głośność",
"RTC_MODE_OFF": "AEC wyłączony", "RTC_MODE_OFF": "AEC wyłączony",
"RTC_MODE_ON": "AEC włączony", "RTC_MODE_ON": "AEC włączony",
"DOWNLOAD_ASSETS_FAILED": "Nie udało się pobrać zasobów", "DOWNLOAD_ASSETS_FAILED": "Nie udało się pobrać zasobów",
"LOADING_ASSETS": "Ładowanie zasobów...", "LOADING_ASSETS": "Ładowanie zasobów...",
"PLEASE_WAIT": "Proszę czekać...", "PLEASE_WAIT": "Proszę czekać...",
"FOUND_NEW_ASSETS": "Znaleziono nowe zasoby: %s", "FOUND_NEW_ASSETS": "Znaleziono nowe zasoby: %s",
"HELLO_MY_FRIEND": "Cześć, mój przyjacielu!" "HELLO_MY_FRIEND": "Cześć, mój przyjacielu!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "pt-PT" "type": "pt-PT"
}, },
"strings": { "strings": {
"WARNING": "Aviso", "WARNING": "Aviso",
"INFO": "Informação", "INFO": "Informação",
"ERROR": "Erro", "ERROR": "Erro",
"VERSION": "Versão ", "VERSION": "Versão ",
"LOADING_PROTOCOL": "Ligando ao servidor...", "LOADING_PROTOCOL": "Ligando ao servidor...",
"INITIALIZING": "A inicializar...", "INITIALIZING": "A inicializar...",
"PIN_ERROR": "Por favor insira o cartão SIM", "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", "REG_ERROR": "Não é possível aceder à rede, verifique o estado do cartão de dados",
"DETECTING_MODULE": "A detectar módulo...", "DETECTING_MODULE": "A detectar módulo...",
"REGISTERING_NETWORK": "À espera da rede...", "REGISTERING_NETWORK": "À espera da rede...",
"CHECKING_NEW_VERSION": "A verificar nova versão...", "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", "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_WIFI_NETWORK": "A mudar para Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "A mudar para 4G...", "SWITCH_TO_4G_NETWORK": "A mudar para 4G...",
"STANDBY": "Em espera", "STANDBY": "Em espera",
"CONNECT_TO": "Ligar a ", "CONNECT_TO": "Ligar a ",
"CONNECTING": "A ligar...", "CONNECTING": "A ligar...",
"CONNECTED_TO": "Ligado a ", "CONNECTED_TO": "Ligado a ",
"LISTENING": "A escutar...", "LISTENING": "A escutar...",
"SPEAKING": "A falar...", "SPEAKING": "A falar...",
"SERVER_NOT_FOUND": "A procurar serviço disponível", "SERVER_NOT_FOUND": "A procurar serviço disponível",
"SERVER_NOT_CONNECTED": "Não é possível ligar ao serviço, tente mais tarde", "SERVER_NOT_CONNECTED": "Não é possível ligar ao serviço, tente mais tarde",
"SERVER_TIMEOUT": "Tempo limite de resposta", "SERVER_TIMEOUT": "Tempo limite de resposta",
"SERVER_ERROR": "Falha no envio, verifique a rede", "SERVER_ERROR": "Falha no envio, verifique a rede",
"CONNECT_TO_HOTSPOT": "Ligue o telefone ao hotspot ", "CONNECT_TO_HOTSPOT": "Ligue o telefone ao hotspot ",
"ACCESS_VIA_BROWSER": "aceder através do navegador ", "ACCESS_VIA_BROWSER": "aceder através do navegador ",
"WIFI_CONFIG_MODE": "Modo de configuração de rede", "WIFI_CONFIG_MODE": "Modo de configuração de rede",
"ENTERING_WIFI_CONFIG_MODE": "A entrar no 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...", "SCANNING_WIFI": "A procurar Wi-Fi...",
"NEW_VERSION": "Nova versão ", "NEW_VERSION": "Nova versão ",
"OTA_UPGRADE": "Atualização OTA", "OTA_UPGRADE": "Atualização OTA",
"UPGRADING": "A atualizar sistema...", "UPGRADING": "A atualizar sistema...",
"UPGRADE_FAILED": "Atualização falhada", "UPGRADE_FAILED": "Atualização falhada",
"ACTIVATION": "Ativação do dispositivo", "ACTIVATION": "Ativação do dispositivo",
"BATTERY_LOW": "Bateria fraca", "BATTERY_LOW": "Bateria fraca",
"BATTERY_CHARGING": "A carregar", "BATTERY_CHARGING": "A carregar",
"BATTERY_FULL": "Bateria cheia", "BATTERY_FULL": "Bateria cheia",
"BATTERY_NEED_CHARGE": "Bateria fraca, por favor carregue", "BATTERY_NEED_CHARGE": "Bateria fraca, por favor carregue",
"VOLUME": "Volume ", "VOLUME": "Volume ",
"MUTED": "Silenciado", "MUTED": "Silenciado",
"MAX_VOLUME": "Volume máximo", "MAX_VOLUME": "Volume máximo",
"RTC_MODE_OFF": "AEC desligado", "RTC_MODE_OFF": "AEC desligado",
"RTC_MODE_ON": "AEC ligado", "RTC_MODE_ON": "AEC ligado",
"DOWNLOAD_ASSETS_FAILED": "Falha ao descarregar recursos", "DOWNLOAD_ASSETS_FAILED": "Falha ao descarregar recursos",
"LOADING_ASSETS": "A carregar recursos...", "LOADING_ASSETS": "A carregar recursos...",
"PLEASE_WAIT": "Por favor aguarde...", "PLEASE_WAIT": "Por favor aguarde...",
"FOUND_NEW_ASSETS": "Encontrados novos recursos: %s", "FOUND_NEW_ASSETS": "Encontrados novos recursos: %s",
"HELLO_MY_FRIEND": "Olá, meu amigo!" "HELLO_MY_FRIEND": "Olá, meu amigo!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "ro-RO" "type": "ro-RO"
}, },
"strings": { "strings": {
"WARNING": "Avertisment", "WARNING": "Avertisment",
"INFO": "Informație", "INFO": "Informație",
"ERROR": "Eroare", "ERROR": "Eroare",
"VERSION": "Versiune ", "VERSION": "Versiune ",
"LOADING_PROTOCOL": "Se conectează la server...", "LOADING_PROTOCOL": "Se conectează la server...",
"INITIALIZING": "Se inițializează...", "INITIALIZING": "Se inițializează...",
"PIN_ERROR": "Vă rugăm să introduceți cardul SIM", "PIN_ERROR": "Vă rugăm să introduceți cardul SIM",
"REG_ERROR": "Nu se poate accesa rețeaua, verificați starea cardului de date", "REG_ERROR": "Nu se poate accesa rețeaua, verificați starea cardului de date",
"DETECTING_MODULE": "Se detectează modulul...", "DETECTING_MODULE": "Se detectează modulul...",
"REGISTERING_NETWORK": "Se așteaptă rețeaua...", "REGISTERING_NETWORK": "Se așteaptă rețeaua...",
"CHECKING_NEW_VERSION": "Se verifică versiunea nouă...", "CHECKING_NEW_VERSION": "Se verifică versiunea nouă...",
"CHECK_NEW_VERSION_FAILED": "Verificarea versiunii noi a eșuat, se reîncearcă în %d secunde: %s", "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_WIFI_NETWORK": "Se comută la Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Se comută la 4G...", "SWITCH_TO_4G_NETWORK": "Se comută la 4G...",
"STANDBY": "În așteptare", "STANDBY": "În așteptare",
"CONNECT_TO": "Conectare la ", "CONNECT_TO": "Conectare la ",
"CONNECTING": "Se conectează...", "CONNECTING": "Se conectează...",
"CONNECTED_TO": "Conectat la ", "CONNECTED_TO": "Conectat la ",
"LISTENING": "Se ascultă...", "LISTENING": "Se ascultă...",
"SPEAKING": "Se vorbește...", "SPEAKING": "Se vorbește...",
"SERVER_NOT_FOUND": "Se caută serviciul disponibil", "SERVER_NOT_FOUND": "Se caută serviciul disponibil",
"SERVER_NOT_CONNECTED": "Nu se poate conecta la serviciu, încercați mai târziu", "SERVER_NOT_CONNECTED": "Nu se poate conecta la serviciu, încercați mai târziu",
"SERVER_TIMEOUT": "Timpul de răspuns a expirat", "SERVER_TIMEOUT": "Timpul de răspuns a expirat",
"SERVER_ERROR": "Trimiterea a eșuat, verificați rețeaua", "SERVER_ERROR": "Trimiterea a eșuat, verificați rețeaua",
"CONNECT_TO_HOTSPOT": "Conectați telefonul la hotspot ", "CONNECT_TO_HOTSPOT": "Conectați telefonul la hotspot ",
"ACCESS_VIA_BROWSER": "accesați prin browser ", "ACCESS_VIA_BROWSER": "accesați prin browser ",
"WIFI_CONFIG_MODE": "Modul de configurare rețea", "WIFI_CONFIG_MODE": "Modul de configurare rețea",
"ENTERING_WIFI_CONFIG_MODE": "Se intră în modul de configurare rețea...", "ENTERING_WIFI_CONFIG_MODE": "Se intră în modul de configurare rețea...",
"SCANNING_WIFI": "Se scanează Wi-Fi...", "SCANNING_WIFI": "Se scanează Wi-Fi...",
"NEW_VERSION": "Versiune nouă ", "NEW_VERSION": "Versiune nouă ",
"OTA_UPGRADE": "Actualizare OTA", "OTA_UPGRADE": "Actualizare OTA",
"UPGRADING": "Se actualizează sistemul...", "UPGRADING": "Se actualizează sistemul...",
"UPGRADE_FAILED": "Actualizarea a eșuat", "UPGRADE_FAILED": "Actualizarea a eșuat",
"ACTIVATION": "Activarea dispozitivului", "ACTIVATION": "Activarea dispozitivului",
"BATTERY_LOW": "Baterie scăzută", "BATTERY_LOW": "Baterie scăzută",
"BATTERY_CHARGING": "Se încarcă", "BATTERY_CHARGING": "Se încarcă",
"BATTERY_FULL": "Baterie plină", "BATTERY_FULL": "Baterie plină",
"BATTERY_NEED_CHARGE": "Baterie scăzută, vă rugăm să încărcați", "BATTERY_NEED_CHARGE": "Baterie scăzută, vă rugăm să încărcați",
"VOLUME": "Volum ", "VOLUME": "Volum ",
"MUTED": "Silențios", "MUTED": "Silențios",
"MAX_VOLUME": "Volum maxim", "MAX_VOLUME": "Volum maxim",
"RTC_MODE_OFF": "AEC oprit", "RTC_MODE_OFF": "AEC oprit",
"RTC_MODE_ON": "AEC pornit", "RTC_MODE_ON": "AEC pornit",
"DOWNLOAD_ASSETS_FAILED": "Eșec la descărcarea resurselor", "DOWNLOAD_ASSETS_FAILED": "Eșec la descărcarea resurselor",
"LOADING_ASSETS": "Se încarcă resursele...", "LOADING_ASSETS": "Se încarcă resursele...",
"PLEASE_WAIT": "Vă rugăm să așteptați...", "PLEASE_WAIT": "Vă rugăm să așteptați...",
"FOUND_NEW_ASSETS": "S-au găsit resurse noi: %s", "FOUND_NEW_ASSETS": "S-au găsit resurse noi: %s",
"HELLO_MY_FRIEND": "Salut, prietenul meu!" "HELLO_MY_FRIEND": "Salut, prietenul meu!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "ru-RU" "type": "ru-RU"
}, },
"strings": { "strings": {
"WARNING": "Предупреждение", "WARNING": "Предупреждение",
"INFO": "Информация", "INFO": "Информация",
"ERROR": "Ошибка", "ERROR": "Ошибка",
"VERSION": "Версия ", "VERSION": "Версия ",
"LOADING_PROTOCOL": "Подключение к серверу...", "LOADING_PROTOCOL": "Подключение к серверу...",
"INITIALIZING": "Инициализация...", "INITIALIZING": "Инициализация...",
"PIN_ERROR": "Пожалуйста, вставьте SIM-карту", "PIN_ERROR": "Пожалуйста, вставьте SIM-карту",
"REG_ERROR": "Невозможно подключиться к сети, проверьте состояние карты данных", "REG_ERROR": "Невозможно подключиться к сети, проверьте состояние карты данных",
"DETECTING_MODULE": "Обнаружение модуля...", "DETECTING_MODULE": "Обнаружение модуля...",
"REGISTERING_NETWORK": "Ожидание сети...", "REGISTERING_NETWORK": "Ожидание сети...",
"CHECKING_NEW_VERSION": "Проверка новой версии...", "CHECKING_NEW_VERSION": "Проверка новой версии...",
"CHECK_NEW_VERSION_FAILED": "Ошибка проверки новой версии, повтор через %d секунд: %s", "CHECK_NEW_VERSION_FAILED": "Ошибка проверки новой версии, повтор через %d секунд: %s",
"SWITCH_TO_WIFI_NETWORK": "Переключение на Wi-Fi...", "SWITCH_TO_WIFI_NETWORK": "Переключение на Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Переключение на 4G...", "SWITCH_TO_4G_NETWORK": "Переключение на 4G...",
"STANDBY": "Ожидание", "STANDBY": "Ожидание",
"CONNECT_TO": "Подключение к ", "CONNECT_TO": "Подключение к ",
"CONNECTING": "Подключение...", "CONNECTING": "Подключение...",
"CONNECTED_TO": "Подключено к ", "CONNECTED_TO": "Подключено к ",
"LISTENING": "Прослушивание...", "LISTENING": "Прослушивание...",
"SPEAKING": "Говорение...", "SPEAKING": "Говорение...",
"SERVER_NOT_FOUND": "Поиск доступного сервиса", "SERVER_NOT_FOUND": "Поиск доступного сервиса",
"SERVER_NOT_CONNECTED": "Невозможно подключиться к сервису, попробуйте позже", "SERVER_NOT_CONNECTED": "Невозможно подключиться к сервису, попробуйте позже",
"SERVER_TIMEOUT": "Тайм-аут ответа", "SERVER_TIMEOUT": "Тайм-аут ответа",
"SERVER_ERROR": "Ошибка отправки, проверьте сеть", "SERVER_ERROR": "Ошибка отправки, проверьте сеть",
"CONNECT_TO_HOTSPOT": "Подключите телефон к точке доступа ", "CONNECT_TO_HOTSPOT": "Подключите телефон к точке доступа ",
"ACCESS_VIA_BROWSER": ",доступ через браузер ", "ACCESS_VIA_BROWSER": ",доступ через браузер ",
"WIFI_CONFIG_MODE": "Режим настройки сети", "WIFI_CONFIG_MODE": "Режим настройки сети",
"ENTERING_WIFI_CONFIG_MODE": "Вход в режим настройки сети...", "ENTERING_WIFI_CONFIG_MODE": "Вход в режим настройки сети...",
"SCANNING_WIFI": "Сканирование Wi-Fi...", "SCANNING_WIFI": "Сканирование Wi-Fi...",
"NEW_VERSION": "Новая версия ", "NEW_VERSION": "Новая версия ",
"OTA_UPGRADE": "Обновление OTA", "OTA_UPGRADE": "Обновление OTA",
"UPGRADING": "Обновление системы...", "UPGRADING": "Обновление системы...",
"UPGRADE_FAILED": "Обновление не удалось", "UPGRADE_FAILED": "Обновление не удалось",
"ACTIVATION": "Активация устройства", "ACTIVATION": "Активация устройства",
"BATTERY_LOW": "Низкий заряд батареи", "BATTERY_LOW": "Низкий заряд батареи",
"BATTERY_CHARGING": "Зарядка", "BATTERY_CHARGING": "Зарядка",
"BATTERY_FULL": "Батарея полная", "BATTERY_FULL": "Батарея полная",
"BATTERY_NEED_CHARGE": "Низкий заряд, пожалуйста, зарядите", "BATTERY_NEED_CHARGE": "Низкий заряд, пожалуйста, зарядите",
"VOLUME": "Громкость ", "VOLUME": "Громкость ",
"MUTED": "Звук отключен", "MUTED": "Звук отключен",
"MAX_VOLUME": "Максимальная громкость", "MAX_VOLUME": "Максимальная громкость",
"RTC_MODE_OFF": "AEC выключен", "RTC_MODE_OFF": "AEC выключен",
"RTC_MODE_ON": "AEC включен", "RTC_MODE_ON": "AEC включен",
"DOWNLOAD_ASSETS_FAILED": "Не удалось загрузить ресурсы", "DOWNLOAD_ASSETS_FAILED": "Не удалось загрузить ресурсы",
"LOADING_ASSETS": "Загрузка ресурсов...", "LOADING_ASSETS": "Загрузка ресурсов...",
"PLEASE_WAIT": "Пожалуйста, подождите...", "PLEASE_WAIT": "Пожалуйста, подождите...",
"FOUND_NEW_ASSETS": "Найдены новые ресурсы: %s", "FOUND_NEW_ASSETS": "Найдены новые ресурсы: %s",
"HELLO_MY_FRIEND": "Привет, мой друг!" "HELLO_MY_FRIEND": "Привет, мой друг!"
} }
} }

View File

@@ -1,56 +1,56 @@
{ {
"language": { "language": {
"type": "th-TH" "type": "th-TH"
}, },
"strings": { "strings": {
"WARNING": "คำเตือน", "WARNING": "คำเตือน",
"INFO": "ข้อมูล", "INFO": "ข้อมูล",
"ERROR": "ข้อผิดพลาด", "ERROR": "ข้อผิดพลาด",
"VERSION": "เวอร์ชัน ", "VERSION": "เวอร์ชัน ",
"LOADING_PROTOCOL": "กำลังเข้าสู่ระบบ...", "LOADING_PROTOCOL": "กำลังเข้าสู่ระบบ...",
"INITIALIZING": "กำลังเริ่มต้นระบบ...", "INITIALIZING": "กำลังเริ่มต้นระบบ...",
"PIN_ERROR": "กรุณาใส่ซิมการ์ด", "PIN_ERROR": "กรุณาใส่ซิมการ์ด",
"REG_ERROR": "ไม่สามารถเข้าถึงเครือข่ายได้ กรุณาตรวจสอบสถานะซิมการ์ด", "REG_ERROR": "ไม่สามารถเข้าถึงเครือข่ายได้ กรุณาตรวจสอบสถานะซิมการ์ด",
"DETECTING_MODULE": "กำลังตรวจจับโมดูล...", "DETECTING_MODULE": "กำลังตรวจจับโมดูล...",
"REGISTERING_NETWORK": "กำลังรอเครือข่าย...", "REGISTERING_NETWORK": "กำลังรอเครือข่าย...",
"CHECKING_NEW_VERSION": "กำลังตรวจสอบเวอร์ชันใหม่...", "CHECKING_NEW_VERSION": "กำลังตรวจสอบเวอร์ชันใหม่...",
"CHECK_NEW_VERSION_FAILED": "การตรวจสอบเวอร์ชันใหม่ล้มเหลว จะลองใหม่ใน %d วินาที: %s", "CHECK_NEW_VERSION_FAILED": "การตรวจสอบเวอร์ชันใหม่ล้มเหลว จะลองใหม่ใน %d วินาที: %s",
"SWITCH_TO_WIFI_NETWORK": "กำลังเปลี่ยนเป็น Wi-Fi...", "SWITCH_TO_WIFI_NETWORK": "กำลังเปลี่ยนเป็น Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "กำลังเปลี่ยนเป็น 4G...", "SWITCH_TO_4G_NETWORK": "กำลังเปลี่ยนเป็น 4G...",
"STANDBY": "พร้อม", "STANDBY": "พร้อม",
"CONNECT_TO": "เชื่อมต่อกับ ", "CONNECT_TO": "เชื่อมต่อกับ ",
"CONNECTING": "กำลังเชื่อมต่อ...", "CONNECTING": "กำลังเชื่อมต่อ...",
"CONNECTION_SUCCESSFUL": "เชื่อมต่อสำเร็จ", "CONNECTION_SUCCESSFUL": "เชื่อมต่อสำเร็จ",
"CONNECTED_TO": "เชื่อมต่อกับ ", "CONNECTED_TO": "เชื่อมต่อกับ ",
"LISTENING": "กำลังฟัง...", "LISTENING": "กำลังฟัง...",
"SPEAKING": "กำลังพูด...", "SPEAKING": "กำลังพูด...",
"SERVER_NOT_FOUND": "กำลังค้นหาบริการที่ใช้งานได้", "SERVER_NOT_FOUND": "กำลังค้นหาบริการที่ใช้งานได้",
"SERVER_NOT_CONNECTED": "ไม่สามารถเชื่อมต่อกับบริการได้ กรุณาลองใหม่ในภายหลัง", "SERVER_NOT_CONNECTED": "ไม่สามารถเชื่อมต่อกับบริการได้ กรุณาลองใหม่ในภายหลัง",
"SERVER_TIMEOUT": "หมดเวลารอการตอบกลับ", "SERVER_TIMEOUT": "หมดเวลารอการตอบกลับ",
"SERVER_ERROR": "การส่งข้อมูลล้มเหลว กรุณาตรวจสอบเครือข่าย", "SERVER_ERROR": "การส่งข้อมูลล้มเหลว กรุณาตรวจสอบเครือข่าย",
"CONNECT_TO_HOTSPOT": "ฮอตสปอต: ", "CONNECT_TO_HOTSPOT": "ฮอตสปอต: ",
"ACCESS_VIA_BROWSER": " URL การตั้งค่า: ", "ACCESS_VIA_BROWSER": " URL การตั้งค่า: ",
"WIFI_CONFIG_MODE": "โหมดการตั้งค่า Wi-Fi", "WIFI_CONFIG_MODE": "โหมดการตั้งค่า Wi-Fi",
"ENTERING_WIFI_CONFIG_MODE": "กำลังเข้าสู่โหมดการตั้งค่า Wi-Fi...", "ENTERING_WIFI_CONFIG_MODE": "กำลังเข้าสู่โหมดการตั้งค่า Wi-Fi...",
"SCANNING_WIFI": "กำลังสแกน Wi-Fi...", "SCANNING_WIFI": "กำลังสแกน Wi-Fi...",
"NEW_VERSION": "เวอร์ชันใหม่ ", "NEW_VERSION": "เวอร์ชันใหม่ ",
"OTA_UPGRADE": "การอัปเกรด OTA", "OTA_UPGRADE": "การอัปเกรด OTA",
"UPGRADING": "ระบบกำลังอัปเกรด...", "UPGRADING": "ระบบกำลังอัปเกรด...",
"UPGRADE_FAILED": "การอัปเกรดล้มเหลว", "UPGRADE_FAILED": "การอัปเกรดล้มเหลว",
"ACTIVATION": "การเปิดใช้งาน", "ACTIVATION": "การเปิดใช้งาน",
"BATTERY_LOW": "แบตเตอรี่ต่ำ", "BATTERY_LOW": "แบตเตอรี่ต่ำ",
"BATTERY_CHARGING": "กำลังชาร์จ", "BATTERY_CHARGING": "กำลังชาร์จ",
"BATTERY_FULL": "แบตเตอรี่เต็ม", "BATTERY_FULL": "แบตเตอรี่เต็ม",
"BATTERY_NEED_CHARGE": "แบตเตอรี่ต่ำ กรุณาชาร์จ", "BATTERY_NEED_CHARGE": "แบตเตอรี่ต่ำ กรุณาชาร์จ",
"VOLUME": "เสียง ", "VOLUME": "เสียง ",
"MUTED": "ปิดเสียง", "MUTED": "ปิดเสียง",
"MAX_VOLUME": "เสียงสูงสุด", "MAX_VOLUME": "เสียงสูงสุด",
"RTC_MODE_OFF": "ปิด AEC", "RTC_MODE_OFF": "ปิด AEC",
"RTC_MODE_ON": "เปิด AEC", "RTC_MODE_ON": "เปิด AEC",
"DOWNLOAD_ASSETS_FAILED": "ดาวน์โหลดทรัพยากรล้มเหลว", "DOWNLOAD_ASSETS_FAILED": "ดาวน์โหลดทรัพยากรล้มเหลว",
"LOADING_ASSETS": "กำลังโหลดทรัพยากร...", "LOADING_ASSETS": "กำลังโหลดทรัพยากร...",
"PLEASE_WAIT": "กรุณารอสักครู่...", "PLEASE_WAIT": "กรุณารอสักครู่...",
"FOUND_NEW_ASSETS": "พบทรัพยากรใหม่: %s", "FOUND_NEW_ASSETS": "พบทรัพยากรใหม่: %s",
"HELLO_MY_FRIEND": "สวัสดี เพื่อนของฉัน!" "HELLO_MY_FRIEND": "สวัสดี เพื่อนของฉัน!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "tr-TR" "type": "tr-TR"
}, },
"strings": { "strings": {
"WARNING": "Uyarı", "WARNING": "Uyarı",
"INFO": "Bilgi", "INFO": "Bilgi",
"ERROR": "Hata", "ERROR": "Hata",
"VERSION": "Sürüm ", "VERSION": "Sürüm ",
"LOADING_PROTOCOL": "Sunucuya bağlanıyor...", "LOADING_PROTOCOL": "Sunucuya bağlanıyor...",
"INITIALIZING": "Başlatılıyor...", "INITIALIZING": "Başlatılıyor...",
"PIN_ERROR": "Lütfen SIM kartı takın", "PIN_ERROR": "Lütfen SIM kartı takın",
"REG_ERROR": "Ağa erişilemiyor, veri kartı durumunu kontrol edin", "REG_ERROR": "Ağa erişilemiyor, veri kartı durumunu kontrol edin",
"DETECTING_MODULE": "Modül algılanıyor...", "DETECTING_MODULE": "Modül algılanıyor...",
"REGISTERING_NETWORK": "Ağ bekleniyor...", "REGISTERING_NETWORK": "Ağ bekleniyor...",
"CHECKING_NEW_VERSION": "Yeni sürüm kontrol ediliyor...", "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", "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_WIFI_NETWORK": "Wi-Fi'ye geçiliyor...",
"SWITCH_TO_4G_NETWORK": "4G'ye geçiliyor...", "SWITCH_TO_4G_NETWORK": "4G'ye geçiliyor...",
"STANDBY": "Bekleme", "STANDBY": "Bekleme",
"CONNECT_TO": "Bağlan ", "CONNECT_TO": "Bağlan ",
"CONNECTING": "Bağlanıyor...", "CONNECTING": "Bağlanıyor...",
"CONNECTED_TO": "Bağlandı ", "CONNECTED_TO": "Bağlandı ",
"LISTENING": "Dinleniyor...", "LISTENING": "Dinleniyor...",
"SPEAKING": "Konuşuluyor...", "SPEAKING": "Konuşuluyor...",
"SERVER_NOT_FOUND": "Mevcut hizmet aranıyor", "SERVER_NOT_FOUND": "Mevcut hizmet aranıyor",
"SERVER_NOT_CONNECTED": "Hizmete bağlanılamıyor, lütfen daha sonra deneyin", "SERVER_NOT_CONNECTED": "Hizmete bağlanılamıyor, lütfen daha sonra deneyin",
"SERVER_TIMEOUT": "Yanıt zaman aşımı", "SERVER_TIMEOUT": "Yanıt zaman aşımı",
"SERVER_ERROR": "Gönderme başarısız, ağı kontrol edin", "SERVER_ERROR": "Gönderme başarısız, ağı kontrol edin",
"CONNECT_TO_HOTSPOT": "Telefonu hotspot'a bağlayın ", "CONNECT_TO_HOTSPOT": "Telefonu hotspot'a bağlayın ",
"ACCESS_VIA_BROWSER": "tarayıcı üzerinden erişin ", "ACCESS_VIA_BROWSER": "tarayıcı üzerinden erişin ",
"WIFI_CONFIG_MODE": "Ağ yapılandırma modu", "WIFI_CONFIG_MODE": "Ağ yapılandırma modu",
"ENTERING_WIFI_CONFIG_MODE": "Ağ yapılandırma moduna giriliyor...", "ENTERING_WIFI_CONFIG_MODE": "Ağ yapılandırma moduna giriliyor...",
"SCANNING_WIFI": "Wi-Fi taranıyor...", "SCANNING_WIFI": "Wi-Fi taranıyor...",
"NEW_VERSION": "Yeni sürüm ", "NEW_VERSION": "Yeni sürüm ",
"OTA_UPGRADE": "OTA güncelleme", "OTA_UPGRADE": "OTA güncelleme",
"UPGRADING": "Sistem güncelleniyor...", "UPGRADING": "Sistem güncelleniyor...",
"UPGRADE_FAILED": "Güncelleme başarısız", "UPGRADE_FAILED": "Güncelleme başarısız",
"ACTIVATION": "Cihaz aktivasyonu", "ACTIVATION": "Cihaz aktivasyonu",
"BATTERY_LOW": "Pil düşük", "BATTERY_LOW": "Pil düşük",
"BATTERY_CHARGING": "Şarj oluyor", "BATTERY_CHARGING": "Şarj oluyor",
"BATTERY_FULL": "Pil dolu", "BATTERY_FULL": "Pil dolu",
"BATTERY_NEED_CHARGE": "Pil düşük, lütfen şarj edin", "BATTERY_NEED_CHARGE": "Pil düşük, lütfen şarj edin",
"VOLUME": "Ses ", "VOLUME": "Ses ",
"MUTED": "Sessiz", "MUTED": "Sessiz",
"MAX_VOLUME": "Maksimum ses", "MAX_VOLUME": "Maksimum ses",
"RTC_MODE_OFF": "AEC kapalı", "RTC_MODE_OFF": "AEC kapalı",
"RTC_MODE_ON": "AEC açık", "RTC_MODE_ON": "AEC açık",
"DOWNLOAD_ASSETS_FAILED": "Varlıklar indirilemedi", "DOWNLOAD_ASSETS_FAILED": "Varlıklar indirilemedi",
"LOADING_ASSETS": "Varlıklar yükleniyor...", "LOADING_ASSETS": "Varlıklar yükleniyor...",
"PLEASE_WAIT": "Lütfen bekleyin...", "PLEASE_WAIT": "Lütfen bekleyin...",
"FOUND_NEW_ASSETS": "Yeni varlıklar bulundu: %s", "FOUND_NEW_ASSETS": "Yeni varlıklar bulundu: %s",
"HELLO_MY_FRIEND": "Merhaba, arkadaşım!" "HELLO_MY_FRIEND": "Merhaba, arkadaşım!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "uk-UA" "type": "uk-UA"
}, },
"strings": { "strings": {
"WARNING": "Попередження", "WARNING": "Попередження",
"INFO": "Інформація", "INFO": "Інформація",
"ERROR": "Помилка", "ERROR": "Помилка",
"VERSION": "Версія ", "VERSION": "Версія ",
"LOADING_PROTOCOL": "Підключення до сервера...", "LOADING_PROTOCOL": "Підключення до сервера...",
"INITIALIZING": "Ініціалізація...", "INITIALIZING": "Ініціалізація...",
"PIN_ERROR": "Будь ласка, вставте SIM-карту", "PIN_ERROR": "Будь ласка, вставте SIM-карту",
"REG_ERROR": "Неможливо отримати доступ до мережі, перевірте стан карти даних", "REG_ERROR": "Неможливо отримати доступ до мережі, перевірте стан карти даних",
"DETECTING_MODULE": "Виявлення модуля...", "DETECTING_MODULE": "Виявлення модуля...",
"REGISTERING_NETWORK": "Очікування мережі...", "REGISTERING_NETWORK": "Очікування мережі...",
"CHECKING_NEW_VERSION": "Перевірка нової версії...", "CHECKING_NEW_VERSION": "Перевірка нової версії...",
"CHECK_NEW_VERSION_FAILED": "Перевірка нової версії не вдалася, повтор через %d секунд: %s", "CHECK_NEW_VERSION_FAILED": "Перевірка нової версії не вдалася, повтор через %d секунд: %s",
"SWITCH_TO_WIFI_NETWORK": "Перемикання на Wi-Fi...", "SWITCH_TO_WIFI_NETWORK": "Перемикання на Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Перемикання на 4G...", "SWITCH_TO_4G_NETWORK": "Перемикання на 4G...",
"STANDBY": "Очікування", "STANDBY": "Очікування",
"CONNECT_TO": "Підключитися до ", "CONNECT_TO": "Підключитися до ",
"CONNECTING": "Підключення...", "CONNECTING": "Підключення...",
"CONNECTED_TO": "Підключено до ", "CONNECTED_TO": "Підключено до ",
"LISTENING": "Прослуховування...", "LISTENING": "Прослуховування...",
"SPEAKING": "Говоріння...", "SPEAKING": "Говоріння...",
"SERVER_NOT_FOUND": "Пошук доступного сервісу", "SERVER_NOT_FOUND": "Пошук доступного сервісу",
"SERVER_NOT_CONNECTED": "Неможливо підключитися до сервісу, спробуйте пізніше", "SERVER_NOT_CONNECTED": "Неможливо підключитися до сервісу, спробуйте пізніше",
"SERVER_TIMEOUT": "Час очікування відповіді", "SERVER_TIMEOUT": "Час очікування відповіді",
"SERVER_ERROR": "Помилка відправки, перевірте мережу", "SERVER_ERROR": "Помилка відправки, перевірте мережу",
"CONNECT_TO_HOTSPOT": "Підключіть телефон до точки доступу ", "CONNECT_TO_HOTSPOT": "Підключіть телефон до точки доступу ",
"ACCESS_VIA_BROWSER": ",доступ через браузер ", "ACCESS_VIA_BROWSER": ",доступ через браузер ",
"WIFI_CONFIG_MODE": "Режим налаштування мережі", "WIFI_CONFIG_MODE": "Режим налаштування мережі",
"ENTERING_WIFI_CONFIG_MODE": "Вхід у режим налаштування мережі...", "ENTERING_WIFI_CONFIG_MODE": "Вхід у режим налаштування мережі...",
"SCANNING_WIFI": "Сканування Wi-Fi...", "SCANNING_WIFI": "Сканування Wi-Fi...",
"NEW_VERSION": "Нова версія ", "NEW_VERSION": "Нова версія ",
"OTA_UPGRADE": "Оновлення OTA", "OTA_UPGRADE": "Оновлення OTA",
"UPGRADING": "Оновлення системи...", "UPGRADING": "Оновлення системи...",
"UPGRADE_FAILED": "Оновлення не вдалося", "UPGRADE_FAILED": "Оновлення не вдалося",
"ACTIVATION": "Активація пристрою", "ACTIVATION": "Активація пристрою",
"BATTERY_LOW": "Низький заряд батареї", "BATTERY_LOW": "Низький заряд батареї",
"BATTERY_CHARGING": "Зарядка", "BATTERY_CHARGING": "Зарядка",
"BATTERY_FULL": "Батарея повна", "BATTERY_FULL": "Батарея повна",
"BATTERY_NEED_CHARGE": "Низький заряд, будь ласка, зарядіть", "BATTERY_NEED_CHARGE": "Низький заряд, будь ласка, зарядіть",
"VOLUME": "Гучність ", "VOLUME": "Гучність ",
"MUTED": "Звук вимкнено", "MUTED": "Звук вимкнено",
"MAX_VOLUME": "Максимальна гучність", "MAX_VOLUME": "Максимальна гучність",
"RTC_MODE_OFF": "AEC вимкнено", "RTC_MODE_OFF": "AEC вимкнено",
"RTC_MODE_ON": "AEC увімкнено", "RTC_MODE_ON": "AEC увімкнено",
"DOWNLOAD_ASSETS_FAILED": "Не вдалося завантажити ресурси", "DOWNLOAD_ASSETS_FAILED": "Не вдалося завантажити ресурси",
"LOADING_ASSETS": "Завантаження ресурсів...", "LOADING_ASSETS": "Завантаження ресурсів...",
"PLEASE_WAIT": "Будь ласка, зачекайте...", "PLEASE_WAIT": "Будь ласка, зачекайте...",
"FOUND_NEW_ASSETS": "Знайдено нові ресурси: %s", "FOUND_NEW_ASSETS": "Знайдено нові ресурси: %s",
"HELLO_MY_FRIEND": "Привіт, мій друже!" "HELLO_MY_FRIEND": "Привіт, мій друже!"
} }
} }

View File

@@ -1,56 +1,56 @@
{ {
"language": { "language": {
"type": "vi-VN" "type": "vi-VN"
}, },
"strings": { "strings": {
"WARNING": "Cảnh báo", "WARNING": "Cảnh báo",
"INFO": "Thông tin", "INFO": "Thông tin",
"ERROR": "Lỗi", "ERROR": "Lỗi",
"VERSION": "Phiên bản ", "VERSION": "Phiên bản ",
"LOADING_PROTOCOL": "Đang đăng nhập...", "LOADING_PROTOCOL": "Đang đăng nhập...",
"INITIALIZING": "Đang khởi tạo...", "INITIALIZING": "Đang khởi tạo...",
"PIN_ERROR": "Vui lòng cắm thẻ SIM", "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", "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...", "DETECTING_MODULE": "Đang phát hiện module...",
"REGISTERING_NETWORK": "Đang chờ mạng...", "REGISTERING_NETWORK": "Đang chờ mạng...",
"CHECKING_NEW_VERSION": "Đang kiểm tra phiên bản mới...", "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", "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_WIFI_NETWORK": "Đang chuyển sang Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "Đang chuyển sang 4G...", "SWITCH_TO_4G_NETWORK": "Đang chuyển sang 4G...",
"STANDBY": "Chờ", "STANDBY": "Chờ",
"CONNECT_TO": "Kết nối đến ", "CONNECT_TO": "Kết nối đến ",
"CONNECTING": "Đang kết nối...", "CONNECTING": "Đang kết nối...",
"CONNECTION_SUCCESSFUL": "Kết nối thành công", "CONNECTION_SUCCESSFUL": "Kết nối thành công",
"CONNECTED_TO": "Đã kết nối đến ", "CONNECTED_TO": "Đã kết nối đến ",
"LISTENING": "Đang lắng nghe...", "LISTENING": "Đang lắng nghe...",
"SPEAKING": "Đang nói...", "SPEAKING": "Đang nói...",
"SERVER_NOT_FOUND": "Đang tìm dịch vụ khả dụng", "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_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_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", "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: ", "CONNECT_TO_HOTSPOT": "Điểm phát sóng: ",
"ACCESS_VIA_BROWSER": " URL cấu hình: ", "ACCESS_VIA_BROWSER": " URL cấu hình: ",
"WIFI_CONFIG_MODE": "Chế độ cấu hình Wi-Fi", "WIFI_CONFIG_MODE": "Chế độ cấu hình Wi-Fi",
"ENTERING_WIFI_CONFIG_MODE": "Đang vào 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...", "SCANNING_WIFI": "Đang quét Wi-Fi...",
"NEW_VERSION": "Phiên bản mới ", "NEW_VERSION": "Phiên bản mới ",
"OTA_UPGRADE": "Nâng cấp OTA", "OTA_UPGRADE": "Nâng cấp OTA",
"UPGRADING": "Hệ thống đang nâng cấp...", "UPGRADING": "Hệ thống đang nâng cấp...",
"UPGRADE_FAILED": "Nâng cấp thất bại", "UPGRADE_FAILED": "Nâng cấp thất bại",
"ACTIVATION": "Kích hoạt", "ACTIVATION": "Kích hoạt",
"BATTERY_LOW": "Pin yếu", "BATTERY_LOW": "Pin yếu",
"BATTERY_CHARGING": "Đang sạc", "BATTERY_CHARGING": "Đang sạc",
"BATTERY_FULL": "Pin đầy", "BATTERY_FULL": "Pin đầy",
"BATTERY_NEED_CHARGE": "Pin yếu, vui lòng sạc", "BATTERY_NEED_CHARGE": "Pin yếu, vui lòng sạc",
"VOLUME": "Âm lượng ", "VOLUME": "Âm lượng ",
"MUTED": "Tắt tiếng", "MUTED": "Tắt tiếng",
"MAX_VOLUME": "Âm lượng tối đa", "MAX_VOLUME": "Âm lượng tối đa",
"RTC_MODE_OFF": "Tắt AEC", "RTC_MODE_OFF": "Tắt AEC",
"RTC_MODE_ON": "Bật AEC", "RTC_MODE_ON": "Bật AEC",
"DOWNLOAD_ASSETS_FAILED": "Tải xuống tài nguyên thất bại", "DOWNLOAD_ASSETS_FAILED": "Tải xuống tài nguyên thất bại",
"LOADING_ASSETS": "Đang tải tài nguyên...", "LOADING_ASSETS": "Đang tải tài nguyên...",
"PLEASE_WAIT": "Vui lòng đợi...", "PLEASE_WAIT": "Vui lòng đợi...",
"FOUND_NEW_ASSETS": "Tìm thấy tài nguyên mới: %s", "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!" "HELLO_MY_FRIEND": "Xin chào, bạn của tôi!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "zh-CN" "type": "zh-CN"
}, },
"strings": { "strings": {
"WARNING": "警告", "WARNING": "警告",
"INFO": "信息", "INFO": "信息",
"ERROR": "错误", "ERROR": "错误",
"VERSION": "版本 ", "VERSION": "版本 ",
"LOADING_PROTOCOL": "登录服务器...", "LOADING_PROTOCOL": "登录服务器...",
"INITIALIZING": "正在初始化...", "INITIALIZING": "正在初始化...",
"PIN_ERROR": "请插入 SIM 卡", "PIN_ERROR": "请插入 SIM 卡",
"REG_ERROR": "无法接入网络,请检查流量卡状态", "REG_ERROR": "无法接入网络,请检查流量卡状态",
"DETECTING_MODULE": "检测模组...", "DETECTING_MODULE": "检测模组...",
"REGISTERING_NETWORK": "等待网络...", "REGISTERING_NETWORK": "等待网络...",
"CHECKING_NEW_VERSION": "检查新版本...", "CHECKING_NEW_VERSION": "检查新版本...",
"CHECK_NEW_VERSION_FAILED": "检查新版本失败,将在 %d 秒后重试:%s", "CHECK_NEW_VERSION_FAILED": "检查新版本失败,将在 %d 秒后重试:%s",
"SWITCH_TO_WIFI_NETWORK": "切换到 Wi-Fi...", "SWITCH_TO_WIFI_NETWORK": "切换到 Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "切换到 4G...", "SWITCH_TO_4G_NETWORK": "切换到 4G...",
"STANDBY": "待命", "STANDBY": "待命",
"CONNECT_TO": "连接 ", "CONNECT_TO": "连接 ",
"CONNECTING": "连接中...", "CONNECTING": "连接中...",
"CONNECTED_TO": "已连接 ", "CONNECTED_TO": "已连接 ",
"LISTENING": "聆听中...", "LISTENING": "聆听中...",
"SPEAKING": "说话中...", "SPEAKING": "说话中...",
"SERVER_NOT_FOUND": "正在寻找可用服务", "SERVER_NOT_FOUND": "正在寻找可用服务",
"SERVER_NOT_CONNECTED": "无法连接服务,请稍后再试", "SERVER_NOT_CONNECTED": "无法连接服务,请稍后再试",
"SERVER_TIMEOUT": "等待响应超时", "SERVER_TIMEOUT": "等待响应超时",
"SERVER_ERROR": "发送失败,请检查网络", "SERVER_ERROR": "发送失败,请检查网络",
"CONNECT_TO_HOTSPOT": "手机连接热点 ", "CONNECT_TO_HOTSPOT": "手机连接热点 ",
"ACCESS_VIA_BROWSER": ",浏览器访问 ", "ACCESS_VIA_BROWSER": ",浏览器访问 ",
"WIFI_CONFIG_MODE": "配网模式", "WIFI_CONFIG_MODE": "配网模式",
"ENTERING_WIFI_CONFIG_MODE": "进入配网模式...", "ENTERING_WIFI_CONFIG_MODE": "进入配网模式...",
"SCANNING_WIFI": "扫描 Wi-Fi...", "SCANNING_WIFI": "扫描 Wi-Fi...",
"NEW_VERSION": "新版本 ", "NEW_VERSION": "新版本 ",
"OTA_UPGRADE": "OTA 升级", "OTA_UPGRADE": "OTA 升级",
"UPGRADING": "正在升级系统...", "UPGRADING": "正在升级系统...",
"UPGRADE_FAILED": "升级失败", "UPGRADE_FAILED": "升级失败",
"ACTIVATION": "激活设备", "ACTIVATION": "激活设备",
"BATTERY_LOW": "电量不足", "BATTERY_LOW": "电量不足",
"BATTERY_CHARGING": "正在充电", "BATTERY_CHARGING": "正在充电",
"BATTERY_FULL": "电量已满", "BATTERY_FULL": "电量已满",
"BATTERY_NEED_CHARGE": "电量低,请充电", "BATTERY_NEED_CHARGE": "电量低,请充电",
"VOLUME": "音量 ", "VOLUME": "音量 ",
"MUTED": "已静音", "MUTED": "已静音",
"MAX_VOLUME": "最大音量", "MAX_VOLUME": "最大音量",
"RTC_MODE_OFF": "AEC 关闭", "RTC_MODE_OFF": "AEC 关闭",
"RTC_MODE_ON": "AEC 开启", "RTC_MODE_ON": "AEC 开启",
"DOWNLOAD_ASSETS_FAILED": "下载资源失败", "DOWNLOAD_ASSETS_FAILED": "下载资源失败",
"LOADING_ASSETS": "加载资源...", "LOADING_ASSETS": "加载资源...",
"PLEASE_WAIT": "请稍候...", "PLEASE_WAIT": "请稍候...",
"FOUND_NEW_ASSETS": "发现新资源: %s", "FOUND_NEW_ASSETS": "发现新资源: %s",
"HELLO_MY_FRIEND": "你好,我的朋友!" "HELLO_MY_FRIEND": "你好,我的朋友!"
} }
} }

View File

@@ -1,55 +1,55 @@
{ {
"language": { "language": {
"type": "zh-TW" "type": "zh-TW"
}, },
"strings": { "strings": {
"WARNING": "警告", "WARNING": "警告",
"INFO": "資訊", "INFO": "資訊",
"ERROR": "錯誤", "ERROR": "錯誤",
"VERSION": "版本 ", "VERSION": "版本 ",
"LOADING_PROTOCOL": "登入伺服器...", "LOADING_PROTOCOL": "登入伺服器...",
"INITIALIZING": "正在初始化...", "INITIALIZING": "正在初始化...",
"PIN_ERROR": "請插入 SIM 卡", "PIN_ERROR": "請插入 SIM 卡",
"REG_ERROR": "無法接入網絡,請檢查網路狀態", "REG_ERROR": "無法接入網絡,請檢查網路狀態",
"DETECTING_MODULE": "檢測模組...", "DETECTING_MODULE": "檢測模組...",
"REGISTERING_NETWORK": "等待網絡...", "REGISTERING_NETWORK": "等待網絡...",
"CHECKING_NEW_VERSION": "檢查新版本...", "CHECKING_NEW_VERSION": "檢查新版本...",
"CHECK_NEW_VERSION_FAILED": "檢查新版本失敗,將在 %d 秒後重試:%s", "CHECK_NEW_VERSION_FAILED": "檢查新版本失敗,將在 %d 秒後重試:%s",
"SWITCH_TO_WIFI_NETWORK": "切換到 Wi-Fi...", "SWITCH_TO_WIFI_NETWORK": "切換到 Wi-Fi...",
"SWITCH_TO_4G_NETWORK": "切換到 4G...", "SWITCH_TO_4G_NETWORK": "切換到 4G...",
"STANDBY": "待命", "STANDBY": "待命",
"CONNECT_TO": "連接 ", "CONNECT_TO": "連接 ",
"CONNECTING": "連接中...", "CONNECTING": "連接中...",
"CONNECTED_TO": "已連接 ", "CONNECTED_TO": "已連接 ",
"LISTENING": "聆聽中...", "LISTENING": "聆聽中...",
"SPEAKING": "說話中...", "SPEAKING": "說話中...",
"SERVER_NOT_FOUND": "正在尋找可用服務", "SERVER_NOT_FOUND": "正在尋找可用服務",
"SERVER_NOT_CONNECTED": "無法連接服務,請稍後再試", "SERVER_NOT_CONNECTED": "無法連接服務,請稍後再試",
"SERVER_TIMEOUT": "等待響應超時", "SERVER_TIMEOUT": "等待響應超時",
"SERVER_ERROR": "發送失敗,請檢查網絡", "SERVER_ERROR": "發送失敗,請檢查網絡",
"CONNECT_TO_HOTSPOT": "手機連接WiFi ", "CONNECT_TO_HOTSPOT": "手機連接WiFi ",
"ACCESS_VIA_BROWSER": ",瀏覽器訪問 ", "ACCESS_VIA_BROWSER": ",瀏覽器訪問 ",
"WIFI_CONFIG_MODE": "網路設定模式", "WIFI_CONFIG_MODE": "網路設定模式",
"ENTERING_WIFI_CONFIG_MODE": "正在設定網路...", "ENTERING_WIFI_CONFIG_MODE": "正在設定網路...",
"SCANNING_WIFI": "掃描 Wi-Fi...", "SCANNING_WIFI": "掃描 Wi-Fi...",
"NEW_VERSION": "新版本 ", "NEW_VERSION": "新版本 ",
"OTA_UPGRADE": "OTA 升級", "OTA_UPGRADE": "OTA 升級",
"UPGRADING": "正在升級系統...", "UPGRADING": "正在升級系統...",
"UPGRADE_FAILED": "升級失敗", "UPGRADE_FAILED": "升級失敗",
"ACTIVATION": "啟用設備", "ACTIVATION": "啟用設備",
"BATTERY_LOW": "電量不足", "BATTERY_LOW": "電量不足",
"BATTERY_CHARGING": "正在充電", "BATTERY_CHARGING": "正在充電",
"BATTERY_FULL": "電量已滿", "BATTERY_FULL": "電量已滿",
"BATTERY_NEED_CHARGE": "電量低,請充電", "BATTERY_NEED_CHARGE": "電量低,請充電",
"VOLUME": "音量 ", "VOLUME": "音量 ",
"MUTED": "已靜音", "MUTED": "已靜音",
"MAX_VOLUME": "最大音量", "MAX_VOLUME": "最大音量",
"RTC_MODE_OFF": "AEC 關閉", "RTC_MODE_OFF": "AEC 關閉",
"RTC_MODE_ON": "AEC 開啟", "RTC_MODE_ON": "AEC 開啟",
"DOWNLOAD_ASSETS_FAILED": "下載資源失敗", "DOWNLOAD_ASSETS_FAILED": "下載資源失敗",
"LOADING_ASSETS": "載入資源...", "LOADING_ASSETS": "載入資源...",
"PLEASE_WAIT": "請稍候...", "PLEASE_WAIT": "請稍候...",
"FOUND_NEW_ASSETS": "發現新資源: %s", "FOUND_NEW_ASSETS": "發現新資源: %s",
"HELLO_MY_FRIEND": "你好,我的朋友!" "HELLO_MY_FRIEND": "你好,我的朋友!"
} }
} }

View File

@@ -1,88 +1,88 @@
# Audio Service Architecture # 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. 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 ## Key Components
- **`AudioService`**: The central orchestrator. It initializes and manages all other audio components, tasks, and data queues. - **`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. - **`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. - **`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. - **`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. - **`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). - **`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 ## Threading Model
The service operates on three primary tasks to handle the different stages of the audio pipeline concurrently: 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. 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. 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_`. 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 ## Data Flow
There are two primary data flows: audio input (uplink) and audio output (downlink). There are two primary data flows: audio input (uplink) and audio output (downlink).
### 1. Audio Input (Uplink) Flow ### 1. Audio Input (Uplink) Flow
This flow captures audio from the microphone, processes it, encodes it, and prepares it for sending to a server. This flow captures audio from the microphone, processes it, encodes it, and prepares it for sending to a server.
```mermaid ```mermaid
graph TD graph TD
subgraph Device subgraph Device
Mic[("Microphone")] -->|I2S| Codec(AudioCodec) Mic[("Microphone")] -->|I2S| Codec(AudioCodec)
subgraph AudioInputTask subgraph AudioInputTask
Codec -->|Raw PCM| Read(ReadAudioData) Codec -->|Raw PCM| Read(ReadAudioData)
Read -->|16kHz PCM| Processor(AudioProcessor) Read -->|16kHz PCM| Processor(AudioProcessor)
end end
subgraph OpusCodecTask subgraph OpusCodecTask
Processor -->|Clean PCM| EncodeQueue(audio_encode_queue_) Processor -->|Clean PCM| EncodeQueue(audio_encode_queue_)
EncodeQueue --> Encoder(OpusEncoder) EncodeQueue --> Encoder(OpusEncoder)
Encoder -->|Opus Packet| SendQueue(audio_send_queue_) Encoder -->|Opus Packet| SendQueue(audio_send_queue_)
end end
SendQueue --> |"PopPacketFromSendQueue()"| App(Application Layer) SendQueue --> |"PopPacketFromSendQueue()"| App(Application Layer)
end end
App -->|Network| Server((Cloud Server)) App -->|Network| Server((Cloud Server))
``` ```
- The `AudioInputTask` continuously reads raw PCM data from the `AudioCodec`. - The `AudioInputTask` continuously reads raw PCM data from the `AudioCodec`.
- This data is fed into an `AudioProcessor` for cleaning (AEC, VAD). - This data is fed into an `AudioProcessor` for cleaning (AEC, VAD).
- The processed PCM data is pushed into the `audio_encode_queue_`. - 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 `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. - The application can then retrieve these Opus packets and send them over the network.
### 2. Audio Output (Downlink) Flow ### 2. Audio Output (Downlink) Flow
This flow receives encoded audio data, decodes it, and plays it on the speaker. This flow receives encoded audio data, decodes it, and plays it on the speaker.
```mermaid ```mermaid
graph TD graph TD
Server((Cloud Server)) -->|Network| App(Application Layer) Server((Cloud Server)) -->|Network| App(Application Layer)
subgraph Device subgraph Device
App -->|"PushPacketToDecodeQueue()"| DecodeQueue(audio_decode_queue_) App -->|"PushPacketToDecodeQueue()"| DecodeQueue(audio_decode_queue_)
subgraph OpusCodecTask subgraph OpusCodecTask
DecodeQueue -->|Opus Packet| Decoder(OpusDecoder) DecodeQueue -->|Opus Packet| Decoder(OpusDecoder)
Decoder -->|PCM| PlaybackQueue(audio_playback_queue_) Decoder -->|PCM| PlaybackQueue(audio_playback_queue_)
end end
subgraph AudioOutputTask subgraph AudioOutputTask
PlaybackQueue -->|PCM| Codec(AudioCodec) PlaybackQueue -->|PCM| Codec(AudioCodec)
end end
Codec -->|I2S| Speaker[("Speaker")] Codec -->|I2S| Speaker[("Speaker")]
end end
``` ```
- The application receives Opus packets from the network and pushes them into the `audio_decode_queue_`. - 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 `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. - The `AudioOutputTask` takes the PCM data from the queue and sends it to the `AudioCodec` for playback.
## Power Management ## 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. 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.

View File

@@ -1,162 +1,149 @@
#include "audio_codec.h" #include "audio_codec.h"
#include "board.h" #include "board.h"
#include "settings.h" #include "settings.h"
#include <esp_log.h> #include <esp_log.h>
#include <cstring> #include <cstring>
#include <driver/i2s_common.h> #include <driver/i2s_common.h>
#define TAG "AudioCodec" #define TAG "AudioCodec"
AudioCodec::AudioCodec() { AudioCodec::AudioCodec() {
} }
AudioCodec::~AudioCodec() { AudioCodec::~AudioCodec() {
} }
void AudioCodec::OutputData(std::vector<int16_t>& data) { void AudioCodec::OutputData(std::vector<int16_t>& data) {
Write(data.data(), data.size()); Write(data.data(), data.size());
} }
bool AudioCodec::InputData(std::vector<int16_t>& data) { bool AudioCodec::InputData(std::vector<int16_t>& data) {
int samples = Read(data.data(), data.size()); int samples = Read(data.data(), data.size());
if (samples > 0) { if (samples > 0) {
return true; return true;
} }
return false; return false;
} }
void AudioCodec::Start() { void AudioCodec::Start() {
Settings settings("audio", false); Settings settings("audio", false);
output_volume_ = settings.GetInt("output_volume", output_volume_); output_volume_ = settings.GetInt("output_volume", output_volume_);
if (output_volume_ <= 0) { if (output_volume_ <= 0) {
ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_); ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_);
output_volume_ = 10; output_volume_ = 10;
} }
// 保存原始输出采样率
// 保存原始输出采样率 if (original_output_sample_rate_ == 0){
if (original_output_sample_rate_ == 0) { original_output_sample_rate_ = output_sample_rate_;
original_output_sample_rate_ = output_sample_rate_; ESP_LOGI(TAG, "Saved original output sample rate: %d Hz", original_output_sample_rate_);
ESP_LOGI(TAG, "Saved original output sample rate: %d Hz", original_output_sample_rate_); }
} if (tx_handle_ != nullptr) {
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
if (tx_handle_ != nullptr) { }
esp_err_t err = i2s_channel_enable(tx_handle_);
if (err == ESP_ERR_INVALID_STATE) { if (rx_handle_ != nullptr) {
// 已经启用,忽略 ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));
ESP_LOGW(TAG, "TX channel already enabled"); }
} else {
ESP_ERROR_CHECK(err); EnableInput(true);
} EnableOutput(true);
} ESP_LOGI(TAG, "Audio codec started");
}
if (rx_handle_ != nullptr) {
esp_err_t err = i2s_channel_enable(rx_handle_); void AudioCodec::SetOutputVolume(int volume) {
if (err == ESP_ERR_INVALID_STATE) { output_volume_ = volume;
ESP_LOGW(TAG, "RX channel already enabled"); ESP_LOGI(TAG, "Set output volume to %d", output_volume_);
} else {
ESP_ERROR_CHECK(err); Settings settings("audio", true);
} settings.SetInt("output_volume", output_volume_);
} }
EnableInput(true); void AudioCodec::EnableInput(bool enable) {
EnableOutput(true); if (enable == input_enabled_) {
ESP_LOGI(TAG, "Audio codec started"); return;
} }
input_enabled_ = enable;
void AudioCodec::SetOutputVolume(int volume) { ESP_LOGI(TAG, "Set input enable to %s", enable ? "true" : "false");
output_volume_ = volume; }
ESP_LOGI(TAG, "Set output volume to %d", output_volume_);
void AudioCodec::EnableOutput(bool enable) {
Settings settings("audio", true); if (enable == output_enabled_) {
settings.SetInt("output_volume", output_volume_); return;
} }
output_enabled_ = enable;
void AudioCodec::EnableInput(bool enable) { ESP_LOGI(TAG, "Set output enable to %s", enable ? "true" : "false");
if (enable == input_enabled_) { }
return;
} bool AudioCodec::SetOutputSampleRate(int sample_rate) {
input_enabled_ = enable; // 特殊处理:如果传入 -1表示重置到原始采样率
ESP_LOGI(TAG, "Set input enable to %s", enable ? "true" : "false"); if (sample_rate == -1) {
} if (original_output_sample_rate_ > 0) {
sample_rate = original_output_sample_rate_;
void AudioCodec::EnableOutput(bool enable) { ESP_LOGI(TAG, "Resetting to original output sample rate: %d Hz", sample_rate);
if (enable == output_enabled_) { } else {
return; ESP_LOGW(TAG, "Original sample rate not available, cannot reset");
} return false;
output_enabled_ = enable; }
ESP_LOGI(TAG, "Set output enable to %s", enable ? "true" : "false"); }
}
if (sample_rate <= 0 || sample_rate > 192000) {
bool AudioCodec::SetOutputSampleRate(int sample_rate) { ESP_LOGE(TAG, "Invalid sample rate: %d", sample_rate);
// 特殊处理:如果传入 -1表示重置到原始采样率 return false;
if (sample_rate == -1) { }
if (original_output_sample_rate_ > 0) {
sample_rate = original_output_sample_rate_; if (output_sample_rate_ == sample_rate) {
ESP_LOGI(TAG, "Resetting to original output sample rate: %d Hz", sample_rate); ESP_LOGI(TAG, "Sample rate already set to %d Hz", sample_rate);
} else { return true;
ESP_LOGW(TAG, "Original sample rate not available, cannot reset"); }
return false;
} if (tx_handle_ == nullptr) {
} ESP_LOGW(TAG, "TX handle is null, only updating sample rate variable");
output_sample_rate_ = sample_rate;
if (sample_rate <= 0 || sample_rate > 192000) { return true;
ESP_LOGE(TAG, "Invalid sample rate: %d", sample_rate); }
return false;
} ESP_LOGI(TAG, "Changing output sample rate from %d to %d Hz", output_sample_rate_, sample_rate);
if (output_sample_rate_ == sample_rate) { // 先尝试禁用 I2S 通道(如果已启用的话)
ESP_LOGI(TAG, "Sample rate already set to %d Hz", sample_rate); bool was_enabled = false;
return true; esp_err_t disable_ret = i2s_channel_disable(tx_handle_);
} if (disable_ret == ESP_OK) {
was_enabled = true;
if (tx_handle_ == nullptr) { ESP_LOGI(TAG, "Disabled I2S TX channel for reconfiguration");
ESP_LOGW(TAG, "TX handle is null, only updating sample rate variable"); } else if (disable_ret == ESP_ERR_INVALID_STATE) {
output_sample_rate_ = sample_rate; // 通道可能已经是禁用状态,这是正常的
return true; ESP_LOGI(TAG, "I2S TX channel was already disabled");
} } else {
ESP_LOGW(TAG, "Failed to disable I2S TX channel: %s", esp_err_to_name(disable_ret));
ESP_LOGI(TAG, "Changing output sample rate from %d to %d Hz", output_sample_rate_, sample_rate); }
// 先尝试禁用 I2S 通道(如果已启用的话) // 重新配置 I2S 时钟
bool was_enabled = false; i2s_std_clk_config_t clk_cfg = {
esp_err_t disable_ret = i2s_channel_disable(tx_handle_); .sample_rate_hz = (uint32_t)sample_rate,
if (disable_ret == ESP_OK) { .clk_src = I2S_CLK_SRC_DEFAULT,
was_enabled = true; .mclk_multiple = I2S_MCLK_MULTIPLE_256,
ESP_LOGI(TAG, "Disabled I2S TX channel for reconfiguration"); #ifdef I2S_HW_VERSION_2
} else if (disable_ret == ESP_ERR_INVALID_STATE) { .ext_clk_freq_hz = 0,
// 通道可能已经是禁用状态,这是正常的 #endif
ESP_LOGI(TAG, "I2S TX channel was already disabled"); };
} else {
ESP_LOGW(TAG, "Failed to disable I2S TX channel: %s", esp_err_to_name(disable_ret)); esp_err_t ret = i2s_channel_reconfig_std_clock(tx_handle_, &clk_cfg);
}
// 重新启用通道(无论之前是什么状态,现在都需要启用以便播放音频)
// 重新配置 I2S 时钟 esp_err_t enable_ret = i2s_channel_enable(tx_handle_);
i2s_std_clk_config_t clk_cfg = { if (enable_ret != ESP_OK) {
.sample_rate_hz = (uint32_t)sample_rate, ESP_LOGE(TAG, "Failed to enable I2S TX channel: %s", esp_err_to_name(enable_ret));
.clk_src = I2S_CLK_SRC_DEFAULT, } else {
.mclk_multiple = I2S_MCLK_MULTIPLE_256, ESP_LOGI(TAG, "Enabled I2S TX channel");
#ifdef I2S_HW_VERSION_2 }
.ext_clk_freq_hz = 0,
#endif if (ret == ESP_OK) {
}; output_sample_rate_ = sample_rate;
ESP_LOGI(TAG, "Successfully changed output sample rate to %d Hz", sample_rate);
esp_err_t ret = i2s_channel_reconfig_std_clock(tx_handle_, &clk_cfg); return true;
} else {
// 重新启用通道(无论之前是什么状态,现在都需要启用以便播放音频) ESP_LOGE(TAG, "Failed to change sample rate to %d Hz: %s", sample_rate, esp_err_to_name(ret));
esp_err_t enable_ret = i2s_channel_enable(tx_handle_); return false;
if (enable_ret != ESP_OK) { }
ESP_LOGE(TAG, "Failed to enable I2S TX channel: %s", esp_err_to_name(enable_ret));
} else {
ESP_LOGI(TAG, "Enabled I2S TX channel");
}
if (ret == ESP_OK) {
output_sample_rate_ = sample_rate;
ESP_LOGI(TAG, "Successfully changed output sample rate to %d Hz", sample_rate);
return true;
} else {
ESP_LOGE(TAG, "Failed to change sample rate to %d Hz: %s", sample_rate, esp_err_to_name(ret));
return false;
}
} }

View File

@@ -1,62 +1,62 @@
#ifndef _AUDIO_CODEC_H #ifndef _AUDIO_CODEC_H
#define _AUDIO_CODEC_H #define _AUDIO_CODEC_H
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h> #include <freertos/event_groups.h>
#include <driver/i2s_std.h> #include <driver/i2s_std.h>
#include <vector> #include <vector>
#include <string> #include <string>
#include <functional> #include <functional>
#include "board.h" #include "board.h"
#define AUDIO_CODEC_DMA_DESC_NUM 6 #define AUDIO_CODEC_DMA_DESC_NUM 6
#define AUDIO_CODEC_DMA_FRAME_NUM 240 #define AUDIO_CODEC_DMA_FRAME_NUM 240
#define AUDIO_CODEC_DEFAULT_MIC_GAIN 30.0 #define AUDIO_CODEC_DEFAULT_MIC_GAIN 30.0
class AudioCodec { class AudioCodec {
public: public:
AudioCodec(); AudioCodec();
virtual ~AudioCodec(); virtual ~AudioCodec();
virtual void SetOutputVolume(int volume); virtual void SetOutputVolume(int volume);
virtual void EnableInput(bool enable); virtual void EnableInput(bool enable);
virtual void EnableOutput(bool enable); virtual void EnableOutput(bool enable);
virtual bool SetOutputSampleRate(int sample_rate); virtual bool SetOutputSampleRate(int sample_rate);
virtual void OutputData(std::vector<int16_t>& data); virtual void OutputData(std::vector<int16_t>& data);
virtual bool InputData(std::vector<int16_t>& data); virtual bool InputData(std::vector<int16_t>& data);
virtual void Start(); virtual void Start();
inline bool duplex() const { return duplex_; } inline bool duplex() const { return duplex_; }
inline bool input_reference() const { return input_reference_; } inline bool input_reference() const { return input_reference_; }
inline int input_sample_rate() const { return input_sample_rate_; } inline int input_sample_rate() const { return input_sample_rate_; }
inline int output_sample_rate() const { return output_sample_rate_; } inline int output_sample_rate() const { return output_sample_rate_; }
inline int original_output_sample_rate() const { return original_output_sample_rate_; } inline int original_output_sample_rate() const { return original_output_sample_rate_; }
inline int input_channels() const { return input_channels_; } inline int input_channels() const { return input_channels_; }
inline int output_channels() const { return output_channels_; } inline int output_channels() const { return output_channels_; }
inline int output_volume() const { return output_volume_; } inline int output_volume() const { return output_volume_; }
inline bool input_enabled() const { return input_enabled_; } inline bool input_enabled() const { return input_enabled_; }
inline bool output_enabled() const { return output_enabled_; } inline bool output_enabled() const { return output_enabled_; }
protected: protected:
i2s_chan_handle_t tx_handle_ = nullptr; i2s_chan_handle_t tx_handle_ = nullptr;
i2s_chan_handle_t rx_handle_ = nullptr; i2s_chan_handle_t rx_handle_ = nullptr;
bool duplex_ = false; bool duplex_ = false;
bool input_reference_ = false; bool input_reference_ = false;
bool input_enabled_ = false; bool input_enabled_ = false;
bool output_enabled_ = false; bool output_enabled_ = false;
int input_sample_rate_ = 0; int input_sample_rate_ = 0;
int output_sample_rate_ = 0; int output_sample_rate_ = 0;
int original_output_sample_rate_ = 0; int original_output_sample_rate_ = 0;
int input_channels_ = 1; int input_channels_ = 1;
int output_channels_ = 1; int output_channels_ = 1;
int output_volume_ = 70; int output_volume_ = 70;
virtual int Read(int16_t* dest, int samples) = 0; virtual int Read(int16_t* dest, int samples) = 0;
virtual int Write(const int16_t* data, int samples) = 0; virtual int Write(const int16_t* data, int samples) = 0;
}; };
#endif // _AUDIO_CODEC_H #endif // _AUDIO_CODEC_H

View File

@@ -1,26 +1,26 @@
#ifndef AUDIO_PROCESSOR_H #ifndef AUDIO_PROCESSOR_H
#define AUDIO_PROCESSOR_H #define AUDIO_PROCESSOR_H
#include <string> #include <string>
#include <vector> #include <vector>
#include <functional> #include <functional>
#include <model_path.h> #include <model_path.h>
#include "audio_codec.h" #include "audio_codec.h"
class AudioProcessor { class AudioProcessor {
public: public:
virtual ~AudioProcessor() = default; virtual ~AudioProcessor() = default;
virtual void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) = 0; virtual void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) = 0;
virtual void Feed(std::vector<int16_t>&& data) = 0; virtual void Feed(std::vector<int16_t>&& data) = 0;
virtual void Start() = 0; virtual void Start() = 0;
virtual void Stop() = 0; virtual void Stop() = 0;
virtual bool IsRunning() = 0; virtual bool IsRunning() = 0;
virtual void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) = 0; virtual void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) = 0;
virtual void OnVadStateChange(std::function<void(bool speaking)> callback) = 0; virtual void OnVadStateChange(std::function<void(bool speaking)> callback) = 0;
virtual size_t GetFeedSize() = 0; virtual size_t GetFeedSize() = 0;
virtual void EnableDeviceAec(bool enable) = 0; virtual void EnableDeviceAec(bool enable) = 0;
}; };
#endif #endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,160 +1,162 @@
#ifndef AUDIO_SERVICE_H #ifndef AUDIO_SERVICE_H
#define AUDIO_SERVICE_H #define AUDIO_SERVICE_H
#include <memory> #include <memory>
#include <deque> #include <deque>
#include <condition_variable> #include <condition_variable>
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <freertos/event_groups.h> #include <freertos/event_groups.h>
#include <esp_timer.h> #include <esp_timer.h>
#include <model_path.h> #include <model_path.h>
#include <opus_encoder.h> #include <opus_encoder.h>
#include <opus_decoder.h> #include <opus_decoder.h>
#include <opus_resampler.h> #include <opus_resampler.h>
#include "audio_codec.h" #include "audio_codec.h"
#include "audio_processor.h" #include "audio_processor.h"
#include "processors/audio_debugger.h" #include "processors/audio_debugger.h"
#include "wake_word.h" #include "wake_word.h"
#include "protocol.h" #include "protocol.h"
/* /*
* There are two types of audio data flow: * There are two types of audio data flow:
* 1. (MIC) -> [Processors] -> {Encode Queue} -> [Opus Encoder] -> {Send Queue} -> (Server) * 1. (MIC) -> [Processors] -> {Encode Queue} -> [Opus Encoder] -> {Send Queue} -> (Server)
* 2. (Server) -> {Decode Queue} -> [Opus Decoder] -> {Playback Queue} -> (Speaker) * 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. * 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. * 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 OPUS_FRAME_DURATION_MS 60
#define MAX_ENCODE_TASKS_IN_QUEUE 2 #define MAX_ENCODE_TASKS_IN_QUEUE 2
#define MAX_PLAYBACK_TASKS_IN_QUEUE 2 #define MAX_PLAYBACK_TASKS_IN_QUEUE 2
#define MAX_DECODE_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS) #define MAX_DECODE_PACKETS_IN_QUEUE (2400 / OPUS_FRAME_DURATION_MS)
#define MAX_SEND_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 AUDIO_TESTING_MAX_DURATION_MS 10000
#define MAX_TIMESTAMPS_IN_QUEUE 3 #define MAX_TIMESTAMPS_IN_QUEUE 3
#define AUDIO_POWER_TIMEOUT_MS 15000 #define AUDIO_POWER_TIMEOUT_MS 15000
#define AUDIO_POWER_CHECK_INTERVAL_MS 1000 #define AUDIO_POWER_CHECK_INTERVAL_MS 1000
#define AS_EVENT_AUDIO_TESTING_RUNNING (1 << 0) #define AS_EVENT_AUDIO_TESTING_RUNNING (1 << 0)
#define AS_EVENT_WAKE_WORD_RUNNING (1 << 1) #define AS_EVENT_WAKE_WORD_RUNNING (1 << 1)
#define AS_EVENT_AUDIO_PROCESSOR_RUNNING (1 << 2) #define AS_EVENT_AUDIO_PROCESSOR_RUNNING (1 << 2)
#define AS_EVENT_PLAYBACK_NOT_EMPTY (1 << 3) #define AS_EVENT_PLAYBACK_NOT_EMPTY (1 << 3)
struct AudioServiceCallbacks { struct AudioServiceCallbacks {
std::function<void(void)> on_send_queue_available; std::function<void(void)> on_send_queue_available;
std::function<void(const std::string&)> on_wake_word_detected; std::function<void(const std::string&)> on_wake_word_detected;
std::function<void(bool)> on_vad_change; std::function<void(bool)> on_vad_change;
std::function<void(void)> on_audio_testing_queue_full; std::function<void(void)> on_audio_testing_queue_full;
}; };
enum AudioTaskType { enum AudioTaskType {
kAudioTaskTypeEncodeToSendQueue, kAudioTaskTypeEncodeToSendQueue,
kAudioTaskTypeEncodeToTestingQueue, kAudioTaskTypeEncodeToTestingQueue,
kAudioTaskTypeDecodeToPlaybackQueue, kAudioTaskTypeDecodeToPlaybackQueue,
}; };
struct AudioTask { struct AudioTask {
AudioTaskType type; AudioTaskType type;
std::vector<int16_t> pcm; std::vector<int16_t> pcm;
uint32_t timestamp; uint32_t timestamp;
}; };
struct DebugStatistics { struct DebugStatistics {
uint32_t input_count = 0; uint32_t input_count = 0;
uint32_t decode_count = 0; uint32_t decode_count = 0;
uint32_t encode_count = 0; uint32_t encode_count = 0;
uint32_t playback_count = 0; uint32_t playback_count = 0;
}; };
class AudioService { class AudioService {
public: public:
AudioService(); AudioService();
~AudioService(); ~AudioService();
void Initialize(AudioCodec* codec); void Initialize(AudioCodec* codec);
void Start(); void Start();
void Stop(); void Stop();
void EncodeWakeWord(); void EncodeWakeWord();
std::unique_ptr<AudioStreamPacket> PopWakeWordPacket(); std::unique_ptr<AudioStreamPacket> PopWakeWordPacket();
const std::string& GetLastWakeWord() const; const std::string& GetLastWakeWord() const;
bool IsVoiceDetected() const { return voice_detected_; } bool IsVoiceDetected() const { return voice_detected_; }
bool IsIdle(); bool IsIdle();
bool IsWakeWordRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_WAKE_WORD_RUNNING; } bool IsWakeWordRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_WAKE_WORD_RUNNING; }
bool IsAudioProcessorRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_AUDIO_PROCESSOR_RUNNING; } bool IsAudioProcessorRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_AUDIO_PROCESSOR_RUNNING; }
bool IsAfeWakeWord();
void EnableWakeWordDetection(bool enable);
void EnableVoiceProcessing(bool enable); void EnableWakeWordDetection(bool enable);
void EnableAudioTesting(bool enable); void EnableVoiceProcessing(bool enable);
void EnableDeviceAec(bool enable); void EnableAudioTesting(bool enable);
void EnableDeviceAec(bool enable);
void SetCallbacks(AudioServiceCallbacks& callbacks);
void SetCallbacks(AudioServiceCallbacks& callbacks);
bool PushPacketToDecodeQueue(std::unique_ptr<AudioStreamPacket> packet, bool wait = false);
std::unique_ptr<AudioStreamPacket> PopPacketFromSendQueue(); bool PushPacketToDecodeQueue(std::unique_ptr<AudioStreamPacket> packet, bool wait = false);
void PlaySound(const std::string_view& sound); std::unique_ptr<AudioStreamPacket> PopPacketFromSendQueue();
bool ReadAudioData(std::vector<int16_t>& data, int sample_rate, int samples); void PlaySound(const std::string_view& sound);
void ResetDecoder(); bool ReadAudioData(std::vector<int16_t>& data, int sample_rate, int samples);
void SetModelsList(srmodel_list_t* models_list); void ResetDecoder();
void UpdateOutputTimestamp(); void SetModelsList(srmodel_list_t* models_list);
private: void UpdateOutputTimestamp();
AudioCodec* codec_ = nullptr;
AudioServiceCallbacks callbacks_; private:
std::unique_ptr<AudioProcessor> audio_processor_; AudioCodec* codec_ = nullptr;
std::unique_ptr<WakeWord> wake_word_; AudioServiceCallbacks callbacks_;
std::unique_ptr<AudioDebugger> audio_debugger_; std::unique_ptr<AudioProcessor> audio_processor_;
std::unique_ptr<OpusEncoderWrapper> opus_encoder_; std::unique_ptr<WakeWord> wake_word_;
std::unique_ptr<OpusDecoderWrapper> opus_decoder_; std::unique_ptr<AudioDebugger> audio_debugger_;
OpusResampler input_resampler_; std::unique_ptr<OpusEncoderWrapper> opus_encoder_;
OpusResampler reference_resampler_; std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
OpusResampler output_resampler_; OpusResampler input_resampler_;
DebugStatistics debug_statistics_; OpusResampler reference_resampler_;
srmodel_list_t* models_list_ = nullptr; OpusResampler output_resampler_;
DebugStatistics debug_statistics_;
EventGroupHandle_t event_group_; srmodel_list_t* models_list_ = nullptr;
// Audio encode / decode EventGroupHandle_t event_group_;
TaskHandle_t audio_input_task_handle_ = nullptr;
TaskHandle_t audio_output_task_handle_ = nullptr; // Audio encode / decode
TaskHandle_t opus_codec_task_handle_ = nullptr; TaskHandle_t audio_input_task_handle_ = nullptr;
std::mutex audio_queue_mutex_; TaskHandle_t audio_output_task_handle_ = nullptr;
std::condition_variable audio_queue_cv_; TaskHandle_t opus_codec_task_handle_ = nullptr;
std::deque<std::unique_ptr<AudioStreamPacket>> audio_decode_queue_; std::mutex audio_queue_mutex_;
std::deque<std::unique_ptr<AudioStreamPacket>> audio_send_queue_; std::condition_variable audio_queue_cv_;
std::deque<std::unique_ptr<AudioStreamPacket>> audio_testing_queue_; std::deque<std::unique_ptr<AudioStreamPacket>> audio_decode_queue_;
std::deque<std::unique_ptr<AudioTask>> audio_encode_queue_; std::deque<std::unique_ptr<AudioStreamPacket>> audio_send_queue_;
std::deque<std::unique_ptr<AudioTask>> audio_playback_queue_; std::deque<std::unique_ptr<AudioStreamPacket>> audio_testing_queue_;
// For server AEC std::deque<std::unique_ptr<AudioTask>> audio_encode_queue_;
std::deque<uint32_t> timestamp_queue_; std::deque<std::unique_ptr<AudioTask>> audio_playback_queue_;
// For server AEC
bool wake_word_initialized_ = false; std::deque<uint32_t> timestamp_queue_;
bool audio_processor_initialized_ = false;
bool voice_detected_ = false; bool wake_word_initialized_ = false;
bool service_stopped_ = true; bool audio_processor_initialized_ = false;
bool audio_input_need_warmup_ = false; bool voice_detected_ = false;
bool service_stopped_ = true;
esp_timer_handle_t audio_power_timer_ = nullptr; bool audio_input_need_warmup_ = false;
std::chrono::steady_clock::time_point last_input_time_;
std::chrono::steady_clock::time_point last_output_time_; esp_timer_handle_t audio_power_timer_ = nullptr;
std::chrono::steady_clock::time_point last_input_time_;
void AudioInputTask(); std::chrono::steady_clock::time_point last_output_time_;
void AudioOutputTask();
void OpusCodecTask(); void AudioInputTask();
void PushTaskToEncodeQueue(AudioTaskType type, std::vector<int16_t>&& pcm); void AudioOutputTask();
void SetDecodeSampleRate(int sample_rate, int frame_duration); void OpusCodecTask();
void CheckAndUpdateAudioPowerState(); void PushTaskToEncodeQueue(AudioTaskType type, std::vector<int16_t>&& pcm);
}; void SetDecodeSampleRate(int sample_rate, int frame_duration);
void CheckAndUpdateAudioPowerState();
};
#endif #endif

View File

@@ -1,244 +1,244 @@
#include "box_audio_codec.h" #include "box_audio_codec.h"
#include <esp_log.h> #include <esp_log.h>
#include <driver/i2c_master.h> #include <driver/i2c_master.h>
#include <driver/i2s_tdm.h> #include <driver/i2s_tdm.h>
#define TAG "BoxAudioCodec" #define TAG "BoxAudioCodec"
BoxAudioCodec::BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, 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 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) { gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference) {
duplex_ = true; // 是否双工 duplex_ = true; // 是否双工
input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 input_reference_ = input_reference; // 是否使用参考输入,实现回声消除
input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
CreateDuplexChannels(mclk, bclk, ws, dout, din); CreateDuplexChannels(mclk, bclk, ws, dout, din);
// Do initialize of related interface: data_if, ctrl_if and gpio_if // Do initialize of related interface: data_if, ctrl_if and gpio_if
audio_codec_i2s_cfg_t i2s_cfg = { audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_NUM_0, .port = I2S_NUM_0,
.rx_handle = rx_handle_, .rx_handle = rx_handle_,
.tx_handle = tx_handle_, .tx_handle = tx_handle_,
}; };
data_if_ = audio_codec_new_i2s_data(&i2s_cfg); data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
assert(data_if_ != NULL); assert(data_if_ != NULL);
// Output // Output
audio_codec_i2c_cfg_t i2c_cfg = { audio_codec_i2c_cfg_t i2c_cfg = {
.port = (i2c_port_t)1, .port = (i2c_port_t)1,
.addr = es8311_addr, .addr = es8311_addr,
.bus_handle = i2c_master_handle, .bus_handle = i2c_master_handle,
}; };
out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(out_ctrl_if_ != NULL); assert(out_ctrl_if_ != NULL);
gpio_if_ = audio_codec_new_gpio(); gpio_if_ = audio_codec_new_gpio();
assert(gpio_if_ != NULL); assert(gpio_if_ != NULL);
es8311_codec_cfg_t es8311_cfg = {}; es8311_codec_cfg_t es8311_cfg = {};
es8311_cfg.ctrl_if = out_ctrl_if_; es8311_cfg.ctrl_if = out_ctrl_if_;
es8311_cfg.gpio_if = gpio_if_; es8311_cfg.gpio_if = gpio_if_;
es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC; es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC;
es8311_cfg.pa_pin = pa_pin; es8311_cfg.pa_pin = pa_pin;
es8311_cfg.use_mclk = true; es8311_cfg.use_mclk = true;
es8311_cfg.hw_gain.pa_voltage = 5.0; es8311_cfg.hw_gain.pa_voltage = 5.0;
es8311_cfg.hw_gain.codec_dac_voltage = 3.3; es8311_cfg.hw_gain.codec_dac_voltage = 3.3;
out_codec_if_ = es8311_codec_new(&es8311_cfg); out_codec_if_ = es8311_codec_new(&es8311_cfg);
assert(out_codec_if_ != NULL); assert(out_codec_if_ != NULL);
esp_codec_dev_cfg_t dev_cfg = { esp_codec_dev_cfg_t dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT, .dev_type = ESP_CODEC_DEV_TYPE_OUT,
.codec_if = out_codec_if_, .codec_if = out_codec_if_,
.data_if = data_if_, .data_if = data_if_,
}; };
output_dev_ = esp_codec_dev_new(&dev_cfg); output_dev_ = esp_codec_dev_new(&dev_cfg);
assert(output_dev_ != NULL); assert(output_dev_ != NULL);
// Input // Input
i2c_cfg.addr = es7210_addr; i2c_cfg.addr = es7210_addr;
in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(in_ctrl_if_ != NULL); assert(in_ctrl_if_ != NULL);
es7210_codec_cfg_t es7210_cfg = {}; es7210_codec_cfg_t es7210_cfg = {};
es7210_cfg.ctrl_if = in_ctrl_if_; es7210_cfg.ctrl_if = in_ctrl_if_;
es7210_cfg.mic_selected = ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4; es7210_cfg.mic_selected = ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4;
in_codec_if_ = es7210_codec_new(&es7210_cfg); in_codec_if_ = es7210_codec_new(&es7210_cfg);
assert(in_codec_if_ != NULL); assert(in_codec_if_ != NULL);
dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
dev_cfg.codec_if = in_codec_if_; dev_cfg.codec_if = in_codec_if_;
input_dev_ = esp_codec_dev_new(&dev_cfg); input_dev_ = esp_codec_dev_new(&dev_cfg);
assert(input_dev_ != NULL); assert(input_dev_ != NULL);
ESP_LOGI(TAG, "BoxAudioDevice initialized"); ESP_LOGI(TAG, "BoxAudioDevice initialized");
} }
BoxAudioCodec::~BoxAudioCodec() { BoxAudioCodec::~BoxAudioCodec() {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
esp_codec_dev_delete(output_dev_); esp_codec_dev_delete(output_dev_);
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
esp_codec_dev_delete(input_dev_); esp_codec_dev_delete(input_dev_);
audio_codec_delete_codec_if(in_codec_if_); audio_codec_delete_codec_if(in_codec_if_);
audio_codec_delete_ctrl_if(in_ctrl_if_); audio_codec_delete_ctrl_if(in_ctrl_if_);
audio_codec_delete_codec_if(out_codec_if_); audio_codec_delete_codec_if(out_codec_if_);
audio_codec_delete_ctrl_if(out_ctrl_if_); audio_codec_delete_ctrl_if(out_ctrl_if_);
audio_codec_delete_gpio_if(gpio_if_); audio_codec_delete_gpio_if(gpio_if_);
audio_codec_delete_data_if(data_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) { 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_); assert(input_sample_rate_ == output_sample_rate_);
i2s_chan_config_t chan_cfg = { i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0, .id = I2S_NUM_0,
.role = I2S_ROLE_MASTER, .role = I2S_ROLE_MASTER,
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
.auto_clear_after_cb = true, .auto_clear_after_cb = true,
.auto_clear_before_cb = false, .auto_clear_before_cb = false,
.intr_priority = 0, .intr_priority = 0,
}; };
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = { i2s_std_config_t std_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_, .sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256 .mclk_multiple = I2S_MCLK_MULTIPLE_256
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO, .slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH, .slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT, .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
.left_align = true, .left_align = true,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false .bit_order_lsb = false
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = mclk, .mclk = mclk,
.bclk = bclk, .bclk = bclk,
.ws = ws, .ws = ws,
.dout = dout, .dout = dout,
.din = I2S_GPIO_UNUSED, .din = I2S_GPIO_UNUSED,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_inv = false .ws_inv = false
} }
} }
}; };
i2s_tdm_config_t tdm_cfg = { i2s_tdm_config_t tdm_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)input_sample_rate_, .sample_rate_hz = (uint32_t)input_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = I2S_MCLK_MULTIPLE_256,
.bclk_div = 8, .bclk_div = 8,
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO, .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), .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_width = I2S_TDM_AUTO_WS_WIDTH,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
.left_align = false, .left_align = false,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false, .bit_order_lsb = false,
.skip_mask = false, .skip_mask = false,
.total_slot = I2S_TDM_AUTO_SLOT_NUM .total_slot = I2S_TDM_AUTO_SLOT_NUM
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = mclk, .mclk = mclk,
.bclk = bclk, .bclk = bclk,
.ws = ws, .ws = ws,
.dout = I2S_GPIO_UNUSED, .dout = I2S_GPIO_UNUSED,
.din = din, .din = din,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_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(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg)); ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg));
ESP_LOGI(TAG, "Duplex channels created"); ESP_LOGI(TAG, "Duplex channels created");
} }
void BoxAudioCodec::SetOutputVolume(int volume) { void BoxAudioCodec::SetOutputVolume(int volume) {
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
AudioCodec::SetOutputVolume(volume); AudioCodec::SetOutputVolume(volume);
} }
void BoxAudioCodec::EnableInput(bool enable) { void BoxAudioCodec::EnableInput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == input_enabled_) { if (enable == input_enabled_) {
return; return;
} }
if (enable) { if (enable) {
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = 4, .channel = 4,
.channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
.sample_rate = (uint32_t)output_sample_rate_, .sample_rate = (uint32_t)output_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
if (input_reference_) { if (input_reference_) {
fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); 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_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)); 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 { } else {
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
} }
AudioCodec::EnableInput(enable); AudioCodec::EnableInput(enable);
} }
void BoxAudioCodec::EnableOutput(bool enable) { void BoxAudioCodec::EnableOutput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == output_enabled_) { if (enable == output_enabled_) {
return; return;
} }
if (enable) { if (enable) {
// Play 16bit 1 channel // Play 16bit 1 channel
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = 1, .channel = 1,
.channel_mask = 0, .channel_mask = 0,
.sample_rate = (uint32_t)output_sample_rate_, .sample_rate = (uint32_t)output_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
} else { } else {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
} }
AudioCodec::EnableOutput(enable); AudioCodec::EnableOutput(enable);
} }
int BoxAudioCodec::Read(int16_t* dest, int samples) { int BoxAudioCodec::Read(int16_t* dest, int samples) {
if (input_enabled_) { if (input_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }
int BoxAudioCodec::Write(const int16_t* data, int samples) { int BoxAudioCodec::Write(const int16_t* data, int samples) {
if (output_enabled_) { if (output_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }

View File

@@ -1,40 +1,40 @@
#ifndef _BOX_AUDIO_CODEC_H #ifndef _BOX_AUDIO_CODEC_H
#define _BOX_AUDIO_CODEC_H #define _BOX_AUDIO_CODEC_H
#include "audio_codec.h" #include "audio_codec.h"
#include <esp_codec_dev.h> #include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h> #include <esp_codec_dev_defaults.h>
#include <mutex> #include <mutex>
class BoxAudioCodec : public AudioCodec { class BoxAudioCodec : public AudioCodec {
private: private:
const audio_codec_data_if_t* data_if_ = nullptr; const audio_codec_data_if_t* data_if_ = nullptr;
const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr;
const audio_codec_if_t* out_codec_if_ = nullptr; const audio_codec_if_t* out_codec_if_ = nullptr;
const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr;
const audio_codec_if_t* in_codec_if_ = nullptr; const audio_codec_if_t* in_codec_if_ = nullptr;
const audio_codec_gpio_if_t* gpio_if_ = nullptr; const audio_codec_gpio_if_t* gpio_if_ = nullptr;
esp_codec_dev_handle_t output_dev_ = nullptr; esp_codec_dev_handle_t output_dev_ = nullptr;
esp_codec_dev_handle_t input_dev_ = nullptr; esp_codec_dev_handle_t input_dev_ = nullptr;
std::mutex data_if_mutex_; 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 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 Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override; virtual int Write(const int16_t* data, int samples) override;
public: public:
BoxAudioCodec(void* i2c_master_handle, int input_sample_rate, int output_sample_rate, 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 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); gpio_num_t pa_pin, uint8_t es8311_addr, uint8_t es7210_addr, bool input_reference);
virtual ~BoxAudioCodec(); virtual ~BoxAudioCodec();
virtual void SetOutputVolume(int volume) override; virtual void SetOutputVolume(int volume) override;
virtual void EnableInput(bool enable) override; virtual void EnableInput(bool enable) override;
virtual void EnableOutput(bool enable) override; virtual void EnableOutput(bool enable) override;
}; };
#endif // _BOX_AUDIO_CODEC_H #endif // _BOX_AUDIO_CODEC_H

View File

@@ -1,20 +1,20 @@
#include "dummy_audio_codec.h" #include "dummy_audio_codec.h"
DummyAudioCodec::DummyAudioCodec(int input_sample_rate, int output_sample_rate) { DummyAudioCodec::DummyAudioCodec(int input_sample_rate, int output_sample_rate) {
duplex_ = true; duplex_ = true;
input_reference_ = false; input_reference_ = false;
input_channels_ = 1; input_channels_ = 1;
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
} }
DummyAudioCodec::~DummyAudioCodec() { DummyAudioCodec::~DummyAudioCodec() {
} }
int DummyAudioCodec::Read(int16_t* dest, int samples) { int DummyAudioCodec::Read(int16_t* dest, int samples) {
return 0; return 0;
} }
int DummyAudioCodec::Write(const int16_t* data, int samples) { int DummyAudioCodec::Write(const int16_t* data, int samples) {
return 0; return 0;
} }

View File

@@ -1,16 +1,16 @@
#ifndef _DUMMY_AUDIO_CODEC_H #ifndef _DUMMY_AUDIO_CODEC_H
#define _DUMMY_AUDIO_CODEC_H #define _DUMMY_AUDIO_CODEC_H
#include "audio_codec.h" #include "audio_codec.h"
class DummyAudioCodec : public AudioCodec { class DummyAudioCodec : public AudioCodec {
private: private:
virtual int Read(int16_t* dest, int samples) override; virtual int Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override; virtual int Write(const int16_t* data, int samples) override;
public: public:
DummyAudioCodec(int input_sample_rate, int output_sample_rate); DummyAudioCodec(int input_sample_rate, int output_sample_rate);
virtual ~DummyAudioCodec(); virtual ~DummyAudioCodec();
}; };
#endif // _DUMMY_AUDIO_CODEC_H #endif // _DUMMY_AUDIO_CODEC_H

View File

@@ -1,187 +1,187 @@
#include "es8311_audio_codec.h" #include "es8311_audio_codec.h"
#include <esp_log.h> #include <esp_log.h>
#define TAG "Es8311AudioCodec" #define TAG "Es8311AudioCodec"
Es8311AudioCodec::Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, 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 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) { gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk, bool pa_inverted) {
duplex_ = true; // 是否双工 duplex_ = true; // 是否双工
input_reference_ = false; // 是否使用参考输入,实现回声消除 input_reference_ = false; // 是否使用参考输入,实现回声消除
input_channels_ = 1; // 输入通道数 input_channels_ = 1; // 输入通道数
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
pa_pin_ = pa_pin; pa_pin_ = pa_pin;
pa_inverted_ = pa_inverted; pa_inverted_ = pa_inverted;
assert(input_sample_rate_ == output_sample_rate_); assert(input_sample_rate_ == output_sample_rate_);
CreateDuplexChannels(mclk, bclk, ws, dout, din); CreateDuplexChannels(mclk, bclk, ws, dout, din);
// Do initialize of related interface: data_if, ctrl_if and gpio_if // Do initialize of related interface: data_if, ctrl_if and gpio_if
audio_codec_i2s_cfg_t i2s_cfg = { audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_NUM_0, .port = I2S_NUM_0,
.rx_handle = rx_handle_, .rx_handle = rx_handle_,
.tx_handle = tx_handle_, .tx_handle = tx_handle_,
}; };
data_if_ = audio_codec_new_i2s_data(&i2s_cfg); data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
assert(data_if_ != NULL); assert(data_if_ != NULL);
// Output // Output
audio_codec_i2c_cfg_t i2c_cfg = { audio_codec_i2c_cfg_t i2c_cfg = {
.port = i2c_port, .port = i2c_port,
.addr = es8311_addr, .addr = es8311_addr,
.bus_handle = i2c_master_handle, .bus_handle = i2c_master_handle,
}; };
ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(ctrl_if_ != NULL); assert(ctrl_if_ != NULL);
gpio_if_ = audio_codec_new_gpio(); gpio_if_ = audio_codec_new_gpio();
assert(gpio_if_ != NULL); assert(gpio_if_ != NULL);
es8311_codec_cfg_t es8311_cfg = {}; es8311_codec_cfg_t es8311_cfg = {};
es8311_cfg.ctrl_if = ctrl_if_; es8311_cfg.ctrl_if = ctrl_if_;
es8311_cfg.gpio_if = gpio_if_; es8311_cfg.gpio_if = gpio_if_;
es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
es8311_cfg.pa_pin = pa_pin; es8311_cfg.pa_pin = pa_pin;
es8311_cfg.use_mclk = use_mclk; es8311_cfg.use_mclk = use_mclk;
es8311_cfg.hw_gain.pa_voltage = 5.0; es8311_cfg.hw_gain.pa_voltage = 5.0;
es8311_cfg.hw_gain.codec_dac_voltage = 3.3; es8311_cfg.hw_gain.codec_dac_voltage = 3.3;
es8311_cfg.pa_reverted = pa_inverted_; es8311_cfg.pa_reverted = pa_inverted_;
codec_if_ = es8311_codec_new(&es8311_cfg); codec_if_ = es8311_codec_new(&es8311_cfg);
assert(codec_if_ != NULL); assert(codec_if_ != NULL);
ESP_LOGI(TAG, "Es8311AudioCodec initialized"); ESP_LOGI(TAG, "Es8311AudioCodec initialized");
} }
Es8311AudioCodec::~Es8311AudioCodec() { Es8311AudioCodec::~Es8311AudioCodec() {
esp_codec_dev_delete(dev_); esp_codec_dev_delete(dev_);
audio_codec_delete_codec_if(codec_if_); audio_codec_delete_codec_if(codec_if_);
audio_codec_delete_ctrl_if(ctrl_if_); audio_codec_delete_ctrl_if(ctrl_if_);
audio_codec_delete_gpio_if(gpio_if_); audio_codec_delete_gpio_if(gpio_if_);
audio_codec_delete_data_if(data_if_); audio_codec_delete_data_if(data_if_);
} }
void Es8311AudioCodec::UpdateDeviceState() { void Es8311AudioCodec::UpdateDeviceState() {
if ((input_enabled_ || output_enabled_) && dev_ == nullptr) { if ((input_enabled_ || output_enabled_) && dev_ == nullptr) {
esp_codec_dev_cfg_t dev_cfg = { esp_codec_dev_cfg_t dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_IN_OUT, .dev_type = ESP_CODEC_DEV_TYPE_IN_OUT,
.codec_if = codec_if_, .codec_if = codec_if_,
.data_if = data_if_, .data_if = data_if_,
}; };
dev_ = esp_codec_dev_new(&dev_cfg); dev_ = esp_codec_dev_new(&dev_cfg);
assert(dev_ != NULL); assert(dev_ != NULL);
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = 1, .channel = 1,
.channel_mask = 0, .channel_mask = 0,
.sample_rate = (uint32_t)input_sample_rate_, .sample_rate = (uint32_t)input_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
ESP_ERROR_CHECK(esp_codec_dev_open(dev_, &fs)); 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_in_gain(dev_, AUDIO_CODEC_DEFAULT_MIC_GAIN));
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(dev_, output_volume_)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(dev_, output_volume_));
} else if (!input_enabled_ && !output_enabled_ && dev_ != nullptr) { } else if (!input_enabled_ && !output_enabled_ && dev_ != nullptr) {
esp_codec_dev_close(dev_); esp_codec_dev_close(dev_);
dev_ = nullptr; dev_ = nullptr;
} }
if (pa_pin_ != GPIO_NUM_NC) { if (pa_pin_ != GPIO_NUM_NC) {
int level = output_enabled_ ? 1 : 0; int level = output_enabled_ ? 1 : 0;
gpio_set_level(pa_pin_, pa_inverted_ ? !level : level); 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) { 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_); assert(input_sample_rate_ == output_sample_rate_);
i2s_chan_config_t chan_cfg = { i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0, .id = I2S_NUM_0,
.role = I2S_ROLE_MASTER, .role = I2S_ROLE_MASTER,
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
.auto_clear_after_cb = true, .auto_clear_after_cb = true,
.auto_clear_before_cb = false, .auto_clear_before_cb = false,
.intr_priority = 0, .intr_priority = 0,
}; };
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = { i2s_std_config_t std_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_, .sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = I2S_MCLK_MULTIPLE_256,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
#endif #endif
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO, .slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH, .slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT, .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.left_align = true, .left_align = true,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false .bit_order_lsb = false
#endif #endif
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = mclk, .mclk = mclk,
.bclk = bclk, .bclk = bclk,
.ws = ws, .ws = ws,
.dout = dout, .dout = dout,
.din = din, .din = din,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_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(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
ESP_LOGI(TAG, "Duplex channels created"); ESP_LOGI(TAG, "Duplex channels created");
} }
void Es8311AudioCodec::SetOutputVolume(int volume) { void Es8311AudioCodec::SetOutputVolume(int volume) {
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(dev_, volume)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(dev_, volume));
AudioCodec::SetOutputVolume(volume); AudioCodec::SetOutputVolume(volume);
} }
void Es8311AudioCodec::EnableInput(bool enable) { void Es8311AudioCodec::EnableInput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == input_enabled_) { if (enable == input_enabled_) {
return; return;
} }
AudioCodec::EnableInput(enable); AudioCodec::EnableInput(enable);
UpdateDeviceState(); UpdateDeviceState();
} }
void Es8311AudioCodec::EnableOutput(bool enable) { void Es8311AudioCodec::EnableOutput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == output_enabled_) { if (enable == output_enabled_) {
return; return;
} }
AudioCodec::EnableOutput(enable); AudioCodec::EnableOutput(enable);
UpdateDeviceState(); UpdateDeviceState();
} }
int Es8311AudioCodec::Read(int16_t* dest, int samples) { int Es8311AudioCodec::Read(int16_t* dest, int samples) {
if (input_enabled_) { if (input_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(dev_, (void*)dest, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(dev_, (void*)dest, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }
int Es8311AudioCodec::Write(const int16_t* data, int samples) { int Es8311AudioCodec::Write(const int16_t* data, int samples) {
if (output_enabled_) { if (output_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(dev_, (void*)data, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(dev_, (void*)data, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }

View File

@@ -1,42 +1,42 @@
#ifndef _ES8311_AUDIO_CODEC_H #ifndef _ES8311_AUDIO_CODEC_H
#define _ES8311_AUDIO_CODEC_H #define _ES8311_AUDIO_CODEC_H
#include "audio_codec.h" #include "audio_codec.h"
#include <driver/i2c_master.h> #include <driver/i2c_master.h>
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_codec_dev.h> #include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h> #include <esp_codec_dev_defaults.h>
#include <mutex> #include <mutex>
class Es8311AudioCodec : public AudioCodec { class Es8311AudioCodec : public AudioCodec {
private: private:
const audio_codec_data_if_t* data_if_ = nullptr; const audio_codec_data_if_t* data_if_ = nullptr;
const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
const audio_codec_if_t* codec_if_ = nullptr; const audio_codec_if_t* codec_if_ = nullptr;
const audio_codec_gpio_if_t* gpio_if_ = nullptr; const audio_codec_gpio_if_t* gpio_if_ = nullptr;
esp_codec_dev_handle_t dev_ = nullptr; esp_codec_dev_handle_t dev_ = nullptr;
gpio_num_t pa_pin_ = GPIO_NUM_NC; gpio_num_t pa_pin_ = GPIO_NUM_NC;
bool pa_inverted_ = false; bool pa_inverted_ = false;
std::mutex data_if_mutex_; 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 CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din);
void UpdateDeviceState(); void UpdateDeviceState();
virtual int Read(int16_t* dest, int samples) override; virtual int Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override; virtual int Write(const int16_t* data, int samples) override;
public: public:
Es8311AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, 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 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); gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk = true, bool pa_inverted = false);
virtual ~Es8311AudioCodec(); virtual ~Es8311AudioCodec();
virtual void SetOutputVolume(int volume) override; virtual void SetOutputVolume(int volume) override;
virtual void EnableInput(bool enable) override; virtual void EnableInput(bool enable) override;
virtual void EnableOutput(bool enable) override; virtual void EnableOutput(bool enable) override;
}; };
#endif // _ES8311_AUDIO_CODEC_H #endif // _ES8311_AUDIO_CODEC_H

View File

@@ -1,196 +1,196 @@
#include "es8374_audio_codec.h" #include "es8374_audio_codec.h"
#include <esp_log.h> #include <esp_log.h>
#define TAG "Es8374AudioCodec" #define TAG "Es8374AudioCodec"
Es8374AudioCodec::Es8374AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, 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 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) { gpio_num_t pa_pin, uint8_t es8374_addr, bool use_mclk) {
duplex_ = true; // 是否双工 duplex_ = true; // 是否双工
input_reference_ = false; // 是否使用参考输入,实现回声消除 input_reference_ = false; // 是否使用参考输入,实现回声消除
input_channels_ = 1; // 输入通道数 input_channels_ = 1; // 输入通道数
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
pa_pin_ = pa_pin; pa_pin_ = pa_pin;
CreateDuplexChannels(mclk, bclk, ws, dout, din); CreateDuplexChannels(mclk, bclk, ws, dout, din);
// Do initialize of related interface: data_if, ctrl_if and gpio_if // Do initialize of related interface: data_if, ctrl_if and gpio_if
audio_codec_i2s_cfg_t i2s_cfg = { audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_NUM_0, .port = I2S_NUM_0,
.rx_handle = rx_handle_, .rx_handle = rx_handle_,
.tx_handle = tx_handle_, .tx_handle = tx_handle_,
}; };
data_if_ = audio_codec_new_i2s_data(&i2s_cfg); data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
assert(data_if_ != NULL); assert(data_if_ != NULL);
// Output // Output
audio_codec_i2c_cfg_t i2c_cfg = { audio_codec_i2c_cfg_t i2c_cfg = {
.port = i2c_port, .port = i2c_port,
.addr = es8374_addr, .addr = es8374_addr,
.bus_handle = i2c_master_handle, .bus_handle = i2c_master_handle,
}; };
ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(ctrl_if_ != NULL); assert(ctrl_if_ != NULL);
gpio_if_ = audio_codec_new_gpio(); gpio_if_ = audio_codec_new_gpio();
assert(gpio_if_ != NULL); assert(gpio_if_ != NULL);
es8374_codec_cfg_t es8374_cfg = {}; es8374_codec_cfg_t es8374_cfg = {};
es8374_cfg.ctrl_if = ctrl_if_; es8374_cfg.ctrl_if = ctrl_if_;
es8374_cfg.gpio_if = gpio_if_; es8374_cfg.gpio_if = gpio_if_;
es8374_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; es8374_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
es8374_cfg.pa_pin = pa_pin; es8374_cfg.pa_pin = pa_pin;
codec_if_ = es8374_codec_new(&es8374_cfg); codec_if_ = es8374_codec_new(&es8374_cfg);
assert(codec_if_ != NULL); assert(codec_if_ != NULL);
esp_codec_dev_cfg_t dev_cfg = { esp_codec_dev_cfg_t dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT, .dev_type = ESP_CODEC_DEV_TYPE_OUT,
.codec_if = codec_if_, .codec_if = codec_if_,
.data_if = data_if_, .data_if = data_if_,
}; };
output_dev_ = esp_codec_dev_new(&dev_cfg); output_dev_ = esp_codec_dev_new(&dev_cfg);
assert(output_dev_ != NULL); assert(output_dev_ != NULL);
dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
input_dev_ = esp_codec_dev_new(&dev_cfg); input_dev_ = esp_codec_dev_new(&dev_cfg);
assert(input_dev_ != NULL); assert(input_dev_ != NULL);
esp_codec_set_disable_when_closed(output_dev_, false); esp_codec_set_disable_when_closed(output_dev_, false);
esp_codec_set_disable_when_closed(input_dev_, false); esp_codec_set_disable_when_closed(input_dev_, false);
ESP_LOGI(TAG, "Es8374AudioCodec initialized"); ESP_LOGI(TAG, "Es8374AudioCodec initialized");
} }
Es8374AudioCodec::~Es8374AudioCodec() { Es8374AudioCodec::~Es8374AudioCodec() {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
esp_codec_dev_delete(output_dev_); esp_codec_dev_delete(output_dev_);
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
esp_codec_dev_delete(input_dev_); esp_codec_dev_delete(input_dev_);
audio_codec_delete_codec_if(codec_if_); audio_codec_delete_codec_if(codec_if_);
audio_codec_delete_ctrl_if(ctrl_if_); audio_codec_delete_ctrl_if(ctrl_if_);
audio_codec_delete_gpio_if(gpio_if_); audio_codec_delete_gpio_if(gpio_if_);
audio_codec_delete_data_if(data_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) { 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_); assert(input_sample_rate_ == output_sample_rate_);
i2s_chan_config_t chan_cfg = { i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0, .id = I2S_NUM_0,
.role = I2S_ROLE_MASTER, .role = I2S_ROLE_MASTER,
.dma_desc_num = 6, .dma_desc_num = 6,
.dma_frame_num = 240, .dma_frame_num = 240,
.auto_clear_after_cb = true, .auto_clear_after_cb = true,
.auto_clear_before_cb = false, .auto_clear_before_cb = false,
.intr_priority = 0, .intr_priority = 0,
}; };
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = { i2s_std_config_t std_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_, .sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = I2S_MCLK_MULTIPLE_256,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
#endif #endif
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO, .slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH, .slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT, .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.left_align = true, .left_align = true,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false .bit_order_lsb = false
#endif #endif
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = mclk, .mclk = mclk,
.bclk = bclk, .bclk = bclk,
.ws = ws, .ws = ws,
.dout = dout, .dout = dout,
.din = din, .din = din,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_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(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
ESP_LOGI(TAG, "Duplex channels created"); ESP_LOGI(TAG, "Duplex channels created");
} }
void Es8374AudioCodec::SetOutputVolume(int volume) { void Es8374AudioCodec::SetOutputVolume(int volume) {
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
AudioCodec::SetOutputVolume(volume); AudioCodec::SetOutputVolume(volume);
} }
void Es8374AudioCodec::EnableInput(bool enable) { void Es8374AudioCodec::EnableInput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == input_enabled_) { if (enable == input_enabled_) {
return; return;
} }
if (enable) { if (enable) {
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = 1, .channel = 1,
.channel_mask = 0, .channel_mask = 0,
.sample_rate = (uint32_t)input_sample_rate_, .sample_rate = (uint32_t)input_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); 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)); ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, AUDIO_CODEC_DEFAULT_MIC_GAIN));
} else { } else {
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
} }
AudioCodec::EnableInput(enable); AudioCodec::EnableInput(enable);
} }
void Es8374AudioCodec::EnableOutput(bool enable) { void Es8374AudioCodec::EnableOutput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == output_enabled_) { if (enable == output_enabled_) {
return; return;
} }
if (enable) { if (enable) {
// Play 16bit 1 channel // Play 16bit 1 channel
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = 1, .channel = 1,
.channel_mask = 0, .channel_mask = 0,
.sample_rate = (uint32_t)output_sample_rate_, .sample_rate = (uint32_t)output_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
if (pa_pin_ != GPIO_NUM_NC) { if (pa_pin_ != GPIO_NUM_NC) {
gpio_set_level(pa_pin_, 1); gpio_set_level(pa_pin_, 1);
} }
} else { } else {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
if (pa_pin_ != GPIO_NUM_NC) { if (pa_pin_ != GPIO_NUM_NC) {
gpio_set_level(pa_pin_, 0); gpio_set_level(pa_pin_, 0);
} }
} }
AudioCodec::EnableOutput(enable); AudioCodec::EnableOutput(enable);
} }
int Es8374AudioCodec::Read(int16_t* dest, int samples) { int Es8374AudioCodec::Read(int16_t* dest, int samples) {
if (input_enabled_) { if (input_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }
int Es8374AudioCodec::Write(const int16_t* data, int samples) { int Es8374AudioCodec::Write(const int16_t* data, int samples) {
if (output_enabled_) { if (output_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }

View File

@@ -1,41 +1,41 @@
#ifndef _ES8374_AUDIO_CODEC_H #ifndef _ES8374_AUDIO_CODEC_H
#define _ES8374_AUDIO_CODEC_H #define _ES8374_AUDIO_CODEC_H
#include "audio_codec.h" #include "audio_codec.h"
#include <driver/i2c.h> #include <driver/i2c.h>
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_codec_dev.h> #include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h> #include <esp_codec_dev_defaults.h>
#include <mutex> #include <mutex>
class Es8374AudioCodec : public AudioCodec { class Es8374AudioCodec : public AudioCodec {
private: private:
const audio_codec_data_if_t* data_if_ = nullptr; const audio_codec_data_if_t* data_if_ = nullptr;
const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
const audio_codec_if_t* codec_if_ = nullptr; const audio_codec_if_t* codec_if_ = nullptr;
const audio_codec_gpio_if_t* gpio_if_ = nullptr; const audio_codec_gpio_if_t* gpio_if_ = nullptr;
esp_codec_dev_handle_t output_dev_ = nullptr; esp_codec_dev_handle_t output_dev_ = nullptr;
esp_codec_dev_handle_t input_dev_ = nullptr; esp_codec_dev_handle_t input_dev_ = nullptr;
gpio_num_t pa_pin_ = GPIO_NUM_NC; gpio_num_t pa_pin_ = GPIO_NUM_NC;
std::mutex data_if_mutex_; 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 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 Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override; virtual int Write(const int16_t* data, int samples) override;
public: public:
Es8374AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, 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 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); gpio_num_t pa_pin, uint8_t es8374_addr, bool use_mclk = true);
virtual ~Es8374AudioCodec(); virtual ~Es8374AudioCodec();
virtual void SetOutputVolume(int volume) override; virtual void SetOutputVolume(int volume) override;
virtual void EnableInput(bool enable) override; virtual void EnableInput(bool enable) override;
virtual void EnableOutput(bool enable) override; virtual void EnableOutput(bool enable) override;
}; };
#endif // _ES8374_AUDIO_CODEC_H #endif // _ES8374_AUDIO_CODEC_H

View File

@@ -1,219 +1,219 @@
#include "es8388_audio_codec.h" #include "es8388_audio_codec.h"
#include <esp_log.h> #include <esp_log.h>
#define TAG "Es8388AudioCodec" #define TAG "Es8388AudioCodec"
Es8388AudioCodec::Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, 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 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) { gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference) {
duplex_ = true; // 是否双工 duplex_ = true; // 是否双工
input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 input_reference_ = input_reference; // 是否使用参考输入,实现回声消除
input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 input_channels_ = input_reference_ ? 2 : 1; // 输入通道数
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
pa_pin_ = pa_pin; pa_pin_ = pa_pin;
CreateDuplexChannels(mclk, bclk, ws, dout, din); CreateDuplexChannels(mclk, bclk, ws, dout, din);
// Do initialize of related interface: data_if, ctrl_if and gpio_if // Do initialize of related interface: data_if, ctrl_if and gpio_if
audio_codec_i2s_cfg_t i2s_cfg = { audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_NUM_0, .port = I2S_NUM_0,
.rx_handle = rx_handle_, .rx_handle = rx_handle_,
.tx_handle = tx_handle_, .tx_handle = tx_handle_,
}; };
data_if_ = audio_codec_new_i2s_data(&i2s_cfg); data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
assert(data_if_ != NULL); assert(data_if_ != NULL);
// Output // Output
audio_codec_i2c_cfg_t i2c_cfg = { audio_codec_i2c_cfg_t i2c_cfg = {
.port = i2c_port, .port = i2c_port,
.addr = es8388_addr, .addr = es8388_addr,
.bus_handle = i2c_master_handle, .bus_handle = i2c_master_handle,
}; };
ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(ctrl_if_ != NULL); assert(ctrl_if_ != NULL);
gpio_if_ = audio_codec_new_gpio(); gpio_if_ = audio_codec_new_gpio();
assert(gpio_if_ != NULL); assert(gpio_if_ != NULL);
es8388_codec_cfg_t es8388_cfg = {}; es8388_codec_cfg_t es8388_cfg = {};
es8388_cfg.ctrl_if = ctrl_if_; es8388_cfg.ctrl_if = ctrl_if_;
es8388_cfg.gpio_if = gpio_if_; es8388_cfg.gpio_if = gpio_if_;
es8388_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; es8388_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
es8388_cfg.master_mode = true; es8388_cfg.master_mode = true;
es8388_cfg.pa_pin = pa_pin; es8388_cfg.pa_pin = pa_pin;
es8388_cfg.pa_reverted = false; es8388_cfg.pa_reverted = false;
es8388_cfg.hw_gain.pa_voltage = 5.0; es8388_cfg.hw_gain.pa_voltage = 5.0;
es8388_cfg.hw_gain.codec_dac_voltage = 3.3; es8388_cfg.hw_gain.codec_dac_voltage = 3.3;
codec_if_ = es8388_codec_new(&es8388_cfg); codec_if_ = es8388_codec_new(&es8388_cfg);
assert(codec_if_ != NULL); assert(codec_if_ != NULL);
esp_codec_dev_cfg_t outdev_cfg = { esp_codec_dev_cfg_t outdev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT, .dev_type = ESP_CODEC_DEV_TYPE_OUT,
.codec_if = codec_if_, .codec_if = codec_if_,
.data_if = data_if_, .data_if = data_if_,
}; };
output_dev_ = esp_codec_dev_new(&outdev_cfg); output_dev_ = esp_codec_dev_new(&outdev_cfg);
assert(output_dev_ != NULL); assert(output_dev_ != NULL);
esp_codec_dev_cfg_t indev_cfg = { esp_codec_dev_cfg_t indev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_IN, .dev_type = ESP_CODEC_DEV_TYPE_IN,
.codec_if = codec_if_, .codec_if = codec_if_,
.data_if = data_if_, .data_if = data_if_,
}; };
input_dev_ = esp_codec_dev_new(&indev_cfg); input_dev_ = esp_codec_dev_new(&indev_cfg);
assert(input_dev_ != NULL); assert(input_dev_ != NULL);
esp_codec_set_disable_when_closed(output_dev_, false); esp_codec_set_disable_when_closed(output_dev_, false);
esp_codec_set_disable_when_closed(input_dev_, false); esp_codec_set_disable_when_closed(input_dev_, false);
ESP_LOGI(TAG, "Es8388AudioCodec initialized"); ESP_LOGI(TAG, "Es8388AudioCodec initialized");
} }
Es8388AudioCodec::~Es8388AudioCodec() { Es8388AudioCodec::~Es8388AudioCodec() {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
esp_codec_dev_delete(output_dev_); esp_codec_dev_delete(output_dev_);
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
esp_codec_dev_delete(input_dev_); esp_codec_dev_delete(input_dev_);
audio_codec_delete_codec_if(codec_if_); audio_codec_delete_codec_if(codec_if_);
audio_codec_delete_ctrl_if(ctrl_if_); audio_codec_delete_ctrl_if(ctrl_if_);
audio_codec_delete_gpio_if(gpio_if_); audio_codec_delete_gpio_if(gpio_if_);
audio_codec_delete_data_if(data_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){ 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_); assert(input_sample_rate_ == output_sample_rate_);
i2s_chan_config_t chan_cfg = { i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0, .id = I2S_NUM_0,
.role = I2S_ROLE_MASTER, .role = I2S_ROLE_MASTER,
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
.auto_clear_after_cb = true, .auto_clear_after_cb = true,
.auto_clear_before_cb = false, .auto_clear_before_cb = false,
.intr_priority = 0, .intr_priority = 0,
}; };
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = { i2s_std_config_t std_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_, .sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256 .mclk_multiple = I2S_MCLK_MULTIPLE_256
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO, .slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH, .slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT, .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
.left_align = true, .left_align = true,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false .bit_order_lsb = false
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = mclk, .mclk = mclk,
.bclk = bclk, .bclk = bclk,
.ws = ws, .ws = ws,
.dout = dout, .dout = dout,
.din = din, .din = din,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_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(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
ESP_LOGI(TAG, "Duplex channels created"); ESP_LOGI(TAG, "Duplex channels created");
} }
void Es8388AudioCodec::SetOutputVolume(int volume) { void Es8388AudioCodec::SetOutputVolume(int volume) {
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
AudioCodec::SetOutputVolume(volume); AudioCodec::SetOutputVolume(volume);
} }
void Es8388AudioCodec::EnableInput(bool enable) { void Es8388AudioCodec::EnableInput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == input_enabled_) { if (enable == input_enabled_) {
return; return;
} }
if (enable) { if (enable) {
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = (uint8_t) input_channels_, .channel = (uint8_t) input_channels_,
.channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
.sample_rate = (uint32_t)input_sample_rate_, .sample_rate = (uint32_t)input_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
if (input_reference_) { if (input_reference_) {
fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); 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_open(input_dev_, &fs));
if (input_reference_) { if (input_reference_) {
uint8_t gain = (11 << 4) + 0; uint8_t gain = (11 << 4) + 0;
ctrl_if_->write_reg(ctrl_if_, 0x09, 1, &gain, 1); ctrl_if_->write_reg(ctrl_if_, 0x09, 1, &gain, 1);
}else{ }else{
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0)); ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 24.0));
} }
} else { } else {
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
} }
AudioCodec::EnableInput(enable); AudioCodec::EnableInput(enable);
} }
void Es8388AudioCodec::EnableOutput(bool enable) { void Es8388AudioCodec::EnableOutput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == output_enabled_) { if (enable == output_enabled_) {
return; return;
} }
if (enable) { if (enable) {
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = 1, .channel = 1,
.channel_mask = 0, .channel_mask = 0,
.sample_rate = (uint32_t)output_sample_rate_, .sample_rate = (uint32_t)output_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
// Set analog output volume to 0dB, default is -45dB // Set analog output volume to 0dB, default is -45dB
uint8_t reg_val = 30; // 0dB uint8_t reg_val = 30; // 0dB
if(input_reference_){ if(input_reference_){
reg_val = 27; reg_val = 27;
} }
uint8_t regs[] = { 46, 47, 48, 49 }; // HP_LVOL, HP_RVOL, SPK_LVOL, SPK_RVOL uint8_t regs[] = { 46, 47, 48, 49 }; // HP_LVOL, HP_RVOL, SPK_LVOL, SPK_RVOL
for (uint8_t reg : regs) { for (uint8_t reg : regs) {
ctrl_if_->write_reg(ctrl_if_, reg, 1, &reg_val, 1); ctrl_if_->write_reg(ctrl_if_, reg, 1, &reg_val, 1);
} }
if (pa_pin_ != GPIO_NUM_NC) { if (pa_pin_ != GPIO_NUM_NC) {
gpio_set_level(pa_pin_, 1); gpio_set_level(pa_pin_, 1);
} }
} else { } else {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
if (pa_pin_ != GPIO_NUM_NC) { if (pa_pin_ != GPIO_NUM_NC) {
gpio_set_level(pa_pin_, 0); gpio_set_level(pa_pin_, 0);
} }
} }
AudioCodec::EnableOutput(enable); AudioCodec::EnableOutput(enable);
} }
int Es8388AudioCodec::Read(int16_t* dest, int samples) { int Es8388AudioCodec::Read(int16_t* dest, int samples) {
if (input_enabled_) { if (input_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }
int Es8388AudioCodec::Write(const int16_t* data, int samples) { int Es8388AudioCodec::Write(const int16_t* data, int samples) {
if (output_enabled_ && output_dev_ && data != nullptr) { if (output_enabled_ && output_dev_ && data != nullptr) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }

View File

@@ -1,40 +1,40 @@
#ifndef _ES8388_AUDIO_CODEC_H #ifndef _ES8388_AUDIO_CODEC_H
#define _ES8388_AUDIO_CODEC_H #define _ES8388_AUDIO_CODEC_H
#include "audio_codec.h" #include "audio_codec.h"
#include <driver/i2c_master.h> #include <driver/i2c_master.h>
#include <esp_codec_dev.h> #include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h> #include <esp_codec_dev_defaults.h>
#include <mutex> #include <mutex>
class Es8388AudioCodec : public AudioCodec { class Es8388AudioCodec : public AudioCodec {
private: private:
const audio_codec_data_if_t* data_if_ = nullptr; const audio_codec_data_if_t* data_if_ = nullptr;
const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
const audio_codec_if_t* codec_if_ = nullptr; const audio_codec_if_t* codec_if_ = nullptr;
const audio_codec_gpio_if_t* gpio_if_ = nullptr; const audio_codec_gpio_if_t* gpio_if_ = nullptr;
esp_codec_dev_handle_t output_dev_ = nullptr; esp_codec_dev_handle_t output_dev_ = nullptr;
esp_codec_dev_handle_t input_dev_ = nullptr; esp_codec_dev_handle_t input_dev_ = nullptr;
gpio_num_t pa_pin_ = GPIO_NUM_NC; gpio_num_t pa_pin_ = GPIO_NUM_NC;
std::mutex data_if_mutex_; 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 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 Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override; virtual int Write(const int16_t* data, int samples) override;
public: public:
Es8388AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, 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 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); gpio_num_t pa_pin, uint8_t es8388_addr, bool input_reference = false);
virtual ~Es8388AudioCodec(); virtual ~Es8388AudioCodec();
virtual void SetOutputVolume(int volume) override; virtual void SetOutputVolume(int volume) override;
virtual void EnableInput(bool enable) override; virtual void EnableInput(bool enable) override;
virtual void EnableOutput(bool enable) override; virtual void EnableOutput(bool enable) override;
}; };
#endif // _ES8388_AUDIO_CODEC_H #endif // _ES8388_AUDIO_CODEC_H

View File

@@ -1,203 +1,203 @@
#include "es8389_audio_codec.h" #include "es8389_audio_codec.h"
#include <esp_log.h> #include <esp_log.h>
static const char TAG[] = "Es8389AudioCodec"; static const char TAG[] = "Es8389AudioCodec";
Es8389AudioCodec::Es8389AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, 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 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) { gpio_num_t pa_pin, uint8_t es8389_addr, bool use_mclk) {
duplex_ = true; // 是否双工 duplex_ = true; // 是否双工
input_reference_ = false; // 是否使用参考输入,实现回声消除 input_reference_ = false; // 是否使用参考输入,实现回声消除
input_channels_ = 1; // 输入通道数 input_channels_ = 1; // 输入通道数
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
pa_pin_ = pa_pin; pa_pin_ = pa_pin;
CreateDuplexChannels(mclk, bclk, ws, dout, din); CreateDuplexChannels(mclk, bclk, ws, dout, din);
// Do initialize of related interface: data_if, ctrl_if and gpio_if // Do initialize of related interface: data_if, ctrl_if and gpio_if
audio_codec_i2s_cfg_t i2s_cfg = { audio_codec_i2s_cfg_t i2s_cfg = {
.port = I2S_NUM_0, .port = I2S_NUM_0,
.rx_handle = rx_handle_, .rx_handle = rx_handle_,
.tx_handle = tx_handle_, .tx_handle = tx_handle_,
}; };
data_if_ = audio_codec_new_i2s_data(&i2s_cfg); data_if_ = audio_codec_new_i2s_data(&i2s_cfg);
assert(data_if_ != NULL); assert(data_if_ != NULL);
// Output // Output
audio_codec_i2c_cfg_t i2c_cfg = { audio_codec_i2c_cfg_t i2c_cfg = {
.port = i2c_port, .port = i2c_port,
.addr = es8389_addr, .addr = es8389_addr,
.bus_handle = i2c_master_handle, .bus_handle = i2c_master_handle,
}; };
ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(ctrl_if_ != NULL); assert(ctrl_if_ != NULL);
gpio_if_ = audio_codec_new_gpio(); gpio_if_ = audio_codec_new_gpio();
assert(gpio_if_ != NULL); assert(gpio_if_ != NULL);
es8389_codec_cfg_t es8389_cfg = {}; es8389_codec_cfg_t es8389_cfg = {};
es8389_cfg.ctrl_if = ctrl_if_; es8389_cfg.ctrl_if = ctrl_if_;
es8389_cfg.gpio_if = gpio_if_; es8389_cfg.gpio_if = gpio_if_;
es8389_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH; es8389_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
es8389_cfg.pa_pin = pa_pin; es8389_cfg.pa_pin = pa_pin;
es8389_cfg.use_mclk = use_mclk; es8389_cfg.use_mclk = use_mclk;
es8389_cfg.hw_gain.pa_voltage = 5.0; es8389_cfg.hw_gain.pa_voltage = 5.0;
es8389_cfg.hw_gain.codec_dac_voltage = 3.3; es8389_cfg.hw_gain.codec_dac_voltage = 3.3;
codec_if_ = es8389_codec_new(&es8389_cfg); codec_if_ = es8389_codec_new(&es8389_cfg);
assert(codec_if_ != NULL); assert(codec_if_ != NULL);
esp_codec_dev_cfg_t outdev_cfg = { esp_codec_dev_cfg_t outdev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT, .dev_type = ESP_CODEC_DEV_TYPE_OUT,
.codec_if = codec_if_, .codec_if = codec_if_,
.data_if = data_if_, .data_if = data_if_,
}; };
output_dev_ = esp_codec_dev_new(&outdev_cfg); output_dev_ = esp_codec_dev_new(&outdev_cfg);
assert(output_dev_ != NULL); assert(output_dev_ != NULL);
esp_codec_dev_cfg_t indev_cfg = { esp_codec_dev_cfg_t indev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_IN, .dev_type = ESP_CODEC_DEV_TYPE_IN,
.codec_if = codec_if_, .codec_if = codec_if_,
.data_if = data_if_, .data_if = data_if_,
}; };
input_dev_ = esp_codec_dev_new(&indev_cfg); input_dev_ = esp_codec_dev_new(&indev_cfg);
assert(input_dev_ != NULL); assert(input_dev_ != NULL);
esp_codec_set_disable_when_closed(output_dev_, false); esp_codec_set_disable_when_closed(output_dev_, false);
esp_codec_set_disable_when_closed(input_dev_, false); esp_codec_set_disable_when_closed(input_dev_, false);
ESP_LOGI(TAG, "Es8389AudioCodec initialized"); ESP_LOGI(TAG, "Es8389AudioCodec initialized");
} }
Es8389AudioCodec::~Es8389AudioCodec() { Es8389AudioCodec::~Es8389AudioCodec() {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
esp_codec_dev_delete(output_dev_); esp_codec_dev_delete(output_dev_);
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
esp_codec_dev_delete(input_dev_); esp_codec_dev_delete(input_dev_);
audio_codec_delete_codec_if(codec_if_); audio_codec_delete_codec_if(codec_if_);
audio_codec_delete_ctrl_if(ctrl_if_); audio_codec_delete_ctrl_if(ctrl_if_);
audio_codec_delete_gpio_if(gpio_if_); audio_codec_delete_gpio_if(gpio_if_);
audio_codec_delete_data_if(data_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) { 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_); assert(input_sample_rate_ == output_sample_rate_);
i2s_chan_config_t chan_cfg = { i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0, .id = I2S_NUM_0,
.role = I2S_ROLE_MASTER, .role = I2S_ROLE_MASTER,
.dma_desc_num = 6, .dma_desc_num = 6,
.dma_frame_num = 240, .dma_frame_num = 240,
.auto_clear_after_cb = true, .auto_clear_after_cb = true,
.auto_clear_before_cb = false, .auto_clear_before_cb = false,
.intr_priority = 0, .intr_priority = 0,
}; };
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = { i2s_std_config_t std_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_, .sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = I2S_MCLK_MULTIPLE_256,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
#endif #endif
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO, .slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH, .slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT, .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
.left_align = true, .left_align = true,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false .bit_order_lsb = false
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = mclk, .mclk = mclk,
.bclk = bclk, .bclk = bclk,
.ws = ws, .ws = ws,
.dout = dout, .dout = dout,
.din = din, .din = din,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_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(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
ESP_LOGI(TAG, "Duplex channels created"); ESP_LOGI(TAG, "Duplex channels created");
} }
void Es8389AudioCodec::SetOutputVolume(int volume) { void Es8389AudioCodec::SetOutputVolume(int volume) {
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume));
AudioCodec::SetOutputVolume(volume); AudioCodec::SetOutputVolume(volume);
} }
void Es8389AudioCodec::EnableInput(bool enable) { void Es8389AudioCodec::EnableInput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == input_enabled_) { if (enable == input_enabled_) {
return; return;
} }
if (enable) { if (enable) {
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = 1, .channel = 1,
.channel_mask = 0, .channel_mask = 0,
.sample_rate = (uint32_t)input_sample_rate_, .sample_rate = (uint32_t)input_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 40.0)); ESP_ERROR_CHECK(esp_codec_dev_set_in_gain(input_dev_, 40.0));
} else { } else {
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
} }
AudioCodec::EnableInput(enable); AudioCodec::EnableInput(enable);
} }
void Es8389AudioCodec::EnableOutput(bool enable) { void Es8389AudioCodec::EnableOutput(bool enable) {
std::lock_guard<std::mutex> lock(data_if_mutex_); std::lock_guard<std::mutex> lock(data_if_mutex_);
if (enable == output_enabled_) { if (enable == output_enabled_) {
return; return;
} }
if (enable) { if (enable) {
// Play 16bit 1 channel // Play 16bit 1 channel
esp_codec_dev_sample_info_t fs = { esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16, .bits_per_sample = 16,
.channel = 1, .channel = 1,
.channel_mask = 0, .channel_mask = 0,
.sample_rate = (uint32_t)output_sample_rate_, .sample_rate = (uint32_t)output_sample_rate_,
.mclk_multiple = 0, .mclk_multiple = 0,
}; };
ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
if (pa_pin_ != GPIO_NUM_NC) { if (pa_pin_ != GPIO_NUM_NC) {
gpio_set_level(pa_pin_, 1); gpio_set_level(pa_pin_, 1);
} }
} else { } else {
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
if (pa_pin_ != GPIO_NUM_NC) { if (pa_pin_ != GPIO_NUM_NC) {
gpio_set_level(pa_pin_, 0); gpio_set_level(pa_pin_, 0);
} }
} }
AudioCodec::EnableOutput(enable); AudioCodec::EnableOutput(enable);
} }
int Es8389AudioCodec::Read(int16_t* dest, int samples) { int Es8389AudioCodec::Read(int16_t* dest, int samples) {
if (input_enabled_) { if (input_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, (void*)dest, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }
int Es8389AudioCodec::Write(const int16_t* data, int samples) { int Es8389AudioCodec::Write(const int16_t* data, int samples) {
if (output_enabled_) { if (output_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t))); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
} }
return samples; return samples;
} }

View File

@@ -1,40 +1,40 @@
#ifndef _ES8389_AUDIO_CODEC_H #ifndef _ES8389_AUDIO_CODEC_H
#define _ES8389_AUDIO_CODEC_H #define _ES8389_AUDIO_CODEC_H
#include "audio_codec.h" #include "audio_codec.h"
#include <driver/i2c.h> #include <driver/i2c.h>
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_codec_dev.h> #include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h> #include <esp_codec_dev_defaults.h>
#include <mutex> #include <mutex>
class Es8389AudioCodec : public AudioCodec { class Es8389AudioCodec : public AudioCodec {
private: private:
const audio_codec_data_if_t* data_if_ = nullptr; const audio_codec_data_if_t* data_if_ = nullptr;
const audio_codec_ctrl_if_t* ctrl_if_ = nullptr; const audio_codec_ctrl_if_t* ctrl_if_ = nullptr;
const audio_codec_if_t* codec_if_ = nullptr; const audio_codec_if_t* codec_if_ = nullptr;
const audio_codec_gpio_if_t* gpio_if_ = nullptr; const audio_codec_gpio_if_t* gpio_if_ = nullptr;
esp_codec_dev_handle_t output_dev_ = nullptr; esp_codec_dev_handle_t output_dev_ = nullptr;
esp_codec_dev_handle_t input_dev_ = nullptr; esp_codec_dev_handle_t input_dev_ = nullptr;
gpio_num_t pa_pin_ = GPIO_NUM_NC; gpio_num_t pa_pin_ = GPIO_NUM_NC;
std::mutex data_if_mutex_; 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 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 Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override; virtual int Write(const int16_t* data, int samples) override;
public: public:
Es8389AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate, 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 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); gpio_num_t pa_pin, uint8_t es8389_addr, bool use_mclk = true);
virtual ~Es8389AudioCodec(); virtual ~Es8389AudioCodec();
virtual void SetOutputVolume(int volume) override; virtual void SetOutputVolume(int volume) override;
virtual void EnableInput(bool enable) override; virtual void EnableInput(bool enable) override;
virtual void EnableOutput(bool enable) override; virtual void EnableOutput(bool enable) override;
}; };
#endif // _ES8389_AUDIO_CODEC_H #endif // _ES8389_AUDIO_CODEC_H

View File

@@ -1,418 +1,410 @@
#include "no_audio_codec.h" #include "no_audio_codec.h"
#include <esp_log.h> #include <esp_log.h>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#define TAG "NoAudioCodec" #define TAG "NoAudioCodec"
NoAudioCodec::~NoAudioCodec() { NoAudioCodec::~NoAudioCodec() {
if (rx_handle_ != nullptr) { if (rx_handle_ != nullptr) {
ESP_ERROR_CHECK(i2s_channel_disable(rx_handle_)); ESP_ERROR_CHECK(i2s_channel_disable(rx_handle_));
} }
if (tx_handle_ != nullptr) { if (tx_handle_ != nullptr) {
ESP_ERROR_CHECK(i2s_channel_disable(tx_handle_)); 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) { 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; duplex_ = true;
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
i2s_chan_config_t chan_cfg = { i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0, .id = I2S_NUM_0,
.role = I2S_ROLE_MASTER, .role = I2S_ROLE_MASTER,
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
.auto_clear_after_cb = true, .auto_clear_after_cb = true,
.auto_clear_before_cb = false, .auto_clear_before_cb = false,
.intr_priority = 0, .intr_priority = 0,
}; };
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = { i2s_std_config_t std_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_, .sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = I2S_MCLK_MULTIPLE_256,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
#endif #endif
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_MONO, .slot_mode = I2S_SLOT_MODE_MONO,
.slot_mask = I2S_STD_SLOT_LEFT, .slot_mask = I2S_STD_SLOT_LEFT,
.ws_width = I2S_DATA_BIT_WIDTH_32BIT, .ws_width = I2S_DATA_BIT_WIDTH_32BIT,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.left_align = true, .left_align = true,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false .bit_order_lsb = false
#endif #endif
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = I2S_GPIO_UNUSED, .mclk = I2S_GPIO_UNUSED,
.bclk = bclk, .bclk = bclk,
.ws = ws, .ws = ws,
.dout = dout, .dout = dout,
.din = din, .din = din,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_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(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
ESP_LOGI(TAG, "Duplex channels created"); 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) { 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; duplex_ = false;
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
// Create a new channel for speaker // Create a new channel for speaker
i2s_chan_config_t chan_cfg = { i2s_chan_config_t chan_cfg = {
.id = (i2s_port_t)0, .id = (i2s_port_t)0,
.role = I2S_ROLE_MASTER, .role = I2S_ROLE_MASTER,
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
.auto_clear_after_cb = true, .auto_clear_after_cb = true,
.auto_clear_before_cb = false, .auto_clear_before_cb = false,
.intr_priority = 0, .intr_priority = 0,
}; };
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr)); ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr));
i2s_std_config_t std_cfg = { i2s_std_config_t std_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_, .sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = I2S_MCLK_MULTIPLE_256,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
#endif #endif
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO, .slot_mode = I2S_SLOT_MODE_MONO,
.slot_mask = I2S_STD_SLOT_BOTH, .slot_mask = I2S_STD_SLOT_LEFT,
.ws_width = I2S_DATA_BIT_WIDTH_32BIT, .ws_width = I2S_DATA_BIT_WIDTH_32BIT,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.left_align = true, .left_align = true,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false .bit_order_lsb = false
#endif #endif
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = I2S_GPIO_UNUSED, .mclk = I2S_GPIO_UNUSED,
.bclk = spk_bclk, .bclk = spk_bclk,
.ws = spk_ws, .ws = spk_ws,
.dout = spk_dout, .dout = spk_dout,
.din = I2S_GPIO_UNUSED, .din = I2S_GPIO_UNUSED,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_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(tx_handle_, &std_cfg));
// Create a new channel for MIC // Create a new channel for MIC
chan_cfg.id = (i2s_port_t)1; chan_cfg.id = (i2s_port_t)1;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_)); 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.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
// RX 使用单声道 LEFT std_cfg.gpio_cfg.bclk = mic_sck;
std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO; std_cfg.gpio_cfg.ws = mic_ws;
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT; std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
std_cfg.gpio_cfg.bclk = mic_sck; std_cfg.gpio_cfg.din = mic_din;
std_cfg.gpio_cfg.ws = mic_ws; ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; ESP_LOGI(TAG, "Simplex channels created");
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;
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){ output_sample_rate_ = output_sample_rate;
duplex_ = false;
input_sample_rate_ = input_sample_rate; // Create a new channel for speaker
output_sample_rate_ = output_sample_rate; i2s_chan_config_t chan_cfg = {
.id = (i2s_port_t)0,
// Create a new channel for speaker .role = I2S_ROLE_MASTER,
i2s_chan_config_t chan_cfg = { .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
.id = (i2s_port_t)0, .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
.role = I2S_ROLE_MASTER, .auto_clear_after_cb = true,
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, .auto_clear_before_cb = false,
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, .intr_priority = 0,
.auto_clear_after_cb = true, };
.auto_clear_before_cb = false, ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr));
.intr_priority = 0,
}; i2s_std_config_t std_cfg = {
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, nullptr)); .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_,
i2s_std_config_t std_cfg = { .clk_src = I2S_CLK_SRC_DEFAULT,
.clk_cfg = { .mclk_multiple = I2S_MCLK_MULTIPLE_256,
.sample_rate_hz = (uint32_t)output_sample_rate_, #ifdef I2S_HW_VERSION_2
.clk_src = I2S_CLK_SRC_DEFAULT, .ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, #endif
#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_cfg = { .slot_mode = I2S_SLOT_MODE_MONO,
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, .slot_mask = spk_slot_mask,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .ws_width = I2S_DATA_BIT_WIDTH_32BIT,
.slot_mode = I2S_SLOT_MODE_MONO, .ws_pol = false,
.slot_mask = spk_slot_mask, .bit_shift = true,
.ws_width = I2S_DATA_BIT_WIDTH_32BIT, #ifdef I2S_HW_VERSION_2
.ws_pol = false, .left_align = true,
.bit_shift = true, .big_endian = false,
#ifdef I2S_HW_VERSION_2 .bit_order_lsb = false
.left_align = true, #endif
.big_endian = false,
.bit_order_lsb = false },
#endif .gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
}, .bclk = spk_bclk,
.gpio_cfg = { .ws = spk_ws,
.mclk = I2S_GPIO_UNUSED, .dout = spk_dout,
.bclk = spk_bclk, .din = I2S_GPIO_UNUSED,
.ws = spk_ws, .invert_flags = {
.dout = spk_dout, .mclk_inv = false,
.din = I2S_GPIO_UNUSED, .bclk_inv = false,
.invert_flags = { .ws_inv = false
.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
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); chan_cfg.id = (i2s_port_t)1;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_));
// Create a new channel for MIC std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
chan_cfg.id = (i2s_port_t)1; std_cfg.slot_cfg.slot_mask = mic_slot_mask;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_)); std_cfg.gpio_cfg.bclk = mic_sck;
std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_; std_cfg.gpio_cfg.ws = mic_ws;
std_cfg.slot_cfg.slot_mask = mic_slot_mask; std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
std_cfg.gpio_cfg.bclk = mic_sck; std_cfg.gpio_cfg.din = mic_din;
std_cfg.gpio_cfg.ws = mic_ws; ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED; ESP_LOGI(TAG, "Simplex channels created");
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, i2s_std_slot_mask_t spk_slot_mask,gpio_num_t mic_sck, gpio_num_t mic_din) {
} duplex_ = false;
input_sample_rate_ = input_sample_rate;
NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask,gpio_num_t mic_sck, gpio_num_t mic_din) { output_sample_rate_ = output_sample_rate;
duplex_ = false;
input_sample_rate_ = input_sample_rate; // Create a new channel for speaker
output_sample_rate_ = output_sample_rate; 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;
// Create a new channel for speaker tx_chan_cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM;
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)1, I2S_ROLE_MASTER); tx_chan_cfg.auto_clear_after_cb = true;
tx_chan_cfg.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM; tx_chan_cfg.auto_clear_before_cb = false;
tx_chan_cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM; tx_chan_cfg.intr_priority = 0;
tx_chan_cfg.auto_clear_after_cb = true; ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle_, NULL));
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_,
i2s_std_config_t tx_std_cfg = { .clk_src = I2S_CLK_SRC_DEFAULT,
.clk_cfg = { .mclk_multiple = I2S_MCLK_MULTIPLE_256,
.sample_rate_hz = (uint32_t)output_sample_rate_, #ifdef I2S_HW_VERSION_2
.clk_src = I2S_CLK_SRC_DEFAULT, .ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, #endif
#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_cfg = { .slot_mode = I2S_SLOT_MODE_MONO,
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT, .slot_mask = spk_slot_mask,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .ws_width = I2S_DATA_BIT_WIDTH_32BIT,
.slot_mode = I2S_SLOT_MODE_MONO, .ws_pol = false,
.slot_mask = spk_slot_mask, .bit_shift = true,
.ws_width = I2S_DATA_BIT_WIDTH_32BIT, #ifdef I2S_HW_VERSION_2
.ws_pol = false, .left_align = true,
.bit_shift = true, .big_endian = false,
#ifdef I2S_HW_VERSION_2 .bit_order_lsb = false
.left_align = true, #endif
.big_endian = false,
.bit_order_lsb = false },
#endif .gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
}, .bclk = spk_bclk,
.gpio_cfg = { .ws = spk_ws,
.mclk = I2S_GPIO_UNUSED, .dout = spk_dout,
.bclk = spk_bclk, .din = I2S_GPIO_UNUSED,
.ws = spk_ws, .invert_flags = {
.dout = spk_dout, .mclk_inv = false,
.din = I2S_GPIO_UNUSED, .bclk_inv = false,
.invert_flags = { .ws_inv = false,
.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
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &tx_std_cfg)); i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER);
#if SOC_I2S_SUPPORTS_PDM_RX ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_));
// Create a new channel for MIC in PDM mode i2s_pdm_rx_config_t pdm_rx_cfg = {
i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER); .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_),
ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_)); /* The data bit-width of PDM mode is fixed to 16 */
i2s_pdm_rx_config_t pdm_rx_cfg = { .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_), .gpio_cfg = {
/* The data bit-width of PDM mode is fixed to 16 */ .clk = mic_sck,
.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), .din = mic_din,
.gpio_cfg = {
.clk = mic_sck, .invert_flags = {
.din = mic_din, .clk_inv = false,
},
.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");
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle_, &pdm_rx_cfg)); #endif
#else ESP_LOGI(TAG, "Simplex channels created");
ESP_LOGE(TAG, "PDM is not supported"); }
#endif
ESP_LOGI(TAG, "Simplex channels created"); NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din) {
} duplex_ = false;
input_sample_rate_ = input_sample_rate;
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) { output_sample_rate_ = output_sample_rate;
duplex_ = false;
input_sample_rate_ = input_sample_rate; // Create a new channel for speaker
output_sample_rate_ = output_sample_rate; 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;
// Create a new channel for speaker tx_chan_cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM;
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)1, I2S_ROLE_MASTER); tx_chan_cfg.auto_clear_after_cb = true;
tx_chan_cfg.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM; tx_chan_cfg.auto_clear_before_cb = false;
tx_chan_cfg.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM; tx_chan_cfg.intr_priority = 0;
tx_chan_cfg.auto_clear_after_cb = true; ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle_, NULL));
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_,
i2s_std_config_t tx_std_cfg = { .clk_src = I2S_CLK_SRC_DEFAULT,
.clk_cfg = { .mclk_multiple = I2S_MCLK_MULTIPLE_256,
.sample_rate_hz = (uint32_t)output_sample_rate_, #ifdef I2S_HW_VERSION_2
.clk_src = I2S_CLK_SRC_DEFAULT, .ext_clk_freq_hz = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, #endif
#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,
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), .bclk = spk_bclk,
.gpio_cfg = { .ws = spk_ws,
.mclk = I2S_GPIO_UNUSED, .dout = spk_dout,
.bclk = spk_bclk, .din = I2S_GPIO_UNUSED,
.ws = spk_ws, .invert_flags = {
.dout = spk_dout, .mclk_inv = false,
.din = I2S_GPIO_UNUSED, .bclk_inv = false,
.invert_flags = { .ws_inv = false,
.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
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &tx_std_cfg)); i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER);
#if SOC_I2S_SUPPORTS_PDM_RX ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_));
// Create a new channel for MIC in PDM mode i2s_pdm_rx_config_t pdm_rx_cfg = {
i2s_chan_config_t rx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG((i2s_port_t)0, I2S_ROLE_MASTER); .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_),
ESP_ERROR_CHECK(i2s_new_channel(&rx_chan_cfg, NULL, &rx_handle_)); /* The data bit-width of PDM mode is fixed to 16 */
i2s_pdm_rx_config_t pdm_rx_cfg = { .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)input_sample_rate_), .gpio_cfg = {
/* The data bit-width of PDM mode is fixed to 16 */ .clk = mic_sck,
.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), .din = mic_din,
.gpio_cfg = {
.clk = mic_sck, .invert_flags = {
.din = mic_din, .clk_inv = false,
},
.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");
ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(rx_handle_, &pdm_rx_cfg)); #endif
#else ESP_LOGI(TAG, "Simplex channels created");
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<std::mutex> lock(data_if_mutex_);
std::vector<int32_t> buffer(samples);
int NoAudioCodec::Write(const int16_t* data, int samples) {
std::lock_guard<std::mutex> lock(data_if_mutex_); // output_volume_: 0-100
// 立体声交织输出L,R,L,R ... 每声道32位 // volume_factor_: 0-65536
std::vector<int32_t> buffer(samples * 2); int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536;
for (int i = 0; i < samples; i++) {
// output_volume_: 0-100 int64_t temp = int64_t(data[i]) * volume_factor; // 使用 int64_t 进行乘法运算
// volume_factor_: 0-65536 if (temp > INT32_MAX) {
int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536; buffer[i] = INT32_MAX;
for (int i = 0; i < samples; i++) { } else if (temp < INT32_MIN) {
int64_t temp = int64_t(data[i]) * volume_factor; // 使用 int64_t 进行乘法运算 buffer[i] = INT32_MIN;
int32_t s32; } else {
if (temp > INT32_MAX) { buffer[i] = static_cast<int32_t>(temp);
s32 = INT32_MAX; }
} else if (temp < INT32_MIN) { }
s32 = INT32_MIN;
} else { size_t bytes_written;
s32 = static_cast<int32_t>(temp); ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY));
} return bytes_written / sizeof(int32_t);
// 交织到左右声道 }
buffer[2 * i] = s32; // Left
buffer[2 * i + 1] = s32; // Right复制 int NoAudioCodec::Read(int16_t* dest, int samples) {
} size_t bytes_read;
size_t bytes_written; std::vector<int32_t> bit32_buffer(samples);
ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), (samples * 2) * sizeof(int32_t), &bytes_written, portMAX_DELAY)); if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
return bytes_written / sizeof(int32_t) / 2; // 返回每声道样本数 ESP_LOGE(TAG, "Read Failed!");
} return 0;
}
int NoAudioCodec::Read(int16_t* dest, int samples) {
size_t bytes_read; samples = bytes_read / sizeof(int32_t);
for (int i = 0; i < samples; i++) {
std::vector<int32_t> bit32_buffer(samples); int32_t value = bit32_buffer[i] >> 12;
if (i2s_channel_read(rx_handle_, bit32_buffer.data(), samples * sizeof(int32_t), &bytes_read, portMAX_DELAY) != ESP_OK) { dest[i] = (value > INT16_MAX) ? INT16_MAX : (value < -INT16_MAX) ? -INT16_MAX : (int16_t)value;
ESP_LOGE(TAG, "Read Failed!"); }
return 0; return samples;
} }
samples = bytes_read / sizeof(int32_t); int NoAudioCodecSimplexPdm::Read(int16_t* dest, int samples) {
for (int i = 0; i < samples; i++) { size_t bytes_read;
int32_t value = bit32_buffer[i] >> 12;
dest[i] = (value > INT16_MAX) ? INT16_MAX : (value < -INT16_MAX) ? -INT16_MAX : (int16_t)value; // PDM 解调后的数据位宽为 16 位,直接读取到目标缓冲区
} if (i2s_channel_read(rx_handle_, dest, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY) != ESP_OK) {
return samples; ESP_LOGE(TAG, "Read Failed!");
} return 0;
}
int NoAudioCodecSimplexPdm::Read(int16_t* dest, int samples) {
size_t bytes_read; // 计算实际读取的样本数
return bytes_read / sizeof(int16_t);
// 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);
}

View File

@@ -1,39 +1,39 @@
#ifndef _NO_AUDIO_CODEC_H #ifndef _NO_AUDIO_CODEC_H
#define _NO_AUDIO_CODEC_H #define _NO_AUDIO_CODEC_H
#include "audio_codec.h" #include "audio_codec.h"
#include <driver/gpio.h> #include <driver/gpio.h>
#include <driver/i2s_pdm.h> #include <driver/i2s_pdm.h>
#include <mutex> #include <mutex>
class NoAudioCodec : public AudioCodec { class NoAudioCodec : public AudioCodec {
protected: protected:
std::mutex data_if_mutex_; std::mutex data_if_mutex_;
virtual int Write(const int16_t* data, int samples) override; virtual int Write(const int16_t* data, int samples) override;
virtual int Read(int16_t* dest, int samples) override; virtual int Read(int16_t* dest, int samples) override;
public: public:
virtual ~NoAudioCodec(); virtual ~NoAudioCodec();
}; };
class NoAudioCodecDuplex : public NoAudioCodec { class NoAudioCodecDuplex : public NoAudioCodec {
public: 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); 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 { class NoAudioCodecSimplex : public NoAudioCodec {
public: 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, 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); 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 { class NoAudioCodecSimplexPdm : public NoAudioCodec {
public: public:
NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din); NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, gpio_num_t mic_sck, gpio_num_t mic_din);
NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_din); NoAudioCodecSimplexPdm(int input_sample_rate, int output_sample_rate, gpio_num_t spk_bclk, gpio_num_t spk_ws, gpio_num_t spk_dout, i2s_std_slot_mask_t spk_slot_mask, gpio_num_t mic_sck, gpio_num_t mic_din);
int Read(int16_t* dest, int samples); int Read(int16_t* dest, int samples);
}; };
#endif // _NO_AUDIO_CODEC_H #endif // _NO_AUDIO_CODEC_H

View File

@@ -1,189 +1,187 @@
#include "afe_audio_processor.h" #include "afe_audio_processor.h"
#include <esp_log.h> #include <esp_log.h>
#define PROCESSOR_RUNNING 0x01 #define PROCESSOR_RUNNING 0x01
#define TAG "AfeAudioProcessor" #define TAG "AfeAudioProcessor"
AfeAudioProcessor::AfeAudioProcessor() AfeAudioProcessor::AfeAudioProcessor()
: afe_data_(nullptr) { : afe_data_(nullptr) {
event_group_ = xEventGroupCreate(); event_group_ = xEventGroupCreate();
} }
void AfeAudioProcessor::Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) { void AfeAudioProcessor::Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) {
codec_ = codec; codec_ = codec;
frame_samples_ = frame_duration_ms * 16000 / 1000; frame_samples_ = frame_duration_ms * 16000 / 1000;
// Pre-allocate output buffer capacity // Pre-allocate output buffer capacity
output_buffer_.reserve(frame_samples_); output_buffer_.reserve(frame_samples_);
int ref_num = codec_->input_reference() ? 1 : 0; int ref_num = codec_->input_reference() ? 1 : 0;
std::string input_format; std::string input_format;
for (int i = 0; i < codec_->input_channels() - ref_num; i++) { for (int i = 0; i < codec_->input_channels() - ref_num; i++) {
input_format.push_back('M'); input_format.push_back('M');
} }
for (int i = 0; i < ref_num; i++) { for (int i = 0; i < ref_num; i++) {
input_format.push_back('R'); input_format.push_back('R');
} }
srmodel_list_t *models; srmodel_list_t *models;
if (models_list == nullptr) { if (models_list == nullptr) {
models = esp_srmodel_init("model"); models = esp_srmodel_init("model");
} else { } else {
models = models_list; models = models_list;
} }
char* ns_model_name = esp_srmodel_filter(models, ESP_NSNET_PREFIX, NULL); char* ns_model_name = esp_srmodel_filter(models, ESP_NSNET_PREFIX, NULL);
char* vad_model_name = esp_srmodel_filter(models, ESP_VADN_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_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->aec_mode = AEC_MODE_VOIP_HIGH_PERF;
afe_config->vad_mode = VAD_MODE_0; afe_config->vad_mode = VAD_MODE_0;
afe_config->vad_min_noise_ms = 100; afe_config->vad_min_noise_ms = 100;
if (vad_model_name != nullptr) { if (vad_model_name != nullptr) {
afe_config->vad_model_name = vad_model_name; afe_config->vad_model_name = vad_model_name;
} }
if (ns_model_name != nullptr) { if (ns_model_name != nullptr) {
afe_config->ns_init = true; afe_config->ns_init = true;
afe_config->ns_model_name = ns_model_name; afe_config->ns_model_name = ns_model_name;
afe_config->afe_ns_mode = AFE_NS_MODE_NET; afe_config->afe_ns_mode = AFE_NS_MODE_NET;
} else { } else {
afe_config->ns_init = false; afe_config->ns_init = false;
} }
afe_config->afe_perferred_core = 1; afe_config->agc_init = false;
afe_config->afe_perferred_priority = 1; afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM;
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;
#ifdef CONFIG_USE_DEVICE_AEC afe_config->vad_init = false;
afe_config->aec_init = true; #else
afe_config->vad_init = false; afe_config->aec_init = false;
#else afe_config->vad_init = true;
afe_config->aec_init = false; #endif
afe_config->vad_init = true;
#endif afe_iface_ = esp_afe_handle_from_config(afe_config);
afe_data_ = afe_iface_->create_from_config(afe_config);
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;
xTaskCreate([](void* arg) { this_->AudioProcessorTask();
auto this_ = (AfeAudioProcessor*)arg; vTaskDelete(NULL);
this_->AudioProcessorTask(); }, "audio_communication", 4096, this, 3, NULL);
vTaskDelete(NULL); }
}, "audio_communication", 4096, this, 3, NULL);
} AfeAudioProcessor::~AfeAudioProcessor() {
if (afe_data_ != nullptr) {
AfeAudioProcessor::~AfeAudioProcessor() { afe_iface_->destroy(afe_data_);
if (afe_data_ != nullptr) { }
afe_iface_->destroy(afe_data_); vEventGroupDelete(event_group_);
} }
vEventGroupDelete(event_group_);
} size_t AfeAudioProcessor::GetFeedSize() {
if (afe_data_ == nullptr) {
size_t AfeAudioProcessor::GetFeedSize() { return 0;
if (afe_data_ == nullptr) { }
return 0; return afe_iface_->get_feed_chunksize(afe_data_);
} }
return afe_iface_->get_feed_chunksize(afe_data_);
} void AfeAudioProcessor::Feed(std::vector<int16_t>&& data) {
if (afe_data_ == nullptr) {
void AfeAudioProcessor::Feed(std::vector<int16_t>&& data) { return;
if (afe_data_ == nullptr) { }
return; afe_iface_->feed(afe_data_, data.data());
} }
afe_iface_->feed(afe_data_, data.data());
} void AfeAudioProcessor::Start() {
xEventGroupSetBits(event_group_, PROCESSOR_RUNNING);
void AfeAudioProcessor::Start() { }
xEventGroupSetBits(event_group_, PROCESSOR_RUNNING);
} void AfeAudioProcessor::Stop() {
xEventGroupClearBits(event_group_, PROCESSOR_RUNNING);
void AfeAudioProcessor::Stop() { if (afe_data_ != nullptr) {
xEventGroupClearBits(event_group_, PROCESSOR_RUNNING); afe_iface_->reset_buffer(afe_data_);
if (afe_data_ != nullptr) { }
afe_iface_->reset_buffer(afe_data_); }
}
} bool AfeAudioProcessor::IsRunning() {
return xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING;
bool AfeAudioProcessor::IsRunning() { }
return xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING;
} void AfeAudioProcessor::OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) {
output_callback_ = callback;
void AfeAudioProcessor::OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) { }
output_callback_ = callback;
} void AfeAudioProcessor::OnVadStateChange(std::function<void(bool speaking)> callback) {
vad_state_change_callback_ = callback;
void AfeAudioProcessor::OnVadStateChange(std::function<void(bool speaking)> callback) { }
vad_state_change_callback_ = callback;
} void AfeAudioProcessor::AudioProcessorTask() {
auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_);
void AfeAudioProcessor::AudioProcessorTask() { auto feed_size = afe_iface_->get_feed_chunksize(afe_data_);
auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_); ESP_LOGI(TAG, "Audio communication task started, feed size: %d fetch size: %d",
auto feed_size = afe_iface_->get_feed_chunksize(afe_data_); feed_size, fetch_size);
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);
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) {
auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY); continue;
if ((xEventGroupGetBits(event_group_) & PROCESSOR_RUNNING) == 0) { }
continue; if (res == nullptr || res->ret_value == ESP_FAIL) {
} if (res != nullptr) {
if (res == nullptr || res->ret_value == ESP_FAIL) { ESP_LOGI(TAG, "Error code: %d", res->ret_value);
if (res != nullptr) { }
ESP_LOGI(TAG, "Error code: %d", res->ret_value); continue;
} }
continue;
} // VAD state change
if (vad_state_change_callback_) {
// VAD state change if (res->vad_state == VAD_SPEECH && !is_speaking_) {
if (vad_state_change_callback_) { is_speaking_ = true;
if (res->vad_state == VAD_SPEECH && !is_speaking_) { vad_state_change_callback_(true);
is_speaking_ = true; } else if (res->vad_state == VAD_SILENCE && is_speaking_) {
vad_state_change_callback_(true); is_speaking_ = false;
} else if (res->vad_state == VAD_SILENCE && is_speaking_) { vad_state_change_callback_(false);
is_speaking_ = false; }
vad_state_change_callback_(false); }
}
} if (output_callback_) {
size_t samples = res->data_size / sizeof(int16_t);
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);
// 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_) {
// Output complete frames when buffer has enough data if (output_buffer_.size() == frame_samples_) {
while (output_buffer_.size() >= frame_samples_) { // If buffer size equals frame size, move the entire buffer
if (output_buffer_.size() == frame_samples_) { output_callback_(std::move(output_buffer_));
// If buffer size equals frame size, move the entire buffer output_buffer_.clear();
output_callback_(std::move(output_buffer_)); output_buffer_.reserve(frame_samples_);
output_buffer_.clear(); } else {
output_buffer_.reserve(frame_samples_); // If buffer size exceeds frame size, copy one frame and remove it
} else { output_callback_(std::vector<int16_t>(output_buffer_.begin(), output_buffer_.begin() + frame_samples_));
// If buffer size exceeds frame size, copy one frame and remove it output_buffer_.erase(output_buffer_.begin(), output_buffer_.begin() + frame_samples_);
output_callback_(std::vector<int16_t>(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) {
void AfeAudioProcessor::EnableDeviceAec(bool enable) { #if CONFIG_USE_DEVICE_AEC
if (enable) { afe_iface_->disable_vad(afe_data_);
#if CONFIG_USE_DEVICE_AEC afe_iface_->enable_aec(afe_data_);
afe_iface_->disable_vad(afe_data_); #else
afe_iface_->enable_aec(afe_data_); ESP_LOGE(TAG, "Device AEC is not supported");
#else #endif
ESP_LOGE(TAG, "Device AEC is not supported"); } else {
#endif afe_iface_->disable_aec(afe_data_);
} else { afe_iface_->enable_vad(afe_data_);
afe_iface_->disable_aec(afe_data_); }
afe_iface_->enable_vad(afe_data_); }
}
}

View File

@@ -1,45 +1,45 @@
#ifndef AFE_AUDIO_PROCESSOR_H #ifndef AFE_AUDIO_PROCESSOR_H
#define AFE_AUDIO_PROCESSOR_H #define AFE_AUDIO_PROCESSOR_H
#include <esp_afe_sr_models.h> #include <esp_afe_sr_models.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <freertos/event_groups.h> #include <freertos/event_groups.h>
#include <string> #include <string>
#include <vector> #include <vector>
#include <functional> #include <functional>
#include "audio_processor.h" #include "audio_processor.h"
#include "audio_codec.h" #include "audio_codec.h"
class AfeAudioProcessor : public AudioProcessor { class AfeAudioProcessor : public AudioProcessor {
public: public:
AfeAudioProcessor(); AfeAudioProcessor();
~AfeAudioProcessor(); ~AfeAudioProcessor();
void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) override; void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) override;
void Feed(std::vector<int16_t>&& data) override; void Feed(std::vector<int16_t>&& data) override;
void Start() override; void Start() override;
void Stop() override; void Stop() override;
bool IsRunning() override; bool IsRunning() override;
void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) override; void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) override;
void OnVadStateChange(std::function<void(bool speaking)> callback) override; void OnVadStateChange(std::function<void(bool speaking)> callback) override;
size_t GetFeedSize() override; size_t GetFeedSize() override;
void EnableDeviceAec(bool enable) override; void EnableDeviceAec(bool enable) override;
private: private:
EventGroupHandle_t event_group_ = nullptr; EventGroupHandle_t event_group_ = nullptr;
esp_afe_sr_iface_t* afe_iface_ = nullptr; esp_afe_sr_iface_t* afe_iface_ = nullptr;
esp_afe_sr_data_t* afe_data_ = nullptr; esp_afe_sr_data_t* afe_data_ = nullptr;
std::function<void(std::vector<int16_t>&& data)> output_callback_; std::function<void(std::vector<int16_t>&& data)> output_callback_;
std::function<void(bool speaking)> vad_state_change_callback_; std::function<void(bool speaking)> vad_state_change_callback_;
AudioCodec* codec_ = nullptr; AudioCodec* codec_ = nullptr;
int frame_samples_ = 0; int frame_samples_ = 0;
bool is_speaking_ = false; bool is_speaking_ = false;
std::vector<int16_t> output_buffer_; std::vector<int16_t> output_buffer_;
void AudioProcessorTask(); void AudioProcessorTask();
}; };
#endif #endif

View File

@@ -1,68 +1,68 @@
#include "audio_debugger.h" #include "audio_debugger.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#if CONFIG_USE_AUDIO_DEBUGGER #if CONFIG_USE_AUDIO_DEBUGGER
#include <esp_log.h> #include <esp_log.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
#include <cstring> #include <cstring>
#include <string> #include <string>
#endif #endif
#define TAG "AudioDebugger" #define TAG "AudioDebugger"
AudioDebugger::AudioDebugger() { AudioDebugger::AudioDebugger() {
#if CONFIG_USE_AUDIO_DEBUGGER #if CONFIG_USE_AUDIO_DEBUGGER
udp_sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); udp_sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_sockfd_ >= 0) { if (udp_sockfd_ >= 0) {
// 解析配置的服务器地址 "IP:PORT" // 解析配置的服务器地址 "IP:PORT"
std::string server_addr = CONFIG_AUDIO_DEBUG_UDP_SERVER; std::string server_addr = CONFIG_AUDIO_DEBUG_UDP_SERVER;
size_t colon_pos = server_addr.find(':'); size_t colon_pos = server_addr.find(':');
if (colon_pos != std::string::npos) { if (colon_pos != std::string::npos) {
std::string ip = server_addr.substr(0, colon_pos); std::string ip = server_addr.substr(0, colon_pos);
int port = std::stoi(server_addr.substr(colon_pos + 1)); int port = std::stoi(server_addr.substr(colon_pos + 1));
memset(&udp_server_addr_, 0, sizeof(udp_server_addr_)); memset(&udp_server_addr_, 0, sizeof(udp_server_addr_));
udp_server_addr_.sin_family = AF_INET; udp_server_addr_.sin_family = AF_INET;
udp_server_addr_.sin_port = htons(port); udp_server_addr_.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &udp_server_addr_.sin_addr); inet_pton(AF_INET, ip.c_str(), &udp_server_addr_.sin_addr);
ESP_LOGI(TAG, "Initialized server address: %s", CONFIG_AUDIO_DEBUG_UDP_SERVER); ESP_LOGI(TAG, "Initialized server address: %s", CONFIG_AUDIO_DEBUG_UDP_SERVER);
} else { } else {
ESP_LOGW(TAG, "Invalid server address: %s, should be IP:PORT", CONFIG_AUDIO_DEBUG_UDP_SERVER); ESP_LOGW(TAG, "Invalid server address: %s, should be IP:PORT", CONFIG_AUDIO_DEBUG_UDP_SERVER);
close(udp_sockfd_); close(udp_sockfd_);
udp_sockfd_ = -1; udp_sockfd_ = -1;
} }
} else { } else {
ESP_LOGW(TAG, "Failed to create UDP socket: %d", errno); ESP_LOGW(TAG, "Failed to create UDP socket: %d", errno);
} }
#endif #endif
} }
AudioDebugger::~AudioDebugger() { AudioDebugger::~AudioDebugger() {
#if CONFIG_USE_AUDIO_DEBUGGER #if CONFIG_USE_AUDIO_DEBUGGER
if (udp_sockfd_ >= 0) { if (udp_sockfd_ >= 0) {
close(udp_sockfd_); close(udp_sockfd_);
ESP_LOGI(TAG, "Closed UDP socket"); ESP_LOGI(TAG, "Closed UDP socket");
} }
#endif #endif
} }
void AudioDebugger::Feed(const std::vector<int16_t>& data) { void AudioDebugger::Feed(const std::vector<int16_t>& data) {
#if CONFIG_USE_AUDIO_DEBUGGER #if CONFIG_USE_AUDIO_DEBUGGER
if (udp_sockfd_ >= 0) { if (udp_sockfd_ >= 0) {
ssize_t sent = sendto(udp_sockfd_, data.data(), data.size() * sizeof(int16_t), 0, ssize_t sent = sendto(udp_sockfd_, data.data(), data.size() * sizeof(int16_t), 0,
(struct sockaddr*)&udp_server_addr_, sizeof(udp_server_addr_)); (struct sockaddr*)&udp_server_addr_, sizeof(udp_server_addr_));
if (sent < 0) { if (sent < 0) {
ESP_LOGW(TAG, "Failed to send audio data to %s: %d", CONFIG_AUDIO_DEBUG_UDP_SERVER, errno); ESP_LOGW(TAG, "Failed to send audio data to %s: %d", CONFIG_AUDIO_DEBUG_UDP_SERVER, errno);
} else { } else {
ESP_LOGD(TAG, "Sent %d bytes audio data to %s", sent, CONFIG_AUDIO_DEBUG_UDP_SERVER); ESP_LOGD(TAG, "Sent %d bytes audio data to %s", sent, CONFIG_AUDIO_DEBUG_UDP_SERVER);
} }
} }
#endif #endif
} }

View File

@@ -1,22 +1,22 @@
#ifndef AUDIO_DEBUGGER_H #ifndef AUDIO_DEBUGGER_H
#define AUDIO_DEBUGGER_H #define AUDIO_DEBUGGER_H
#include <vector> #include <vector>
#include <cstdint> #include <cstdint>
#include <sys/socket.h> #include <sys/socket.h>
#include <netinet/in.h> #include <netinet/in.h>
class AudioDebugger { class AudioDebugger {
public: public:
AudioDebugger(); AudioDebugger();
~AudioDebugger(); ~AudioDebugger();
void Feed(const std::vector<int16_t>& data); void Feed(const std::vector<int16_t>& data);
private: private:
int udp_sockfd_ = -1; int udp_sockfd_ = -1;
struct sockaddr_in udp_server_addr_; struct sockaddr_in udp_server_addr_;
}; };
#endif #endif

View File

@@ -1,59 +1,59 @@
#include "no_audio_processor.h" #include "no_audio_processor.h"
#include <esp_log.h> #include <esp_log.h>
#define TAG "NoAudioProcessor" #define TAG "NoAudioProcessor"
void NoAudioProcessor::Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) { void NoAudioProcessor::Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) {
codec_ = codec; codec_ = codec;
frame_samples_ = frame_duration_ms * 16000 / 1000; frame_samples_ = frame_duration_ms * 16000 / 1000;
} }
void NoAudioProcessor::Feed(std::vector<int16_t>&& data) { void NoAudioProcessor::Feed(std::vector<int16_t>&& data) {
if (!is_running_ || !output_callback_) { if (!is_running_ || !output_callback_) {
return; return;
} }
if (codec_->input_channels() == 2) { if (codec_->input_channels() == 2) {
// If input channels is 2, we need to fetch the left channel data // If input channels is 2, we need to fetch the left channel data
auto mono_data = std::vector<int16_t>(data.size() / 2); auto mono_data = std::vector<int16_t>(data.size() / 2);
for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) { for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
mono_data[i] = data[j]; mono_data[i] = data[j];
} }
output_callback_(std::move(mono_data)); output_callback_(std::move(mono_data));
} else { } else {
output_callback_(std::move(data)); output_callback_(std::move(data));
} }
} }
void NoAudioProcessor::Start() { void NoAudioProcessor::Start() {
is_running_ = true; is_running_ = true;
} }
void NoAudioProcessor::Stop() { void NoAudioProcessor::Stop() {
is_running_ = false; is_running_ = false;
} }
bool NoAudioProcessor::IsRunning() { bool NoAudioProcessor::IsRunning() {
return is_running_; return is_running_;
} }
void NoAudioProcessor::OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) { void NoAudioProcessor::OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) {
output_callback_ = callback; output_callback_ = callback;
} }
void NoAudioProcessor::OnVadStateChange(std::function<void(bool speaking)> callback) { void NoAudioProcessor::OnVadStateChange(std::function<void(bool speaking)> callback) {
vad_state_change_callback_ = callback; vad_state_change_callback_ = callback;
} }
size_t NoAudioProcessor::GetFeedSize() { size_t NoAudioProcessor::GetFeedSize() {
if (!codec_) { if (!codec_) {
return 0; return 0;
} }
return frame_samples_; return frame_samples_;
} }
void NoAudioProcessor::EnableDeviceAec(bool enable) { void NoAudioProcessor::EnableDeviceAec(bool enable) {
if (enable) { if (enable) {
ESP_LOGE(TAG, "Device AEC is not supported"); ESP_LOGE(TAG, "Device AEC is not supported");
} }
} }

View File

@@ -1,33 +1,33 @@
#ifndef DUMMY_AUDIO_PROCESSOR_H #ifndef DUMMY_AUDIO_PROCESSOR_H
#define DUMMY_AUDIO_PROCESSOR_H #define DUMMY_AUDIO_PROCESSOR_H
#include <vector> #include <vector>
#include <functional> #include <functional>
#include "audio_processor.h" #include "audio_processor.h"
#include "audio_codec.h" #include "audio_codec.h"
class NoAudioProcessor : public AudioProcessor { class NoAudioProcessor : public AudioProcessor {
public: public:
NoAudioProcessor() = default; NoAudioProcessor() = default;
~NoAudioProcessor() = default; ~NoAudioProcessor() = default;
void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) override; void Initialize(AudioCodec* codec, int frame_duration_ms, srmodel_list_t* models_list) override;
void Feed(std::vector<int16_t>&& data) override; void Feed(std::vector<int16_t>&& data) override;
void Start() override; void Start() override;
void Stop() override; void Stop() override;
bool IsRunning() override; bool IsRunning() override;
void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) override; void OnOutput(std::function<void(std::vector<int16_t>&& data)> callback) override;
void OnVadStateChange(std::function<void(bool speaking)> callback) override; void OnVadStateChange(std::function<void(bool speaking)> callback) override;
size_t GetFeedSize() override; size_t GetFeedSize() override;
void EnableDeviceAec(bool enable) override; void EnableDeviceAec(bool enable) override;
private: private:
AudioCodec* codec_ = nullptr; AudioCodec* codec_ = nullptr;
int frame_samples_ = 0; int frame_samples_ = 0;
std::function<void(std::vector<int16_t>&& data)> output_callback_; std::function<void(std::vector<int16_t>&& data)> output_callback_;
std::function<void(bool speaking)> vad_state_change_callback_; std::function<void(bool speaking)> vad_state_change_callback_;
bool is_running_ = false; bool is_running_ = false;
}; };
#endif #endif

View File

@@ -1,26 +1,26 @@
#ifndef WAKE_WORD_H #ifndef WAKE_WORD_H
#define WAKE_WORD_H #define WAKE_WORD_H
#include <string> #include <string>
#include <vector> #include <vector>
#include <functional> #include <functional>
#include <model_path.h> #include <model_path.h>
#include "audio_codec.h" #include "audio_codec.h"
class WakeWord { class WakeWord {
public: public:
virtual ~WakeWord() = default; virtual ~WakeWord() = default;
virtual bool Initialize(AudioCodec* codec, srmodel_list_t* models_list) = 0; virtual bool Initialize(AudioCodec* codec, srmodel_list_t* models_list) = 0;
virtual void Feed(const std::vector<int16_t>& data) = 0; virtual void Feed(const std::vector<int16_t>& data) = 0;
virtual void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) = 0; virtual void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) = 0;
virtual void Start() = 0; virtual void Start() = 0;
virtual void Stop() = 0; virtual void Stop() = 0;
virtual size_t GetFeedSize() = 0; virtual size_t GetFeedSize() = 0;
virtual void EncodeWakeWordData() = 0; virtual void EncodeWakeWordData() = 0;
virtual bool GetWakeWordOpus(std::vector<uint8_t>& opus) = 0; virtual bool GetWakeWordOpus(std::vector<uint8_t>& opus) = 0;
virtual const std::string& GetLastDetectedWakeWord() const = 0; virtual const std::string& GetLastDetectedWakeWord() const = 0;
}; };
#endif #endif

View File

@@ -1,208 +1,208 @@
#include "afe_wake_word.h" #include "afe_wake_word.h"
#include "audio_service.h" #include "audio_service.h"
#include <esp_log.h> #include <esp_log.h>
#include <sstream> #include <sstream>
#define DETECTION_RUNNING_EVENT 1 #define DETECTION_RUNNING_EVENT 1
#define TAG "AfeWakeWord" #define TAG "AfeWakeWord"
AfeWakeWord::AfeWakeWord() AfeWakeWord::AfeWakeWord()
: afe_data_(nullptr), : afe_data_(nullptr),
wake_word_pcm_(), wake_word_pcm_(),
wake_word_opus_() { wake_word_opus_() {
event_group_ = xEventGroupCreate(); event_group_ = xEventGroupCreate();
} }
AfeWakeWord::~AfeWakeWord() { AfeWakeWord::~AfeWakeWord() {
if (afe_data_ != nullptr) { if (afe_data_ != nullptr) {
afe_iface_->destroy(afe_data_); afe_iface_->destroy(afe_data_);
} }
if (wake_word_encode_task_stack_ != nullptr) { if (wake_word_encode_task_stack_ != nullptr) {
heap_caps_free(wake_word_encode_task_stack_); heap_caps_free(wake_word_encode_task_stack_);
} }
if (wake_word_encode_task_buffer_ != nullptr) { if (wake_word_encode_task_buffer_ != nullptr) {
heap_caps_free(wake_word_encode_task_buffer_); heap_caps_free(wake_word_encode_task_buffer_);
} }
if (models_ != nullptr) { if (models_ != nullptr) {
esp_srmodel_deinit(models_); esp_srmodel_deinit(models_);
} }
vEventGroupDelete(event_group_); vEventGroupDelete(event_group_);
} }
bool AfeWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list) { bool AfeWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list) {
codec_ = codec; codec_ = codec;
int ref_num = codec_->input_reference() ? 1 : 0; int ref_num = codec_->input_reference() ? 1 : 0;
if (models_list == nullptr) { if (models_list == nullptr) {
models_ = esp_srmodel_init("model"); models_ = esp_srmodel_init("model");
} else { } else {
models_ = models_list; models_ = models_list;
} }
if (models_ == nullptr || models_->num == -1) { if (models_ == nullptr || models_->num == -1) {
ESP_LOGE(TAG, "Failed to initialize wakenet model"); ESP_LOGE(TAG, "Failed to initialize wakenet model");
return false; return false;
} }
for (int i = 0; i < models_->num; i++) { for (int i = 0; i < models_->num; i++) {
ESP_LOGI(TAG, "Model %d: %s", i, models_->model_name[i]); ESP_LOGI(TAG, "Model %d: %s", i, models_->model_name[i]);
if (strstr(models_->model_name[i], ESP_WN_PREFIX) != NULL) { if (strstr(models_->model_name[i], ESP_WN_PREFIX) != NULL) {
wakenet_model_ = models_->model_name[i]; wakenet_model_ = models_->model_name[i];
auto words = esp_srmodel_get_wake_words(models_, wakenet_model_); auto words = esp_srmodel_get_wake_words(models_, wakenet_model_);
// split by ";" to get all wake words // split by ";" to get all wake words
std::stringstream ss(words); std::stringstream ss(words);
std::string word; std::string word;
while (std::getline(ss, word, ';')) { while (std::getline(ss, word, ';')) {
wake_words_.push_back(word); wake_words_.push_back(word);
} }
} }
} }
std::string input_format; std::string input_format;
for (int i = 0; i < codec_->input_channels() - ref_num; i++) { for (int i = 0; i < codec_->input_channels() - ref_num; i++) {
input_format.push_back('M'); input_format.push_back('M');
} }
for (int i = 0; i < ref_num; i++) { for (int i = 0; i < ref_num; i++) {
input_format.push_back('R'); input_format.push_back('R');
} }
afe_config_t* afe_config = afe_config_init(input_format.c_str(), models_, AFE_TYPE_SR, AFE_MODE_HIGH_PERF); afe_config_t* afe_config = afe_config_init(input_format.c_str(), models_, AFE_TYPE_SR, AFE_MODE_HIGH_PERF);
afe_config->aec_init = codec_->input_reference(); afe_config->aec_init = codec_->input_reference();
afe_config->aec_mode = AEC_MODE_SR_HIGH_PERF; afe_config->aec_mode = AEC_MODE_SR_HIGH_PERF;
afe_config->afe_perferred_core = 1; afe_config->afe_perferred_core = 1;
afe_config->afe_perferred_priority = 1; afe_config->afe_perferred_priority = 1;
afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM; afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM;
afe_iface_ = esp_afe_handle_from_config(afe_config); afe_iface_ = esp_afe_handle_from_config(afe_config);
afe_data_ = afe_iface_->create_from_config(afe_config); afe_data_ = afe_iface_->create_from_config(afe_config);
xTaskCreate([](void* arg) { xTaskCreate([](void* arg) {
auto this_ = (AfeWakeWord*)arg; auto this_ = (AfeWakeWord*)arg;
this_->AudioDetectionTask(); this_->AudioDetectionTask();
vTaskDelete(NULL); vTaskDelete(NULL);
}, "audio_detection", 4096, this, 3, nullptr); }, "audio_detection", 4096, this, 3, nullptr);
return true; return true;
} }
void AfeWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) { void AfeWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
wake_word_detected_callback_ = callback; wake_word_detected_callback_ = callback;
} }
void AfeWakeWord::Start() { void AfeWakeWord::Start() {
xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT); xEventGroupSetBits(event_group_, DETECTION_RUNNING_EVENT);
} }
void AfeWakeWord::Stop() { void AfeWakeWord::Stop() {
xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT); xEventGroupClearBits(event_group_, DETECTION_RUNNING_EVENT);
if (afe_data_ != nullptr) { if (afe_data_ != nullptr) {
afe_iface_->reset_buffer(afe_data_); afe_iface_->reset_buffer(afe_data_);
} }
} }
void AfeWakeWord::Feed(const std::vector<int16_t>& data) { void AfeWakeWord::Feed(const std::vector<int16_t>& data) {
if (afe_data_ == nullptr) { if (afe_data_ == nullptr) {
return; return;
} }
afe_iface_->feed(afe_data_, data.data()); afe_iface_->feed(afe_data_, data.data());
} }
size_t AfeWakeWord::GetFeedSize() { size_t AfeWakeWord::GetFeedSize() {
if (afe_data_ == nullptr) { if (afe_data_ == nullptr) {
return 0; return 0;
} }
return afe_iface_->get_feed_chunksize(afe_data_); return afe_iface_->get_feed_chunksize(afe_data_);
} }
void AfeWakeWord::AudioDetectionTask() { void AfeWakeWord::AudioDetectionTask() {
auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_); auto fetch_size = afe_iface_->get_fetch_chunksize(afe_data_);
auto feed_size = afe_iface_->get_feed_chunksize(afe_data_); auto feed_size = afe_iface_->get_feed_chunksize(afe_data_);
ESP_LOGI(TAG, "Audio detection task started, feed size: %d fetch size: %d", ESP_LOGI(TAG, "Audio detection task started, feed size: %d fetch size: %d",
feed_size, fetch_size); feed_size, fetch_size);
while (true) { while (true) {
xEventGroupWaitBits(event_group_, DETECTION_RUNNING_EVENT, pdFALSE, pdTRUE, portMAX_DELAY); xEventGroupWaitBits(event_group_, DETECTION_RUNNING_EVENT, pdFALSE, pdTRUE, portMAX_DELAY);
auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY); auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
if (res == nullptr || res->ret_value == ESP_FAIL) { if (res == nullptr || res->ret_value == ESP_FAIL) {
continue;; continue;;
} }
// Store the wake word data for voice recognition, like who is speaking // Store the wake word data for voice recognition, like who is speaking
StoreWakeWordData(res->data, res->data_size / sizeof(int16_t)); StoreWakeWordData(res->data, res->data_size / sizeof(int16_t));
if (res->wakeup_state == WAKENET_DETECTED) { if (res->wakeup_state == WAKENET_DETECTED) {
Stop(); Stop();
last_detected_wake_word_ = wake_words_[res->wakenet_model_index - 1]; last_detected_wake_word_ = wake_words_[res->wakenet_model_index - 1];
if (wake_word_detected_callback_) { if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_); wake_word_detected_callback_(last_detected_wake_word_);
} }
} }
} }
} }
void AfeWakeWord::StoreWakeWordData(const int16_t* data, size_t samples) { void AfeWakeWord::StoreWakeWordData(const int16_t* data, size_t samples) {
// store audio data to wake_word_pcm_ // store audio data to wake_word_pcm_
wake_word_pcm_.emplace_back(std::vector<int16_t>(data, data + samples)); wake_word_pcm_.emplace_back(std::vector<int16_t>(data, data + samples));
// keep about 2 seconds of data, detect duration is 30ms (sample_rate == 16000, chunksize == 512) // keep about 2 seconds of data, detect duration is 30ms (sample_rate == 16000, chunksize == 512)
while (wake_word_pcm_.size() > 2000 / 30) { while (wake_word_pcm_.size() > 2000 / 30) {
wake_word_pcm_.pop_front(); wake_word_pcm_.pop_front();
} }
} }
void AfeWakeWord::EncodeWakeWordData() { void AfeWakeWord::EncodeWakeWordData() {
const size_t stack_size = 4096 * 7; const size_t stack_size = 4096 * 7;
wake_word_opus_.clear(); wake_word_opus_.clear();
if (wake_word_encode_task_stack_ == nullptr) { if (wake_word_encode_task_stack_ == nullptr) {
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM); wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM);
assert(wake_word_encode_task_stack_ != nullptr); assert(wake_word_encode_task_stack_ != nullptr);
} }
if (wake_word_encode_task_buffer_ == nullptr) { if (wake_word_encode_task_buffer_ == nullptr) {
wake_word_encode_task_buffer_ = (StaticTask_t*)heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL); wake_word_encode_task_buffer_ = (StaticTask_t*)heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL);
assert(wake_word_encode_task_buffer_ != nullptr); assert(wake_word_encode_task_buffer_ != nullptr);
} }
wake_word_encode_task_ = xTaskCreateStatic([](void* arg) { wake_word_encode_task_ = xTaskCreateStatic([](void* arg) {
auto this_ = (AfeWakeWord*)arg; auto this_ = (AfeWakeWord*)arg;
{ {
auto start_time = esp_timer_get_time(); auto start_time = esp_timer_get_time();
auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS); auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
encoder->SetComplexity(0); // 0 is the fastest encoder->SetComplexity(0); // 0 is the fastest
int packets = 0; int packets = 0;
for (auto& pcm: this_->wake_word_pcm_) { for (auto& pcm: this_->wake_word_pcm_) {
encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) { encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) {
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_); std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.emplace_back(std::move(opus)); this_->wake_word_opus_.emplace_back(std::move(opus));
this_->wake_word_cv_.notify_all(); this_->wake_word_cv_.notify_all();
}); });
packets++; packets++;
} }
this_->wake_word_pcm_.clear(); this_->wake_word_pcm_.clear();
auto end_time = esp_timer_get_time(); auto end_time = esp_timer_get_time();
ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000)); ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000));
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_); std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.push_back(std::vector<uint8_t>()); this_->wake_word_opus_.push_back(std::vector<uint8_t>());
this_->wake_word_cv_.notify_all(); this_->wake_word_cv_.notify_all();
} }
vTaskDelete(NULL); vTaskDelete(NULL);
}, "encode_wake_word", stack_size, this, 2, wake_word_encode_task_stack_, wake_word_encode_task_buffer_); }, "encode_wake_word", stack_size, this, 2, wake_word_encode_task_stack_, wake_word_encode_task_buffer_);
} }
bool AfeWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) { bool AfeWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) {
std::unique_lock<std::mutex> lock(wake_word_mutex_); std::unique_lock<std::mutex> lock(wake_word_mutex_);
wake_word_cv_.wait(lock, [this]() { wake_word_cv_.wait(lock, [this]() {
return !wake_word_opus_.empty(); return !wake_word_opus_.empty();
}); });
opus.swap(wake_word_opus_.front()); opus.swap(wake_word_opus_.front());
wake_word_opus_.pop_front(); wake_word_opus_.pop_front();
return !opus.empty(); return !opus.empty();
} }

View File

@@ -1,60 +1,60 @@
#ifndef AFE_WAKE_WORD_H #ifndef AFE_WAKE_WORD_H
#define AFE_WAKE_WORD_H #define AFE_WAKE_WORD_H
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <freertos/event_groups.h> #include <freertos/event_groups.h>
#include <esp_afe_sr_models.h> #include <esp_afe_sr_models.h>
#include <esp_nsn_models.h> #include <esp_nsn_models.h>
#include <model_path.h> #include <model_path.h>
#include <deque> #include <deque>
#include <string> #include <string>
#include <vector> #include <vector>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <condition_variable> #include <condition_variable>
#include "audio_codec.h" #include "audio_codec.h"
#include "wake_word.h" #include "wake_word.h"
class AfeWakeWord : public WakeWord { class AfeWakeWord : public WakeWord {
public: public:
AfeWakeWord(); AfeWakeWord();
~AfeWakeWord(); ~AfeWakeWord();
bool Initialize(AudioCodec* codec, srmodel_list_t* models_list); bool Initialize(AudioCodec* codec, srmodel_list_t* models_list);
void Feed(const std::vector<int16_t>& data); void Feed(const std::vector<int16_t>& data);
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback); void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
void Start(); void Start();
void Stop(); void Stop();
size_t GetFeedSize(); size_t GetFeedSize();
void EncodeWakeWordData(); void EncodeWakeWordData();
bool GetWakeWordOpus(std::vector<uint8_t>& opus); bool GetWakeWordOpus(std::vector<uint8_t>& opus);
const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; } const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; }
private: private:
srmodel_list_t *models_ = nullptr; srmodel_list_t *models_ = nullptr;
esp_afe_sr_iface_t* afe_iface_ = nullptr; esp_afe_sr_iface_t* afe_iface_ = nullptr;
esp_afe_sr_data_t* afe_data_ = nullptr; esp_afe_sr_data_t* afe_data_ = nullptr;
char* wakenet_model_ = NULL; char* wakenet_model_ = NULL;
std::vector<std::string> wake_words_; std::vector<std::string> wake_words_;
EventGroupHandle_t event_group_; EventGroupHandle_t event_group_;
std::function<void(const std::string& wake_word)> wake_word_detected_callback_; std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
AudioCodec* codec_ = nullptr; AudioCodec* codec_ = nullptr;
std::string last_detected_wake_word_; std::string last_detected_wake_word_;
TaskHandle_t wake_word_encode_task_ = nullptr; TaskHandle_t wake_word_encode_task_ = nullptr;
StaticTask_t* wake_word_encode_task_buffer_ = nullptr; StaticTask_t* wake_word_encode_task_buffer_ = nullptr;
StackType_t* wake_word_encode_task_stack_ = nullptr; StackType_t* wake_word_encode_task_stack_ = nullptr;
std::deque<std::vector<int16_t>> wake_word_pcm_; std::deque<std::vector<int16_t>> wake_word_pcm_;
std::deque<std::vector<uint8_t>> wake_word_opus_; std::deque<std::vector<uint8_t>> wake_word_opus_;
std::mutex wake_word_mutex_; std::mutex wake_word_mutex_;
std::condition_variable wake_word_cv_; std::condition_variable wake_word_cv_;
void StoreWakeWordData(const int16_t* data, size_t size); void StoreWakeWordData(const int16_t* data, size_t size);
void AudioDetectionTask(); void AudioDetectionTask();
}; };
#endif #endif

View File

@@ -1,190 +1,250 @@
#include "custom_wake_word.h" #include "custom_wake_word.h"
#include "audio_service.h" #include "audio_service.h"
#include "system_info.h" #include "system_info.h"
#include "assets.h"
#include <esp_log.h>
#include "esp_mn_iface.h" #include <esp_log.h>
#include "esp_mn_models.h" #include <esp_mn_iface.h>
#include "esp_mn_speech_commands.h" #include <esp_mn_models.h>
#include <esp_mn_speech_commands.h>
#include <cJSON.h>
#define TAG "CustomWakeWord"
#define TAG "CustomWakeWord"
CustomWakeWord::CustomWakeWord()
: wake_word_pcm_(), wake_word_opus_() {
} CustomWakeWord::CustomWakeWord()
: wake_word_pcm_(), wake_word_opus_() {
CustomWakeWord::~CustomWakeWord() { }
if (multinet_model_data_ != nullptr && multinet_ != nullptr) {
multinet_->destroy(multinet_model_data_); CustomWakeWord::~CustomWakeWord() {
multinet_model_data_ = nullptr; if (multinet_model_data_ != nullptr && multinet_ != nullptr) {
} multinet_->destroy(multinet_model_data_);
multinet_model_data_ = nullptr;
if (wake_word_encode_task_stack_ != nullptr) { }
heap_caps_free(wake_word_encode_task_stack_);
} if (wake_word_encode_task_stack_ != nullptr) {
heap_caps_free(wake_word_encode_task_stack_);
if (wake_word_encode_task_buffer_ != nullptr) { }
heap_caps_free(wake_word_encode_task_buffer_);
} if (wake_word_encode_task_buffer_ != nullptr) {
heap_caps_free(wake_word_encode_task_buffer_);
if (models_ != nullptr) { }
esp_srmodel_deinit(models_);
} if (models_ != nullptr) {
} esp_srmodel_deinit(models_);
}
bool CustomWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list) { }
codec_ = codec;
void CustomWakeWord::ParseWakenetModelConfig() {
if (models_list == nullptr) { // Read index.json
models_ = esp_srmodel_init("model"); auto& assets = Assets::GetInstance();
} else { void* ptr = nullptr;
models_ = models_list; size_t size = 0;
} if (!assets.GetAssetData("index.json", ptr, size)) {
ESP_LOGE(TAG, "Failed to read index.json");
if (models_ == nullptr || models_->num == -1) { return;
ESP_LOGE(TAG, "Failed to initialize wakenet model"); }
return false; cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
} if (root == nullptr) {
ESP_LOGE(TAG, "Failed to parse index.json");
// 初始化 multinet (命令词识别) return;
mn_name_ = esp_srmodel_filter(models_, ESP_MN_PREFIX, ESP_MN_CHINESE); }
if (mn_name_ == nullptr) { cJSON* multinet_model = cJSON_GetObjectItem(root, "multinet_model");
ESP_LOGE(TAG, "Failed to initialize multinet, mn_name is nullptr"); if (cJSON_IsObject(multinet_model)) {
ESP_LOGI(TAG, "Please refer to https://pcn7cs20v8cr.feishu.cn/wiki/CpQjwQsCJiQSWSkYEvrcxcbVnwh to add custom wake word"); cJSON* language = cJSON_GetObjectItem(multinet_model, "language");
return false; cJSON* duration = cJSON_GetObjectItem(multinet_model, "duration");
} cJSON* threshold = cJSON_GetObjectItem(multinet_model, "threshold");
cJSON* commands = cJSON_GetObjectItem(multinet_model, "commands");
ESP_LOGI(TAG, "multinet: %s", mn_name_); if (cJSON_IsString(language)) {
multinet_ = esp_mn_handle_from_name(mn_name_); language_ = language->valuestring;
multinet_model_data_ = multinet_->create(mn_name_, 3000); // 3 秒超时 }
multinet_->set_det_threshold(multinet_model_data_, CONFIG_CUSTOM_WAKE_WORD_THRESHOLD / 100.0f); if (cJSON_IsNumber(duration)) {
esp_mn_commands_clear(); duration_ = duration->valueint;
esp_mn_commands_add(1, CONFIG_CUSTOM_WAKE_WORD); }
esp_mn_commands_update(); if (cJSON_IsNumber(threshold)) {
threshold_ = threshold->valuedouble;
multinet_->print_active_speech_commands(multinet_model_data_); }
return true; if (cJSON_IsArray(commands)) {
} for (int i = 0; i < cJSON_GetArraySize(commands); i++) {
cJSON* command = cJSON_GetArrayItem(commands, i);
void CustomWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) { if (cJSON_IsObject(command)) {
wake_word_detected_callback_ = callback; cJSON* command_name = cJSON_GetObjectItem(command, "command");
} cJSON* text = cJSON_GetObjectItem(command, "text");
cJSON* action = cJSON_GetObjectItem(command, "action");
void CustomWakeWord::Start() { if (cJSON_IsString(command_name) && cJSON_IsString(text) && cJSON_IsString(action)) {
running_ = true; commands_.push_back({command_name->valuestring, text->valuestring, action->valuestring});
} ESP_LOGI(TAG, "Command: %s, Text: %s, Action: %s", command_name->valuestring, text->valuestring, action->valuestring);
}
void CustomWakeWord::Stop() { }
running_ = false; }
} }
}
void CustomWakeWord::Feed(const std::vector<int16_t>& data) { cJSON_Delete(root);
if (multinet_model_data_ == nullptr || !running_) { }
return;
}
bool CustomWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list) {
esp_mn_state_t mn_state; codec_ = codec;
// If input channels is 2, we need to fetch the left channel data commands_.clear();
if (codec_->input_channels() == 2) {
auto mono_data = std::vector<int16_t>(data.size() / 2); if (models_list == nullptr) {
for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) { language_ = "cn";
mono_data[i] = data[j]; models_ = esp_srmodel_init("model");
} #ifdef CONFIG_CUSTOM_WAKE_WORD
threshold_ = CONFIG_CUSTOM_WAKE_WORD_THRESHOLD / 100.0f;
StoreWakeWordData(mono_data); commands_.push_back({CONFIG_CUSTOM_WAKE_WORD, CONFIG_CUSTOM_WAKE_WORD_DISPLAY, "wake"});
mn_state = multinet_->detect(multinet_model_data_, const_cast<int16_t*>(mono_data.data())); #endif
} else { } else {
StoreWakeWordData(data); models_ = models_list;
mn_state = multinet_->detect(multinet_model_data_, const_cast<int16_t*>(data.data())); ParseWakenetModelConfig();
} }
if (mn_state == ESP_MN_STATE_DETECTING) { if (models_ == nullptr || models_->num == -1) {
return; ESP_LOGE(TAG, "Failed to initialize wakenet model");
} else if (mn_state == ESP_MN_STATE_DETECTED) { return false;
esp_mn_results_t *mn_result = multinet_->get_results(multinet_model_data_); }
ESP_LOGI(TAG, "Custom wake word detected: command_id=%d, string=%s, prob=%f",
mn_result->command_id[0], mn_result->string, mn_result->prob[0]); // 初始化 multinet (命令词识别)
mn_name_ = esp_srmodel_filter(models_, ESP_MN_PREFIX, language_.c_str());
if (mn_result->command_id[0] == 1) { if (mn_name_ == nullptr) {
last_detected_wake_word_ = CONFIG_CUSTOM_WAKE_WORD_DISPLAY; ESP_LOGE(TAG, "Failed to initialize multinet, mn_name is nullptr");
} ESP_LOGI(TAG, "Please refer to https://pcn7cs20v8cr.feishu.cn/wiki/CpQjwQsCJiQSWSkYEvrcxcbVnwh to add custom wake word");
running_ = false; return false;
}
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_); multinet_ = esp_mn_handle_from_name(mn_name_);
} multinet_model_data_ = multinet_->create(mn_name_, duration_);
multinet_->clean(multinet_model_data_); multinet_->set_det_threshold(multinet_model_data_, threshold_);
} else if (mn_state == ESP_MN_STATE_TIMEOUT) { esp_mn_commands_clear();
ESP_LOGD(TAG, "Command word detection timeout, cleaning state"); for (int i = 0; i < commands_.size(); i++) {
multinet_->clean(multinet_model_data_); esp_mn_commands_add(i + 1, commands_[i].command.c_str());
} }
} esp_mn_commands_update();
size_t CustomWakeWord::GetFeedSize() { multinet_->print_active_speech_commands(multinet_model_data_);
if (multinet_model_data_ == nullptr) { return true;
return 0; }
}
return multinet_->get_samp_chunksize(multinet_model_data_); void CustomWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
} wake_word_detected_callback_ = callback;
}
void CustomWakeWord::StoreWakeWordData(const std::vector<int16_t>& data) {
// store audio data to wake_word_pcm_ void CustomWakeWord::Start() {
wake_word_pcm_.push_back(data); running_ = true;
// keep about 2 seconds of data, detect duration is 30ms (sample_rate == 16000, chunksize == 512) }
while (wake_word_pcm_.size() > 2000 / 30) {
wake_word_pcm_.pop_front(); void CustomWakeWord::Stop() {
} running_ = false;
} }
void CustomWakeWord::EncodeWakeWordData() { void CustomWakeWord::Feed(const std::vector<int16_t>& data) {
const size_t stack_size = 4096 * 7; if (multinet_model_data_ == nullptr || !running_) {
wake_word_opus_.clear(); return;
if (wake_word_encode_task_stack_ == nullptr) { }
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM);
assert(wake_word_encode_task_stack_ != nullptr); esp_mn_state_t mn_state;
} // If input channels is 2, we need to fetch the left channel data
if (wake_word_encode_task_buffer_ == nullptr) { if (codec_->input_channels() == 2) {
wake_word_encode_task_buffer_ = (StaticTask_t*)heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL); auto mono_data = std::vector<int16_t>(data.size() / 2);
assert(wake_word_encode_task_buffer_ != nullptr); for (size_t i = 0, j = 0; i < mono_data.size(); ++i, j += 2) {
} mono_data[i] = data[j];
}
wake_word_encode_task_ = xTaskCreateStatic([](void* arg) {
auto this_ = (CustomWakeWord*)arg; StoreWakeWordData(mono_data);
{ mn_state = multinet_->detect(multinet_model_data_, const_cast<int16_t*>(mono_data.data()));
auto start_time = esp_timer_get_time(); } else {
auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS); StoreWakeWordData(data);
encoder->SetComplexity(0); // 0 is the fastest mn_state = multinet_->detect(multinet_model_data_, const_cast<int16_t*>(data.data()));
}
int packets = 0;
for (auto& pcm: this_->wake_word_pcm_) { if (mn_state == ESP_MN_STATE_DETECTING) {
encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) { return;
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_); } else if (mn_state == ESP_MN_STATE_DETECTED) {
this_->wake_word_opus_.emplace_back(std::move(opus)); esp_mn_results_t *mn_result = multinet_->get_results(multinet_model_data_);
this_->wake_word_cv_.notify_all(); for (int i = 0; i < mn_result->num && running_; i++) {
}); ESP_LOGI(TAG, "Custom wake word detected: command_id=%d, string=%s, prob=%f",
packets++; mn_result->command_id[i], mn_result->string, mn_result->prob[i]);
} auto& command = commands_[mn_result->command_id[i] - 1];
this_->wake_word_pcm_.clear(); if (command.action == "wake") {
last_detected_wake_word_ = command.text;
auto end_time = esp_timer_get_time(); running_ = false;
ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000));
if (wake_word_detected_callback_) {
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_); wake_word_detected_callback_(last_detected_wake_word_);
this_->wake_word_opus_.push_back(std::vector<uint8_t>()); }
this_->wake_word_cv_.notify_all(); }
} }
vTaskDelete(NULL); multinet_->clean(multinet_model_data_);
}, "encode_wake_word", stack_size, this, 2, wake_word_encode_task_stack_, wake_word_encode_task_buffer_); } else if (mn_state == ESP_MN_STATE_TIMEOUT) {
} ESP_LOGD(TAG, "Command word detection timeout, cleaning state");
multinet_->clean(multinet_model_data_);
bool CustomWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) { }
std::unique_lock<std::mutex> lock(wake_word_mutex_); }
wake_word_cv_.wait(lock, [this]() {
return !wake_word_opus_.empty(); size_t CustomWakeWord::GetFeedSize() {
}); if (multinet_model_data_ == nullptr) {
opus.swap(wake_word_opus_.front()); return 0;
wake_word_opus_.pop_front(); }
return !opus.empty(); return multinet_->get_samp_chunksize(multinet_model_data_);
} }
void CustomWakeWord::StoreWakeWordData(const std::vector<int16_t>& data) {
// store audio data to wake_word_pcm_
wake_word_pcm_.push_back(data);
// keep about 2 seconds of data, detect duration is 30ms (sample_rate == 16000, chunksize == 512)
while (wake_word_pcm_.size() > 2000 / 30) {
wake_word_pcm_.pop_front();
}
}
void CustomWakeWord::EncodeWakeWordData() {
const size_t stack_size = 4096 * 7;
wake_word_opus_.clear();
if (wake_word_encode_task_stack_ == nullptr) {
wake_word_encode_task_stack_ = (StackType_t*)heap_caps_malloc(stack_size, MALLOC_CAP_SPIRAM);
assert(wake_word_encode_task_stack_ != nullptr);
}
if (wake_word_encode_task_buffer_ == nullptr) {
wake_word_encode_task_buffer_ = (StaticTask_t*)heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL);
assert(wake_word_encode_task_buffer_ != nullptr);
}
wake_word_encode_task_ = xTaskCreateStatic([](void* arg) {
auto this_ = (CustomWakeWord*)arg;
{
auto start_time = esp_timer_get_time();
auto encoder = std::make_unique<OpusEncoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
encoder->SetComplexity(0); // 0 is the fastest
int packets = 0;
for (auto& pcm: this_->wake_word_pcm_) {
encoder->Encode(std::move(pcm), [this_](std::vector<uint8_t>&& opus) {
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.emplace_back(std::move(opus));
this_->wake_word_cv_.notify_all();
});
packets++;
}
this_->wake_word_pcm_.clear();
auto end_time = esp_timer_get_time();
ESP_LOGI(TAG, "Encode wake word opus %d packets in %ld ms", packets, (long)((end_time - start_time) / 1000));
std::lock_guard<std::mutex> lock(this_->wake_word_mutex_);
this_->wake_word_opus_.push_back(std::vector<uint8_t>());
this_->wake_word_cv_.notify_all();
}
vTaskDelete(NULL);
}, "encode_wake_word", stack_size, this, 2, wake_word_encode_task_stack_, wake_word_encode_task_buffer_);
}
bool CustomWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) {
std::unique_lock<std::mutex> lock(wake_word_mutex_);
wake_word_cv_.wait(lock, [this]() {
return !wake_word_opus_.empty();
});
opus.swap(wake_word_opus_.front());
wake_word_opus_.pop_front();
return !opus.empty();
}

View File

@@ -1,58 +1,69 @@
#ifndef CUSTOM_WAKE_WORD_H #ifndef CUSTOM_WAKE_WORD_H
#define CUSTOM_WAKE_WORD_H #define CUSTOM_WAKE_WORD_H
#include <esp_attr.h> #include <esp_attr.h>
#include <esp_mn_iface.h> #include <esp_mn_iface.h>
#include <esp_mn_models.h> #include <esp_mn_models.h>
#include <model_path.h> #include <model_path.h>
#include <deque> #include <deque>
#include <string> #include <string>
#include <vector> #include <vector>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <condition_variable> #include <condition_variable>
#include <atomic> #include <atomic>
#include "audio_codec.h" #include "audio_codec.h"
#include "wake_word.h" #include "wake_word.h"
class CustomWakeWord : public WakeWord { class CustomWakeWord : public WakeWord {
public: public:
CustomWakeWord(); CustomWakeWord();
~CustomWakeWord(); ~CustomWakeWord();
bool Initialize(AudioCodec* codec, srmodel_list_t* models_list); bool Initialize(AudioCodec* codec, srmodel_list_t* models_list);
void Feed(const std::vector<int16_t>& data); void Feed(const std::vector<int16_t>& data);
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback); void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
void Start(); void Start();
void Stop(); void Stop();
size_t GetFeedSize(); size_t GetFeedSize();
void EncodeWakeWordData(); void EncodeWakeWordData();
bool GetWakeWordOpus(std::vector<uint8_t>& opus); bool GetWakeWordOpus(std::vector<uint8_t>& opus);
const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; } const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; }
private: private:
// multinet 相关成员变量 struct Command {
esp_mn_iface_t* multinet_ = nullptr; std::string command;
model_iface_data_t* multinet_model_data_ = nullptr; std::string text;
srmodel_list_t *models_ = nullptr; std::string action;
char* mn_name_ = nullptr; };
std::function<void(const std::string& wake_word)> wake_word_detected_callback_; // multinet 相关成员变量
AudioCodec* codec_ = nullptr; esp_mn_iface_t* multinet_ = nullptr;
std::string last_detected_wake_word_; model_iface_data_t* multinet_model_data_ = nullptr;
std::atomic<bool> running_ = false; srmodel_list_t *models_ = nullptr;
char* mn_name_ = nullptr;
TaskHandle_t wake_word_encode_task_ = nullptr; std::string language_ = "cn";
StaticTask_t* wake_word_encode_task_buffer_ = nullptr; int duration_ = 3000;
StackType_t* wake_word_encode_task_stack_ = nullptr; float threshold_ = 0.2;
std::deque<std::vector<int16_t>> wake_word_pcm_; std::deque<Command> commands_;
std::deque<std::vector<uint8_t>> wake_word_opus_;
std::mutex wake_word_mutex_; std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
std::condition_variable wake_word_cv_; AudioCodec* codec_ = nullptr;
std::string last_detected_wake_word_;
void StoreWakeWordData(const std::vector<int16_t>& data); std::atomic<bool> running_ = false;
};
TaskHandle_t wake_word_encode_task_ = nullptr;
#endif StaticTask_t* wake_word_encode_task_buffer_ = nullptr;
StackType_t* wake_word_encode_task_stack_ = nullptr;
std::deque<std::vector<int16_t>> wake_word_pcm_;
std::deque<std::vector<uint8_t>> wake_word_opus_;
std::mutex wake_word_mutex_;
std::condition_variable wake_word_cv_;
void StoreWakeWordData(const std::vector<int16_t>& data);
void ParseWakenetModelConfig();
};
#endif

View File

@@ -1,87 +1,87 @@
#include "esp_wake_word.h" #include "esp_wake_word.h"
#include <esp_log.h> #include <esp_log.h>
#define TAG "EspWakeWord" #define TAG "EspWakeWord"
EspWakeWord::EspWakeWord() { EspWakeWord::EspWakeWord() {
} }
EspWakeWord::~EspWakeWord() { EspWakeWord::~EspWakeWord() {
if (wakenet_data_ != nullptr) { if (wakenet_data_ != nullptr) {
wakenet_iface_->destroy(wakenet_data_); wakenet_iface_->destroy(wakenet_data_);
esp_srmodel_deinit(wakenet_model_); esp_srmodel_deinit(wakenet_model_);
} }
} }
bool EspWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list) { bool EspWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list) {
codec_ = codec; codec_ = codec;
if (models_list == nullptr) { if (models_list == nullptr) {
wakenet_model_ = esp_srmodel_init("model"); wakenet_model_ = esp_srmodel_init("model");
} else { } else {
wakenet_model_ = models_list; wakenet_model_ = models_list;
} }
if (wakenet_model_ == nullptr || wakenet_model_->num == -1) { if (wakenet_model_ == nullptr || wakenet_model_->num == -1) {
ESP_LOGE(TAG, "Failed to initialize wakenet model"); ESP_LOGE(TAG, "Failed to initialize wakenet model");
return false; return false;
} }
if(wakenet_model_->num > 1) { if(wakenet_model_->num > 1) {
ESP_LOGW(TAG, "More than one model found, using the first one"); ESP_LOGW(TAG, "More than one model found, using the first one");
} else if (wakenet_model_->num == 0) { } else if (wakenet_model_->num == 0) {
ESP_LOGE(TAG, "No model found"); ESP_LOGE(TAG, "No model found");
return false; return false;
} }
char *model_name = wakenet_model_->model_name[0]; char *model_name = wakenet_model_->model_name[0];
wakenet_iface_ = (esp_wn_iface_t*)esp_wn_handle_from_name(model_name); wakenet_iface_ = (esp_wn_iface_t*)esp_wn_handle_from_name(model_name);
wakenet_data_ = wakenet_iface_->create(model_name, DET_MODE_95); wakenet_data_ = wakenet_iface_->create(model_name, DET_MODE_95);
int frequency = wakenet_iface_->get_samp_rate(wakenet_data_); int frequency = wakenet_iface_->get_samp_rate(wakenet_data_);
int audio_chunksize = wakenet_iface_->get_samp_chunksize(wakenet_data_); int audio_chunksize = wakenet_iface_->get_samp_chunksize(wakenet_data_);
ESP_LOGI(TAG, "Wake word(%s),freq: %d, chunksize: %d", model_name, frequency, audio_chunksize); ESP_LOGI(TAG, "Wake word(%s),freq: %d, chunksize: %d", model_name, frequency, audio_chunksize);
return true; return true;
} }
void EspWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) { void EspWakeWord::OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback) {
wake_word_detected_callback_ = callback; wake_word_detected_callback_ = callback;
} }
void EspWakeWord::Start() { void EspWakeWord::Start() {
running_ = true; running_ = true;
} }
void EspWakeWord::Stop() { void EspWakeWord::Stop() {
running_ = false; running_ = false;
} }
void EspWakeWord::Feed(const std::vector<int16_t>& data) { void EspWakeWord::Feed(const std::vector<int16_t>& data) {
if (wakenet_data_ == nullptr || !running_) { if (wakenet_data_ == nullptr || !running_) {
return; return;
} }
int res = wakenet_iface_->detect(wakenet_data_, (int16_t *)data.data()); int res = wakenet_iface_->detect(wakenet_data_, (int16_t *)data.data());
if (res > 0) { if (res > 0) {
last_detected_wake_word_ = wakenet_iface_->get_word_name(wakenet_data_, res); last_detected_wake_word_ = wakenet_iface_->get_word_name(wakenet_data_, res);
running_ = false; running_ = false;
if (wake_word_detected_callback_) { if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_); wake_word_detected_callback_(last_detected_wake_word_);
} }
} }
} }
size_t EspWakeWord::GetFeedSize() { size_t EspWakeWord::GetFeedSize() {
if (wakenet_data_ == nullptr) { if (wakenet_data_ == nullptr) {
return 0; return 0;
} }
return wakenet_iface_->get_samp_chunksize(wakenet_data_); return wakenet_iface_->get_samp_chunksize(wakenet_data_);
} }
void EspWakeWord::EncodeWakeWordData() { void EspWakeWord::EncodeWakeWordData() {
} }
bool EspWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) { bool EspWakeWord::GetWakeWordOpus(std::vector<uint8_t>& opus) {
return false; return false;
} }

View File

@@ -1,42 +1,42 @@
#ifndef ESP_WAKE_WORD_H #ifndef ESP_WAKE_WORD_H
#define ESP_WAKE_WORD_H #define ESP_WAKE_WORD_H
#include <esp_wn_iface.h> #include <esp_wn_iface.h>
#include <esp_wn_models.h> #include <esp_wn_models.h>
#include <model_path.h> #include <model_path.h>
#include <string> #include <string>
#include <vector> #include <vector>
#include <functional> #include <functional>
#include <atomic> #include <atomic>
#include "audio_codec.h" #include "audio_codec.h"
#include "wake_word.h" #include "wake_word.h"
class EspWakeWord : public WakeWord { class EspWakeWord : public WakeWord {
public: public:
EspWakeWord(); EspWakeWord();
~EspWakeWord(); ~EspWakeWord();
bool Initialize(AudioCodec* codec, srmodel_list_t* models_list); bool Initialize(AudioCodec* codec, srmodel_list_t* models_list);
void Feed(const std::vector<int16_t>& data); void Feed(const std::vector<int16_t>& data);
void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback); void OnWakeWordDetected(std::function<void(const std::string& wake_word)> callback);
void Start(); void Start();
void Stop(); void Stop();
size_t GetFeedSize(); size_t GetFeedSize();
void EncodeWakeWordData(); void EncodeWakeWordData();
bool GetWakeWordOpus(std::vector<uint8_t>& opus); bool GetWakeWordOpus(std::vector<uint8_t>& opus);
const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; } const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; }
private: private:
esp_wn_iface_t *wakenet_iface_ = nullptr; esp_wn_iface_t *wakenet_iface_ = nullptr;
model_iface_data_t *wakenet_data_ = nullptr; model_iface_data_t *wakenet_data_ = nullptr;
srmodel_list_t *wakenet_model_ = nullptr; srmodel_list_t *wakenet_model_ = nullptr;
AudioCodec* codec_ = nullptr; AudioCodec* codec_ = nullptr;
std::atomic<bool> running_ = false; std::atomic<bool> running_ = false;
std::function<void(const std::string& wake_word)> wake_word_detected_callback_; std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
std::string last_detected_wake_word_; std::string last_detected_wake_word_;
}; };
#endif #endif

View File

@@ -1,326 +1,326 @@
# 自定义开发板指南 # 自定义开发板指南
本指南介绍如何为小智AI语音聊天机器人项目定制一个新的开发板初始化程序。小智AI支持70多种ESP32系列开发板每个开发板的初始化代码都放在对应的目录下。 本指南介绍如何为小智AI语音聊天机器人项目定制一个新的开发板初始化程序。小智AI支持70多种ESP32系列开发板每个开发板的初始化代码都放在对应的目录下。
## 重要提示 ## 重要提示
> **警告**: 对于自定义开发板当IO配置与原有开发板不同时切勿直接覆盖原有开发板的配置编译固件。必须创建新的开发板类型或者通过config.json文件中的builds配置不同的name和sdkconfig宏定义来区分。使用 `python scripts/release.py [开发板目录名字]` 来编译打包固件。 > **警告**: 对于自定义开发板当IO配置与原有开发板不同时切勿直接覆盖原有开发板的配置编译固件。必须创建新的开发板类型或者通过config.json文件中的builds配置不同的name和sdkconfig宏定义来区分。使用 `python scripts/release.py [开发板目录名字]` 来编译打包固件。
> >
> 如果直接覆盖原有配置将来OTA升级时您的自定义固件可能会被原有开发板的标准固件覆盖导致您的设备无法正常工作。每个开发板有唯一的标识和对应的固件升级通道保持开发板标识的唯一性非常重要。 > 如果直接覆盖原有配置将来OTA升级时您的自定义固件可能会被原有开发板的标准固件覆盖导致您的设备无法正常工作。每个开发板有唯一的标识和对应的固件升级通道保持开发板标识的唯一性非常重要。
## 目录结构 ## 目录结构
每个开发板的目录结构通常包含以下文件: 每个开发板的目录结构通常包含以下文件:
- `xxx_board.cc` - 主要的板级初始化代码,实现了板子相关的初始化和功能 - `xxx_board.cc` - 主要的板级初始化代码,实现了板子相关的初始化和功能
- `config.h` - 板级配置文件,定义了硬件管脚映射和其他配置项 - `config.h` - 板级配置文件,定义了硬件管脚映射和其他配置项
- `config.json` - 编译配置,指定目标芯片和特殊的编译选项 - `config.json` - 编译配置,指定目标芯片和特殊的编译选项
- `README.md` - 开发板相关的说明文档 - `README.md` - 开发板相关的说明文档
## 定制开发板步骤 ## 定制开发板步骤
### 1. 创建新的开发板目录 ### 1. 创建新的开发板目录
首先在`boards/`目录下创建一个新的目录,例如`my-custom-board/` 首先在`boards/`目录下创建一个新的目录,例如`my-custom-board/`
```bash ```bash
mkdir main/boards/my-custom-board mkdir main/boards/my-custom-board
``` ```
### 2. 创建配置文件 ### 2. 创建配置文件
#### config.h #### config.h
`config.h`中定义所有的硬件配置,包括: `config.h`中定义所有的硬件配置,包括:
- 音频采样率和I2S引脚配置 - 音频采样率和I2S引脚配置
- 音频编解码芯片地址和I2C引脚配置 - 音频编解码芯片地址和I2C引脚配置
- 按钮和LED引脚配置 - 按钮和LED引脚配置
- 显示屏参数和引脚配置 - 显示屏参数和引脚配置
参考示例来自lichuang-c3-dev 参考示例来自lichuang-c3-dev
```c ```c
#ifndef _BOARD_CONFIG_H_ #ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_ #define _BOARD_CONFIG_H_
#include <driver/gpio.h> #include <driver/gpio.h>
// 音频配置 // 音频配置
#define AUDIO_INPUT_SAMPLE_RATE 24000 #define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000 #define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10 #define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10
#define AUDIO_I2S_GPIO_WS GPIO_NUM_12 #define AUDIO_I2S_GPIO_WS GPIO_NUM_12
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8 #define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7 #define AUDIO_I2S_GPIO_DIN GPIO_NUM_7
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11 #define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11
#define AUDIO_CODEC_PA_PIN GPIO_NUM_13 #define AUDIO_CODEC_PA_PIN GPIO_NUM_13
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0 #define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1 #define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR #define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
// 按钮配置 // 按钮配置
#define BOOT_BUTTON_GPIO GPIO_NUM_9 #define BOOT_BUTTON_GPIO GPIO_NUM_9
// 显示屏配置 // 显示屏配置
#define DISPLAY_SPI_SCK_PIN GPIO_NUM_3 #define DISPLAY_SPI_SCK_PIN GPIO_NUM_3
#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_5 #define DISPLAY_SPI_MOSI_PIN GPIO_NUM_5
#define DISPLAY_DC_PIN GPIO_NUM_6 #define DISPLAY_DC_PIN GPIO_NUM_6
#define DISPLAY_SPI_CS_PIN GPIO_NUM_4 #define DISPLAY_SPI_CS_PIN GPIO_NUM_4
#define DISPLAY_WIDTH 320 #define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240 #define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X true #define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false #define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY true #define DISPLAY_SWAP_XY true
#define DISPLAY_OFFSET_X 0 #define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0 #define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2 #define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true #define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
#endif // _BOARD_CONFIG_H_ #endif // _BOARD_CONFIG_H_
``` ```
#### config.json #### config.json
`config.json`中定义编译配置: `config.json`中定义编译配置:
```json ```json
{ {
"target": "esp32s3", // 目标芯片型号: esp32, esp32s3, esp32c3等 "target": "esp32s3", // 目标芯片型号: esp32, esp32s3, esp32c3等
"builds": [ "builds": [
{ {
"name": "my-custom-board", // 开发板名称 "name": "my-custom-board", // 开发板名称
"sdkconfig_append": [ "sdkconfig_append": [
// 额外需要的编译配置 // 额外需要的编译配置
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"" "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
] ]
} }
] ]
} }
``` ```
### 3. 编写板级初始化代码 ### 3. 编写板级初始化代码
创建一个`my_custom_board.cc`文件,实现开发板的所有初始化逻辑。 创建一个`my_custom_board.cc`文件,实现开发板的所有初始化逻辑。
一个基本的开发板类定义包含以下几个部分: 一个基本的开发板类定义包含以下几个部分:
1. **类定义**:继承自`WifiBoard``Ml307Board` 1. **类定义**:继承自`WifiBoard``Ml307Board`
2. **初始化函数**包括I2C、显示屏、按钮、IoT等组件的初始化 2. **初始化函数**包括I2C、显示屏、按钮、IoT等组件的初始化
3. **虚函数重写**:如`GetAudioCodec()``GetDisplay()``GetBacklight()` 3. **虚函数重写**:如`GetAudioCodec()``GetDisplay()``GetBacklight()`
4. **注册开发板**:使用`DECLARE_BOARD`宏注册开发板 4. **注册开发板**:使用`DECLARE_BOARD`宏注册开发板
```cpp ```cpp
#include "wifi_board.h" #include "wifi_board.h"
#include "codecs/es8311_audio_codec.h" #include "codecs/es8311_audio_codec.h"
#include "display/lcd_display.h" #include "display/lcd_display.h"
#include "application.h" #include "application.h"
#include "button.h" #include "button.h"
#include "config.h" #include "config.h"
#include "mcp_server.h" #include "mcp_server.h"
#include <esp_log.h> #include <esp_log.h>
#include <driver/i2c_master.h> #include <driver/i2c_master.h>
#include <driver/spi_common.h> #include <driver/spi_common.h>
#define TAG "MyCustomBoard" #define TAG "MyCustomBoard"
class MyCustomBoard : public WifiBoard { class MyCustomBoard : public WifiBoard {
private: private:
i2c_master_bus_handle_t codec_i2c_bus_; i2c_master_bus_handle_t codec_i2c_bus_;
Button boot_button_; Button boot_button_;
LcdDisplay* display_; LcdDisplay* display_;
// I2C初始化 // I2C初始化
void InitializeI2c() { void InitializeI2c() {
i2c_master_bus_config_t i2c_bus_cfg = { i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = I2C_NUM_0, .i2c_port = I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT, .clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7, .glitch_ignore_cnt = 7,
.intr_priority = 0, .intr_priority = 0,
.trans_queue_depth = 0, .trans_queue_depth = 0,
.flags = { .flags = {
.enable_internal_pullup = 1, .enable_internal_pullup = 1,
}, },
}; };
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_)); ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
} }
// SPI初始化用于显示屏 // SPI初始化用于显示屏
void InitializeSpi() { void InitializeSpi() {
spi_bus_config_t buscfg = {}; spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN; buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
buscfg.miso_io_num = GPIO_NUM_NC; buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN; buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN;
buscfg.quadwp_io_num = GPIO_NUM_NC; buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC; buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t); buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
} }
// 按钮初始化 // 按钮初始化
void InitializeButtons() { void InitializeButtons() {
boot_button_.OnClick([this]() { boot_button_.OnClick([this]() {
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration(); ResetWifiConfiguration();
} }
app.ToggleChatState(); app.ToggleChatState();
}); });
} }
// 显示屏初始化以ST7789为例 // 显示屏初始化以ST7789为例
void InitializeDisplay() { void InitializeDisplay() {
esp_lcd_panel_io_handle_t panel_io = nullptr; esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr; esp_lcd_panel_handle_t panel = nullptr;
esp_lcd_panel_io_spi_config_t io_config = {}; esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN; io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
io_config.dc_gpio_num = DISPLAY_DC_PIN; io_config.dc_gpio_num = DISPLAY_DC_PIN;
io_config.spi_mode = 2; io_config.spi_mode = 2;
io_config.pclk_hz = 80 * 1000 * 1000; io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 10; io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8; io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8; io_config.lcd_param_bits = 8;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io)); ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io));
esp_lcd_panel_dev_config_t panel_config = {}; esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC; panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16; panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel); esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel); esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true); esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
// 创建显示屏对象 // 创建显示屏对象
display_ = new SpiLcdDisplay(panel_io, panel, display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_WIDTH, DISPLAY_HEIGHT,
DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
} }
// MCP Tools 初始化 // MCP Tools 初始化
void InitializeTools() { void InitializeTools() {
// 参考 MCP 文档 // 参考 MCP 文档
} }
public: public:
// 构造函数 // 构造函数
MyCustomBoard() : boot_button_(BOOT_BUTTON_GPIO) { MyCustomBoard() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c(); InitializeI2c();
InitializeSpi(); InitializeSpi();
InitializeDisplay(); InitializeDisplay();
InitializeButtons(); InitializeButtons();
InitializeTools(); InitializeTools();
GetBacklight()->SetBrightness(100); GetBacklight()->SetBrightness(100);
} }
// 获取音频编解码器 // 获取音频编解码器
virtual AudioCodec* GetAudioCodec() override { virtual AudioCodec* GetAudioCodec() override {
static Es8311AudioCodec audio_codec( static Es8311AudioCodec audio_codec(
codec_i2c_bus_, codec_i2c_bus_,
I2C_NUM_0, I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE, AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN, AUDIO_I2S_GPIO_DIN,
AUDIO_CODEC_PA_PIN, AUDIO_CODEC_PA_PIN,
AUDIO_CODEC_ES8311_ADDR); AUDIO_CODEC_ES8311_ADDR);
return &audio_codec; return &audio_codec;
} }
// 获取显示屏 // 获取显示屏
virtual Display* GetDisplay() override { virtual Display* GetDisplay() override {
return display_; return display_;
} }
// 获取背光控制 // 获取背光控制
virtual Backlight* GetBacklight() override { virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight; return &backlight;
} }
}; };
// 注册开发板 // 注册开发板
DECLARE_BOARD(MyCustomBoard); DECLARE_BOARD(MyCustomBoard);
``` ```
### 4. 创建README.md ### 4. 创建README.md
在README.md中说明开发板的特性、硬件要求、编译和烧录步骤 在README.md中说明开发板的特性、硬件要求、编译和烧录步骤
## 常见开发板组件 ## 常见开发板组件
### 1. 显示屏 ### 1. 显示屏
项目支持多种显示屏驱动,包括: 项目支持多种显示屏驱动,包括:
- ST7789 (SPI) - ST7789 (SPI)
- ILI9341 (SPI) - ILI9341 (SPI)
- SH8601 (QSPI) - SH8601 (QSPI)
- 等... - 等...
### 2. 音频编解码器 ### 2. 音频编解码器
支持的编解码器包括: 支持的编解码器包括:
- ES8311 (常用) - ES8311 (常用)
- ES7210 (麦克风阵列) - ES7210 (麦克风阵列)
- AW88298 (功放) - AW88298 (功放)
- 等... - 等...
### 3. 电源管理 ### 3. 电源管理
一些开发板使用电源管理芯片: 一些开发板使用电源管理芯片:
- AXP2101 - AXP2101
- 其他可用的PMIC - 其他可用的PMIC
### 4. MCP设备控制 ### 4. MCP设备控制
可以添加各种MCP工具让AI能够使用: 可以添加各种MCP工具让AI能够使用:
- Speaker (扬声器控制) - Speaker (扬声器控制)
- Screen (屏幕亮度调节) - Screen (屏幕亮度调节)
- Battery (电池电量读取) - Battery (电池电量读取)
- Light (灯光控制) - Light (灯光控制)
- 等... - 等...
## 开发板类继承关系 ## 开发板类继承关系
- `Board` - 基础板级类 - `Board` - 基础板级类
- `WifiBoard` - Wi-Fi连接的开发板 - `WifiBoard` - Wi-Fi连接的开发板
- `Ml307Board` - 使用4G模块的开发板 - `Ml307Board` - 使用4G模块的开发板
- `DualNetworkBoard` - 支持Wi-Fi与4G网络切换的开发板 - `DualNetworkBoard` - 支持Wi-Fi与4G网络切换的开发板
## 开发技巧 ## 开发技巧
1. **参考相似的开发板**:如果您的新开发板与现有开发板有相似之处,可以参考现有实现 1. **参考相似的开发板**:如果您的新开发板与现有开发板有相似之处,可以参考现有实现
2. **分步调试**:先实现基础功能(如显示),再添加更复杂的功能(如音频) 2. **分步调试**:先实现基础功能(如显示),再添加更复杂的功能(如音频)
3. **管脚映射**确保在config.h中正确配置所有管脚映射 3. **管脚映射**确保在config.h中正确配置所有管脚映射
4. **检查硬件兼容性**:确认所有芯片和驱动程序的兼容性 4. **检查硬件兼容性**:确认所有芯片和驱动程序的兼容性
## 可能遇到的问题 ## 可能遇到的问题
1. **显示屏不正常**检查SPI配置、镜像设置和颜色反转设置 1. **显示屏不正常**检查SPI配置、镜像设置和颜色反转设置
2. **音频无输出**检查I2S配置、PA使能引脚和编解码器地址 2. **音频无输出**检查I2S配置、PA使能引脚和编解码器地址
3. **无法连接网络**检查Wi-Fi凭据和网络配置 3. **无法连接网络**检查Wi-Fi凭据和网络配置
4. **无法与服务器通信**检查MQTT或WebSocket配置 4. **无法与服务器通信**检查MQTT或WebSocket配置
## 参考资料 ## 参考资料
- ESP-IDF 文档: https://docs.espressif.com/projects/esp-idf/ - ESP-IDF 文档: https://docs.espressif.com/projects/esp-idf/
- LVGL 文档: https://docs.lvgl.io/ - LVGL 文档: https://docs.lvgl.io/
- ESP-SR 文档: https://github.com/espressif/esp-sr - ESP-SR 文档: https://github.com/espressif/esp-sr

View File

@@ -1,299 +1,299 @@
#include "wifi_board.h" #include "wifi_board.h"
#include "codecs/es8311_audio_codec.h" #include "codecs/es8311_audio_codec.h"
#include "codecs/no_audio_codec.h" #include "codecs/no_audio_codec.h"
#include "display/lcd_display.h" #include "display/lcd_display.h"
#include "application.h" #include "application.h"
#include "button.h" #include "button.h"
#include "config.h" #include "config.h"
#include "led/single_led.h" #include "led/single_led.h"
#include "i2c_device.h" #include "i2c_device.h"
#include <wifi_station.h> #include <wifi_station.h>
#include <esp_log.h> #include <esp_log.h>
#include <driver/i2c_master.h> #include <driver/i2c_master.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <freertos/timers.h> #include <freertos/timers.h>
#include <esp_lcd_panel_io.h> #include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_vendor.h> #include <esp_lcd_panel_vendor.h>
#include <esp_lcd_panel_ops.h> #include <esp_lcd_panel_ops.h>
#define TAG "atk_dnesp32s3_box" #define TAG "atk_dnesp32s3_box"
class ATK_NoAudioCodecDuplex : public NoAudioCodec { class ATK_NoAudioCodecDuplex : public NoAudioCodec {
public: public:
ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { ATK_NoAudioCodecDuplex(int input_sample_rate, int output_sample_rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) {
duplex_ = true; duplex_ = true;
input_sample_rate_ = input_sample_rate; input_sample_rate_ = input_sample_rate;
output_sample_rate_ = output_sample_rate; output_sample_rate_ = output_sample_rate;
i2s_chan_config_t chan_cfg = { i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0, .id = I2S_NUM_0,
.role = I2S_ROLE_MASTER, .role = I2S_ROLE_MASTER,
.dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM,
.dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM,
.auto_clear_after_cb = true, .auto_clear_after_cb = true,
.auto_clear_before_cb = false, .auto_clear_before_cb = false,
.intr_priority = 0, .intr_priority = 0,
}; };
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_));
i2s_std_config_t std_cfg = { i2s_std_config_t std_cfg = {
.clk_cfg = { .clk_cfg = {
.sample_rate_hz = (uint32_t)output_sample_rate_, .sample_rate_hz = (uint32_t)output_sample_rate_,
.clk_src = I2S_CLK_SRC_DEFAULT, .clk_src = I2S_CLK_SRC_DEFAULT,
.mclk_multiple = I2S_MCLK_MULTIPLE_256, .mclk_multiple = I2S_MCLK_MULTIPLE_256,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.ext_clk_freq_hz = 0, .ext_clk_freq_hz = 0,
#endif #endif
}, },
.slot_cfg = { .slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO, .slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH, .slot_mask = I2S_STD_SLOT_BOTH,
.ws_width = I2S_DATA_BIT_WIDTH_16BIT, .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
.ws_pol = false, .ws_pol = false,
.bit_shift = true, .bit_shift = true,
#ifdef I2S_HW_VERSION_2 #ifdef I2S_HW_VERSION_2
.left_align = true, .left_align = true,
.big_endian = false, .big_endian = false,
.bit_order_lsb = false .bit_order_lsb = false
#endif #endif
}, },
.gpio_cfg = { .gpio_cfg = {
.mclk = I2S_GPIO_UNUSED, .mclk = I2S_GPIO_UNUSED,
.bclk = bclk, .bclk = bclk,
.ws = ws, .ws = ws,
.dout = dout, .dout = dout,
.din = din, .din = din,
.invert_flags = { .invert_flags = {
.mclk_inv = false, .mclk_inv = false,
.bclk_inv = false, .bclk_inv = false,
.ws_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(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg));
ESP_LOGI(TAG, "Duplex channels created"); ESP_LOGI(TAG, "Duplex channels created");
} }
}; };
class XL9555_IN : public I2cDevice { class XL9555_IN : public I2cDevice {
public: public:
XL9555_IN(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { XL9555_IN(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
WriteReg(0x06, 0x3B); WriteReg(0x06, 0x3B);
WriteReg(0x07, 0xFE); WriteReg(0x07, 0xFE);
} }
void xl9555_cfg(void) { void xl9555_cfg(void) {
WriteReg(0x06, 0x1B); WriteReg(0x06, 0x1B);
WriteReg(0x07, 0xFE); WriteReg(0x07, 0xFE);
} }
void SetOutputState(uint8_t bit, uint8_t level) { void SetOutputState(uint8_t bit, uint8_t level) {
uint16_t data; uint16_t data;
int index = bit; int index = bit;
if (bit < 8) { if (bit < 8) {
data = ReadReg(0x02); data = ReadReg(0x02);
} else { } else {
data = ReadReg(0x03); data = ReadReg(0x03);
index -= 8; index -= 8;
} }
data = (data & ~(1 << index)) | (level << index); data = (data & ~(1 << index)) | (level << index);
if (bit < 8) { if (bit < 8) {
WriteReg(0x02, data); WriteReg(0x02, data);
} else { } else {
WriteReg(0x03, data); WriteReg(0x03, data);
} }
} }
int GetPingState(uint16_t pin) { int GetPingState(uint16_t pin) {
uint8_t data; uint8_t data;
if (pin <= 0x0080) { if (pin <= 0x0080) {
data = ReadReg(0x00); data = ReadReg(0x00);
return (data & (uint8_t)(pin & 0xFF)) ? 1 : 0; return (data & (uint8_t)(pin & 0xFF)) ? 1 : 0;
} else { } else {
data = ReadReg(0x01); data = ReadReg(0x01);
return (data & (uint8_t)((pin >> 8) & 0xFF )) ? 1 : 0; return (data & (uint8_t)((pin >> 8) & 0xFF )) ? 1 : 0;
} }
return 0; return 0;
} }
}; };
class atk_dnesp32s3_box : public WifiBoard { class atk_dnesp32s3_box : public WifiBoard {
private: private:
i2c_master_bus_handle_t i2c_bus_; i2c_master_bus_handle_t i2c_bus_;
i2c_master_dev_handle_t xl9555_handle_; i2c_master_dev_handle_t xl9555_handle_;
Button boot_button_; Button boot_button_;
LcdDisplay* display_; LcdDisplay* display_;
XL9555_IN* xl9555_in_; XL9555_IN* xl9555_in_;
bool es8311_detected_ = false; bool es8311_detected_ = false;
void InitializeI2c() { void InitializeI2c() {
// Initialize I2C peripheral // Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = { i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)0, .i2c_port = (i2c_port_t)0,
.sda_io_num = GPIO_NUM_48, .sda_io_num = GPIO_NUM_48,
.scl_io_num = GPIO_NUM_45, .scl_io_num = GPIO_NUM_45,
.clk_source = I2C_CLK_SRC_DEFAULT, .clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7, .glitch_ignore_cnt = 7,
.intr_priority = 0, .intr_priority = 0,
.trans_queue_depth = 0, .trans_queue_depth = 0,
.flags = { .flags = {
.enable_internal_pullup = 1, .enable_internal_pullup = 1,
}, },
}; };
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
// Initialize XL9555 // Initialize XL9555
xl9555_in_ = new XL9555_IN(i2c_bus_, 0x20); xl9555_in_ = new XL9555_IN(i2c_bus_, 0x20);
if (xl9555_in_->GetPingState(0x0020) == 1) { if (xl9555_in_->GetPingState(0x0020) == 1) {
es8311_detected_ = true; /* 音频设备标志位SPK_CTRL_IO为高电平时该标志位置1且判定为ES8311 */ es8311_detected_ = true; /* 音频设备标志位SPK_CTRL_IO为高电平时该标志位置1且判定为ES8311 */
} else { } else {
es8311_detected_ = false; /* 音频设备标志位SPK_CTRL_IO为低电平时该标志位置0且判定为NS4168 */ es8311_detected_ = false; /* 音频设备标志位SPK_CTRL_IO为低电平时该标志位置0且判定为NS4168 */
} }
xl9555_in_->xl9555_cfg(); xl9555_in_->xl9555_cfg();
} }
void InitializeATK_ST7789_80_Display() { void InitializeATK_ST7789_80_Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr; esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr; esp_lcd_panel_handle_t panel = nullptr;
/* 配置RD引脚 */ /* 配置RD引脚 */
gpio_config_t gpio_init_struct; gpio_config_t gpio_init_struct;
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT; gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT;
gpio_init_struct.pin_bit_mask = 1ull << LCD_NUM_RD; gpio_init_struct.pin_bit_mask = 1ull << LCD_NUM_RD;
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&gpio_init_struct); gpio_config(&gpio_init_struct);
gpio_set_level(LCD_NUM_RD, 1); gpio_set_level(LCD_NUM_RD, 1);
esp_lcd_i80_bus_handle_t i80_bus = NULL; esp_lcd_i80_bus_handle_t i80_bus = NULL;
esp_lcd_i80_bus_config_t bus_config = { esp_lcd_i80_bus_config_t bus_config = {
.dc_gpio_num = LCD_NUM_DC, .dc_gpio_num = LCD_NUM_DC,
.wr_gpio_num = LCD_NUM_WR, .wr_gpio_num = LCD_NUM_WR,
.clk_src = LCD_CLK_SRC_DEFAULT, .clk_src = LCD_CLK_SRC_DEFAULT,
.data_gpio_nums = { .data_gpio_nums = {
GPIO_LCD_D0, GPIO_LCD_D0,
GPIO_LCD_D1, GPIO_LCD_D1,
GPIO_LCD_D2, GPIO_LCD_D2,
GPIO_LCD_D3, GPIO_LCD_D3,
GPIO_LCD_D4, GPIO_LCD_D4,
GPIO_LCD_D5, GPIO_LCD_D5,
GPIO_LCD_D6, GPIO_LCD_D6,
GPIO_LCD_D7, GPIO_LCD_D7,
}, },
.bus_width = 8, .bus_width = 8,
.max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t), .max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t),
.psram_trans_align = 64, .psram_trans_align = 64,
.sram_trans_align = 4, .sram_trans_align = 4,
}; };
ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
esp_lcd_panel_io_i80_config_t io_config = { esp_lcd_panel_io_i80_config_t io_config = {
.cs_gpio_num = LCD_NUM_CS, .cs_gpio_num = LCD_NUM_CS,
.pclk_hz = (10 * 1000 * 1000), .pclk_hz = (10 * 1000 * 1000),
.trans_queue_depth = 10, .trans_queue_depth = 10,
.on_color_trans_done = nullptr, .on_color_trans_done = nullptr,
.user_ctx = nullptr, .user_ctx = nullptr,
.lcd_cmd_bits = 8, .lcd_cmd_bits = 8,
.lcd_param_bits = 8, .lcd_param_bits = 8,
.dc_levels = { .dc_levels = {
.dc_idle_level = 0, .dc_idle_level = 0,
.dc_cmd_level = 0, .dc_cmd_level = 0,
.dc_dummy_level = 0, .dc_dummy_level = 0,
.dc_data_level = 1, .dc_data_level = 1,
}, },
.flags = { .flags = {
.swap_color_bytes = 0, .swap_color_bytes = 0,
}, },
}; };
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io)); ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io));
esp_lcd_panel_dev_config_t panel_config = { esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = LCD_NUM_RST, .reset_gpio_num = LCD_NUM_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16, .bits_per_pixel = 16,
}; };
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel); esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel); esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT); esp_lcd_panel_invert_color(panel, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
esp_lcd_panel_set_gap(panel, 0, 0); esp_lcd_panel_set_gap(panel, 0, 0);
uint8_t data0[] = {0x00}; uint8_t data0[] = {0x00};
uint8_t data1[] = {0x65}; uint8_t data1[] = {0x65};
esp_lcd_panel_io_tx_param(panel_io, 0x36, data0, 1); esp_lcd_panel_io_tx_param(panel_io, 0x36, data0, 1);
esp_lcd_panel_io_tx_param(panel_io, 0x3A, data1, 1); esp_lcd_panel_io_tx_param(panel_io, 0x3A, data1, 1);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true)); ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel, true));
display_ = new SpiLcdDisplay(panel_io, panel, display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
} }
void InitializeButtons() { void InitializeButtons() {
boot_button_.OnClick([this]() { boot_button_.OnClick([this]() {
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration(); ResetWifiConfiguration();
} }
app.ToggleChatState(); app.ToggleChatState();
}); });
} }
public: public:
atk_dnesp32s3_box() : boot_button_(BOOT_BUTTON_GPIO) { atk_dnesp32s3_box() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c(); InitializeI2c();
InitializeATK_ST7789_80_Display(); InitializeATK_ST7789_80_Display();
xl9555_in_->SetOutputState(5, 1); xl9555_in_->SetOutputState(5, 1);
xl9555_in_->SetOutputState(7, 1); xl9555_in_->SetOutputState(7, 1);
InitializeButtons(); InitializeButtons();
} }
virtual AudioCodec* GetAudioCodec() override { virtual AudioCodec* GetAudioCodec() override {
/* 根据探测结果初始化编解码器 */ /* 根据探测结果初始化编解码器 */
if (es8311_detected_) { if (es8311_detected_) {
/* 使用ES8311 驱动 */ /* 使用ES8311 驱动 */
static Es8311AudioCodec audio_codec( static Es8311AudioCodec audio_codec(
i2c_bus_, i2c_bus_,
I2C_NUM_0, I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE, AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
GPIO_NUM_NC, GPIO_NUM_NC,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN, AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC, GPIO_NUM_NC,
AUDIO_CODEC_ES8311_ADDR, AUDIO_CODEC_ES8311_ADDR,
false); false);
return &audio_codec; return &audio_codec;
} else { } else {
static ATK_NoAudioCodecDuplex audio_codec( static ATK_NoAudioCodecDuplex audio_codec(
AUDIO_INPUT_SAMPLE_RATE, AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN); AUDIO_I2S_GPIO_DIN);
return &audio_codec; return &audio_codec;
} }
return NULL; return NULL;
} }
virtual Display* GetDisplay() override { virtual Display* GetDisplay() override {
return display_; return display_;
} }
}; };
DECLARE_BOARD(atk_dnesp32s3_box); DECLARE_BOARD(atk_dnesp32s3_box);

View File

@@ -1,46 +1,46 @@
#ifndef _BOARD_CONFIG_H_ #ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_ #define _BOARD_CONFIG_H_
#include <driver/gpio.h> #include <driver/gpio.h>
#define AUDIO_INPUT_SAMPLE_RATE 24000 #define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000 #define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_I2S_GPIO_WS GPIO_NUM_13 #define AUDIO_I2S_GPIO_WS GPIO_NUM_13
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_21 #define AUDIO_I2S_GPIO_BCLK GPIO_NUM_21
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_47 #define AUDIO_I2S_GPIO_DIN GPIO_NUM_47
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_14 #define AUDIO_I2S_GPIO_DOUT GPIO_NUM_14
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR #define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define BUILTIN_LED_GPIO GPIO_NUM_4 #define BUILTIN_LED_GPIO GPIO_NUM_4
#define BOOT_BUTTON_GPIO GPIO_NUM_0 #define BOOT_BUTTON_GPIO GPIO_NUM_0
#define DISPLAY_OFFSET_X 0 #define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0 #define DISPLAY_OFFSET_Y 0
#define DISPLAY_WIDTH 320 #define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240 #define DISPLAY_HEIGHT 240
#define DISPLAY_SWAP_XY true #define DISPLAY_SWAP_XY true
#define DISPLAY_MIRROR_X true #define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false #define DISPLAY_MIRROR_Y false
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC #define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true #define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
// Pin Definitions // Pin Definitions
#define LCD_NUM_CS GPIO_NUM_1 #define LCD_NUM_CS GPIO_NUM_1
#define LCD_NUM_DC GPIO_NUM_2 #define LCD_NUM_DC GPIO_NUM_2
#define LCD_NUM_RD GPIO_NUM_41 #define LCD_NUM_RD GPIO_NUM_41
#define LCD_NUM_WR GPIO_NUM_42 #define LCD_NUM_WR GPIO_NUM_42
#define LCD_NUM_RST GPIO_NUM_NC #define LCD_NUM_RST GPIO_NUM_NC
#define GPIO_LCD_D0 GPIO_NUM_40 #define GPIO_LCD_D0 GPIO_NUM_40
#define GPIO_LCD_D1 GPIO_NUM_39 #define GPIO_LCD_D1 GPIO_NUM_39
#define GPIO_LCD_D2 GPIO_NUM_38 #define GPIO_LCD_D2 GPIO_NUM_38
#define GPIO_LCD_D3 GPIO_NUM_12 #define GPIO_LCD_D3 GPIO_NUM_12
#define GPIO_LCD_D4 GPIO_NUM_11 #define GPIO_LCD_D4 GPIO_NUM_11
#define GPIO_LCD_D5 GPIO_NUM_10 #define GPIO_LCD_D5 GPIO_NUM_10
#define GPIO_LCD_D6 GPIO_NUM_9 #define GPIO_LCD_D6 GPIO_NUM_9
#define GPIO_LCD_D7 GPIO_NUM_46 #define GPIO_LCD_D7 GPIO_NUM_46
#endif // _BOARD_CONFIG_H_ #endif // _BOARD_CONFIG_H_

View File

@@ -1,11 +1,11 @@
{ {
"target": "esp32s3", "target": "esp32s3",
"builds": [ "builds": [
{ {
"name": "atk-dnesp32s3-box", "name": "atk-dnesp32s3-box",
"sdkconfig_append": [ "sdkconfig_append": [
"CONFIG_USE_WECHAT_MESSAGE_STYLE=y" "CONFIG_USE_WECHAT_MESSAGE_STYLE=y"
] ]
} }
] ]
} }

View File

@@ -1,388 +1,388 @@
#include "wifi_board.h" #include "wifi_board.h"
#include "codecs/es8311_audio_codec.h" #include "codecs/es8311_audio_codec.h"
#include "display/lcd_display.h" #include "display/lcd_display.h"
#include "system_reset.h" #include "system_reset.h"
#include "application.h" #include "application.h"
#include "button.h" #include "button.h"
#include "config.h" #include "config.h"
#include "power_save_timer.h" #include "power_save_timer.h"
#include "led/single_led.h" #include "led/single_led.h"
#include "assets/lang_config.h" #include "assets/lang_config.h"
#include "power_manager.h" #include "power_manager.h"
#include "i2c_device.h" #include "i2c_device.h"
#include <esp_log.h> #include <esp_log.h>
#include <esp_lcd_panel_vendor.h> #include <esp_lcd_panel_vendor.h>
#include <wifi_station.h> #include <wifi_station.h>
#include <driver/rtc_io.h> #include <driver/rtc_io.h>
#include <esp_sleep.h> #include <esp_sleep.h>
#define TAG "atk_dnesp32s3_box0" #define TAG "atk_dnesp32s3_box0"
class atk_dnesp32s3_box0 : public WifiBoard { class atk_dnesp32s3_box0 : public WifiBoard {
private: private:
i2c_master_bus_handle_t i2c_bus_; i2c_master_bus_handle_t i2c_bus_;
Button right_button_; Button right_button_;
Button left_button_; Button left_button_;
Button middle_button_; Button middle_button_;
LcdDisplay* display_; LcdDisplay* display_;
PowerSaveTimer* power_save_timer_; PowerSaveTimer* power_save_timer_;
PowerManager* power_manager_; PowerManager* power_manager_;
PowerSupply power_status_; PowerSupply power_status_;
LcdStatus LcdStatus_ = kDevicelcdbacklightOn; LcdStatus LcdStatus_ = kDevicelcdbacklightOn;
PowerSleep power_sleep_ = kDeviceNoSleep; PowerSleep power_sleep_ = kDeviceNoSleep;
WakeStatus wake_status_ = kDeviceAwakened; WakeStatus wake_status_ = kDeviceAwakened;
XiaozhiStatus XiaozhiStatus_ = kDevice_Exit_Distributionnetwork; XiaozhiStatus XiaozhiStatus_ = kDevice_Exit_Distributionnetwork;
esp_timer_handle_t wake_timer_handle_; esp_timer_handle_t wake_timer_handle_;
esp_lcd_panel_io_handle_t panel_io = nullptr; esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr; esp_lcd_panel_handle_t panel = nullptr;
int ticks_ = 0; int ticks_ = 0;
const int kChgCtrlInterval = 5; const int kChgCtrlInterval = 5;
void InitializeBoardPowerManager() { void InitializeBoardPowerManager() {
gpio_config_t gpio_init_struct = {0}; gpio_config_t gpio_init_struct = {0};
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT; gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT;
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_init_struct.pin_bit_mask = (1ull << CODEC_PWR_PIN) | (1ull << SYS_POW_PIN); gpio_init_struct.pin_bit_mask = (1ull << CODEC_PWR_PIN) | (1ull << SYS_POW_PIN);
gpio_config(&gpio_init_struct); gpio_config(&gpio_init_struct);
gpio_set_level(CODEC_PWR_PIN, 1); gpio_set_level(CODEC_PWR_PIN, 1);
gpio_set_level(SYS_POW_PIN, 1); gpio_set_level(SYS_POW_PIN, 1);
gpio_config_t chg_init_struct = {0}; gpio_config_t chg_init_struct = {0};
chg_init_struct.intr_type = GPIO_INTR_DISABLE; chg_init_struct.intr_type = GPIO_INTR_DISABLE;
chg_init_struct.mode = GPIO_MODE_INPUT; chg_init_struct.mode = GPIO_MODE_INPUT;
chg_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; chg_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
chg_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; chg_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
chg_init_struct.pin_bit_mask = 1ull << CHRG_PIN; chg_init_struct.pin_bit_mask = 1ull << CHRG_PIN;
ESP_ERROR_CHECK(gpio_config(&chg_init_struct)); ESP_ERROR_CHECK(gpio_config(&chg_init_struct));
chg_init_struct.mode = GPIO_MODE_OUTPUT; chg_init_struct.mode = GPIO_MODE_OUTPUT;
chg_init_struct.pull_up_en = GPIO_PULLUP_DISABLE; chg_init_struct.pull_up_en = GPIO_PULLUP_DISABLE;
chg_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; chg_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
chg_init_struct.pin_bit_mask = 1ull << CHG_CTRL_PIN; chg_init_struct.pin_bit_mask = 1ull << CHG_CTRL_PIN;
ESP_ERROR_CHECK(gpio_config(&chg_init_struct)); ESP_ERROR_CHECK(gpio_config(&chg_init_struct));
gpio_set_level(CHG_CTRL_PIN, 1); gpio_set_level(CHG_CTRL_PIN, 1);
if (gpio_get_level(CHRG_PIN) == 0) { if (gpio_get_level(CHRG_PIN) == 0) {
power_status_ = kDeviceTypecSupply; power_status_ = kDeviceTypecSupply;
} else { } else {
power_status_ = kDeviceBatterySupply; power_status_ = kDeviceBatterySupply;
} }
esp_timer_create_args_t wake_display_timer_args = { esp_timer_create_args_t wake_display_timer_args = {
.callback = [](void *arg) { .callback = [](void *arg) {
atk_dnesp32s3_box0* self = static_cast<atk_dnesp32s3_box0*>(arg); atk_dnesp32s3_box0* self = static_cast<atk_dnesp32s3_box0*>(arg);
if (self->LcdStatus_ == kDevicelcdbacklightOff && Application::GetInstance().GetDeviceState() == kDeviceStateListening if (self->LcdStatus_ == kDevicelcdbacklightOff && Application::GetInstance().GetDeviceState() == kDeviceStateListening
&& self->wake_status_ == kDeviceWaitWake) { && self->wake_status_ == kDeviceWaitWake) {
if (self->power_sleep_ == kDeviceNeutralSleep) { if (self->power_sleep_ == kDeviceNeutralSleep) {
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
} }
self->GetBacklight()->RestoreBrightness(); self->GetBacklight()->RestoreBrightness();
self->wake_status_ = kDeviceAwakened; self->wake_status_ = kDeviceAwakened;
self->LcdStatus_ = kDevicelcdbacklightOn; self->LcdStatus_ = kDevicelcdbacklightOn;
} else if (self->power_sleep_ == kDeviceNeutralSleep && Application::GetInstance().GetDeviceState() == kDeviceStateListening } else if (self->power_sleep_ == kDeviceNeutralSleep && Application::GetInstance().GetDeviceState() == kDeviceStateListening
&& self->LcdStatus_ != kDevicelcdbacklightOff && self->wake_status_ == kDeviceAwakened) { && self->LcdStatus_ != kDevicelcdbacklightOff && self->wake_status_ == kDeviceAwakened) {
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->power_sleep_ = kDeviceNoSleep; self->power_sleep_ = kDeviceNoSleep;
} else { } else {
self->ticks_ ++; self->ticks_ ++;
if (self->ticks_ % self->kChgCtrlInterval == 0) { if (self->ticks_ % self->kChgCtrlInterval == 0) {
if (gpio_get_level(CHRG_PIN) == 0) { if (gpio_get_level(CHRG_PIN) == 0) {
self->power_status_ = kDeviceTypecSupply; self->power_status_ = kDeviceTypecSupply;
} else { } else {
self->power_status_ = kDeviceBatterySupply; self->power_status_ = kDeviceBatterySupply;
} }
if (self->power_manager_->low_voltage_ < 2877 && self->power_status_ != kDeviceTypecSupply) { if (self->power_manager_->low_voltage_ < 2877 && self->power_status_ != kDeviceTypecSupply) {
esp_timer_stop(self->power_manager_->timer_handle_); esp_timer_stop(self->power_manager_->timer_handle_);
gpio_set_level(CHG_CTRL_PIN, 0); gpio_set_level(CHG_CTRL_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(SYS_POW_PIN, 0); gpio_set_level(SYS_POW_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} }
} }
} }
}, },
.arg = this, .arg = this,
.dispatch_method = ESP_TIMER_TASK, .dispatch_method = ESP_TIMER_TASK,
.name = "wake_update_timer", .name = "wake_update_timer",
.skip_unhandled_events = true, .skip_unhandled_events = true,
}; };
ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_)); ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_));
ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 300000)); ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 300000));
} }
void InitializePowerManager() { void InitializePowerManager() {
power_manager_ = new PowerManager(CHRG_PIN); power_manager_ = new PowerManager(CHRG_PIN);
power_manager_->OnChargingStatusChanged([this](bool is_charging) { power_manager_->OnChargingStatusChanged([this](bool is_charging) {
if (is_charging) { if (is_charging) {
power_save_timer_->SetEnabled(false); power_save_timer_->SetEnabled(false);
} else { } else {
power_save_timer_->SetEnabled(true); power_save_timer_->SetEnabled(true);
} }
}); });
} }
void InitializePowerSaveTimer() { void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300); power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() { power_save_timer_->OnEnterSleepMode([this]() {
power_sleep_ = kDeviceNeutralSleep; power_sleep_ = kDeviceNeutralSleep;
XiaozhiStatus_ = kDevice_join_Sleep; XiaozhiStatus_ = kDevice_join_Sleep;
GetDisplay()->SetPowerSaveMode(true); GetDisplay()->SetPowerSaveMode(true);
if (LcdStatus_ != kDevicelcdbacklightOff) { if (LcdStatus_ != kDevicelcdbacklightOff) {
GetBacklight()->SetBrightness(1); GetBacklight()->SetBrightness(1);
} }
}); });
power_save_timer_->OnExitSleepMode([this]() { power_save_timer_->OnExitSleepMode([this]() {
power_sleep_ = kDeviceNoSleep; power_sleep_ = kDeviceNoSleep;
GetDisplay()->SetPowerSaveMode(false); GetDisplay()->SetPowerSaveMode(false);
if (XiaozhiStatus_ != kDevice_Exit_Sleep) { if (XiaozhiStatus_ != kDevice_Exit_Sleep) {
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
} }
}); });
power_save_timer_->OnShutdownRequest([this]() { power_save_timer_->OnShutdownRequest([this]() {
if (power_status_ == kDeviceBatterySupply) { if (power_status_ == kDeviceBatterySupply) {
esp_timer_stop(power_manager_->timer_handle_); esp_timer_stop(power_manager_->timer_handle_);
gpio_set_level(CHG_CTRL_PIN, 0); gpio_set_level(CHG_CTRL_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(SYS_POW_PIN, 0); gpio_set_level(SYS_POW_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} }
}); });
power_save_timer_->SetEnabled(true); power_save_timer_->SetEnabled(true);
} }
// Initialize I2C peripheral // Initialize I2C peripheral
void InitializeI2c() { void InitializeI2c() {
i2c_master_bus_config_t i2c_bus_cfg = { i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)I2C_NUM_0, .i2c_port = (i2c_port_t)I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT, .clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7, .glitch_ignore_cnt = 7,
.intr_priority = 0, .intr_priority = 0,
.trans_queue_depth = 0, .trans_queue_depth = 0,
.flags = { .flags = {
.enable_internal_pullup = 1, .enable_internal_pullup = 1,
}, },
}; };
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
} }
// Initialize spi peripheral // Initialize spi peripheral
void InitializeSpi() { void InitializeSpi() {
spi_bus_config_t buscfg = {}; spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = LCD_MOSI_PIN; buscfg.mosi_io_num = LCD_MOSI_PIN;
buscfg.miso_io_num = GPIO_NUM_NC; buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = LCD_SCLK_PIN; buscfg.sclk_io_num = LCD_SCLK_PIN;
buscfg.quadwp_io_num = GPIO_NUM_NC; buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC; buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t); buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
} }
void InitializeButtons() { void InitializeButtons() {
middle_button_.OnClick([this]() { middle_button_.OnClick([this]() {
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
if (LcdStatus_ != kDevicelcdbacklightOff) { if (LcdStatus_ != kDevicelcdbacklightOff) {
if (power_sleep_ == kDeviceNeutralSleep) { if (power_sleep_ == kDeviceNeutralSleep) {
power_save_timer_->WakeUp(); power_save_timer_->WakeUp();
power_sleep_ = kDeviceNoSleep; power_sleep_ = kDeviceNoSleep;
} }
app.ToggleChatState(); app.ToggleChatState();
} }
}); });
middle_button_.OnPressUp([this]() { middle_button_.OnPressUp([this]() {
if (LcdStatus_ == kDevicelcdbacklightOff) { if (LcdStatus_ == kDevicelcdbacklightOff) {
Application::GetInstance().StopListening(); Application::GetInstance().StopListening();
Application::GetInstance().SetDeviceState(kDeviceStateIdle); Application::GetInstance().SetDeviceState(kDeviceStateIdle);
wake_status_ = kDeviceWaitWake; wake_status_ = kDeviceWaitWake;
} }
if (XiaozhiStatus_ == kDevice_Distributionnetwork || XiaozhiStatus_ == kDevice_Exit_Sleep) { if (XiaozhiStatus_ == kDevice_Distributionnetwork || XiaozhiStatus_ == kDevice_Exit_Sleep) {
esp_timer_stop(power_manager_->timer_handle_); esp_timer_stop(power_manager_->timer_handle_);
gpio_set_level(CHG_CTRL_PIN, 0); gpio_set_level(CHG_CTRL_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(SYS_POW_PIN, 0); gpio_set_level(SYS_POW_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} else if (XiaozhiStatus_ == kDevice_join_Sleep) { } else if (XiaozhiStatus_ == kDevice_join_Sleep) {
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
XiaozhiStatus_ = kDevice_null; XiaozhiStatus_ = kDevice_null;
} }
}); });
middle_button_.OnLongPress([this]() { middle_button_.OnLongPress([this]() {
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration(); ResetWifiConfiguration();
} }
if (app.GetDeviceState() != kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) { if (app.GetDeviceState() != kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
if (app.GetDeviceState() == kDeviceStateWifiConfiguring && power_status_ != kDeviceTypecSupply) { if (app.GetDeviceState() == kDeviceStateWifiConfiguring && power_status_ != kDeviceTypecSupply) {
GetBacklight()->SetBrightness(0); GetBacklight()->SetBrightness(0);
XiaozhiStatus_ = kDevice_Distributionnetwork; XiaozhiStatus_ = kDevice_Distributionnetwork;
} else if (power_status_ == kDeviceBatterySupply && LcdStatus_ != kDevicelcdbacklightOff) { } else if (power_status_ == kDeviceBatterySupply && LcdStatus_ != kDevicelcdbacklightOff) {
Application::GetInstance().StartListening(); Application::GetInstance().StartListening();
GetBacklight()->SetBrightness(0); GetBacklight()->SetBrightness(0);
XiaozhiStatus_ = kDevice_Exit_Sleep; XiaozhiStatus_ = kDevice_Exit_Sleep;
} else if (power_status_ == kDeviceTypecSupply && LcdStatus_ == kDevicelcdbacklightOn && Application::GetInstance().GetDeviceState() != kDeviceStateStarting) { } else if (power_status_ == kDeviceTypecSupply && LcdStatus_ == kDevicelcdbacklightOn && Application::GetInstance().GetDeviceState() != kDeviceStateStarting) {
Application::GetInstance().StartListening(); Application::GetInstance().StartListening();
GetBacklight()->SetBrightness(0); GetBacklight()->SetBrightness(0);
LcdStatus_ = kDevicelcdbacklightOff; LcdStatus_ = kDevicelcdbacklightOff;
} else if (LcdStatus_ == kDevicelcdbacklightOff && (power_status_ == kDeviceTypecSupply || power_status_ == kDeviceBatterySupply)) { } else if (LcdStatus_ == kDevicelcdbacklightOff && (power_status_ == kDeviceTypecSupply || power_status_ == kDeviceBatterySupply)) {
GetDisplay()->SetChatMessage("system", ""); GetDisplay()->SetChatMessage("system", "");
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
wake_status_ = kDeviceAwakened; wake_status_ = kDeviceAwakened;
LcdStatus_ = kDevicelcdbacklightOn; LcdStatus_ = kDevicelcdbacklightOn;
} }
} }
}); });
left_button_.OnClick([this]() { left_button_.OnClick([this]() {
if (power_sleep_ == kDeviceNeutralSleep && LcdStatus_ != kDevicelcdbacklightOff) { if (power_sleep_ == kDeviceNeutralSleep && LcdStatus_ != kDevicelcdbacklightOff) {
power_save_timer_->WakeUp(); power_save_timer_->WakeUp();
power_sleep_ = kDeviceNoSleep; power_sleep_ = kDeviceNoSleep;
} }
auto codec = GetAudioCodec(); auto codec = GetAudioCodec();
auto volume = codec->output_volume() - 10; auto volume = codec->output_volume() - 10;
if (volume < 0) { if (volume < 0) {
volume = 0; volume = 0;
} }
codec->SetOutputVolume(volume); codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
}); });
left_button_.OnLongPress([this]() { left_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(0); GetAudioCodec()->SetOutputVolume(0);
GetDisplay()->ShowNotification(Lang::Strings::MUTED); GetDisplay()->ShowNotification(Lang::Strings::MUTED);
}); });
right_button_.OnClick([this]() { right_button_.OnClick([this]() {
if (power_sleep_ == kDeviceNeutralSleep && LcdStatus_ != kDevicelcdbacklightOff) { if (power_sleep_ == kDeviceNeutralSleep && LcdStatus_ != kDevicelcdbacklightOff) {
power_save_timer_->WakeUp(); power_save_timer_->WakeUp();
power_sleep_ = kDeviceNoSleep; power_sleep_ = kDeviceNoSleep;
} }
auto codec = GetAudioCodec(); auto codec = GetAudioCodec();
auto volume = codec->output_volume() + 10; auto volume = codec->output_volume() + 10;
if (volume > 100) { if (volume > 100) {
volume = 100; volume = 100;
} }
codec->SetOutputVolume(volume); codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
}); });
right_button_.OnLongPress([this]() { right_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(100); GetAudioCodec()->SetOutputVolume(100);
GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME);
}); });
} }
void InitializeSt7789Display() { void InitializeSt7789Display() {
ESP_LOGI(TAG, "Install panel IO"); ESP_LOGI(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = {}; esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = LCD_CS_PIN; io_config.cs_gpio_num = LCD_CS_PIN;
io_config.dc_gpio_num = LCD_DC_PIN; io_config.dc_gpio_num = LCD_DC_PIN;
io_config.spi_mode = 0; io_config.spi_mode = 0;
io_config.pclk_hz = 80 * 1000 * 1000; io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 7; io_config.trans_queue_depth = 7;
io_config.lcd_cmd_bits = 8; io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8; io_config.lcd_param_bits = 8;
esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io); esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io);
ESP_LOGI(TAG, "Install LCD driver"); ESP_LOGI(TAG, "Install LCD driver");
esp_lcd_panel_dev_config_t panel_config = {}; esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = LCD_RST_PIN; panel_config.reset_gpio_num = LCD_RST_PIN;
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16; panel_config.bits_per_pixel = 16;
panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG, panel_config.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel); esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel);
esp_lcd_panel_reset(panel); esp_lcd_panel_reset(panel);
esp_lcd_panel_invert_color(panel, true); esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_init(panel); esp_lcd_panel_init(panel);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new SpiLcdDisplay(panel_io, panel, display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
} }
public: public:
atk_dnesp32s3_box0() : atk_dnesp32s3_box0() :
right_button_(R_BUTTON_GPIO, false), right_button_(R_BUTTON_GPIO, false),
left_button_(L_BUTTON_GPIO, false), left_button_(L_BUTTON_GPIO, false),
middle_button_(M_BUTTON_GPIO, true) { middle_button_(M_BUTTON_GPIO, true) {
InitializeBoardPowerManager(); InitializeBoardPowerManager();
InitializePowerManager(); InitializePowerManager();
InitializePowerSaveTimer(); InitializePowerSaveTimer();
InitializeI2c(); InitializeI2c();
InitializeSpi(); InitializeSpi();
InitializeSt7789Display(); InitializeSt7789Display();
InitializeButtons(); InitializeButtons();
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
} }
virtual AudioCodec* GetAudioCodec() override { virtual AudioCodec* GetAudioCodec() override {
static Es8311AudioCodec audio_codec( static Es8311AudioCodec audio_codec(
i2c_bus_, i2c_bus_,
I2C_NUM_0, I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE, AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
GPIO_NUM_NC, GPIO_NUM_NC,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN, AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC, GPIO_NUM_NC,
AUDIO_CODEC_ES8311_ADDR, AUDIO_CODEC_ES8311_ADDR,
false); false);
return &audio_codec; return &audio_codec;
} }
virtual Display* GetDisplay() override { virtual Display* GetDisplay() override {
return display_; return display_;
} }
virtual Backlight* GetBacklight() override { virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight; return &backlight;
} }
virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override { virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
static bool last_discharging = false; static bool last_discharging = false;
charging = power_manager_->IsCharging(); charging = power_manager_->IsCharging();
discharging = power_manager_->IsDischarging(); discharging = power_manager_->IsDischarging();
if (discharging != last_discharging) { if (discharging != last_discharging) {
power_save_timer_->SetEnabled(discharging); power_save_timer_->SetEnabled(discharging);
last_discharging = discharging; last_discharging = discharging;
} }
level = power_manager_->GetBatteryLevel(); level = power_manager_->GetBatteryLevel();
return true; return true;
} }
virtual void SetPowerSaveMode(bool enabled) override { virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) { if (!enabled) {
power_save_timer_->WakeUp(); power_save_timer_->WakeUp();
} }
WifiBoard::SetPowerSaveMode(enabled); WifiBoard::SetPowerSaveMode(enabled);
} }
}; };
DECLARE_BOARD(atk_dnesp32s3_box0); DECLARE_BOARD(atk_dnesp32s3_box0);

View File

@@ -1,84 +1,84 @@
#ifndef _BOARD_CONFIG_H_ #ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_ #define _BOARD_CONFIG_H_
#include <driver/gpio.h> #include <driver/gpio.h>
enum XiaozhiStatus { enum XiaozhiStatus {
kDevice_null, kDevice_null,
kDevice_join_Sleep, kDevice_join_Sleep,
kDevice_Exit_Sleep, kDevice_Exit_Sleep,
kDevice_Distributionnetwork, kDevice_Distributionnetwork,
kDevice_Exit_Distributionnetwork, kDevice_Exit_Distributionnetwork,
}; };
enum LcdStatus { enum LcdStatus {
kDevicelcdbacklightOn, kDevicelcdbacklightOn,
kDevicelcdbacklightOff, kDevicelcdbacklightOff,
}; };
enum WakeStatus { enum WakeStatus {
kDeviceAwakened, kDeviceAwakened,
kDeviceWaitWake, kDeviceWaitWake,
kDeviceSleeped, kDeviceSleeped,
}; };
enum PowerSupply { enum PowerSupply {
kDeviceTypecSupply, kDeviceTypecSupply,
kDeviceBatterySupply, kDeviceBatterySupply,
}; };
enum PowerSleep { enum PowerSleep {
kDeviceNoSleep, kDeviceNoSleep,
kDeviceDeepSleep, kDeviceDeepSleep,
kDeviceNeutralSleep, kDeviceNeutralSleep,
}; };
#define SYS_POW_PIN GPIO_NUM_2 #define SYS_POW_PIN GPIO_NUM_2
#define CHG_CTRL_PIN GPIO_NUM_47 #define CHG_CTRL_PIN GPIO_NUM_47
#define CODEC_PWR_PIN GPIO_NUM_14 #define CODEC_PWR_PIN GPIO_NUM_14
#define CHRG_PIN GPIO_NUM_48 #define CHRG_PIN GPIO_NUM_48
#define BAT_VSEN_PIN GPIO_NUM_1 #define BAT_VSEN_PIN GPIO_NUM_1
#define AUDIO_INPUT_SAMPLE_RATE 16000 #define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 16000 #define AUDIO_OUTPUT_SAMPLE_RATE 16000
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_13 #define AUDIO_I2S_GPIO_MCLK GPIO_NUM_13
#define AUDIO_I2S_GPIO_WS GPIO_NUM_10 #define AUDIO_I2S_GPIO_WS GPIO_NUM_10
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5 #define AUDIO_I2S_GPIO_BCLK GPIO_NUM_5
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_6 #define AUDIO_I2S_GPIO_DIN GPIO_NUM_6
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_9 #define AUDIO_I2S_GPIO_DOUT GPIO_NUM_9
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_11 #define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_11
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_12 #define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_12
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR #define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define AUDIO_SPK_GPIO_PIN GPIO_NUM_21 #define AUDIO_SPK_GPIO_PIN GPIO_NUM_21
#define R_BUTTON_GPIO GPIO_NUM_0 #define R_BUTTON_GPIO GPIO_NUM_0
#define M_BUTTON_GPIO GPIO_NUM_4 #define M_BUTTON_GPIO GPIO_NUM_4
#define L_BUTTON_GPIO GPIO_NUM_3 #define L_BUTTON_GPIO GPIO_NUM_3
#define BUILTIN_LED_GPIO GPIO_NUM_13 #define BUILTIN_LED_GPIO GPIO_NUM_13
#define LCD_SCLK_PIN GPIO_NUM_39 #define LCD_SCLK_PIN GPIO_NUM_39
#define LCD_MOSI_PIN GPIO_NUM_40 #define LCD_MOSI_PIN GPIO_NUM_40
#define LCD_MISO_PIN GPIO_NUM_NC #define LCD_MISO_PIN GPIO_NUM_NC
#define LCD_DC_PIN GPIO_NUM_38 #define LCD_DC_PIN GPIO_NUM_38
#define LCD_CS_PIN GPIO_NUM_41 #define LCD_CS_PIN GPIO_NUM_41
#define LCD_RST_PIN GPIO_NUM_NC #define LCD_RST_PIN GPIO_NUM_NC
#define DISPLAY_WIDTH 240 #define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 240 #define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X false #define DISPLAY_MIRROR_X false
#define DISPLAY_MIRROR_Y false #define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY false #define DISPLAY_SWAP_XY false
#define DISPLAY_OFFSET_X 0 #define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0 #define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42 #define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false #define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
#endif // _BOARD_CONFIG_H_ #endif // _BOARD_CONFIG_H_

View File

@@ -1,9 +1,9 @@
{ {
"target": "esp32s3", "target": "esp32s3",
"builds": [ "builds": [
{ {
"name": "atk-dnesp32s3-box0", "name": "atk-dnesp32s3-box0",
"sdkconfig_append": [] "sdkconfig_append": []
} }
] ]
} }

View File

@@ -1,193 +1,193 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include <functional> #include <functional>
#include <esp_timer.h> #include <esp_timer.h>
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_adc/adc_oneshot.h> #include <esp_adc/adc_oneshot.h>
class PowerManager { class PowerManager {
private: private:
std::function<void(bool)> on_charging_status_changed_; std::function<void(bool)> on_charging_status_changed_;
std::function<void(bool)> on_low_battery_status_changed_; std::function<void(bool)> on_low_battery_status_changed_;
gpio_num_t charging_pin_ = GPIO_NUM_NC; gpio_num_t charging_pin_ = GPIO_NUM_NC;
std::vector<uint16_t> adc_values_; std::vector<uint16_t> adc_values_;
uint32_t battery_level_ = 0; uint32_t battery_level_ = 0;
bool is_charging_ = false; bool is_charging_ = false;
bool is_low_battery_ = false; bool is_low_battery_ = false;
int ticks_ = 0; int ticks_ = 0;
const int kBatteryAdcInterval = 60; const int kBatteryAdcInterval = 60;
const int kBatteryAdcDataCount = 3; const int kBatteryAdcDataCount = 3;
const int kLowBatteryLevel = 20; const int kLowBatteryLevel = 20;
adc_oneshot_unit_handle_t adc_handle_; adc_oneshot_unit_handle_t adc_handle_;
void CheckBatteryStatus() { void CheckBatteryStatus() {
// Get charging status // Get charging status
bool new_charging_status = gpio_get_level(charging_pin_) == 0; bool new_charging_status = gpio_get_level(charging_pin_) == 0;
if (new_charging_status != is_charging_) { if (new_charging_status != is_charging_) {
is_charging_ = new_charging_status; is_charging_ = new_charging_status;
if (on_charging_status_changed_) { if (on_charging_status_changed_) {
on_charging_status_changed_(is_charging_); on_charging_status_changed_(is_charging_);
} }
ReadBatteryAdcData(); ReadBatteryAdcData();
return; return;
} }
// 如果电池电量数据不足,则读取电池电量数据 // 如果电池电量数据不足,则读取电池电量数据
if (adc_values_.size() < kBatteryAdcDataCount) { if (adc_values_.size() < kBatteryAdcDataCount) {
ReadBatteryAdcData(); ReadBatteryAdcData();
return; return;
} }
// 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据
ticks_++; ticks_++;
if (ticks_ % kBatteryAdcInterval == 0) { if (ticks_ % kBatteryAdcInterval == 0) {
ReadBatteryAdcData(); ReadBatteryAdcData();
} }
} }
void ReadBatteryAdcData() { void ReadBatteryAdcData() {
int adc_value; int adc_value;
uint32_t temp_val = 0; uint32_t temp_val = 0;
gpio_set_level(CHG_CTRL_PIN, 0); gpio_set_level(CHG_CTRL_PIN, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
for(int t = 0; t < 10; t ++) { for(int t = 0; t < 10; t ++) {
ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_0, &adc_value)); ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_0, &adc_value));
temp_val += adc_value; temp_val += adc_value;
} }
gpio_set_level(CHG_CTRL_PIN, 1); gpio_set_level(CHG_CTRL_PIN, 1);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
adc_value = temp_val / 10; adc_value = temp_val / 10;
// 将 ADC 值添加到队列中 // 将 ADC 值添加到队列中
adc_values_.push_back(adc_value); adc_values_.push_back(adc_value);
if (adc_values_.size() > kBatteryAdcDataCount) { if (adc_values_.size() > kBatteryAdcDataCount) {
adc_values_.erase(adc_values_.begin()); adc_values_.erase(adc_values_.begin());
} }
uint32_t average_adc = 0; uint32_t average_adc = 0;
for (auto value : adc_values_) { for (auto value : adc_values_) {
average_adc += value; average_adc += value;
} }
average_adc /= adc_values_.size(); average_adc /= adc_values_.size();
// 定义电池电量区间 // 定义电池电量区间
const struct { const struct {
uint16_t adc; uint16_t adc;
uint8_t level; uint8_t level;
} levels[] = { } levels[] = {
{2951, 0}, /* 3.80V */ {2951, 0}, /* 3.80V */
{3019, 20}, {3019, 20},
{3037, 40}, {3037, 40},
{3091, 60}, /* 3.88 */ {3091, 60}, /* 3.88 */
{3124, 80}, {3124, 80},
{3231, 100} {3231, 100}
}; };
// 低于最低值时 // 低于最低值时
if (average_adc < levels[0].adc) { if (average_adc < levels[0].adc) {
battery_level_ = 0; battery_level_ = 0;
} }
// 高于最高值时 // 高于最高值时
else if (average_adc >= levels[5].adc) { else if (average_adc >= levels[5].adc) {
battery_level_ = 100; battery_level_ = 100;
} else { } else {
// 线性插值计算中间值 // 线性插值计算中间值
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) { if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) {
float ratio = static_cast<float>(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc); float ratio = static_cast<float>(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc);
battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level);
break; break;
} }
} }
} }
// Check low battery status // Check low battery status
if (adc_values_.size() >= kBatteryAdcDataCount) { if (adc_values_.size() >= kBatteryAdcDataCount) {
bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; bool new_low_battery_status = battery_level_ <= kLowBatteryLevel;
if (new_low_battery_status != is_low_battery_) { if (new_low_battery_status != is_low_battery_) {
is_low_battery_ = new_low_battery_status; is_low_battery_ = new_low_battery_status;
if (on_low_battery_status_changed_) { if (on_low_battery_status_changed_) {
on_low_battery_status_changed_(is_low_battery_); on_low_battery_status_changed_(is_low_battery_);
} }
} }
} }
low_voltage_ = adc_value; low_voltage_ = adc_value;
ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_);
} }
public: public:
esp_timer_handle_t timer_handle_; esp_timer_handle_t timer_handle_;
uint16_t low_voltage_ = 2877; uint16_t low_voltage_ = 2877;
PowerManager(gpio_num_t pin) : charging_pin_(pin) { PowerManager(gpio_num_t pin) : charging_pin_(pin) {
// 创建电池电量检查定时器 // 创建电池电量检查定时器
esp_timer_create_args_t timer_args = { esp_timer_create_args_t timer_args = {
.callback = [](void* arg) { .callback = [](void* arg) {
PowerManager* self = static_cast<PowerManager*>(arg); PowerManager* self = static_cast<PowerManager*>(arg);
self->CheckBatteryStatus(); self->CheckBatteryStatus();
}, },
.arg = this, .arg = this,
.dispatch_method = ESP_TIMER_TASK, .dispatch_method = ESP_TIMER_TASK,
.name = "battery_check_timer", .name = "battery_check_timer",
.skip_unhandled_events = true, .skip_unhandled_events = true,
}; };
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_));
ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000)); ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000));
// 初始化 ADC // 初始化 ADC
adc_oneshot_unit_init_cfg_t init_config = { adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = ADC_UNIT_1, .unit_id = ADC_UNIT_1,
.ulp_mode = ADC_ULP_MODE_DISABLE, .ulp_mode = ADC_ULP_MODE_DISABLE,
}; };
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_));
adc_oneshot_chan_cfg_t chan_config = { adc_oneshot_chan_cfg_t chan_config = {
.atten = ADC_ATTEN_DB_12, .atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12, .bitwidth = ADC_BITWIDTH_12,
}; };
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_0, &chan_config)); ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_0, &chan_config));
} }
~PowerManager() { ~PowerManager() {
if (timer_handle_) { if (timer_handle_) {
esp_timer_stop(timer_handle_); esp_timer_stop(timer_handle_);
esp_timer_delete(timer_handle_); esp_timer_delete(timer_handle_);
} }
if (adc_handle_) { if (adc_handle_) {
adc_oneshot_del_unit(adc_handle_); adc_oneshot_del_unit(adc_handle_);
} }
} }
bool IsCharging() { bool IsCharging() {
// 如果电量已经满了,则不再显示充电中 // 如果电量已经满了,则不再显示充电中
if (battery_level_ == 100) { if (battery_level_ == 100) {
return false; return false;
} }
return is_charging_; return is_charging_;
} }
bool IsDischarging() { bool IsDischarging() {
// 没有区分充电和放电,所以直接返回相反状态 // 没有区分充电和放电,所以直接返回相反状态
return !is_charging_; return !is_charging_;
} }
uint8_t GetBatteryLevel() { uint8_t GetBatteryLevel() {
return battery_level_; return battery_level_;
} }
void OnLowBatteryStatusChanged(std::function<void(bool)> callback) { void OnLowBatteryStatusChanged(std::function<void(bool)> callback) {
on_low_battery_status_changed_ = callback; on_low_battery_status_changed_ = callback;
} }
void OnChargingStatusChanged(std::function<void(bool)> callback) { void OnChargingStatusChanged(std::function<void(bool)> callback) {
on_charging_status_changed_ = callback; on_charging_status_changed_ = callback;
} }
}; };

View File

@@ -1,477 +1,477 @@
#include "dual_network_board.h" #include "dual_network_board.h"
#include "codecs/es8389_audio_codec.h" #include "codecs/es8389_audio_codec.h"
#include "display/lcd_display.h" #include "display/lcd_display.h"
#include "system_reset.h" #include "system_reset.h"
#include "application.h" #include "application.h"
#include "button.h" #include "button.h"
#include "config.h" #include "config.h"
#include "power_save_timer.h" #include "power_save_timer.h"
#include "led/single_led.h" #include "led/single_led.h"
#include "assets/lang_config.h" #include "assets/lang_config.h"
#include "power_manager.h" #include "power_manager.h"
#include "i2c_device.h" #include "i2c_device.h"
#include <esp_log.h> #include <esp_log.h>
#include <esp_lcd_panel_vendor.h> #include <esp_lcd_panel_vendor.h>
#include <wifi_station.h> #include <wifi_station.h>
#include <driver/rtc_io.h> #include <driver/rtc_io.h>
#include <esp_sleep.h> #include <esp_sleep.h>
#include "esp_io_expander_tca95xx_16bit.h" #include "esp_io_expander_tca95xx_16bit.h"
#define TAG "atk_dnesp32s3_box2_4g" #define TAG "atk_dnesp32s3_box2_4g"
class atk_dnesp32s3_box2_4g : public DualNetworkBoard { class atk_dnesp32s3_box2_4g : public DualNetworkBoard {
private: private:
i2c_master_bus_handle_t i2c_bus_; i2c_master_bus_handle_t i2c_bus_;
LcdDisplay* display_; LcdDisplay* display_;
esp_io_expander_handle_t io_exp_handle; esp_io_expander_handle_t io_exp_handle;
button_handle_t btns; button_handle_t btns;
button_driver_t* btn_driver_ = nullptr; button_driver_t* btn_driver_ = nullptr;
static atk_dnesp32s3_box2_4g* instance_; static atk_dnesp32s3_box2_4g* instance_;
PowerSaveTimer* power_save_timer_; PowerSaveTimer* power_save_timer_;
PowerManager* power_manager_; PowerManager* power_manager_;
PowerSupply power_status_; PowerSupply power_status_;
esp_timer_handle_t wake_timer_handle_; esp_timer_handle_t wake_timer_handle_;
esp_lcd_panel_io_handle_t panel_io = nullptr; esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr; esp_lcd_panel_handle_t panel = nullptr;
int ticks_ = 0; int ticks_ = 0;
const int kChgCtrlInterval = 5; const int kChgCtrlInterval = 5;
void InitializeBoardPowerManager() { void InitializeBoardPowerManager() {
instance_ = this; instance_ = this;
if (IoExpanderGetLevel(XIO_CHRG) == 0) { if (IoExpanderGetLevel(XIO_CHRG) == 0) {
power_status_ = kDeviceTypecSupply; power_status_ = kDeviceTypecSupply;
} else { } else {
power_status_ = kDeviceBatterySupply; power_status_ = kDeviceBatterySupply;
} }
esp_timer_create_args_t wake_display_timer_args = { esp_timer_create_args_t wake_display_timer_args = {
.callback = [](void *arg) { .callback = [](void *arg) {
atk_dnesp32s3_box2_4g* self = static_cast<atk_dnesp32s3_box2_4g*>(arg); atk_dnesp32s3_box2_4g* self = static_cast<atk_dnesp32s3_box2_4g*>(arg);
self->ticks_ ++; self->ticks_ ++;
if (self->ticks_ % self->kChgCtrlInterval == 0) { if (self->ticks_ % self->kChgCtrlInterval == 0) {
if (self->IoExpanderGetLevel(XIO_CHRG) == 0) { if (self->IoExpanderGetLevel(XIO_CHRG) == 0) {
self->power_status_ = kDeviceTypecSupply; self->power_status_ = kDeviceTypecSupply;
} else { } else {
self->power_status_ = kDeviceBatterySupply; self->power_status_ = kDeviceBatterySupply;
} }
/* 低于某个电量,会自动关机 */ /* 低于某个电量,会自动关机 */
if (self->power_manager_->low_voltage_ < 2630 && self->power_status_ == kDeviceBatterySupply) { if (self->power_manager_->low_voltage_ < 2630 && self->power_status_ == kDeviceBatterySupply) {
esp_timer_stop(self->power_manager_->timer_handle_); esp_timer_stop(self->power_manager_->timer_handle_);
esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT);
esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_INPUT); esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_INPUT);
esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} }
} }
}, },
.arg = this, .arg = this,
.dispatch_method = ESP_TIMER_TASK, .dispatch_method = ESP_TIMER_TASK,
.name = "wake_update_timer", .name = "wake_update_timer",
.skip_unhandled_events = true, .skip_unhandled_events = true,
}; };
ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_)); ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_));
ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 100000)); ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 100000));
} }
void InitializePowerManager() { void InitializePowerManager() {
power_manager_ = new PowerManager(io_exp_handle); power_manager_ = new PowerManager(io_exp_handle);
power_manager_->OnChargingStatusChanged([this](bool is_charging) { power_manager_->OnChargingStatusChanged([this](bool is_charging) {
if (is_charging) { if (is_charging) {
power_save_timer_->SetEnabled(false); power_save_timer_->SetEnabled(false);
} else { } else {
power_save_timer_->SetEnabled(true); power_save_timer_->SetEnabled(true);
} }
}); });
} }
void InitializePowerSaveTimer() { void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300); power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() { power_save_timer_->OnEnterSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(true); GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1); GetBacklight()->SetBrightness(1);
}); });
power_save_timer_->OnExitSleepMode([this]() { power_save_timer_->OnExitSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(false); GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
}); });
power_save_timer_->OnShutdownRequest([this]() { power_save_timer_->OnShutdownRequest([this]() {
if (power_status_ == kDeviceBatterySupply) { if (power_status_ == kDeviceBatterySupply) {
GetBacklight()->SetBrightness(0); GetBacklight()->SetBrightness(0);
esp_timer_stop(power_manager_->timer_handle_); esp_timer_stop(power_manager_->timer_handle_);
esp_io_expander_set_dir( io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); esp_io_expander_set_dir( io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT);
esp_io_expander_set_level(io_exp_handle, XIO_CHG_CTRL, 0); esp_io_expander_set_level(io_exp_handle, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 0); esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 0);
} }
}); });
power_save_timer_->SetEnabled(true); power_save_timer_->SetEnabled(true);
} }
void audio_volume_change(bool direction) { void audio_volume_change(bool direction) {
auto codec = GetAudioCodec(); auto codec = GetAudioCodec();
auto volume = codec->output_volume(); auto volume = codec->output_volume();
if (direction) { if (direction) {
volume += 10; volume += 10;
if (volume > 100) { if (volume > 100) {
volume = 100; volume = 100;
} }
codec->SetOutputVolume(volume); codec->SetOutputVolume(volume);
} else { } else {
volume -= 10; volume -= 10;
if (volume < 0) { if (volume < 0) {
volume = 0; volume = 0;
} }
codec->SetOutputVolume(volume); codec->SetOutputVolume(volume);
} }
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
} }
void audio_volume_minimum(){ void audio_volume_minimum(){
GetAudioCodec()->SetOutputVolume(0); GetAudioCodec()->SetOutputVolume(0);
GetDisplay()->ShowNotification(Lang::Strings::MUTED); GetDisplay()->ShowNotification(Lang::Strings::MUTED);
} }
void audio_volume_maxmum(){ void audio_volume_maxmum(){
GetAudioCodec()->SetOutputVolume(100); GetAudioCodec()->SetOutputVolume(100);
GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME);
} }
esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) { esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) {
return esp_io_expander_set_level(io_exp_handle, pin_mask, level); return esp_io_expander_set_level(io_exp_handle, pin_mask, level);
} }
uint8_t IoExpanderGetLevel(uint16_t pin_mask) { uint8_t IoExpanderGetLevel(uint16_t pin_mask) {
uint32_t pin_val = 0; uint32_t pin_val = 0;
esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val); esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val);
pin_mask &= DRV_IO_EXP_INPUT_MASK; pin_mask &= DRV_IO_EXP_INPUT_MASK;
return (uint8_t)((pin_val & pin_mask) ? 1 : 0); return (uint8_t)((pin_val & pin_mask) ? 1 : 0);
} }
void InitializeIoExpander() { void InitializeIoExpander() {
esp_err_t ret = ESP_OK; esp_err_t ret = ESP_OK;
esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_exp_handle); esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_exp_handle);
ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, IO_EXPANDER_OUTPUT); ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, IO_EXPANDER_OUTPUT);
ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_INPUT_MASK, IO_EXPANDER_INPUT); ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_INPUT_MASK, IO_EXPANDER_INPUT);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_3V3A, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_3V3A, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_4G, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_4G, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_SPK_EN, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_SPK_EN, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_USB_SEL, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_USB_SEL, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_VBUS_EN, 0); ret |= esp_io_expander_set_level(io_exp_handle, XIO_VBUS_EN, 0);
assert(ret == ESP_OK); assert(ret == ESP_OK);
} }
void InitializeI2c() { void InitializeI2c() {
// Initialize I2C peripheral // Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = { i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)I2C_NUM_0, .i2c_port = (i2c_port_t)I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT, .clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7, .glitch_ignore_cnt = 7,
.intr_priority = 0, .intr_priority = 0,
.trans_queue_depth = 0, .trans_queue_depth = 0,
.flags = { .flags = {
.enable_internal_pullup = 1, .enable_internal_pullup = 1,
}, },
}; };
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
} }
void InitializeButtons() { void InitializeButtons() {
instance_ = this; instance_ = this;
button_config_t l_btn_cfg = { button_config_t l_btn_cfg = {
.long_press_time = 800, .long_press_time = 800,
.short_press_time = 500 .short_press_time = 500
}; };
button_config_t m_btn_cfg = { button_config_t m_btn_cfg = {
.long_press_time = 800, .long_press_time = 800,
.short_press_time = 500 .short_press_time = 500
}; };
button_config_t r_btn_cfg = { button_config_t r_btn_cfg = {
.long_press_time = 800, .long_press_time = 800,
.short_press_time = 500 .short_press_time = 500
}; };
button_driver_t* xio_l_btn_driver_ = nullptr; button_driver_t* xio_l_btn_driver_ = nullptr;
button_driver_t* xio_m_btn_driver_ = nullptr; button_driver_t* xio_m_btn_driver_ = nullptr;
button_handle_t l_btn_handle = NULL; button_handle_t l_btn_handle = NULL;
button_handle_t m_btn_handle = NULL; button_handle_t m_btn_handle = NULL;
button_handle_t r_btn_handle = NULL; button_handle_t r_btn_handle = NULL;
xio_l_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t)); xio_l_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t));
xio_l_btn_driver_->enable_power_save = false; xio_l_btn_driver_->enable_power_save = false;
xio_l_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t { xio_l_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t {
return !instance_->IoExpanderGetLevel(XIO_KEY_L); return !instance_->IoExpanderGetLevel(XIO_KEY_L);
}; };
ESP_ERROR_CHECK(iot_button_create(&l_btn_cfg, xio_l_btn_driver_, &l_btn_handle)); ESP_ERROR_CHECK(iot_button_create(&l_btn_cfg, xio_l_btn_driver_, &l_btn_handle));
xio_m_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t)); xio_m_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t));
xio_m_btn_driver_->enable_power_save = false; xio_m_btn_driver_->enable_power_save = false;
xio_m_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t { xio_m_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t {
return instance_->IoExpanderGetLevel(XIO_KEY_M); return instance_->IoExpanderGetLevel(XIO_KEY_M);
}; };
ESP_ERROR_CHECK(iot_button_create(&m_btn_cfg, xio_m_btn_driver_, &m_btn_handle)); ESP_ERROR_CHECK(iot_button_create(&m_btn_cfg, xio_m_btn_driver_, &m_btn_handle));
button_gpio_config_t r_cfg = { button_gpio_config_t r_cfg = {
.gpio_num = R_BUTTON_GPIO, .gpio_num = R_BUTTON_GPIO,
.active_level = BUTTON_INACTIVE, .active_level = BUTTON_INACTIVE,
.enable_power_save = false, .enable_power_save = false,
.disable_pull = false .disable_pull = false
}; };
ESP_ERROR_CHECK(iot_button_new_gpio_device(&r_btn_cfg, &r_cfg, &r_btn_handle)); ESP_ERROR_CHECK(iot_button_new_gpio_device(&r_btn_cfg, &r_cfg, &r_btn_handle));
iot_button_register_cb(l_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(l_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->audio_volume_change(false); self->audio_volume_change(false);
}, this); }, this);
iot_button_register_cb(l_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(l_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->audio_volume_minimum(); self->audio_volume_minimum();
}, this); }, this);
iot_button_register_cb(m_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(m_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
if (self->GetNetworkType() == NetworkType::WIFI) { if (self->GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) { if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
} }
else { else {
app.ToggleChatState(); app.ToggleChatState();
} }
} else { } else {
app.ToggleChatState(); app.ToggleChatState();
} }
}, this); }, this);
iot_button_register_cb(m_btn_handle, BUTTON_DOUBLE_CLICK, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(m_btn_handle, BUTTON_DOUBLE_CLICK, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) { if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
self->SwitchNetworkType(); self->SwitchNetworkType();
} }
}, this); }, this);
iot_button_register_cb(m_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(m_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data);
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
if (self->GetNetworkType() == NetworkType::WIFI) { if (self->GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
auto& wifi_board = static_cast<WifiBoard&>(self->GetCurrentBoard()); auto& wifi_board = static_cast<WifiBoard&>(self->GetCurrentBoard());
wifi_board.ResetWifiConfiguration(); wifi_board.ResetWifiConfiguration();
} }
} }
if (self->power_status_ == kDeviceBatterySupply) { if (self->power_status_ == kDeviceBatterySupply) {
auto backlight = Board::GetInstance().GetBacklight(); auto backlight = Board::GetInstance().GetBacklight();
backlight->SetBrightness(0); backlight->SetBrightness(0);
esp_timer_stop(self->power_manager_->timer_handle_); esp_timer_stop(self->power_manager_->timer_handle_);
esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT);
esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
esp_io_expander_set_level(self->io_exp_handle, XIO_SYS_POW, 0); esp_io_expander_set_level(self->io_exp_handle, XIO_SYS_POW, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} }
}, this); }, this);
iot_button_register_cb(r_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(r_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->audio_volume_change(true); self->audio_volume_change(true);
}, this); }, this);
iot_button_register_cb(r_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(r_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_4g*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->audio_volume_maxmum(); self->audio_volume_maxmum();
}, this); }, this);
} }
void InitializeSt7789Display() { void InitializeSt7789Display() {
ESP_LOGI(TAG, "Install panel IO"); ESP_LOGI(TAG, "Install panel IO");
/*RD PIN */ /*RD PIN */
gpio_config_t gpio_init_struct; gpio_config_t gpio_init_struct;
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT; gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT;
gpio_init_struct.pin_bit_mask = 1ull << LCD_PIN_RD; gpio_init_struct.pin_bit_mask = 1ull << LCD_PIN_RD;
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&gpio_init_struct); gpio_config(&gpio_init_struct);
gpio_set_level(LCD_PIN_RD, 1); gpio_set_level(LCD_PIN_RD, 1);
/* BL PIN */ /* BL PIN */
gpio_init_struct.pin_bit_mask = 1ull << DISPLAY_BACKLIGHT_PIN; gpio_init_struct.pin_bit_mask = 1ull << DISPLAY_BACKLIGHT_PIN;
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&gpio_init_struct); gpio_config(&gpio_init_struct);
esp_lcd_i80_bus_handle_t i80_bus = NULL; esp_lcd_i80_bus_handle_t i80_bus = NULL;
esp_lcd_i80_bus_config_t bus_config = { esp_lcd_i80_bus_config_t bus_config = {
.dc_gpio_num = LCD_PIN_DC, .dc_gpio_num = LCD_PIN_DC,
.wr_gpio_num = LCD_PIN_WR, .wr_gpio_num = LCD_PIN_WR,
.clk_src = LCD_CLK_SRC_DEFAULT, .clk_src = LCD_CLK_SRC_DEFAULT,
.data_gpio_nums = { .data_gpio_nums = {
LCD_PIN_D0, LCD_PIN_D0,
LCD_PIN_D1, LCD_PIN_D1,
LCD_PIN_D2, LCD_PIN_D2,
LCD_PIN_D3, LCD_PIN_D3,
LCD_PIN_D4, LCD_PIN_D4,
LCD_PIN_D5, LCD_PIN_D5,
LCD_PIN_D6, LCD_PIN_D6,
LCD_PIN_D7, LCD_PIN_D7,
}, },
.bus_width = 8, .bus_width = 8,
.max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t), .max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t),
.psram_trans_align = 64, .psram_trans_align = 64,
.sram_trans_align = 4, .sram_trans_align = 4,
}; };
ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
esp_lcd_panel_io_i80_config_t io_config = { esp_lcd_panel_io_i80_config_t io_config = {
.cs_gpio_num = LCD_PIN_CS, .cs_gpio_num = LCD_PIN_CS,
.pclk_hz = (20 * 1000 * 1000), .pclk_hz = (20 * 1000 * 1000),
.trans_queue_depth = 7, .trans_queue_depth = 7,
.on_color_trans_done = nullptr, .on_color_trans_done = nullptr,
.user_ctx = nullptr, .user_ctx = nullptr,
.lcd_cmd_bits = 8, .lcd_cmd_bits = 8,
.lcd_param_bits = 8, .lcd_param_bits = 8,
.dc_levels = { .dc_levels = {
.dc_idle_level = 1, .dc_idle_level = 1,
.dc_cmd_level = 0, .dc_cmd_level = 0,
.dc_dummy_level = 0, .dc_dummy_level = 0,
.dc_data_level = 1, .dc_data_level = 1,
}, },
.flags = { .flags = {
.cs_active_high = 0, .cs_active_high = 0,
.pclk_active_neg = 0, .pclk_active_neg = 0,
.pclk_idle_low = 0, .pclk_idle_low = 0,
}, },
}; };
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io)); ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io));
esp_lcd_panel_dev_config_t panel_config = { esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = LCD_PIN_RST, .reset_gpio_num = LCD_PIN_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16, .bits_per_pixel = 16,
}; };
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel); esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel); esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true); esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_set_gap(panel, 0, 0); esp_lcd_panel_set_gap(panel, 0, 0);
esp_lcd_panel_io_tx_param(panel_io, 0xCF, (uint8_t[]) {0x00,0x83,0x30}, 3); esp_lcd_panel_io_tx_param(panel_io, 0xCF, (uint8_t[]) {0x00,0x83,0x30}, 3);
esp_lcd_panel_io_tx_param(panel_io, 0xED, (uint8_t[]) {0x64,0x03,0x12,0x81}, 4); esp_lcd_panel_io_tx_param(panel_io, 0xED, (uint8_t[]) {0x64,0x03,0x12,0x81}, 4);
esp_lcd_panel_io_tx_param(panel_io, 0xE8, (uint8_t[]) {0x85,0x01,0x79}, 3); esp_lcd_panel_io_tx_param(panel_io, 0xE8, (uint8_t[]) {0x85,0x01,0x79}, 3);
esp_lcd_panel_io_tx_param(panel_io, 0xCB, (uint8_t[]) {0x39,0x2C,0x00,0x34,0x02}, 5); esp_lcd_panel_io_tx_param(panel_io, 0xCB, (uint8_t[]) {0x39,0x2C,0x00,0x34,0x02}, 5);
esp_lcd_panel_io_tx_param(panel_io, 0xF7, (uint8_t[]) {0x20}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xF7, (uint8_t[]) {0x20}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xEA, (uint8_t[]) {0x00,0x00}, 2); esp_lcd_panel_io_tx_param(panel_io, 0xEA, (uint8_t[]) {0x00,0x00}, 2);
esp_lcd_panel_io_tx_param(panel_io, 0xbb, (uint8_t[]) {0x20}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xbb, (uint8_t[]) {0x20}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xc3, (uint8_t[]) {0x00}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xc3, (uint8_t[]) {0x00}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xC4, (uint8_t[]) {0x20}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xC4, (uint8_t[]) {0x20}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xC5, (uint8_t[]) {0x20}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xC5, (uint8_t[]) {0x20}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xC6, (uint8_t[]) {0x10}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xC6, (uint8_t[]) {0x10}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xC7, (uint8_t[]) {0xB0}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xC7, (uint8_t[]) {0xB0}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0x36, (uint8_t[]) {0x60}, 1); esp_lcd_panel_io_tx_param(panel_io, 0x36, (uint8_t[]) {0x60}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0x3A, (uint8_t[]) {0x55}, 1); esp_lcd_panel_io_tx_param(panel_io, 0x3A, (uint8_t[]) {0x55}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xB1, (uint8_t[]) {0x00,0x1B}, 2); esp_lcd_panel_io_tx_param(panel_io, 0xB1, (uint8_t[]) {0x00,0x1B}, 2);
esp_lcd_panel_io_tx_param(panel_io, 0xF2, (uint8_t[]) {0x08}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xF2, (uint8_t[]) {0x08}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0x26, (uint8_t[]) {0x01}, 1); esp_lcd_panel_io_tx_param(panel_io, 0x26, (uint8_t[]) {0x01}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xE0, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x32,0x44,0x42,0x06,0x0E,0x12,0x14,0x17}, 14); esp_lcd_panel_io_tx_param(panel_io, 0xE0, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x32,0x44,0x42,0x06,0x0E,0x12,0x14,0x17}, 14);
esp_lcd_panel_io_tx_param(panel_io, 0xE1, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x31,0x54,0x47,0x0E,0x1C,0x17,0x1B,0x1E}, 14); esp_lcd_panel_io_tx_param(panel_io, 0xE1, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x31,0x54,0x47,0x0E,0x1C,0x17,0x1B,0x1E}, 14);
esp_lcd_panel_io_tx_param(panel_io, 0xB7, (uint8_t[]) {0x07}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xB7, (uint8_t[]) {0x07}, 1);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new SpiLcdDisplay(panel_io, panel, display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
} }
public: public:
atk_dnesp32s3_box2_4g() : atk_dnesp32s3_box2_4g() :
DualNetworkBoard(Module_4G_TX_PIN, Module_4G_RX_PIN) { DualNetworkBoard(Module_4G_TX_PIN, Module_4G_RX_PIN) {
InitializeI2c(); InitializeI2c();
InitializeIoExpander(); InitializeIoExpander();
InitializePowerSaveTimer(); InitializePowerSaveTimer();
InitializePowerManager(); InitializePowerManager();
InitializeSt7789Display(); InitializeSt7789Display();
InitializeButtons(); InitializeButtons();
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
InitializeBoardPowerManager(); InitializeBoardPowerManager();
} }
virtual AudioCodec* GetAudioCodec() override { virtual AudioCodec* GetAudioCodec() override {
static Es8389AudioCodec audio_codec( static Es8389AudioCodec audio_codec(
i2c_bus_, i2c_bus_,
I2C_NUM_0, I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE, AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN, AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC, GPIO_NUM_NC,
AUDIO_CODEC_ES8389_ADDR AUDIO_CODEC_ES8389_ADDR
); );
return &audio_codec; return &audio_codec;
} }
virtual Display* GetDisplay() override { virtual Display* GetDisplay() override {
return display_; return display_;
} }
virtual Backlight* GetBacklight() override { virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight; return &backlight;
} }
virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override { virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
static bool last_discharging = false; static bool last_discharging = false;
charging = power_manager_->IsCharging(); charging = power_manager_->IsCharging();
discharging = power_manager_->IsDischarging(); discharging = power_manager_->IsDischarging();
if (discharging != last_discharging) { if (discharging != last_discharging) {
power_save_timer_->SetEnabled(discharging); power_save_timer_->SetEnabled(discharging);
last_discharging = discharging; last_discharging = discharging;
} }
level = power_manager_->GetBatteryLevel(); level = power_manager_->GetBatteryLevel();
return true; return true;
} }
virtual void SetPowerSaveMode(bool enabled) override { virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) { if (!enabled) {
power_save_timer_->WakeUp(); power_save_timer_->WakeUp();
} }
DualNetworkBoard::SetPowerSaveMode(enabled); DualNetworkBoard::SetPowerSaveMode(enabled);
} }
}; };
DECLARE_BOARD(atk_dnesp32s3_box2_4g); DECLARE_BOARD(atk_dnesp32s3_box2_4g);
// 定义静态成员变量 // 定义静态成员变量
atk_dnesp32s3_box2_4g* atk_dnesp32s3_box2_4g::instance_ = nullptr; atk_dnesp32s3_box2_4g* atk_dnesp32s3_box2_4g::instance_ = nullptr;

View File

@@ -1,80 +1,80 @@
#ifndef _BOARD_CONFIG_H_ #ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_ #define _BOARD_CONFIG_H_
#include <driver/gpio.h> #include <driver/gpio.h>
enum PowerSupply { enum PowerSupply {
kDeviceTypecSupply, kDeviceTypecSupply,
kDeviceBatterySupply, kDeviceBatterySupply,
}; };
#define AUDIO_INPUT_SAMPLE_RATE 16000 #define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 16000 #define AUDIO_OUTPUT_SAMPLE_RATE 16000
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38 #define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38
#define AUDIO_I2S_GPIO_WS GPIO_NUM_42 #define AUDIO_I2S_GPIO_WS GPIO_NUM_42
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_40 #define AUDIO_I2S_GPIO_BCLK GPIO_NUM_40
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39 #define AUDIO_I2S_GPIO_DIN GPIO_NUM_39
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_41 #define AUDIO_I2S_GPIO_DOUT GPIO_NUM_41
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_48 #define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_48
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_47 #define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_47
#define AUDIO_CODEC_ES8389_ADDR ES8389_CODEC_DEFAULT_ADDR #define AUDIO_CODEC_ES8389_ADDR ES8389_CODEC_DEFAULT_ADDR
#define SPISD_PIN_MOSI GPIO_NUM_16 #define SPISD_PIN_MOSI GPIO_NUM_16
#define SPISD_PIN_MISO GPIO_NUM_18 #define SPISD_PIN_MISO GPIO_NUM_18
#define SPISD_PIN_CLK GPIO_NUM_17 #define SPISD_PIN_CLK GPIO_NUM_17
#define SPISD_PIN_TS GPIO_NUM_15 #define SPISD_PIN_TS GPIO_NUM_15
#define R_BUTTON_GPIO GPIO_NUM_0 #define R_BUTTON_GPIO GPIO_NUM_0
#define XL9555_INT_GPIO GPIO_NUM_2 #define XL9555_INT_GPIO GPIO_NUM_2
#define XIO_IO_SBU2 (IO_EXPANDER_PIN_NUM_3) #define XIO_IO_SBU2 (IO_EXPANDER_PIN_NUM_3)
#define XIO_IO_SBU1 (IO_EXPANDER_PIN_NUM_4) #define XIO_IO_SBU1 (IO_EXPANDER_PIN_NUM_4)
#define XIO_KEY_L (IO_EXPANDER_PIN_NUM_5) #define XIO_KEY_L (IO_EXPANDER_PIN_NUM_5)
#define XIO_KEY_Q (IO_EXPANDER_PIN_NUM_6) #define XIO_KEY_Q (IO_EXPANDER_PIN_NUM_6)
#define XIO_KEY_M (IO_EXPANDER_PIN_NUM_7) #define XIO_KEY_M (IO_EXPANDER_PIN_NUM_7)
#define XIO_USB_SEL (IO_EXPANDER_PIN_NUM_8) #define XIO_USB_SEL (IO_EXPANDER_PIN_NUM_8)
#define XIO_SPK_EN (IO_EXPANDER_PIN_NUM_9) #define XIO_SPK_EN (IO_EXPANDER_PIN_NUM_9)
#define XIO_SYS_POW (IO_EXPANDER_PIN_NUM_10) #define XIO_SYS_POW (IO_EXPANDER_PIN_NUM_10)
#define XIO_VBUS_EN (IO_EXPANDER_PIN_NUM_11) #define XIO_VBUS_EN (IO_EXPANDER_PIN_NUM_11)
#define XIO_EN_4G (IO_EXPANDER_PIN_NUM_12) #define XIO_EN_4G (IO_EXPANDER_PIN_NUM_12)
#define XIO_EN_3V3A (IO_EXPANDER_PIN_NUM_13) #define XIO_EN_3V3A (IO_EXPANDER_PIN_NUM_13)
#define XIO_CHG_CTRL (IO_EXPANDER_PIN_NUM_14) #define XIO_CHG_CTRL (IO_EXPANDER_PIN_NUM_14)
#define XIO_CHRG (IO_EXPANDER_PIN_NUM_15) #define XIO_CHRG (IO_EXPANDER_PIN_NUM_15)
#define DRV_IO_EXP_OUTPUT_MASK 0x3F18 #define DRV_IO_EXP_OUTPUT_MASK 0x3F18
#define DRV_IO_EXP_INPUT_MASK 0xC0E7 #define DRV_IO_EXP_INPUT_MASK 0xC0E7
#define LCD_PIN_CS GPIO_NUM_14 #define LCD_PIN_CS GPIO_NUM_14
#define LCD_PIN_DC GPIO_NUM_12 #define LCD_PIN_DC GPIO_NUM_12
#define LCD_PIN_RD GPIO_NUM_10 #define LCD_PIN_RD GPIO_NUM_10
#define LCD_PIN_WR GPIO_NUM_11 #define LCD_PIN_WR GPIO_NUM_11
#define LCD_PIN_RST GPIO_NUM_NC #define LCD_PIN_RST GPIO_NUM_NC
#define LCD_PIN_D0 GPIO_NUM_13 #define LCD_PIN_D0 GPIO_NUM_13
#define LCD_PIN_D1 GPIO_NUM_9 #define LCD_PIN_D1 GPIO_NUM_9
#define LCD_PIN_D2 GPIO_NUM_8 #define LCD_PIN_D2 GPIO_NUM_8
#define LCD_PIN_D3 GPIO_NUM_7 #define LCD_PIN_D3 GPIO_NUM_7
#define LCD_PIN_D4 GPIO_NUM_6 #define LCD_PIN_D4 GPIO_NUM_6
#define LCD_PIN_D5 GPIO_NUM_5 #define LCD_PIN_D5 GPIO_NUM_5
#define LCD_PIN_D6 GPIO_NUM_4 #define LCD_PIN_D6 GPIO_NUM_4
#define LCD_PIN_D7 GPIO_NUM_3 #define LCD_PIN_D7 GPIO_NUM_3
#define DISPLAY_WIDTH 240 #define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 320 #define DISPLAY_HEIGHT 320
#define DISPLAY_MIRROR_X false #define DISPLAY_MIRROR_X false
#define DISPLAY_MIRROR_Y false #define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY false #define DISPLAY_SWAP_XY false
#define DISPLAY_OFFSET_X 0 #define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0 #define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_21 #define DISPLAY_BACKLIGHT_PIN GPIO_NUM_21
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false #define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
#define Module_4G_RX_PIN GPIO_NUM_44 #define Module_4G_RX_PIN GPIO_NUM_44
#define Module_4G_TX_PIN GPIO_NUM_43 #define Module_4G_TX_PIN GPIO_NUM_43
#endif // _BOARD_CONFIG_H_ #endif // _BOARD_CONFIG_H_

View File

@@ -1,9 +1,9 @@
{ {
"target": "esp32s3", "target": "esp32s3",
"builds": [ "builds": [
{ {
"name": "atk-dnesp32s3-box2-4g", "name": "atk-dnesp32s3-box2-4g",
"sdkconfig_append": [] "sdkconfig_append": []
} }
] ]
} }

View File

@@ -1,195 +1,195 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include <functional> #include <functional>
#include "esp_io_expander_tca95xx_16bit.h" #include "esp_io_expander_tca95xx_16bit.h"
#include <esp_timer.h> #include <esp_timer.h>
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_adc/adc_oneshot.h> #include <esp_adc/adc_oneshot.h>
class PowerManager { class PowerManager {
private: private:
std::function<void(bool)> on_charging_status_changed_; std::function<void(bool)> on_charging_status_changed_;
std::function<void(bool)> on_low_battery_status_changed_; std::function<void(bool)> on_low_battery_status_changed_;
esp_io_expander_handle_t xl9555_; esp_io_expander_handle_t xl9555_;
uint32_t pin_val = 0; uint32_t pin_val = 0;
gpio_num_t charging_pin_ = GPIO_NUM_NC; gpio_num_t charging_pin_ = GPIO_NUM_NC;
std::vector<uint16_t> adc_values_; std::vector<uint16_t> adc_values_;
uint32_t battery_level_ = 0; uint32_t battery_level_ = 0;
bool is_charging_ = false; bool is_charging_ = false;
bool is_low_battery_ = false; bool is_low_battery_ = false;
int ticks_ = 0; int ticks_ = 0;
const int kBatteryAdcInterval = 60; const int kBatteryAdcInterval = 60;
const int kBatteryAdcDataCount = 3; const int kBatteryAdcDataCount = 3;
const int kLowBatteryLevel = 20; const int kLowBatteryLevel = 20;
adc_oneshot_unit_handle_t adc_handle_; adc_oneshot_unit_handle_t adc_handle_;
void CheckBatteryStatus() { void CheckBatteryStatus() {
// Get charging status // Get charging status
esp_io_expander_get_level(xl9555_, DRV_IO_EXP_INPUT_MASK, &pin_val); esp_io_expander_get_level(xl9555_, DRV_IO_EXP_INPUT_MASK, &pin_val);
bool new_charging_status = ((uint8_t)((pin_val & XIO_CHRG) ? 1 : 0)) == 0; bool new_charging_status = ((uint8_t)((pin_val & XIO_CHRG) ? 1 : 0)) == 0;
if (new_charging_status != is_charging_) { if (new_charging_status != is_charging_) {
is_charging_ = new_charging_status; is_charging_ = new_charging_status;
if (on_charging_status_changed_) { if (on_charging_status_changed_) {
on_charging_status_changed_(is_charging_); on_charging_status_changed_(is_charging_);
} }
ReadBatteryAdcData(); ReadBatteryAdcData();
return; return;
} }
// 如果电池电量数据不足,则读取电池电量数据 // 如果电池电量数据不足,则读取电池电量数据
if (adc_values_.size() < kBatteryAdcDataCount) { if (adc_values_.size() < kBatteryAdcDataCount) {
ReadBatteryAdcData(); ReadBatteryAdcData();
return; return;
} }
// 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据 // 如果电池电量数据充足,则每 kBatteryAdcInterval 个 tick 读取一次电池电量数据
ticks_++; ticks_++;
if (ticks_ % kBatteryAdcInterval == 0) { if (ticks_ % kBatteryAdcInterval == 0) {
ReadBatteryAdcData(); ReadBatteryAdcData();
} }
} }
void ReadBatteryAdcData() { void ReadBatteryAdcData() {
int adc_value; int adc_value;
uint32_t temp_val = 0; uint32_t temp_val = 0;
esp_io_expander_set_dir(xl9555_, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); esp_io_expander_set_dir(xl9555_, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT);
esp_io_expander_set_level(xl9555_, XIO_CHG_CTRL, 0); esp_io_expander_set_level(xl9555_, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(500)); vTaskDelay(pdMS_TO_TICKS(500));
for(int t = 0; t < 10; t ++) { for(int t = 0; t < 10; t ++) {
ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_0, &adc_value)); ESP_ERROR_CHECK(adc_oneshot_read(adc_handle_, ADC_CHANNEL_0, &adc_value));
temp_val += adc_value; temp_val += adc_value;
} }
esp_io_expander_set_dir(xl9555_, XIO_CHG_CTRL, IO_EXPANDER_INPUT); esp_io_expander_set_dir(xl9555_, XIO_CHG_CTRL, IO_EXPANDER_INPUT);
adc_value = temp_val / 10; adc_value = temp_val / 10;
// 将 ADC 值添加到队列中 // 将 ADC 值添加到队列中
adc_values_.push_back(adc_value); adc_values_.push_back(adc_value);
if (adc_values_.size() > kBatteryAdcDataCount) { if (adc_values_.size() > kBatteryAdcDataCount) {
adc_values_.erase(adc_values_.begin()); adc_values_.erase(adc_values_.begin());
} }
uint32_t average_adc = 0; uint32_t average_adc = 0;
for (auto value : adc_values_) { for (auto value : adc_values_) {
average_adc += value; average_adc += value;
} }
average_adc /= adc_values_.size(); average_adc /= adc_values_.size();
// 定义电池电量区间 // 定义电池电量区间
const struct { const struct {
uint16_t adc; uint16_t adc;
uint8_t level; uint8_t level;
} levels[] = { } levels[] = {
{2696, 0}, /* 3.48V -屏幕闪屏 */ {2696, 0}, /* 3.48V -屏幕闪屏 */
{2724, 20}, /* 3.53V */ {2724, 20}, /* 3.53V */
{2861, 40}, /* 3.7V */ {2861, 40}, /* 3.7V */
{3038, 60}, /* 3.90V */ {3038, 60}, /* 3.90V */
{3150, 80}, /* 4.02V */ {3150, 80}, /* 4.02V */
{3280, 100} /* 4.14V */ {3280, 100} /* 4.14V */
}; };
// 低于最低值时 // 低于最低值时
if (average_adc < levels[0].adc) { if (average_adc < levels[0].adc) {
battery_level_ = 0; battery_level_ = 0;
} }
// 高于最高值时 // 高于最高值时
else if (average_adc >= levels[5].adc) { else if (average_adc >= levels[5].adc) {
battery_level_ = 100; battery_level_ = 100;
} else { } else {
// 线性插值计算中间值 // 线性插值计算中间值
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) { if (average_adc >= levels[i].adc && average_adc < levels[i+1].adc) {
float ratio = static_cast<float>(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc); float ratio = static_cast<float>(average_adc - levels[i].adc) / (levels[i+1].adc - levels[i].adc);
battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level); battery_level_ = levels[i].level + ratio * (levels[i+1].level - levels[i].level);
break; break;
} }
} }
} }
// Check low battery status // Check low battery status
if (adc_values_.size() >= kBatteryAdcDataCount) { if (adc_values_.size() >= kBatteryAdcDataCount) {
bool new_low_battery_status = battery_level_ <= kLowBatteryLevel; bool new_low_battery_status = battery_level_ <= kLowBatteryLevel;
if (new_low_battery_status != is_low_battery_) { if (new_low_battery_status != is_low_battery_) {
is_low_battery_ = new_low_battery_status; is_low_battery_ = new_low_battery_status;
if (on_low_battery_status_changed_) { if (on_low_battery_status_changed_) {
on_low_battery_status_changed_(is_low_battery_); on_low_battery_status_changed_(is_low_battery_);
} }
} }
} }
low_voltage_ = adc_value; low_voltage_ = adc_value;
// ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_); // ESP_LOGI("PowerManager", "ADC value: %d average: %ld level: %ld", adc_value, average_adc, battery_level_);
} }
public: public:
esp_timer_handle_t timer_handle_; esp_timer_handle_t timer_handle_;
uint16_t low_voltage_ = 2630; uint16_t low_voltage_ = 2630;
PowerManager(esp_io_expander_handle_t xl9555) : xl9555_(xl9555) { PowerManager(esp_io_expander_handle_t xl9555) : xl9555_(xl9555) {
// 创建电池电量检查定时器 // 创建电池电量检查定时器
esp_timer_create_args_t timer_args = { esp_timer_create_args_t timer_args = {
.callback = [](void* arg) { .callback = [](void* arg) {
PowerManager* self = static_cast<PowerManager*>(arg); PowerManager* self = static_cast<PowerManager*>(arg);
self->CheckBatteryStatus(); self->CheckBatteryStatus();
}, },
.arg = this, .arg = this,
.dispatch_method = ESP_TIMER_TASK, .dispatch_method = ESP_TIMER_TASK,
.name = "battery_check_timer", .name = "battery_check_timer",
.skip_unhandled_events = true, .skip_unhandled_events = true,
}; };
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_)); ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle_));
ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000)); ESP_ERROR_CHECK(esp_timer_start_periodic(timer_handle_, 1000000));
// 初始化 ADC // 初始化 ADC
adc_oneshot_unit_init_cfg_t init_config = { adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = ADC_UNIT_1, .unit_id = ADC_UNIT_1,
.ulp_mode = ADC_ULP_MODE_DISABLE, .ulp_mode = ADC_ULP_MODE_DISABLE,
}; };
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_)); ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle_));
adc_oneshot_chan_cfg_t chan_config = { adc_oneshot_chan_cfg_t chan_config = {
.atten = ADC_ATTEN_DB_12, .atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12, .bitwidth = ADC_BITWIDTH_12,
}; };
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_0, &chan_config)); ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle_, ADC_CHANNEL_0, &chan_config));
} }
~PowerManager() { ~PowerManager() {
if (timer_handle_) { if (timer_handle_) {
esp_timer_stop(timer_handle_); esp_timer_stop(timer_handle_);
esp_timer_delete(timer_handle_); esp_timer_delete(timer_handle_);
} }
if (adc_handle_) { if (adc_handle_) {
adc_oneshot_del_unit(adc_handle_); adc_oneshot_del_unit(adc_handle_);
} }
} }
bool IsCharging() { bool IsCharging() {
// 如果电量已经满了,则不再显示充电中 // 如果电量已经满了,则不再显示充电中
if (battery_level_ == 100) { if (battery_level_ == 100) {
return false; return false;
} }
return is_charging_; return is_charging_;
} }
bool IsDischarging() { bool IsDischarging() {
// 没有区分充电和放电,所以直接返回相反状态 // 没有区分充电和放电,所以直接返回相反状态
return !is_charging_; return !is_charging_;
} }
uint8_t GetBatteryLevel() { uint8_t GetBatteryLevel() {
return battery_level_; return battery_level_;
} }
void OnLowBatteryStatusChanged(std::function<void(bool)> callback) { void OnLowBatteryStatusChanged(std::function<void(bool)> callback) {
on_low_battery_status_changed_ = callback; on_low_battery_status_changed_ = callback;
} }
void OnChargingStatusChanged(std::function<void(bool)> callback) { void OnChargingStatusChanged(std::function<void(bool)> callback) {
on_charging_status_changed_ = callback; on_charging_status_changed_ = callback;
} }
}; };

View File

@@ -1,456 +1,456 @@
#include "wifi_board.h" #include "wifi_board.h"
#include "codecs/es8389_audio_codec.h" #include "codecs/es8389_audio_codec.h"
#include "display/lcd_display.h" #include "display/lcd_display.h"
#include "system_reset.h" #include "system_reset.h"
#include "application.h" #include "application.h"
#include "button.h" #include "button.h"
#include "config.h" #include "config.h"
#include "power_save_timer.h" #include "power_save_timer.h"
#include "led/single_led.h" #include "led/single_led.h"
#include "assets/lang_config.h" #include "assets/lang_config.h"
#include "power_manager.h" #include "power_manager.h"
#include "i2c_device.h" #include "i2c_device.h"
#include <esp_log.h> #include <esp_log.h>
#include <esp_lcd_panel_vendor.h> #include <esp_lcd_panel_vendor.h>
#include <wifi_station.h> #include <wifi_station.h>
#include <driver/rtc_io.h> #include <driver/rtc_io.h>
#include <esp_sleep.h> #include <esp_sleep.h>
#include "esp_io_expander_tca95xx_16bit.h" #include "esp_io_expander_tca95xx_16bit.h"
#define TAG "atk_dnesp32s3_box2_wifi" #define TAG "atk_dnesp32s3_box2_wifi"
class atk_dnesp32s3_box2_wifi : public WifiBoard { class atk_dnesp32s3_box2_wifi : public WifiBoard {
private: private:
i2c_master_bus_handle_t i2c_bus_; i2c_master_bus_handle_t i2c_bus_;
LcdDisplay* display_; LcdDisplay* display_;
esp_io_expander_handle_t io_exp_handle; esp_io_expander_handle_t io_exp_handle;
button_handle_t btns; button_handle_t btns;
button_driver_t* btn_driver_ = nullptr; button_driver_t* btn_driver_ = nullptr;
static atk_dnesp32s3_box2_wifi* instance_; static atk_dnesp32s3_box2_wifi* instance_;
PowerSaveTimer* power_save_timer_; PowerSaveTimer* power_save_timer_;
PowerManager* power_manager_; PowerManager* power_manager_;
PowerSupply power_status_; PowerSupply power_status_;
esp_timer_handle_t wake_timer_handle_; esp_timer_handle_t wake_timer_handle_;
esp_lcd_panel_io_handle_t panel_io = nullptr; esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr; esp_lcd_panel_handle_t panel = nullptr;
int ticks_ = 0; int ticks_ = 0;
const int kChgCtrlInterval = 5; const int kChgCtrlInterval = 5;
void InitializeBoardPowerManager() { void InitializeBoardPowerManager() {
instance_ = this; instance_ = this;
if (IoExpanderGetLevel(XIO_CHRG) == 0) { if (IoExpanderGetLevel(XIO_CHRG) == 0) {
power_status_ = kDeviceTypecSupply; power_status_ = kDeviceTypecSupply;
} else { } else {
power_status_ = kDeviceBatterySupply; power_status_ = kDeviceBatterySupply;
} }
esp_timer_create_args_t wake_display_timer_args = { esp_timer_create_args_t wake_display_timer_args = {
.callback = [](void *arg) { .callback = [](void *arg) {
atk_dnesp32s3_box2_wifi* self = static_cast<atk_dnesp32s3_box2_wifi*>(arg); atk_dnesp32s3_box2_wifi* self = static_cast<atk_dnesp32s3_box2_wifi*>(arg);
self->ticks_ ++; self->ticks_ ++;
if (self->ticks_ % self->kChgCtrlInterval == 0) { if (self->ticks_ % self->kChgCtrlInterval == 0) {
if (self->IoExpanderGetLevel(XIO_CHRG) == 0) { if (self->IoExpanderGetLevel(XIO_CHRG) == 0) {
self->power_status_ = kDeviceTypecSupply; self->power_status_ = kDeviceTypecSupply;
} else { } else {
self->power_status_ = kDeviceBatterySupply; self->power_status_ = kDeviceBatterySupply;
} }
/* 低于某个电量,会自动关机 */ /* 低于某个电量,会自动关机 */
if (self->power_manager_->low_voltage_ < 2630 && self->power_status_ == kDeviceBatterySupply) { if (self->power_manager_->low_voltage_ < 2630 && self->power_status_ == kDeviceBatterySupply) {
esp_timer_stop(self->power_manager_->timer_handle_); esp_timer_stop(self->power_manager_->timer_handle_);
esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT);
esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_INPUT); esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_INPUT);
esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} }
} }
}, },
.arg = this, .arg = this,
.dispatch_method = ESP_TIMER_TASK, .dispatch_method = ESP_TIMER_TASK,
.name = "wake_update_timer", .name = "wake_update_timer",
.skip_unhandled_events = true, .skip_unhandled_events = true,
}; };
ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_)); ESP_ERROR_CHECK(esp_timer_create(&wake_display_timer_args, &wake_timer_handle_));
ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 100000)); ESP_ERROR_CHECK(esp_timer_start_periodic(wake_timer_handle_, 100000));
} }
void InitializePowerManager() { void InitializePowerManager() {
power_manager_ = new PowerManager(io_exp_handle); power_manager_ = new PowerManager(io_exp_handle);
power_manager_->OnChargingStatusChanged([this](bool is_charging) { power_manager_->OnChargingStatusChanged([this](bool is_charging) {
if (is_charging) { if (is_charging) {
power_save_timer_->SetEnabled(false); power_save_timer_->SetEnabled(false);
} else { } else {
power_save_timer_->SetEnabled(true); power_save_timer_->SetEnabled(true);
} }
}); });
} }
void InitializePowerSaveTimer() { void InitializePowerSaveTimer() {
power_save_timer_ = new PowerSaveTimer(-1, 60, 300); power_save_timer_ = new PowerSaveTimer(-1, 60, 300);
power_save_timer_->OnEnterSleepMode([this]() { power_save_timer_->OnEnterSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(true); GetDisplay()->SetPowerSaveMode(true);
GetBacklight()->SetBrightness(1); GetBacklight()->SetBrightness(1);
}); });
power_save_timer_->OnExitSleepMode([this]() { power_save_timer_->OnExitSleepMode([this]() {
GetDisplay()->SetPowerSaveMode(false); GetDisplay()->SetPowerSaveMode(false);
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
}); });
power_save_timer_->OnShutdownRequest([this]() { power_save_timer_->OnShutdownRequest([this]() {
if (power_status_ == kDeviceBatterySupply) { if (power_status_ == kDeviceBatterySupply) {
GetBacklight()->SetBrightness(0); GetBacklight()->SetBrightness(0);
esp_timer_stop(power_manager_->timer_handle_); esp_timer_stop(power_manager_->timer_handle_);
esp_io_expander_set_dir( io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); esp_io_expander_set_dir( io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT);
esp_io_expander_set_level(io_exp_handle, XIO_CHG_CTRL, 0); esp_io_expander_set_level(io_exp_handle, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 0); esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 0);
} }
}); });
power_save_timer_->SetEnabled(true); power_save_timer_->SetEnabled(true);
} }
void audio_volume_change(bool direction) { void audio_volume_change(bool direction) {
auto codec = GetAudioCodec(); auto codec = GetAudioCodec();
auto volume = codec->output_volume(); auto volume = codec->output_volume();
if (direction) { if (direction) {
volume += 10; volume += 10;
if (volume > 100) { if (volume > 100) {
volume = 100; volume = 100;
} }
codec->SetOutputVolume(volume); codec->SetOutputVolume(volume);
} else { } else {
volume -= 10; volume -= 10;
if (volume < 0) { if (volume < 0) {
volume = 0; volume = 0;
} }
codec->SetOutputVolume(volume); codec->SetOutputVolume(volume);
} }
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume)); GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
} }
void audio_volume_minimum(){ void audio_volume_minimum(){
GetAudioCodec()->SetOutputVolume(0); GetAudioCodec()->SetOutputVolume(0);
GetDisplay()->ShowNotification(Lang::Strings::MUTED); GetDisplay()->ShowNotification(Lang::Strings::MUTED);
} }
void audio_volume_maxmum(){ void audio_volume_maxmum(){
GetAudioCodec()->SetOutputVolume(100); GetAudioCodec()->SetOutputVolume(100);
GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME); GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME);
} }
esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) { esp_err_t IoExpanderSetLevel(uint16_t pin_mask, uint8_t level) {
return esp_io_expander_set_level(io_exp_handle, pin_mask, level); return esp_io_expander_set_level(io_exp_handle, pin_mask, level);
} }
uint8_t IoExpanderGetLevel(uint16_t pin_mask) { uint8_t IoExpanderGetLevel(uint16_t pin_mask) {
uint32_t pin_val = 0; uint32_t pin_val = 0;
esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val); esp_io_expander_get_level(io_exp_handle, DRV_IO_EXP_INPUT_MASK, &pin_val);
pin_mask &= DRV_IO_EXP_INPUT_MASK; pin_mask &= DRV_IO_EXP_INPUT_MASK;
return (uint8_t)((pin_val & pin_mask) ? 1 : 0); return (uint8_t)((pin_val & pin_mask) ? 1 : 0);
} }
void InitializeIoExpander() { void InitializeIoExpander() {
esp_err_t ret = ESP_OK; esp_err_t ret = ESP_OK;
esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_exp_handle); esp_io_expander_new_i2c_tca95xx_16bit(i2c_bus_, ESP_IO_EXPANDER_I2C_TCA9555_ADDRESS_000, &io_exp_handle);
ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, IO_EXPANDER_OUTPUT); ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_OUTPUT_MASK, IO_EXPANDER_OUTPUT);
ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_INPUT_MASK, IO_EXPANDER_INPUT); ret |= esp_io_expander_set_dir(io_exp_handle, DRV_IO_EXP_INPUT_MASK, IO_EXPANDER_INPUT);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_SYS_POW, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_3V3A, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_3V3A, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_4G, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_EN_4G, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_SPK_EN, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_SPK_EN, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_USB_SEL, 1); ret |= esp_io_expander_set_level(io_exp_handle, XIO_USB_SEL, 1);
ret |= esp_io_expander_set_level(io_exp_handle, XIO_VBUS_EN, 0); ret |= esp_io_expander_set_level(io_exp_handle, XIO_VBUS_EN, 0);
assert(ret == ESP_OK); assert(ret == ESP_OK);
} }
// Initialize I2C peripheral // Initialize I2C peripheral
void InitializeI2c() { void InitializeI2c() {
i2c_master_bus_config_t i2c_bus_cfg = { i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)I2C_NUM_0, .i2c_port = (i2c_port_t)I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN, .sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN, .scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT, .clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7, .glitch_ignore_cnt = 7,
.intr_priority = 0, .intr_priority = 0,
.trans_queue_depth = 0, .trans_queue_depth = 0,
.flags = { .flags = {
.enable_internal_pullup = 1, .enable_internal_pullup = 1,
}, },
}; };
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_)); ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
} }
void InitializeButtons() { void InitializeButtons() {
instance_ = this; instance_ = this;
button_config_t l_btn_cfg = { button_config_t l_btn_cfg = {
.long_press_time = 800, .long_press_time = 800,
.short_press_time = 500 .short_press_time = 500
}; };
button_config_t m_btn_cfg = { button_config_t m_btn_cfg = {
.long_press_time = 800, .long_press_time = 800,
.short_press_time = 500 .short_press_time = 500
}; };
button_config_t r_btn_cfg = { button_config_t r_btn_cfg = {
.long_press_time = 800, .long_press_time = 800,
.short_press_time = 500 .short_press_time = 500
}; };
button_driver_t* xio_l_btn_driver_ = nullptr; button_driver_t* xio_l_btn_driver_ = nullptr;
button_driver_t* xio_m_btn_driver_ = nullptr; button_driver_t* xio_m_btn_driver_ = nullptr;
button_handle_t l_btn_handle = NULL; button_handle_t l_btn_handle = NULL;
button_handle_t m_btn_handle = NULL; button_handle_t m_btn_handle = NULL;
button_handle_t r_btn_handle = NULL; button_handle_t r_btn_handle = NULL;
xio_l_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t)); xio_l_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t));
xio_l_btn_driver_->enable_power_save = false; xio_l_btn_driver_->enable_power_save = false;
xio_l_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t { xio_l_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t {
return !instance_->IoExpanderGetLevel(XIO_KEY_L); return !instance_->IoExpanderGetLevel(XIO_KEY_L);
}; };
ESP_ERROR_CHECK(iot_button_create(&l_btn_cfg, xio_l_btn_driver_, &l_btn_handle)); ESP_ERROR_CHECK(iot_button_create(&l_btn_cfg, xio_l_btn_driver_, &l_btn_handle));
xio_m_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t)); xio_m_btn_driver_ = (button_driver_t*)calloc(1, sizeof(button_driver_t));
xio_m_btn_driver_->enable_power_save = false; xio_m_btn_driver_->enable_power_save = false;
xio_m_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t { xio_m_btn_driver_->get_key_level = [](button_driver_t *button_driver) -> uint8_t {
return instance_->IoExpanderGetLevel(XIO_KEY_M); return instance_->IoExpanderGetLevel(XIO_KEY_M);
}; };
ESP_ERROR_CHECK(iot_button_create(&m_btn_cfg, xio_m_btn_driver_, &m_btn_handle)); ESP_ERROR_CHECK(iot_button_create(&m_btn_cfg, xio_m_btn_driver_, &m_btn_handle));
button_gpio_config_t r_cfg = { button_gpio_config_t r_cfg = {
.gpio_num = R_BUTTON_GPIO, .gpio_num = R_BUTTON_GPIO,
.active_level = BUTTON_INACTIVE, .active_level = BUTTON_INACTIVE,
.enable_power_save = false, .enable_power_save = false,
.disable_pull = false .disable_pull = false
}; };
ESP_ERROR_CHECK(iot_button_new_gpio_device(&r_btn_cfg, &r_cfg, &r_btn_handle)); ESP_ERROR_CHECK(iot_button_new_gpio_device(&r_btn_cfg, &r_cfg, &r_btn_handle));
iot_button_register_cb(l_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(l_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->audio_volume_change(false); self->audio_volume_change(false);
}, this); }, this);
iot_button_register_cb(l_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(l_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->audio_volume_minimum(); self->audio_volume_minimum();
}, this); }, this);
iot_button_register_cb(m_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(m_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
app.ToggleChatState(); app.ToggleChatState();
}, this); }, this);
iot_button_register_cb(m_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(m_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data);
auto& app = Application::GetInstance(); auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
self->ResetWifiConfiguration(); self->ResetWifiConfiguration();
} }
if (self->power_status_ == kDeviceBatterySupply) { if (self->power_status_ == kDeviceBatterySupply) {
auto backlight = Board::GetInstance().GetBacklight(); auto backlight = Board::GetInstance().GetBacklight();
backlight->SetBrightness(0); backlight->SetBrightness(0);
esp_timer_stop(self->power_manager_->timer_handle_); esp_timer_stop(self->power_manager_->timer_handle_);
esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT); esp_io_expander_set_dir(self->io_exp_handle, XIO_CHG_CTRL, IO_EXPANDER_OUTPUT);
esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0); esp_io_expander_set_level(self->io_exp_handle, XIO_CHG_CTRL, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
esp_io_expander_set_level(self->io_exp_handle, XIO_SYS_POW, 0); esp_io_expander_set_level(self->io_exp_handle, XIO_SYS_POW, 0);
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} }
}, this); }, this);
iot_button_register_cb(r_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(r_btn_handle, BUTTON_PRESS_DOWN, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->audio_volume_change(true); self->audio_volume_change(true);
}, this); }, this);
iot_button_register_cb(r_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) { iot_button_register_cb(r_btn_handle, BUTTON_LONG_PRESS_START, nullptr, [](void* button_handle, void* usr_data) {
auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data); auto self = static_cast<atk_dnesp32s3_box2_wifi*>(usr_data);
self->power_save_timer_->WakeUp(); self->power_save_timer_->WakeUp();
self->audio_volume_maxmum(); self->audio_volume_maxmum();
}, this); }, this);
} }
void InitializeSt7789Display() { void InitializeSt7789Display() {
ESP_LOGI(TAG, "Install panel IO"); ESP_LOGI(TAG, "Install panel IO");
/* RD PIN */ /* RD PIN */
gpio_config_t gpio_init_struct; gpio_config_t gpio_init_struct;
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT; gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT;
gpio_init_struct.pin_bit_mask = 1ull << LCD_PIN_RD; gpio_init_struct.pin_bit_mask = 1ull << LCD_PIN_RD;
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&gpio_init_struct); gpio_config(&gpio_init_struct);
gpio_set_level(LCD_PIN_RD, 1); gpio_set_level(LCD_PIN_RD, 1);
/* BL PIN */ /* BL PIN */
gpio_init_struct.pin_bit_mask = 1ull << DISPLAY_BACKLIGHT_PIN; gpio_init_struct.pin_bit_mask = 1ull << DISPLAY_BACKLIGHT_PIN;
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&gpio_init_struct); gpio_config(&gpio_init_struct);
esp_lcd_i80_bus_handle_t i80_bus = NULL; esp_lcd_i80_bus_handle_t i80_bus = NULL;
esp_lcd_i80_bus_config_t bus_config = { esp_lcd_i80_bus_config_t bus_config = {
.dc_gpio_num = LCD_PIN_DC, .dc_gpio_num = LCD_PIN_DC,
.wr_gpio_num = LCD_PIN_WR, .wr_gpio_num = LCD_PIN_WR,
.clk_src = LCD_CLK_SRC_DEFAULT, .clk_src = LCD_CLK_SRC_DEFAULT,
.data_gpio_nums = { .data_gpio_nums = {
LCD_PIN_D0, LCD_PIN_D0,
LCD_PIN_D1, LCD_PIN_D1,
LCD_PIN_D2, LCD_PIN_D2,
LCD_PIN_D3, LCD_PIN_D3,
LCD_PIN_D4, LCD_PIN_D4,
LCD_PIN_D5, LCD_PIN_D5,
LCD_PIN_D6, LCD_PIN_D6,
LCD_PIN_D7, LCD_PIN_D7,
}, },
.bus_width = 8, .bus_width = 8,
.max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t), .max_transfer_bytes = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t),
.psram_trans_align = 64, .psram_trans_align = 64,
.sram_trans_align = 4, .sram_trans_align = 4,
}; };
ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
esp_lcd_panel_io_i80_config_t io_config = { esp_lcd_panel_io_i80_config_t io_config = {
.cs_gpio_num = LCD_PIN_CS, .cs_gpio_num = LCD_PIN_CS,
.pclk_hz = (20 * 1000 * 1000), .pclk_hz = (20 * 1000 * 1000),
.trans_queue_depth = 7, .trans_queue_depth = 7,
.on_color_trans_done = nullptr, .on_color_trans_done = nullptr,
.user_ctx = nullptr, .user_ctx = nullptr,
.lcd_cmd_bits = 8, .lcd_cmd_bits = 8,
.lcd_param_bits = 8, .lcd_param_bits = 8,
.dc_levels = { .dc_levels = {
.dc_idle_level = 1, .dc_idle_level = 1,
.dc_cmd_level = 0, .dc_cmd_level = 0,
.dc_dummy_level = 0, .dc_dummy_level = 0,
.dc_data_level = 1, .dc_data_level = 1,
}, },
.flags = { .flags = {
.cs_active_high = 0, .cs_active_high = 0,
.pclk_active_neg = 0, .pclk_active_neg = 0,
.pclk_idle_low = 0, .pclk_idle_low = 0,
}, },
}; };
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io)); ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &panel_io));
esp_lcd_panel_dev_config_t panel_config = { esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = LCD_PIN_RST, .reset_gpio_num = LCD_PIN_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16, .bits_per_pixel = 16,
}; };
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel)); ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel); esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel); esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true); esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_set_gap(panel, 0, 0); esp_lcd_panel_set_gap(panel, 0, 0);
esp_lcd_panel_io_tx_param(panel_io, 0xCF, (uint8_t[]) {0x00,0x83,0x30}, 3); esp_lcd_panel_io_tx_param(panel_io, 0xCF, (uint8_t[]) {0x00,0x83,0x30}, 3);
esp_lcd_panel_io_tx_param(panel_io, 0xED, (uint8_t[]) {0x64,0x03,0x12,0x81}, 4); esp_lcd_panel_io_tx_param(panel_io, 0xED, (uint8_t[]) {0x64,0x03,0x12,0x81}, 4);
esp_lcd_panel_io_tx_param(panel_io, 0xE8, (uint8_t[]) {0x85,0x01,0x79}, 3); esp_lcd_panel_io_tx_param(panel_io, 0xE8, (uint8_t[]) {0x85,0x01,0x79}, 3);
esp_lcd_panel_io_tx_param(panel_io, 0xCB, (uint8_t[]) {0x39,0x2C,0x00,0x34,0x02}, 5); esp_lcd_panel_io_tx_param(panel_io, 0xCB, (uint8_t[]) {0x39,0x2C,0x00,0x34,0x02}, 5);
esp_lcd_panel_io_tx_param(panel_io, 0xF7, (uint8_t[]) {0x20}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xF7, (uint8_t[]) {0x20}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xEA, (uint8_t[]) {0x00,0x00}, 2); esp_lcd_panel_io_tx_param(panel_io, 0xEA, (uint8_t[]) {0x00,0x00}, 2);
esp_lcd_panel_io_tx_param(panel_io, 0xbb, (uint8_t[]) {0x20}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xbb, (uint8_t[]) {0x20}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xc3, (uint8_t[]) {0x00}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xc3, (uint8_t[]) {0x00}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xC4, (uint8_t[]) {0x20}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xC4, (uint8_t[]) {0x20}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xC5, (uint8_t[]) {0x20}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xC5, (uint8_t[]) {0x20}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xC6, (uint8_t[]) {0x10}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xC6, (uint8_t[]) {0x10}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xC7, (uint8_t[]) {0xB0}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xC7, (uint8_t[]) {0xB0}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0x36, (uint8_t[]) {0x60}, 1); esp_lcd_panel_io_tx_param(panel_io, 0x36, (uint8_t[]) {0x60}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0x3A, (uint8_t[]) {0x55}, 1); esp_lcd_panel_io_tx_param(panel_io, 0x3A, (uint8_t[]) {0x55}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xB1, (uint8_t[]) {0x00,0x1B}, 2); esp_lcd_panel_io_tx_param(panel_io, 0xB1, (uint8_t[]) {0x00,0x1B}, 2);
esp_lcd_panel_io_tx_param(panel_io, 0xF2, (uint8_t[]) {0x08}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xF2, (uint8_t[]) {0x08}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0x26, (uint8_t[]) {0x01}, 1); esp_lcd_panel_io_tx_param(panel_io, 0x26, (uint8_t[]) {0x01}, 1);
esp_lcd_panel_io_tx_param(panel_io, 0xE0, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x32,0x44,0x42,0x06,0x0E,0x12,0x14,0x17}, 14); esp_lcd_panel_io_tx_param(panel_io, 0xE0, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x32,0x44,0x42,0x06,0x0E,0x12,0x14,0x17}, 14);
esp_lcd_panel_io_tx_param(panel_io, 0xE1, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x31,0x54,0x47,0x0E,0x1C,0x17,0x1B,0x1E}, 14); esp_lcd_panel_io_tx_param(panel_io, 0xE1, (uint8_t[]) {0xD0,0x00,0x02,0x07,0x0A,0x28,0x31,0x54,0x47,0x0E,0x1C,0x17,0x1B,0x1E}, 14);
esp_lcd_panel_io_tx_param(panel_io, 0xB7, (uint8_t[]) {0x07}, 1); esp_lcd_panel_io_tx_param(panel_io, 0xB7, (uint8_t[]) {0x07}, 1);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new SpiLcdDisplay(panel_io, panel, display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
} }
public: public:
atk_dnesp32s3_box2_wifi() { atk_dnesp32s3_box2_wifi() {
InitializeI2c(); InitializeI2c();
InitializeIoExpander(); InitializeIoExpander();
InitializePowerSaveTimer(); InitializePowerSaveTimer();
InitializePowerManager(); InitializePowerManager();
InitializeSt7789Display(); InitializeSt7789Display();
InitializeButtons(); InitializeButtons();
GetBacklight()->RestoreBrightness(); GetBacklight()->RestoreBrightness();
InitializeBoardPowerManager(); InitializeBoardPowerManager();
} }
virtual AudioCodec* GetAudioCodec() override { virtual AudioCodec* GetAudioCodec() override {
static Es8389AudioCodec audio_codec( static Es8389AudioCodec audio_codec(
i2c_bus_, i2c_bus_,
I2C_NUM_0, I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE, AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN, AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC, GPIO_NUM_NC,
AUDIO_CODEC_ES8389_ADDR, AUDIO_CODEC_ES8389_ADDR,
false); false);
return &audio_codec; return &audio_codec;
} }
virtual Display* GetDisplay() override { virtual Display* GetDisplay() override {
return display_; return display_;
} }
virtual Backlight* GetBacklight() override { virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT); static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight; return &backlight;
} }
virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override { virtual bool GetBatteryLevel(int& level, bool& charging, bool& discharging) override {
static bool last_discharging = false; static bool last_discharging = false;
charging = power_manager_->IsCharging(); charging = power_manager_->IsCharging();
discharging = power_manager_->IsDischarging(); discharging = power_manager_->IsDischarging();
if (discharging != last_discharging) { if (discharging != last_discharging) {
power_save_timer_->SetEnabled(discharging); power_save_timer_->SetEnabled(discharging);
last_discharging = discharging; last_discharging = discharging;
} }
level = power_manager_->GetBatteryLevel(); level = power_manager_->GetBatteryLevel();
return true; return true;
} }
virtual void SetPowerSaveMode(bool enabled) override { virtual void SetPowerSaveMode(bool enabled) override {
if (!enabled) { if (!enabled) {
power_save_timer_->WakeUp(); power_save_timer_->WakeUp();
} }
WifiBoard::SetPowerSaveMode(enabled); WifiBoard::SetPowerSaveMode(enabled);
} }
}; };
DECLARE_BOARD(atk_dnesp32s3_box2_wifi); DECLARE_BOARD(atk_dnesp32s3_box2_wifi);
// 定义静态成员变量 // 定义静态成员变量
atk_dnesp32s3_box2_wifi* atk_dnesp32s3_box2_wifi::instance_ = nullptr; atk_dnesp32s3_box2_wifi* atk_dnesp32s3_box2_wifi::instance_ = nullptr;

View File

@@ -1,71 +1,71 @@
#ifndef _BOARD_CONFIG_H_ #ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_ #define _BOARD_CONFIG_H_
#include <driver/gpio.h> #include <driver/gpio.h>
enum PowerSupply { enum PowerSupply {
kDeviceTypecSupply, kDeviceTypecSupply,
kDeviceBatterySupply, kDeviceBatterySupply,
}; };
#define AUDIO_INPUT_SAMPLE_RATE 16000 #define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 16000 #define AUDIO_OUTPUT_SAMPLE_RATE 16000
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38 #define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38
#define AUDIO_I2S_GPIO_WS GPIO_NUM_42 #define AUDIO_I2S_GPIO_WS GPIO_NUM_42
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_40 #define AUDIO_I2S_GPIO_BCLK GPIO_NUM_40
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_39 #define AUDIO_I2S_GPIO_DIN GPIO_NUM_39
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_41 #define AUDIO_I2S_GPIO_DOUT GPIO_NUM_41
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_48 #define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_48
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_47 #define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_47
#define AUDIO_CODEC_ES8389_ADDR ES8389_CODEC_DEFAULT_ADDR #define AUDIO_CODEC_ES8389_ADDR ES8389_CODEC_DEFAULT_ADDR
#define R_BUTTON_GPIO GPIO_NUM_0 #define R_BUTTON_GPIO GPIO_NUM_0
#define XL9555_INT_GPIO GPIO_NUM_2 #define XL9555_INT_GPIO GPIO_NUM_2
#define XIO_IO_SBU2 (IO_EXPANDER_PIN_NUM_3) #define XIO_IO_SBU2 (IO_EXPANDER_PIN_NUM_3)
#define XIO_IO_SBU1 (IO_EXPANDER_PIN_NUM_4) #define XIO_IO_SBU1 (IO_EXPANDER_PIN_NUM_4)
#define XIO_KEY_L (IO_EXPANDER_PIN_NUM_5) #define XIO_KEY_L (IO_EXPANDER_PIN_NUM_5)
#define XIO_KEY_Q (IO_EXPANDER_PIN_NUM_6) #define XIO_KEY_Q (IO_EXPANDER_PIN_NUM_6)
#define XIO_KEY_M (IO_EXPANDER_PIN_NUM_7) #define XIO_KEY_M (IO_EXPANDER_PIN_NUM_7)
#define XIO_USB_SEL (IO_EXPANDER_PIN_NUM_8) #define XIO_USB_SEL (IO_EXPANDER_PIN_NUM_8)
#define XIO_SPK_EN (IO_EXPANDER_PIN_NUM_9) #define XIO_SPK_EN (IO_EXPANDER_PIN_NUM_9)
#define XIO_SYS_POW (IO_EXPANDER_PIN_NUM_10) #define XIO_SYS_POW (IO_EXPANDER_PIN_NUM_10)
#define XIO_VBUS_EN (IO_EXPANDER_PIN_NUM_11) #define XIO_VBUS_EN (IO_EXPANDER_PIN_NUM_11)
#define XIO_EN_4G (IO_EXPANDER_PIN_NUM_12) #define XIO_EN_4G (IO_EXPANDER_PIN_NUM_12)
#define XIO_EN_3V3A (IO_EXPANDER_PIN_NUM_13) #define XIO_EN_3V3A (IO_EXPANDER_PIN_NUM_13)
#define XIO_CHG_CTRL (IO_EXPANDER_PIN_NUM_14) #define XIO_CHG_CTRL (IO_EXPANDER_PIN_NUM_14)
#define XIO_CHRG (IO_EXPANDER_PIN_NUM_15) #define XIO_CHRG (IO_EXPANDER_PIN_NUM_15)
#define DRV_IO_EXP_OUTPUT_MASK 0x3F18 #define DRV_IO_EXP_OUTPUT_MASK 0x3F18
#define DRV_IO_EXP_INPUT_MASK 0xC0E7 #define DRV_IO_EXP_INPUT_MASK 0xC0E7
#define LCD_PIN_CS GPIO_NUM_14 #define LCD_PIN_CS GPIO_NUM_14
#define LCD_PIN_DC GPIO_NUM_12 #define LCD_PIN_DC GPIO_NUM_12
#define LCD_PIN_RD GPIO_NUM_10 #define LCD_PIN_RD GPIO_NUM_10
#define LCD_PIN_WR GPIO_NUM_11 #define LCD_PIN_WR GPIO_NUM_11
#define LCD_PIN_RST GPIO_NUM_NC #define LCD_PIN_RST GPIO_NUM_NC
#define LCD_PIN_D0 GPIO_NUM_13 #define LCD_PIN_D0 GPIO_NUM_13
#define LCD_PIN_D1 GPIO_NUM_9 #define LCD_PIN_D1 GPIO_NUM_9
#define LCD_PIN_D2 GPIO_NUM_8 #define LCD_PIN_D2 GPIO_NUM_8
#define LCD_PIN_D3 GPIO_NUM_7 #define LCD_PIN_D3 GPIO_NUM_7
#define LCD_PIN_D4 GPIO_NUM_6 #define LCD_PIN_D4 GPIO_NUM_6
#define LCD_PIN_D5 GPIO_NUM_5 #define LCD_PIN_D5 GPIO_NUM_5
#define LCD_PIN_D6 GPIO_NUM_4 #define LCD_PIN_D6 GPIO_NUM_4
#define LCD_PIN_D7 GPIO_NUM_3 #define LCD_PIN_D7 GPIO_NUM_3
#define DISPLAY_WIDTH 240 #define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 320 #define DISPLAY_HEIGHT 320
#define DISPLAY_MIRROR_X false #define DISPLAY_MIRROR_X false
#define DISPLAY_MIRROR_Y false #define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY false #define DISPLAY_SWAP_XY false
#define DISPLAY_OFFSET_X 0 #define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0 #define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_21 #define DISPLAY_BACKLIGHT_PIN GPIO_NUM_21
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false #define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
#endif // _BOARD_CONFIG_H_ #endif // _BOARD_CONFIG_H_

View File

@@ -1,9 +1,9 @@
{ {
"target": "esp32s3", "target": "esp32s3",
"builds": [ "builds": [
{ {
"name": "atk-dnesp32s3-box2-wifi", "name": "atk-dnesp32s3-box2-wifi",
"sdkconfig_append": [] "sdkconfig_append": []
} }
] ]
} }

Some files were not shown because too many files have changed in this diff Show More