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*]

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

6
CMakeLists.txt Executable file → Normal file
View File

@@ -4,10 +4,14 @@
# CMakeLists in this exact order for cmake to work correctly
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_compile_options(-Wno-missing-field-initializers)
# Fix Windows command line length limit
set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(xiaozhi)

View File

@@ -1,78 +1,33 @@
# 超级小智-ESP32
(中文 | English(编写中) | 日本語(编写中)
# An MCP-based Chatbot | 一个基于 MCP 的聊天机器人
基于 https://github.com/78/xiaozhi-esp32 改良的船新版本
(中文 | [English](README_en.md) | [日本語](README_ja.md)
## 视频
👉 [人类:给 AI 装摄像头 vs AI当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
👉 [手工打造你的 AI 女友新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
## 介绍
## 💡介绍
这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,或用于商业用途。
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:暂无Telegram群暂无。
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:1011329060
项目主要贡献者:小霜霜Meow(抖音、B站UP)、空白泡泡糖果B站UP硅灵造物科技B站UP
项目其它贡献者:[@zhubinsheng](https://github.com/zhubinsheng)
项目其它贡献者:@zhubinsheng
贡献者说明:引入部分其它贡献者在其它项目上的代码,并进行了部分修改。
音乐服务器相关源码提供者(为爱发电)小霜霜Meow
音乐服务器相关源码提供者(为爱发电)小霜霜Meow
感谢群友 cz 提供音乐服务器
音乐服务器源码请见 https://github.com/IntelligentlyEverything/MeowMusicServer
### ❕注意事项
1. 如果小智说找不到歌曲怎么办?
进入[小智后台](https://xiaozhi.me/),找到对应设备,修改角色配置
- 选择 DeepSeekV3 大语言模型
- 在人物介绍中填入
- 收到音乐相关的需求时,只使用 MPC tool self.music.play_song 工具,同时禁止使用 search_music 功能。
2. 内置API调用失败怎么办
请查看具体错误代码后加入QQ群865754861或电报群 http://t.me/MeowMusicServer 给出错误代码和日志,等待我们修复。
### ⚙️已支持硬件芯片系列
- ESP32
- ESP32-S3
- ESP32-C3
- ESP32-C6
- ESP32-P4
❕大部分硬件由于没有进行完整测试可能会存在一些问题属于正常现象具体可提交issues进行反馈。
### 项目改动范围
新增:
- main/schedule_manager.h
- main/schedule_manager.cc
- main/audio/timer_manager.h
- main/audio/timer_manager.cc
- 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
修改:
- main/audio/codecs/no_audio_codec.h
- main/audio/codecs/no_audio_codec.cc
- main/audio/audio_codec.h
- main/audio/audio_codec.cc
- main/audio/audio_service.h
- main/audio/audio_service.cc
- main/boards/common/board.h
- main/boards/common/board.cc
- main/display/display.h
- main/display/display.cc
- main/display/lcd_display.h
- main/display/lcd_display.cc
- main/application.h
- main/application.cc
- main/idf_component.yml
- main/mcp_server.cc
### 基于 MCP 控制万物
小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制。
![通过MCP控制万物](docs/mcp-based-graph.jpg)
@@ -91,8 +46,6 @@
- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台
- 通过设备端 MCP 实现设备控制音量、灯光、电机、GPIO 等)
- 通过云端 MCP 扩展大模型能力智能家居控制、PC桌面操作、知识搜索、邮件收发等
本项目新增功能:
- 新增音乐播放功能,支持播放本地音乐(开发中,敬请期待)、云端音乐(完善中)。
## 硬件
@@ -209,10 +162,10 @@
## Star History
<a href="https://star-history.com/#IntelligentlyEverything/xiaozhi-esp32&Date">
<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=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" />
<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_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.

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

BIN
esptool.exe Executable file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ config OTA_URL
choice
prompt "Flash Assets"
default FLASH_NONE_ASSETS
default FLASH_DEFAULT_ASSETS
help
Select the assets to flash.
@@ -86,40 +86,37 @@ choice BOARD_TYPE
help
Board type. 开发板类型
config BOARD_TYPE_BREAD_COMPACT_WIFI
bool "面包板新版接线WiFi"
bool "Bread Compact WiFi (面包板)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_BREAD_COMPACT_WIFI_LCD
bool "面包板新版接线(WiFi+ LCD"
bool "Bread Compact WiFi + LCD (面包板)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_BREAD_COMPACT_WIFI_CAM
bool "面包板新版接线(WiFi+ LCD + Camera"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP32S3_SMART_SPEAKER
bool "ESP32-S3 智能音箱PCM5102+INMP441"
bool "Bread Compact WiFi + LCD + Camera (面包板)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_BREAD_COMPACT_ML307
bool "面包板新版接线ML307 AT"
bool "Bread Compact ML307/EC801E (面包板 4G)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_BREAD_COMPACT_ESP32
bool "面包板WiFi ESP32 DevKit"
bool "Bread Compact ESP32 DevKit (面包板)"
depends on IDF_TARGET_ESP32
config BOARD_TYPE_BREAD_COMPACT_ESP32_LCD
bool "面包板WiFi+ LCD ESP32 DevKit"
bool "Bread Compact ESP32 DevKit + LCD (面包板)"
depends on IDF_TARGET_ESP32
config BOARD_TYPE_XMINI_C3_V3
bool "虾哥 Mini C3 V3"
bool "Xmini C3 V3"
depends on IDF_TARGET_ESP32C3
config BOARD_TYPE_XMINI_C3_4G
bool "虾哥 Mini C3 4G"
bool "Xmini C3 4G"
depends on IDF_TARGET_ESP32C3
config BOARD_TYPE_XMINI_C3
bool "虾哥 Mini C3"
bool "Xmini C3"
depends on IDF_TARGET_ESP32C3
config BOARD_TYPE_ESP32S3_KORVO2_V3
bool "ESP32S3_KORVO2_V3开发板"
bool "ESP32S3 KORVO2 V3"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP_SPARKBOT
bool "ESP-SparkBot开发板"
bool "ESP-SparkBot"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP_SPOT_S3
bool "ESP-Spot-S3"
@@ -149,10 +146,10 @@ choice BOARD_TYPE
bool "Kevin C3"
depends on IDF_TARGET_ESP32C3
config BOARD_TYPE_KEVIN_SP_V3_DEV
bool "Kevin SP V3开发板"
bool "Kevin SP V3"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_KEVIN_SP_V4_DEV
bool "Kevin SP V4开发板"
bool "Kevin SP V4"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP32_CGC
bool "ESP32 CGC"
@@ -161,13 +158,13 @@ choice BOARD_TYPE
bool "ESP32 CGC 144"
depends on IDF_TARGET_ESP32
config BOARD_TYPE_KEVIN_YUYING_313LCD
bool "鱼鹰科技3.13LCD开发板"
bool "鱼鹰科技 3.13LCD"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_LICHUANG_DEV
bool "立创·实战派ESP32-S3开发板"
bool "立创·实战派 ESP32-S3"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_LICHUANG_C3_DEV
bool "立创·实战派ESP32-C3开发板"
bool "立创·实战派 ESP32-C3"
depends on IDF_TARGET_ESP32C3
config BOARD_TYPE_DF_K10
bool "DFRobot 行空板 k10"
@@ -218,6 +215,8 @@ choice BOARD_TYPE
bool "Waveshare ESP32-S3-Touch-AMOLED-2.06"
config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75
bool "Waveshare ESP32-S3-Touch-AMOLED-1.75"
config BOARD_TYPE_ESP32S3_Touch_LCD_4B
bool "Waveshare ESP32-S3-Touch-LCD-4B"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP32S3_Touch_LCD_1_85C
bool "Waveshare ESP32-S3-Touch-LCD-1.85C"
@@ -234,6 +233,9 @@ choice BOARD_TYPE
config BOARD_TYPE_ESP32C6_Touch_AMOLED_1_43
bool "Waveshare ESP32-C6-Touch-AMOLOED-1.43"
depends on IDF_TARGET_ESP32C6
config BOARD_TYPE_ESP32S3_Touch_LCD_3_49
bool "Waveshare ESP32-S3-Touch-LCD-3.49"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_ESP32S3_Touch_LCD_3_5
bool "Waveshare ESP32-S3-Touch-LCD-3.5"
depends on IDF_TARGET_ESP32S3
@@ -371,19 +373,20 @@ choice BOARD_TYPE
depends on IDF_TARGET_ESP32S3
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_SURFER_C3_1_14TFT
bool "Surfer-C3-1-14TFT"
bool "Surfer-C3-1.14TFT"
depends on IDF_TARGET_ESP32C3
config BOARD_TYPE_YUNLIAO_S3
bool "小智云聊-S3"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_JINAO_S3
bool "极脑ESP32-S3开发板"
depends on IDF_TARGET_ESP32S3
endchoice
choice ESP_S3_LCD_EV_Board_Version_TYPE
depends on BOARD_TYPE_ESP_S3_LCD_EV_Board
prompt "EV_BOARD Type"
default ESP_S3_LCD_EV_Board_1p4
help
开发板硬件版本型号选择
config ESP_S3_LCD_EV_Board_1p4
bool "乐鑫ESP32_S3_LCD_EV_Board-MB_V1.4"
config ESP_S3_LCD_EV_Board_1p5
@@ -395,13 +398,13 @@ choice DISPLAY_OLED_TYPE
prompt "OLED Type"
default OLED_SSD1306_128X32
help
OLED 屏幕类型选择
OLED Monochrome Display Type
config OLED_SSD1306_128X32
bool "SSD1306, 分辨率128*32"
bool "SSD1306 128*32"
config OLED_SSD1306_128X64
bool "SSD1306, 分辨率128*64"
bool "SSD1306 128*64"
config OLED_SH1106_128X64
bool "SH1106, 分辨率128*64"
bool "SH1106 128*64"
endchoice
choice DISPLAY_LCD_TYPE
@@ -409,37 +412,37 @@ choice DISPLAY_LCD_TYPE
prompt "LCD Type"
default LCD_ST7789_240X320
help
屏幕类型选择
LCD Display Type
config LCD_ST7789_240X320
bool "ST7789, 分辨率240*320, IPS"
bool "ST7789 240*320, IPS"
config LCD_ST7789_240X320_NO_IPS
bool "ST7789, 分辨率240*320, IPS"
bool "ST7789 240*320, Non-IPS"
config LCD_ST7789_170X320
bool "ST7789, 分辨率170*320"
bool "ST7789 170*320"
config LCD_ST7789_172X320
bool "ST7789, 分辨率172*320"
bool "ST7789 172*320"
config LCD_ST7789_240X280
bool "ST7789, 分辨率240*280"
bool "ST7789 240*280"
config LCD_ST7789_240X240
bool "ST7789, 分辨率240*240"
bool "ST7789 240*240"
config LCD_ST7789_240X240_7PIN
bool "ST7789, 分辨率240*240, 7PIN"
bool "ST7789 240*240, 7PIN"
config LCD_ST7789_240X135
bool "ST7789, 分辨率240*135"
bool "ST7789 240*135"
config LCD_ST7735_128X160
bool "ST7735, 分辨率128*160"
bool "ST7735 128*160"
config LCD_ST7735_128X128
bool "ST7735, 分辨率128*128"
bool "ST7735 128*128"
config LCD_ST7796_320X480
bool "ST7796, 分辨率320*480 IPS"
bool "ST7796 320*480 IPS"
config LCD_ST7796_320X480_NO_IPS
bool "ST7796, 分辨率320*480, IPS"
bool "ST7796 320*480, Non-IPS"
config LCD_ILI9341_240X320
bool "ILI9341, 分辨率240*320"
bool "ILI9341 240*320"
config LCD_ILI9341_240X320_NO_IPS
bool "ILI9341, 分辨率240*320, IPS"
bool "ILI9341 240*320, Non-IPS"
config LCD_GC9A01_240X240
bool "GC9A01, 分辨率240*240, 圆屏"
bool "GC9A01 240*240 Circle"
config LCD_TYPE_800_1280_10_1_INCH
bool "Waveshare 101M-8001280-IPS-CT-K Display"
config LCD_TYPE_800_1280_10_1_INCH_A
@@ -449,7 +452,7 @@ choice DISPLAY_LCD_TYPE
config LCD_TYPE_720_720_4_INCH
bool "Waveshare ESP32-P4-WIFI6-Touch-LCD-4C with 720*720 4inch round display"
config LCD_CUSTOM
bool "自定义屏幕参数"
bool "Custom LCD (自定义屏幕参数)"
endchoice
choice DISPLAY_ESP32S3_KORVO2_V3
@@ -457,11 +460,11 @@ choice DISPLAY_ESP32S3_KORVO2_V3
prompt "ESP32S3_KORVO2_V3 LCD Type"
default ESP32S3_KORVO2_V3_LCD_ST7789
help
屏幕类型选择
LCD Display Type
config ESP32S3_KORVO2_V3_LCD_ST7789
bool "ST7789, 分辨率240*280"
bool "ST7789 240*280"
config ESP32S3_KORVO2_V3_LCD_ILI9341
bool "ILI9341, 分辨率240*320"
bool "ILI9341 240*320"
endchoice
choice DISPLAY_ESP32S3_AUDIO_BOARD
@@ -469,53 +472,75 @@ choice DISPLAY_ESP32S3_AUDIO_BOARD
prompt "ESP32S3_AUDIO_BOARD LCD Type"
default AUDIO_BOARD_LCD_JD9853
help
屏幕类型选择
LCD Display Type
config AUDIO_BOARD_LCD_JD9853
bool "JD9853, 分辨率320*172"
bool "JD9853 320*172"
config AUDIO_BOARD_LCD_ST7789
bool "ST7789, 分辨率240*320"
bool "ST7789 240*320"
endchoice
config USE_WECHAT_MESSAGE_STYLE
bool "Enable WeChat Message Style"
default n
choice DISPLAY_STYLE
prompt "Select display style"
default USE_DEFAULT_MESSAGE_STYLE
help
使用微信聊天界面风格
Select display style for Xiaozhi device
config USE_ESP_WAKE_WORD
bool "Enable Wake Word Detection (without AFE)"
default n
depends on IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C5 || IDF_TARGET_ESP32C6 || (IDF_TARGET_ESP32 && SPIRAM)
help
支持 ESP32 C3、ESP32 C5 与 ESP32 C6增加ESP32支持需要开启PSRAM
config USE_DEFAULT_MESSAGE_STYLE
bool "Enable default message style"
config USE_AFE_WAKE_WORD
bool "Enable Wake Word Detection (AFE)"
default y
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
help
需要 ESP32 S3 与 PSRAM 支持
config USE_WECHAT_MESSAGE_STYLE
bool "Enable WeChat Message Style"
config USE_CUSTOM_WAKE_WORD
bool "Enable Custom Wake Word Detection"
default n
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM && (!USE_AFE_WAKE_WORD)
config USE_EMOTE_MESSAGE_STYLE
bool "Emote animation style"
depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR
endchoice
choice WAKE_WORD_TYPE
prompt "Wake Word Implementation Type"
default USE_AFE_WAKE_WORD if (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
default WAKE_WORD_DISABLED
help
需要 ESP32 S3 与 PSRAM 支持
Choose the type of wake word implementation to use
config WAKE_WORD_DISABLED
bool "Disabled"
help
Disable wake word detection
config USE_ESP_WAKE_WORD
bool "Wakenet model without AFE"
depends on IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C5 || IDF_TARGET_ESP32C6 || (IDF_TARGET_ESP32 && SPIRAM)
help
Support ESP32 C3、ESP32 C5 与 ESP32 C6, and (ESP32 with PSRAM)
config USE_AFE_WAKE_WORD
bool "Wakenet model with AFE"
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
help
Support AEC if available, requires ESP32 S3 and PSRAM
config USE_CUSTOM_WAKE_WORD
bool "Multinet model (Custom Wake Word)"
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
help
Requires ESP32 S3 and PSRAM
endchoice
config CUSTOM_WAKE_WORD
string "Custom Wake Word"
default "xiao tu dou"
depends on USE_CUSTOM_WAKE_WORD
help
自定义唤醒词,中文用拼音表示,每个字之间用空格隔开
Custom Wake Word, use pinyin for Chinese, separated by spaces
config CUSTOM_WAKE_WORD_DISPLAY
string "Custom Wake Word Display"
default "小土豆"
depends on USE_CUSTOM_WAKE_WORD
help
唤醒后发送给服务器的问候语
Greeting sent to the server after wake word detection
config CUSTOM_WAKE_WORD_THRESHOLD
int "Custom Wake Word Threshold (%)"
@@ -523,58 +548,68 @@ config CUSTOM_WAKE_WORD_THRESHOLD
range 1 99
depends on USE_CUSTOM_WAKE_WORD
help
自定义唤醒词阈值范围1-99越小越敏感默认10
Custom Wake Word Threshold, range 1-99, the smaller the more sensitive, default 20
config SEND_WAKE_WORD_DATA
bool "Send Wake Word Data"
default y
depends on USE_AFE_WAKE_WORD || USE_CUSTOM_WAKE_WORD
help
Send wake word data to the server as the first message of the conversation and wait for response
config USE_AUDIO_PROCESSOR
bool "Enable Audio Noise Reduction"
default y
depends on (IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
help
需要 ESP32 S3 PSRAM 支持
Requires ESP32 S3 and PSRAM
config USE_DEVICE_AEC
bool "Enable Device-Side AEC"
default n
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE || BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 || BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3)
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE \
|| BOARD_TYPE_LICHUANG_DEV || BOARD_TYPE_ESP32S3_KORVO2_V3 || BOARD_TYPE_ESP32S3_Touch_AMOLED_1_75 \
|| BOARD_TYPE_ESP32S3_Touch_AMOLED_2_06 || BOARD_TYPE_ESP32S3_Touch_LCD_4B || BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_4B \
|| BOARD_TYPE_ESP32P4_WIFI6_Touch_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3 \
|| BOARD_TYPE_ECHOEAR || BOARD_TYPE_ESP32S3_Touch_LCD_3_49)
help
因为性能不够,不建议和微信聊天界面风格同时开启
To work properly, device-side AEC requires a clean output reference path from the speaker signal and physical acoustic isolation between the microphone and speaker.
config USE_SERVER_AEC
bool "Enable Server-Side AEC (Unstable)"
default n
depends on USE_AUDIO_PROCESSOR
help
启用服务器端 AEC需要服务器支持
To work perperly, server-side AEC requires server support
config USE_AUDIO_DEBUGGER
bool "Enable Audio Debugger"
default n
help
启用音频调试功能通过UDP发送音频数据
config USE_ACOUSTIC_WIFI_PROVISIONING
bool "Enable Acoustic WiFi Provisioning"
default n
help
启用声波配网功能,使用音频信号传输 WiFi 配置数据
Enable audio debugger, send audio data through UDP to the host machine
config AUDIO_DEBUG_UDP_SERVER
string "Audio Debug UDP Server Address"
default "192.168.2.100:8000"
depends on USE_AUDIO_DEBUGGER
help
UDP服务器地址,格式: IP:PORT用于接收音频调试数据
UDP server address, format: IP:PORT, used to receive audio debugging data
config USE_ACOUSTIC_WIFI_PROVISIONING
bool "Enable Acoustic WiFi Provisioning"
default n
help
Enable acoustic WiFi provisioning, use audio signal to transmit WiFi configuration data
config RECEIVE_CUSTOM_MESSAGE
bool "Enable Custom Message Reception"
default n
help
启用接收自定义消息功能,允许设备接收来自服务器的自定义消息(最好通过 MQTT 协议)
Enable custom message reception, allow the device to receive custom messages from the server (preferably through the MQTT protocol)
menu "TAIJIPAI_S3_CONFIG"
menu TAIJIPAI_S3_CONFIG
depends on BOARD_TYPE_ESP32S3_Taiji_Pi
choice I2S_TYPE_TAIJIPI_S3
depends on BOARD_TYPE_ESP32S3_Taiji_Pi
prompt "taiji-pi-S3 I2S Type"
default TAIJIPAI_I2S_TYPE_STD
help
@@ -586,7 +621,7 @@ menu "TAIJIPAI_S3_CONFIG"
endchoice
config I2S_USE_2SLOT
bool "Enable Use 2 Slot"
bool "Enable I2S 2 Slot"
default n
help
启动双声道

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

View File

@@ -11,6 +11,8 @@
#include "settings.h"
#include <cstring>
#include <cstdlib>
#include <vector>
#include <esp_log.h>
#include <cJSON.h>
#include <driver/gpio.h>
@@ -72,28 +74,20 @@ Application::~Application() {
void Application::CheckAssetsVersion() {
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
auto assets = board.GetAssets();
if (!assets) {
ESP_LOGE(TAG, "Assets is not set for board %s", BOARD_NAME);
return;
}
auto& assets = Assets::GetInstance();
if (!assets->partition_valid()) {
ESP_LOGE(TAG, "Assets partition is not valid for board %s", BOARD_NAME);
if (!assets.partition_valid()) {
ESP_LOGW(TAG, "Assets partition is disabled for board %s", BOARD_NAME);
return;
}
Settings settings("assets", true);
// Check if there is a new assets need to be downloaded
std::string download_url = settings.GetString("download_url");
if (!download_url.empty()) {
settings.EraseKey("download_url");
}
if (download_url.empty() && !assets->checksum_valid()) {
download_url = assets->default_assets_url();
}
if (!download_url.empty()) {
settings.EraseKey("download_url");
char message[256];
snprintf(message, sizeof(message), Lang::Strings::FOUND_NEW_ASSETS, download_url.c_str());
Alert(Lang::Strings::LOADING_ASSETS, message, "cloud_arrow_down", Lang::Sounds::OGG_UPGRADE);
@@ -104,7 +98,7 @@ void Application::CheckAssetsVersion() {
board.SetPowerSaveMode(false);
display->SetChatMessage("system", Lang::Strings::PLEASE_WAIT);
bool success = assets->Download(download_url, [display](int progress, size_t speed) -> void {
bool success = assets.Download(download_url, [display](int progress, size_t speed) -> void {
std::thread([display, progress, speed]() {
char buffer[32];
snprintf(buffer, sizeof(buffer), "%d%% %uKB/s", progress, speed / 1024);
@@ -123,7 +117,7 @@ void Application::CheckAssetsVersion() {
}
// Apply assets
assets->Apply();
assets.Apply();
display->SetChatMessage("system", "");
display->SetEmotion("microchip_ai");
}
@@ -360,6 +354,9 @@ void Application::Start() {
/* Setup the display */
auto display = board.GetDisplay();
// Print board name/version info
display->SetChatMessage("system", SystemInfo::GetUserAgent().c_str());
/* Setup the audio service */
auto codec = board.GetAudioCodec();
audio_service_.Initialize(codec);
@@ -377,6 +374,12 @@ void Application::Start() {
};
audio_service_.SetCallbacks(callbacks);
// Start the main event loop task with priority 3
xTaskCreate([](void* arg) {
((Application*)arg)->MainEventLoop();
vTaskDelete(NULL);
}, "main_event_loop", 2048 * 4, this, 3, &main_event_loop_task_handle_);
/* Start the clock timer to update the status bar */
esp_timer_start_periodic(clock_timer_handle_, 1000000);
@@ -537,15 +540,25 @@ void Application::Start() {
std::string message = std::string(Lang::Strings::VERSION) + ota.GetCurrentVersion();
display->ShowNotification(message.c_str());
display->SetChatMessage("system", "");
// 初始化闹钟管理器
auto& alarm_manager = AlarmManager::GetInstance();
alarm_manager.Initialize();
// 设置闹钟回调
alarm_manager.SetAlarmTriggeredCallback([this](const AlarmItem& alarm) {
Schedule([this, alarm]() { OnAlarmTriggered(alarm); });
});
alarm_manager.SetAlarmSnoozeCallback([this](const AlarmItem& alarm) {
Schedule([this, alarm]() { OnAlarmSnoozed(alarm); });
});
alarm_manager.SetAlarmStopCallback([this](const AlarmItem& alarm) {
Schedule([this, alarm]() { OnAlarmStopped(alarm); });
});
// Play the success sound to indicate the device is ready
audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS);
}
// Start the main event loop task with priority 3
xTaskCreate([](void* arg) {
((Application*)arg)->MainEventLoop();
vTaskDelete(NULL);
}, "main_event_loop", 2048 * 4, this, 3, &main_event_loop_task_handle_);
}
// Add a async task to MainLoop
@@ -606,6 +619,16 @@ void Application::MainEventLoop() {
clock_ticks_++;
auto display = Board::GetInstance().GetDisplay();
display->UpdateStatusBar();
display->OnClockTimer();
// 检查闹钟(每秒检查一次)
auto& alarm_manager = AlarmManager::GetInstance();
alarm_manager.CheckAlarms();
// 更新音乐播放进度(每秒更新一次)
if (is_music_playing_) {
UpdateMusicProgress();
}
// Print the debug info every 10 seconds
if (clock_ticks_ % 10 == 0) {
@@ -635,7 +658,7 @@ void Application::OnWakeWordDetected() {
auto wake_word = audio_service_.GetLastWakeWord();
ESP_LOGI(TAG, "Wake word detected: %s", wake_word.c_str());
#if CONFIG_USE_AFE_WAKE_WORD || CONFIG_USE_CUSTOM_WAKE_WORD
#if CONFIG_SEND_WAKE_WORD_DATA
// Encode and send the wake word data to the server
while (auto packet = audio_service_.PopWakeWordPacket()) {
protocol_->SendAudio(std::move(packet));
@@ -685,6 +708,7 @@ void Application::SetDeviceState(DeviceState state) {
auto display = board.GetDisplay();
auto led = board.GetLed();
led->OnStateChanged();
display->OnStateChanged();
switch (state) {
case kDeviceStateUnknown:
case kDeviceStateIdle:
@@ -716,11 +740,7 @@ void Application::SetDeviceState(DeviceState state) {
if (listening_mode_ != kListeningModeRealtime) {
audio_service_.EnableVoiceProcessing(false);
// Only AFE wake word can be detected in speaking mode
#if CONFIG_USE_AFE_WAKE_WORD
audio_service_.EnableWakeWordDetection(true);
#else
audio_service_.EnableWakeWordDetection(false);
#endif
audio_service_.EnableWakeWordDetection(audio_service_.IsAfeWakeWord());
}
audio_service_.ResetDecoder();
break;
@@ -841,10 +861,8 @@ void Application::SendMcpMessage(const std::string& payload) {
// Make sure you are using main thread to send MCP message
if (xTaskGetCurrentTaskHandle() == main_event_loop_task_handle_) {
ESP_LOGI(TAG, "Send MCP message in main thread");
protocol_->SendMcpMessage(payload);
} else {
ESP_LOGI(TAG, "Send MCP message in sub thread");
Schedule([this, payload = std::move(payload)]() {
protocol_->SendMcpMessage(payload);
});
@@ -878,6 +896,10 @@ void Application::SetAecMode(AecMode mode) {
});
}
void Application::PlaySound(const std::string_view& sound) {
audio_service_.PlaySound(sound);
}
// 新增:接收外部音频数据(如音乐播放)
void Application::AddAudioData(AudioStreamPacket&& packet) {
auto codec = Board::GetInstance().GetAudioCodec();
@@ -890,9 +912,6 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
// 检查采样率是否匹配,如果不匹配则进行简单重采样
if (packet.sample_rate != codec->output_sample_rate()) {
// ESP_LOGI(TAG, "Resampling music audio from %d to %d Hz",
// packet.sample_rate, codec->output_sample_rate());
// 验证采样率参数
if (packet.sample_rate <= 0 || codec->output_sample_rate() <= 0) {
ESP_LOGE(TAG, "Invalid sample rates: %d -> %d",
@@ -900,19 +919,12 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
return;
}
std::vector<int16_t> resampled;
if (packet.sample_rate > codec->output_sample_rate()) {
ESP_LOGI(TAG, "Music Player: Adjust the sampling rate from %d Hz to %d Hz",
codec->output_sample_rate(), packet.sample_rate);
// 尝试动态切换采样率
if (codec->SetOutputSampleRate(packet.sample_rate)) {
ESP_LOGI(TAG, "Successfully switched to music playback sampling rate: %d Hz", packet.sample_rate);
} else {
ESP_LOGW(TAG, "Unable to switch sampling rate, continue using current sampling rate: %d Hz", codec->output_sample_rate());
}
// 尝试动态切换采样率
if (codec->SetOutputSampleRate(packet.sample_rate)) {
ESP_LOGI(TAG, "Successfully switched to music playback sampling rate: %d Hz", packet.sample_rate);
} else {
ESP_LOGW(TAG, "Unable to switch sampling rate, continue using current sampling rate: %d Hz", codec->output_sample_rate());
// 如果无法切换采样率,继续使用当前的采样率进行处理
if (packet.sample_rate > codec->output_sample_rate()) {
// 下采样:简单丢弃部分样本
float downsample_ratio = static_cast<float>(packet.sample_rate) / codec->output_sample_rate();
@@ -933,12 +945,11 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
// 上采样:线性插值
float upsample_ratio = codec->output_sample_rate() / static_cast<float>(packet.sample_rate);
size_t expected_size = static_cast<size_t>(pcm_data.size() * upsample_ratio + 0.5f);
resampled.reserve(expected_size);
std::vector<int16_t> resampled(expected_size);
for (size_t i = 0; i < pcm_data.size(); ++i) {
// 添加原始样本
resampled.push_back(pcm_data[i]);
resampled[i * static_cast<size_t>(upsample_ratio)] = pcm_data[i];
// 计算需要插值的样本数
int interpolation_count = static_cast<int>(upsample_ratio) - 1;
@@ -948,22 +959,21 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
for (int j = 1; j <= interpolation_count; ++j) {
float t = static_cast<float>(j) / (interpolation_count + 1);
int16_t interpolated = static_cast<int16_t>(current + (next - current) * t);
resampled.push_back(interpolated);
resampled[i * static_cast<size_t>(upsample_ratio) + j] = interpolated;
}
} else if (interpolation_count > 0) {
// 最后一个样本,直接重复
for (int j = 1; j <= interpolation_count; ++j) {
resampled.push_back(pcm_data[i]);
resampled[i * static_cast<size_t>(upsample_ratio) + j] = pcm_data[i];
}
}
}
pcm_data = std::move(resampled);
ESP_LOGI(TAG, "Upsampled %d -> %d samples (ratio: %.2f)",
pcm_data.size(), resampled.size(), upsample_ratio);
pcm_data.size() / static_cast<size_t>(upsample_ratio), pcm_data.size(), upsample_ratio);
}
}
pcm_data = std::move(resampled);
}
// 确保音频输出已启用
@@ -979,6 +989,234 @@ void Application::AddAudioData(AudioStreamPacket&& packet) {
}
}
void Application::PlaySound(const std::string_view& sound) {
audio_service_.PlaySound(sound);
// 随机闹钟音乐列表 - 流行、经典、适合早晨的歌曲
static const std::vector<std::string> DEFAULT_ALARM_SONGS = {
"晴天", "七里香", "青花瓷", "稻香", "彩虹", "告白气球", "说好不哭",
"夜曲", "花海", "简单爱", "听妈妈的话", "东风破", "菊花台",
"起风了", "红豆", "好久不见", "匆匆那年", "老男孩", "那些年",
"小幸运", "成都", "南山南", "演员", "体面", "盗将行", "大鱼",
"新不了情", "月亮代表我的心", "甜蜜蜜", "邓丽君", "我只在乎你",
"友谊之光", "童年", "海阔天空", "光辉岁月", "真的爱你", "喜欢你",
"突然好想你", "情非得已", "温柔", "倔强", "知足", "三个傻瓜",
"恋爱循环", "千本樱", "打上花火", "lemon", "残酷天使的行动纲领",
"鸟笼", "", "青鸟", "closer", "sugar", "shape of you",
"despacito", "perfect", "happier", "someone like you"
};
// 获取随机闹钟音乐
static std::string GetRandomAlarmMusic() {
if (DEFAULT_ALARM_SONGS.empty()) {
return "";
}
// 使用当前时间作为随机种子
srand(esp_timer_get_time() / 1000000);
size_t index = rand() % DEFAULT_ALARM_SONGS.size();
return DEFAULT_ALARM_SONGS[index];
}
// 闹钟回调方法实现
void Application::OnAlarmTriggered(const AlarmItem& alarm) {
ESP_LOGI("Application", "Alarm triggered: %s at %02d:%02d",
alarm.label.c_str(), alarm.hour, alarm.minute);
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
auto music = board.GetMusic();
// 显示闹钟信息
std::string alarm_message = "🎵 闹钟";
if (!alarm.label.empty()) {
alarm_message += "\n" + alarm.label;
}
alarm_message += "\n" + AlarmManager::FormatTime(alarm.hour, alarm.minute);
// 优先在时钟界面显示(如果支持)
display->ShowAlarmOnIdleScreen(alarm_message.c_str());
// 同时设置聊天消息(作为备用)
display->SetChatMessage("system", alarm_message.c_str());
display->SetEmotion("music");
// 确定要播放的音乐
std::string music_to_play;
if (!alarm.music_name.empty()) {
// 使用用户指定的音乐
music_to_play = alarm.music_name;
ESP_LOGI("Application", "Playing user specified alarm music: %s", music_to_play.c_str());
} else {
// 随机选择一首默认闹钟音乐
music_to_play = GetRandomAlarmMusic();
ESP_LOGI("Application", "Playing random alarm music: %s", music_to_play.c_str());
}
// 播放音乐
if (music && !music_to_play.empty()) {
// 更新显示,显示正在播放的歌曲
std::string playing_message = "🎵 正在播放: " + music_to_play;
display->SetChatMessage("system", playing_message.c_str());
// 开始下载并播放音乐
if (music->Download(music_to_play)) {
ESP_LOGI("Application", "Successfully started alarm music: %s", music_to_play.c_str());
// 开始音乐进度跟踪
current_music_name_ = music_to_play;
music_start_time_ms_ = esp_timer_get_time() / 1000; // 转换为毫秒
is_music_playing_ = true;
// 尝试获取真实的歌曲长度
int real_duration = music->GetCurrentSongDurationSeconds();
if (real_duration > 0) {
music_duration_seconds_ = real_duration;
ESP_LOGI("Application", "Got real song duration: %d seconds", real_duration);
}
// 启动进度显示
display->SetMusicProgress(music_to_play.c_str(), 0, music_duration_seconds_, 0.0f);
} else {
ESP_LOGW("Application", "Failed to download alarm music: %s, using fallback", music_to_play.c_str());
// 如果下载失败,播放默认铃声
audio_service_.PlaySound(Lang::Sounds::OGG_VIBRATION);
}
} else {
ESP_LOGW("Application", "Music service not available or no music selected, using default alarm sound");
// 如果没有音乐功能或选择失败,播放默认铃声
audio_service_.PlaySound(Lang::Sounds::OGG_VIBRATION);
}
// 显示闹钟控制提示
std::string control_message = "🎵 说\"贪睡\"延后5分钟\"关闭闹钟\"停止音乐";
display->ShowNotification(control_message.c_str());
}
void Application::OnAlarmSnoozed(const AlarmItem& alarm) {
ESP_LOGI("Application", "Alarm snoozed: %s, count: %d/%d",
alarm.label.c_str(), alarm.snooze_count, alarm.max_snooze_count);
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
// 隐藏空闲屏幕上的闹钟信息
display->HideAlarmOnIdleScreen();
// 停止当前播放的音乐
auto music = board.GetMusic();
if (music) {
music->StopStreaming();
}
// 停止音乐进度跟踪并清除音乐界面
is_music_playing_ = false;
display->ClearMusicInfo();
std::string snooze_message = "💤 闹钟已贪睡 " + std::to_string(alarm.snooze_minutes) + " 分钟";
display->SetChatMessage("system", snooze_message.c_str());
display->SetEmotion("neutral");
audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS);
}
void Application::OnAlarmStopped(const AlarmItem& alarm) {
ESP_LOGI("Application", "Alarm stopped: %s", alarm.label.c_str());
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
// 隐藏空闲屏幕上的闹钟信息
display->HideAlarmOnIdleScreen();
// 停止当前播放的音乐
auto music = board.GetMusic();
if (music) {
music->StopStreaming();
}
// 停止音乐进度跟踪并清除音乐界面
is_music_playing_ = false;
display->ClearMusicInfo();
display->SetChatMessage("system", "✅ 闹钟已关闭");
display->SetEmotion("neutral");
// 显示下一个闹钟信息
auto& alarm_manager = AlarmManager::GetInstance();
std::string next_alarm_info = alarm_manager.GetNextAlarmInfo();
display->ShowNotification(next_alarm_info.c_str());
audio_service_.PlaySound(Lang::Sounds::OGG_SUCCESS);
}
// 获取默认闹钟音乐列表
std::vector<std::string> Application::GetDefaultAlarmMusicList() const {
return DEFAULT_ALARM_SONGS;
}
// 更新音乐播放进度
void Application::UpdateMusicProgress() {
if (!is_music_playing_) {
return;
}
auto& board = Board::GetInstance();
auto music = board.GetMusic();
auto display = board.GetDisplay();
if (!music || !display) {
return;
}
// 从音乐播放器获取真实的播放信息
int real_current_seconds = music->GetCurrentPlayTimeSeconds();
int real_duration_seconds = music->GetCurrentSongDurationSeconds();
float real_progress_percent = music->GetPlayProgress();
// 更新存储的歌曲长度(如果有变化)
if (real_duration_seconds > 0 && real_duration_seconds != music_duration_seconds_) {
music_duration_seconds_ = real_duration_seconds;
ESP_LOGI("Application", "Updated song duration: %d seconds", music_duration_seconds_);
}
// 检查是否播放结束
bool is_still_playing = music->IsDownloading() || (real_current_seconds < real_duration_seconds && real_current_seconds > 0);
if (!is_still_playing && real_current_seconds >= real_duration_seconds && real_duration_seconds > 0) {
is_music_playing_ = false; // 停止跟踪
ESP_LOGI("Application", "Music playback finished: %s (%d/%d seconds)",
current_music_name_.c_str(), real_current_seconds, real_duration_seconds);
// 🎵 音乐播放完毕,自动停止所有活跃的闹钟
auto& alarm_manager = AlarmManager::GetInstance();
auto active_alarms = alarm_manager.GetActiveAlarms();
if (!active_alarms.empty()) {
ESP_LOGI("Application", "Auto-stopping alarms after music finished");
// 停止闹钟
for (const auto& alarm : active_alarms) {
alarm_manager.StopAlarm(alarm.id);
}
// 在界面上显示用户确认消息
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
if (display) {
display->SetChatMessage("user", "我听到你的闹钟啦 ✅");
display->SetEmotion("happy");
}
}
}
// 更新显示(使用真实的播放时间)
if (is_music_playing_) {
display->SetMusicProgress(current_music_name_.c_str(),
real_current_seconds,
real_duration_seconds,
real_progress_percent);
ESP_LOGD("Application", "Music progress: %s - %d/%d seconds (%.1f%%)",
current_music_name_.c_str(), real_current_seconds, real_duration_seconds, real_progress_percent);
} else {
// 播放结束,清除界面
display->ClearMusicInfo();
}
}

View File

@@ -15,6 +15,7 @@
#include "ota.h"
#include "audio_service.h"
#include "device_state_event.h"
#include "alarm_manager.h"
#define MAIN_EVENT_SCHEDULE (1 << 0)
@@ -62,8 +63,13 @@ public:
void SetAecMode(AecMode mode);
AecMode GetAecMode() const { return aec_mode_; }
void PlaySound(const std::string_view& sound);
AudioService& GetAudioService() { return audio_service_; }
// 新增:接收外部音频数据(如音乐播放)
void AddAudioData(AudioStreamPacket&& packet);
AudioService& GetAudioService() { return audio_service_; }
// 闹钟功能
AlarmManager& GetAlarmManager() { return AlarmManager::GetInstance(); }
std::vector<std::string> GetDefaultAlarmMusicList() const;
private:
Application();
@@ -91,6 +97,18 @@ private:
void CheckAssetsVersion();
void ShowActivationCode(const std::string& code, const std::string& message);
void SetListeningMode(ListeningMode mode);
// 闹钟相关私有方法
void OnAlarmTriggered(const AlarmItem& alarm);
void OnAlarmSnoozed(const AlarmItem& alarm);
void OnAlarmStopped(const AlarmItem& alarm);
// 音乐进度跟踪相关
void UpdateMusicProgress();
std::string current_music_name_;
int64_t music_start_time_ms_ = 0; // 音乐开始播放的时间戳
int music_duration_seconds_ = 180; // 默认歌曲长度3分钟
bool is_music_playing_ = false;
};

View File

@@ -3,6 +3,7 @@
#include "display.h"
#include "application.h"
#include "lvgl_theme.h"
#include "emote_display.h"
#include <esp_log.h>
#include <spi_flash_mmap.h>
@@ -21,13 +22,7 @@ struct mmap_assets_table {
};
Assets::Assets(std::string default_assets_url) {
if (default_assets_url.find("http") == 0) {
default_assets_url_ = default_assets_url;
} else {
ESP_LOGE(TAG, "The default assets url is not a http url: %s", default_assets_url.c_str());
}
Assets::Assets() {
// Initialize the partition
InitializePartition();
}
@@ -113,6 +108,7 @@ bool Assets::Apply() {
ESP_LOGE(TAG, "The index.json file is not found");
return false;
}
cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
if (root == nullptr) {
ESP_LOGE(TAG, "The index.json file is not valid");
@@ -181,7 +177,8 @@ bool Assets::Apply() {
if (cJSON_IsObject(emoji)) {
cJSON* name = cJSON_GetObjectItem(emoji, "name");
cJSON* file = cJSON_GetObjectItem(emoji, "file");
if (cJSON_IsString(name) && cJSON_IsString(file)) {
cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf");
if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL== eaf)) {
if (!GetAssetData(file->valuestring, ptr, size)) {
ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring, file->valuestring);
continue;
@@ -243,7 +240,6 @@ bool Assets::Apply() {
}
}
}
#endif
auto display = Board::GetInstance().GetDisplay();
ESP_LOGI(TAG, "Refreshing display theme...");
@@ -252,6 +248,121 @@ bool Assets::Apply() {
if (current_theme != nullptr) {
display->SetTheme(current_theme);
}
#elif defined(CONFIG_USE_EMOTE_MESSAGE_STYLE)
auto &board = Board::GetInstance();
auto display = board.GetDisplay();
auto emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
cJSON* font = cJSON_GetObjectItem(root, "text_font");
if (cJSON_IsString(font)) {
std::string fonts_text_file = font->valuestring;
if (GetAssetData(fonts_text_file, ptr, size)) {
auto text_font = std::make_shared<LvglCBinFont>(ptr);
if (text_font->font() == nullptr) {
ESP_LOGE(TAG, "Failed to load fonts.bin");
return false;
}
if (emote_display) {
emote_display->AddTextFont(text_font);
}
} else {
ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
}
}
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
if (cJSON_IsArray(emoji_collection)) {
int emoji_count = cJSON_GetArraySize(emoji_collection);
if (emote_display) {
for (int i = 0; i < emoji_count; i++) {
cJSON* icon = cJSON_GetArrayItem(emoji_collection, i);
if (cJSON_IsObject(icon)) {
cJSON* name = cJSON_GetObjectItem(icon, "name");
cJSON* file = cJSON_GetObjectItem(icon, "file");
if (cJSON_IsString(name) && cJSON_IsString(file)) {
if (GetAssetData(file->valuestring, ptr, size)) {
cJSON* eaf = cJSON_GetObjectItem(icon, "eaf");
bool lack_value = false;
bool loop_value = false;
int fps_value = 0;
if (cJSON_IsObject(eaf)) {
cJSON* lack = cJSON_GetObjectItem(eaf, "lack");
cJSON* loop = cJSON_GetObjectItem(eaf, "loop");
cJSON* fps = cJSON_GetObjectItem(eaf, "fps");
lack_value = lack ? cJSON_IsTrue(lack) : false;
loop_value = loop ? cJSON_IsTrue(loop) : false;
fps_value = fps ? fps->valueint : 0;
emote_display->AddEmojiData(name->valuestring, ptr, size,
static_cast<uint8_t>(fps_value),
loop_value, lack_value);
}
} else {
ESP_LOGE(TAG, "Emoji \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
}
}
}
}
}
}
cJSON* icon_collection = cJSON_GetObjectItem(root, "icon_collection");
if (cJSON_IsArray(icon_collection)) {
if (emote_display) {
int icon_count = cJSON_GetArraySize(icon_collection);
for (int i = 0; i < icon_count; i++) {
cJSON* icon = cJSON_GetArrayItem(icon_collection, i);
if (cJSON_IsObject(icon)) {
cJSON* name = cJSON_GetObjectItem(icon, "name");
cJSON* file = cJSON_GetObjectItem(icon, "file");
if (cJSON_IsString(name) && cJSON_IsString(file)) {
if (GetAssetData(file->valuestring, ptr, size)) {
emote_display->AddIconData(name->valuestring, ptr, size);
} else {
ESP_LOGE(TAG, "Icon \"%10s\" image file %s is not found", name->valuestring, file->valuestring);
}
}
}
}
}
}
cJSON* layout_json = cJSON_GetObjectItem(root, "layout");
if (cJSON_IsArray(layout_json)) {
int layout_count = cJSON_GetArraySize(layout_json);
for (int i = 0; i < layout_count; i++) {
cJSON* layout_item = cJSON_GetArrayItem(layout_json, i);
if (cJSON_IsObject(layout_item)) {
cJSON* name = cJSON_GetObjectItem(layout_item, "name");
cJSON* align = cJSON_GetObjectItem(layout_item, "align");
cJSON* x = cJSON_GetObjectItem(layout_item, "x");
cJSON* y = cJSON_GetObjectItem(layout_item, "y");
cJSON* width = cJSON_GetObjectItem(layout_item, "width");
cJSON* height = cJSON_GetObjectItem(layout_item, "height");
if (cJSON_IsString(name) && cJSON_IsString(align) && cJSON_IsNumber(x) && cJSON_IsNumber(y)) {
int width_val = cJSON_IsNumber(width) ? width->valueint : 0;
int height_val = cJSON_IsNumber(height) ? height->valueint : 0;
if (emote_display) {
emote_display->AddLayoutData(name->valuestring, align->valuestring,
x->valueint, y->valueint, width_val, height_val);
}
} else {
ESP_LOGW(TAG, "Invalid layout item %d: missing required fields", i);
}
}
}
}
#endif
cJSON_Delete(root);
return true;
}

View File

@@ -10,23 +10,6 @@
#include <model_path.h>
// All combinations of wakenet_model, text_font, emoji_collection can be found from the following url:
// https://github.com/78/xiaozhi-fonts/releases/tag/assets
#define ASSETS_PUHUI_COMMON_14_1 "none-font_puhui_common_14_1-none.bin"
#define ASSETS_XIAOZHI_WAKENET "wn9_nihaoxiaozhi_tts-none-none.bin"
#define ASSETS_XIAOZHI_WAKENET_SMALL "wn9s_nihaoxiaozhi-none-none.bin"
#define ASSETS_XIAOZHI_PUHUI_COMMON_14_1 "wn9_nihaoxiaozhi_tts-font_puhui_common_14_1-none.bin"
#define ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_32 "wn9_nihaoxiaozhi_tts-font_puhui_common_16_4-emojis_32.bin"
#define ASSETS_XIAOZHI_PUHUI_COMMON_16_4_EMOJI_64 "wn9_nihaoxiaozhi_tts-font_puhui_common_16_4-emojis_64.bin"
#define ASSETS_XIAOZHI_PUHUI_COMMON_20_4_EMOJI_64 "wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-emojis_64.bin"
#define ASSETS_XIAOZHI_PUHUI_COMMON_30_4_EMOJI_64 "wn9_nihaoxiaozhi_tts-font_puhui_common_30_4-emojis_64.bin"
#define ASSETS_XIAOZHI_S_PUHUI_COMMON_14_1 "wn9s_nihaoxiaozhi-font_puhui_common_14_1-none.bin"
#define ASSETS_XIAOZHI_S_PUHUI_COMMON_16_4_EMOJI_32 "wn9s_nihaoxiaozhi-font_puhui_common_16_4-emojis_32.bin"
#define ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_32 "wn9s_nihaoxiaozhi-font_puhui_common_20_4-emojis_32.bin"
#define ASSETS_XIAOZHI_S_PUHUI_COMMON_20_4_EMOJI_64 "wn9s_nihaoxiaozhi-font_puhui_common_20_4-emojis_64.bin"
#define ASSETS_XIAOZHI_S_PUHUI_COMMON_30_4_EMOJI_64 "wn9s_nihaoxiaozhi-font_puhui_common_30_4-emojis_64.bin"
struct Asset {
size_t size;
size_t offset;
@@ -34,23 +17,27 @@ struct Asset {
class Assets {
public:
Assets(std::string default_assets_url);
static Assets& GetInstance() {
static Assets instance;
return instance;
}
~Assets();
bool Download(std::string url, std::function<void(int progress, size_t speed)> progress_callback);
bool Apply();
bool GetAssetData(const std::string& name, void*& ptr, size_t& size);
inline bool partition_valid() const { return partition_valid_; }
inline bool checksum_valid() const { return checksum_valid_; }
inline std::string default_assets_url() const { return default_assets_url_; }
private:
Assets();
Assets(const Assets&) = delete;
Assets& operator=(const Assets&) = delete;
bool InitializePartition();
uint32_t CalculateChecksum(const char* data, uint32_t length);
bool GetAssetData(const std::string& name, void*& ptr, size_t& size);
const esp_partition_t* partition_ = nullptr;
esp_partition_mmap_handle_t mmap_handle_ = 0;

View File

@@ -1,68 +1,68 @@
// Auto-generated language config
// Language: zh-CN with en-US fallback
// Language: en-US with en-US fallback
#pragma once
#include <string_view>
#ifndef zh_cn
#define zh_cn // 預設語言
#ifndef en_us
#define en_us // 預設語言
#endif
namespace Lang {
// 语言元数据
constexpr const char* CODE = "zh-CN";
constexpr const char* CODE = "en-US";
// 字符串资源 (en-US as fallback for missing keys)
namespace Strings {
constexpr const char* ACCESS_VIA_BROWSER = ",浏览器访问 ";
constexpr const char* ACTIVATION = "激活设备";
constexpr const char* BATTERY_CHARGING = "正在充电";
constexpr const char* BATTERY_FULL = "电量已满";
constexpr const char* BATTERY_LOW = "电量不足";
constexpr const char* BATTERY_NEED_CHARGE = "电量低,请充电";
constexpr const char* CHECKING_NEW_VERSION = "检查新版本...";
constexpr const char* CHECK_NEW_VERSION_FAILED = "检查新版本失败,将在 %d 秒后重试:%s";
constexpr const char* CONNECTED_TO = "已连接 ";
constexpr const char* CONNECTING = "连接中...";
constexpr const char* ACCESS_VIA_BROWSER = " Config URL: ";
constexpr const char* ACTIVATION = "Activation";
constexpr const char* BATTERY_CHARGING = "Charging";
constexpr const char* BATTERY_FULL = "Battery full";
constexpr const char* BATTERY_LOW = "Low battery";
constexpr const char* BATTERY_NEED_CHARGE = "Low battery, please charge";
constexpr const char* CHECKING_NEW_VERSION = "Checking for new version...";
constexpr const char* CHECK_NEW_VERSION_FAILED = "Check for new version failed, will retry in %d seconds: %s";
constexpr const char* CONNECTED_TO = "Connected to ";
constexpr const char* CONNECTING = "Connecting...";
constexpr const char* CONNECTION_SUCCESSFUL = "Connection Successful";
constexpr const char* CONNECT_TO = "连接 ";
constexpr const char* CONNECT_TO_HOTSPOT = "手机连接热点 ";
constexpr const char* DETECTING_MODULE = "检测模组...";
constexpr const char* DOWNLOAD_ASSETS_FAILED = "下载资源失败";
constexpr const char* ENTERING_WIFI_CONFIG_MODE = "进入配网模式...";
constexpr const char* ERROR = "错误";
constexpr const char* FOUND_NEW_ASSETS = "发现新资源: %s";
constexpr const char* HELLO_MY_FRIEND = "你好,我的朋友!";
constexpr const char* INFO = "信息";
constexpr const char* INITIALIZING = "正在初始化...";
constexpr const char* LISTENING = "聆听中...";
constexpr const char* LOADING_ASSETS = "加载资源...";
constexpr const char* LOADING_PROTOCOL = "登录服务器...";
constexpr const char* MAX_VOLUME = "最大音量";
constexpr const char* MUTED = "已静音";
constexpr const char* NEW_VERSION = "新版本 ";
constexpr const char* OTA_UPGRADE = "OTA 升级";
constexpr const char* PIN_ERROR = "请插入 SIM ";
constexpr const char* PLEASE_WAIT = "请稍候...";
constexpr const char* REGISTERING_NETWORK = "等待网络...";
constexpr const char* REG_ERROR = "无法接入网络,请检查流量卡状态";
constexpr const char* RTC_MODE_OFF = "AEC 关闭";
constexpr const char* RTC_MODE_ON = "AEC 开启";
constexpr const char* SCANNING_WIFI = "扫描 Wi-Fi...";
constexpr const char* SERVER_ERROR = "发送失败,请检查网络";
constexpr const char* SERVER_NOT_CONNECTED = "无法连接服务,请稍后再试";
constexpr const char* SERVER_NOT_FOUND = "正在寻找可用服务";
constexpr const char* SERVER_TIMEOUT = "等待响应超时";
constexpr const char* SPEAKING = "说话中...";
constexpr const char* STANDBY = "待命";
constexpr const char* SWITCH_TO_4G_NETWORK = "切换到 4G...";
constexpr const char* SWITCH_TO_WIFI_NETWORK = "切换到 Wi-Fi...";
constexpr const char* UPGRADE_FAILED = "升级失败";
constexpr const char* UPGRADING = "正在升级系统...";
constexpr const char* VERSION = "版本 ";
constexpr const char* VOLUME = "音量 ";
constexpr const char* WARNING = "警告";
constexpr const char* WIFI_CONFIG_MODE = "配网模式";
constexpr const char* CONNECT_TO = "Connect to ";
constexpr const char* CONNECT_TO_HOTSPOT = "Hotspot: ";
constexpr const char* DETECTING_MODULE = "Detecting module...";
constexpr const char* DOWNLOAD_ASSETS_FAILED = "Failed to download assets";
constexpr const char* ENTERING_WIFI_CONFIG_MODE = "Entering Wi-Fi configuration mode...";
constexpr const char* ERROR = "Error";
constexpr const char* FOUND_NEW_ASSETS = "Found new assets: %s";
constexpr const char* HELLO_MY_FRIEND = "Hello, my friend!";
constexpr const char* INFO = "Information";
constexpr const char* INITIALIZING = "Initializing...";
constexpr const char* LISTENING = "Listening...";
constexpr const char* LOADING_ASSETS = "Loading assets...";
constexpr const char* LOADING_PROTOCOL = "Logging in...";
constexpr const char* MAX_VOLUME = "Max volume";
constexpr const char* MUTED = "Muted";
constexpr const char* NEW_VERSION = "New version ";
constexpr const char* OTA_UPGRADE = "OTA Upgrade";
constexpr const char* PIN_ERROR = "Please insert SIM card";
constexpr const char* PLEASE_WAIT = "Please wait...";
constexpr const char* REGISTERING_NETWORK = "Waiting for network...";
constexpr const char* REG_ERROR = "Unable to access network, please check SIM card status";
constexpr const char* RTC_MODE_OFF = "AEC Off";
constexpr const char* RTC_MODE_ON = "AEC On";
constexpr const char* SCANNING_WIFI = "Scanning Wi-Fi...";
constexpr const char* SERVER_ERROR = "Sending failed, please check the network";
constexpr const char* SERVER_NOT_CONNECTED = "Unable to connect to service, please try again later";
constexpr const char* SERVER_NOT_FOUND = "Looking for available service";
constexpr const char* SERVER_TIMEOUT = "Waiting for response timeout";
constexpr const char* SPEAKING = "Speaking...";
constexpr const char* STANDBY = "Standby";
constexpr const char* SWITCH_TO_4G_NETWORK = "Switching to 4G...";
constexpr const char* SWITCH_TO_WIFI_NETWORK = "Switching to Wi-Fi...";
constexpr const char* UPGRADE_FAILED = "Upgrade failed";
constexpr const char* UPGRADING = "System is upgrading...";
constexpr const char* VERSION = "Ver ";
constexpr const char* VOLUME = "Volume ";
constexpr const char* WARNING = "Warning";
constexpr const char* WIFI_CONFIG_MODE = "Wi-Fi Configuration Mode";
}
// 音效资源 (en-US as fallback for missing audio files)

View File

@@ -33,30 +33,17 @@ void AudioCodec::Start() {
ESP_LOGW(TAG, "Output volume value (%d) is too small, setting to default (10)", output_volume_);
output_volume_ = 10;
}
// 保存原始输出采样率
if (original_output_sample_rate_ == 0) {
if (original_output_sample_rate_ == 0){
original_output_sample_rate_ = output_sample_rate_;
ESP_LOGI(TAG, "Saved original output sample rate: %d Hz", original_output_sample_rate_);
}
if (tx_handle_ != nullptr) {
esp_err_t err = i2s_channel_enable(tx_handle_);
if (err == ESP_ERR_INVALID_STATE) {
// 已经启用,忽略
ESP_LOGW(TAG, "TX channel already enabled");
} else {
ESP_ERROR_CHECK(err);
}
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
}
if (rx_handle_ != nullptr) {
esp_err_t err = i2s_channel_enable(rx_handle_);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG, "RX channel already enabled");
} else {
ESP_ERROR_CHECK(err);
}
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));
}
EnableInput(true);

View File

@@ -8,12 +8,11 @@
#include "processors/no_audio_processor.h"
#endif
#if CONFIG_USE_AFE_WAKE_WORD
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4
#include "wake_words/afe_wake_word.h"
#elif CONFIG_USE_ESP_WAKE_WORD
#include "wake_words/esp_wake_word.h"
#elif CONFIG_USE_CUSTOM_WAKE_WORD
#include "wake_words/custom_wake_word.h"
#else
#include "wake_words/esp_wake_word.h"
#endif
#define TAG "AudioService"
@@ -50,16 +49,6 @@ void AudioService::Initialize(AudioCodec* codec) {
audio_processor_ = std::make_unique<NoAudioProcessor>();
#endif
#if CONFIG_USE_AFE_WAKE_WORD
wake_word_ = std::make_unique<AfeWakeWord>();
#elif CONFIG_USE_ESP_WAKE_WORD
wake_word_ = std::make_unique<EspWakeWord>();
#elif CONFIG_USE_CUSTOM_WAKE_WORD
wake_word_ = std::make_unique<CustomWakeWord>();
#else
wake_word_ = nullptr;
#endif
audio_processor_->OnOutput([this](std::vector<int16_t>&& data) {
PushTaskToEncodeQueue(kAudioTaskTypeEncodeToSendQueue, std::move(data));
});
@@ -71,14 +60,6 @@ void AudioService::Initialize(AudioCodec* codec) {
}
});
if (wake_word_) {
wake_word_->OnWakeWordDetected([this](const std::string& wake_word) {
if (callbacks_.on_wake_word_detected) {
callbacks_.on_wake_word_detected(wake_word);
}
});
}
esp_timer_create_args_t audio_power_timer_args = {
.callback = [](void* arg) {
AudioService* audio_service = (AudioService*)arg;
@@ -104,7 +85,7 @@ void AudioService::Start() {
AudioService* audio_service = (AudioService*)arg;
audio_service->AudioInputTask();
vTaskDelete(NULL);
}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_, 1);
}, "audio_input", 2048 * 3, this, 8, &audio_input_task_handle_, 0);
/* Start the audio output task */
xTaskCreate([](void* arg) {
@@ -670,8 +651,39 @@ void AudioService::CheckAndUpdateAudioPowerState() {
void AudioService::SetModelsList(srmodel_list_t* models_list) {
models_list_ = models_list;
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4
if (esp_srmodel_filter(models_list_, ESP_MN_PREFIX, NULL) != nullptr) {
wake_word_ = std::make_unique<CustomWakeWord>();
} else if (esp_srmodel_filter(models_list_, ESP_WN_PREFIX, NULL) != nullptr) {
wake_word_ = std::make_unique<AfeWakeWord>();
} else {
wake_word_ = nullptr;
}
#else
if (esp_srmodel_filter(models_list_, ESP_WN_PREFIX, NULL) != nullptr) {
wake_word_ = std::make_unique<EspWakeWord>();
} else {
wake_word_ = nullptr;
}
#endif
if (wake_word_) {
wake_word_->OnWakeWordDetected([this](const std::string& wake_word) {
if (callbacks_.on_wake_word_detected) {
callbacks_.on_wake_word_detected(wake_word);
}
});
}
}
bool AudioService::IsAfeWakeWord() {
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4
return wake_word_ != nullptr && dynamic_cast<AfeWakeWord*>(wake_word_.get()) != nullptr;
#else
return false;
#endif
}
void AudioService::UpdateOutputTimestamp() {
last_output_time_ = std::chrono::steady_clock::now();

View File

@@ -94,6 +94,7 @@ public:
bool IsIdle();
bool IsWakeWordRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_WAKE_WORD_RUNNING; }
bool IsAudioProcessorRunning() const { return xEventGroupGetBits(event_group_) & AS_EVENT_AUDIO_PROCESSOR_RUNNING; }
bool IsAfeWakeWord();
void EnableWakeWordDetection(bool enable);
void EnableVoiceProcessing(bool enable);
@@ -109,6 +110,7 @@ public:
void ResetDecoder();
void SetModelsList(srmodel_list_t* models_list);
void UpdateOutputTimestamp();
private:
AudioCodec* codec_ = nullptr;
AudioServiceCallbacks callbacks_;

View File

@@ -105,8 +105,8 @@ NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sampl
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
.slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_BOTH,
.slot_mode = I2S_SLOT_MODE_MONO,
.slot_mask = I2S_STD_SLOT_LEFT,
.ws_width = I2S_DATA_BIT_WIDTH_32BIT,
.ws_pol = false,
.bit_shift = true,
@@ -136,9 +136,6 @@ NoAudioCodecSimplex::NoAudioCodecSimplex(int input_sample_rate, int output_sampl
chan_cfg.id = (i2s_port_t)1;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, nullptr, &rx_handle_));
std_cfg.clk_cfg.sample_rate_hz = (uint32_t)input_sample_rate_;
// RX 使用单声道 LEFT
std_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO;
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
std_cfg.gpio_cfg.bclk = mic_sck;
std_cfg.gpio_cfg.ws = mic_ws;
std_cfg.gpio_cfg.dout = I2S_GPIO_UNUSED;
@@ -361,30 +358,25 @@ NoAudioCodecSimplexPdm::NoAudioCodecSimplexPdm(int input_sample_rate, int output
int NoAudioCodec::Write(const int16_t* data, int samples) {
std::lock_guard<std::mutex> lock(data_if_mutex_);
// 立体声交织输出L,R,L,R ... 每声道32位
std::vector<int32_t> buffer(samples * 2);
std::vector<int32_t> buffer(samples);
// output_volume_: 0-100
// volume_factor_: 0-65536
int32_t volume_factor = pow(double(output_volume_) / 100.0, 2) * 65536;
for (int i = 0; i < samples; i++) {
int64_t temp = int64_t(data[i]) * volume_factor; // 使用 int64_t 进行乘法运算
int32_t s32;
if (temp > INT32_MAX) {
s32 = INT32_MAX;
buffer[i] = INT32_MAX;
} else if (temp < INT32_MIN) {
s32 = INT32_MIN;
buffer[i] = INT32_MIN;
} else {
s32 = static_cast<int32_t>(temp);
buffer[i] = static_cast<int32_t>(temp);
}
// 交织到左右声道
buffer[2 * i] = s32; // Left
buffer[2 * i + 1] = s32; // Right复制
}
size_t bytes_written;
ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), (samples * 2) * sizeof(int32_t), &bytes_written, portMAX_DELAY));
return bytes_written / sizeof(int32_t) / 2; // 返回每声道样本数
ESP_ERROR_CHECK(i2s_channel_write(tx_handle_, buffer.data(), samples * sizeof(int32_t), &bytes_written, portMAX_DELAY));
return bytes_written / sizeof(int32_t);
}
int NoAudioCodec::Read(int16_t* dest, int samples) {

View File

@@ -53,8 +53,6 @@ void AfeAudioProcessor::Initialize(AudioCodec* codec, int frame_duration_ms, srm
afe_config->ns_init = false;
}
afe_config->afe_perferred_core = 1;
afe_config->afe_perferred_priority = 1;
afe_config->agc_init = false;
afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM;

View File

@@ -1,11 +1,13 @@
#include "custom_wake_word.h"
#include "audio_service.h"
#include "system_info.h"
#include "assets.h"
#include <esp_log.h>
#include "esp_mn_iface.h"
#include "esp_mn_models.h"
#include "esp_mn_speech_commands.h"
#include <esp_mn_iface.h>
#include <esp_mn_models.h>
#include <esp_mn_speech_commands.h>
#include <cJSON.h>
#define TAG "CustomWakeWord"
@@ -34,13 +36,68 @@ CustomWakeWord::~CustomWakeWord() {
}
}
void CustomWakeWord::ParseWakenetModelConfig() {
// Read index.json
auto& assets = Assets::GetInstance();
void* ptr = nullptr;
size_t size = 0;
if (!assets.GetAssetData("index.json", ptr, size)) {
ESP_LOGE(TAG, "Failed to read index.json");
return;
}
cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
if (root == nullptr) {
ESP_LOGE(TAG, "Failed to parse index.json");
return;
}
cJSON* multinet_model = cJSON_GetObjectItem(root, "multinet_model");
if (cJSON_IsObject(multinet_model)) {
cJSON* language = cJSON_GetObjectItem(multinet_model, "language");
cJSON* duration = cJSON_GetObjectItem(multinet_model, "duration");
cJSON* threshold = cJSON_GetObjectItem(multinet_model, "threshold");
cJSON* commands = cJSON_GetObjectItem(multinet_model, "commands");
if (cJSON_IsString(language)) {
language_ = language->valuestring;
}
if (cJSON_IsNumber(duration)) {
duration_ = duration->valueint;
}
if (cJSON_IsNumber(threshold)) {
threshold_ = threshold->valuedouble;
}
if (cJSON_IsArray(commands)) {
for (int i = 0; i < cJSON_GetArraySize(commands); i++) {
cJSON* command = cJSON_GetArrayItem(commands, i);
if (cJSON_IsObject(command)) {
cJSON* command_name = cJSON_GetObjectItem(command, "command");
cJSON* text = cJSON_GetObjectItem(command, "text");
cJSON* action = cJSON_GetObjectItem(command, "action");
if (cJSON_IsString(command_name) && cJSON_IsString(text) && cJSON_IsString(action)) {
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);
}
}
}
}
}
cJSON_Delete(root);
}
bool CustomWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list) {
codec_ = codec;
commands_.clear();
if (models_list == nullptr) {
language_ = "cn";
models_ = esp_srmodel_init("model");
#ifdef CONFIG_CUSTOM_WAKE_WORD
threshold_ = CONFIG_CUSTOM_WAKE_WORD_THRESHOLD / 100.0f;
commands_.push_back({CONFIG_CUSTOM_WAKE_WORD, CONFIG_CUSTOM_WAKE_WORD_DISPLAY, "wake"});
#endif
} else {
models_ = models_list;
ParseWakenetModelConfig();
}
if (models_ == nullptr || models_->num == -1) {
@@ -49,19 +106,20 @@ bool CustomWakeWord::Initialize(AudioCodec* codec, srmodel_list_t* models_list)
}
// 初始化 multinet (命令词识别)
mn_name_ = esp_srmodel_filter(models_, ESP_MN_PREFIX, ESP_MN_CHINESE);
mn_name_ = esp_srmodel_filter(models_, ESP_MN_PREFIX, language_.c_str());
if (mn_name_ == nullptr) {
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");
return false;
}
ESP_LOGI(TAG, "multinet: %s", mn_name_);
multinet_ = esp_mn_handle_from_name(mn_name_);
multinet_model_data_ = multinet_->create(mn_name_, 3000); // 3 秒超时
multinet_->set_det_threshold(multinet_model_data_, CONFIG_CUSTOM_WAKE_WORD_THRESHOLD / 100.0f);
multinet_model_data_ = multinet_->create(mn_name_, duration_);
multinet_->set_det_threshold(multinet_model_data_, threshold_);
esp_mn_commands_clear();
esp_mn_commands_add(1, CONFIG_CUSTOM_WAKE_WORD);
for (int i = 0; i < commands_.size(); i++) {
esp_mn_commands_add(i + 1, commands_[i].command.c_str());
}
esp_mn_commands_update();
multinet_->print_active_speech_commands(multinet_model_data_);
@@ -104,16 +162,18 @@ void CustomWakeWord::Feed(const std::vector<int16_t>& data) {
return;
} else if (mn_state == ESP_MN_STATE_DETECTED) {
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]);
for (int i = 0; i < mn_result->num && running_; i++) {
ESP_LOGI(TAG, "Custom wake word detected: command_id=%d, string=%s, prob=%f",
mn_result->command_id[i], mn_result->string, mn_result->prob[i]);
auto& command = commands_[mn_result->command_id[i] - 1];
if (command.action == "wake") {
last_detected_wake_word_ = command.text;
running_ = false;
if (mn_result->command_id[0] == 1) {
last_detected_wake_word_ = CONFIG_CUSTOM_WAKE_WORD_DISPLAY;
}
running_ = false;
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
if (wake_word_detected_callback_) {
wake_word_detected_callback_(last_detected_wake_word_);
}
}
}
multinet_->clean(multinet_model_data_);
} else if (mn_state == ESP_MN_STATE_TIMEOUT) {

View File

@@ -33,11 +33,21 @@ public:
const std::string& GetLastDetectedWakeWord() const { return last_detected_wake_word_; }
private:
struct Command {
std::string command;
std::string text;
std::string action;
};
// multinet 相关成员变量
esp_mn_iface_t* multinet_ = nullptr;
model_iface_data_t* multinet_model_data_ = nullptr;
srmodel_list_t *models_ = nullptr;
char* mn_name_ = nullptr;
std::string language_ = "cn";
int duration_ = 3000;
float threshold_ = 0.2;
std::deque<Command> commands_;
std::function<void(const std::string& wake_word)> wake_word_detected_callback_;
AudioCodec* codec_ = nullptr;
@@ -53,6 +63,7 @@ private:
std::condition_variable wake_word_cv_;
void StoreWakeWordData(const std::vector<int16_t>& data);
void ParseWakenetModelConfig();
};
#endif

View File

@@ -18,18 +18,6 @@ idf.py menuconfig
Xiaozhi Assistant -> Board Type -> AtomMatrix + Echo Base
```
**修改 flash 大小:**
```
Serial flasher config -> Flash size -> 4 MB
```
**修改分区表:**
```
Partition Table -> Custom partition CSV file -> partitions/v1/4m.csv
```
**编译:**
```bash

View File

@@ -4,8 +4,6 @@
{
"name": "atommatrix-echo-base",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m.csv\""
]
}
]

View File

@@ -5,7 +5,6 @@
"name": "atoms3-echo-base",
"sdkconfig_append": [
"CONFIG_SPIRAM=n",
"CONFIG_USE_AFE=n",
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
]

View File

@@ -34,6 +34,8 @@
#define ASR_BUTTON_GPIO GPIO_NUM_19
#define BUILTIN_LED_GPIO GPIO_NUM_2
#define ML307_RX_PIN GPIO_NUM_16
#define ML307_TX_PIN GPIO_NUM_17
#ifdef CONFIG_LCD_ST7789_240X240_7PIN
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_22

View File

@@ -4,8 +4,6 @@
{
"name": "bread-compact-esp32-lcd",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m.csv\"",
"LCD_ST7789_240X240_7PIN=y"
]
}

View File

@@ -1,4 +1,4 @@
#include "wifi_board.h"
#include "dual_network_board.h"
#include "codecs/no_audio_codec.h"
#include "display/lcd_display.h"
#include "system_reset.h"
@@ -58,7 +58,7 @@ static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = {
#define TAG "ESP32-LCD-MarsbearSupport"
class CompactWifiBoardLCD : public WifiBoard {
class CompactWifiBoardLCD : public DualNetworkBoard {
private:
Button boot_button_;
Button touch_button_;
@@ -137,13 +137,25 @@ private:
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
// cast to WifiBoard
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.ResetWifiConfiguration();
}
}
gpio_set_level(BUILTIN_LED_GPIO, 1);
app.ToggleChatState();
});
boot_button_.OnDoubleClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
SwitchNetworkType();
}
});
asr_button_.OnClick([this]() {
std::string wake_word="你好小智";
Application::GetInstance().WakeWordInvoke(wake_word);
@@ -163,6 +175,7 @@ private:
public:
CompactWifiBoardLCD() :
DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN),
boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO) {
InitializeSpi();
InitializeLcdDisplay();

View File

@@ -18,18 +18,6 @@ idf.py menuconfig
Xiaozhi Assistant -> Board Type -> 面包板 ESP32 DevKit
```
**修改 flash 大小:**
```
Serial flasher config -> Flash size -> 4 MB
```
**修改分区表:**
```
Partition Table -> Custom partition CSV file -> partitions/v1/4m.csv
```
**编译:**
```bash

View File

@@ -33,6 +33,9 @@
#define ASR_BUTTON_GPIO GPIO_NUM_19
#define BUILTIN_LED_GPIO GPIO_NUM_2
#define ML307_RX_PIN GPIO_NUM_16
#define ML307_TX_PIN GPIO_NUM_17
#define DISPLAY_SDA_PIN GPIO_NUM_4
#define DISPLAY_SCL_PIN GPIO_NUM_15
#define DISPLAY_WIDTH 128

View File

@@ -4,16 +4,12 @@
{
"name": "bread-compact-esp32",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m.csv\"",
"CONFIG_OLED_SSD1306_128X64=y"
]
},
{
"name": "bread-compact-esp32-128x32",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m.csv\"",
"CONFIG_OLED_SSD1306_128X32=y"
]
}

View File

@@ -1,4 +1,4 @@
#include "wifi_board.h"
#include "dual_network_board.h"
#include "codecs/no_audio_codec.h"
#include "system_reset.h"
#include "application.h"
@@ -17,7 +17,7 @@
#define TAG "ESP32-MarsbearSupport"
class CompactWifiBoard : public WifiBoard {
class CompactWifiBoard : public DualNetworkBoard {
private:
Button boot_button_;
Button touch_button_;
@@ -105,13 +105,25 @@ private:
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
if (GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
// cast to WifiBoard
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.ResetWifiConfiguration();
}
}
gpio_set_level(BUILTIN_LED_GPIO, 1);
app.ToggleChatState();
});
boot_button_.OnDoubleClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
SwitchNetworkType();
}
});
asr_button_.OnClick([this]() {
std::string wake_word="你好小智";
Application::GetInstance().WakeWordInvoke(wake_word);
@@ -133,7 +145,7 @@ private:
}
public:
CompactWifiBoard() : boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO)
CompactWifiBoard() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN), boot_button_(BOOT_BUTTON_GPIO), touch_button_(TOUCH_BUTTON_GPIO), asr_button_(ASR_BUTTON_GPIO)
{
InitializeDisplayI2c();
InitializeSsd1306Display();

View File

@@ -4,19 +4,16 @@
#include "display/display.h"
#include "display/oled_display.h"
#include "assets/lang_config.h"
#include "esp32_music.h"
#include <esp_log.h>
#include <esp_ota_ops.h>
#include <esp_chip_info.h>
#include <esp_random.h>
#include "esp32_music.h"
#define TAG "Board"
Board::Board() {
music_ = nullptr; // 先初始化为空指针
Settings settings("board", true);
uuid_ = settings.GetString("uuid");
if (uuid_.empty()) {
@@ -29,7 +26,6 @@ Board::Board() {
music_ = new Esp32Music();
ESP_LOGI(TAG, "Music player initialized for all boards");
}
Board::~Board() {
if (music_) {
delete music_;
@@ -37,7 +33,6 @@ Board::~Board() {
ESP_LOGI(TAG, "Music player destroyed");
}
}
std::string Board::GenerateUuid() {
// UUID v4 需要 16 字节的随机数据
uint8_t uuid[16];
@@ -78,14 +73,13 @@ Camera* Board::GetCamera() {
return nullptr;
}
Music* Board::GetMusic() {
return music_;
}
Led* Board::GetLed() {
static NoLed led;
return &led;
}
Music* Board::GetMusic() {
return music_;
}
std::string Board::GetSystemInfoJson() {
/*
@@ -196,12 +190,3 @@ std::string Board::GetSystemInfoJson() {
json += R"(})";
return json;
}
Assets* Board::GetAssets() {
#ifdef DEFAULT_ASSETS
static Assets assets(DEFAULT_ASSETS);
return &assets;
#else
return nullptr;
#endif
}

View File

@@ -12,10 +12,8 @@
#include "backlight.h"
#include "camera.h"
#include "assets.h"
#include "music.h"
void* create_board();
class AudioCodec;
class Display;
@@ -45,11 +43,11 @@ public:
virtual std::string GetUuid() { return uuid_; }
virtual Backlight* GetBacklight() { return nullptr; }
virtual Led* GetLed();
virtual Music* GetMusic();
virtual AudioCodec* GetAudioCodec() = 0;
virtual bool GetTemperature(float& esp32temp);
virtual Display* GetDisplay();
virtual Camera* GetCamera();
virtual Music* GetMusic();
virtual NetworkInterface* GetNetwork() = 0;
virtual void StartNetwork() = 0;
virtual const char* GetNetworkStateIcon() = 0;
@@ -58,7 +56,6 @@ public:
virtual void SetPowerSaveMode(bool enabled) = 0;
virtual std::string GetBoardJson() = 0;
virtual std::string GetDeviceStatusJson() = 0;
virtual Assets* GetAssets();
};
#define DECLARE_BOARD(BOARD_CLASS_NAME) \

View File

@@ -4,10 +4,10 @@
#include "board.h"
#include "system_info.h"
#include "lvgl_display.h"
#include "jpg/image_to_jpeg.h"
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <img_converters.h>
#include <cstring>
#define TAG "Esp32Camera"
@@ -152,9 +152,10 @@ std::string Esp32Camera::Explain(const std::string& question) {
throw std::runtime_error("Failed to create JPEG queue");
}
// We spawn a thread to encode the image to JPEG
// We spawn a thread to encode the image to JPEG using optimized encoder (cost about 500ms and 8KB SRAM)
encoder_thread_ = std::thread([this, jpeg_queue]() {
frame2jpg_cb(fb_, 80, [](void* arg, size_t index, const void* data, size_t len) -> unsigned int {
image_to_jpeg_cb(fb_->buf, fb_->len, fb_->width, fb_->height, fb_->format, 80,
[](void* arg, size_t index, const void* data, size_t len) -> size_t {
auto jpeg_queue = (QueueHandle_t)arg;
JpegChunk chunk = {
.data = (uint8_t*)heap_caps_aligned_alloc(16, len, MALLOC_CAP_SPIRAM),

View File

@@ -5,6 +5,8 @@
#include "application.h"
#include "protocols/protocol.h"
#include "display/display.h"
#include "server_config.h"
#include "device_manager.h"
#include <esp_log.h>
#include <esp_heap_caps.h>
@@ -98,6 +100,14 @@ static void add_auth_headers(Http* http) {
http->SetHeader("X-Timestamp", std::to_string(timestamp));
http->SetHeader("X-Dynamic-Key", dynamic_key);
// 获取并添加设备Token
auto& device_manager = DeviceManager::GetInstance();
std::string token = device_manager.GetDeviceToken();
if (!token.empty()) {
http->SetHeader("X-Device-Token", token);
ESP_LOGI(TAG, "Added X-Device-Token: %s...", token.substr(0, 8).c_str());
}
ESP_LOGI(TAG, "Added auth headers - MAC: %s, ChipID: %s, Timestamp: %lld",
mac.c_str(), chip_id.c_str(), timestamp);
}
@@ -126,52 +136,54 @@ static std::string url_encode(const std::string& str) {
return encoded;
}
//// 在文件开头添加一个辅助函数统一处理URL构建
//static std::string buildUrlWithParams(const std::string& base_url, const std::string& path, const std::string& query) {
// std::string result_url = base_url + path + "?";
// size_t pos = 0;
// size_t amp_pos = 0;
//
// while ((amp_pos = query.find("&", pos)) != std::string::npos) {
// std::string param = query.substr(pos, amp_pos - pos);
// size_t eq_pos = param.find("=");
//
// if (eq_pos != std::string::npos) {
// std::string key = param.substr(0, eq_pos);
// std::string value = param.substr(eq_pos + 1);
// result_url += key + "=" + url_encode(value) + "&";
// } else {
// result_url += param + "&";
// }
//
// pos = amp_pos + 1;
// }
//
// // 处理最后一个参数
// std::string last_param = query.substr(pos);
// size_t eq_pos = last_param.find("=");
//
// if (eq_pos != std::string::npos) {
// std::string key = last_param.substr(0, eq_pos);
// std::string value = last_param.substr(eq_pos + 1);
// result_url += key + "=" + url_encode(value);
// } else {
// result_url += last_param;
// }
//
// return result_url;
//}
// 在文件开头添加一个辅助函数统一处理URL构建
static std::string buildUrlWithParams(const std::string& base_url, const std::string& path, const std::string& query) {
std::string result_url = base_url + path + "?";
size_t pos = 0;
size_t amp_pos = 0;
while ((amp_pos = query.find("&", pos)) != std::string::npos) {
std::string param = query.substr(pos, amp_pos - pos);
size_t eq_pos = param.find("=");
if (eq_pos != std::string::npos) {
std::string key = param.substr(0, eq_pos);
std::string value = param.substr(eq_pos + 1);
result_url += key + "=" + url_encode(value) + "&";
} else {
result_url += param + "&";
}
pos = amp_pos + 1;
}
// 处理最后一个参数
std::string last_param = query.substr(pos);
size_t eq_pos = last_param.find("=");
if (eq_pos != std::string::npos) {
std::string key = last_param.substr(0, eq_pos);
std::string value = last_param.substr(eq_pos + 1);
result_url += key + "=" + url_encode(value);
} else {
result_url += last_param;
}
return result_url;
}
Esp32Music::Esp32Music() : last_downloaded_data_(), current_music_url_(), current_song_name_(),
song_name_displayed_(false), current_lyric_url_(), lyrics_(),
current_lyric_index_(-1), lyric_thread_(), is_lyric_running_(false),
display_mode_(DISPLAY_MODE_LYRICS), is_playing_(false), is_downloading_(false),
is_paused_(false), play_thread_(), download_thread_(), audio_buffer_(), buffer_mutex_(),
play_thread_(), download_thread_(), current_play_time_ms_(0),
last_frame_time_ms_(0), total_frames_decoded_(0), current_song_duration_seconds_(0),
audio_buffer_(), buffer_mutex_(),
buffer_cv_(), buffer_size_(0), mp3_decoder_(nullptr), mp3_frame_info_(),
mp3_decoder_initialized_(false) {
mp3_decoder_initialized_(false), playlist_(), playlist_mutex_(),
current_playlist_index_(-1), playlist_mode_(false), playlist_thread_() {
ESP_LOGI(TAG, "Music player initialized with default spectrum display mode");
// 延迟MP3解码器初始化避免在构造函数中初始化导致的问题
// InitializeMp3Decoder();
InitializeMp3Decoder();
}
Esp32Music::~Esp32Music() {
@@ -181,6 +193,7 @@ Esp32Music::~Esp32Music() {
is_downloading_ = false;
is_playing_ = false;
is_lyric_running_ = false;
playlist_mode_ = false;
// 通知所有等待的线程
{
@@ -275,6 +288,13 @@ Esp32Music::~Esp32Music() {
ESP_LOGI(TAG, "Lyric thread finished");
}
// 等待播放队列线程结束
if (playlist_thread_.joinable()) {
ESP_LOGI(TAG, "Waiting for playlist thread to finish");
playlist_thread_.join();
ESP_LOGI(TAG, "Playlist thread finished");
}
// 清理缓冲区和MP3解码器
ClearAudioBuffer();
CleanupMp3Decoder();
@@ -283,9 +303,9 @@ Esp32Music::~Esp32Music() {
}
bool Esp32Music::Download(const std::string& song_name, const std::string& artist_name) {
ESP_LOGI(TAG, "Starting to get music details for: %s", song_name.c_str());
ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供");
ESP_LOGI(TAG, "喵波音律QQ交流群:865754861");
ESP_LOGI(TAG, "Starting to get music details for: %s", song_name.c_str());
// 清空之前的下载数据
last_downloaded_data_.clear();
@@ -294,8 +314,8 @@ bool Esp32Music::Download(const std::string& song_name, const std::string& artis
current_song_name_ = song_name;
// 第一步请求stream_pcm接口获取音频信息
std::string base_url = "http://http-embedded-music.miao-lab.top:2233";
std::string full_url = base_url + "/stream_pcm?song=" + url_encode(song_name) + "&artist=" + url_encode(artist_name);
std::string base_url = MUSIC_SERVER_URL;
std::string full_url = base_url + "/stream_pcm?song=" + url_encode(song_name) + "&singer=" + url_encode(artist_name);
ESP_LOGI(TAG, "Request URL: %s", full_url.c_str());
@@ -303,9 +323,6 @@ bool Esp32Music::Download(const std::string& song_name, const std::string& artis
auto network = Board::GetInstance().GetNetwork();
auto http = network->CreateHttp(0);
// 复用连接(服务端支持 Keep-Alive
http->SetHeader("Connection", "keep-alive");
// 设置基本请求头
http->SetHeader("User-Agent", "ESP32-Music-Player/1.0");
http->SetHeader("Accept", "application/json");
@@ -319,9 +336,6 @@ bool Esp32Music::Download(const std::string& song_name, const std::string& artis
return false;
}
// 添加超时
http->SetTimeout(15000);
// 检查响应状态码
int status_code = http->GetStatusCode();
if (status_code != 200) {
@@ -352,6 +366,7 @@ bool Esp32Music::Download(const std::string& song_name, const std::string& artis
cJSON* title = cJSON_GetObjectItem(response_json, "title");
cJSON* audio_url = cJSON_GetObjectItem(response_json, "audio_url");
cJSON* lyric_url = cJSON_GetObjectItem(response_json, "lyric_url");
cJSON* duration = cJSON_GetObjectItem(response_json, "duration");
if (cJSON_IsString(artist)) {
ESP_LOGI(TAG, "Artist: %s", artist->valuestring);
@@ -360,21 +375,35 @@ bool Esp32Music::Download(const std::string& song_name, const std::string& artis
ESP_LOGI(TAG, "Title: %s", title->valuestring);
}
// 解析歌曲总时长
if (cJSON_IsNumber(duration)) {
current_song_duration_seconds_ = duration->valueint;
ESP_LOGI(TAG, "Song duration: %d seconds", current_song_duration_seconds_);
} else {
// 如果API没有返回时长设置为0未知
current_song_duration_seconds_ = 0;
ESP_LOGW(TAG, "Song duration not available from API");
}
// 检查audio_url是否有效
if (cJSON_IsString(audio_url) && audio_url->valuestring && strlen(audio_url->valuestring) > 0) {
ESP_LOGI(TAG, "Audio URL path: %s", audio_url->valuestring);
// 第二步:直接使用音频URL开始流式播放
std::string current_music_url_ = audio_url->valuestring;
// 第二步:直接使用audio_url播放音乐
std::string audio_path = audio_url->valuestring;
current_music_url_ = audio_path;
ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供");
ESP_LOGI(TAG, "喵波音律QQ交流群:865754861");
ESP_LOGI(TAG, "Starting streaming playback for: %s", song_name.c_str());
song_name_displayed_ = false; // 重置歌名显示标志
StartStreaming(current_music_url_);
// 处理歌词URL - 只有在歌词显示模式下才启动歌词
if (cJSON_IsString(lyric_url) && lyric_url->valuestring && strlen(lyric_url->valuestring) > 0) {
// 使用歌词URL获取歌词
std::string current_lyric_url_ = lyric_url->valuestring;
// 直接使用歌词URL
std::string lyric_path = lyric_url->valuestring;
current_lyric_url_ = lyric_path;
// 根据显示模式决定是否启动歌词
if (display_mode_ == DISPLAY_MODE_LYRICS) {
@@ -434,14 +463,6 @@ bool Esp32Music::StartStreaming(const std::string& music_url) {
ESP_LOGD(TAG, "Starting streaming for URL: %s", music_url.c_str());
// 确保MP3解码器已初始化
if (!mp3_decoder_initialized_) {
if (!InitializeMp3Decoder()) {
ESP_LOGE(TAG, "Failed to initialize MP3 decoder");
return false;
}
}
// 停止之前的播放和下载
is_downloading_ = false;
is_playing_ = false;
@@ -502,7 +523,6 @@ bool Esp32Music::StopStreaming() {
// 停止下载和播放标志
is_downloading_ = false;
is_playing_ = false;
is_paused_ = false; // 重置暂停状态
// 清空歌名显示
auto& board = Board::GetInstance();
@@ -735,6 +755,8 @@ void Esp32Music::PlayAudioStream() {
});
}
ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供");
ESP_LOGI(TAG, "喵波音律QQ交流群:865754861");
ESP_LOGI(TAG, "Starting playback with buffer size: %d", buffer_size_);
size_t total_played = 0;
@@ -754,13 +776,6 @@ void Esp32Music::PlayAudioStream() {
bool id3_processed = false;
while (is_playing_) {
// 检查是否被暂停
if (is_paused_) {
ESP_LOGD(TAG, "Music playback paused, waiting...");
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
// 检查设备状态,只有在空闲状态才播放音乐
auto& app = Application::GetInstance();
DeviceState current_state = app.GetDeviceState();
@@ -1150,6 +1165,8 @@ bool Esp32Music::DownloadLyrics(const std::string& lyric_url) {
add_auth_headers(http.get());
// 打开GET连接
ESP_LOGI(TAG, "云端由MeowEmbeddedMusicServer喵波音律嵌入式提供");
ESP_LOGI(TAG, "喵波音律QQ交流群:865754861");
if (!http->Open("GET", current_url)) {
ESP_LOGE(TAG, "Failed to open HTTP connection for lyrics");
// 移除delete http; 因为unique_ptr会自动管理内存
@@ -1443,97 +1460,154 @@ void Esp32Music::SetDisplayMode(DisplayMode mode) {
(mode == DISPLAY_MODE_SPECTRUM) ? "SPECTRUM" : "LYRICS");
}
// MCP工具需要的方法实现
bool Esp32Music::SetVolume(int volume) {
ESP_LOGI(TAG, "SetVolume called with volume: %d", volume);
// ========== 播放队列功能实现 ==========
// 验证音量范围
if (volume < 0 || volume > 100) {
ESP_LOGW(TAG, "Invalid volume level: %d, must be between 0-100", volume);
bool Esp32Music::PlayPlaylist(const std::vector<SongInfo>& songs) {
if (songs.empty()) {
ESP_LOGW(TAG, "Playlist is empty");
return false;
}
// 通过Board获取AudioCodec并设置音量
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (codec) {
codec->SetOutputVolume(volume);
ESP_LOGI(TAG, "Volume set to %d%%", volume);
ESP_LOGI(TAG, "Starting playlist with %d songs", (int)songs.size());
// 停止当前播放
StopPlaylist();
// 设置播放队列
{
std::lock_guard<std::mutex> lock(playlist_mutex_);
playlist_ = songs;
current_playlist_index_ = 0;
playlist_mode_ = true;
}
// 启动播放队列管理线程
playlist_thread_ = std::thread(&Esp32Music::PlaylistManagerThread, this);
return true;
}
bool Esp32Music::NextSong() {
if (!playlist_mode_.load()) {
ESP_LOGW(TAG, "Not in playlist mode");
return false;
}
std::lock_guard<std::mutex> lock(playlist_mutex_);
if (current_playlist_index_ + 1 < (int)playlist_.size()) {
current_playlist_index_++;
ESP_LOGI(TAG, "Moving to next song: %d/%d", current_playlist_index_ + 1, (int)playlist_.size());
return true;
} else {
ESP_LOGE(TAG, "No audio codec available");
ESP_LOGI(TAG, "Reached end of playlist");
return false;
}
}
bool Esp32Music::PlaySong() {
ESP_LOGI(TAG, "PlaySong called");
return false;
}
bool Esp32Music::StopSong() {
ESP_LOGI(TAG, "StopSong called");
return StopStreaming();
}
bool Esp32Music::PauseSong() {
ESP_LOGI(TAG, "PauseSong called");
// 检查是否正在播放
if (!is_playing_) {
ESP_LOGW(TAG, "No music is currently playing");
bool Esp32Music::PreviousSong() {
if (!playlist_mode_.load()) {
ESP_LOGW(TAG, "Not in playlist mode");
return false;
}
// 检查是否已经暂停
if (is_paused_) {
ESP_LOGW(TAG, "Music is already paused");
std::lock_guard<std::mutex> lock(playlist_mutex_);
if (current_playlist_index_ > 0) {
current_playlist_index_--;
ESP_LOGI(TAG, "Moving to previous song: %d/%d", current_playlist_index_ + 1, (int)playlist_.size());
return true;
}
// 设置暂停标志
is_paused_ = true;
ESP_LOGI(TAG, "Music playback paused");
// 更新显示状态
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
if (display && !current_song_name_.empty()) {
std::string formatted_song_name = "" + current_song_name_ + "》已暂停";
display->SetMusicInfo(formatted_song_name.c_str());
ESP_LOGI(TAG, "Updated display: %s", formatted_song_name.c_str());
}
return true;
}
bool Esp32Music::ResumeSong() {
ESP_LOGI(TAG, "ResumeSong called");
// 检查是否正在播放
if (!is_playing_) {
ESP_LOGW(TAG, "No music is currently playing");
} else {
ESP_LOGI(TAG, "Already at first song");
return false;
}
// 检查是否已经恢复
if (!is_paused_) {
ESP_LOGW(TAG, "Music is not paused");
return true;
}
// 清除暂停标志
is_paused_ = false;
ESP_LOGI(TAG, "Music playback resumed");
// 更新显示状态
auto& board = Board::GetInstance();
auto display = board.GetDisplay();
if (display && !current_song_name_.empty()) {
std::string formatted_song_name = "" + current_song_name_ + "》播放中...";
display->SetMusicInfo(formatted_song_name.c_str());
ESP_LOGI(TAG, "Updated display: %s", formatted_song_name.c_str());
}
return true;
}
void Esp32Music::StopPlaylist() {
ESP_LOGI(TAG, "Stopping playlist");
playlist_mode_ = false;
// 停止当前播放
StopStreaming();
// 等待播放队列线程结束
if (playlist_thread_.joinable()) {
playlist_thread_.join();
}
// 清空播放队列
{
std::lock_guard<std::mutex> lock(playlist_mutex_);
playlist_.clear();
current_playlist_index_ = -1;
}
}
size_t Esp32Music::GetPlaylistSize() const {
std::lock_guard<std::mutex> lock(playlist_mutex_);
return playlist_.size();
}
SongInfo Esp32Music::GetCurrentSong() const {
std::lock_guard<std::mutex> lock(playlist_mutex_);
if (current_playlist_index_ >= 0 && current_playlist_index_ < (int)playlist_.size()) {
return playlist_[current_playlist_index_];
}
return SongInfo();
}
void Esp32Music::PlaylistManagerThread() {
ESP_LOGI(TAG, "Playlist manager thread started");
while (playlist_mode_.load()) {
SongInfo current_song;
// 获取当前要播放的歌曲
{
std::lock_guard<std::mutex> lock(playlist_mutex_);
if (current_playlist_index_ >= 0 && current_playlist_index_ < (int)playlist_.size()) {
current_song = playlist_[current_playlist_index_];
} else {
ESP_LOGI(TAG, "Playlist finished");
break;
}
}
// 播放当前歌曲
ESP_LOGI(TAG, "Playing song %d/%d: %s - %s",
current_playlist_index_ + 1, (int)GetPlaylistSize(),
current_song.title.c_str(), current_song.artist.c_str());
PlayCurrentSong();
// 等待当前歌曲播放完成
while (playlist_mode_.load() && (is_playing_.load() || is_downloading_.load())) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
if (!playlist_mode_.load()) {
break;
}
// 自动播放下一首
if (!NextSong()) {
ESP_LOGI(TAG, "Playlist completed");
break;
}
}
ESP_LOGI(TAG, "Playlist manager thread finished");
playlist_mode_ = false;
}
void Esp32Music::PlayCurrentSong() {
SongInfo song = GetCurrentSong();
if (song.title.empty()) {
ESP_LOGE(TAG, "No current song to play");
return;
}
// 调用现有的Download方法播放歌曲
if (!Download(song.title, song.artist)) {
ESP_LOGE(TAG, "Failed to play song: %s - %s", song.title.c_str(), song.artist.c_str());
}
}

View File

@@ -25,6 +25,15 @@ struct AudioChunk {
AudioChunk(uint8_t* d, size_t s) : data(d), size(s) {}
};
// 歌曲信息结构
struct SongInfo {
std::string title;
std::string artist;
SongInfo() : title(""), artist("") {}
SongInfo(const std::string& t, const std::string& a) : title(t), artist(a) {}
};
class Esp32Music : public Music {
public:
// 显示模式控制 - 移动到public区域
@@ -38,6 +47,7 @@ private:
std::string current_music_url_;
std::string current_song_name_;
bool song_name_displayed_;
std::atomic<bool> stop_flag_{false}; // 停止播放标志位
// 歌词相关
std::string current_lyric_url_;
@@ -50,12 +60,12 @@ private:
std::atomic<DisplayMode> display_mode_;
std::atomic<bool> is_playing_;
std::atomic<bool> is_downloading_;
std::atomic<bool> is_paused_;
std::thread play_thread_;
std::thread download_thread_;
int64_t current_play_time_ms_; // 当前播放时间(毫秒)
int64_t last_frame_time_ms_; // 上一帧的时间戳
int total_frames_decoded_; // 已解码的帧数
int current_song_duration_seconds_; // 当前歌曲总时长(秒)
// 音频缓冲区
std::queue<AudioChunk> audio_buffer_;
@@ -70,6 +80,13 @@ private:
MP3FrameInfo mp3_frame_info_;
bool mp3_decoder_initialized_;
// 播放队列相关
std::vector<SongInfo> playlist_;
mutable std::mutex playlist_mutex_;
std::atomic<int> current_playlist_index_;
std::atomic<bool> playlist_mode_;
std::thread playlist_thread_;
// 私有方法
void DownloadAudioStream(const std::string& music_url);
void PlayAudioStream();
@@ -87,6 +104,10 @@ private:
// ID3标签处理
size_t SkipId3Tag(uint8_t* data, size_t size);
// 播放队列管理私有方法
void PlaylistManagerThread();
void PlayCurrentSong();
int16_t* final_pcm_data_fft = nullptr;
public:
@@ -102,20 +123,29 @@ public:
virtual bool StopStreaming() override; // 停止流式播放
virtual size_t GetBufferSize() const override { return buffer_size_; }
virtual bool IsDownloading() const override { return is_downloading_; }
virtual bool IsPlaying() const override { return is_playing_; }
virtual bool IsPaused() const override { return is_paused_; }
virtual int16_t* GetAudioData() override { return final_pcm_data_fft; }
// 显示模式控制方法
void SetDisplayMode(DisplayMode mode);
DisplayMode GetDisplayMode() const { return display_mode_.load(); }
// MCP工具需要的方法
virtual bool PlaySong() override;
virtual bool SetVolume(int volume) override;
virtual bool StopSong() override;
virtual bool PauseSong() override;
virtual bool ResumeSong() override;
// 音乐播放信息获取方法
virtual int GetCurrentSongDurationSeconds() const override { return current_song_duration_seconds_; }
virtual int GetCurrentPlayTimeSeconds() const override { return (int)(current_play_time_ms_ / 1000); }
virtual float GetPlayProgress() const override {
if (current_song_duration_seconds_ <= 0) return 0.0f;
return (float)(current_play_time_ms_ / 1000) / current_song_duration_seconds_ * 100.0f;
}
// 播放队列相关方法
virtual bool PlayPlaylist(const std::vector<SongInfo>& songs) override;
virtual bool NextSong() override;
virtual bool PreviousSong() override;
virtual void StopPlaylist() override;
virtual bool IsPlaylistMode() const override { return playlist_mode_.load(); }
virtual int GetCurrentPlaylistIndex() const override { return current_playlist_index_.load(); }
virtual size_t GetPlaylistSize() const override;
virtual SongInfo GetCurrentSong() const override;
};
#endif // ESP32_MUSIC_H

View File

@@ -2,6 +2,10 @@
#define MUSIC_H
#include <string>
#include <vector>
// 前向声明
struct SongInfo;
class Music {
public:
@@ -15,16 +19,22 @@ public:
virtual bool StopStreaming() = 0; // 停止流式播放
virtual size_t GetBufferSize() const = 0;
virtual bool IsDownloading() const = 0;
virtual bool IsPlaying() const = 0;
virtual bool IsPaused() const = 0;
virtual int16_t* GetAudioData() = 0;
// MCP工具需要的方法
virtual bool PlaySong() = 0;
virtual bool SetVolume(int volume) = 0;
virtual bool StopSong() = 0;
virtual bool PauseSong() = 0;
virtual bool ResumeSong() = 0;
// 音乐播放信息获取方法
virtual int GetCurrentSongDurationSeconds() const = 0;
virtual int GetCurrentPlayTimeSeconds() const = 0;
virtual float GetPlayProgress() const = 0;
// 播放队列相关方法
virtual bool PlayPlaylist(const std::vector<SongInfo>& songs) = 0;
virtual bool NextSong() = 0;
virtual bool PreviousSong() = 0;
virtual void StopPlaylist() = 0;
virtual bool IsPlaylistMode() const = 0;
virtual int GetCurrentPlaylistIndex() const = 0;
virtual size_t GetPlaylistSize() const = 0;
virtual SongInfo GetCurrentSong() const = 0;
};
#endif // MUSIC_H

View File

@@ -41,6 +41,9 @@ void WifiBoard::EnterWifiConfigMode() {
wifi_ap.SetSsidPrefix("Xiaozhi");
wifi_ap.Start();
// 等待 1.5 秒显示开发板信息
vTaskDelay(pdMS_TO_TICKS(1500));
// 显示 WiFi 配置 AP 的 SSID 和 Web 服务器 URL
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
hint += wifi_ap.GetSsid();

View File

@@ -1,11 +1,11 @@
#include "wifi_board.h"
#include "codecs/box_audio_codec.h"
#include "display/lcd_display.h"
#include "display/emote_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "backlight.h"
#include "emote_display.h"
#include <wifi_station.h>
#include <esp_log.h>
@@ -26,7 +26,6 @@
#define TAG "EchoEar"
#define USE_LVGL_DEFAULT 0
temperature_sensor_handle_t temp_sensor = NULL;
static const st77916_lcd_init_cmd_t vendor_specific_init_yysj[] = {
@@ -387,11 +386,7 @@ private:
Cst816s* cst816s_;
Charge* charge_;
Button boot_button_;
#if USE_LVGL_DEFAULT
LcdDisplay* display_;
#else
anim::EmoteDisplay* display_ = nullptr;
#endif
Display* display_ = nullptr;
PwmBacklight* backlight_ = nullptr;
esp_timer_handle_t touchpad_timer_;
esp_lcd_touch_handle_t tp; // LCD touch handle
@@ -558,11 +553,11 @@ private:
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
#if USE_LVGL_DEFAULT
#if CONFIG_USE_EMOTE_MESSAGE_STYLE
display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT);
#else
display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
#else
display_ = new anim::EmoteDisplay(panel, panel_io);
#endif
backlight_ = new PwmBacklight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
backlight_->RestoreBrightness();

View File

@@ -27,10 +27,6 @@ idf.py menuconfig
### 基本配置
- `Xiaozhi Assistant``Board Type` → 选择 `EchoEar`
### 分区表配置
- `Partition Table``Partition Table` → 选择 `Custom partition table CSV`
- `Partition Table``Custom partition CSV file` → 输入 `partitions/v1/16m_echoear.csv`
### UI风格选择
EchoEar 支持两种不同的UI显示风格通过修改代码中的宏定义来选择

View File

@@ -3,9 +3,13 @@
"builds": [
{
"name": "echoear",
"sdkconfig_append": [
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/16m_echoear.csv\""
]
"sdkconfig_append": [
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"",
"CONFIG_USE_EMOTE_MESSAGE_STYLE=y",
"CONFIG_BOARD_TYPE_ECHOEAR=y",
"CONFIG_FLASH_CUSTOM_ASSETS=y",
"CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-echoear.bin\""
]
}
]
}

View File

@@ -0,0 +1,22 @@
[
{"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20},
{"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20},
{"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20},
{"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20},
{"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20},
{"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20},
{"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20},
{"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20},
{"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20},
{"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20}
]

View File

@@ -1,419 +0,0 @@
#include "emote_display.h"
#include <cstring>
#include <memory>
#include <unordered_map>
#include <tuple>
#include <esp_log.h>
#include <esp_lcd_panel_io.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <sys/time.h>
#include <time.h>
#include "display/lcd_display.h"
#include "mmap_generate_emoji_normal.h"
#include "config.h"
#include "gfx.h"
namespace anim {
static const char* TAG = "emoji";
// UI element management
static gfx_obj_t* obj_label_tips = nullptr;
static gfx_obj_t* obj_label_time = nullptr;
static gfx_obj_t* obj_anim_eye = nullptr;
static gfx_obj_t* obj_anim_mic = nullptr;
static gfx_obj_t* obj_img_icon = nullptr;
static gfx_image_dsc_t icon_img_dsc;
// Track current icon to determine when to show time
static int current_icon_type = MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN;
enum class UIDisplayMode : uint8_t {
SHOW_ANIM_TOP = 1, // Show obj_anim_mic
SHOW_TIME = 2, // Show obj_label_time
SHOW_TIPS = 3 // Show obj_label_tips
};
static void SetUIDisplayMode(UIDisplayMode mode)
{
gfx_obj_set_visible(obj_anim_mic, false);
gfx_obj_set_visible(obj_label_time, false);
gfx_obj_set_visible(obj_label_tips, false);
// Show the selected control
switch (mode) {
case UIDisplayMode::SHOW_ANIM_TOP:
gfx_obj_set_visible(obj_anim_mic, true);
break;
case UIDisplayMode::SHOW_TIME:
gfx_obj_set_visible(obj_label_time, true);
break;
case UIDisplayMode::SHOW_TIPS:
gfx_obj_set_visible(obj_label_tips, true);
break;
}
}
static void clock_tm_callback(void* user_data)
{
// Only display time when battery icon is shown
if (current_icon_type == MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN) {
time_t now;
struct tm timeinfo;
time(&now);
setenv("TZ", "GMT+0", 1);
tzset();
localtime_r(&now, &timeinfo);
char time_str[6];
snprintf(time_str, sizeof(time_str), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
gfx_label_set_text(obj_label_time, time_str);
SetUIDisplayMode(UIDisplayMode::SHOW_TIME);
}
}
static void InitializeAssets(mmap_assets_handle_t* assets_handle)
{
const mmap_assets_config_t assets_cfg = {
.partition_label = "assets_A",
.max_files = MMAP_EMOJI_NORMAL_FILES,
.checksum = MMAP_EMOJI_NORMAL_CHECKSUM,
.flags = {.mmap_enable = true, .full_check = true}
};
mmap_assets_new(&assets_cfg, assets_handle);
}
static void InitializeGraphics(esp_lcd_panel_handle_t panel, gfx_handle_t* engine_handle)
{
gfx_core_config_t gfx_cfg = {
.flush_cb = EmoteEngine::OnFlush,
.user_data = panel,
.flags = {
.swap = true,
.double_buffer = true,
.buff_dma = true,
},
.h_res = DISPLAY_WIDTH,
.v_res = DISPLAY_HEIGHT,
.fps = 30,
.buffers = {
.buf1 = nullptr,
.buf2 = nullptr,
.buf_pixels = DISPLAY_WIDTH * 16,
},
.task = GFX_EMOTE_INIT_CONFIG()
};
gfx_cfg.task.task_stack_caps = MALLOC_CAP_DEFAULT;
gfx_cfg.task.task_affinity = 0;
gfx_cfg.task.task_priority = 5;
gfx_cfg.task.task_stack = 20 * 1024;
*engine_handle = gfx_emote_init(&gfx_cfg);
}
static void InitializeEyeAnimation(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle)
{
obj_anim_eye = gfx_anim_create(engine_handle);
const void* anim_data = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_IDLE_ONE_AAF);
size_t anim_size = mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_IDLE_ONE_AAF);
gfx_anim_set_src(obj_anim_eye, anim_data, anim_size);
gfx_obj_align(obj_anim_eye, GFX_ALIGN_LEFT_MID, 10, -20);
gfx_anim_set_mirror(obj_anim_eye, true, (DISPLAY_WIDTH - (173 + 10) * 2));
gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, 20, false);
gfx_anim_start(obj_anim_eye);
}
static void InitializeFont(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle)
{
gfx_font_t font;
gfx_label_cfg_t font_cfg = {
.name = "DejaVuSans.ttf",
.mem = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_KAITI_TTF),
.mem_size = static_cast<size_t>(mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_KAITI_TTF)),
};
gfx_label_new_font(engine_handle, &font_cfg, &font);
ESP_LOGI(TAG, "stack: %d", uxTaskGetStackHighWaterMark(nullptr));
}
static void InitializeLabels(gfx_handle_t engine_handle)
{
// Initialize tips label
obj_label_tips = gfx_label_create(engine_handle);
gfx_obj_align(obj_label_tips, GFX_ALIGN_TOP_MID, 0, 45);
gfx_obj_set_size(obj_label_tips, 160, 40);
gfx_label_set_text(obj_label_tips, "启动中...");
gfx_label_set_font_size(obj_label_tips, 20);
gfx_label_set_color(obj_label_tips, GFX_COLOR_HEX(0xFFFFFF));
gfx_label_set_text_align(obj_label_tips, GFX_TEXT_ALIGN_LEFT);
gfx_label_set_long_mode(obj_label_tips, GFX_LABEL_LONG_SCROLL);
gfx_label_set_scroll_speed(obj_label_tips, 20);
gfx_label_set_scroll_loop(obj_label_tips, true);
// Initialize time label
obj_label_time = gfx_label_create(engine_handle);
gfx_obj_align(obj_label_time, GFX_ALIGN_TOP_MID, 0, 30);
gfx_obj_set_size(obj_label_time, 160, 50);
gfx_label_set_text(obj_label_time, "--:--");
gfx_label_set_font_size(obj_label_time, 40);
gfx_label_set_color(obj_label_time, GFX_COLOR_HEX(0xFFFFFF));
gfx_label_set_text_align(obj_label_time, GFX_TEXT_ALIGN_CENTER);
}
static void InitializeMicAnimation(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle)
{
obj_anim_mic = gfx_anim_create(engine_handle);
gfx_obj_align(obj_anim_mic, GFX_ALIGN_TOP_MID, 0, 25);
const void* anim_data = mmap_assets_get_mem(assets_handle, MMAP_EMOJI_NORMAL_LISTEN_AAF);
size_t anim_size = mmap_assets_get_size(assets_handle, MMAP_EMOJI_NORMAL_LISTEN_AAF);
gfx_anim_set_src(obj_anim_mic, anim_data, anim_size);
gfx_anim_start(obj_anim_mic);
gfx_obj_set_visible(obj_anim_mic, false);
}
static void InitializeIcon(gfx_handle_t engine_handle, mmap_assets_handle_t assets_handle)
{
obj_img_icon = gfx_img_create(engine_handle);
gfx_obj_align(obj_img_icon, GFX_ALIGN_TOP_MID, -100, 38);
SetupImageDescriptor(assets_handle, &icon_img_dsc, MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN);
gfx_img_set_src(obj_img_icon, static_cast<void*>(&icon_img_dsc));
}
static void RegisterCallbacks(esp_lcd_panel_io_handle_t panel_io, gfx_handle_t engine_handle)
{
const esp_lcd_panel_io_callbacks_t cbs = {
.on_color_trans_done = EmoteEngine::OnFlushIoReady,
};
esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, engine_handle);
}
void SetupImageDescriptor(mmap_assets_handle_t assets_handle,
gfx_image_dsc_t* img_dsc,
int asset_id)
{
const void* img_data = mmap_assets_get_mem(assets_handle, asset_id);
size_t img_size = mmap_assets_get_size(assets_handle, asset_id);
std::memcpy(&img_dsc->header, img_data, sizeof(gfx_image_header_t));
img_dsc->data = static_cast<const uint8_t*>(img_data) + sizeof(gfx_image_header_t);
img_dsc->data_size = img_size - sizeof(gfx_image_header_t);
}
EmoteEngine::EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
{
ESP_LOGI(TAG, "Create EmoteEngine, panel: %p, panel_io: %p", panel, panel_io);
InitializeAssets(&assets_handle_);
InitializeGraphics(panel, &engine_handle_);
gfx_emote_lock(engine_handle_);
gfx_emote_set_bg_color(engine_handle_, GFX_COLOR_HEX(0x000000));
// Initialize all UI components
InitializeEyeAnimation(engine_handle_, assets_handle_);
InitializeFont(engine_handle_, assets_handle_);
InitializeLabels(engine_handle_);
InitializeMicAnimation(engine_handle_, assets_handle_);
InitializeIcon(engine_handle_, assets_handle_);
current_icon_type = MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN;
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
gfx_timer_create(engine_handle_, clock_tm_callback, 1000, obj_label_tips);
gfx_emote_unlock(engine_handle_);
RegisterCallbacks(panel_io, engine_handle_);
}
EmoteEngine::~EmoteEngine()
{
if (engine_handle_) {
gfx_emote_deinit(engine_handle_);
engine_handle_ = nullptr;
}
if (assets_handle_) {
mmap_assets_del(assets_handle_);
assets_handle_ = nullptr;
}
}
void EmoteEngine::setEyes(int aaf, bool repeat, int fps)
{
if (!engine_handle_) {
return;
}
const void* src_data = mmap_assets_get_mem(assets_handle_, aaf);
size_t src_len = mmap_assets_get_size(assets_handle_, aaf);
Lock();
gfx_anim_set_src(obj_anim_eye, src_data, src_len);
gfx_anim_set_segment(obj_anim_eye, 0, 0xFFFF, fps, repeat);
gfx_anim_start(obj_anim_eye);
Unlock();
}
void EmoteEngine::stopEyes()
{
// Implementation if needed
}
void EmoteEngine::Lock()
{
if (engine_handle_) {
gfx_emote_lock(engine_handle_);
}
}
void EmoteEngine::Unlock()
{
if (engine_handle_) {
gfx_emote_unlock(engine_handle_);
}
}
void EmoteEngine::SetIcon(int asset_id)
{
if (!engine_handle_) {
return;
}
Lock();
SetupImageDescriptor(assets_handle_, &icon_img_dsc, asset_id);
gfx_img_set_src(obj_img_icon, static_cast<void*>(&icon_img_dsc));
current_icon_type = asset_id;
Unlock();
}
bool EmoteEngine::OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io,
esp_lcd_panel_io_event_data_t* edata,
void* user_ctx)
{
return true;
}
void EmoteEngine::OnFlush(gfx_handle_t handle, int x_start, int y_start,
int x_end, int y_end, const void* color_data)
{
auto* panel = static_cast<esp_lcd_panel_handle_t>(gfx_emote_get_user_data(handle));
if (panel) {
esp_lcd_panel_draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data);
}
gfx_emote_flush_ready(handle, true);
}
// EmoteDisplay implementation
EmoteDisplay::EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
{
InitializeEngine(panel, panel_io);
}
EmoteDisplay::~EmoteDisplay() = default;
void EmoteDisplay::SetEmotion(const char* emotion)
{
if (!engine_) {
return;
}
using EmotionParam = std::tuple<int, bool, int>;
static const std::unordered_map<std::string, EmotionParam> emotion_map = {
{"happy", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"laughing", {MMAP_EMOJI_NORMAL_ENJOY_ONE_AAF, true, 20}},
{"funny", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"loving", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"embarrassed", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"confident", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"delicious", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"sad", {MMAP_EMOJI_NORMAL_SAD_ONE_AAF, true, 20}},
{"crying", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"sleepy", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"silly", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"angry", {MMAP_EMOJI_NORMAL_ANGRY_ONE_AAF, true, 20}},
{"surprised", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"shocked", {MMAP_EMOJI_NORMAL_SHOCKED_ONE_AAF, true, 20}},
{"thinking", {MMAP_EMOJI_NORMAL_THINKING_ONE_AAF, true, 20}},
{"winking", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"relaxed", {MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20}},
{"confused", {MMAP_EMOJI_NORMAL_DIZZY_ONE_AAF, true, 20}},
{"neutral", {MMAP_EMOJI_NORMAL_IDLE_ONE_AAF, false, 20}},
{"idle", {MMAP_EMOJI_NORMAL_IDLE_ONE_AAF, false, 20}},
};
auto it = emotion_map.find(emotion);
if (it != emotion_map.end()) {
int aaf = std::get<0>(it->second);
bool repeat = std::get<1>(it->second);
int fps = std::get<2>(it->second);
engine_->setEyes(aaf, repeat, fps);
}
}
void EmoteDisplay::SetChatMessage(const char* role, const char* content)
{
engine_->Lock();
if (content && strlen(content) > 0) {
gfx_label_set_text(obj_label_tips, content);
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
}
engine_->Unlock();
}
void EmoteDisplay::SetStatus(const char* status)
{
if (!engine_) {
return;
}
if (std::strcmp(status, "聆听中...") == 0) {
SetUIDisplayMode(UIDisplayMode::SHOW_ANIM_TOP);
engine_->setEyes(MMAP_EMOJI_NORMAL_HAPPY_ONE_AAF, true, 20);
engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_MIC_BIN);
} else if (std::strcmp(status, "待命") == 0) {
SetUIDisplayMode(UIDisplayMode::SHOW_TIME);
engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_BATTERY_BIN);
} else if (std::strcmp(status, "说话中...") == 0) {
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_SPEAKER_ZZZ_BIN);
} else if (std::strcmp(status, "错误") == 0) {
SetUIDisplayMode(UIDisplayMode::SHOW_TIPS);
engine_->SetIcon(MMAP_EMOJI_NORMAL_ICON_WIFI_FAILED_BIN);
}
engine_->Lock();
if (std::strcmp(status, "连接中...") != 0) {
gfx_label_set_text(obj_label_tips, status);
}
engine_->Unlock();
}
void EmoteDisplay::InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
{
engine_ = std::make_unique<EmoteEngine>(panel, panel_io);
}
bool EmoteDisplay::Lock(int timeout_ms)
{
return true;
}
void EmoteDisplay::Unlock()
{
// Implementation if needed
}
} // namespace anim

View File

@@ -1,66 +0,0 @@
#pragma once
#include "display/lcd_display.h"
#include <memory>
#include <functional>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include "mmap_generate_emoji_normal.h"
#include "gfx.h"
namespace anim {
// Helper function for setting up image descriptors
void SetupImageDescriptor(mmap_assets_handle_t assets_handle, gfx_image_dsc_t* img_dsc, int asset_id);
class EmoteEngine;
using FlushIoReadyCallback = std::function<bool(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void*)>;
using FlushCallback = std::function<void(gfx_handle_t, int, int, int, int, const void*)>;
class EmoteEngine {
public:
EmoteEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
~EmoteEngine();
void setEyes(int aaf, bool repeat, int fps);
void stopEyes();
void Lock();
void Unlock();
void SetIcon(int asset_id);
mmap_assets_handle_t GetAssetsHandle() const { return assets_handle_; }
// Callback functions (public to be accessible from static helper functions)
static bool OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx);
static void OnFlush(gfx_handle_t handle, int x_start, int y_start, int x_end, int y_end, const void *color_data);
private:
gfx_handle_t engine_handle_;
mmap_assets_handle_t assets_handle_;
};
class EmoteDisplay : public Display {
public:
EmoteDisplay(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
virtual ~EmoteDisplay();
virtual void SetEmotion(const char* emotion) override;
virtual void SetStatus(const char* status) override;
virtual void SetChatMessage(const char* role, const char* content) override;
anim::EmoteEngine* GetEngine()
{
return engine_.get();
}
private:
void InitializeEngine(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
virtual bool Lock(int timeout_ms = 0) override;
virtual void Unlock() override;
std::unique_ptr<anim::EmoteEngine> engine_;
};
} // namespace anim

View File

@@ -0,0 +1,37 @@
[
{
"name": "eye_anim",
"align": "GFX_ALIGN_LEFT_MID",
"x": 10,
"y": 10
},
{
"name": "status_icon",
"align": "GFX_ALIGN_TOP_MID",
"x": -100,
"y": 38
},
{
"name": "toast_label",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 40,
"width": 160,
"height": 40
},
{
"name": "clock_label",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 40,
"width": 60,
"height": 50
},
{
"name": "listen_anim",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 25
}
]

View File

@@ -4,7 +4,12 @@
{
"name": "esp-box-3",
"sdkconfig_append": [
"CONFIG_USE_DEVICE_AEC=y"
"CONFIG_USE_DEVICE_AEC=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\"",
"CONFIG_USE_EMOTE_MESSAGE_STYLE=y",
"CONFIG_BOARD_TYPE_ESP_BOX_3=y",
"CONFIG_FLASH_CUSTOM_ASSETS=y",
"CONFIG_CUSTOM_ASSETS_FILE=\"https://dl.espressif.com/AE/wn9_nihaoxiaozhi_tts-font_puhui_common_20_4-esp-box-3.bin\""
]
}
]

View File

@@ -0,0 +1,22 @@
[
{"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20},
{"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20},
{"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20},
{"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20},
{"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20},
{"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20},
{"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20},
{"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20},
{"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20},
{"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20}
]

View File

@@ -1,5 +1,7 @@
#include "wifi_board.h"
#include "codecs/box_audio_codec.h"
#include "display/display.h"
#include "display/emote_display.h"
#include "display/lcd_display.h"
#include "esp_lcd_ili9341.h"
#include "application.h"
@@ -39,7 +41,7 @@ class EspBox3Board : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
Button boot_button_;
LcdDisplay* display_;
Display* display_;
void InitializeI2c() {
// Initialize I2C peripheral
@@ -125,8 +127,13 @@ private:
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
esp_lcd_panel_disp_on_off(panel, true);
#if CONFIG_USE_EMOTE_MESSAGE_STYLE
display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT);
#else
display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
#endif
}
public:

View File

@@ -0,0 +1,37 @@
[
{
"name": "eye_anim",
"align": "GFX_ALIGN_LEFT_MID",
"x": 10,
"y": 30
},
{
"name": "status_icon",
"align": "GFX_ALIGN_TOP_MID",
"x": -120,
"y": 18
},
{
"name": "toast_label",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 20,
"width": 200,
"height": 40
},
{
"name": "clock_label",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 20,
"width": 200,
"height": 50
},
{
"name": "listen_anim",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 5
}
]

View File

@@ -1,6 +1,7 @@
#include "adc_pdm_audio_codec.h"
#include <esp_log.h>
#include <esp_timer.h>
#include <driver/i2c.h>
#include <driver/i2c_master.h>
#include <driver/i2s_tdm.h>
@@ -11,6 +12,7 @@
#include "hal/rtc_io_hal.h"
#include "hal/gpio_ll.h"
#include "settings.h"
#include "config.h"
static const char TAG[] = "AdcPdmAudioCodec";
@@ -71,7 +73,7 @@ AdcPdmAudioCodec::AdcPdmAudioCodec(int input_sample_rate, int output_sample_rate
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, NULL));
i2s_pdm_tx_config_t pdm_cfg_default = BSP_I2S_DUPLEX_MONO_CFG((uint32_t)output_sample_rate, pdm_speak_p);
pdm_cfg_default.clk_cfg.up_sample_fs = output_sample_rate / 100;
pdm_cfg_default.clk_cfg.up_sample_fs = AUDIO_PDM_UPSAMPLE_FS;
pdm_cfg_default.slot_cfg.sd_scale = I2S_PDM_SIG_SCALING_MUL_4;
pdm_cfg_default.slot_cfg.hp_scale = I2S_PDM_SIG_SCALING_MUL_4;
pdm_cfg_default.slot_cfg.lp_scale = I2S_PDM_SIG_SCALING_MUL_4;
@@ -112,10 +114,27 @@ AdcPdmAudioCodec::AdcPdmAudioCodec(int input_sample_rate, int output_sample_rate
esp_rom_gpio_connect_out_signal(pdm_speak_n, I2SO_SD_OUT_IDX, 1, 0); //反转输出 SD OUT 信号
gpio_set_drive_capability(pdm_speak_n, GPIO_DRIVE_CAP_0);
}
// 初始化输出定时器
esp_timer_create_args_t output_timer_args = {
.callback = &AdcPdmAudioCodec::OutputTimerCallback,
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "output_timer"
};
ESP_ERROR_CHECK(esp_timer_create(&output_timer_args, &output_timer_));
ESP_LOGI(TAG, "AdcPdmAudioCodec initialized");
}
AdcPdmAudioCodec::~AdcPdmAudioCodec() {
// 删除定时器
if (output_timer_) {
esp_timer_stop(output_timer_);
esp_timer_delete(output_timer_);
output_timer_ = nullptr;
}
ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_));
esp_codec_dev_delete(output_dev_);
ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_));
@@ -161,11 +180,27 @@ void AdcPdmAudioCodec::EnableOutput(bool enable) {
};
ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_));
// 强制按板卡配置重配PDM TX时钟覆盖第三方库在set_fmt中的默认up_sample_fs
// 若通道已启用,先禁用再重配,最后再启用
ESP_ERROR_CHECK_WITHOUT_ABORT(i2s_channel_disable(tx_handle_));
i2s_pdm_tx_clk_config_t clk_cfg = I2S_PDM_TX_CLK_DEFAULT_CONFIG((uint32_t)output_sample_rate_);
clk_cfg.up_sample_fs = AUDIO_PDM_UPSAMPLE_FS;
ESP_ERROR_CHECK(i2s_channel_reconfig_pdm_tx_clock(tx_handle_, &clk_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
if(pa_ctrl_pin_ != GPIO_NUM_NC){
gpio_set_level(pa_ctrl_pin_, 1);
}
// 启用输出时启动定时器
if (output_timer_) {
esp_timer_start_once(output_timer_, TIMER_TIMEOUT_US);
}
} else {
// 禁用输出时停止定时器
if (output_timer_) {
esp_timer_stop(output_timer_);
}
if(pa_ctrl_pin_ != GPIO_NUM_NC){
gpio_set_level(pa_ctrl_pin_, 0);
}
@@ -183,6 +218,11 @@ int AdcPdmAudioCodec::Read(int16_t* dest, int samples) {
int AdcPdmAudioCodec::Write(const int16_t* data, int samples) {
if (output_enabled_) {
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, (void*)data, samples * sizeof(int16_t)));
// 重置输出定时器
if (output_timer_) {
esp_timer_stop(output_timer_);
esp_timer_start_once(output_timer_, TIMER_TIMEOUT_US);
}
}
return samples;
}
@@ -195,9 +235,15 @@ void AdcPdmAudioCodec::Start() {
output_volume_ = 10;
}
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
EnableInput(true);
EnableOutput(true);
ESP_LOGI(TAG, "Audio codec started");
}
// 定时器回调函数实现
void AdcPdmAudioCodec::OutputTimerCallback(void* arg) {
AdcPdmAudioCodec* codec = static_cast<AdcPdmAudioCodec*>(arg);
if (codec && codec->output_enabled_) {
codec->EnableOutput(false);
}
}

View File

@@ -5,6 +5,7 @@
#include <esp_codec_dev.h>
#include <esp_codec_dev_defaults.h>
#include <esp_timer.h>
class AdcPdmAudioCodec : public AudioCodec {
private:
@@ -12,6 +13,13 @@ private:
esp_codec_dev_handle_t input_dev_ = nullptr;
gpio_num_t pa_ctrl_pin_ = GPIO_NUM_NC;
// 定时器相关成员变量
esp_timer_handle_t output_timer_ = nullptr;
static constexpr uint64_t TIMER_TIMEOUT_US = 120000; // 120ms = 120000us
// 定时器回调函数
static void OutputTimerCallback(void* arg);
virtual int Read(int16_t* dest, int samples) override;
virtual int Write(const int16_t* data, int samples) override;

View File

@@ -6,6 +6,9 @@
#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
// 配置PDM上采样fs参数取值范围<=480。部分设备在441时表现更稳定
#define AUDIO_PDM_UPSAMPLE_FS 441
#define AUDIO_ADC_MIC_CHANNEL 2
#define AUDIO_PDM_SPEAK_P_GPIO GPIO_NUM_6
#define AUDIO_PDM_SPEAK_N_GPIO GPIO_NUM_7

View File

@@ -6,7 +6,7 @@
"sdkconfig_append": [
"CONFIG_IDF_TARGET=\"esp32c3\"",
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m_esp-hi.csv\"",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\"",
"CONFIG_BOARD_TYPE_ESP_HI=y",
"CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=3",
"CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=4",
@@ -25,7 +25,6 @@
"CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=2048",
"CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y",
"CONFIG_NEWLIB_NANO_FORMAT=y",
"CONFIG_MMAP_FILE_NAME_LENGTH=25",
"CONFIG_ESP_CONSOLE_NONE=y",
"CONFIG_USE_ESP_WAKE_WORD=y",
"CONFIG_COMPILER_OPTIMIZATION_SIZE=y"

View File

@@ -1,9 +1,9 @@
#include <cstring>
#include "display/lcd_display.h"
#include <esp_log.h>
#include "mmap_generate_emoji.h"
#include "emoji_display.h"
#include "assets/lang_config.h"
#include "assets.h"
#include <esp_lcd_panel_io.h>
#include <freertos/FreeRTOS.h>
@@ -15,6 +15,19 @@ static const char *TAG = "emoji";
namespace anim {
// Emoji asset name mapping based on usage pattern
static const std::unordered_map<std::string, std::string> emoji_asset_name_map = {
{"connecting", "connecting.aaf"},
{"wake", "wake.aaf"},
{"asking", "asking.aaf"},
{"happy_loop", "happy_loop.aaf"},
{"sad_loop", "sad_loop.aaf"},
{"anger_loop", "anger_loop.aaf"},
{"panic_loop", "panic_loop.aaf"},
{"blink_quick", "blink_quick.aaf"},
{"scorn_loop", "scorn_loop.aaf"}
};
bool EmojiPlayer::OnFlushIoReady(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{
auto* disp_drv = static_cast<anim_player_handle_t*>(user_ctx);
@@ -31,14 +44,6 @@ void EmojiPlayer::OnFlush(anim_player_handle_t handle, int x_start, int y_start,
EmojiPlayer::EmojiPlayer(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io)
{
ESP_LOGI(TAG, "Create EmojiPlayer, panel: %p, panel_io: %p", panel, panel_io);
const mmap_assets_config_t assets_cfg = {
.partition_label = "assets_A",
.max_files = MMAP_EMOJI_FILES,
.checksum = MMAP_EMOJI_CHECKSUM,
.flags = {.mmap_enable = true, .full_check = true}
};
mmap_assets_new(&assets_cfg, &assets_handle_);
anim_player_config_t player_cfg = {
.flush_cb = OnFlush,
@@ -48,13 +53,15 @@ EmojiPlayer::EmojiPlayer(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t
.task = ANIM_PLAYER_INIT_CONFIG()
};
player_cfg.task.task_priority = 1;
player_cfg.task.task_stack = 4096;
player_handle_ = anim_player_init(&player_cfg);
const esp_lcd_panel_io_callbacks_t cbs = {
.on_color_trans_done = OnFlushIoReady,
};
esp_lcd_panel_io_register_event_callbacks(panel_io, &cbs, player_handle_);
StartPlayer(MMAP_EMOJI_CONNECTING_AAF, true, 15);
StartPlayer("connecting", true, 15);
}
EmojiPlayer::~EmojiPlayer()
@@ -64,26 +71,25 @@ EmojiPlayer::~EmojiPlayer()
anim_player_deinit(player_handle_);
player_handle_ = nullptr;
}
if (assets_handle_) {
mmap_assets_del(assets_handle_);
assets_handle_ = NULL;
}
}
void EmojiPlayer::StartPlayer(int aaf, bool repeat, int fps)
void EmojiPlayer::StartPlayer(const std::string& asset_name, bool repeat, int fps)
{
if (player_handle_) {
uint32_t start, end;
const void *src_data;
size_t src_len;
void *src_data = nullptr;
size_t src_len = 0;
src_data = mmap_assets_get_mem(assets_handle_, aaf);
src_len = mmap_assets_get_size(assets_handle_, aaf);
auto& assets = Assets::GetInstance();
std::string filename = emoji_asset_name_map.at(asset_name);
if (!assets.GetAssetData(filename, src_data, src_len)) {
ESP_LOGE(TAG, "Failed to get asset data for %s", asset_name.c_str());
return;
}
anim_player_set_src_data(player_handle_, src_data, src_len);
anim_player_get_segment(player_handle_, &start, &end);
if(MMAP_EMOJI_WAKE_AAF == aaf){
if(asset_name == "wake"){
start = 7;
}
anim_player_set_segment(player_handle_, start, end, fps, true);
@@ -114,26 +120,26 @@ void EmojiWidget::SetEmotion(const char* emotion)
return;
}
using Param = std::tuple<int, bool, int>;
using Param = std::tuple<std::string, bool, int>;
static const std::unordered_map<std::string, Param> emotion_map = {
{"happy", {MMAP_EMOJI_HAPPY_LOOP_AAF, true, 25}},
{"laughing", {MMAP_EMOJI_HAPPY_LOOP_AAF, true, 25}},
{"funny", {MMAP_EMOJI_HAPPY_LOOP_AAF, true, 25}},
{"loving", {MMAP_EMOJI_HAPPY_LOOP_AAF, true, 25}},
{"embarrassed", {MMAP_EMOJI_HAPPY_LOOP_AAF, true, 25}},
{"confident", {MMAP_EMOJI_HAPPY_LOOP_AAF, true, 25}},
{"delicious", {MMAP_EMOJI_HAPPY_LOOP_AAF, true, 25}},
{"sad", {MMAP_EMOJI_SAD_LOOP_AAF, true, 25}},
{"crying", {MMAP_EMOJI_SAD_LOOP_AAF, true, 25}},
{"sleepy", {MMAP_EMOJI_SAD_LOOP_AAF, true, 25}},
{"silly", {MMAP_EMOJI_SAD_LOOP_AAF, true, 25}},
{"angry", {MMAP_EMOJI_ANGER_LOOP_AAF, true, 25}},
{"surprised", {MMAP_EMOJI_PANIC_LOOP_AAF, true, 25}},
{"shocked", {MMAP_EMOJI_PANIC_LOOP_AAF, true, 25}},
{"thinking", {MMAP_EMOJI_HAPPY_LOOP_AAF, true, 25}},
{"winking", {MMAP_EMOJI_BLINK_QUICK_AAF, true, 5}},
{"relaxed", {MMAP_EMOJI_SCORN_LOOP_AAF, true, 25}},
{"confused", {MMAP_EMOJI_SCORN_LOOP_AAF, true, 25}},
{"happy", {"happy_loop", true, 25}},
{"laughing", {"happy_loop", true, 25}},
{"funny", {"happy_loop", true, 25}},
{"loving", {"happy_loop", true, 25}},
{"embarrassed", {"happy_loop", true, 25}},
{"confident", {"happy_loop", true, 25}},
{"delicious", {"happy_loop", true, 25}},
{"sad", {"sad_loop", true, 25}},
{"crying", {"sad_loop", true, 25}},
{"sleepy", {"sad_loop", true, 25}},
{"silly", {"sad_loop", true, 25}},
{"angry", {"anger_loop", true, 25}},
{"surprised", {"panic_loop", true, 25}},
{"shocked", {"panic_loop", true, 25}},
{"thinking", {"happy_loop", true, 25}},
{"winking", {"blink_quick", true, 5}},
{"relaxed", {"scorn_loop", true, 25}},
{"confused", {"scorn_loop", true, 25}},
};
auto it = emotion_map.find(emotion);
@@ -148,9 +154,9 @@ void EmojiWidget::SetStatus(const char* status)
{
if (player_) {
if (strcmp(status, Lang::Strings::LISTENING) == 0) {
player_->StartPlayer(MMAP_EMOJI_ASKING_AAF, true, 15);
player_->StartPlayer("asking", true, 15);
} else if (strcmp(status, Lang::Strings::STANDBY) == 0) {
player_->StartPlayer(MMAP_EMOJI_WAKE_AAF, true, 15);
player_->StartPlayer("wake", true, 15);
}
}
}

View File

@@ -6,7 +6,7 @@
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include "anim_player.h"
#include "mmap_generate_emoji.h"
#include "assets.h"
namespace anim {
@@ -20,7 +20,7 @@ public:
EmojiPlayer(esp_lcd_panel_handle_t panel, esp_lcd_panel_io_handle_t panel_io);
~EmojiPlayer();
void StartPlayer(int aaf, bool repeat, int fps);
void StartPlayer(const std::string& asset_name, bool repeat, int fps);
void StopPlayer();
private:
@@ -28,7 +28,6 @@ private:
static void OnFlush(anim_player_handle_t handle, int x_start, int y_start, int x_end, int y_end, const void *color_data);
anim_player_handle_t player_handle_;
mmap_assets_handle_t assets_handle_;
};
class EmojiWidget : public Display {
@@ -38,6 +37,8 @@ public:
virtual void SetEmotion(const char* emotion) override;
virtual void SetStatus(const char* status) override;
virtual void SetChatMessage(const char* role, const char* content) override {}
anim::EmojiPlayer* GetPlayer()
{
return player_.get();

View File

@@ -397,11 +397,6 @@ public:
InitializeSpi();
InitializeLcdDisplay();
InitializeTools();
DeviceStateEventManager::GetInstance().RegisterStateChangeCallback([this](DeviceState previous_state, DeviceState current_state) {
ESP_LOGD(TAG, "Device state changed from %d to %d", previous_state, current_state);
this->GetAudioCodec()->EnableOutput(current_state == kDeviceStateSpeaking);
});
}
virtual AudioCodec* GetAudioCodec() override

View File

@@ -23,18 +23,6 @@ idf.py menuconfig
Xiaozhi Assistant -> Board Type -> ESP32 CGC 144
```
**修改 flash 大小:**
```
Serial flasher config -> Flash size -> 4 MB
```
**修改分区表:**
```
Partition Table -> Custom partition CSV file -> partitions/v1/4m.csv
```
**编译:**
```bash

View File

@@ -4,8 +4,6 @@
{
"name": "esp32-cgc-144",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m.csv\""
]
}
]

View File

@@ -29,18 +29,6 @@ Xiaozhi Assistant -> Board Type -> ESP32 CGC
Xiaozhi Assistant -> LCD Type -> "ST7735, 分辨率128*128"
```
**修改 flash 大小:**
```
Serial flasher config -> Flash size -> 4 MB
```
**修改分区表:**
```
Partition Table -> Custom partition CSV file -> partitions/v1/4m.csv
```
**编译:**
```bash

View File

@@ -4,8 +4,6 @@
{
"name": "esp32-cgc",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m.csv\"",
"CONFIG_LCD_ST7735_128X128=y"
]
}

View File

@@ -1,211 +0,0 @@
# MPU6050传感器集成说明
## 概述
本项目为ESP32-S3智能音箱开发板集成了MPU6050六轴传感器支持提供了现代化的C++封装接口。
## 文件结构
- `mpu6050_sensor.h` - MPU6050传感器封装类的头文件
- `mpu6050_sensor.cc` - MPU6050传感器封装类的实现文件
- `mpu6050_test.cc` - MPU6050传感器测试程序可选
- `esp32s3_smart_speaker.cc` - 已集成MPU6050支持的板子实现
## 功能特性
### 1. 传感器支持
- **加速度计**: 支持±2g, ±4g, ±8g, ±16g量程
- **陀螺仪**: 支持±250°/s, ±500°/s, ±1000°/s, ±2000°/s量程
- **温度传感器**: 内置温度传感器
- **姿态角计算**: 使用互补滤波算法计算俯仰角、横滚角和偏航角
### 2. 技术特点
- 使用ESP-IDF的现代I2C Master API
- 支持多量程配置
- 内置数字低通滤波器
- 可配置采样率
- 互补滤波姿态解算
- 完整的错误处理机制
## 硬件连接
根据`config.h`中的定义:
```c
// IMU传感器 (I2C接口)
#define IMU_I2C_SDA_PIN GPIO_NUM_21
#define IMU_I2C_SCL_PIN GPIO_NUM_20
#define IMU_INT_PIN GPIO_NUM_19
```
### 连接方式
- **VCC**: 3.3V
- **GND**: 地
- **SDA**: GPIO21
- **SCL**: GPIO20
- **INT**: GPIO19中断引脚可选
## 使用方法
### 1. 基本使用
```cpp
#include "mpu6050_sensor.h"
// 创建传感器实例
auto sensor = std::make_unique<Mpu6050Sensor>(i2c_bus_handle);
// 初始化传感器
if (sensor->Initialize(ACCE_FS_4G, GYRO_FS_500DPS)) {
// 唤醒传感器
if (sensor->WakeUp()) {
// 验证设备ID
uint8_t device_id;
if (sensor->GetDeviceId(&device_id)) {
ESP_LOGI(TAG, "MPU6050 initialized, ID: 0x%02X", device_id);
}
}
}
```
### 2. 读取传感器数据
```cpp
mpu6050_acce_value_t acce;
mpu6050_gyro_value_t gyro;
mpu6050_temp_value_t temp;
complimentary_angle_t angle;
// 读取加速度计数据
if (sensor->GetAccelerometer(&acce)) {
ESP_LOGI(TAG, "Accelerometer - X:%.2f, Y:%.2f, Z:%.2f",
acce.acce_x, acce.acce_y, acce.acce_z);
}
// 读取陀螺仪数据
if (sensor->GetGyroscope(&gyro)) {
ESP_LOGI(TAG, "Gyroscope - X:%.2f, Y:%.2f, Z:%.2f",
gyro.gyro_x, gyro.gyro_y, gyro.gyro_z);
}
// 读取温度数据
if (sensor->GetTemperature(&temp)) {
ESP_LOGI(TAG, "Temperature: %.2f°C", temp.temp);
}
// 计算姿态角
if (sensor->ComplimentaryFilter(&acce, &gyro, &angle)) {
ESP_LOGI(TAG, "Attitude - Pitch:%.2f°, Roll:%.2f°, Yaw:%.2f°",
angle.pitch, angle.roll, angle.yaw);
}
```
### 3. 获取传感器状态
```cpp
// 检查是否已初始化
if (sensor->IsInitialized()) {
// 获取状态信息
std::string status = sensor->GetStatusJson();
ESP_LOGI(TAG, "Sensor status: %s", status.c_str());
}
```
## 配置参数
### 加速度计量程
- `ACCE_FS_2G`: ±2g (16384 LSB/g)
- `ACCE_FS_4G`: ±4g (8192 LSB/g)
- `ACCE_FS_8G`: ±8g (4096 LSB/g)
- `ACCE_FS_16G`: ±16g (2048 LSB/g)
### 陀螺仪量程
- `GYRO_FS_250DPS`: ±250°/s (131 LSB/°/s)
- `GYRO_FS_500DPS`: ±500°/s (65.5 LSB/°/s)
- `GYRO_FS_1000DPS`: ±1000°/s (32.8 LSB/°/s)
- `GYRO_FS_2000DPS`: ±2000°/s (16.4 LSB/°/s)
### 互补滤波参数
- **alpha**: 0.98 (默认值,表示更信任陀螺仪)
- **采样率**: 125Hz
- **数字低通滤波器**: 5Hz
## 集成到板子
MPU6050已经集成到`Esp32s3SmartSpeaker`类中:
1. **自动初始化**: 在板子构造函数中自动初始化MPU6050
2. **后台任务**: 自动创建后台任务持续读取传感器数据
3. **状态报告**: 在`GetBoardJson()`中报告传感器状态
## 日志输出
传感器会输出以下日志信息:
```
I (1234) SmartSpeaker: MPU6050 sensor initialized successfully (ID: 0x68)
I (1235) SmartSpeaker: IMU data task created successfully
I (1236) SmartSpeaker: IMU data task started
I (1237) SmartSpeaker: Accelerometer - X:0.12, Y:-0.05, Z:0.98
I (1238) SmartSpeaker: Gyroscope - X:0.15, Y:-0.02, Z:0.08
I (1239) SmartSpeaker: Temperature: 25.3°C
I (1240) SmartSpeaker: Attitude - Pitch:2.1°, Roll:-1.5°, Yaw:0.3°
```
## 故障排除
### 常见问题
1. **设备ID不匹配**
- 检查I2C连接
- 确认设备地址是否正确
- 检查电源供应
2. **初始化失败**
- 检查I2C总线配置
- 确认GPIO引脚配置正确
- 检查上拉电阻
3. **数据读取失败**
- 检查I2C通信
- 确认传感器已唤醒
- 检查采样率配置
### 调试建议
1. 启用I2C调试日志
2. 检查硬件连接
3. 使用示波器检查I2C信号
4. 验证电源电压稳定性
## 技术细节
### I2C配置
- **时钟频率**: 100kHz
- **地址**: 0x68 (7位地址)
- **上拉电阻**: 内部使能
### 寄存器配置
- **加速度计量程**: 寄存器0x1C
- **陀螺仪量程**: 寄存器0x1B
- **数字低通滤波器**: 寄存器0x1A
- **采样率**: 寄存器0x19
- **电源管理**: 寄存器0x6B
### 数据格式
- **加速度计**: 16位有符号整数转换为g值
- **陀螺仪**: 16位有符号整数转换为度/秒
- **温度**: 16位有符号整数转换为摄氏度
## 扩展功能
### 可扩展的功能
1. **中断支持**: 可以配置数据就绪中断
2. **运动检测**: 可以配置运动检测中断
3. **自由落体检测**: 可以配置自由落体检测
4. **FIFO支持**: 可以使用FIFO缓冲区
5. **DMP支持**: 可以使用数字运动处理器
### 性能优化
1. **降低采样率**: 减少功耗
2. **使用中断**: 避免轮询
3. **FIFO缓冲**: 批量读取数据
4. **休眠模式**: 不使用时进入低功耗模式
## 许可证
本代码遵循项目的许可证要求。

View File

@@ -1,376 +0,0 @@
#include "adc_manager.h"
#include "application.h"
#include <board.h>
#include <cmath>
#include <cstring>
#include <esp_adc/adc_cali.h>
#include <esp_adc/adc_oneshot.h>
#include <esp_log.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#define TAG "AdcManager"
// 检测时间定义(毫秒)
#define PRESSURE_DETECTION_TIME_MS 2000 // 压力检测持续时间2秒
#define LOW_VALUE_DETECTION_TIME_MS 2000 // 低值检测持续时间2秒
AdcManager &AdcManager::GetInstance() {
static AdcManager instance;
return instance;
}
bool AdcManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "AdcManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing AdcManager...");
// 初始化ADC数组
memset(pressure_adc_values_, 0, sizeof(pressure_adc_values_));
InitializeAdc();
// InitializeDigitalOutput(); // 暂时注释掉DO初始化
// 先设置初始化状态,再启动任务
initialized_ = true;
// 初始化后立刻读取一次,便于快速确认链路
int init_read_raw = -1;
esp_err_t init_read_ret = adc_oneshot_read(
adc1_handle_, PRESSURE_SENSOR_ADC_CHANNEL, &init_read_raw);
if (init_read_ret != ESP_OK) {
ESP_LOGE(TAG, "Initial ADC read failed: %s",
esp_err_to_name(init_read_ret));
} else {
ESP_LOGI(TAG, "Initial ADC read ok: Raw=%d", init_read_raw);
}
// 启动ADC任务
StartAdcTask();
ESP_LOGI(TAG, "AdcManager initialized successfully");
return true;
}
void AdcManager::InitializeAdc() {
ESP_LOGI(TAG, "Initializing ADC for pressure sensor on GPIO4 (ADC1_CH3)...");
// 初始化ADC驱动
adc_oneshot_unit_init_cfg_t init_config1 = {
.unit_id = ADC_UNIT_1,
};
esp_err_t ret = adc_oneshot_new_unit(&init_config1, &adc1_handle_);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize ADC unit: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "ADC unit initialized successfully");
// 配置ADC通道
adc_oneshot_chan_cfg_t chan_config = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12,
};
ret = adc_oneshot_config_channel(adc1_handle_, PRESSURE_SENSOR_ADC_CHANNEL,
&chan_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure ADC channel %d: %s",
PRESSURE_SENSOR_ADC_CHANNEL, esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "ADC channel %d configured successfully",
PRESSURE_SENSOR_ADC_CHANNEL);
// 初始化ADC校准
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12,
};
ret = adc_cali_create_scheme_curve_fitting(&cali_config, &adc1_cali_handle_);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "ADC calibration not available: %s", esp_err_to_name(ret));
adc1_cali_handle_ = NULL;
} else {
ESP_LOGI(TAG, "ADC calibration initialized successfully");
}
ESP_LOGI(TAG, "ADC initialized for pressure sensor monitoring on GPIO4");
}
void AdcManager::ReadPressureSensorData() {
if (!initialized_) {
return;
}
int adc_value;
esp_err_t ret =
adc_oneshot_read(adc1_handle_, PRESSURE_SENSOR_ADC_CHANNEL, &adc_value);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to read pressure sensor ADC: %s",
esp_err_to_name(ret));
return;
}
// 直接使用原始ADC值不进行电压转换
current_pressure_value_ = adc_value;
// 压力检测触发音乐播放
static bool last_pressure_state = false;
static int64_t pressure_start_time = 0;
static bool pressure_triggered = false;
if (last_pressure_state) {
// 长时间不动检测逻辑
CheckLongTimeNoMovement(current_pressure_value_);
}
// 每隔5次打印一次详细日志便于定位问题
static int adc_log_counter = 0;
adc_log_counter++;
if (adc_log_counter >= 10) {
ESP_LOGI(TAG, "ADC read: Raw=%d", adc_value);
adc_log_counter = 0;
}
bool current_pressure_state = IsPressureDetected();
if (current_pressure_state && !last_pressure_state) {
// 压力开始检测,记录开始时间
pressure_start_time = esp_timer_get_time();
pressure_triggered = false;
ESP_LOGI(TAG, "Pressure detection started");
} else if (current_pressure_state && last_pressure_state) {
// 压力持续检测中检查是否超过2秒
int64_t current_time = esp_timer_get_time();
int64_t duration_ms =
(current_time - pressure_start_time) / 1000; // 转换为毫秒
if (duration_ms >= PRESSURE_DETECTION_TIME_MS && !pressure_triggered) {
ESP_LOGI(TAG,
"Pressure detected for %ld ms! Triggering music playback...",
(long)duration_ms);
// 触发音乐播放
TriggerMusicPlayback();
pressure_triggered = true; // 防止重复触发
}
} else if (!current_pressure_state && last_pressure_state) {
// 压力结束检测
ESP_LOGI(TAG, "Pressure detection ended");
pressure_triggered = false;
}
last_pressure_state = current_pressure_state;
// ADC值小于100时的暂停检测也需要持续2秒
static int64_t low_value_start_time = 0;
static bool low_value_triggered = false;
if (adc_value < 100) {
if (low_value_start_time == 0) {
// 第一次检测到小于100的值记录开始时间
low_value_start_time = esp_timer_get_time();
low_value_triggered = false;
ESP_LOGI(TAG, "ADC low value detection started (value: %d)", adc_value);
} else {
// 持续检测小于100的值检查是否超过2秒
int64_t current_time = esp_timer_get_time();
int64_t duration_ms =
(current_time - low_value_start_time) / 1000; // 转换为毫秒
if (duration_ms >= LOW_VALUE_DETECTION_TIME_MS && !low_value_triggered) {
ESP_LOGI(TAG,
"ADC low value detected for %ld ms! (value: %d) Triggering music pause...",
(long)duration_ms, adc_value);
TriggerMusicPauseback();
low_value_triggered = true; // 防止重复触发
}
}
} else {
// ADC值大于等于100重置低值检测
if (low_value_start_time != 0) {
ESP_LOGI(TAG, "ADC low value detection ended (value: %d)", adc_value);
low_value_start_time = 0;
low_value_triggered = false;
}
}
}
void AdcManager::StartAdcTask() {
if (!initialized_) {
ESP_LOGE(TAG, "AdcManager not initialized");
return;
}
BaseType_t ret =
xTaskCreate(AdcTask, "adc_task", 4096, this, 2, &adc_task_handle_);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create ADC task");
} else {
ESP_LOGI(TAG, "ADC task created successfully");
}
}
void AdcManager::StopAdcTask() {
if (adc_task_handle_) {
vTaskDelete(adc_task_handle_);
adc_task_handle_ = nullptr;
}
}
void AdcManager::AdcTask(void *pvParameters) {
AdcManager *manager = static_cast<AdcManager *>(pvParameters);
ESP_LOGI(TAG, "ADC task started");
while (true) {
if (manager->initialized_) {
manager->ReadPressureSensorData();
}
vTaskDelay(pdMS_TO_TICKS(100)); // 100ms间隔
}
}
int AdcManager::GetCurrentPressureValue() const {
return current_pressure_value_;
}
const int *AdcManager::GetPressureAdcValues() const {
return pressure_adc_values_;
}
size_t AdcManager::GetPressureSampleCount() const {
return (pressure_data_index_ == 0) ? kPressureAdcDataCount
: pressure_data_index_;
}
bool AdcManager::IsPressureDetected() const {
if (!initialized_) {
return false;
}
return current_pressure_value_ > 1000; // 压力阈值100
}
bool AdcManager::IsLightPressure() const {
if (!initialized_) {
return false;
}
return current_pressure_value_ > 500; // 轻压阈值500
}
void AdcManager::CheckLongTimeNoMovement(int adc_value) {
uint32_t current_time = esp_timer_get_time() / 1000000; // 转换为秒
// 计算ADC值变化
int adc_change = abs(adc_value - last_stable_value_);
if (adc_change > kMovementThreshold) {
// 有显著变化,重置不动检测
last_stable_value_ = adc_value;
no_movement_start_time_ = current_time;
is_no_movement_detected_ = false;
} else {
// 变化很小,检查是否长时间不动
if (no_movement_start_time_ == 0) {
no_movement_start_time_ = current_time;
}
uint32_t no_movement_duration = current_time - no_movement_start_time_;
if (no_movement_duration >= kLongTimeThreshold &&
!is_no_movement_detected_) {
is_no_movement_detected_ = true;
ESP_LOGW(TAG,
"Long time no movement detected! Duration: %lu seconds, ADC: %d",
no_movement_duration, adc_value);
// 停止音乐播放和语音交互
TriggerMusicPauseback();
}
}
}
bool AdcManager::IsLongTimeNoMovement() const {
if (!initialized_) {
return false;
}
return is_no_movement_detected_;
}
uint32_t AdcManager::GetNoMovementDuration() const {
if (!initialized_ || no_movement_start_time_ == 0) {
return 0;
}
uint32_t current_time = esp_timer_get_time() / 1000000;
return current_time - no_movement_start_time_;
}
void AdcManager::TriggerMusicPauseback() {
ESP_LOGI(TAG, "Triggering music pauseback");
auto music = Board::GetInstance().GetMusic();
if (!music) {
ESP_LOGI(TAG, "No music player found");
return;
}
// 检查音乐状态,避免重复操作
if (!music->IsPlaying() && !music->IsPaused()) {
ESP_LOGI(TAG, "Music is not playing or paused, skipping pause operation");
return;
}
music->PauseSong();
// 停止语音交互
auto &app = Application::GetInstance();
app.GetAudioService().EnableWakeWordDetection(false);
ESP_LOGI(TAG, "Stopped wake word detection due to long time no movement");
}
void AdcManager::TriggerMusicPlayback() {
ESP_LOGI(TAG, "Triggering music playback");
// 确保音频输出已启用
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (!codec) {
ESP_LOGE(TAG, "Audio codec not available");
return;
}
codec->EnableOutput(true);
ESP_LOGI(TAG, "Audio output enabled");
// 通过Board接口获取音乐播放器并触发播放
auto music = board.GetMusic();
if (!music) {
ESP_LOGI(TAG, "No music player found");
return;
}
if (music->IsPlaying()) {
ESP_LOGI(TAG, "Music is already playing");
return;
}
if (music->IsDownloading()) {
ESP_LOGI(TAG, "Music is already downloading");
return;
}
if (music->IsPaused()) {
ESP_LOGI(TAG, "Music is already paused");
music->ResumeSong();
return;
}
auto song_name = "稻香";
auto artist_name = "";
if (!music->Download(song_name, artist_name)) {
ESP_LOGI(TAG, "获取音乐资源失败");
return;
}
auto download_result = music->GetDownloadResult();
ESP_LOGI(TAG, "Music details result: %s", download_result.c_str());
}

View File

@@ -1,80 +0,0 @@
#ifndef ADC_MANAGER_H
#define ADC_MANAGER_H
#include "config.h"
#include <esp_adc/adc_oneshot.h>
#include <esp_adc/adc_cali.h>
#include <esp_adc/adc_cali_scheme.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
class AdcManager {
public:
static AdcManager& GetInstance();
// 初始化ADC系统
bool Initialize();
// 读取压感传感器数据
void ReadPressureSensorData();
// 获取当前压感值
int GetCurrentPressureValue() const;
// 获取ADC原始值数组
const int* GetPressureAdcValues() const;
// 获取有效样本数量
size_t GetPressureSampleCount() const;
// 启动/停止ADC任务
void StartAdcTask();
void StopAdcTask();
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
// 基于ADC值判断压力状态
bool IsPressureDetected() const;
bool IsLightPressure() const;
// 长时间不动检测
bool IsLongTimeNoMovement() const;
uint32_t GetNoMovementDuration() const; // 返回不动持续时间(秒)
// 压力检测触发音乐播放
void TriggerMusicPlayback();
void TriggerMusicPauseback();
private:
AdcManager() = default;
~AdcManager() = default;
AdcManager(const AdcManager&) = delete;
AdcManager& operator=(const AdcManager&) = delete;
void InitializeAdc();
void CheckLongTimeNoMovement(int adc_value);
static void AdcTask(void *pvParameters);
bool initialized_ = false;
adc_oneshot_unit_handle_t adc1_handle_;
adc_cali_handle_t adc1_cali_handle_;
// 压感传感器数据
static constexpr size_t kPressureAdcDataCount = 10;
int pressure_adc_values_[kPressureAdcDataCount];
size_t pressure_data_index_ = 0;
int current_pressure_value_ = 0;
// 长时间不动检测相关变量
int last_stable_value_ = 0;
uint32_t no_movement_start_time_ = 0;
bool is_no_movement_detected_ = false;
static constexpr int kMovementThreshold = 50; // ADC变化阈值
static constexpr uint32_t kLongTimeThreshold = 30; // 长时间阈值(秒)
// 任务句柄
TaskHandle_t adc_task_handle_ = nullptr;
};
#endif // ADC_MANAGER_H

View File

@@ -1,131 +0,0 @@
#include "button_manager.h"
#include "application.h"
#include <esp_log.h>
#define TAG "ButtonManager"
ButtonManager& ButtonManager::GetInstance() {
static ButtonManager instance;
return instance;
}
ButtonManager::ButtonManager()
: boot_button_(BOOT_BUTTON_GPIO),
volume_up_button_(VOLUME_UP_BUTTON_GPIO),
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) {
}
bool ButtonManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "ButtonManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing ButtonManager...");
// 设置按钮回调
SetupButtonCallbacks();
initialized_ = true;
ESP_LOGI(TAG, "ButtonManager initialized successfully");
return true;
}
void ButtonManager::SetupButtonCallbacks() {
ESP_LOGI(TAG, "Setting up button callbacks...");
// BOOT按钮回调
boot_button_.OnClick([]() {
ESP_LOGI(TAG, "Boot button clicked");
});
boot_button_.OnLongPress([]() {
ESP_LOGI(TAG, "BOOT long pressed: play boot tone");
// 确保音频输出已启用
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
if (!codec) {
ESP_LOGE(TAG, "Audio codec not available");
return;
}
codec->EnableOutput(true);
codec->SetOutputVolume(10);
auto music = Board::GetInstance().GetMusic();
if (!music) {
ESP_LOGE(TAG, "Music player not available");
return;
}
auto song_name = "稻香";
auto artist_name = "";
if (!music->Download(song_name, artist_name)) {
ESP_LOGI(TAG, "获取音乐资源失败");
return;
}
auto download_result = music->GetDownloadResult();
ESP_LOGI(TAG, "Music details result: %s", download_result.c_str());
});
// 音量上按钮回调
volume_up_button_.OnClick([]() {
ESP_LOGI(TAG, "Volume up button clicked");
// 通过AudioService间接控制音量
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
codec->SetOutputVolume(codec->output_volume() + 10);
ESP_LOGI(TAG, "Volume up requested");
});
volume_up_button_.OnLongPress([]() {
ESP_LOGI(TAG, "Volume up long pressed: switching to voice interaction mode");
// 播放进入语音交互模式的提示音
auto& app = Application::GetInstance();
app.PlaySound("success"); // 播放成功提示音
// 暂停音乐播放
auto music = Board::GetInstance().GetMusic();
if (music && music->IsPlaying()) {
music->PauseSong();
ESP_LOGI(TAG, "Music paused for voice interaction");
}
// 切换到语音交互模式
app.GetAudioService().EnableWakeWordDetection(true);
app.GetAudioService().EnableVoiceProcessing(true);
ESP_LOGI(TAG, "Switched to voice interaction mode - waiting for user voice input");
});
// 音量下按钮回调
volume_down_button_.OnClick([]() {
ESP_LOGI(TAG, "Volume down button clicked");
auto& board = Board::GetInstance();
auto codec = board.GetAudioCodec();
codec->SetOutputVolume(codec->output_volume() - 10);
ESP_LOGI(TAG, "Volume down requested");
});
volume_down_button_.OnLongPress([]() {
ESP_LOGI(TAG, "Volume down long pressed: stopping audio playback and voice interaction");
// 播放停止提示音
auto& app = Application::GetInstance();
app.PlaySound("exclamation"); // 播放感叹号提示音
// 停止音乐播放
auto music = Board::GetInstance().GetMusic();
if (music && music->IsPlaying()) {
music->PauseSong();
ESP_LOGI(TAG, "Music playback stopped");
}
// 停止语音交互
app.GetAudioService().EnableWakeWordDetection(false);
app.GetAudioService().EnableVoiceProcessing(false);
ESP_LOGI(TAG, "Voice interaction stopped");
});
}

View File

@@ -1,31 +0,0 @@
#ifndef BUTTON_MANAGER_H
#define BUTTON_MANAGER_H
#include "button.h"
#include "config.h"
class ButtonManager {
public:
static ButtonManager& GetInstance();
// 初始化按钮系统
bool Initialize();
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
ButtonManager();
~ButtonManager() = default;
ButtonManager(const ButtonManager&) = delete;
ButtonManager& operator=(const ButtonManager&) = delete;
void SetupButtonCallbacks();
bool initialized_ = false;
Button boot_button_;
Button volume_up_button_;
Button volume_down_button_;
};
#endif // BUTTON_MANAGER_H

View File

@@ -1,77 +0,0 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
// ESP32-S3 智能音箱开发板配置
// 音频配置
#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 16000
// 扬声器I2S配置
#define AUDIO_I2S_GPIO_WS GPIO_NUM_47 // 扬声器Word Select
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_17 // 扬声器Bit Clock
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_15 // 扬声器数据输出
// INMP441 I2S麦克风配置 (BCLK/WS/DIN)
// 说明: INMP441 是 I2S 数字麦,需要标准 I2S 三线
// 建议映射: BCLK=GPIO14, WS=GPIO38, DIN=GPIO16可按需要调整
#define AUDIO_MIC_I2S_BCLK GPIO_NUM_14
#define AUDIO_MIC_I2S_WS GPIO_NUM_38
#define AUDIO_MIC_I2S_DIN GPIO_NUM_16
// 用户交互
#define BUILTIN_LED_GPIO GPIO_NUM_48
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_41
// IMU传感器 (I2C接口)
#define IMU_I2C_SDA_PIN GPIO_NUM_21
#define IMU_I2C_SCL_PIN GPIO_NUM_20
#define IMU_INT_PIN GPIO_NUM_13
// 压感传感器 (ADC接口)
#define PRESSURE_SENSOR_ADC_CHANNEL ADC_CHANNEL_3 // GPIO4 (ADC1_CHANNEL_3)
// 功能IO定义
// LED控制
#define LED_RING_GPIO GPIO_NUM_6 // LED灯环控制
#define STATUS_LED_GPIO GPIO_NUM_10 // 状态指示灯
// 无显示配置
#define DISPLAY_SDA_PIN GPIO_NUM_NC
#define DISPLAY_SCL_PIN GPIO_NUM_NC
#define DISPLAY_WIDTH 0
#define DISPLAY_HEIGHT 0
#define DISPLAY_MIRROR_X false
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY false
#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
// 无摄像头配置
#define CAMERA_PIN_PWDN GPIO_NUM_NC
#define CAMERA_PIN_RESET GPIO_NUM_NC
#define CAMERA_PIN_XCLK GPIO_NUM_NC
#define CAMERA_PIN_SIOD GPIO_NUM_NC
#define CAMERA_PIN_SIOC GPIO_NUM_NC
#define CAMERA_PIN_D0 GPIO_NUM_NC
#define CAMERA_PIN_D1 GPIO_NUM_NC
#define CAMERA_PIN_D2 GPIO_NUM_NC
#define CAMERA_PIN_D3 GPIO_NUM_NC
#define CAMERA_PIN_D4 GPIO_NUM_NC
#define CAMERA_PIN_D5 GPIO_NUM_NC
#define CAMERA_PIN_D6 GPIO_NUM_NC
#define CAMERA_PIN_D7 GPIO_NUM_NC
#define CAMERA_PIN_VSYNC GPIO_NUM_NC
#define CAMERA_PIN_HREF GPIO_NUM_NC
#define CAMERA_PIN_PCLK GPIO_NUM_NC
// 开发板版本
#define SMART_SPEAKER_VERSION "1.0.0"
#endif // _BOARD_CONFIG_H_

View File

@@ -1,15 +0,0 @@
{
"target": "esp32s3",
"builds": [
{
"name": "esp32s3-smart-speaker",
"sdkconfig_append": [
"CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=n",
"CONFIG_LWIP_IPV6=n",
"CONFIG_USE_ESP_WAKE_WORD=y",
"CONFIG_USE_AUDIO_DEBUGGER=y",
"CONFIG_AUDIO_DEBUG_UDP_SERVER=\"192.168.122.143:8000\""
]
}
]
}

View File

@@ -1,151 +0,0 @@
#include "assets.h"
#include "adc_manager.h"
#include "button_manager.h"
#include "codecs/no_audio_codec.h"
#include "config.h"
#include "esplog_display.h"
#include "esp32_music.h"
#include "gpio_manager.h"
#include "imu_manager.h"
#include "led/single_led.h"
#include "tools_manager.h"
#include "wifi_board.h"
#include "wifi_manager.h"
#include <driver/i2c_master.h>
#include <esp_log.h>
#include <string>
#define TAG "SmartSpeaker"
class Esp32s3SmartSpeaker : public WifiBoard {
private:
// I2C总线句柄
i2c_master_bus_handle_t codec_i2c_bus_;
void InitializeManagers() {
ESP_LOGI(TAG, "Initializing managers...");
// 初始化各个管理器Initialize内部会自动启动任务
AdcManager::GetInstance().Initialize();
ImuManager::GetInstance().Initialize();
ButtonManager::GetInstance().Initialize();
GpioManager::GetInstance().Initialize();
ToolsManager::GetInstance().Initialize();
WifiManager::GetInstance().Initialize();
ESP_LOGI(TAG, "All managers initialized successfully");
}
void InitializeCodecI2c() {
return;
}
public:
Esp32s3SmartSpeaker() {
ESP_LOGI(TAG, "Initializing ESP32-S3 Smart Speaker");
// 初始化音乐播放器
music_ = new Esp32Music();
ESP_LOGI(TAG, "Music player initialized");
// 初始化I2C总线
InitializeCodecI2c();
// 初始化各个管理器
InitializeManagers();
ESP_LOGI(TAG, "ESP32-S3 Smart Speaker initialized successfully");
}
virtual ~Esp32s3SmartSpeaker() = default;
virtual std::string GetBoardType() override {
return std::string("esp32s3-smart-speaker");
}
virtual AudioCodec *GetAudioCodec() override {
static NoAudioCodecSimplex audio_codec(
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
// 扬声器(标准 I2S 输出)
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
// 麦克风(标准 I2S 输入,单声道)
AUDIO_MIC_I2S_BCLK,
AUDIO_MIC_I2S_WS,
AUDIO_MIC_I2S_DIN);
return &audio_codec;
}
virtual Display *GetDisplay() override {
static EspLogDisplay display;
return &display;
}
virtual Led *GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO);
return &led;
}
virtual std::string GetBoardJson() override {
char json_buffer[2048];
// 安全地获取管理器状态,避免在初始化过程中访问
bool imu_initialized = false;
bool adc_initialized = false;
int pressure_value = 0;
size_t pressure_sample_count = 0;
bool imu_sensor_initialized = false;
try {
auto& imu_manager = ImuManager::GetInstance();
imu_initialized = imu_manager.IsInitialized();
auto& adc_manager = AdcManager::GetInstance();
adc_initialized = adc_manager.IsInitialized();
pressure_value = adc_manager.GetCurrentPressureValue();
pressure_sample_count = adc_manager.GetPressureSampleCount();
auto imu_sensor = imu_manager.GetImuSensor();
imu_sensor_initialized = imu_sensor && imu_sensor->IsInitialized();
} catch (...) {
ESP_LOGW(TAG, "Error accessing managers in GetBoardJson, using default values");
}
snprintf(json_buffer, sizeof(json_buffer),
"{"
"\"board_type\":\"esp32s3-smart-speaker\","
"\"version\":\"%s\","
"\"features\":[\"audio\",\"imu\",\"pressure\",\"led_ring\",\"fan\",\"relay\",\"status_led\"],"
"\"audio_codec\":\"NoAudioCodecSimplex\","
"\"audio_method\":\"i2s_standard\","
"\"microphone\":\"INMP441_I2S\","
"\"speaker\":\"NoAudioCodec\","
"\"imu_initialized\":%s,"
"\"pressure_sensor_initialized\":%s,"
"\"pressure_sensor\":{\"current_value\":%d,\"adc_channel\":%d,\"sample_count\":%u},"
"\"imu_sensor\":{\"type\":\"MPU6050\",\"initialized\":%s,\"status\":\"unknown\"}"
"}",
SMART_SPEAKER_VERSION,
imu_initialized ? "true" : "false",
adc_initialized ? "true" : "false",
pressure_value,
PRESSURE_SENSOR_ADC_CHANNEL,
(unsigned int)pressure_sample_count,
imu_sensor_initialized ? "true" : "false"
);
ESP_LOGI(TAG, "GetBoardJson completed successfully");
return std::string(json_buffer);
}
virtual Assets *GetAssets() override {
static Assets assets(std::string(ASSETS_XIAOZHI_WAKENET_SMALL));
return &assets;
}
};
DECLARE_BOARD(Esp32s3SmartSpeaker);

View File

@@ -1,53 +0,0 @@
#include "gpio_manager.h"
#include <esp_log.h>
#define TAG "GpioManager"
GpioManager& GpioManager::GetInstance() {
static GpioManager instance;
return instance;
}
bool GpioManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "GpioManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing GpioManager...");
// 初始化GPIO输出
gpio_config_t io_conf = {};
// LED灯环控制
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL << LED_RING_GPIO);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
// 状态指示灯
io_conf.pin_bit_mask = (1ULL << STATUS_LED_GPIO);
gpio_config(&io_conf);
initialized_ = true;
ESP_LOGI(TAG, "GpioManager initialized successfully");
return true;
}
void GpioManager::SetLedRing(bool state) {
if (!initialized_) {
ESP_LOGE(TAG, "GpioManager not initialized");
return;
}
gpio_set_level(LED_RING_GPIO, state ? 1 : 0);
}
void GpioManager::SetStatusLed(bool state) {
if (!initialized_) {
ESP_LOGE(TAG, "GpioManager not initialized");
return;
}
gpio_set_level(STATUS_LED_GPIO, state ? 1 : 0);
}

View File

@@ -1,30 +0,0 @@
#ifndef GPIO_MANAGER_H
#define GPIO_MANAGER_H
#include "config.h"
#include <driver/gpio.h>
class GpioManager {
public:
static GpioManager& GetInstance();
// 初始化GPIO系统
bool Initialize();
// GPIO控制方法
void SetLedRing(bool state);
void SetStatusLed(bool state);
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
GpioManager() = default;
~GpioManager() = default;
GpioManager(const GpioManager&) = delete;
GpioManager& operator=(const GpioManager&) = delete;
bool initialized_ = false;
};
#endif // GPIO_MANAGER_H

View File

@@ -1,139 +0,0 @@
#include "imu_manager.h"
#include <esp_log.h>
#define TAG "ImuManager"
ImuManager& ImuManager::GetInstance() {
static ImuManager instance;
return instance;
}
bool ImuManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "ImuManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing ImuManager...");
InitializeImu();
// 启动IMU任务
StartImuTask();
initialized_ = true;
ESP_LOGI(TAG, "ImuManager initialized successfully");
return true;
}
void ImuManager::InitializeImu() {
ESP_LOGI(TAG, "Initializing MPU6050 IMU sensor...");
// IMU传感器I2C总线
i2c_master_bus_config_t imu_i2c_cfg = {
.i2c_port = I2C_NUM_1,
.sda_io_num = IMU_I2C_SDA_PIN,
.scl_io_num = IMU_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
.allow_pd = false,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&imu_i2c_cfg, &imu_i2c_bus_));
// 初始化MPU6050传感器
mpu6050_sensor_ = std::make_unique<Mpu6050Sensor>(imu_i2c_bus_);
if (mpu6050_sensor_) {
uint8_t device_id;
if (mpu6050_sensor_->GetDeviceId(&device_id)) {
ESP_LOGI(TAG, "MPU6050 device ID: 0x%02X", device_id);
if (device_id == MPU6050_WHO_AM_I_VAL) {
if (mpu6050_sensor_->Initialize(ACCE_FS_4G, GYRO_FS_500DPS)) {
if (mpu6050_sensor_->WakeUp()) {
initialized_ = true;
ESP_LOGI(TAG, "MPU6050 sensor initialized successfully");
} else {
ESP_LOGE(TAG, "Failed to wake up MPU6050");
}
} else {
ESP_LOGE(TAG, "Failed to initialize MPU6050");
}
} else {
ESP_LOGE(TAG, "MPU6050 device ID mismatch: expected 0x%02X, got 0x%02X",
MPU6050_WHO_AM_I_VAL, device_id);
}
} else {
ESP_LOGE(TAG, "Failed to read MPU6050 device ID");
}
} else {
ESP_LOGE(TAG, "Failed to create MPU6050 sensor instance");
}
if (!initialized_) {
ESP_LOGW(TAG, "IMU sensor initialization failed - continuing without IMU");
}
}
void ImuManager::StartImuTask() {
if (!initialized_) {
ESP_LOGW(TAG, "ImuManager not initialized, skipping IMU task creation");
return;
}
BaseType_t ret = xTaskCreate(ImuDataTask, "imu_data_task", 4096, this, 5, &imu_task_handle_);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create IMU data task");
} else {
ESP_LOGI(TAG, "IMU data task created successfully");
}
}
void ImuManager::StopImuTask() {
if (imu_task_handle_) {
vTaskDelete(imu_task_handle_);
imu_task_handle_ = nullptr;
}
}
void ImuManager::ImuDataTask(void *pvParameters) {
ImuManager *manager = static_cast<ImuManager *>(pvParameters);
ESP_LOGI(TAG, "IMU data task started");
mpu6050_acce_value_t acce;
mpu6050_gyro_value_t gyro;
mpu6050_temp_value_t temp;
complimentary_angle_t angle;
while (true) {
if (manager->mpu6050_sensor_ && manager->initialized_) {
// 读取加速度计数据
if (manager->mpu6050_sensor_->GetAccelerometer(&acce)) {
ESP_LOGI(TAG, "Accelerometer - X:%.2f, Y:%.2f, Z:%.2f", acce.acce_x,
acce.acce_y, acce.acce_z);
}
// 读取陀螺仪数据
if (manager->mpu6050_sensor_->GetGyroscope(&gyro)) {
ESP_LOGI(TAG, "Gyroscope - X:%.2f, Y:%.2f, Z:%.2f", gyro.gyro_x,
gyro.gyro_y, gyro.gyro_z);
}
// 读取温度数据
if (manager->mpu6050_sensor_->GetTemperature(&temp)) {
ESP_LOGI(TAG, "Temperature: %.2f°C", temp.temp);
}
// 计算姿态角
if (manager->mpu6050_sensor_->ComplimentaryFilter(&acce, &gyro, &angle)) {
ESP_LOGI(TAG, "Attitude - Pitch:%.2f°, Roll:%.2f°, Yaw:%.2f°",
angle.pitch, angle.roll, angle.yaw);
}
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}

View File

@@ -1,43 +0,0 @@
#ifndef IMU_MANAGER_H
#define IMU_MANAGER_H
#include "config.h"
#include "mpu6050_sensor.h"
#include <driver/i2c_master.h>
#include <memory>
class ImuManager {
public:
static ImuManager& GetInstance();
// 初始化IMU系统
bool Initialize();
// 启动/停止IMU任务
void StartImuTask();
void StopImuTask();
// 获取IMU传感器实例
Mpu6050Sensor* GetImuSensor() const { return mpu6050_sensor_.get(); }
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
ImuManager() = default;
~ImuManager() = default;
ImuManager(const ImuManager&) = delete;
ImuManager& operator=(const ImuManager&) = delete;
void InitializeImu();
static void ImuDataTask(void *pvParameters);
bool initialized_ = false;
i2c_master_bus_handle_t imu_i2c_bus_;
std::unique_ptr<Mpu6050Sensor> mpu6050_sensor_;
// 任务句柄
TaskHandle_t imu_task_handle_ = nullptr;
};
#endif // IMU_MANAGER_H

View File

@@ -1,280 +0,0 @@
#include "mpu6050_sensor.h"
#include <esp_timer.h>
#include <cmath>
const char* Mpu6050Sensor::TAG = "MPU6050";
Mpu6050Sensor::Mpu6050Sensor(i2c_master_bus_handle_t i2c_bus, uint8_t device_addr)
: i2c_bus_(i2c_bus), device_addr_(device_addr), initialized_(false),
acce_fs_(ACCE_FS_4G), gyro_fs_(GYRO_FS_500DPS), dt_(0.0f), alpha_(0.98f) {
// 初始化设备句柄
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = device_addr_,
.scl_speed_hz = 100000,
};
esp_err_t ret = i2c_master_bus_add_device(i2c_bus_, &dev_cfg, &device_handle_);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to add MPU6050 device to I2C bus: %s", esp_err_to_name(ret));
device_handle_ = nullptr;
}
// 初始化互补滤波
InitializeComplimentaryFilter();
}
Mpu6050Sensor::~Mpu6050Sensor() {
if (device_handle_) {
i2c_master_bus_rm_device(device_handle_);
}
}
bool Mpu6050Sensor::Initialize(mpu6050_acce_fs_t acce_fs, mpu6050_gyro_fs_t gyro_fs) {
if (!device_handle_) {
ESP_LOGE(TAG, "Device handle is null");
return false;
}
acce_fs_ = acce_fs;
gyro_fs_ = gyro_fs;
// 配置加速度计量程
uint8_t acce_config = (acce_fs << 3) & 0x18;
if (!WriteRegister(0x1C, acce_config)) {
ESP_LOGE(TAG, "Failed to configure accelerometer");
return false;
}
// 配置陀螺仪量程
uint8_t gyro_config = (gyro_fs << 3) & 0x18;
if (!WriteRegister(0x1B, gyro_config)) {
ESP_LOGE(TAG, "Failed to configure gyroscope");
return false;
}
// 配置数字低通滤波器
if (!WriteRegister(0x1A, 0x06)) { // DLPF_CFG = 6 (5Hz)
ESP_LOGE(TAG, "Failed to configure DLPF");
return false;
}
// 配置采样率 (1kHz / (1 + 7) = 125Hz)
if (!WriteRegister(0x19, 0x07)) {
ESP_LOGE(TAG, "Failed to configure sample rate");
return false;
}
initialized_ = true;
ESP_LOGI(TAG, "MPU6050 initialized successfully");
ESP_LOGI(TAG, "Accelerometer range: %d, Gyroscope range: %d", acce_fs, gyro_fs);
return true;
}
bool Mpu6050Sensor::WakeUp() {
if (!device_handle_) {
ESP_LOGE(TAG, "Device handle is null");
return false;
}
// 清除睡眠模式位
if (!WriteRegister(0x6B, 0x00)) {
ESP_LOGE(TAG, "Failed to wake up MPU6050");
return false;
}
// 等待传感器稳定
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "MPU6050 woken up");
return true;
}
bool Mpu6050Sensor::GetDeviceId(uint8_t* device_id) {
if (!device_id) {
ESP_LOGE(TAG, "Device ID pointer is null");
return false;
}
return ReadRegister(MPU6050_WHO_AM_I_REG, device_id, 1);
}
bool Mpu6050Sensor::GetAccelerometer(mpu6050_acce_value_t* acce) {
if (!acce) {
ESP_LOGE(TAG, "Accelerometer data pointer is null");
return false;
}
uint8_t data[6];
if (!ReadRegister(0x3B, data, 6)) {
ESP_LOGE(TAG, "Failed to read accelerometer data");
return false;
}
// 组合16位数据
int16_t raw_x = (data[0] << 8) | data[1];
int16_t raw_y = (data[2] << 8) | data[3];
int16_t raw_z = (data[4] << 8) | data[5];
// 根据量程转换为g值
float scale_factor;
switch (acce_fs_) {
case ACCE_FS_2G: scale_factor = 16384.0f; break;
case ACCE_FS_4G: scale_factor = 8192.0f; break;
case ACCE_FS_8G: scale_factor = 4096.0f; break;
case ACCE_FS_16G: scale_factor = 2048.0f; break;
default: scale_factor = 8192.0f; break;
}
acce->acce_x = raw_x / scale_factor;
acce->acce_y = raw_y / scale_factor;
acce->acce_z = raw_z / scale_factor;
return true;
}
bool Mpu6050Sensor::GetGyroscope(mpu6050_gyro_value_t* gyro) {
if (!gyro) {
ESP_LOGE(TAG, "Gyroscope data pointer is null");
return false;
}
uint8_t data[6];
if (!ReadRegister(0x43, data, 6)) {
ESP_LOGE(TAG, "Failed to read gyroscope data");
return false;
}
// 组合16位数据
int16_t raw_x = (data[0] << 8) | data[1];
int16_t raw_y = (data[2] << 8) | data[3];
int16_t raw_z = (data[4] << 8) | data[5];
// 根据量程转换为度/秒
float scale_factor;
switch (gyro_fs_) {
case GYRO_FS_250DPS: scale_factor = 131.0f; break;
case GYRO_FS_500DPS: scale_factor = 65.5f; break;
case GYRO_FS_1000DPS: scale_factor = 32.8f; break;
case GYRO_FS_2000DPS: scale_factor = 16.4f; break;
default: scale_factor = 65.5f; break;
}
gyro->gyro_x = raw_x / scale_factor;
gyro->gyro_y = raw_y / scale_factor;
gyro->gyro_z = raw_z / scale_factor;
return true;
}
bool Mpu6050Sensor::GetTemperature(mpu6050_temp_value_t* temp) {
if (!temp) {
ESP_LOGE(TAG, "Temperature data pointer is null");
return false;
}
uint8_t data[2];
if (!ReadRegister(0x41, data, 2)) {
ESP_LOGE(TAG, "Failed to read temperature data");
return false;
}
// 组合16位数据
int16_t raw_temp = (data[0] << 8) | data[1];
// 转换为摄氏度: T = (TEMP_OUT / 340) + 36.53
temp->temp = raw_temp / 340.0f + 36.53f;
return true;
}
bool Mpu6050Sensor::ComplimentaryFilter(const mpu6050_acce_value_t* acce,
const mpu6050_gyro_value_t* gyro,
complimentary_angle_t* angle) {
if (!acce || !gyro || !angle) {
ESP_LOGE(TAG, "Input pointers are null");
return false;
}
uint64_t current_time = GetCurrentTimeUs();
// 计算时间间隔
if (last_time_ > 0) {
dt_ = (current_time - last_time_) / 1000000.0f; // 转换为秒
} else {
dt_ = 0.01f; // 默认10ms
}
// 从加速度计计算俯仰角和横滚角
float accel_pitch = atan2f(acce->acce_y, sqrtf(acce->acce_x * acce->acce_x + acce->acce_z * acce->acce_z)) * 180.0f / M_PI;
float accel_roll = atan2f(-acce->acce_x, acce->acce_z) * 180.0f / M_PI;
// 互补滤波
angle->pitch = alpha_ * (last_angle_.pitch + gyro->gyro_x * dt_) + (1.0f - alpha_) * accel_pitch;
angle->roll = alpha_ * (last_angle_.roll + gyro->gyro_y * dt_) + (1.0f - alpha_) * accel_roll;
angle->yaw = last_angle_.yaw + gyro->gyro_z * dt_; // 偏航角只能通过陀螺仪积分
// 更新上次的角度和时间
last_angle_ = *angle;
last_time_ = current_time;
return true;
}
std::string Mpu6050Sensor::GetStatusJson() const {
std::string json = "{";
json += "\"initialized\":" + std::string(initialized_ ? "true" : "false") + ",";
json += "\"device_address\":" + std::to_string(device_addr_) + ",";
json += "\"accelerometer_range\":" + std::to_string(static_cast<int>(acce_fs_)) + ",";
json += "\"gyroscope_range\":" + std::to_string(static_cast<int>(gyro_fs_)) + ",";
json += "\"filter_alpha\":" + std::to_string(alpha_) + ",";
json += "\"sample_rate\":125";
json += "}";
return json;
}
bool Mpu6050Sensor::WriteRegister(uint8_t reg_addr, uint8_t data) {
if (!device_handle_) {
return false;
}
uint8_t write_buf[2] = {reg_addr, data};
esp_err_t ret = i2c_master_transmit(device_handle_, write_buf, 2, 1000);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to write register 0x%02X: %s", reg_addr, esp_err_to_name(ret));
return false;
}
return true;
}
bool Mpu6050Sensor::ReadRegister(uint8_t reg_addr, uint8_t* data, size_t len) {
if (!device_handle_ || !data) {
return false;
}
esp_err_t ret = i2c_master_transmit_receive(device_handle_, &reg_addr, 1, data, len, 1000);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to read register 0x%02X: %s", reg_addr, esp_err_to_name(ret));
return false;
}
return true;
}
uint64_t Mpu6050Sensor::GetCurrentTimeUs() {
return esp_timer_get_time();
}
void Mpu6050Sensor::InitializeComplimentaryFilter() {
last_angle_.pitch = 0.0f;
last_angle_.roll = 0.0f;
last_angle_.yaw = 0.0f;
last_time_ = 0;
dt_ = 0.01f;
alpha_ = 0.98f; // 互补滤波系数0.98表示更信任陀螺仪
}

View File

@@ -1,184 +0,0 @@
#ifndef MPU6050_SENSOR_H
#define MPU6050_SENSOR_H
#include <driver/i2c_master.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <string>
// MPU6050相关定义
#define MPU6050_I2C_ADDRESS 0x68
#define MPU6050_WHO_AM_I_REG 0x75
#define MPU6050_WHO_AM_I_VAL 0x68
// 加速度计量程
typedef enum {
ACCE_FS_2G = 0, // ±2g
ACCE_FS_4G = 1, // ±4g
ACCE_FS_8G = 2, // ±8g
ACCE_FS_16G = 3 // ±16g
} mpu6050_acce_fs_t;
// 陀螺仪量程
typedef enum {
GYRO_FS_250DPS = 0, // ±250°/s
GYRO_FS_500DPS = 1, // ±500°/s
GYRO_FS_1000DPS = 2, // ±1000°/s
GYRO_FS_2000DPS = 3 // ±2000°/s
} mpu6050_gyro_fs_t;
// 传感器数据结构
typedef struct {
float acce_x;
float acce_y;
float acce_z;
} mpu6050_acce_value_t;
typedef struct {
float gyro_x;
float gyro_y;
float gyro_z;
} mpu6050_gyro_value_t;
typedef struct {
float temp;
} mpu6050_temp_value_t;
typedef struct {
float pitch;
float roll;
float yaw;
} complimentary_angle_t;
/**
* @brief MPU6050传感器封装类
*
* 提供现代化的C++接口来操作MPU6050六轴传感器
* 支持加速度计、陀螺仪、温度传感器和互补滤波
*/
class Mpu6050Sensor {
public:
/**
* @brief 构造函数
* @param i2c_bus I2C总线句柄
* @param device_addr 设备地址默认为0x68
*/
explicit Mpu6050Sensor(i2c_master_bus_handle_t i2c_bus, uint8_t device_addr = MPU6050_I2C_ADDRESS);
/**
* @brief 析构函数
*/
~Mpu6050Sensor();
/**
* @brief 初始化传感器
* @param acce_fs 加速度计量程
* @param gyro_fs 陀螺仪量程
* @return true表示初始化成功false表示失败
*/
bool Initialize(mpu6050_acce_fs_t acce_fs = ACCE_FS_4G, mpu6050_gyro_fs_t gyro_fs = GYRO_FS_500DPS);
/**
* @brief 唤醒传感器
* @return true表示成功false表示失败
*/
bool WakeUp();
/**
* @brief 获取设备ID
* @param device_id 输出设备ID
* @return true表示成功false表示失败
*/
bool GetDeviceId(uint8_t* device_id);
/**
* @brief 获取加速度计数据
* @param acce 输出加速度计数据
* @return true表示成功false表示失败
*/
bool GetAccelerometer(mpu6050_acce_value_t* acce);
/**
* @brief 获取陀螺仪数据
* @param gyro 输出陀螺仪数据
* @return true表示成功false表示失败
*/
bool GetGyroscope(mpu6050_gyro_value_t* gyro);
/**
* @brief 获取温度数据
* @param temp 输出温度数据
* @return true表示成功false表示失败
*/
bool GetTemperature(mpu6050_temp_value_t* temp);
/**
* @brief 互补滤波计算姿态角
* @param acce 加速度计数据
* @param gyro 陀螺仪数据
* @param angle 输出姿态角
* @return true表示成功false表示失败
*/
bool ComplimentaryFilter(const mpu6050_acce_value_t* acce,
const mpu6050_gyro_value_t* gyro,
complimentary_angle_t* angle);
/**
* @brief 检查传感器是否已初始化
* @return true表示已初始化false表示未初始化
*/
bool IsInitialized() const { return initialized_; }
/**
* @brief 获取传感器状态信息
* @return JSON格式的状态信息
*/
std::string GetStatusJson() const;
private:
i2c_master_bus_handle_t i2c_bus_;
i2c_master_dev_handle_t device_handle_;
uint8_t device_addr_;
bool initialized_;
mpu6050_acce_fs_t acce_fs_;
mpu6050_gyro_fs_t gyro_fs_;
// 互补滤波相关
float dt_;
float alpha_;
complimentary_angle_t last_angle_;
uint64_t last_time_;
static const char* TAG;
/**
* @brief 写入寄存器
* @param reg_addr 寄存器地址
* @param data 数据
* @return true表示成功false表示失败
*/
bool WriteRegister(uint8_t reg_addr, uint8_t data);
/**
* @brief 读取寄存器
* @param reg_addr 寄存器地址
* @param data 输出数据
* @param len 数据长度
* @return true表示成功false表示失败
*/
bool ReadRegister(uint8_t reg_addr, uint8_t* data, size_t len);
/**
* @brief 获取当前时间戳(微秒)
* @return 时间戳
*/
uint64_t GetCurrentTimeUs();
/**
* @brief 初始化互补滤波
*/
void InitializeComplimentaryFilter();
};
#endif // MPU6050_SENSOR_H

View File

@@ -1,199 +0,0 @@
#include "tools_manager.h"
#include "application.h"
#include "board.h"
#include <esp_log.h>
#define TAG "ToolsManager"
ToolsManager& ToolsManager::GetInstance() {
static ToolsManager instance;
return instance;
}
bool ToolsManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "ToolsManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing ToolsManager...");
// 注册各种工具
RegisterMcpTools();
RegisterSystemTools();
RegisterAudioTools();
RegisterSensorTools();
initialized_ = true;
ESP_LOGI(TAG, "ToolsManager initialized successfully");
return true;
}
void ToolsManager::RegisterMcpTools() {
ESP_LOGI(TAG, "Registering MCP tools...");
auto& mcp_server = McpServer::GetInstance();
// 系统信息查询工具
mcp_server.AddTool(
"self.smart_speaker.get_system_info",
"获取智能音箱系统信息,包括板卡类型、版本、功能特性等",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& board = Board::GetInstance();
return board.GetBoardJson();
}
);
// 设备状态查询工具
mcp_server.AddTool(
"self.smart_speaker.get_device_state",
"获取设备当前状态,包括启动状态、连接状态等",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
DeviceState state = app.GetDeviceState();
const char* state_str = "unknown";
switch (state) {
case kDeviceStateStarting: state_str = "starting"; break;
case kDeviceStateWifiConfiguring: state_str = "configuring"; break;
case kDeviceStateIdle: state_str = "idle"; break;
case kDeviceStateConnecting: state_str = "connecting"; break;
case kDeviceStateListening: state_str = "listening"; break;
case kDeviceStateSpeaking: state_str = "speaking"; break;
case kDeviceStateUpgrading: state_str = "upgrading"; break;
case kDeviceStateActivating: state_str = "activating"; break;
case kDeviceStateAudioTesting: state_str = "audio_testing"; break;
case kDeviceStateFatalError: state_str = "fatal_error"; break;
default: state_str = "unknown"; break;
}
return std::string("{\"state\":\"") + state_str + "\"}";
}
);
ESP_LOGI(TAG, "MCP tools registered successfully");
}
void ToolsManager::RegisterSystemTools() {
ESP_LOGI(TAG, "Registering system tools...");
auto& mcp_server = McpServer::GetInstance();
// 系统重启工具
mcp_server.AddTool(
"self.smart_speaker.reboot",
"重启智能音箱系统",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
app.Reboot();
return "{\"message\":\"System reboot initiated\"}";
}
);
// 设备控制工具
mcp_server.AddTool(
"self.smart_speaker.start_listening",
"开始语音监听",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
app.StartListening();
return "{\"message\":\"Started listening\"}";
}
);
mcp_server.AddTool(
"self.smart_speaker.stop_listening",
"停止语音监听",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
app.StopListening();
return "{\"message\":\"Stopped listening\"}";
}
);
ESP_LOGI(TAG, "System tools registered successfully");
}
void ToolsManager::RegisterAudioTools() {
ESP_LOGI(TAG, "Registering audio tools...");
auto& mcp_server = McpServer::GetInstance();
// 音频播放工具
mcp_server.AddTool(
"self.smart_speaker.play_sound",
"播放指定音效。sound: 音效名称(activation, welcome, upgrade, wificonfig等)",
PropertyList({Property("sound", kPropertyTypeString, "activation")}),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
std::string sound = properties["sound"].value<std::string>();
app.PlaySound(sound);
return "{\"message\":\"Playing sound: " + sound + "\"}";
}
);
// 语音检测状态工具
mcp_server.AddTool(
"self.smart_speaker.is_voice_detected",
"检查是否检测到语音",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& app = Application::GetInstance();
bool voice_detected = app.IsVoiceDetected();
return "{\"voice_detected\":" + std::string(voice_detected ? "true" : "false") + "}";
}
);
ESP_LOGI(TAG, "Audio tools registered successfully");
}
void ToolsManager::RegisterSensorTools() {
ESP_LOGI(TAG, "Registering sensor tools...");
auto& mcp_server = McpServer::GetInstance();
// 压感传感器读取工具
mcp_server.AddTool(
"self.smart_speaker.get_pressure_sensor",
"获取压感传感器数据包括当前值、ADC通道、样本数量等",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& board = Board::GetInstance();
std::string board_json = board.GetBoardJson();
// 从board JSON中提取压感传感器信息
// 这里简化处理直接返回board信息中包含的传感器数据
return board_json;
}
);
// IMU传感器状态工具
mcp_server.AddTool(
"self.smart_speaker.get_imu_status",
"获取IMU传感器状态信息",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
auto& board = Board::GetInstance();
std::string board_json = board.GetBoardJson();
// 从board JSON中提取IMU信息
return board_json;
}
);
// 传感器数据重置工具
mcp_server.AddTool(
"self.smart_speaker.reset_sensor_data",
"重置传感器数据缓冲区",
PropertyList(),
[](const PropertyList& properties) -> ReturnValue {
// TODO: 实现传感器数据重置
return "{\"message\":\"Sensor data reset requested\"}";
}
);
ESP_LOGI(TAG, "Sensor tools registered successfully");
}

View File

@@ -1,32 +0,0 @@
#ifndef TOOLS_MANAGER_H
#define TOOLS_MANAGER_H
#include <string>
#include "mcp_server.h"
class ToolsManager {
public:
static ToolsManager& GetInstance();
// 初始化工具系统
bool Initialize();
// 工具注册方法
void RegisterMcpTools();
void RegisterSystemTools();
void RegisterAudioTools();
void RegisterSensorTools();
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
ToolsManager() = default;
~ToolsManager() = default;
ToolsManager(const ToolsManager&) = delete;
ToolsManager& operator=(const ToolsManager&) = delete;
bool initialized_ = false;
};
#endif // TOOLS_MANAGER_H

View File

@@ -1,55 +0,0 @@
#include "wifi_manager.h"
#include "settings.h"
#include "wifi_station.h"
#include <esp_log.h>
#define TAG "WifiManager"
WifiManager& WifiManager::GetInstance() {
static WifiManager instance;
return instance;
}
bool WifiManager::Initialize() {
if (initialized_) {
ESP_LOGW(TAG, "WifiManager already initialized");
return true;
}
ESP_LOGI(TAG, "Initializing WifiManager...");
// 配置WiFi设置
ConfigureWifiSettings();
// 设置默认凭据
SetDefaultCredentials();
initialized_ = true;
ESP_LOGI(TAG, "WifiManager initialized successfully");
return true;
}
void WifiManager::ConfigureWifiSettings() {
ESP_LOGI(TAG, "Configuring WiFi settings...");
// 配置WiFi参数到NVS
Settings wifi_settings("wifi", true);
// 设置不记住BSSID (不区分MAC地址)
wifi_settings.SetInt("remember_bssid", 0);
// 设置最大发射功率
wifi_settings.SetInt("max_tx_power", 0);
ESP_LOGI(TAG, "WiFi settings configured");
}
void WifiManager::SetDefaultCredentials() {
ESP_LOGI(TAG, "Setting default WiFi credentials...");
// 添加默认WiFi配置
auto &wifi_station = WifiStation::GetInstance();
wifi_station.AddAuth("xoxo", "12340000");
ESP_LOGI(TAG, "Default WiFi credentials added: SSID=xoxo, Password=12340000");
}

View File

@@ -1,30 +0,0 @@
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H
#include <string>
class WifiManager {
public:
static WifiManager& GetInstance();
// 初始化WiFi系统
bool Initialize();
// WiFi配置方法
void SetDefaultCredentials();
void ConfigureWifiSettings();
// 检查是否已初始化
bool IsInitialized() const { return initialized_; }
private:
WifiManager() = default;
~WifiManager() = default;
WifiManager(const WifiManager&) = delete;
WifiManager& operator=(const WifiManager&) = delete;
bool initialized_ = false;
};
#endif // WIFI_MANAGER_H

View File

@@ -0,0 +1,185 @@
# 天气时钟界面说明
## 📋 概述
已成功将Arduino版本的MiniTV天气时钟UI移植到ESP-IDF LVGL环境中。新的天气时钟界面完全替换了原来的简单时钟界面提供更丰富的信息显示。
## 🎨 界面布局
参考Arduino版本新界面采用240x240分区布局
```
┌─────────────────────────────────┐
│ 天气信息滚动 │ 城市名称 │ 顶部 (0-34px)
├─────────────────────────────────┤
│ │
│ 时:分 秒 │ 中部 (35-165px)
│ (大字体) (小字体) │
│ │
├─────┬─────────────┬─────────────┤
│ AQI │ 湿度图标 │ 温度图标 │ 底部上 (166-200px)
│空气 │ 💧XX% │ 🌡XX℃ │
├─────┼─────────────┼─────────────┤
│周X │ XX月XX日 │ (空白) │ 底部下 (200-240px)
└─────┴─────────────┴─────────────┘
```
## ✨ 新增功能
### 1. 天气数据显示
- ✅ 城市名称
- ✅ 实时温度
- ✅ 湿度百分比
- ✅ 空气质量指数AQI和等级
- ✅ 最高/最低温度
- ✅ 风向和风速
### 2. 动态信息滚动
顶部滚动区域每2.5秒切换显示:
- 实时天气状况
- 空气质量描述
- 风向风速信息
- 今日天气概况
- 最低/最高温度
### 3. AQI颜色编码
空气质量指数自动显示对应颜色:
- 🟢 优 (0-50): 绿色
- 🟡 良 (51-100): 黄色
- 🟠 轻度污染 (101-150): 橙色
- 🟣 中度污染 (151-200): 紫红色
- 🔴 重度污染 (200+): 深红色
### 4. 自动更新
- ⏰ 时间:每秒更新
- 🌤️ 天气每10分钟更新一次
## 📁 新增文件
1. **idle_screen.h** - 天气时钟UI头文件重写
2. **idle_screen.cc** - 天气时钟UI实现重写
3. **weather_service.h** - 天气API服务头文件
4. **weather_service.cc** - 天气API服务实现
## 🔧 修改文件
1. **genjutech-s3-1.54tft.cc**
- 添加 `SpiLcdDisplayEx` 类扩展
- 集成天气服务
- 添加 `InitWeatherService()` 方法
- 自动启动天气更新任务
## 🌐 天气API
使用中国天气网API
- 城市代码自动检测:`http://wgeo.weather.com.cn/ip/`
- 天气数据获取:`http://d1.weather.com.cn/weather_index/{城市代码}.html`
### 🎯 城市代码配置(两种方式)
#### 方式1自动检测推荐
**默认已启用**WiFi连接后会根据IP地址自动获取所在城市
`genjutech-s3-1.54tft.cc``InitWeatherService()` 中:
```cpp
self->weather_service_.Initialize(""); // 空字符串 = 自动检测
```
**优点**
- ✅ 无需手动配置
- ✅ 根据实际位置显示天气
- ✅ 设备移动到其他城市会自动适应
- ✅ 失败时自动回退到北京
#### 方式2手动指定城市代码
如果你想固定显示某个城市的天气,可以指定城市代码:
```cpp
self->weather_service_.Initialize("101280601"); // 指定:深圳
```
**常用城市代码:**
- 北京:`101010100`
- 上海:`101020100`
- 广州:`101280101`
- 深圳:`101280601`
- 成都:`101270101`
- 杭州:`101210101`
- 武汉:`101200101`
- 西安:`101110101`
- 青岛:`101120201`
- 南京:`101190101`
- 重庆:`101040100`
- 天津:`101030100`
**优点**
- ✅ 可查看其他城市天气
- ✅ 不受网络IP地址影响
## 🎯 使用说明
### 1. 编译
```bash
idf.py build
```
### 2. 烧录
```bash
idf.py flash monitor
```
### 3. 配置城市
修改 `genjutech-s3-1.54tft.cc` 中的城市代码后重新编译烧录。
## 🔄 与原有功能的集成
- ✅ 保留语音交互功能
- ✅ 保留闹钟功能
- ✅ 闹钟触发时在时钟界面上显示
- ✅ 设备空闲时自动显示天气时钟
- ✅ 语音交互时自动切换到聊天界面
## 🐛 故障排查
### 天气数据不显示
1. 检查WiFi连接状态
2. 检查城市代码是否正确
3. 查看串口日志中的天气API响应
### 界面显示异常
1. 确认LVGL初始化正常
2. 检查显示锁是否正确使用
3. 查看内存使用情况
## 📝 注意事项
1. 天气更新需要WiFi连接
2. 首次获取天气数据需等待5秒WiFi连接时间
3. 天气API可能受网络状况影响
4. 建议在WiFi稳定环境下使用
## 🎨 设计理念
- **简洁实用**:一屏显示所有关键信息
- **视觉清晰**:分区明确,信息层次分明
- **动态更新**:滚动显示更多天气详情
- **色彩编码**AQI用颜色直观表示空气质量
## 🚀 未来扩展
可能的扩展方向:
- [ ] 支持多城市切换
- [ ] 添加未来天气预报
- [ ] 自定义UI主题和颜色
- [ ] 添加天气图标/动画
- [ ] 支持更多天气数据源
---
**移植时间**: 2025-01-10
**原始参考**: Arduino MiniTV Weather Clock by DIY攻城狮
**实现版本**: ESP-IDF + LVGL

View File

@@ -40,4 +40,7 @@
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
/**< use new idle screen of xiaozhi */
#define IDLE_SCREEN_HOOK 1
#endif // _BOARD_CONFIG_H_

View File

@@ -18,9 +18,160 @@
#include "assets/lang_config.h"
#include "power_manager.h"
#include "alarm_manager.h" // 用于检测和停止闹钟
#if IDLE_SCREEN_HOOK
#include "idle_screen.h"
#include "weather_service.h"
#endif
#define TAG "GenJuTech_s3_1_54TFT"
#if IDLE_SCREEN_HOOK
LV_FONT_DECLARE(font_puhui_20_4);
// Extended SpiLcdDisplay with idle screen support
class SpiLcdDisplayEx : public SpiLcdDisplay {
public:
SpiLcdDisplayEx(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
int width, int height, int offset_x, int offset_y,
bool mirror_x, bool mirror_y, bool swap_xy) :
SpiLcdDisplay(panel_io, panel,
width, height, offset_x, offset_y,
mirror_x, mirror_y, swap_xy) {
DisplayLockGuard lock(this);
lv_obj_set_style_pad_left(status_bar_, 20, 0);
lv_obj_set_style_pad_right(status_bar_, 20, 0);
}
virtual void OnStateChanged() override {
DisplayLockGuard lock(this);
auto& app = Application::GetInstance();
auto device_state = app.GetDeviceState();
switch (device_state) {
case kDeviceStateIdle:
ESP_LOGI(TAG, "hide xiaozhi, show idle screen");
if (!lv_obj_has_flag(container_, LV_OBJ_FLAG_HIDDEN)) {
lv_obj_add_flag(container_, LV_OBJ_FLAG_HIDDEN);
}
_lcdScnIdle.ui_showScreen(true);
break;
case kDeviceStateListening:
case kDeviceStateConnecting:
case kDeviceStateSpeaking:
ESP_LOGI(TAG, "show xiaozhi, hide idle screen");
_lcdScnIdle.ui_showScreen(false);
if (lv_obj_has_flag(container_, LV_OBJ_FLAG_HIDDEN)) {
lv_obj_clear_flag(container_, LV_OBJ_FLAG_HIDDEN);
}
break;
default:
break;
}
}
virtual void OnClockTimer() override {
DisplayLockGuard lock(this);
_lcdScnIdle.ui_update(); // update screen every 1s
}
void IdleScrSetupUi() {
DisplayLockGuard lock(this); // ← 必须加锁LVGL不是线程安全的
ESP_LOGI(TAG, "IdleScrSetupUi()");
// Get ThemeColors from current theme
ThemeColors theme_colors;
theme_colors.background = lv_color_hex(0x000000);
theme_colors.text = lv_color_hex(0xFFFFFF);
theme_colors.border = lv_color_hex(0x444444);
theme_colors.chat_background = lv_color_hex(0x111111);
theme_colors.user_bubble = lv_color_hex(0x0078D4);
theme_colors.assistant_bubble = lv_color_hex(0x2D2D2D);
theme_colors.system_bubble = lv_color_hex(0x1A1A1A);
theme_colors.system_text = lv_color_hex(0xFFFFFF);
theme_colors.low_battery = lv_color_hex(0xFF0000);
_lcdScnIdle.ui_init(&theme_colors);
}
void UpdateTheme() {
DisplayLockGuard lock(this); // ← 必须加锁!
ThemeColors theme_colors;
theme_colors.background = lv_color_hex(0x000000);
theme_colors.text = lv_color_hex(0xFFFFFF);
theme_colors.border = lv_color_hex(0x444444);
theme_colors.chat_background = lv_color_hex(0x111111);
theme_colors.user_bubble = lv_color_hex(0x0078D4);
theme_colors.assistant_bubble = lv_color_hex(0x2D2D2D);
theme_colors.system_bubble = lv_color_hex(0x1A1A1A);
theme_colors.system_text = lv_color_hex(0xFFFFFF);
theme_colors.low_battery = lv_color_hex(0xFF0000);
_lcdScnIdle.ui_updateTheme(&theme_colors);
}
// Override alarm display methods
virtual void ShowAlarmOnIdleScreen(const char* alarm_message) override {
DisplayLockGuard lock(this);
ESP_LOGI(TAG, "ShowAlarmOnIdleScreen: %s", alarm_message);
_lcdScnIdle.ui_showAlarmInfo(alarm_message);
// Make sure idle screen is visible
if (!_lcdScnIdle.ui_shown) {
_lcdScnIdle.ui_showScreen(true);
}
// Hide xiaozhi interface
if (!lv_obj_has_flag(container_, LV_OBJ_FLAG_HIDDEN)) {
lv_obj_add_flag(container_, LV_OBJ_FLAG_HIDDEN);
}
}
virtual void HideAlarmOnIdleScreen() override {
DisplayLockGuard lock(this);
ESP_LOGI(TAG, "HideAlarmOnIdleScreen");
_lcdScnIdle.ui_hideAlarmInfo();
}
void InitWeatherService() {
ESP_LOGI(TAG, "Initializing weather service");
// Start weather update task
xTaskCreate([](void* param) {
auto* self = static_cast<SpiLcdDisplayEx*>(param);
ESP_LOGI(TAG, "Weather update task started");
// Wait 5 seconds for WiFi to connect
vTaskDelay(pdMS_TO_TICKS(5000));
// Auto-detect city code by IP (leave empty string for auto-detect)
// Or specify a city code like "101010100" for Beijing
self->weather_service_.Initialize(""); // Empty = auto-detect
// Set callback to update UI when weather data is received
self->weather_service_.SetWeatherCallback([self](const WeatherData& weather) {
DisplayLockGuard lock(self);
self->_lcdScnIdle.ui_updateWeather(weather);
});
// Fetch weather immediately after initialization
self->weather_service_.FetchWeather();
// Continue updating weather every 10 minutes
while (true) {
vTaskDelay(pdMS_TO_TICKS(600000)); // 10 minutes
self->weather_service_.FetchWeather();
}
}, "weather_task", 8192, this, 5, NULL); // Increased stack size for HTTP operations
}
private:
IdleScreen _lcdScnIdle;
WeatherService weather_service_;
};
#endif // IDLE_SCREEN_HOOK
class SparkBotEs8311AudioCodec : public Es8311AudioCodec {
private:
@@ -49,7 +200,11 @@ private:
Button boot_button_;
Button volume_up_button_;
Button volume_down_button_;
#if IDLE_SCREEN_HOOK
SpiLcdDisplayEx* display_;
#else
LcdDisplay* display_;
#endif
i2c_master_bus_handle_t codec_i2c_bus_;
PowerSaveTimer* power_save_timer_;
PowerManager* power_manager_;
@@ -109,6 +264,18 @@ private:
boot_button_.OnClick([this]() {
power_save_timer_->WakeUp();
auto& app = Application::GetInstance();
// 如果有闹钟正在播放,优先停止闹钟,而不是切换界面
auto& alarm_manager = AlarmManager::GetInstance();
auto active_alarms = alarm_manager.GetActiveAlarms();
if (!active_alarms.empty()) {
ESP_LOGI(TAG, "Boot button pressed during alarm, stopping alarm");
for (const auto& alarm : active_alarms) {
alarm_manager.StopAlarm(alarm.id);
}
return; // 不切换界面
}
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
@@ -196,8 +363,13 @@ private:
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel, true));
#if IDLE_SCREEN_HOOK
display_ = new SpiLcdDisplayEx(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
#else
display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
#endif
}
public:
@@ -213,6 +385,12 @@ public:
InitializeButtons();
InitializeSt7789Display();
GetBacklight()->RestoreBrightness();
#if IDLE_SCREEN_HOOK
auto* display_ex = static_cast<SpiLcdDisplayEx*>(display_);
display_ex->IdleScrSetupUi();
display_ex->InitWeatherService();
#endif
}
virtual Led* GetLed() override {

View File

@@ -0,0 +1,456 @@
/**
* @file idle_screen.cc
* @brief Weather Clock Idle Screen Implementation
* @version 2.0
*/
#include "config.h"
#include "application.h"
#include "idle_screen.h"
#include "ui_helpers.h"
#include "display/lcd_display.h"
#include <esp_log.h>
#include <time.h>
#include <sys/time.h>
#if IDLE_SCREEN_HOOK
LV_FONT_DECLARE(font_puhui_20_4);
LV_FONT_DECLARE(ui_font_font48Seg);
LV_IMG_DECLARE(ui_img_xiaozhi_48_png);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion"
static const char *TAG = "WeatherClock";
// Helper function to format time
static void get_time_string(char* hour_min_buf, char* second_buf, char* week_buf, char* date_buf) {
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
// Format hour:minute (HH:MM)
snprintf(hour_min_buf, 16, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
// Format second (SS)
snprintf(second_buf, 8, "%02d", timeinfo.tm_sec);
// Format week (周X)
const char* week_names[] = {"", "", "", "", "", "", ""};
snprintf(week_buf, 16, "周%s", week_names[timeinfo.tm_wday]);
// Format date (MM月DD日)
snprintf(date_buf, 32, "%02d月%02d日", timeinfo.tm_mon + 1, timeinfo.tm_mday);
}
IdleScreen::IdleScreen() {
ui_screen = NULL;
ui_main_container = NULL;
ui_scroll_container = NULL;
ui_scroll_label = NULL;
ui_city_label = NULL;
ui_time_container = NULL;
ui_time_hour_min = NULL;
ui_time_second = NULL;
ui_xiaozhi_icon = NULL;
ui_info_container = NULL;
ui_aqi_container = NULL;
ui_aqi_label = NULL;
ui_temp_label = NULL;
ui_temp_icon_label = NULL;
ui_humid_label = NULL;
ui_humid_icon_label = NULL;
ui_date_container = NULL;
ui_week_label = NULL;
ui_date_label = NULL;
ui_alarm_info_label = NULL;
ui_shown = false;
current_scroll_index = 0;
last_scroll_time = 0;
p_theme = NULL;
}
IdleScreen::~IdleScreen() {
ui_destroy();
}
void IdleScreen::ui_init(ThemeColors *p_current_theme) {
auto screen = lv_screen_active();
p_theme = p_current_theme;
ESP_LOGI(TAG, "Initializing weather clock UI");
// Main screen container
ui_screen = lv_obj_create(screen);
lv_obj_remove_flag(ui_screen, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(ui_screen, LV_HOR_RES, LV_VER_RES);
lv_obj_set_style_bg_color(ui_screen, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_width(ui_screen, 0, 0);
lv_obj_set_style_pad_all(ui_screen, 0, 0);
// Main container
ui_main_container = lv_obj_create(ui_screen);
lv_obj_remove_style_all(ui_main_container);
lv_obj_set_size(ui_main_container, 240, 240);
lv_obj_set_align(ui_main_container, LV_ALIGN_CENTER);
lv_obj_remove_flag(ui_main_container, (lv_obj_flag_t)(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE));
lv_obj_set_style_bg_color(ui_main_container, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_bg_opa(ui_main_container, LV_OPA_COVER, 0);
createTopSection();
createMiddleSection();
createBottomSection();
// Alarm info label (initially hidden)
ui_alarm_info_label = lv_label_create(ui_main_container);
lv_obj_set_width(ui_alarm_info_label, 220);
lv_obj_set_height(ui_alarm_info_label, LV_SIZE_CONTENT);
lv_obj_set_pos(ui_alarm_info_label, 10, 80);
lv_label_set_long_mode(ui_alarm_info_label, LV_LABEL_LONG_WRAP);
lv_label_set_text(ui_alarm_info_label, "");
lv_obj_set_style_text_align(ui_alarm_info_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_font(ui_alarm_info_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_alarm_info_label, lv_color_hex(0xFF0000), 0);
lv_obj_add_flag(ui_alarm_info_label, LV_OBJ_FLAG_HIDDEN);
// Draw divider lines
static lv_point_precise_t line_points1[] = {{0, 34}, {240, 34}};
static lv_point_precise_t line_points2[] = {{150, 0}, {150, 34}};
static lv_point_precise_t line_points3[] = {{0, 166}, {240, 166}};
static lv_point_precise_t line_points4[] = {{60, 166}, {60, 200}};
static lv_point_precise_t line_points5[] = {{160, 166}, {160, 200}};
lv_obj_t* line1 = lv_line_create(ui_main_container);
lv_line_set_points(line1, line_points1, 2);
lv_obj_set_style_line_color(line1, lv_color_hex(0x000000), 0);
lv_obj_set_style_line_width(line1, 1, 0);
lv_obj_t* line2 = lv_line_create(ui_main_container);
lv_line_set_points(line2, line_points2, 2);
lv_obj_set_style_line_color(line2, lv_color_hex(0x000000), 0);
lv_obj_set_style_line_width(line2, 1, 0);
lv_obj_t* line3 = lv_line_create(ui_main_container);
lv_line_set_points(line3, line_points3, 2);
lv_obj_set_style_line_color(line3, lv_color_hex(0x000000), 0);
lv_obj_set_style_line_width(line3, 1, 0);
lv_obj_t* line4 = lv_line_create(ui_main_container);
lv_line_set_points(line4, line_points4, 2);
lv_obj_set_style_line_color(line4, lv_color_hex(0x000000), 0);
lv_obj_set_style_line_width(line4, 1, 0);
lv_obj_t* line5 = lv_line_create(ui_main_container);
lv_line_set_points(line5, line_points5, 2);
lv_obj_set_style_line_color(line5, lv_color_hex(0x000000), 0);
lv_obj_set_style_line_width(line5, 1, 0);
// Hide by default
lv_obj_add_flag(ui_screen, LV_OBJ_FLAG_HIDDEN);
ESP_LOGI(TAG, "Weather clock UI initialized");
}
void IdleScreen::createTopSection() {
// Top section container (0-34px height)
ui_scroll_container = lv_obj_create(ui_main_container);
lv_obj_remove_style_all(ui_scroll_container);
lv_obj_set_size(ui_scroll_container, 148, 32);
lv_obj_set_pos(ui_scroll_container, 2, 2);
lv_obj_remove_flag(ui_scroll_container, LV_OBJ_FLAG_SCROLLABLE);
// Scrolling weather info label
ui_scroll_label = lv_label_create(ui_scroll_container);
lv_obj_set_width(ui_scroll_label, 144);
lv_label_set_long_mode(ui_scroll_label, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_label_set_text(ui_scroll_label, "正在获取天气信息...");
lv_obj_set_style_text_font(ui_scroll_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_scroll_label, lv_color_hex(0x000000), 0);
lv_obj_align(ui_scroll_label, LV_ALIGN_LEFT_MID, 0, 0);
// City name label
ui_city_label = lv_label_create(ui_main_container);
lv_obj_set_width(ui_city_label, 88);
lv_label_set_text(ui_city_label, "北京");
lv_obj_set_style_text_font(ui_city_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_city_label, lv_color_hex(0x000000), 0);
lv_obj_set_style_text_align(ui_city_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(ui_city_label, 152, 8);
}
void IdleScreen::createMiddleSection() {
// Middle section container (35-165px, height 130px)
ui_time_container = lv_obj_create(ui_main_container);
lv_obj_remove_style_all(ui_time_container);
lv_obj_set_size(ui_time_container, 240, 130);
lv_obj_set_pos(ui_time_container, 0, 35);
lv_obj_remove_flag(ui_time_container, LV_OBJ_FLAG_SCROLLABLE);
// Large time display (HH:MM) - using ui_font_font48Seg (already in project)
ui_time_hour_min = lv_label_create(ui_time_container);
lv_label_set_text(ui_time_hour_min, "12:34");
lv_obj_set_style_text_font(ui_time_hour_min, &ui_font_font48Seg, 0);
lv_obj_set_style_text_color(ui_time_hour_min, lv_color_hex(0x000000), 0);
lv_obj_align(ui_time_hour_min, LV_ALIGN_CENTER, -20, -25);
// Small second display (SS)
ui_time_second = lv_label_create(ui_time_container);
lv_label_set_text(ui_time_second, "56");
lv_obj_set_style_text_font(ui_time_second, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_time_second, lv_color_hex(0x000000), 0);
lv_obj_align(ui_time_second, LV_ALIGN_CENTER, 65, -25);
// Rotating Xiaozhi icon (位置在时间下方)
ui_xiaozhi_icon = lv_img_create(ui_time_container);
lv_img_set_src(ui_xiaozhi_icon, &ui_img_xiaozhi_48_png);
lv_obj_align(ui_xiaozhi_icon, LV_ALIGN_CENTER, 0, 35);
lv_img_set_pivot(ui_xiaozhi_icon, 24, 24); // 设置旋转中心点 (48/2 = 24)
// Add rotation animation (使用局部变量LVGL会自动管理)
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, ui_xiaozhi_icon);
lv_anim_set_exec_cb(&anim, (lv_anim_exec_xcb_t)lv_img_set_angle);
lv_anim_set_values(&anim, 0, 3600); // 0-360度 (LVGL使用0.1度单位)
lv_anim_set_time(&anim, 3000); // 3秒转一圈
lv_anim_set_repeat_count(&anim, LV_ANIM_REPEAT_INFINITE);
lv_anim_start(&anim);
ESP_LOGI(TAG, "Xiaozhi rotation animation started");
}
void IdleScreen::createBottomSection() {
// Info container (166-200px, height 34px)
ui_info_container = lv_obj_create(ui_main_container);
lv_obj_remove_style_all(ui_info_container);
lv_obj_set_size(ui_info_container, 240, 34);
lv_obj_set_pos(ui_info_container, 0, 166);
lv_obj_remove_flag(ui_info_container, LV_OBJ_FLAG_SCROLLABLE);
// AQI container (0-60px)
ui_aqi_container = lv_obj_create(ui_info_container);
lv_obj_set_size(ui_aqi_container, 50, 24);
lv_obj_set_pos(ui_aqi_container, 5, 5);
lv_obj_set_style_radius(ui_aqi_container, 4, 0);
lv_obj_set_style_bg_color(ui_aqi_container, lv_color_hex(0x9CCA7F), 0); // Default: 优
lv_obj_set_style_bg_opa(ui_aqi_container, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(ui_aqi_container, 0, 0);
lv_obj_set_style_pad_all(ui_aqi_container, 0, 0);
ui_aqi_label = lv_label_create(ui_aqi_container);
lv_label_set_text(ui_aqi_label, "");
lv_obj_set_style_text_font(ui_aqi_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_aqi_label, lv_color_hex(0xFFFFFF), 0);
lv_obj_center(ui_aqi_label);
// Humidity icon and label (middle section)
ui_humid_icon_label = lv_label_create(ui_info_container);
lv_label_set_text(ui_humid_icon_label, "湿"); // 湿度
lv_obj_set_style_text_font(ui_humid_icon_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_humid_icon_label, lv_color_hex(0x0000FF), 0);
lv_obj_set_pos(ui_humid_icon_label, 85, 8);
ui_humid_label = lv_label_create(ui_info_container);
lv_label_set_text(ui_humid_label, "65%");
lv_obj_set_style_text_font(ui_humid_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_humid_label, lv_color_hex(0x000000), 0);
lv_obj_set_pos(ui_humid_label, 110, 8);
// Temperature icon and label (160-240px)
ui_temp_icon_label = lv_label_create(ui_info_container);
lv_label_set_text(ui_temp_icon_label, ""); // 温度
lv_obj_set_style_text_font(ui_temp_icon_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_temp_icon_label, lv_color_hex(0xFF0000), 0);
lv_obj_set_pos(ui_temp_icon_label, 162, 8);
ui_temp_label = lv_label_create(ui_info_container);
lv_label_set_text(ui_temp_label, "25℃");
lv_obj_set_style_text_font(ui_temp_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_temp_label, lv_color_hex(0x000000), 0);
lv_obj_set_pos(ui_temp_label, 182, 8);
// Date container (200-240px, height 40px)
ui_date_container = lv_obj_create(ui_main_container);
lv_obj_remove_style_all(ui_date_container);
lv_obj_set_size(ui_date_container, 240, 34);
lv_obj_set_pos(ui_date_container, 0, 200);
lv_obj_remove_flag(ui_date_container, LV_OBJ_FLAG_SCROLLABLE);
// Week label (0-60px)
ui_week_label = lv_label_create(ui_date_container);
lv_label_set_text(ui_week_label, "周一");
lv_obj_set_style_text_font(ui_week_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_week_label, lv_color_hex(0x000000), 0);
lv_obj_set_style_text_align(ui_week_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(ui_week_label, 5, 8);
// Date label (61-160px)
ui_date_label = lv_label_create(ui_date_container);
lv_label_set_text(ui_date_label, "01月01日");
lv_obj_set_style_text_font(ui_date_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(ui_date_label, lv_color_hex(0x000000), 0);
lv_obj_set_style_text_align(ui_date_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(ui_date_label, 70, 8);
}
void IdleScreen::ui_destroy() {
if (ui_screen) {
// Stop animation before deleting objects
if (ui_xiaozhi_icon) {
lv_anim_delete(ui_xiaozhi_icon, NULL);
}
lv_obj_delete(ui_screen);
ui_screen = NULL;
}
}
void IdleScreen::ui_showScreen(bool showIt) {
if (!ui_screen) return;
if (showIt) {
lv_obj_remove_flag(ui_screen, LV_OBJ_FLAG_HIDDEN);
ui_shown = true;
} else {
lv_obj_add_flag(ui_screen, LV_OBJ_FLAG_HIDDEN);
ui_shown = false;
}
}
void IdleScreen::ui_update() {
if (!ui_screen || !ui_shown) return;
// Update time display
char hour_min_buf[16], second_buf[8], week_buf[16], date_buf[32];
get_time_string(hour_min_buf, second_buf, week_buf, date_buf);
if (ui_time_hour_min) {
lv_label_set_text(ui_time_hour_min, hour_min_buf);
}
if (ui_time_second) {
lv_label_set_text(ui_time_second, second_buf);
}
if (ui_week_label) {
lv_label_set_text(ui_week_label, week_buf);
}
if (ui_date_label) {
lv_label_set_text(ui_date_label, date_buf);
}
// Update scroll text every 2.5 seconds
updateScrollText();
}
void IdleScreen::updateScrollText() {
if (scroll_texts.empty()) return;
uint32_t current_time = lv_tick_get();
if (current_time - last_scroll_time > 2500) { // 2.5 seconds
current_scroll_index = (current_scroll_index + 1) % scroll_texts.size();
if (ui_scroll_label) {
lv_label_set_text(ui_scroll_label, scroll_texts[current_scroll_index].c_str());
}
last_scroll_time = current_time;
}
}
void IdleScreen::ui_updateTheme(ThemeColors *p_current_theme) {
p_theme = p_current_theme;
// Weather clock uses fixed white background and black text
// Theme colors are not applied to maintain Arduino design
}
void IdleScreen::ui_updateWeather(const WeatherData& weather) {
ESP_LOGI(TAG, "Updating weather data");
// Update city name
if (ui_city_label) {
lv_label_set_text(ui_city_label, weather.city_name.c_str());
}
// Update temperature
if (ui_temp_label) {
std::string temp_text = weather.temperature + "";
lv_label_set_text(ui_temp_label, temp_text.c_str());
}
// Update humidity
if (ui_humid_label) {
lv_label_set_text(ui_humid_label, weather.humidity.c_str());
}
// Update AQI
if (ui_aqi_label) {
lv_label_set_text(ui_aqi_label, weather.aqi_desc.c_str());
updateAQIColor(weather.aqi);
}
// Update scroll texts
std::vector<std::string> texts;
texts.push_back("实时天气 " + weather.weather_desc);
texts.push_back("空气质量 " + weather.aqi_desc);
texts.push_back("风向 " + weather.wind_direction + weather.wind_speed);
texts.push_back("今日天气 " + weather.weather_desc);
texts.push_back("最低温度 " + weather.temp_low + "");
texts.push_back("最高温度 " + weather.temp_high + "");
ui_setScrollText(texts);
}
void IdleScreen::ui_setScrollText(const std::vector<std::string>& texts) {
scroll_texts = texts;
current_scroll_index = 0;
last_scroll_time = lv_tick_get();
if (!texts.empty() && ui_scroll_label) {
lv_label_set_text(ui_scroll_label, texts[0].c_str());
}
}
void IdleScreen::updateAQIColor(int aqi) {
if (!ui_aqi_container) return;
lv_color_t color;
if (aqi > 200) {
// 重度污染 - 深红色
color = lv_color_hex(0x880B20);
} else if (aqi > 150) {
// 中度污染 - 紫红色
color = lv_color_hex(0xBA3779);
} else if (aqi > 100) {
// 轻度污染 - 橙色
color = lv_color_hex(0xF29F39);
} else if (aqi > 50) {
// 良 - 黄色
color = lv_color_hex(0xF7DB64);
} else {
// 优 - 绿色
color = lv_color_hex(0x9CCA7F);
}
lv_obj_set_style_bg_color(ui_aqi_container, color, 0);
}
void IdleScreen::ui_showAlarmInfo(const char* alarm_message) {
if (!ui_alarm_info_label) return;
ESP_LOGI(TAG, "Showing alarm info: %s", alarm_message);
lv_label_set_text(ui_alarm_info_label, alarm_message);
lv_obj_remove_flag(ui_alarm_info_label, LV_OBJ_FLAG_HIDDEN);
}
void IdleScreen::ui_hideAlarmInfo() {
if (!ui_alarm_info_label) return;
ESP_LOGI(TAG, "Hiding alarm info");
lv_obj_add_flag(ui_alarm_info_label, LV_OBJ_FLAG_HIDDEN);
}
#pragma GCC diagnostic pop
#endif // IDLE_SCREEN_HOOK

View File

@@ -0,0 +1,118 @@
/**
* @file idle_screen.h
* @brief Weather Clock Idle Screen for GenJuTech S3 1.54TFT
* @brief Inspired by Arduino MiniTV weather clock project
* @version 2.0
* @date 2025-01-10
*/
#pragma once
#include "config.h"
#if IDLE_SCREEN_HOOK
#include <lvgl.h>
#include <string>
#include <vector>
// Theme color structure (copied from lcd_display.h to avoid circular dependency)
struct ThemeColors {
lv_color_t background;
lv_color_t text;
lv_color_t chat_background;
lv_color_t user_bubble;
lv_color_t assistant_bubble;
lv_color_t system_bubble;
lv_color_t system_text;
lv_color_t border;
lv_color_t low_battery;
};
// Weather data structure
struct WeatherData {
std::string city_name; // 城市名称
std::string temperature; // 当前温度
std::string humidity; // 湿度
std::string weather_desc; // 天气描述
std::string wind_direction; // 风向
std::string wind_speed; // 风速
int aqi; // 空气质量指数
std::string aqi_desc; // 空气质量描述
std::string temp_low; // 最低温度
std::string temp_high; // 最高温度
uint32_t last_update_time; // 最后更新时间
};
class IdleScreen
{
public:
IdleScreen();
~IdleScreen();
public:
void ui_init(ThemeColors *p_current_theme);
void ui_destroy();
void ui_showScreen(bool showIt);
void ui_update(); // Called every second to update time
void ui_updateTheme(ThemeColors *p_current_theme);
// Weather related methods
void ui_updateWeather(const WeatherData& weather);
void ui_setScrollText(const std::vector<std::string>& texts);
// Alarm display methods
void ui_showAlarmInfo(const char* alarm_message);
void ui_hideAlarmInfo();
public:
bool ui_shown; // UI shown or not
private:
void createTopSection(); // 顶部:滚动信息 + 城市
void createMiddleSection(); // 中间:时间显示区域
void createBottomSection(); // 底部:温湿度 + 日期
void updateScrollText(); // 更新滚动文字
void updateAQIColor(int aqi); // 更新空气质量颜色
private:
// Main containers
lv_obj_t* ui_screen;
lv_obj_t* ui_main_container;
// Top section - Weather scroll and city
lv_obj_t* ui_scroll_container;
lv_obj_t* ui_scroll_label;
lv_obj_t* ui_city_label;
// Middle section - Time display
lv_obj_t* ui_time_container;
lv_obj_t* ui_time_hour_min; // 时:分 (大字体)
lv_obj_t* ui_time_second; // 秒 (小字体)
lv_obj_t* ui_xiaozhi_icon; // 旋转小智图标
// Bottom upper section - AQI, Temperature, Humidity
lv_obj_t* ui_info_container;
lv_obj_t* ui_aqi_container;
lv_obj_t* ui_aqi_label;
lv_obj_t* ui_temp_label;
lv_obj_t* ui_temp_icon_label;
lv_obj_t* ui_humid_label;
lv_obj_t* ui_humid_icon_label;
// Bottom lower section - Week and Date
lv_obj_t* ui_date_container;
lv_obj_t* ui_week_label;
lv_obj_t* ui_date_label;
// Alarm info (overlays on screen when alarm triggers)
lv_obj_t* ui_alarm_info_label;
// Scroll text management
std::vector<std::string> scroll_texts;
int current_scroll_index;
uint32_t last_scroll_time;
// Theme
ThemeColors* p_theme;
};
#endif // IDLE_SCREEN_HOOK

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,349 @@
// This file was generated by SquareLine Studio
// SquareLine Studio version: SquareLine Studio 1.5.2
// LVGL version: 9.2.2
// Project name: weather0
#include "config.h"
#if IDLE_SCREEN_HOOK
#include "ui_helpers.h"
void _ui_bar_set_property(lv_obj_t * target, int id, int val)
{
if(id == _UI_BAR_PROPERTY_VALUE_WITH_ANIM) lv_bar_set_value(target, val, LV_ANIM_ON);
if(id == _UI_BAR_PROPERTY_VALUE) lv_bar_set_value(target, val, LV_ANIM_OFF);
}
void _ui_basic_set_property(lv_obj_t * target, int id, int val)
{
if(id == _UI_BASIC_PROPERTY_POSITION_X) lv_obj_set_x(target, val);
if(id == _UI_BASIC_PROPERTY_POSITION_Y) lv_obj_set_y(target, val);
if(id == _UI_BASIC_PROPERTY_WIDTH) lv_obj_set_width(target, val);
if(id == _UI_BASIC_PROPERTY_HEIGHT) lv_obj_set_height(target, val);
}
void _ui_dropdown_set_property(lv_obj_t * target, int id, int val)
{
if(id == _UI_DROPDOWN_PROPERTY_SELECTED) lv_dropdown_set_selected(target, val);
}
void _ui_image_set_property(lv_obj_t * target, int id, uint8_t * val)
{
if(id == _UI_IMAGE_PROPERTY_IMAGE) lv_image_set_src(target, val);
}
void _ui_label_set_property(lv_obj_t * target, int id, const char * val)
{
if(id == _UI_LABEL_PROPERTY_TEXT) lv_label_set_text(target, val);
}
void _ui_roller_set_property(lv_obj_t * target, int id, int val)
{
if(id == _UI_ROLLER_PROPERTY_SELECTED_WITH_ANIM) lv_roller_set_selected(target, val, LV_ANIM_ON);
if(id == _UI_ROLLER_PROPERTY_SELECTED) lv_roller_set_selected(target, val, LV_ANIM_OFF);
}
void _ui_slider_set_property(lv_obj_t * target, int id, int val)
{
if(id == _UI_SLIDER_PROPERTY_VALUE_WITH_ANIM) lv_slider_set_value(target, val, LV_ANIM_ON);
if(id == _UI_SLIDER_PROPERTY_VALUE) lv_slider_set_value(target, val, LV_ANIM_OFF);
}
void _ui_screen_change(lv_obj_t ** target, lv_screen_load_anim_t fademode, int spd, int delay,
void (*target_init)(void))
{
if(*target == NULL)
target_init();
lv_screen_load_anim(*target, fademode, spd, delay, false);
}
void _ui_screen_delete(lv_obj_t ** target)
{
if(*target == NULL) {
lv_obj_delete(*target);
target = NULL;
}
}
void _ui_arc_increment(lv_obj_t * target, int val)
{
int old = lv_arc_get_value(target);
lv_arc_set_value(target, old + val);
lv_obj_send_event(target, LV_EVENT_VALUE_CHANGED, 0);
}
void _ui_bar_increment(lv_obj_t * target, int val, int anm)
{
int old = lv_bar_get_value(target);
lv_bar_set_value(target, old + val, anm);
}
void _ui_slider_increment(lv_obj_t * target, int val, int anm)
{
int old = lv_slider_get_value(target);
lv_slider_set_value(target, old + val, anm);
lv_obj_send_event(target, LV_EVENT_VALUE_CHANGED, 0);
}
void _ui_keyboard_set_target(lv_obj_t * keyboard, lv_obj_t * textarea)
{
//lv_keyboard_set_textarea(keyboard, textarea);
}
void _ui_flag_modify(lv_obj_t * target, int32_t flag, int value)
{
if(value == _UI_MODIFY_FLAG_TOGGLE) {
if(lv_obj_has_flag(target, flag)) lv_obj_remove_flag(target, flag);
else lv_obj_add_flag(target, flag);
}
else if(value == _UI_MODIFY_FLAG_ADD) lv_obj_add_flag(target, flag);
else lv_obj_remove_flag(target, flag);
}
void _ui_state_modify(lv_obj_t * target, int32_t state, int value)
{
if(value == _UI_MODIFY_STATE_TOGGLE) {
if(lv_obj_has_state(target, state)) lv_obj_remove_state(target, state);
else lv_obj_add_state(target, state);
}
else if(value == _UI_MODIFY_STATE_ADD) lv_obj_add_state(target, state);
else lv_obj_remove_state(target, state);
}
void _ui_textarea_move_cursor(lv_obj_t * target, int val)
{
if(val == UI_MOVE_CURSOR_UP) lv_textarea_cursor_up(target);
if(val == UI_MOVE_CURSOR_RIGHT) lv_textarea_cursor_right(target);
if(val == UI_MOVE_CURSOR_DOWN) lv_textarea_cursor_down(target);
if(val == UI_MOVE_CURSOR_LEFT) lv_textarea_cursor_left(target);
lv_obj_add_state(target, LV_STATE_FOCUSED);
}
void scr_unloaded_delete_cb(lv_event_t * e)
{
lv_obj_t ** var = lv_event_get_user_data(e);
lv_obj_delete(*var);
(*var) = NULL;
}
void _ui_opacity_set(lv_obj_t * target, int val)
{
lv_obj_set_style_opa(target, val, 0);
}
void _ui_anim_callback_free_user_data(lv_anim_t * a)
{
lv_free(a->user_data);
a->user_data = NULL;
}
void _ui_anim_callback_set_x(lv_anim_t * a, int32_t v)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
lv_obj_set_x(usr->target, v);
}
void _ui_anim_callback_set_y(lv_anim_t * a, int32_t v)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
lv_obj_set_y(usr->target, v);
}
void _ui_anim_callback_set_width(lv_anim_t * a, int32_t v)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
lv_obj_set_width(usr->target, v);
}
void _ui_anim_callback_set_height(lv_anim_t * a, int32_t v)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
lv_obj_set_height(usr->target, v);
}
void _ui_anim_callback_set_opacity(lv_anim_t * a, int32_t v)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
lv_obj_set_style_opa(usr->target, v, 0);
}
void _ui_anim_callback_set_image_zoom(lv_anim_t * a, int32_t v)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
lv_image_set_scale(usr->target, v);
}
void _ui_anim_callback_set_image_angle(lv_anim_t * a, int32_t v)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
lv_image_set_rotation(usr->target, v);
}
void _ui_anim_callback_set_image_frame(lv_anim_t * a, int32_t v)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
usr->val = v;
if(v < 0) v = 0;
if(v >= usr->imgset_size) v = usr->imgset_size - 1;
lv_image_set_src(usr->target, usr->imgset[v]);
}
int32_t _ui_anim_callback_get_x(lv_anim_t * a)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
return lv_obj_get_x_aligned(usr->target);
}
int32_t _ui_anim_callback_get_y(lv_anim_t * a)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
return lv_obj_get_y_aligned(usr->target);
}
int32_t _ui_anim_callback_get_width(lv_anim_t * a)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
return lv_obj_get_width(usr->target);
}
int32_t _ui_anim_callback_get_height(lv_anim_t * a)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
return lv_obj_get_height(usr->target);
}
int32_t _ui_anim_callback_get_opacity(lv_anim_t * a)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
return lv_obj_get_style_opa(usr->target, 0);
}
int32_t _ui_anim_callback_get_image_zoom(lv_anim_t * a)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
return lv_image_get_scale(usr->target);
}
int32_t _ui_anim_callback_get_image_angle(lv_anim_t * a)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
return lv_image_get_rotation(usr->target);
}
int32_t _ui_anim_callback_get_image_frame(lv_anim_t * a)
{
ui_anim_user_data_t * usr = (ui_anim_user_data_t *)a->user_data;
return usr->val;
}
void _ui_arc_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * prefix, const char * postfix)
{
char buf[_UI_TEMPORARY_STRING_BUFFER_SIZE];
lv_snprintf(buf, sizeof(buf), "%s%d%s", prefix, (int)lv_arc_get_value(src), postfix);
lv_label_set_text(trg, buf);
}
void _ui_slider_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * prefix, const char * postfix)
{
char buf[_UI_TEMPORARY_STRING_BUFFER_SIZE];
lv_snprintf(buf, sizeof(buf), "%s%d%s", prefix, (int)lv_slider_get_value(src), postfix);
lv_label_set_text(trg, buf);
}
void _ui_checked_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * txt_on, const char * txt_off)
{
if(lv_obj_has_state(src, LV_STATE_CHECKED)) lv_label_set_text(trg, txt_on);
else lv_label_set_text(trg, txt_off);
}
void _ui_spinbox_step(lv_obj_t * target, int val)
{
// if(val > 0) lv_spinbox_increment(target);
// else lv_spinbox_decrement(target);
// lv_obj_send_event(target, LV_EVENT_VALUE_CHANGED, 0);
}
void _ui_switch_theme(int val)
{
#ifdef UI_THEME_ACTIVE
ui_theme_set(val);
#endif
}
#endif // IDLE_SCREEN_HOOK

