Change LCD display layout from grids to layers (#1438)
Some checks are pending
Build Boards / Determine variants to build (push) Waiting to run
Build Boards / Build ${{ matrix.name }} (push) Blocked by required conditions

* Upgrade component version

* update fonts component version

* change lcd display layout from grids to layers

* Update English README as default

* Handle OTA error code
This commit is contained in:
Xiaoxia 2025-11-17 22:38:31 +08:00 committed by GitHub
parent 764f6e3349
commit 511349a7bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 630 additions and 495 deletions

View File

@ -4,7 +4,7 @@
# CMakeLists in this exact order for cmake to work correctly # CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
set(PROJECT_VER "2.0.4") set(PROJECT_VER "2.0.5")
# Add this line to disable the specific warning # Add this line to disable the specific warning
add_compile_options(-Wno-missing-field-initializers) add_compile_options(-Wno-missing-field-initializers)

164
README.md
View File

@ -1,73 +1,73 @@
# An MCP-based Chatbot # An MCP-based Chatbot
(中文 | [English](README_en.md) | [日本語](README_ja.md) (English | [中文](README_zh.md) | [日本語](README_ja.md))
## 介绍 ## Introduction
👉 [人类:给 AI 装摄像头 vs AI当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/) 👉 [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/)
👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/) 👉 [Handcraft your AI girlfriend, beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 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.
<img src="docs/mcp-based-graph.jpg" alt="通过MCP控制万物" width="320"> <img src="docs/mcp-based-graph.jpg" alt="Control everything via MCP" width="320">
### 版本说明 ## Version Notes
当前 v2 版本与 v1 版本分区表不兼容,所以无法从 v1 版本通过 OTA 升级到 v2 版本。分区表说明参见 [partitions/v2/README.md](partitions/v2/README.md)。 The current v2 version is incompatible with the v1 partition table, so it is not possible to upgrade from v1 to v2 via OTA. For partition table details, see [partitions/v2/README.md](partitions/v2/README.md).
使用 v1 版本的所有硬件,可以通过手动烧录固件来升级到 v2 版本。 All hardware running v1 can be upgraded to v2 by manually flashing the firmware.
v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版本,该分支会持续维护到 2026 年 2 月。 The stable version of v1 is 1.9.2. You can switch to v1 by running `git checkout v1`. The v1 branch will be maintained until February 2026.
### 已实现功能 ### Features Implemented
- Wi-Fi / ML307 Cat.1 4G - Wi-Fi / ML307 Cat.1 4G
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr) - Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
- 支持两种通信协议([Websocket](docs/websocket.md) 或 MQTT+UDP - Supports two communication protocols ([Websocket](docs/websocket.md) or MQTT+UDP)
- 采用 OPUS 音频编解码 - Uses OPUS audio codec
- 基于流式 ASR + LLM + TTS 架构的语音交互 - Voice interaction based on streaming ASR + LLM + TTS architecture
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker) - Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker)
- OLED / LCD 显示屏,支持表情显示 - OLED / LCD display, supports emoji display
- 电量显示与电源管理 - Battery display and power management
- 支持多语言(中文、英文、日文) - Multi-language support (Chinese, English, Japanese)
- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台 - Supports ESP32-C3, ESP32-S3, ESP32-P4 chip platforms
- 通过设备端 MCP 实现设备控制音量、灯光、电机、GPIO 等) - Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.)
- 通过云端 MCP 扩展大模型能力智能家居控制、PC桌面操作、知识搜索、邮件收发等 - Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.)
- 自定义唤醒词、字体、表情与聊天背景,支持网页端在线修改 ([自定义Assets生成器](https://github.com/78/xiaozhi-assets-generator)) - Customizable wake words, fonts, emojis, and chat backgrounds with online web-based editing ([Custom Assets Generator](https://github.com/78/xiaozhi-assets-generator))
## 硬件 ## Hardware
### 面包板手工制作实践 ### Breadboard DIY Practice
详见飞书文档教程: See the Feishu document tutorial:
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink) 👉 ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
面包板效果图如下: Breadboard demo:
![面包板效果图](docs/v1/wiring2.jpg) ![Breadboard Demo](docs/v1/wiring2.jpg)
### 支持 70 多个开源硬件(仅展示部分) ### 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="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a> - <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="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</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/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://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://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="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</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://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/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">璀璨·AI 吊坠</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="无名科技Nologo-星智-1.54">无名科技 Nologo-星智-1.54TFT</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.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> - <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;"> <div style="display: flex; justify-content: space-between;">
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板"> <a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
<img src="docs/v1/lichuang-s3.jpg" width="240" /> <img src="docs/v1/lichuang-s3.jpg" width="240" />
</a> </a>
<a href="docs/v1/espbox3.jpg" target="_blank" title="乐鑫 ESP32-S3-BOX3"> <a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
<img src="docs/v1/espbox3.jpg" width="240" /> <img src="docs/v1/espbox3.jpg" width="240" />
</a> </a>
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3"> <a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
@ -76,86 +76,90 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base"> <a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
<img src="docs/v1/atoms3r.jpg" width="240" /> <img src="docs/v1/atoms3r.jpg" width="240" />
</a> </a>
<a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4"> <a href="docs/v1/magiclick.jpg" target="_blank" title="Magic Button 2.4">
<img src="docs/v1/magiclick.jpg" width="240" /> <img src="docs/v1/magiclick.jpg" width="240" />
</a> </a>
<a href="docs/v1/waveshare.jpg" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8"> <a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
<img src="docs/v1/waveshare.jpg" width="240" /> <img src="docs/v1/waveshare.jpg" width="240" />
</a> </a>
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3"> <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" /> <img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
</a> </a>
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3"> <a href="docs/v1/xmini-c3.jpg" target="_blank" title="XiaGe Mini C3">
<img src="docs/v1/xmini-c3.jpg" width="240" /> <img src="docs/v1/xmini-c3.jpg" width="240" />
</a> </a>
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan"> <a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" /> <img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
</a> </a>
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="无名科技Nologo-星智-1.54"> <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" /> <img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
</a> </a>
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher"> <a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
<img src="docs/v1/sensecap_watcher.jpg" width="240" /> <img src="docs/v1/sensecap_watcher.jpg" width="240" />
</a> </a>
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI 超低成本机器狗"> <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" /> <img src="docs/v1/esp-hi.jpg" width="240" />
</a> </a>
</div> </div>
## 软件 ## Software
### 固件烧录 ### Firmware Flashing
新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。 For beginners, it is recommended to use the firmware that can be flashed without setting up a development environment.
固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,个人用户注册账号可以免费使用 Qwen 实时模型。 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.
👉 [新手烧录固件教程](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS) 👉 [Beginner's Firmware Flashing Guide](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
### 开发环境 ### Development Environment
- Cursor VSCode - Cursor or VSCode
- 安装 ESP-IDF 插件,选择 SDK 版本 5.4 或以上 - Install ESP-IDF plugin, select SDK version 5.4 or above
- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰 - Linux is better than Windows for faster compilation and fewer driver issues
- 本项目使用 Google C++ 代码风格,提交代码时请确保符合规范 - This project uses Google C++ code style, please ensure compliance when submitting code
### 开发者文档 ### Developer Documentation
- [自定义开发板指南](docs/custom-board.md) - 学习如何为小智 AI 创建自定义开发板 - [Custom Board Guide](docs/custom-board.md) - Learn how to create custom boards for XiaoZhi AI
- [MCP 协议物联网控制用法说明](docs/mcp-usage.md) - 了解如何通过 MCP 协议控制物联网设备 - [MCP Protocol IoT Control Usage](docs/mcp-usage.md) - Learn how to control IoT devices via MCP protocol
- [MCP 协议交互流程](docs/mcp-protocol.md) - 设备端 MCP 协议的实现方式 - [MCP Protocol Interaction Flow](docs/mcp-protocol.md) - Device-side MCP protocol implementation
- [MQTT + UDP 混合通信协议文档](docs/mqtt-udp.md) - [MQTT + UDP Hybrid Communication Protocol Document](docs/mqtt-udp.md)
- [一份详细的 WebSocket 通信协议文档](docs/websocket.md) - [A detailed WebSocket communication protocol document](docs/websocket.md)
## 大模型配置 ## Large Model Configuration
如果你已经拥有一个小智 AI 聊天机器人设备,并且已接入官方服务器,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。 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.
👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/) 👉 [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 服务器 - [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 服务器 - [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 服务器 - [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 客户端 - [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python client
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客户端 - [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android client
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百问科技提供的 Linux 客户端 - [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) Linux client by 100ask
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技的蓝牙芯片固件 - [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) Bluetooth chip firmware by Sichuan
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移远提供的 QuecPython 固件 - [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) QuecPython firmware by Quectel
## 关于项目 Custom Assets Tools:
这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,修改或用于商业用途。 - [78/xiaozhi-assets-generator](https://github.com/78/xiaozhi-assets-generator) Custom Assets Generator (Wake words, fonts, emojis, backgrounds)
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。 ## About the Project
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群1011329060 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
## Star History ## Star History
@ -165,4 +169,4 @@ v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" /> <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" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
</picture> </picture>
</a> </a>

View File

@ -1,172 +0,0 @@
# An MCP-based Chatbot
(English | [中文](README.md) | [日本語](README_ja.md))
## Introduction
👉 [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/)
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.
<img src="docs/mcp-based-graph.jpg" alt="Control everything via MCP" width="320">
## Version Notes
The current v2 version is incompatible with the v1 partition table, so it is not possible to upgrade from v1 to v2 via OTA. For partition table details, see [partitions/v2/README.md](partitions/v2/README.md).
All hardware running v1 can be upgraded to v2 by manually flashing the firmware.
The stable version of v1 is 1.9.2. You can switch to v1 by running `git checkout v1`. The v1 branch will be maintained until February 2026.
### 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.)
- Customizable wake words, fonts, emojis, and chat backgrounds with online web-based editing ([Custom Assets Generator](https://github.com/78/xiaozhi-assets-generator))
## Hardware
### Breadboard DIY Practice
See the Feishu document tutorial:
👉 ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
Breadboard demo:
![Breadboard Demo](docs/v1/wiring2.jpg)
### Supports 70+ Open Source Hardware (Partial List)
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="Magic Button 2.4">Magic Button 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a>
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AI Pendant</a>
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="WMnologo-Xingzhi-1.54">WMnologo-Xingzhi-1.54TFT</a>
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI Low Cost Robot Dog">ESP-HI Low Cost Robot Dog</a>
<div style="display: flex; justify-content: space-between;">
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
<img src="docs/v1/lichuang-s3.jpg" width="240" />
</a>
<a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
<img src="docs/v1/espbox3.jpg" width="240" />
</a>
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
<img src="docs/v1/m5cores3.jpg" width="240" />
</a>
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
<img src="docs/v1/atoms3r.jpg" width="240" />
</a>
<a href="docs/v1/magiclick.jpg" target="_blank" title="Magic Button 2.4">
<img src="docs/v1/magiclick.jpg" width="240" />
</a>
<a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
<img src="docs/v1/waveshare.jpg" width="240" />
</a>
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
</a>
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="XiaGe Mini C3">
<img src="docs/v1/xmini-c3.jpg" width="240" />
</a>
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
</a>
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="WMnologo-Xingzhi-1.54">
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
</a>
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
</a>
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI Low Cost Robot Dog">
<img src="docs/v1/esp-hi.jpg" width="240" />
</a>
</div>
## Software
### Firmware Flashing
For beginners, it is recommended to use the firmware that can be flashed without setting up a development environment.
The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by default. Personal users can register an account to use the Qwen real-time model for free.
👉 [Beginner's Firmware Flashing Guide](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
### Development Environment
- Cursor or VSCode
- Install ESP-IDF plugin, select SDK version 5.4 or above
- Linux is better than Windows for faster compilation and fewer driver issues
- This project uses Google C++ code style, please ensure compliance when submitting code
### Developer Documentation
- [Custom Board Guide](docs/custom-board.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
- [MQTT + UDP Hybrid Communication Protocol Document](docs/mqtt-udp.md)
- [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
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) Linux client by 100ask
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) Bluetooth chip firmware by Sichuan
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) QuecPython firmware by Quectel
Custom Assets Tools:
- [78/xiaozhi-assets-generator](https://github.com/78/xiaozhi-assets-generator) Custom Assets Generator (Wake words, fonts, emojis, backgrounds)
## About the Project
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
## 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>

View File

@ -1,6 +1,6 @@
# MCP ベースのチャットボット # MCP ベースのチャットボット
(日本語 | [中文](README.md) | [English](README_en.md) (日本語 | [中文](README_zh.md) | [English](README.md)
## はじめに ## はじめに

168
README_zh.md Normal file
View File

@ -0,0 +1,168 @@
# An MCP-based Chatbot
(中文 | [English](README.md) | [日本語](README_ja.md)
## 介绍
👉 [人类:给 AI 装摄像头 vs AI当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
👉 [手工打造你的 AI 女友新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制。
<img src="docs/mcp-based-graph.jpg" alt="通过MCP控制万物" width="320">
### 版本说明
当前 v2 版本与 v1 版本分区表不兼容,所以无法从 v1 版本通过 OTA 升级到 v2 版本。分区表说明参见 [partitions/v2/README.md](partitions/v2/README.md)。
使用 v1 版本的所有硬件,可以通过手动烧录固件来升级到 v2 版本。
v1 的稳定版本为 1.9.2,可以通过 `git checkout v1` 来切换到 v1 版本,该分支会持续维护到 2026 年 2 月。
### 已实现功能
- Wi-Fi / ML307 Cat.1 4G
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
- 支持两种通信协议([Websocket](docs/websocket.md) 或 MQTT+UDP
- 采用 OPUS 音频编解码
- 基于流式 ASR + LLM + TTS 架构的语音交互
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker)
- OLED / LCD 显示屏,支持表情显示
- 电量显示与电源管理
- 支持多语言(中文、英文、日文)
- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台
- 通过设备端 MCP 实现设备控制音量、灯光、电机、GPIO 等)
- 通过云端 MCP 扩展大模型能力智能家居控制、PC桌面操作、知识搜索、邮件收发等
- 自定义唤醒词、字体、表情与聊天背景,支持网页端在线修改 ([自定义Assets生成器](https://github.com/78/xiaozhi-assets-generator))
## 硬件
### 面包板手工制作实践
详见飞书文档教程:
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
面包板效果图如下:
![面包板效果图](docs/v1/wiring2.jpg)
### 支持 70 多个开源硬件(仅展示部分)
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a>
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a>
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a>
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a>
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">璀璨·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++ 代码风格,提交代码时请确保符合规范
### 开发者文档
- [自定义开发板指南](docs/custom-board.md) - 学习如何为小智 AI 创建自定义开发板
- [MCP 协议物联网控制用法说明](docs/mcp-usage.md) - 了解如何通过 MCP 协议控制物联网设备
- [MCP 协议交互流程](docs/mcp-protocol.md) - 设备端 MCP 协议的实现方式
- [MQTT + UDP 混合通信协议文档](docs/mqtt-udp.md)
- [一份详细的 WebSocket 通信协议文档](docs/websocket.md)
## 大模型配置
如果你已经拥有一个小智 AI 聊天机器人设备,并且已接入官方服务器,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。
👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
## 相关开源项目
在个人电脑上部署服务器,可以参考以下第三方开源的项目:
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python 服务器
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java 服务器
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang 服务器
使用小智通信协议的第三方客户端项目:
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python 客户端
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客户端
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百问科技提供的 Linux 客户端
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技的蓝牙芯片固件
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移远提供的 QuecPython 固件
## 关于项目
这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,修改或用于商业用途。
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群1011329060
## 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>

View File

@ -131,15 +131,18 @@ void Application::CheckNewVersion(Ota& ota) {
auto display = board.GetDisplay(); auto display = board.GetDisplay();
display->SetStatus(Lang::Strings::CHECKING_NEW_VERSION); display->SetStatus(Lang::Strings::CHECKING_NEW_VERSION);
if (!ota.CheckVersion()) { esp_err_t err = ota.CheckVersion();
if (err != ESP_OK) {
retry_count++; retry_count++;
if (retry_count >= MAX_RETRY) { if (retry_count >= MAX_RETRY) {
ESP_LOGE(TAG, "Too many retries, exit version check"); ESP_LOGE(TAG, "Too many retries, exit version check");
return; return;
} }
char error_message[128];
snprintf(error_message, sizeof(error_message), "code=%d, url=%s", err, ota.GetCheckVersionUrl().c_str());
char buffer[256]; char buffer[256];
snprintf(buffer, sizeof(buffer), Lang::Strings::CHECK_NEW_VERSION_FAILED, retry_delay, ota.GetCheckVersionUrl().c_str()); snprintf(buffer, sizeof(buffer), Lang::Strings::CHECK_NEW_VERSION_FAILED, retry_delay, error_message);
Alert(Lang::Strings::ERROR, buffer, "cloud_slash", Lang::Sounds::OGG_EXCLAMATION); Alert(Lang::Strings::ERROR, buffer, "cloud_slash", Lang::Sounds::OGG_EXCLAMATION);
ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", retry_delay, retry_count, MAX_RETRY); ESP_LOGW(TAG, "Check new version failed, retry in %d seconds (%d/%d)", retry_delay, retry_count, MAX_RETRY);

View File

@ -4,6 +4,9 @@
#include "application.h" #include "application.h"
#include "lvgl_theme.h" #include "lvgl_theme.h"
#include "emote_display.h" #include "emote_display.h"
#ifdef HAVE_LVGL
#include "display/lcd_display.h"
#endif
#include <esp_log.h> #include <esp_log.h>
#include <spi_flash_mmap.h> #include <spi_flash_mmap.h>
@ -248,6 +251,18 @@ bool Assets::Apply() {
if (current_theme != nullptr) { if (current_theme != nullptr) {
display->SetTheme(current_theme); display->SetTheme(current_theme);
} }
// Parse hide_subtitle configuration
cJSON* hide_subtitle = cJSON_GetObjectItem(root, "hide_subtitle");
if (cJSON_IsBool(hide_subtitle)) {
bool hide = cJSON_IsTrue(hide_subtitle);
auto lcd_display = dynamic_cast<LcdDisplay*>(display);
if (lcd_display != nullptr) {
lcd_display->SetHideSubtitle(hide);
ESP_LOGI(TAG, "Set hide_subtitle to %s", hide ? "true" : "false");
}
}
#elif defined(CONFIG_USE_EMOTE_MESSAGE_STYLE) #elif defined(CONFIG_USE_EMOTE_MESSAGE_STYLE)
auto &board = Board::GetInstance(); auto &board = Board::GetInstance();
auto display = board.GetDisplay(); auto display = board.GetDisplay();

View File

@ -41,17 +41,16 @@ void WifiBoard::EnterWifiConfigMode() {
wifi_ap.SetSsidPrefix("Xiaozhi"); wifi_ap.SetSsidPrefix("Xiaozhi");
wifi_ap.Start(); wifi_ap.Start();
// 等待 1.5 秒显示开发板信息 // Wait 1.5 seconds to display board information
vTaskDelay(pdMS_TO_TICKS(1500)); vTaskDelay(pdMS_TO_TICKS(1500));
// 显示 WiFi 配置 AP 的 SSID 和 Web 服务器 URL // Display WiFi configuration AP SSID and web server URL
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT; std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
hint += wifi_ap.GetSsid(); hint += wifi_ap.GetSsid();
hint += Lang::Strings::ACCESS_VIA_BROWSER; hint += Lang::Strings::ACCESS_VIA_BROWSER;
hint += wifi_ap.GetWebServerUrl(); hint += wifi_ap.GetWebServerUrl();
hint += "\n\n";
// 播报配置 WiFi 的提示 // Announce WiFi configuration prompt
application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "gear", Lang::Sounds::OGG_WIFICONFIG); application.Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "gear", Lang::Sounds::OGG_WIFICONFIG);
#if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING #if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING
@ -175,9 +174,9 @@ void WifiBoard::ResetWifiConfiguration() {
std::string WifiBoard::GetDeviceStatusJson() { std::string WifiBoard::GetDeviceStatusJson() {
/* /*
* JSON * Return device status JSON
* *
* JSON结构如下 * The returned JSON structure is as follows:
* { * {
* "audio_speaker": { * "audio_speaker": {
* "volume": 70 * "volume": 70

View File

@ -28,30 +28,30 @@ void LcdDisplay::InitializeLcdThemes() {
// light theme // light theme
auto light_theme = new LvglTheme("light"); auto light_theme = new LvglTheme("light");
light_theme->set_background_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255) light_theme->set_background_color(lv_color_hex(0xFFFFFF));
light_theme->set_text_color(lv_color_hex(0x000000)); //rgb(0, 0, 0) light_theme->set_text_color(lv_color_hex(0x000000));
light_theme->set_chat_background_color(lv_color_hex(0xE0E0E0)); //rgb(224, 224, 224) light_theme->set_chat_background_color(lv_color_hex(0xE0E0E0));
light_theme->set_user_bubble_color(lv_color_hex(0x00FF00)); //rgb(0, 128, 0) light_theme->set_user_bubble_color(lv_color_hex(0x00FF00));
light_theme->set_assistant_bubble_color(lv_color_hex(0xDDDDDD)); //rgb(221, 221, 221) light_theme->set_assistant_bubble_color(lv_color_hex(0xDDDDDD));
light_theme->set_system_bubble_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255) light_theme->set_system_bubble_color(lv_color_hex(0xFFFFFF));
light_theme->set_system_text_color(lv_color_hex(0x000000)); //rgb(0, 0, 0) light_theme->set_system_text_color(lv_color_hex(0x000000));
light_theme->set_border_color(lv_color_hex(0x000000)); //rgb(0, 0, 0) light_theme->set_border_color(lv_color_hex(0x000000));
light_theme->set_low_battery_color(lv_color_hex(0x000000)); //rgb(0, 0, 0) light_theme->set_low_battery_color(lv_color_hex(0x000000));
light_theme->set_text_font(text_font); light_theme->set_text_font(text_font);
light_theme->set_icon_font(icon_font); light_theme->set_icon_font(icon_font);
light_theme->set_large_icon_font(large_icon_font); light_theme->set_large_icon_font(large_icon_font);
// dark theme // dark theme
auto dark_theme = new LvglTheme("dark"); auto dark_theme = new LvglTheme("dark");
dark_theme->set_background_color(lv_color_hex(0x000000)); //rgb(0, 0, 0) dark_theme->set_background_color(lv_color_hex(0x000000));
dark_theme->set_text_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255) dark_theme->set_text_color(lv_color_hex(0xFFFFFF));
dark_theme->set_chat_background_color(lv_color_hex(0x1F1F1F)); //rgb(31, 31, 31) dark_theme->set_chat_background_color(lv_color_hex(0x1F1F1F));
dark_theme->set_user_bubble_color(lv_color_hex(0x00FF00)); //rgb(0, 128, 0) dark_theme->set_user_bubble_color(lv_color_hex(0x00FF00));
dark_theme->set_assistant_bubble_color(lv_color_hex(0x222222)); //rgb(34, 34, 34) dark_theme->set_assistant_bubble_color(lv_color_hex(0x222222));
dark_theme->set_system_bubble_color(lv_color_hex(0x000000)); //rgb(0, 0, 0) dark_theme->set_system_bubble_color(lv_color_hex(0x000000));
dark_theme->set_system_text_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255) dark_theme->set_system_text_color(lv_color_hex(0xFFFFFF));
dark_theme->set_border_color(lv_color_hex(0xFFFFFF)); //rgb(255, 255, 255) dark_theme->set_border_color(lv_color_hex(0xFFFFFF));
dark_theme->set_low_battery_color(lv_color_hex(0xFF0000)); //rgb(255, 0, 0) dark_theme->set_low_battery_color(lv_color_hex(0xFF0000));
dark_theme->set_text_font(text_font); dark_theme->set_text_font(text_font);
dark_theme->set_icon_font(icon_font); dark_theme->set_icon_font(icon_font);
dark_theme->set_large_icon_font(large_icon_font); dark_theme->set_large_icon_font(large_icon_font);
@ -173,7 +173,7 @@ SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_h
} }
// RGB LCD实现 // RGB LCD implementation
RgbLcdDisplay::RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, RgbLcdDisplay::RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
int width, int height, int offset_x, int offset_y, int width, int height, int offset_x, int offset_y,
bool mirror_x, bool mirror_y, bool swap_xy) bool mirror_x, bool mirror_y, bool swap_xy)
@ -320,9 +320,15 @@ LcdDisplay::~LcdDisplay() {
if (content_ != nullptr) { if (content_ != nullptr) {
lv_obj_del(content_); lv_obj_del(content_);
} }
if (bottom_bar_ != nullptr) {
lv_obj_del(bottom_bar_);
}
if (status_bar_ != nullptr) { if (status_bar_ != nullptr) {
lv_obj_del(status_bar_); lv_obj_del(status_bar_);
} }
if (top_bar_ != nullptr) {
lv_obj_del(top_bar_);
}
if (side_bar_ != nullptr) { if (side_bar_ != nullptr) {
lv_obj_del(side_bar_); lv_obj_del(side_bar_);
} }
@ -374,12 +380,76 @@ void LcdDisplay::SetupUI() {
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0); lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0); lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0);
/* Status bar */ /* Layer 1: Top bar - for status icons */
status_bar_ = lv_obj_create(container_); top_bar_ = lv_obj_create(container_);
lv_obj_set_size(top_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
lv_obj_set_style_radius(top_bar_, 0, 0);
lv_obj_set_style_bg_opa(top_bar_, LV_OPA_50, 0); // 50% opacity background
lv_obj_set_style_bg_color(top_bar_, lvgl_theme->background_color(), 0);
lv_obj_set_style_border_width(top_bar_, 0, 0);
lv_obj_set_style_pad_all(top_bar_, 0, 0);
lv_obj_set_style_pad_top(top_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_bottom(top_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_left(top_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_pad_right(top_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF);
// Left icon
network_label_ = lv_label_create(top_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, icon_font, 0);
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
// Right icons container
lv_obj_t* right_icons = lv_obj_create(top_bar_);
lv_obj_set_size(right_icons, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(right_icons, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(right_icons, 0, 0);
lv_obj_set_style_pad_all(right_icons, 0, 0);
lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
mute_label_ = lv_label_create(right_icons);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
battery_label_ = lv_label_create(right_icons);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
lv_obj_set_style_margin_left(battery_label_, lvgl_theme->spacing(2), 0);
/* Layer 2: Status bar - for center text labels */
status_bar_ = lv_obj_create(screen);
lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT); lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
lv_obj_set_style_radius(status_bar_, 0, 0); lv_obj_set_style_radius(status_bar_, 0, 0);
lv_obj_set_style_bg_color(status_bar_, lvgl_theme->background_color(), 0); lv_obj_set_style_bg_opa(status_bar_, LV_OPA_TRANSP, 0); // Transparent background
lv_obj_set_style_text_color(status_bar_, lvgl_theme->text_color(), 0); lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_pad_top(status_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning
lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_width(notification_label_, LV_HOR_RES * 0.8);
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
lv_label_set_text(notification_label_, "");
lv_obj_align(notification_label_, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
status_label_ = lv_label_create(status_bar_);
lv_obj_set_width(status_label_, LV_HOR_RES * 0.8);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0);
/* Content - Chat area */ /* Content - Chat area */
content_ = lv_obj_create(container_); content_ = lv_obj_create(container_);
@ -402,49 +472,6 @@ void LcdDisplay::SetupUI() {
// We'll create chat messages dynamically in SetChatMessage // We'll create chat messages dynamically in SetChatMessage
chat_message_label_ = nullptr; chat_message_label_ = nullptr;
/* Status bar */
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_column(status_bar_, 0, 0);
lv_obj_set_style_pad_top(status_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_left(status_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_pad_right(status_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF);
// 设置状态栏的内容垂直居中
lv_obj_set_flex_align(status_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
network_label_ = lv_label_create(status_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, icon_font, 0);
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(notification_label_, 1);
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
lv_label_set_text(notification_label_, "");
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
status_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(status_label_, 1);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
mute_label_ = lv_label_create(status_bar_);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
battery_label_ = lv_label_create(status_bar_);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
lv_obj_set_style_margin_left(battery_label_, lvgl_theme->spacing(2), 0); // 添加左边距,与前面的元素分隔
low_battery_popup_ = lv_obj_create(screen); low_battery_popup_ = lv_obj_create(screen);
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF); lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2); lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2);
@ -478,10 +505,10 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
return; return;
} }
// 检查消息数量是否超过限制 // Check if message count exceeds limit
uint32_t child_count = lv_obj_get_child_cnt(content_); uint32_t child_count = lv_obj_get_child_cnt(content_);
if (child_count >= MAX_MESSAGES) { if (child_count >= MAX_MESSAGES) {
// 删除最早的消息(第一个子对象) // Delete the oldest message (first child object)
lv_obj_t* first_child = lv_obj_get_child(content_, 0); lv_obj_t* first_child = lv_obj_get_child(content_, 0);
lv_obj_t* last_child = lv_obj_get_child(content_, child_count - 1); lv_obj_t* last_child = lv_obj_get_child(content_, child_count - 1);
if (first_child != nullptr) { if (first_child != nullptr) {
@ -493,30 +520,30 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
} }
} }
// 折叠系统消息(如果是系统消息,检查最后一个消息是否也是系统消息) // Collapse system messages (if it's a system message, check if the last message is also a system message)
if (strcmp(role, "system") == 0) { if (strcmp(role, "system") == 0) {
if (child_count > 0) { if (child_count > 0) {
// 获取最后一个消息容器 // Get the last message container
lv_obj_t* last_container = lv_obj_get_child(content_, child_count - 1); lv_obj_t* last_container = lv_obj_get_child(content_, child_count - 1);
if (last_container != nullptr && lv_obj_get_child_cnt(last_container) > 0) { if (last_container != nullptr && lv_obj_get_child_cnt(last_container) > 0) {
// 获取容器内的气泡 // Get the bubble inside the container
lv_obj_t* last_bubble = lv_obj_get_child(last_container, 0); lv_obj_t* last_bubble = lv_obj_get_child(last_container, 0);
if (last_bubble != nullptr) { if (last_bubble != nullptr) {
// 检查气泡类型是否为系统消息 // Check if bubble type is system message
void* bubble_type_ptr = lv_obj_get_user_data(last_bubble); void* bubble_type_ptr = lv_obj_get_user_data(last_bubble);
if (bubble_type_ptr != nullptr && strcmp((const char*)bubble_type_ptr, "system") == 0) { if (bubble_type_ptr != nullptr && strcmp((const char*)bubble_type_ptr, "system") == 0) {
// 如果最后一个消息也是系统消息,则删除它 // If the last message is also a system message, delete it
lv_obj_del(last_container); lv_obj_del(last_container);
} }
} }
} }
} }
} else { } else {
// 隐藏居中显示的 AI logo // Hide the centered AI logo
lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
} }
//避免出现空的消息框 // Avoid empty message boxes
if(strlen(content) == 0) { if(strlen(content) == 0) {
return; return;
} }
@ -535,31 +562,31 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
lv_obj_t* msg_text = lv_label_create(msg_bubble); lv_obj_t* msg_text = lv_label_create(msg_bubble);
lv_label_set_text(msg_text, content); lv_label_set_text(msg_text, content);
// 计算文本实际宽度 // Calculate actual text width
lv_coord_t text_width = lv_txt_get_width(content, strlen(content), text_font, 0); lv_coord_t text_width = lv_txt_get_width(content, strlen(content), text_font, 0);
// 计算气泡宽度 // Calculate bubble width
lv_coord_t max_width = LV_HOR_RES * 85 / 100 - 16; // 屏幕宽度的85% lv_coord_t max_width = LV_HOR_RES * 85 / 100 - 16; // 85% of screen width
lv_coord_t min_width = 20; lv_coord_t min_width = 20;
lv_coord_t bubble_width; lv_coord_t bubble_width;
// 确保文本宽度不小于最小宽度 // Ensure text width is not less than minimum width
if (text_width < min_width) { if (text_width < min_width) {
text_width = min_width; text_width = min_width;
} }
// 如果文本宽度小于最大宽度,使用文本宽度 // If text width is less than max width, use text width
if (text_width < max_width) { if (text_width < max_width) {
bubble_width = text_width; bubble_width = text_width;
} else { } else {
bubble_width = max_width; bubble_width = max_width;
} }
// 设置消息文本的宽度 // Set message text width
lv_obj_set_width(msg_text, bubble_width); // 减去padding lv_obj_set_width(msg_text, bubble_width); // Subtract padding
lv_label_set_long_mode(msg_text, LV_LABEL_LONG_WRAP); lv_label_set_long_mode(msg_text, LV_LABEL_LONG_WRAP);
// 设置气泡宽度 // Set bubble width
lv_obj_set_width(msg_bubble, bubble_width); lv_obj_set_width(msg_bubble, bubble_width);
lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT); lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT);
@ -571,7 +598,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
// Set text color for contrast // Set text color for contrast
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
// 设置自定义属性标记气泡类型 // Set custom attribute to mark bubble type
lv_obj_set_user_data(msg_bubble, (void*)"user"); lv_obj_set_user_data(msg_bubble, (void*)"user");
// Set appropriate width for content // Set appropriate width for content
@ -587,7 +614,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
// Set text color for contrast // Set text color for contrast
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
// 设置自定义属性标记气泡类型 // Set custom attribute to mark bubble type
lv_obj_set_user_data(msg_bubble, (void*)"assistant"); lv_obj_set_user_data(msg_bubble, (void*)"assistant");
// Set appropriate width for content // Set appropriate width for content
@ -603,7 +630,7 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
// Set text color for contrast // Set text color for contrast
lv_obj_set_style_text_color(msg_text, lvgl_theme->system_text_color(), 0); lv_obj_set_style_text_color(msg_text, lvgl_theme->system_text_color(), 0);
// 设置自定义属性标记气泡类型 // Set custom attribute to mark bubble type
lv_obj_set_user_data(msg_bubble, (void*)"system"); lv_obj_set_user_data(msg_bubble, (void*)"system");
// Set appropriate width for content // Set appropriate width for content
@ -635,23 +662,17 @@ void LcdDisplay::SetChatMessage(const char* role, const char* content) {
// Auto-scroll to this container // Auto-scroll to this container
lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON); lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON);
} else if (strcmp(role, "system") == 0) { } else if (strcmp(role, "system") == 0) {
// 为系统消息创建全宽容器以确保居中对齐 // Create full-width container for system messages to ensure center alignment
lv_obj_t* container = lv_obj_create(content_); lv_obj_t* container = lv_obj_create(content_);
lv_obj_set_width(container, LV_HOR_RES); lv_obj_set_width(container, LV_HOR_RES);
lv_obj_set_height(container, LV_SIZE_CONTENT); lv_obj_set_height(container, LV_SIZE_CONTENT);
// 使容器透明且无边框
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(container, 0, 0); lv_obj_set_style_border_width(container, 0, 0);
lv_obj_set_style_pad_all(container, 0, 0); lv_obj_set_style_pad_all(container, 0, 0);
// 将消息气泡移入此容器
lv_obj_set_parent(msg_bubble, container); lv_obj_set_parent(msg_bubble, container);
// 将气泡居中对齐在容器中
lv_obj_align(msg_bubble, LV_ALIGN_CENTER, 0, 0); lv_obj_align(msg_bubble, LV_ALIGN_CENTER, 0, 0);
// 自动滚动底部
lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON); lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON);
} else { } else {
// For assistant messages // For assistant messages
@ -688,7 +709,7 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0); lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0);
lv_obj_set_style_bg_opa(img_bubble, LV_OPA_70, 0); lv_obj_set_style_bg_opa(img_bubble, LV_OPA_70, 0);
// 设置自定义属性标记气泡类型 // Set custom attribute to mark bubble type
lv_obj_set_user_data(img_bubble, (void*)"image"); lv_obj_set_user_data(img_bubble, (void*)"image");
// Create the image object inside the bubble // Create the image object inside the bubble
@ -721,11 +742,11 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
// Add event handler to clean up LvglImage when image is deleted // Add event handler to clean up LvglImage when image is deleted
// We need to transfer ownership of the unique_ptr to the event callback // We need to transfer ownership of the unique_ptr to the event callback
LvglImage* raw_image = image.release(); // 释放智能指针的所有权 LvglImage* raw_image = image.release(); // Release ownership of smart pointer
lv_obj_add_event_cb(preview_image, [](lv_event_t* e) { lv_obj_add_event_cb(preview_image, [](lv_event_t* e) {
LvglImage* img = (LvglImage*)lv_event_get_user_data(e); LvglImage* img = (LvglImage*)lv_event_get_user_data(e);
if (img != nullptr) { if (img != nullptr) {
delete img; // 通过删除 LvglImage 对象来正确释放内存 delete img; // Properly release memory by deleting LvglImage object
} }
}, LV_EVENT_DELETE, (void*)raw_image); }, LV_EVENT_DELETE, (void*)raw_image);
@ -762,49 +783,22 @@ void LcdDisplay::SetupUI() {
lv_obj_set_style_text_color(screen, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(screen, lvgl_theme->text_color(), 0);
lv_obj_set_style_bg_color(screen, lvgl_theme->background_color(), 0); lv_obj_set_style_bg_color(screen, lvgl_theme->background_color(), 0);
/* Container */ /* Container - used as background */
container_ = lv_obj_create(screen); container_ = lv_obj_create(screen);
lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES); lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES);
lv_obj_set_style_radius(container_, 0, 0); lv_obj_set_style_radius(container_, 0, 0);
lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(container_, 0, 0); lv_obj_set_style_pad_all(container_, 0, 0);
lv_obj_set_style_border_width(container_, 0, 0); lv_obj_set_style_border_width(container_, 0, 0);
lv_obj_set_style_pad_row(container_, 0, 0);
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0); lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0); lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0);
/* Status bar */ /* Bottom layer: emoji_box_ - centered display */
status_bar_ = lv_obj_create(container_); emoji_box_ = lv_obj_create(screen);
lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
lv_obj_set_style_radius(status_bar_, 0, 0);
lv_obj_set_style_bg_color(status_bar_, lvgl_theme->background_color(), 0);
lv_obj_set_style_text_color(status_bar_, lvgl_theme->text_color(), 0);
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_top(status_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_left(status_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_pad_right(status_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_column(status_bar_, 0, 0);
/* Content */
content_ = lv_obj_create(container_);
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_radius(content_, 0, 0);
lv_obj_set_width(content_, LV_HOR_RES);
lv_obj_set_flex_grow(content_, 1);
lv_obj_set_style_pad_all(content_, 0, 0);
lv_obj_set_style_border_width(content_, 0, 0);
lv_obj_set_style_bg_color(content_, lvgl_theme->chat_background_color(), 0);
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); // 垂直布局(从上到下)
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_EVENLY); // 子对象居中对齐,等距分布
emoji_box_ = lv_obj_create(content_);
lv_obj_set_size(emoji_box_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_set_size(emoji_box_, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(emoji_box_, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_opa(emoji_box_, LV_OPA_TRANSP, 0);
lv_obj_set_style_pad_all(emoji_box_, 0, 0); lv_obj_set_style_pad_all(emoji_box_, 0, 0);
lv_obj_set_style_border_width(emoji_box_, 0, 0); lv_obj_set_style_border_width(emoji_box_, 0, 0);
lv_obj_align(emoji_box_, LV_ALIGN_CENTER, 0, 0);
emoji_label_ = lv_label_create(emoji_box_); emoji_label_ = lv_label_create(emoji_box_);
lv_obj_set_style_text_font(emoji_label_, large_icon_font, 0); lv_obj_set_style_text_font(emoji_label_, large_icon_font, 0);
@ -815,47 +809,107 @@ void LcdDisplay::SetupUI() {
lv_obj_center(emoji_image_); lv_obj_center(emoji_image_);
lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN);
preview_image_ = lv_image_create(content_); /* Middle layer: preview_image_ - centered display */
preview_image_ = lv_image_create(screen);
lv_obj_set_size(preview_image_, width_ / 2, height_ / 2); lv_obj_set_size(preview_image_, width_ / 2, height_ / 2);
lv_obj_align(preview_image_, LV_ALIGN_CENTER, 0, 0); lv_obj_align(preview_image_, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(preview_image_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
chat_message_label_ = lv_label_create(content_); /* Layer 1: Top bar - for status icons */
lv_label_set_text(chat_message_label_, ""); top_bar_ = lv_obj_create(screen);
lv_obj_set_width(chat_message_label_, width_ * 0.9); // 限制宽度为屏幕宽度的 90% lv_obj_set_size(top_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // 设置为自动换行模式 lv_obj_set_style_radius(top_bar_, 0, 0);
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // 设置文本居中对齐 lv_obj_set_style_bg_opa(top_bar_, LV_OPA_50, 0); // 50% opacity background
lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_bg_color(top_bar_, lvgl_theme->background_color(), 0);
lv_obj_set_style_border_width(top_bar_, 0, 0);
lv_obj_set_style_pad_all(top_bar_, 0, 0);
lv_obj_set_style_pad_top(top_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_bottom(top_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_left(top_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_pad_right(top_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF);
lv_obj_align(top_bar_, LV_ALIGN_TOP_MID, 0, 0);
/* Status bar */ // Left icon
network_label_ = lv_label_create(status_bar_); network_label_ = lv_label_create(top_bar_);
lv_label_set_text(network_label_, ""); lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, icon_font, 0); lv_obj_set_style_text_font(network_label_, icon_font, 0);
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
notification_label_ = lv_label_create(status_bar_); // Right icons container
lv_obj_set_flex_grow(notification_label_, 1); lv_obj_t* right_icons = lv_obj_create(top_bar_);
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_size(right_icons, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_bg_opa(right_icons, LV_OPA_TRANSP, 0);
lv_label_set_text(notification_label_, ""); lv_obj_set_style_border_width(right_icons, 0, 0);
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); lv_obj_set_style_pad_all(right_icons, 0, 0);
lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
status_label_ = lv_label_create(status_bar_); mute_label_ = lv_label_create(right_icons);
lv_obj_set_flex_grow(status_label_, 1);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
mute_label_ = lv_label_create(status_bar_);
lv_label_set_text(mute_label_, ""); lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, icon_font, 0); lv_obj_set_style_text_font(mute_label_, icon_font, 0);
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
battery_label_ = lv_label_create(status_bar_); battery_label_ = lv_label_create(right_icons);
lv_label_set_text(battery_label_, ""); lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, icon_font, 0); lv_obj_set_style_text_font(battery_label_, icon_font, 0);
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
lv_obj_set_style_margin_left(battery_label_, lvgl_theme->spacing(2), 0);
/* Layer 2: Status bar - for center text labels */
status_bar_ = lv_obj_create(screen);
lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
lv_obj_set_style_radius(status_bar_, 0, 0);
lv_obj_set_style_bg_opa(status_bar_, LV_OPA_TRANSP, 0); // Transparent background
lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_pad_top(status_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning
lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_width(notification_label_, LV_HOR_RES * 0.75);
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
lv_label_set_text(notification_label_, "");
lv_obj_align(notification_label_, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
status_label_ = lv_label_create(status_bar_);
lv_obj_set_width(status_label_, LV_HOR_RES * 0.75);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0);
/* Top layer: Bottom bar - fixed at bottom, minimum height 48, height can be adaptive */
bottom_bar_ = lv_obj_create(screen);
lv_obj_set_width(bottom_bar_, LV_HOR_RES);
lv_obj_set_height(bottom_bar_, LV_SIZE_CONTENT);
lv_obj_set_style_min_height(bottom_bar_, 48, 0); // Set minimum height 48
lv_obj_set_style_radius(bottom_bar_, 0, 0);
lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0);
lv_obj_set_style_text_color(bottom_bar_, lvgl_theme->text_color(), 0);
lv_obj_set_style_pad_top(bottom_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_bottom(bottom_bar_, lvgl_theme->spacing(2), 0);
lv_obj_set_style_pad_left(bottom_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_pad_right(bottom_bar_, lvgl_theme->spacing(4), 0);
lv_obj_set_style_border_width(bottom_bar_, 0, 0);
lv_obj_align(bottom_bar_, LV_ALIGN_BOTTOM_MID, 0, 0);
/* chat_message_label_ placed in bottom_bar_ and vertically centered */
chat_message_label_ = lv_label_create(bottom_bar_);
lv_label_set_text(chat_message_label_, "");
lv_obj_set_width(chat_message_label_, LV_HOR_RES - lvgl_theme->spacing(8)); // Subtract left and right padding
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // Auto wrap mode
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // Center text alignment
lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0);
lv_obj_align(chat_message_label_, LV_ALIGN_CENTER, 0, 0); // Vertically and horizontally centered in bottom_bar_
low_battery_popup_ = lv_obj_create(screen); low_battery_popup_ = lv_obj_create(screen);
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF); lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
@ -891,7 +945,6 @@ void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
preview_image_cached_ = std::move(image); preview_image_cached_ = std::move(image);
auto img_dsc = preview_image_cached_->image_dsc(); auto img_dsc = preview_image_cached_->image_dsc();
// 设置图片源并显示预览图片
lv_image_set_src(preview_image_, img_dsc); lv_image_set_src(preview_image_, img_dsc);
if (img_dsc->header.w > 0 && img_dsc->header.h > 0) { if (img_dsc->header.w > 0 && img_dsc->header.h > 0) {
// zoom factor 0.5 // zoom factor 0.5
@ -971,7 +1024,7 @@ void LcdDisplay::SetEmotion(const char* emotion) {
} }
#if CONFIG_USE_WECHAT_MESSAGE_STYLE #if CONFIG_USE_WECHAT_MESSAGE_STYLE
// Wechat message style中如果emotion是neutral则不显示 // In WeChat message style, if emotion is neutral, don't display it
uint32_t child_count = lv_obj_get_child_cnt(content_); uint32_t child_count = lv_obj_get_child_cnt(content_);
if (strcmp(emotion, "neutral") == 0 && child_count > 0) { if (strcmp(emotion, "neutral") == 0 && child_count > 0) {
// Stop GIF animation if running // Stop GIF animation if running
@ -1021,9 +1074,11 @@ void LcdDisplay::SetTheme(Theme* theme) {
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0); lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
} }
// Update status bar background color with 50% opacity // Update top bar background color with 50% opacity
lv_obj_set_style_bg_opa(status_bar_, LV_OPA_50, 0); if (top_bar_ != nullptr) {
lv_obj_set_style_bg_color(status_bar_, lvgl_theme->background_color(), 0); lv_obj_set_style_bg_opa(top_bar_, LV_OPA_50, 0);
lv_obj_set_style_bg_color(top_bar_, lvgl_theme->background_color(), 0);
}
// Update status bar elements // Update status bar elements
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
@ -1033,11 +1088,11 @@ void LcdDisplay::SetTheme(Theme* theme) {
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0);
// If we have the chat message style, update all message bubbles
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
// Set content background opacity // Set content background opacity
lv_obj_set_style_bg_opa(content_, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_opa(content_, LV_OPA_TRANSP, 0);
// If we have the chat message style, update all message bubbles
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
// Iterate through all children of content (message containers or bubbles) // Iterate through all children of content (message containers or bubbles)
uint32_t child_count = lv_obj_get_child_cnt(content_); uint32_t child_count = lv_obj_get_child_cnt(content_);
for (uint32_t i = 0; i < child_count; i++) { for (uint32_t i = 0; i < child_count; i++) {
@ -1046,33 +1101,33 @@ void LcdDisplay::SetTheme(Theme* theme) {
lv_obj_t* bubble = nullptr; lv_obj_t* bubble = nullptr;
// 检查这个对象是容器还是气泡 // Check if this object is a container or bubble
// 如果是容器(用户或系统消息),则获取其子对象作为气泡 // If it's a container (user or system message), get its child as bubble
// 如果是气泡(助手消息),则直接使用 // If it's a bubble (assistant message), use it directly
if (lv_obj_get_child_cnt(obj) > 0) { if (lv_obj_get_child_cnt(obj) > 0) {
// 可能是容器,检查它是否为用户或系统消息容器 // Might be a container, check if it's a user or system message container
// 用户和系统消息容器是透明的 // User and system message containers are transparent
lv_opa_t bg_opa = lv_obj_get_style_bg_opa(obj, 0); lv_opa_t bg_opa = lv_obj_get_style_bg_opa(obj, 0);
if (bg_opa == LV_OPA_TRANSP) { if (bg_opa == LV_OPA_TRANSP) {
// 这是用户或系统消息的容器 // This is a user or system message container
bubble = lv_obj_get_child(obj, 0); bubble = lv_obj_get_child(obj, 0);
} else { } else {
// 这可能是助手消息的气泡自身 // This might be an assistant message bubble itself
bubble = obj; bubble = obj;
} }
} else { } else {
// 没有子元素可能是其他UI元素跳过 // No child elements, might be other UI elements, skip
continue; continue;
} }
if (bubble == nullptr) continue; if (bubble == nullptr) continue;
// 使用保存的用户数据来识别气泡类型 // Use saved user data to identify bubble type
void* bubble_type_ptr = lv_obj_get_user_data(bubble); void* bubble_type_ptr = lv_obj_get_user_data(bubble);
if (bubble_type_ptr != nullptr) { if (bubble_type_ptr != nullptr) {
const char* bubble_type = static_cast<const char*>(bubble_type_ptr); const char* bubble_type = static_cast<const char*>(bubble_type_ptr);
// 根据气泡类型应用正确的颜色 // Apply correct color based on bubble type
if (strcmp(bubble_type, "user") == 0) { if (strcmp(bubble_type, "user") == 0) {
lv_obj_set_style_bg_color(bubble, lvgl_theme->user_bubble_color(), 0); lv_obj_set_style_bg_color(bubble, lvgl_theme->user_bubble_color(), 0);
} else if (strcmp(bubble_type, "assistant") == 0) { } else if (strcmp(bubble_type, "assistant") == 0) {
@ -1090,7 +1145,7 @@ void LcdDisplay::SetTheme(Theme* theme) {
if (lv_obj_get_child_cnt(bubble) > 0) { if (lv_obj_get_child_cnt(bubble) > 0) {
lv_obj_t* text = lv_obj_get_child(bubble, 0); lv_obj_t* text = lv_obj_get_child(bubble, 0);
if (text != nullptr) { if (text != nullptr) {
// 根据气泡类型设置文本颜色 // Set text color based on bubble type
if (strcmp(bubble_type, "system") == 0) { if (strcmp(bubble_type, "system") == 0) {
lv_obj_set_style_text_color(text, lvgl_theme->system_text_color(), 0); lv_obj_set_style_text_color(text, lvgl_theme->system_text_color(), 0);
} else { } else {
@ -1111,6 +1166,12 @@ void LcdDisplay::SetTheme(Theme* theme) {
if (emoji_label_ != nullptr) { if (emoji_label_ != nullptr) {
lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0); lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0);
} }
// Update bottom bar background color with 50% opacity
if (bottom_bar_ != nullptr) {
lv_obj_set_style_bg_opa(bottom_bar_, LV_OPA_50, 0);
lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0);
}
#endif #endif
// Update low battery popup // Update low battery popup
@ -1119,3 +1180,17 @@ void LcdDisplay::SetTheme(Theme* theme) {
// No errors occurred. Save theme to settings // No errors occurred. Save theme to settings
Display::SetTheme(lvgl_theme); Display::SetTheme(lvgl_theme);
} }
void LcdDisplay::SetHideSubtitle(bool hide) {
DisplayLockGuard lock(this);
hide_subtitle_ = hide;
// Immediately update UI visibility based on the setting
if (bottom_bar_ != nullptr) {
if (hide) {
lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_remove_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN);
}
}
}

View File

@ -20,10 +20,12 @@ protected:
esp_lcd_panel_handle_t panel_ = nullptr; esp_lcd_panel_handle_t panel_ = nullptr;
lv_draw_buf_t draw_buf_; lv_draw_buf_t draw_buf_;
lv_obj_t* top_bar_ = nullptr;
lv_obj_t* status_bar_ = nullptr; lv_obj_t* status_bar_ = nullptr;
lv_obj_t* content_ = nullptr; lv_obj_t* content_ = nullptr;
lv_obj_t* container_ = nullptr; lv_obj_t* container_ = nullptr;
lv_obj_t* side_bar_ = nullptr; lv_obj_t* side_bar_ = nullptr;
lv_obj_t* bottom_bar_ = nullptr;
lv_obj_t* preview_image_ = nullptr; lv_obj_t* preview_image_ = nullptr;
lv_obj_t* emoji_label_ = nullptr; lv_obj_t* emoji_label_ = nullptr;
lv_obj_t* emoji_image_ = nullptr; lv_obj_t* emoji_image_ = nullptr;
@ -32,6 +34,7 @@ protected:
lv_obj_t* chat_message_label_ = nullptr; lv_obj_t* chat_message_label_ = nullptr;
esp_timer_handle_t preview_timer_ = nullptr; esp_timer_handle_t preview_timer_ = nullptr;
std::unique_ptr<LvglImage> preview_image_cached_ = nullptr; std::unique_ptr<LvglImage> preview_image_cached_ = nullptr;
bool hide_subtitle_ = false; // Control whether to hide chat messages/subtitles
void InitializeLcdThemes(); void InitializeLcdThemes();
void SetupUI(); void SetupUI();
@ -39,7 +42,7 @@ protected:
virtual void Unlock() override; virtual void Unlock() override;
protected: protected:
// 添加protected构造函数 // Add protected constructor
LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height); LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height);
public: public:
@ -50,9 +53,12 @@ public:
// Add theme switching function // Add theme switching function
virtual void SetTheme(Theme* theme) override; virtual void SetTheme(Theme* theme) override;
// Set whether to hide chat messages/subtitles
void SetHideSubtitle(bool hide);
}; };
// SPI LCD显示器 // SPI LCD display
class SpiLcdDisplay : public LcdDisplay { class SpiLcdDisplay : public LcdDisplay {
public: public:
SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
@ -60,7 +66,7 @@ public:
bool mirror_x, bool mirror_y, bool swap_xy); bool mirror_x, bool mirror_y, bool swap_xy);
}; };
// RGB LCD显示器 // RGB LCD display
class RgbLcdDisplay : public LcdDisplay { class RgbLcdDisplay : public LcdDisplay {
public: public:
RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
@ -68,7 +74,7 @@ public:
bool mirror_x, bool mirror_y, bool swap_xy); bool mirror_x, bool mirror_y, bool swap_xy);
}; };
// MIPI LCD显示器 // MIPI LCD display
class MipiLcdDisplay : public LcdDisplay { class MipiLcdDisplay : public LcdDisplay {
public: public:
MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,

View File

@ -110,7 +110,7 @@ void LvglDisplay::UpdateStatusBar(bool update_all) {
return; return;
} }
// 如果静音状态改变,则更新图标 // Update icon if mute state changes
if (codec->output_volume() == 0 && !muted_) { if (codec->output_volume() == 0 && !muted_) {
muted_ = true; muted_ = true;
lv_label_set_text(mute_label_, FONT_AWESOME_VOLUME_XMARK); lv_label_set_text(mute_label_, FONT_AWESOME_VOLUME_XMARK);
@ -129,7 +129,7 @@ void LvglDisplay::UpdateStatusBar(bool update_all) {
// Check if the we have already set the time // Check if the we have already set the time
if (tm->tm_year >= 2025 - 1900) { if (tm->tm_year >= 2025 - 1900) {
char time_str[16]; char time_str[16];
strftime(time_str, sizeof(time_str), "%H:%M ", tm); strftime(time_str, sizeof(time_str), "%H:%M", tm);
SetStatus(time_str); SetStatus(time_str);
} else { } else {
ESP_LOGW(TAG, "System time is not set, tm_year: %d", tm->tm_year); ESP_LOGW(TAG, "System time is not set, tm_year: %d", tm->tm_year);
@ -138,7 +138,7 @@ void LvglDisplay::UpdateStatusBar(bool update_all) {
} }
esp_pm_lock_acquire(pm_lock_); esp_pm_lock_acquire(pm_lock_);
// 更新电池图标 // Update battery icon
int battery_level; int battery_level;
bool charging, discharging; bool charging, discharging;
const char* icon = nullptr; const char* icon = nullptr;
@ -164,23 +164,23 @@ void LvglDisplay::UpdateStatusBar(bool update_all) {
if (low_battery_popup_ != nullptr) { if (low_battery_popup_ != nullptr) {
if (strcmp(icon, FONT_AWESOME_BATTERY_EMPTY) == 0 && discharging) { if (strcmp(icon, FONT_AWESOME_BATTERY_EMPTY) == 0 && discharging) {
if (lv_obj_has_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN)) { // 如果低电量提示框隐藏,则显示 if (lv_obj_has_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN)) { // Show if low battery popup is hidden
lv_obj_remove_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
app.PlaySound(Lang::Sounds::OGG_LOW_BATTERY); app.PlaySound(Lang::Sounds::OGG_LOW_BATTERY);
} }
} else { } else {
// Hide the low battery popup when the battery is not empty // Hide the low battery popup when the battery is not empty
if (!lv_obj_has_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN)) { // 如果低电量提示框显示,则隐藏 if (!lv_obj_has_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN)) { // Hide if low battery popup is shown
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
} }
} }
} }
} }
// 每 10 秒更新一次网络图标 // Update network icon every 10 seconds
static int seconds_counter = 0; static int seconds_counter = 0;
if (update_all || seconds_counter++ % 10 == 0) { if (update_all || seconds_counter++ % 10 == 0) {
// 升级固件时,不读取 4G 网络状态,避免占用 UART 资源 // Don't read 4G network status during firmware upgrade to avoid occupying UART resources
auto device_state = Application::GetInstance().GetDeviceState(); auto device_state = Application::GetInstance().GetDeviceState();
static const std::vector<DeviceState> allowed_states = { static const std::vector<DeviceState> allowed_states = {
kDeviceStateIdle, kDeviceStateIdle,
@ -233,10 +233,10 @@ bool LvglDisplay::SnapshotToJpeg(std::string& jpeg_data, int quality) {
data[i] = __builtin_bswap16(data[i]); data[i] = __builtin_bswap16(data[i]);
} }
// 清空输出字符串并使用回调版本,避免预分配大内存块 // Clear output string and use callback version to avoid pre-allocating large memory blocks
jpeg_data.clear(); jpeg_data.clear();
// 🚀 使用回调版本的JPEG编码器进一步节省内存 // Use callback-based JPEG encoder to further save memory
bool ret = image_to_jpeg_cb((uint8_t*)draw_buffer->data, draw_buffer->data_size, draw_buffer->header.w, draw_buffer->header.h, V4L2_PIX_FMT_RGB565, quality, bool ret = image_to_jpeg_cb((uint8_t*)draw_buffer->data, draw_buffer->data_size, draw_buffer->header.w, draw_buffer->header.h, V4L2_PIX_FMT_RGB565, quality,
[](void *arg, size_t index, const void *data, size_t len) -> size_t { [](void *arg, size_t index, const void *data, size_t len) -> size_t {
std::string* output = static_cast<std::string*>(arg); std::string* output = static_cast<std::string*>(arg);

View File

@ -87,10 +87,27 @@ OledDisplay::~OledDisplay() {
if (content_ != nullptr) { if (content_ != nullptr) {
lv_obj_del(content_); lv_obj_del(content_);
} }
if (status_bar_ != nullptr) {
bool is_128x64_layout = (top_bar_ != nullptr);
if (status_bar_ != nullptr && is_128x64_layout) {
status_label_ = nullptr;
notification_label_ = nullptr;
lv_obj_del(status_bar_); lv_obj_del(status_bar_);
} }
if (top_bar_ != nullptr) {
network_label_ = nullptr;
mute_label_ = nullptr;
battery_label_ = nullptr;
lv_obj_del(top_bar_);
}
if (side_bar_ != nullptr) { if (side_bar_ != nullptr) {
if (!is_128x64_layout) {
status_label_ = nullptr;
notification_label_ = nullptr;
network_label_ = nullptr;
mute_label_ = nullptr;
battery_label_ = nullptr;
}
lv_obj_del(side_bar_); lv_obj_del(side_bar_);
} }
if (container_ != nullptr) { if (container_ != nullptr) {
@ -156,12 +173,61 @@ void OledDisplay::SetupUI_128x64() {
lv_obj_set_style_border_width(container_, 0, 0); lv_obj_set_style_border_width(container_, 0, 0);
lv_obj_set_style_pad_row(container_, 0, 0); lv_obj_set_style_pad_row(container_, 0, 0);
/* Status bar */ /* Layer 1: Top bar - for status icons */
status_bar_ = lv_obj_create(container_); top_bar_ = lv_obj_create(container_);
lv_obj_set_size(top_bar_, LV_HOR_RES, 16);
lv_obj_set_style_radius(top_bar_, 0, 0);
lv_obj_set_style_bg_opa(top_bar_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(top_bar_, 0, 0);
lv_obj_set_style_pad_all(top_bar_, 0, 0);
lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF);
network_label_ = lv_label_create(top_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, icon_font, 0);
lv_obj_t* right_icons = lv_obj_create(top_bar_);
lv_obj_set_size(right_icons, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(right_icons, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(right_icons, 0, 0);
lv_obj_set_style_pad_all(right_icons, 0, 0);
lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
mute_label_ = lv_label_create(right_icons);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
battery_label_ = lv_label_create(right_icons);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
/* Layer 2: Status bar - for center text labels */
status_bar_ = lv_obj_create(screen);
lv_obj_set_size(status_bar_, LV_HOR_RES, 16); lv_obj_set_size(status_bar_, LV_HOR_RES, 16);
lv_obj_set_style_radius(status_bar_, 0, 0);
lv_obj_set_style_bg_opa(status_bar_, LV_OPA_TRANSP, 0); // Transparent background
lv_obj_set_style_border_width(status_bar_, 0, 0); lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_all(status_bar_, 0, 0); lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_radius(status_bar_, 0, 0); lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning
lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_width(notification_label_, LV_HOR_RES);
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_text(notification_label_, "");
lv_obj_align(notification_label_, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
status_label_ = lv_label_create(status_bar_);
lv_obj_set_width(status_label_, LV_HOR_RES);
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0);
/* Content */ /* Content */
content_ = lv_obj_create(container_); content_ = lv_obj_create(container_);
@ -173,9 +239,8 @@ void OledDisplay::SetupUI_128x64() {
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_flex_main_place(content_, LV_FLEX_ALIGN_CENTER, 0); lv_obj_set_style_flex_main_place(content_, LV_FLEX_ALIGN_CENTER, 0);
// 创建左侧固定宽度的容器
content_left_ = lv_obj_create(content_); content_left_ = lv_obj_create(content_);
lv_obj_set_size(content_left_, 32, LV_SIZE_CONTENT); // 固定宽度32像素 lv_obj_set_size(content_left_, 32, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(content_left_, 0, 0); lv_obj_set_style_pad_all(content_left_, 0, 0);
lv_obj_set_style_border_width(content_left_, 0, 0); lv_obj_set_style_border_width(content_left_, 0, 0);
@ -185,7 +250,6 @@ void OledDisplay::SetupUI_128x64() {
lv_obj_center(emotion_label_); lv_obj_center(emotion_label_);
lv_obj_set_style_pad_top(emotion_label_, 8, 0); lv_obj_set_style_pad_top(emotion_label_, 8, 0);
// 创建右侧可扩展的容器
content_right_ = lv_obj_create(content_); content_right_ = lv_obj_create(content_);
lv_obj_set_size(content_right_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_set_size(content_right_, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(content_right_, 0, 0); lv_obj_set_style_pad_all(content_right_, 0, 0);
@ -200,7 +264,7 @@ void OledDisplay::SetupUI_128x64() {
lv_obj_set_width(chat_message_label_, width_ - 32); lv_obj_set_width(chat_message_label_, width_ - 32);
lv_obj_set_style_pad_top(chat_message_label_, 14, 0); lv_obj_set_style_pad_top(chat_message_label_, 14, 0);
// 延迟一定的时间后开始滚动字幕 // Start scrolling subtitle after a delay
static lv_anim_t a; static lv_anim_t a;
lv_anim_init(&a); lv_anim_init(&a);
lv_anim_set_delay(&a, 1000); lv_anim_set_delay(&a, 1000);
@ -208,35 +272,6 @@ void OledDisplay::SetupUI_128x64() {
lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN); lv_obj_set_style_anim(chat_message_label_, &a, LV_PART_MAIN);
lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN); lv_obj_set_style_anim_duration(chat_message_label_, lv_anim_speed_clamped(60, 300, 60000), LV_PART_MAIN);
/* Status bar */
lv_obj_set_flex_flow(status_bar_, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(status_bar_, 0, 0);
lv_obj_set_style_border_width(status_bar_, 0, 0);
lv_obj_set_style_pad_column(status_bar_, 0, 0);
network_label_ = lv_label_create(status_bar_);
lv_label_set_text(network_label_, "");
lv_obj_set_style_text_font(network_label_, icon_font, 0);
notification_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(notification_label_, 1);
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_text(notification_label_, "");
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
status_label_ = lv_label_create(status_bar_);
lv_obj_set_flex_grow(status_label_, 1);
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
mute_label_ = lv_label_create(status_bar_);
lv_label_set_text(mute_label_, "");
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
battery_label_ = lv_label_create(status_bar_);
lv_label_set_text(battery_label_, "");
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
low_battery_popup_ = lv_obj_create(screen); low_battery_popup_ = lv_obj_create(screen);
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF); lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2); lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2);
@ -328,7 +363,7 @@ void OledDisplay::SetupUI_128x32() {
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_label_set_text(chat_message_label_, ""); lv_label_set_text(chat_message_label_, "");
// 延迟一定的时间后开始滚动字幕 // Start scrolling subtitle after a delay
static lv_anim_t a; static lv_anim_t a;
lv_anim_init(&a); lv_anim_init(&a);
lv_anim_set_delay(&a, 1000); lv_anim_set_delay(&a, 1000);

View File

@ -12,6 +12,7 @@ private:
esp_lcd_panel_io_handle_t panel_io_ = nullptr; esp_lcd_panel_io_handle_t panel_io_ = nullptr;
esp_lcd_panel_handle_t panel_ = nullptr; esp_lcd_panel_handle_t panel_ = nullptr;
lv_obj_t* top_bar_ = nullptr;
lv_obj_t* status_bar_ = nullptr; lv_obj_t* status_bar_ = nullptr;
lv_obj_t* content_ = nullptr; lv_obj_t* content_ = nullptr;
lv_obj_t* content_left_ = nullptr; lv_obj_t* content_left_ = nullptr;

View File

@ -18,10 +18,10 @@ dependencies:
espressif/esp_io_expander_tca9554: ==2.0.0 espressif/esp_io_expander_tca9554: ==2.0.0
espressif/esp_lcd_panel_io_additions: ^1.0.1 espressif/esp_lcd_panel_io_additions: ^1.0.1
78/esp_lcd_nv3023: ~1.0.0 78/esp_lcd_nv3023: ~1.0.0
78/esp-wifi-connect: ~2.6.1 78/esp-wifi-connect: ~2.6.2
78/esp-opus-encoder: ~2.4.1 78/esp-opus-encoder: ~2.4.1
78/esp-ml307: ~3.3.7 78/esp-ml307: ~3.5.1
78/xiaozhi-fonts: ~1.5.4 78/xiaozhi-fonts: ~1.5.5
espressif/led_strip: ~3.0.1 espressif/led_strip: ~3.0.1
espressif/esp_codec_dev: ~1.5 espressif/esp_codec_dev: ~1.5
espressif/esp-sr: ~2.2.0 espressif/esp-sr: ~2.2.0

View File

@ -71,7 +71,7 @@ std::unique_ptr<Http> Ota::SetupHttp() {
/* /*
* Specification: https://ccnphfhqs21z.feishu.cn/wiki/FjW6wZmisimNBBkov6OcmfvknVd * Specification: https://ccnphfhqs21z.feishu.cn/wiki/FjW6wZmisimNBBkov6OcmfvknVd
*/ */
bool Ota::CheckVersion() { esp_err_t Ota::CheckVersion() {
auto& board = Board::GetInstance(); auto& board = Board::GetInstance();
auto app_desc = esp_app_get_description(); auto app_desc = esp_app_get_description();
@ -82,7 +82,7 @@ bool Ota::CheckVersion() {
std::string url = GetCheckVersionUrl(); std::string url = GetCheckVersionUrl();
if (url.length() < 10) { if (url.length() < 10) {
ESP_LOGE(TAG, "Check version URL is not properly set"); ESP_LOGE(TAG, "Check version URL is not properly set");
return false; return ESP_ERR_INVALID_ARG;
} }
auto http = SetupHttp(); auto http = SetupHttp();
@ -92,14 +92,15 @@ bool Ota::CheckVersion() {
http->SetContent(std::move(data)); http->SetContent(std::move(data));
if (!http->Open(method, url)) { if (!http->Open(method, url)) {
ESP_LOGE(TAG, "Failed to open HTTP connection"); int last_error = http->GetLastError();
return false; ESP_LOGE(TAG, "Failed to open HTTP connection, code=0x%x", last_error);
return last_error;
} }
auto status_code = http->GetStatusCode(); auto status_code = http->GetStatusCode();
if (status_code != 200) { if (status_code != 200) {
ESP_LOGE(TAG, "Failed to check version, status code: %d", status_code); ESP_LOGE(TAG, "Failed to check version, status code: %d", status_code);
return false; return status_code;
} }
data = http->ReadAll(); data = http->ReadAll();
@ -112,7 +113,7 @@ bool Ota::CheckVersion() {
cJSON *root = cJSON_Parse(data.c_str()); cJSON *root = cJSON_Parse(data.c_str());
if (root == NULL) { if (root == NULL) {
ESP_LOGE(TAG, "Failed to parse JSON response"); ESP_LOGE(TAG, "Failed to parse JSON response");
return false; return ESP_ERR_INVALID_RESPONSE;
} }
has_activation_code_ = false; has_activation_code_ = false;
@ -237,7 +238,7 @@ bool Ota::CheckVersion() {
} }
cJSON_Delete(root); cJSON_Delete(root);
return true; return ESP_OK;
} }
void Ota::MarkCurrentVersionValid() { void Ota::MarkCurrentVersionValid() {

View File

@ -12,7 +12,7 @@ public:
Ota(); Ota();
~Ota(); ~Ota();
bool CheckVersion(); esp_err_t CheckVersion();
esp_err_t Activate(); esp_err_t Activate();
bool HasActivationChallenge() { return has_activation_challenge_; } bool HasActivationChallenge() { return has_activation_challenge_; }
bool HasNewVersion() { return has_new_version_; } bool HasNewVersion() { return has_new_version_; }

View File

@ -131,7 +131,7 @@ bool MqttProtocol::StartMqttClient(bool report_error) {
broker_address = endpoint; broker_address = endpoint;
} }
if (!mqtt_->Connect(broker_address, broker_port, client_id, username, password)) { if (!mqtt_->Connect(broker_address, broker_port, client_id, username, password)) {
ESP_LOGE(TAG, "Failed to connect to endpoint"); ESP_LOGE(TAG, "Failed to connect to endpoint, code=%d", mqtt_->GetLastError());
SetError(Lang::Strings::SERVER_NOT_CONNECTED); SetError(Lang::Strings::SERVER_NOT_CONNECTED);
return false; return false;
} }

View File

@ -173,7 +173,7 @@ bool WebsocketProtocol::OpenAudioChannel() {
ESP_LOGI(TAG, "Connecting to websocket server: %s with version: %d", url.c_str(), version_); ESP_LOGI(TAG, "Connecting to websocket server: %s with version: %d", url.c_str(), version_);
if (!websocket_->Connect(url.c_str())) { if (!websocket_->Connect(url.c_str())) {
ESP_LOGE(TAG, "Failed to connect to websocket server"); ESP_LOGE(TAG, "Failed to connect to websocket server, code=%d", websocket_->GetLastError());
SetError(Lang::Strings::SERVER_NOT_CONNECTED); SetError(Lang::Strings::SERVER_NOT_CONNECTED);
return false; return false;
} }