Upgrade Playlist Features
This commit is contained in:
111
.github/workflows/build.yml
vendored
Normal file
111
.github/workflows/build.yml
vendored
Normal 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
18
.gitignore
vendored
Normal 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
6
CMakeLists.txt
Executable file → Normal 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)
|
||||
|
||||
|
||||
87
README.md
87
README.md
@@ -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 协议实现多端控制。
|
||||
|
||||

|
||||
@@ -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
157
README_en.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
### Features Implemented
|
||||
|
||||
- Wi-Fi / ML307 Cat.1 4G
|
||||
- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- Supports two communication protocols ([Websocket](docs/websocket.md) or MQTT+UDP)
|
||||
- Uses OPUS audio codec
|
||||
- Voice interaction based on streaming ASR + LLM + TTS architecture
|
||||
- Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD display, supports emoji display
|
||||
- Battery display and power management
|
||||
- Multi-language support (Chinese, English, Japanese)
|
||||
- Supports ESP32-C3, ESP32-S3, ESP32-P4 chip platforms
|
||||
- Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.)
|
||||
- Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.)
|
||||
|
||||
## Hardware
|
||||
|
||||
### Breadboard DIY Practice
|
||||
|
||||
See the Feishu document tutorial:
|
||||
|
||||
👉 ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
|
||||
Breadboard demo:
|
||||
|
||||

|
||||
|
||||
### Supports 70+ Open Source Hardware (Partial List)
|
||||
|
||||
- <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
157
README_ja.md
Normal 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プロトコルを通じてマルチエンド制御を実現します。
|
||||
|
||||

|
||||
|
||||
### 実装済み機能
|
||||
|
||||
- Wi-Fi / ML307 Cat.1 4G
|
||||
- オフライン音声ウェイクアップ [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- 2種類の通信プロトコルに対応([Websocket](docs/websocket.md) または MQTT+UDP)
|
||||
- OPUSオーディオコーデックを採用
|
||||
- ストリーミングASR + LLM + TTSアーキテクチャに基づく音声インタラクション
|
||||
- 話者認識、現在話している人を識別 [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCDディスプレイ、表情表示対応
|
||||
- バッテリー表示と電源管理
|
||||
- 多言語対応(中国語、英語、日本語)
|
||||
- ESP32-C3、ESP32-S3、ESP32-P4チッププラットフォーム対応
|
||||
- デバイス側MCPによるデバイス制御(音量・明るさ調整、アクション制御など)
|
||||
- クラウド側MCPで大規模モデル能力を拡張(スマートホーム制御、PCデスクトップ操作、知識検索、メール送受信など)
|
||||
|
||||
## ハードウェア
|
||||
|
||||
### ブレッドボード手作り実践
|
||||
|
||||
Feishuドキュメントチュートリアルをご覧ください:
|
||||
|
||||
👉 [「シャオジーAIチャットボット百科事典」](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
|
||||
ブレッドボードのデモ:
|
||||
|
||||

|
||||
|
||||
### 70種類以上のオープンソースハードウェアに対応(一部のみ表示)
|
||||
|
||||
- <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
228
SERVER_CONFIG_GUIDE.md
Normal 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界面。
|
||||
|
||||
---
|
||||
|
||||
### **方法2:curl测试**
|
||||
|
||||
```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`)
|
||||
- [ ] 测试连接
|
||||
|
||||
---
|
||||
|
||||
## 🐛 **常见问题**
|
||||
|
||||
### **问题1:ESP32无法连接服务器**
|
||||
|
||||
**现象**:
|
||||
```
|
||||
[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
BIN
combine.exe
Executable file
Binary file not shown.
0
docs/v0/atoms3r-echo-base.jpg
Executable file → Normal file
0
docs/v0/atoms3r-echo-base.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
0
docs/v1/movecall-cuican-esp32s3.jpg
Executable file → Normal file
0
docs/v1/movecall-cuican-esp32s3.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
BIN
esptool.exe
Executable file
BIN
esptool.exe
Executable file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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
671
main/alarm_manager.cc
Normal 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
140
main/alarm_manager.h
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
129
main/assets.cc
129
main/assets.cc
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
{
|
||||
"name": "atommatrix-echo-base",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m.csv\""
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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\""
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) \
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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显示风格,通过修改代码中的宏定义来选择:
|
||||
|
||||
@@ -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\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
22
main/boards/echoear/emote.json
Normal file
22
main/boards/echoear/emote.json
Normal 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}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
37
main/boards/echoear/layout.json
Normal file
37
main/boards/echoear/layout.json
Normal 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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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\""
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
22
main/boards/esp-box-3/emote.json
Normal file
22
main/boards/esp-box-3/emote.json
Normal 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}
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
37
main/boards/esp-box-3/layout.json
Normal file
37
main/boards/esp-box-3/layout.json
Normal 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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
{
|
||||
"name": "esp32-cgc-144",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y",
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/4m.csv\""
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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. **休眠模式**: 不使用时进入低功耗模式
|
||||
|
||||
## 许可证
|
||||
|
||||
本代码遵循项目的许可证要求。
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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_, ®_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表示更信任陀螺仪
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
185
main/boards/genjutech-s3-1.54tft/WEATHER_CLOCK_README.md
Normal file
185
main/boards/genjutech-s3-1.54tft/WEATHER_CLOCK_README.md
Normal 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
|
||||
|
||||
@@ -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_
|
||||
@@ -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 {
|
||||
|
||||
456
main/boards/genjutech-s3-1.54tft/idle_screen.cc
Normal file
456
main/boards/genjutech-s3-1.54tft/idle_screen.cc
Normal 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
|
||||
118
main/boards/genjutech-s3-1.54tft/idle_screen.h
Normal file
118
main/boards/genjutech-s3-1.54tft/idle_screen.h
Normal 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
|
||||
3545
main/boards/genjutech-s3-1.54tft/ui_font_font48Seg.c
Normal file
3545
main/boards/genjutech-s3-1.54tft/ui_font_font48Seg.c
Normal file
File diff suppressed because it is too large
Load Diff
349
main/boards/genjutech-s3-1.54tft/ui_helpers.c
Normal file
349
main/boards/genjutech-s3-1.54tft/ui_helpers.c
Normal 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
|
||||
154
main/boards/genjutech-s3-1.54tft/ui_helpers.h
Normal file
154
main/boards/genjutech-s3-1.54tft/ui_helpers.h
Normal 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
|
||||
107
main/boards/genjutech-s3-1.54tft/ui_img_xiaozhi_48_png.c
Normal file
107
main/boards/genjutech-s3-1.54tft/ui_img_xiaozhi_48_png.c
Normal 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
|
||||
365
main/boards/genjutech-s3-1.54tft/weather_service.cc
Normal file
365
main/boards/genjutech-s3-1.54tft/weather_service.cc
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
46
main/boards/genjutech-s3-1.54tft/weather_service.h
Normal file
46
main/boards/genjutech-s3-1.54tft/weather_service.h
Normal 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_;
|
||||
};
|
||||
|
||||
62
main/boards/jinao-s3/config.h
Normal file
62
main/boards/jinao-s3/config.h
Normal 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_
|
||||
12
main/boards/jinao-s3/config.json
Normal file
12
main/boards/jinao-s3/config.json
Normal 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\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
262
main/boards/jinao-s3/jinao_s3_board.cc
Normal file
262
main/boards/jinao-s3/jinao_s3_board.cc
Normal 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);
|
||||
@@ -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
Reference in New Issue
Block a user