View File

@@ -0,0 +1,154 @@
// This file was generated by SquareLine Studio
// SquareLine Studio version: SquareLine Studio 1.5.2
// LVGL version: 9.2.2
// Project name: weather0
#ifndef _SLS_UI_HELPERS_H
#define _SLS_UI_HELPERS_H
#include "config.h"
#if IDLE_SCREEN_HOOK
#ifdef __cplusplus
extern "C" {
#endif
#include <lvgl.h>
#define _UI_TEMPORARY_STRING_BUFFER_SIZE 32
#define _UI_BAR_PROPERTY_VALUE 0
#define _UI_BAR_PROPERTY_VALUE_WITH_ANIM 1
void _ui_bar_set_property(lv_obj_t * target, int id, int val);
#define _UI_BASIC_PROPERTY_POSITION_X 0
#define _UI_BASIC_PROPERTY_POSITION_Y 1
#define _UI_BASIC_PROPERTY_WIDTH 2
#define _UI_BASIC_PROPERTY_HEIGHT 3
void _ui_basic_set_property(lv_obj_t * target, int id, int val);
#define _UI_DROPDOWN_PROPERTY_SELECTED 0
void _ui_dropdown_set_property(lv_obj_t * target, int id, int val);
#define _UI_IMAGE_PROPERTY_IMAGE 0
void _ui_image_set_property(lv_obj_t * target, int id, uint8_t * val);
#define _UI_LABEL_PROPERTY_TEXT 0
void _ui_label_set_property(lv_obj_t * target, int id, const char * val);
#define _UI_ROLLER_PROPERTY_SELECTED 0
#define _UI_ROLLER_PROPERTY_SELECTED_WITH_ANIM 1
void _ui_roller_set_property(lv_obj_t * target, int id, int val);
#define _UI_SLIDER_PROPERTY_VALUE 0
#define _UI_SLIDER_PROPERTY_VALUE_WITH_ANIM 1
void _ui_slider_set_property(lv_obj_t * target, int id, int val);
void _ui_screen_change(lv_obj_t ** target, lv_screen_load_anim_t fademode, int spd, int delay,
void (*target_init)(void));
void _ui_screen_delete(lv_obj_t ** target);
void _ui_arc_increment(lv_obj_t * target, int val);
void _ui_bar_increment(lv_obj_t * target, int val, int anm);
void _ui_slider_increment(lv_obj_t * target, int val, int anm);
void _ui_keyboard_set_target(lv_obj_t * keyboard, lv_obj_t * textarea);
#define _UI_MODIFY_FLAG_ADD 0
#define _UI_MODIFY_FLAG_REMOVE 1
#define _UI_MODIFY_FLAG_TOGGLE 2
void _ui_flag_modify(lv_obj_t * target, int32_t flag, int value);
#define _UI_MODIFY_STATE_ADD 0
#define _UI_MODIFY_STATE_REMOVE 1
#define _UI_MODIFY_STATE_TOGGLE 2
void _ui_state_modify(lv_obj_t * target, int32_t state, int value);
#define UI_MOVE_CURSOR_UP 0
#define UI_MOVE_CURSOR_RIGHT 1
#define UI_MOVE_CURSOR_DOWN 2
#define UI_MOVE_CURSOR_LEFT 3
void _ui_textarea_move_cursor(lv_obj_t * target, int val)
;
void scr_unloaded_delete_cb(lv_event_t * e);
void _ui_opacity_set(lv_obj_t * target, int val);
/** Describes an animation*/
typedef struct _ui_anim_user_data_t {
lv_obj_t * target;
lv_image_dsc_t ** imgset;
int32_t imgset_size;
int32_t val;
} ui_anim_user_data_t;
void _ui_anim_callback_free_user_data(lv_anim_t * a);
void _ui_anim_callback_set_x(lv_anim_t * a, int32_t v);
void _ui_anim_callback_set_y(lv_anim_t * a, int32_t v);
void _ui_anim_callback_set_width(lv_anim_t * a, int32_t v);
void _ui_anim_callback_set_height(lv_anim_t * a, int32_t v);
void _ui_anim_callback_set_opacity(lv_anim_t * a, int32_t v);
void _ui_anim_callback_set_image_zoom(lv_anim_t * a, int32_t v);
void _ui_anim_callback_set_image_angle(lv_anim_t * a, int32_t v);
void _ui_anim_callback_set_image_frame(lv_anim_t * a, int32_t v);
int32_t _ui_anim_callback_get_x(lv_anim_t * a);
int32_t _ui_anim_callback_get_y(lv_anim_t * a);
int32_t _ui_anim_callback_get_width(lv_anim_t * a);
int32_t _ui_anim_callback_get_height(lv_anim_t * a);
int32_t _ui_anim_callback_get_opacity(lv_anim_t * a);
int32_t _ui_anim_callback_get_image_zoom(lv_anim_t * a);
int32_t _ui_anim_callback_get_image_angle(lv_anim_t * a);
int32_t _ui_anim_callback_get_image_frame(lv_anim_t * a);
void _ui_arc_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * prefix, const char * postfix);
void _ui_slider_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * prefix, const char * postfix);
void _ui_checked_set_text_value(lv_obj_t * trg, lv_obj_t * src, const char * txt_on, const char * txt_off);
void _ui_spinbox_step(lv_obj_t * target, int val)
;
void _ui_switch_theme(int val)
;
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif /* _SLS_UI_HELPERS_H */
#endif // IDLE_SCREEN_HOOK

View File

@@ -0,0 +1,107 @@
// This file was generated by SquareLine Studio
// SquareLine Studio version: SquareLine Studio 1.5.2
// LVGL version: 9.2.2
// Project name: weather0
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
#include "lvgl.h"
#else
#include "lvgl.h"
#endif
#ifndef LV_ATTRIBUTE_MEM_ALIGN
#define LV_ATTRIBUTE_MEM_ALIGN
#endif
#include "config.h"
#if IDLE_SCREEN_HOOK
// IMAGE DATA: assets/xiaozhi_48.png
const LV_ATTRIBUTE_MEM_ALIGN uint8_t ui_img_xiaozhi_48_png_data[] = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x3B,0xEF,0xBD,0x8D,0x9F,0x0B,0xDF,0x1B,0xDF,0x23,0xDF,0x23,0xDF,0x23,0xDF,0x23,0xDF,0x1B,0xDF,0x1B,0xDF,0x23,0xDF,0x23,0xDF,0x23,0xDF,0x23,0xDF,0x1B,0x9F,0x0B,0xBD,0x8D,0x3B,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x3B,0xEF,
0x5C,0xBE,0xFF,0x23,0xBF,0x13,0xDF,0x1B,0xDF,0x1B,0xDF,0x1B,0xDF,0x1B,0xBF,0x13,0xBF,0x13,0xDF,0x1B,0xDF,0x1B,0xDF,0x1B,0xDF,0x1B,0xBF,0x13,0xFF,0x23,0x5C,0xBE,0x3B,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0xFC,0xDE,0x3B,0xEF,0x7C,0xBE,0xDF,0x1B,0xBF,0x13,0xDF,0x1B,0xDF,0x1B,0xDF,0x1B,0xBF,0x13,0xBF,0x13,0xDF,0x1B,0xDF,0x1B,0xDF,0x1B,0xBF,0x13,0xDF,0x1B,0x7C,0xBE,0x3B,0xEF,
0xFC,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xDF,0x3B,0xEF,0x7C,0xC6,0x1F,0x2C,0xDF,0x1B,0xFF,0x23,0xFF,0x23,0xDF,0x1B,0xDF,0x1B,0xFF,0x23,0xFF,0x23,0xDF,0x1B,0x1F,0x2C,0x7C,0xC6,0x3B,0xEF,0x1C,0xDF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3B,0xEF,0x7C,0xBE,0xDF,0x1B,0xBF,0x0B,0xDF,0x13,0xBF,0x13,0xBF,0x13,0xDF,0x13,0xBF,0x0B,0xDF,0x1B,0x7C,0xBE,0x3B,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0xFC,0xDE,0x3B,0xEF,0x7C,0xBE,0xDF,0x1B,0x9F,0x0B,0xBF,0x13,0xBF,0x13,0x9F,0x0B,0xDF,0x1B,0x7C,0xBE,0x3B,0xEF,0xFC,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3B,0xEF,0x5C,0xB6,0x5E,0x3C,0xBF,0x13,0xBF,0x13,0x5E,0x3C,0x5C,0xB6,0x3B,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xDF,0x3B,0xEF,0x5B,0xF7,0x7E,0x44,0x7E,0x44,0x5B,0xF7,0x3B,0xEF,0x1C,0xDF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xDC,0xD6,0xDC,0xD6,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3B,0xE7,0x3B,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3C,0xE7,0x5D,0xEF,0x5D,0xEF,0x3C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3C,0xE7,0x5D,0xEF,0x5D,0xEF,0x3C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x5D,0xEF,0xFB,0xDE,0xB6,0xB5,0xB6,0xB5,0xFB,0xDE,0x5D,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x5D,0xEF,0xFB,0xDE,0xB6,0xB5,0xB6,0xB5,0xFB,0xDE,0x5D,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x5D,0xEF,0x18,0xC6,
0xE7,0x39,0x20,0x00,0x20,0x00,0xE7,0x39,0x18,0xC6,0x5D,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x5D,0xEF,0x18,0xC6,0xE7,0x39,0x20,0x00,0x20,0x00,0xE7,0x39,0x18,0xC6,0x5D,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xC7,0x39,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC7,0x39,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xC7,0x39,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xC7,0x39,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x5D,0xEF,0xB6,0xB5,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xB6,0xB5,0x5D,0xEF,0xFB,0xDE,0xFB,0xDE,0x5D,0xEF,0xB6,0xB5,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xB6,0xB5,0x5D,0xEF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x5D,0xEF,0xD7,0xBD,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xD7,0xBD,0x5D,0xEF,0xFB,0xDE,0xFB,0xDE,0x5D,0xEF,0xD7,0xBD,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xD7,0xBD,0x5D,0xEF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xC7,0x39,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE7,0x39,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xC7,0x39,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE7,0x39,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x5D,0xEF,0x18,0xC6,0xC7,0x39,0x61,0x08,0x61,0x08,0xE7,0x39,0x18,0xC6,0x5D,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x5D,0xEF,0x18,0xC6,0xC7,0x39,0x61,0x08,0x61,0x08,0xE7,0x39,
0x18,0xC6,0x5D,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x5D,0xEF,0xFB,0xDE,0x96,0xB5,0x96,0xB5,0xFB,0xDE,0x5D,0xEF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x5D,0xEF,0xFB,0xDE,0x96,0xB5,0x96,0xB5,0xFB,0xDE,0x5D,0xEF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x7D,0xEF,0xBE,0xF7,0x9E,0xF7,0x3C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x3C,0xE7,0x9E,0xF7,0xBE,0xF7,0x7D,0xEF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xDB,0xDE,
0x96,0xB5,0x79,0xCE,0x3C,0xE7,0x5D,0xEF,0x3C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3C,0xE7,0x5D,0xEF,0x3C,0xE7,0x79,0xCE,0x96,0xB5,0xDB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3C,0xE7,0x9A,0xD6,0xEF,0x7B,0x4D,0x6B,0x92,0x94,0xB6,0xB5,0x9A,0xD6,0x1C,0xE7,0x3C,0xE7,0x3C,0xE7,0x3C,0xE7,0x3C,0xE7,0x1C,0xE7,0x9A,0xD6,0xB6,0xB5,0x92,0x94,0x4D,0x6B,0xEF,0x7B,
0x9A,0xD6,0x3C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xDB,0xDE,0xB6,0xB5,0x51,0x8C,0x8E,0x73,0x8E,0x73,0xCF,0x7B,0x30,0x84,0x92,0x94,0x92,0x94,0x30,0x84,0xCF,0x7B,0x8E,0x73,0x8E,0x73,0x51,0x8C,0xB6,0xB5,0xDB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3C,0xE7,0x3C,0xE7,0x3C,0xE7,0xBA,0xD6,0xF7,0xBD,0x34,0xA5,0xD3,0x9C,0x92,0x94,0x92,0x94,0xD3,0x9C,0x34,0xA5,0xF7,0xBD,0xBA,0xD6,0x3C,0xE7,0x3C,0xE7,0x3C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x3C,0xE7,0x5D,0xEF,0x3C,0xE7,0x3C,0xE7,0x3C,0xE7,0x3C,0xE7,0x5D,0xEF,0x3C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,
0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xDB,0xDE,0xDB,0xDE,0xDB,0xDE,0xDB,0xDE,0xDB,0xDE,0xDB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFB,0xDE,0xDB,0xDE,0xDB,0xDE,0xDB,0xDE,0xDB,0xDE,0xDB,0xDE,0xDB,0xDE,0xFB,0xDE,0x1C,0xE7,0x1C,0xE7,0x1C,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
//alpha channel data:
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xA1,0xA1,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x29,0xDE,0xDE,0x29,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x2C,0xEC,0xEC,0x2C,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xAA,0xAA,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xA5,0xA5,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xA6,0xA6,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xA3,0xA3,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x12,0x21,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1B,0xAF,0xAF,0x1B,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x21,0x12,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x54,0xD5,0xDF,0xDD,0xDD,0xDD,0xDD,0xDD,0xDD,0xDD,0xDB,0xF4,0xF4,0xDB,0xDD,0xDD,0xDD,0xDD,0xDD,0xDD,0xDD,0xDF,0xD5,0x53,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0C,0xDB,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xDA,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0xEF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xEE,0x14,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x13,0xEC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEC,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0xEF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEE,0x14,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0C,0xDA,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xD9,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x53,0xD5,0xDE,0xDA,0xE7,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xE6,0xDA,0xDE,0xD4,0x53,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0E,0x1F,0x25,0xAE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xAD,0x25,0x1F,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x14,0x13,0x13,0x13,0x13,0x0F,0x03,0x17,
0xDC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xDC,0x17,0x03,0x0F,0x13,0x13,0x13,0x13,0x14,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x74,0xE6,0xEE,0xEC,0xEC,0xEC,0xEC,0xEC,0xE0,0x90,0xDE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xDE,
0x8E,0xDF,0xED,0xEC,0xEC,0xEC,0xEC,0xEE,0xE7,0x77,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xEF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFA,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFA,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xEE,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x9E,0xFB,0xFB,0xFB,0xFB,0xFB,0xFB,0xFB,0xF6,0xB1,0xE2,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xE3,0xB3,0xF6,0xFB,0xFB,0xFB,0xFB,0xFB,0xFB,0xFB,0x9B,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x35,0x3C,0x3A,0x3A,0x3A,0x3A,0x3B,0x2E,0x22,
0xDC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xDC,0x22,0x2E,0x3B,0x3A,0x3A,0x3A,0x3A,0x3C,0x35,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x21,0xE0,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xE0,
0x21,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x22,0xE0,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xE0,0x22,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x22,0xE0,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xE0,0x22,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x22,
0xE1,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xE1,0x22,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x22,0xE5,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xE5,
0x22,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0A,0xAE,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xAE,0x0A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x5D,0xB5,0xB4,0xB3,0xB3,0xB3,0xB3,0xB3,0xB3,0xB3,0xB3,0xB3,0xB3,0xB4,0xB5,0x5D,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
const lv_image_dsc_t ui_img_xiaozhi_48_png = {
.header.w = 48,
.header.h = 48,
.data_size = sizeof(ui_img_xiaozhi_48_png_data),
.header.cf = LV_COLOR_FORMAT_NATIVE_WITH_ALPHA,
.header.magic = LV_IMAGE_HEADER_MAGIC,
.data = ui_img_xiaozhi_48_png_data
};
#endif // IDLE_SCREEN_HOOK

View File

@@ -0,0 +1,365 @@
/**
* @file weather_service.cc
* @brief Weather API service implementation
* @version 1.0
*/
#include "weather_service.h"
#include <esp_log.h>
#include <esp_http_client.h>
#include <cJSON.h>
#include <time.h>
static const char *TAG = "WeatherService";
WeatherService::WeatherService() {
city_code_ = "101010100"; // Default: Beijing
auto_detect_enabled_ = false;
}
WeatherService::~WeatherService() {
}
void WeatherService::Initialize(const std::string& city_code) {
if (city_code.empty()) {
// Auto-detect city code by IP
ESP_LOGI(TAG, "Auto-detecting city code by IP address...");
if (AutoDetectCityCode()) {
ESP_LOGI(TAG, "Auto-detected city code: %s", city_code_.c_str());
auto_detect_enabled_ = true;
} else {
ESP_LOGW(TAG, "Failed to auto-detect city code, using default: Beijing (101010100)");
city_code_ = "101010100";
auto_detect_enabled_ = false;
}
} else {
city_code_ = city_code;
auto_detect_enabled_ = false;
ESP_LOGI(TAG, "Weather service initialized with city code: %s", city_code_.c_str());
}
}
bool WeatherService::AutoDetectCityCode() {
// Use weather.com.cn IP geolocation API
time_t now;
time(&now);
char url[256];
snprintf(url, sizeof(url), "http://wgeo.weather.com.cn/ip/?_=%ld", (long)now);
ESP_LOGI(TAG, "Fetching city code from: %s", url);
// Allocate response buffer
char *buffer = (char*)malloc(2048);
if (!buffer) {
ESP_LOGE(TAG, "Failed to allocate buffer");
return false;
}
memset(buffer, 0, 2048);
// Configure HTTP client with larger buffer
esp_http_client_config_t config = {};
config.url = url;
config.timeout_ms = 15000;
config.buffer_size = 2048; // Important: increase buffer size
config.buffer_size_tx = 1024;
config.disable_auto_redirect = false;
config.max_redirection_count = 3;
esp_http_client_handle_t client = esp_http_client_init(&config);
if (!client) {
ESP_LOGE(TAG, "Failed to init HTTP client");
free(buffer);
return false;
}
// Set headers
esp_http_client_set_header(client, "User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
esp_http_client_set_header(client, "Referer", "http://www.weather.com.cn/");
esp_http_client_set_header(client, "Accept", "*/*");
bool success = false;
// Open connection and read
esp_err_t err = esp_http_client_open(client, 0);
if (err == ESP_OK) {
int content_length = esp_http_client_fetch_headers(client);
int status_code = esp_http_client_get_status_code(client);
ESP_LOGI(TAG, "HTTP Status = %d, Content-Length = %d", status_code, content_length);
if (status_code == 200 || status_code == 302) {
// Read response data
int total_read = 0;
int read_len;
while (total_read < 2047) {
read_len = esp_http_client_read(client, buffer + total_read, 2047 - total_read);
if (read_len <= 0) {
break;
}
total_read += read_len;
}
buffer[total_read] = '\0';
ESP_LOGI(TAG, "Read %d bytes from API", total_read);
if (total_read > 0) {
// Print first 200 chars for debugging
char preview[201];
int preview_len = (total_read > 200) ? 200 : total_read;
memcpy(preview, buffer, preview_len);
preview[preview_len] = '\0';
ESP_LOGI(TAG, "Response preview: %s", preview);
std::string response(buffer);
// Try multiple parsing patterns
size_t id_pos = response.find("id=\"");
if (id_pos != std::string::npos) {
// Pattern: id="101010100" (JavaScript variable with double quotes)
size_t start = id_pos + 4; // Skip id="
size_t end = response.find("\"", start);
if (end != std::string::npos && end > start) {
city_code_ = response.substr(start, end - start);
ESP_LOGI(TAG, "✅ Detected city code: %s", city_code_.c_str());
success = true;
}
} else if ((id_pos = response.find("id='")) != std::string::npos) {
// Pattern: id='101010100' (JavaScript variable with single quotes)
size_t start = id_pos + 4;
size_t end = response.find("'", start);
if (end != std::string::npos && end > start) {
city_code_ = response.substr(start, end - start);
ESP_LOGI(TAG, "✅ Detected city code: %s", city_code_.c_str());
success = true;
}
} else if ((id_pos = response.find("id\":\"")) != std::string::npos) {
// Pattern: "id":"101010100" (JSON format)
size_t start = id_pos + 5;
size_t end = response.find("\"", start);
if (end != std::string::npos && end > start) {
city_code_ = response.substr(start, end - start);
ESP_LOGI(TAG, "✅ Detected city code: %s", city_code_.c_str());
success = true;
}
} else {
ESP_LOGW(TAG, "❌ City code pattern not found in response");
}
} else {
ESP_LOGW(TAG, "❌ No data read from API");
}
} else {
ESP_LOGW(TAG, "❌ HTTP status: %d", status_code);
}
esp_http_client_close(client);
} else {
ESP_LOGE(TAG, "❌ Failed to connect: %s", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
free(buffer);
return success;
}
void WeatherService::SetWeatherCallback(std::function<void(const WeatherData&)> callback) {
weather_callback_ = callback;
}
void WeatherService::FetchWeather() {
ESP_LOGI(TAG, "Fetching weather data for city: %s", city_code_.c_str());
// Construct URL
time_t now;
time(&now);
char url[256];
snprintf(url, sizeof(url), "http://d1.weather.com.cn/weather_index/%s.html?_=%ld",
city_code_.c_str(), (long)now);
// Configure HTTP client (no event handler, read directly)
esp_http_client_config_t config = {};
config.url = url;
config.timeout_ms = 10000;
esp_http_client_handle_t client = esp_http_client_init(&config);
// Set User-Agent header
esp_http_client_set_header(client, "User-Agent",
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38");
esp_http_client_set_header(client, "Referer", "http://www.weather.com.cn/");
// Open connection
esp_err_t err = esp_http_client_open(client, 0);
if (err == ESP_OK) {
// Read response
int content_length = esp_http_client_fetch_headers(client);
int status_code = esp_http_client_get_status_code(client);
ESP_LOGI(TAG, "HTTP GET Status = %d, Content-Length = %d", status_code, content_length);
if (status_code == 200) {
// Allocate buffer (use 8KB if content_length unknown or too large)
int buffer_size = (content_length > 0 && content_length < 8192) ? content_length + 1 : 8192;
char *buffer = (char*)malloc(buffer_size);
if (buffer) {
int total_read = 0;
int read_len;
// Read all data
while ((read_len = esp_http_client_read(client, buffer + total_read, buffer_size - total_read - 1)) > 0) {
total_read += read_len;
if (total_read >= buffer_size - 1) {
break;
}
}
buffer[total_read] = '\0';
ESP_LOGI(TAG, "Read %d bytes of weather data", total_read);
std::string response(buffer);
ParseWeatherData(response);
if (weather_callback_) {
weather_callback_(last_weather_data_);
}
free(buffer);
} else {
ESP_LOGE(TAG, "Failed to allocate buffer for response");
}
} else {
ESP_LOGW(TAG, "HTTP request returned status code: %d", status_code);
}
} else {
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
}
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
std::string WeatherService::ExtractJsonValue(const std::string& json, const std::string& key) {
size_t key_pos = json.find("\"" + key + "\":");
if (key_pos == std::string::npos) {
return "";
}
size_t value_start = json.find("\"", key_pos + key.length() + 3);
if (value_start == std::string::npos) {
return "";
}
value_start++;
size_t value_end = json.find("\"", value_start);
if (value_end == std::string::npos) {
return "";
}
return json.substr(value_start, value_end - value_start);
}
void WeatherService::ParseWeatherData(const std::string& response) {
ESP_LOGI(TAG, "Parsing weather data...");
try {
// Extract dataSK JSON section
size_t sk_start = response.find("dataSK =");
size_t sk_end = response.find(";var dataZS");
if (sk_start != std::string::npos && sk_end != std::string::npos) {
std::string dataSK = response.substr(sk_start + 8, sk_end - sk_start - 8);
// Parse JSON using cJSON
cJSON *root = cJSON_Parse(dataSK.c_str());
if (root) {
cJSON *city = cJSON_GetObjectItem(root, "cityname");
cJSON *temp = cJSON_GetObjectItem(root, "temp");
cJSON *humidity = cJSON_GetObjectItem(root, "SD");
cJSON *weather = cJSON_GetObjectItem(root, "weather");
cJSON *wind_dir = cJSON_GetObjectItem(root, "WD");
cJSON *wind_speed = cJSON_GetObjectItem(root, "WS");
cJSON *aqi = cJSON_GetObjectItem(root, "aqi");
if (city && cJSON_IsString(city)) {
last_weather_data_.city_name = city->valuestring;
}
if (temp && cJSON_IsString(temp)) {
last_weather_data_.temperature = temp->valuestring;
}
if (humidity && cJSON_IsString(humidity)) {
last_weather_data_.humidity = humidity->valuestring;
}
if (weather && cJSON_IsString(weather)) {
last_weather_data_.weather_desc = weather->valuestring;
}
if (wind_dir && cJSON_IsString(wind_dir)) {
last_weather_data_.wind_direction = wind_dir->valuestring;
}
if (wind_speed && cJSON_IsString(wind_speed)) {
last_weather_data_.wind_speed = wind_speed->valuestring;
}
if (aqi && cJSON_IsNumber(aqi)) {
last_weather_data_.aqi = aqi->valueint;
// Determine AQI description
if (last_weather_data_.aqi > 200) {
last_weather_data_.aqi_desc = "重度";
} else if (last_weather_data_.aqi > 150) {
last_weather_data_.aqi_desc = "中度";
} else if (last_weather_data_.aqi > 100) {
last_weather_data_.aqi_desc = "轻度";
} else if (last_weather_data_.aqi > 50) {
last_weather_data_.aqi_desc = "";
} else {
last_weather_data_.aqi_desc = "";
}
}
cJSON_Delete(root);
}
}
// Extract forecast data (f section)
size_t fc_start = response.find("\"f\":[");
size_t fc_end = response.find(",{\"fa", fc_start);
if (fc_start != std::string::npos && fc_end != std::string::npos) {
std::string dataFC = response.substr(fc_start + 5, fc_end - fc_start - 5);
cJSON *root = cJSON_Parse(dataFC.c_str());
if (root) {
cJSON *temp_low = cJSON_GetObjectItem(root, "fd");
cJSON *temp_high = cJSON_GetObjectItem(root, "fc");
if (temp_low && cJSON_IsString(temp_low)) {
last_weather_data_.temp_low = temp_low->valuestring;
}
if (temp_high && cJSON_IsString(temp_high)) {
last_weather_data_.temp_high = temp_high->valuestring;
}
cJSON_Delete(root);
}
}
last_weather_data_.last_update_time = esp_timer_get_time() / 1000000;
ESP_LOGI(TAG, "Weather parsed: City=%s, Temp=%s℃, Humidity=%s, AQI=%d(%s)",
last_weather_data_.city_name.c_str(),
last_weather_data_.temperature.c_str(),
last_weather_data_.humidity.c_str(),
last_weather_data_.aqi,
last_weather_data_.aqi_desc.c_str());
} catch (...) {
ESP_LOGE(TAG, "Failed to parse weather data");
}
}

View File

@@ -0,0 +1,46 @@
/**
* @file weather_service.h
* @brief Weather API service for fetching weather data
* @version 1.0
* @date 2025-01-10
*/
#pragma once
#include <string>
#include <functional>
#include "idle_screen.h"
class WeatherService {
public:
WeatherService();
~WeatherService();
// Initialize weather service with city code (optional, will auto-detect if empty)
void Initialize(const std::string& city_code = ""); // Empty: auto-detect
// Auto-detect city code by IP address
bool AutoDetectCityCode();
// Fetch weather data (async)
void FetchWeather();
// Set callback for when weather data is updated
void SetWeatherCallback(std::function<void(const WeatherData&)> callback);
// Get last fetched weather data
const WeatherData& GetLastWeatherData() const { return last_weather_data_; }
// Get current city code
const std::string& GetCityCode() const { return city_code_; }
private:
void ParseWeatherData(const std::string& response);
std::string ExtractJsonValue(const std::string& json, const std::string& key);
private:
std::string city_code_;
WeatherData last_weather_data_;
std::function<void(const WeatherData&)> weather_callback_;
bool auto_detect_enabled_;
};

View File

@@ -0,0 +1,62 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_INPUT_REFERENCE true
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38
#define AUDIO_I2S_GPIO_WS GPIO_NUM_13
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_12
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_45
#define AUDIO_CODEC_USE_PCA9557
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_1
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_2
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define AUDIO_CODEC_ES7210_ADDR 0x82
#define BUILTIN_LED_GPIO GPIO_NUM_48
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY true
#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
/* Camera pins */
#define CAMERA_PIN_PWDN -1
#define CAMERA_PIN_RESET -1
#define CAMERA_PIN_XCLK 5
#define CAMERA_PIN_SIOD 1
#define CAMERA_PIN_SIOC 2
#define CAMERA_PIN_D7 9
#define CAMERA_PIN_D6 4
#define CAMERA_PIN_D5 6
#define CAMERA_PIN_D4 15
#define CAMERA_PIN_D3 17
#define CAMERA_PIN_D2 8
#define CAMERA_PIN_D1 18
#define CAMERA_PIN_D0 16
#define CAMERA_PIN_VSYNC 3
#define CAMERA_PIN_HREF 46
#define CAMERA_PIN_PCLK 7
#define XCLK_FREQ_HZ 24000000
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,12 @@
{
"target": "esp32s3",
"builds": [
{
"name": "jinao-s3",
"sdkconfig_append": [
"CONFIG_USE_DEVICE_AEC=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\""
]
}
]
}

View File

@@ -0,0 +1,262 @@
#include "wifi_board.h"
#include "codecs/box_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "i2c_device.h"
#include "esp32_camera.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <wifi_station.h>
#include <esp_lcd_touch_ft5x06.h>
#include <esp_lvgl_port.h>
#include <lvgl.h>
#define TAG "JiNaoS3Board"
class Pca9557 : public I2cDevice {
public:
Pca9557(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
WriteReg(0x01, 0x03);
WriteReg(0x03, 0xf8);
}
void SetOutputState(uint8_t bit, uint8_t level) {
uint8_t data = ReadReg(0x01);
data = (data & ~(1 << bit)) | (level << bit);
WriteReg(0x01, data);
}
};
class CustomAudioCodec : public BoxAudioCodec {
private:
Pca9557* pca9557_;
public:
CustomAudioCodec(i2c_master_bus_handle_t i2c_bus, Pca9557* pca9557)
: BoxAudioCodec(i2c_bus,
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC,
AUDIO_CODEC_ES8311_ADDR,
AUDIO_CODEC_ES7210_ADDR,
AUDIO_INPUT_REFERENCE),
pca9557_(pca9557) {
}
virtual void EnableOutput(bool enable) override {
BoxAudioCodec::EnableOutput(enable);
if (enable) {
pca9557_->SetOutputState(1, 1);
} else {
pca9557_->SetOutputState(1, 0);
}
}
};
class JiNaoS3Board : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
i2c_master_dev_handle_t pca9557_handle_;
Button boot_button_;
LcdDisplay* display_;
Pca9557* pca9557_;
Esp32Camera* camera_;
void InitializeI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)1,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
// Initialize PCA9557
pca9557_ = new Pca9557(i2c_bus_, 0x19);
}
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = GPIO_NUM_40;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = GPIO_NUM_41;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
ResetWifiConfiguration();
}
app.ToggleChatState();
});
#if CONFIG_USE_DEVICE_AEC
boot_button_.OnDoubleClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateIdle) {
app.SetAecMode(app.GetAecMode() == kAecOff ? kAecOnDeviceSide : kAecOff);
}
});
#endif
}
void InitializeSt7789Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
// 液晶屏控制IO初始化
ESP_LOGD(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = GPIO_NUM_NC;
io_config.dc_gpio_num = GPIO_NUM_39;
io_config.spi_mode = 2;
io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io));
// 初始化液晶屏驱动芯片ST7789
ESP_LOGD(TAG, "Install LCD driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
pca9557_->SetOutputState(0, 0);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
}
void InitializeTouch()
{
esp_lcd_touch_handle_t tp;
esp_lcd_touch_config_t tp_cfg = {
.x_max = DISPLAY_WIDTH,
.y_max = DISPLAY_HEIGHT,
.rst_gpio_num = GPIO_NUM_NC, // Shared with LCD reset
.int_gpio_num = GPIO_NUM_NC,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = 1,
.mirror_x = 1,
.mirror_y = 0,
},
};
esp_lcd_panel_io_handle_t tp_io_handle = NULL;
esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_FT5x06_CONFIG();
tp_io_config.scl_speed_hz = 400000;
esp_lcd_new_panel_io_i2c(i2c_bus_, &tp_io_config, &tp_io_handle);
esp_lcd_touch_new_i2c_ft5x06(tp_io_handle, &tp_cfg, &tp);
assert(tp);
/* Add touch input (for selected screen) */
const lvgl_port_touch_cfg_t touch_cfg = {
.disp = lv_display_get_default(),
.handle = tp,
};
lvgl_port_add_touch(&touch_cfg);
}
void InitializeCamera() {
// Open camera power
pca9557_->SetOutputState(2, 0);
camera_config_t config = {};
config.ledc_channel = LEDC_CHANNEL_2; // LEDC通道选择 用于生成XCLK时钟 但是S3不用
config.ledc_timer = LEDC_TIMER_2; // LEDC timer选择 用于生成XCLK时钟 但是S3不用
config.pin_d0 = CAMERA_PIN_D0;
config.pin_d1 = CAMERA_PIN_D1;
config.pin_d2 = CAMERA_PIN_D2;
config.pin_d3 = CAMERA_PIN_D3;
config.pin_d4 = CAMERA_PIN_D4;
config.pin_d5 = CAMERA_PIN_D5;
config.pin_d6 = CAMERA_PIN_D6;
config.pin_d7 = CAMERA_PIN_D7;
config.pin_xclk = CAMERA_PIN_XCLK;
config.pin_pclk = CAMERA_PIN_PCLK;
config.pin_vsync = CAMERA_PIN_VSYNC;
config.pin_href = CAMERA_PIN_HREF;
config.pin_sccb_sda = -1; // 这里写-1 表示使用已经初始化的I2C接口
config.pin_sccb_scl = CAMERA_PIN_SIOC;
config.sccb_i2c_port = 1;
config.pin_pwdn = CAMERA_PIN_PWDN;
config.pin_reset = CAMERA_PIN_RESET;
config.xclk_freq_hz = XCLK_FREQ_HZ;
config.pixel_format = PIXFORMAT_RGB565;
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 12;
config.fb_count = 1;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
camera_ = new Esp32Camera(config);
}
public:
JiNaoS3Board() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c();
InitializeSpi();
InitializeSt7789Display();
InitializeTouch();
InitializeButtons();
InitializeCamera();
GetBacklight()->RestoreBrightness();
}
virtual AudioCodec* GetAudioCodec() override {
static CustomAudioCodec audio_codec(
i2c_bus_,
pca9557_);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
virtual Camera* GetCamera() override {
return camera_;
}
};
DECLARE_BOARD(JiNaoS3Board);

View File

@@ -7,5 +7,5 @@
```
Partition Table --->
Partition Table (Custom partition table CSV) --->
(partitions/v1/8m.csv) Custom partition CSV file
(partitions/v2/8m.csv) Custom partition CSV file
```

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