mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 10:03:54 +08:00
Merge pull request #2738 from xinnan-tech/xiaozhi-server_ota_bin
Xiaozhi server ota bin
This commit is contained in:
@@ -183,8 +183,8 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
|||||||
#### 🚀 部署方式选择
|
#### 🚀 部署方式选择
|
||||||
| 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 |
|
| 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 |
|
||||||
|---------|------|---------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **最简化安装** | 智能对话、IOT、MCP、视觉感知 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - |
|
| **最简化安装** | 智能对话、单智能体管理 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - |
|
||||||
| **全模块安装** | 智能对话、IOT、MCP接入点、声纹识别、视觉感知、OTA、智控台 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
| **全模块安装** | 智能对话、多用户管理、多智能体管理、智控台界面操作 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||||
|
|
||||||
常见问题及相关教程,可参考[这个链接](./docs/FAQ.md)
|
常见问题及相关教程,可参考[这个链接](./docs/FAQ.md)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -181,8 +181,8 @@ Dieses Projekt bietet zwei Bereitstellungsmethoden. Bitte wählen Sie basierend
|
|||||||
#### 🚀 Auswahl der Bereitstellungsmethode
|
#### 🚀 Auswahl der Bereitstellungsmethode
|
||||||
| Bereitstellungsmethode | Funktionen | Anwendungsszenarien | Deployment-Dokumente | Konfigurationsanforderungen | Video-Tutorials |
|
| Bereitstellungsmethode | Funktionen | Anwendungsszenarien | Deployment-Dokumente | Konfigurationsanforderungen | Video-Tutorials |
|
||||||
|---------|------|---------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **Vereinfachte Installation** | Intelligenter Dialog, IOT, MCP, visuelle Wahrnehmung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - |
|
| **Vereinfachte Installation** | Intelligenter Dialog, Einzel-Agenten-Verwaltung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - |
|
||||||
| **Vollständige Modulinstallation** | Intelligenter Dialog, IOT, MCP-Endpunkte, Stimmabdruckerkennung, visuelle Wahrnehmung, OTA, intelligente Steuerkonsole | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①Docker-Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Quellcode-Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
| **Vollständige Modulinstallation** | Intelligenter Dialog, Mehrbenutzerverwaltung, Mehr-Agenten-Verwaltung, Intelligente Steuerkonsole-Bedienung | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①Docker-Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Quellcode-Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||||
|
|
||||||
Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](./docs/FAQ.md)
|
Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](./docs/FAQ.md)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -181,8 +181,8 @@ This project provides two deployment methods. Please choose based on your specif
|
|||||||
#### 🚀 Deployment Method Selection
|
#### 🚀 Deployment Method Selection
|
||||||
| Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials |
|
| Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials |
|
||||||
|---------|------|---------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **Simplified Installation** | Intelligent dialogue, IOT, MCP, visual perception | Low-configuration environments, data stored in config files, no database required | [①Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - |
|
| **Simplified Installation** | Intelligent dialogue, single agent management | Low-configuration environments, data stored in config files, no database required | [①Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - |
|
||||||
| **Full Module Installation** | Intelligent dialogue, IOT, MCP endpoints, voiceprint recognition, visual perception, OTA, intelligent control console | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
| **Full Module Installation** | Intelligent dialogue, multi-user management, multi-agent management, intelligent console interface operation | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||||
|
|
||||||
|
|
||||||
> 💡 Note: Below is a test platform deployed with the latest code. You can burn and test if needed. Concurrent users: 6, data will be cleared daily.
|
> 💡 Note: Below is a test platform deployed with the latest code. You can burn and test if needed. Concurrent users: 6, data will be cleared daily.
|
||||||
|
|||||||
+2
-2
@@ -182,8 +182,8 @@ Dự án này cung cấp hai phương pháp triển khai, vui lòng chọn theo
|
|||||||
#### 🚀 Lựa chọn phương pháp triển khai
|
#### 🚀 Lựa chọn phương pháp triển khai
|
||||||
| Phương pháp triển khai | Đặc điểm | Tình huống áp dụng | Tài liệu triển khai | Yêu cầu cấu hình | Video hướng dẫn |
|
| Phương pháp triển khai | Đặc điểm | Tình huống áp dụng | Tài liệu triển khai | Yêu cầu cấu hình | Video hướng dẫn |
|
||||||
|---------|------|---------|---------|---------|---------|
|
|---------|------|---------|---------|---------|---------|
|
||||||
| **Cài đặt tối giản** | Đối thoại thông minh, IOT, MCP, cảm nhận thị giác | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - |
|
| **Cài đặt tối giản** | Đối thoại thông minh, quản lý đơn tác nhân | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - |
|
||||||
| **Cài đặt toàn bộ module** | Đối thoại thông minh, IOT, điểm truy cập MCP, nhận dạng giọng nói, cảm nhận thị giác, OTA, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Triển khai mã nguồn](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
| **Cài đặt toàn bộ module** | Đối thoại thông minh, quản lý đa người dùng, quản lý đa tác nhân, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Triển khai mã nguồn](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||||
|
|
||||||
Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](./docs/FAQ.md)
|
Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](./docs/FAQ.md)
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ VAD:
|
|||||||
### 9、编译固件相关教程
|
### 9、编译固件相关教程
|
||||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||||
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
||||||
|
3、[单模块部署如何配置固件OTA自动升级](./ota-upgrade-guide.md)<br/>
|
||||||
|
|
||||||
### 10、拓展相关教程
|
### 10、拓展相关教程
|
||||||
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# 单模块部署固件OTA自动升级配置指南
|
||||||
|
|
||||||
|
本教程将指导你如何在**单模块部署**场景下配置固件OTA自动升级功能,实现设备固件的自动更新。
|
||||||
|
|
||||||
|
如果你已经使用**全模块部署**,请忽略本教程。
|
||||||
|
|
||||||
|
## 功能介绍
|
||||||
|
|
||||||
|
在单模块部署中,xiaozhi-server内置了OTA固件管理功能,可以自动检测设备版本并下发升级固件。系统会根据设备型号和当前版本,自动匹配并推送最新的固件版本。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
- 你已经成功进行**单模块部署**并运行xiaozhi-server
|
||||||
|
- 设备能够正常连接到服务器
|
||||||
|
|
||||||
|
## 第一步 准备固件文件
|
||||||
|
|
||||||
|
### 1. 创建固件存放目录
|
||||||
|
|
||||||
|
固件文件需要放在`data/bin/`目录下。如果该目录不存在,请手动创建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 固件文件命名规则
|
||||||
|
|
||||||
|
固件文件必须遵循以下命名格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
{设备型号}_{版本号}.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
**命名规则说明:**
|
||||||
|
- `设备型号`:设备的型号名称,例如 `lichuang-dev`、`bread-compact-wifi` 等
|
||||||
|
- `版本号`:固件版本号,必须以数字开头,支持数字、字母、点号、下划线和短横线,例如 `1.6.6`、`2.0.0` 等
|
||||||
|
- 文件扩展名必须是 `.bin`
|
||||||
|
|
||||||
|
**命名示例:**
|
||||||
|
```
|
||||||
|
bread-compact-wifi_1.6.6.bin
|
||||||
|
lichuang-dev_2.0.0.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 放置固件文件
|
||||||
|
|
||||||
|
将准备好的固件文件(.bin文件)复制到`data/bin/`目录下:
|
||||||
|
|
||||||
|
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||||
|
|
||||||
|
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||||
|
|
||||||
|
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp xiaozhi.bin data/bin/设备型号_版本号.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:
|
||||||
|
```bash
|
||||||
|
cp xiaozhi.bin data/bin/bread-compact-wifi_1.6.6.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第二步 配置公网访问地址(仅公网部署需要)
|
||||||
|
|
||||||
|
**注意:此步骤仅适用于单模块公网部署的场景。**
|
||||||
|
|
||||||
|
如果你的xiaozhi-server是公网部署(使用公网IP或域名),**必须**配置`server.vision_explain`参数,因为OTA固件下载地址会使用该配置的域名和端口。
|
||||||
|
|
||||||
|
如果你是局域网部署,可以跳过此步骤。
|
||||||
|
|
||||||
|
### 为什么要配置这个参数?
|
||||||
|
|
||||||
|
在单模块部署中,系统生成固件下载地址时,会使用`vision_explain`配置的域名和端口作为基础地址。如果不配置或配置错误,设备将无法访问固件下载地址。
|
||||||
|
|
||||||
|
### 配置方法
|
||||||
|
|
||||||
|
打开`data/.config.yaml`文件,找到`server`配置段,设置`vision_explain`参数:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
vision_explain: http://你的域名或IP:端口号/mcp/vision/explain
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置示例:**
|
||||||
|
|
||||||
|
局域网部署(默认):
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
vision_explain: http://192.168.1.100:8003/mcp/vision/explain
|
||||||
|
```
|
||||||
|
|
||||||
|
公网域名部署:
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
vision_explain: http://yourdomain.com:8003/mcp/vision/explain
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 域名或IP必须是设备能够访问的地址
|
||||||
|
- 如果使用Docker部署,不能使用Docker内部地址(如127.0.0.1或localhost)
|
||||||
|
- 如果你使用了nginx反向代理,请填写对外的地址和端口号,不是本项目运行的端口号
|
||||||
|
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 1. 设备收不到固件更新
|
||||||
|
|
||||||
|
**可能原因和解决方法:**
|
||||||
|
|
||||||
|
- 检查固件文件命名是否符合规则:`{型号}_{版本号}.bin`
|
||||||
|
- 检查固件文件是否正确放置在`data/bin/`目录
|
||||||
|
- 检查设备型号是否与固件文件名中的型号匹配
|
||||||
|
- 检查固件版本号是否高于设备当前版本
|
||||||
|
- 查看服务器日志,确认OTA请求是否正常处理
|
||||||
|
|
||||||
|
### 2. 设备报告下载地址无法访问
|
||||||
|
|
||||||
|
**可能原因和解决方法:**
|
||||||
|
|
||||||
|
- 检查`server.vision_explain`配置的域名或IP是否正确
|
||||||
|
- 确认端口号配置正确(默认8003)
|
||||||
|
- 如果是公网部署,确保设备能够访问该公网地址
|
||||||
|
- 如果是Docker部署,确保不是使用了内部地址(127.0.0.1)
|
||||||
|
- 检查防火墙是否开放了对应端口
|
||||||
|
- 如果你使用了nginx反向代理,请填写对外的地址和端口号,不是本项目运行的端口号
|
||||||
|
|
||||||
|
### 3. 如何确认设备当前版本
|
||||||
|
|
||||||
|
查看OTA请求日志,日志中会显示设备上报的版本号:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ota_handler] - 设备 AA:BB:CC:DD:EE:FF 固件已是最新: 1.6.6
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 固件文件放置后没有生效
|
||||||
|
|
||||||
|
系统有30秒的缓存时间(默认),可以:
|
||||||
|
- 等待30秒后再让设备发起OTA请求
|
||||||
|
- 重启xiaozhi-server服务
|
||||||
|
- 调整`firmware_cache_ttl`配置为更短的时间
|
||||||
@@ -10,7 +10,15 @@ class BaseHandler:
|
|||||||
def _add_cors_headers(self, response):
|
def _add_cors_headers(self, response):
|
||||||
"""添加CORS头信息"""
|
"""添加CORS头信息"""
|
||||||
response.headers["Access-Control-Allow-Headers"] = (
|
response.headers["Access-Control-Allow-Headers"] = (
|
||||||
"client-id, content-type, device-id"
|
"client-id, content-type, device-id, authorization"
|
||||||
)
|
)
|
||||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
|
||||||
|
async def handle_options(self, request):
|
||||||
|
"""处理OPTIONS请求,添加CORS头信息"""
|
||||||
|
response = web.Response(body=b"", content_type="text/plain")
|
||||||
|
self._add_cors_headers(response)
|
||||||
|
# 添加允许的方法
|
||||||
|
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
||||||
|
return response
|
||||||
|
|||||||
@@ -3,15 +3,46 @@ import time
|
|||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import glob
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from core.auth import AuthManager
|
from core.auth import AuthManager
|
||||||
from core.utils.util import get_local_ip
|
from core.utils.util import get_local_ip, get_vision_url
|
||||||
from core.api.base_handler import BaseHandler
|
from core.api.base_handler import BaseHandler
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_basename(filename: str) -> str:
|
||||||
|
# Prevent directory traversal
|
||||||
|
return os.path.basename(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_version(ver: str) -> Tuple[int, ...]:
|
||||||
|
# conservative parser: split by non-digit, keep numeric parts
|
||||||
|
parts = re.findall(r"\d+", ver)
|
||||||
|
return tuple(int(p) for p in parts) if parts else (0,)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_higher_version(a: str, b: str) -> bool:
|
||||||
|
"""Return True if version string a > b (semver-like numeric compare)."""
|
||||||
|
ta = _parse_version(a)
|
||||||
|
tb = _parse_version(b)
|
||||||
|
# compare tuple lexicographically, but allow different lengths
|
||||||
|
maxlen = max(len(ta), len(tb))
|
||||||
|
for i in range(maxlen):
|
||||||
|
ai = ta[i] if i < len(ta) else 0
|
||||||
|
bi = tb[i] if i < len(tb) else 0
|
||||||
|
if ai > bi:
|
||||||
|
return True
|
||||||
|
if ai < bi:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class OTAHandler(BaseHandler):
|
class OTAHandler(BaseHandler):
|
||||||
def __init__(self, config: dict):
|
def __init__(self, config: dict):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
@@ -23,6 +54,54 @@ class OTAHandler(BaseHandler):
|
|||||||
expire_seconds = auth_config.get("expire_seconds")
|
expire_seconds = auth_config.get("expire_seconds")
|
||||||
self.auth = AuthManager(secret_key=secret_key, expire_seconds=expire_seconds)
|
self.auth = AuthManager(secret_key=secret_key, expire_seconds=expire_seconds)
|
||||||
|
|
||||||
|
# firmware storage
|
||||||
|
self.bin_dir = os.path.join(os.getcwd(), "data", "bin")
|
||||||
|
# cache structure: { 'updated_at': timestamp, 'ttl': seconds, 'files_by_model': { model: [(version, filename), ...] } }
|
||||||
|
self._bin_cache: Dict = {
|
||||||
|
"updated_at": 0,
|
||||||
|
"ttl": config.get("firmware_cache_ttl", 30),
|
||||||
|
"files_by_model": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _refresh_bin_cache_if_needed(self):
|
||||||
|
now = int(time.time())
|
||||||
|
ttl = int(self._bin_cache.get("ttl", 30))
|
||||||
|
if now - int(
|
||||||
|
self._bin_cache.get("updated_at", 0)
|
||||||
|
) < ttl and self._bin_cache.get("files_by_model"):
|
||||||
|
return
|
||||||
|
|
||||||
|
files_by_model: Dict[str, List[Tuple[str, str]]] = {}
|
||||||
|
try:
|
||||||
|
if not os.path.isdir(self.bin_dir):
|
||||||
|
os.makedirs(self.bin_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# match files like model_1.2.3.bin (allow dots, dashes, underscores in model and version)
|
||||||
|
pattern = os.path.join(self.bin_dir, "*.bin")
|
||||||
|
for path in glob.glob(pattern):
|
||||||
|
fname = os.path.basename(path)
|
||||||
|
# filename format: {model}_{version}.bin
|
||||||
|
m = re.match(r"^(.+?)_([0-9][A-Za-z0-9\.\-_]*)\.bin$", fname)
|
||||||
|
if not m:
|
||||||
|
# skip files not conforming to naming rule
|
||||||
|
continue
|
||||||
|
model = m.group(1)
|
||||||
|
version = m.group(2)
|
||||||
|
files_by_model.setdefault(model, []).append((version, fname))
|
||||||
|
|
||||||
|
# sort versions for each model descending
|
||||||
|
for model, items in files_by_model.items():
|
||||||
|
items.sort(key=lambda it: _parse_version(it[0]), reverse=True)
|
||||||
|
|
||||||
|
self._bin_cache["files_by_model"] = files_by_model
|
||||||
|
self._bin_cache["updated_at"] = now
|
||||||
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"Firmware cache refreshed: {len(files_by_model)} models"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"刷新固件缓存失败: {e}")
|
||||||
|
# keep previous cache if any
|
||||||
|
|
||||||
def generate_password_signature(self, content: str, secret_key: str) -> str:
|
def generate_password_signature(self, content: str, secret_key: str) -> str:
|
||||||
"""生成MQTT密码签名
|
"""生成MQTT密码签名
|
||||||
|
|
||||||
@@ -62,7 +141,14 @@ class OTAHandler(BaseHandler):
|
|||||||
return f"ws://{local_ip}:{port}/xiaozhi/v1/"
|
return f"ws://{local_ip}:{port}/xiaozhi/v1/"
|
||||||
|
|
||||||
async def handle_post(self, request):
|
async def handle_post(self, request):
|
||||||
"""处理 OTA POST 请求"""
|
"""处理 OTA POST 请求
|
||||||
|
|
||||||
|
This handler will:
|
||||||
|
- read device id/client id (as before)
|
||||||
|
- attempt to determine device model and current firmware version (prefer headers, fallback to body)
|
||||||
|
- check data/bin for newer firmware for that model
|
||||||
|
- if found a newer firmware, set firmware.url to the download endpoint
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
data = await request.text()
|
data = await request.text()
|
||||||
self.logger.bind(tag=TAG).debug(f"OTA请求方法: {request.method}")
|
self.logger.bind(tag=TAG).debug(f"OTA请求方法: {request.method}")
|
||||||
@@ -81,33 +167,76 @@ class OTAHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
raise Exception("OTA请求ClientID为空")
|
raise Exception("OTA请求ClientID为空")
|
||||||
|
|
||||||
data_json = json.loads(data)
|
data_json = {}
|
||||||
|
try:
|
||||||
|
data_json = json.loads(data) if data else {}
|
||||||
|
except Exception:
|
||||||
|
data_json = {}
|
||||||
|
|
||||||
server_config = self.config["server"]
|
server_config = self.config["server"]
|
||||||
port = int(server_config.get("port", 8000))
|
# Distinguish ports:
|
||||||
|
# - websocket_port is used to construct websocket URL (server["port"])
|
||||||
|
# - http_port is used to construct OTA download URLs (server["http_port"])
|
||||||
|
websocket_port = int(server_config.get("port", 8000))
|
||||||
|
http_port = int(server_config.get("http_port", 8003))
|
||||||
local_ip = get_local_ip()
|
local_ip = get_local_ip()
|
||||||
|
|
||||||
|
# Determine device model (prefer headers)
|
||||||
|
device_model = ""
|
||||||
|
# header candidates
|
||||||
|
for h in ("device-model", "device_model", "model"):
|
||||||
|
if h in request.headers:
|
||||||
|
device_model = request.headers.get(h, "").strip()
|
||||||
|
break
|
||||||
|
# body fallback
|
||||||
|
if not device_model:
|
||||||
|
try:
|
||||||
|
if "board" in data_json and isinstance(data_json["board"], dict):
|
||||||
|
device_model = data_json["board"].get("type", "")
|
||||||
|
elif "model" in data_json:
|
||||||
|
device_model = data_json.get("model", "")
|
||||||
|
except Exception:
|
||||||
|
device_model = ""
|
||||||
|
if not device_model:
|
||||||
|
device_model = "default"
|
||||||
|
|
||||||
|
# Determine device current version (prefer headers)
|
||||||
|
device_version = ""
|
||||||
|
for h in (
|
||||||
|
"device-version",
|
||||||
|
"device_version",
|
||||||
|
"firmware-version",
|
||||||
|
"app-version",
|
||||||
|
"application-version",
|
||||||
|
):
|
||||||
|
if h in request.headers:
|
||||||
|
device_version = request.headers.get(h, "").strip()
|
||||||
|
break
|
||||||
|
if not device_version:
|
||||||
|
try:
|
||||||
|
device_version = data_json.get("application", {}).get("version", "")
|
||||||
|
except Exception:
|
||||||
|
device_version = ""
|
||||||
|
if not device_version:
|
||||||
|
device_version = "0.0.0"
|
||||||
|
|
||||||
return_json = {
|
return_json = {
|
||||||
"server_time": {
|
"server_time": {
|
||||||
"timestamp": int(round(time.time() * 1000)),
|
"timestamp": int(round(time.time() * 1000)),
|
||||||
"timezone_offset": server_config.get("timezone_offset", 8) * 60,
|
"timezone_offset": server_config.get("timezone_offset", 8) * 60,
|
||||||
},
|
},
|
||||||
"firmware": {
|
"firmware": {
|
||||||
"version": data_json["application"].get("version", "1.0.0"),
|
"version": device_version,
|
||||||
"url": "",
|
"url": "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# existing mqtt/websocket logic (unchanged)
|
||||||
mqtt_gateway_endpoint = server_config.get("mqtt_gateway")
|
mqtt_gateway_endpoint = server_config.get("mqtt_gateway")
|
||||||
|
|
||||||
if mqtt_gateway_endpoint: # 如果配置了非空字符串
|
if mqtt_gateway_endpoint: # 如果配置了非空字符串
|
||||||
# 尝试从请求数据中获取设备型号
|
# 尝试从请求数据中获取设备型号(已解析 above)
|
||||||
device_model = "default"
|
|
||||||
try:
|
try:
|
||||||
if "device" in data_json and isinstance(data_json["device"], dict):
|
|
||||||
device_model = data_json["device"].get("model", "default")
|
|
||||||
elif "model" in data_json:
|
|
||||||
device_model = data_json["model"]
|
|
||||||
group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_")
|
group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}")
|
self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}")
|
||||||
@@ -159,20 +288,61 @@ class OTAHandler(BaseHandler):
|
|||||||
token = self.auth.generate_token(client_id, device_id)
|
token = self.auth.generate_token(client_id, device_id)
|
||||||
else:
|
else:
|
||||||
token = self.auth.generate_token(client_id, device_id)
|
token = self.auth.generate_token(client_id, device_id)
|
||||||
|
# NOTE: use websocket_port here
|
||||||
return_json["websocket"] = {
|
return_json["websocket"] = {
|
||||||
"url": self._get_websocket_url(local_ip, port),
|
"url": self._get_websocket_url(local_ip, websocket_port),
|
||||||
"token": token,
|
"token": token,
|
||||||
}
|
}
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置"
|
f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置"
|
||||||
)
|
)
|
||||||
self.logger.bind(tag=TAG).info(f"{return_json}")
|
|
||||||
|
# Now check firmware files for updates
|
||||||
|
try:
|
||||||
|
self._refresh_bin_cache_if_needed()
|
||||||
|
files_by_model = self._bin_cache.get("files_by_model", {})
|
||||||
|
candidates = files_by_model.get(device_model, [])
|
||||||
|
|
||||||
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"查找型号 {device_model} 的固件,找到 {len(candidates)} 个候选"
|
||||||
|
)
|
||||||
|
|
||||||
|
chosen_url = ""
|
||||||
|
chosen_version = device_version
|
||||||
|
|
||||||
|
# candidates are sorted descending by version
|
||||||
|
for ver, fname in candidates:
|
||||||
|
if _is_higher_version(ver, device_version):
|
||||||
|
# build download url (only allow download via our download endpoint)
|
||||||
|
chosen_version = ver
|
||||||
|
# Use get_vision_url to get the base URL and replace the path
|
||||||
|
vision_url = get_vision_url(self.config)
|
||||||
|
# Replace the path from "/mcp/vision/explain" to "/xiaozhi/ota/download/{fname}"
|
||||||
|
chosen_url = vision_url.replace(
|
||||||
|
"/mcp/vision/explain", f"/xiaozhi/ota/download/{fname}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
if chosen_url:
|
||||||
|
return_json["firmware"]["version"] = chosen_version
|
||||||
|
return_json["firmware"]["url"] = chosen_url
|
||||||
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"为设备 {device_id} 下发固件 {chosen_version} [如果地址前缀有误,请检查配置文件中的server.vision_explain]-> {chosen_url} "
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"设备 {device_id} 固件已是最新: {device_version}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"检查固件版本时出错: {e}")
|
||||||
|
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
text=json.dumps(return_json, separators=(",", ":")),
|
text=json.dumps(return_json, separators=(",", ":")),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"OTA POST处理异常: {e}")
|
||||||
return_json = {"success": False, "message": "request error."}
|
return_json = {"success": False, "message": "request error."}
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
text=json.dumps(return_json, separators=(",", ":")),
|
text=json.dumps(return_json, separators=(",", ":")),
|
||||||
@@ -187,8 +357,9 @@ class OTAHandler(BaseHandler):
|
|||||||
try:
|
try:
|
||||||
server_config = self.config["server"]
|
server_config = self.config["server"]
|
||||||
local_ip = get_local_ip()
|
local_ip = get_local_ip()
|
||||||
port = int(server_config.get("port", 8000))
|
# use websocket port for websocket URL
|
||||||
websocket_url = self._get_websocket_url(local_ip, port)
|
websocket_port = int(server_config.get("port", 8000))
|
||||||
|
websocket_url = self._get_websocket_url(local_ip, websocket_port)
|
||||||
message = f"OTA接口运行正常,向设备发送的websocket地址是:{websocket_url}"
|
message = f"OTA接口运行正常,向设备发送的websocket地址是:{websocket_url}"
|
||||||
response = web.Response(text=message, content_type="text/plain")
|
response = web.Response(text=message, content_type="text/plain")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -197,3 +368,48 @@ class OTAHandler(BaseHandler):
|
|||||||
finally:
|
finally:
|
||||||
self._add_cors_headers(response)
|
self._add_cors_headers(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
async def handle_download(self, request):
|
||||||
|
"""
|
||||||
|
下载固件接口
|
||||||
|
URL: /xiaozhi/ota/download/{filename}
|
||||||
|
- 只允许下载 data/bin 目录下的 .bin 文件
|
||||||
|
- filename 必须是 basename 且匹配安全的模式
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
fname = request.match_info.get("filename", "")
|
||||||
|
if not fname:
|
||||||
|
raise web.HTTPBadRequest(text="filename required")
|
||||||
|
|
||||||
|
# sanitize
|
||||||
|
fname = _safe_basename(fname)
|
||||||
|
# pattern: allow letters, numbers, dot, underscore, dash
|
||||||
|
if not re.match(r"^[A-Za-z0-9\.\-_]+\.bin$", fname):
|
||||||
|
raise web.HTTPBadRequest(text="invalid filename")
|
||||||
|
|
||||||
|
file_path = os.path.join(self.bin_dir, fname)
|
||||||
|
# ensure realpath is under bin_dir
|
||||||
|
file_real = os.path.realpath(file_path)
|
||||||
|
bin_dir_real = os.path.realpath(self.bin_dir)
|
||||||
|
if (
|
||||||
|
not file_real.startswith(bin_dir_real + os.sep)
|
||||||
|
and file_real != bin_dir_real
|
||||||
|
):
|
||||||
|
raise web.HTTPForbidden(text="forbidden")
|
||||||
|
|
||||||
|
if not os.path.isfile(file_real):
|
||||||
|
raise web.HTTPNotFound(text="file not found")
|
||||||
|
|
||||||
|
# use FileResponse to stream file
|
||||||
|
resp = web.FileResponse(path=file_real)
|
||||||
|
except web.HTTPError as e:
|
||||||
|
resp = e
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"固件下载异常: {e}")
|
||||||
|
resp = web.Response(text="download error", status=500)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
self._add_cors_headers(resp)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return resp
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
import copy
|
import copy
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
|
from core.api.base_handler import BaseHandler
|
||||||
from core.utils.util import get_vision_url, is_valid_image_file
|
from core.utils.util import get_vision_url, is_valid_image_file
|
||||||
from core.utils.vllm import create_instance
|
from core.utils.vllm import create_instance
|
||||||
from config.config_loader import get_private_config_from_api
|
from config.config_loader import get_private_config_from_api
|
||||||
@@ -16,10 +17,9 @@ TAG = __name__
|
|||||||
MAX_FILE_SIZE = 5 * 1024 * 1024
|
MAX_FILE_SIZE = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
class VisionHandler:
|
class VisionHandler(BaseHandler):
|
||||||
def __init__(self, config: dict):
|
def __init__(self, config: dict):
|
||||||
self.config = config
|
super().__init__(config)
|
||||||
self.logger = setup_logging()
|
|
||||||
# 初始化认证工具
|
# 初始化认证工具
|
||||||
self.auth = AuthToken(config["server"]["auth_key"])
|
self.auth = AuthToken(config["server"]["auth_key"])
|
||||||
|
|
||||||
@@ -172,11 +172,3 @@ class VisionHandler:
|
|||||||
finally:
|
finally:
|
||||||
self._add_cors_headers(response)
|
self._add_cors_headers(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _add_cors_headers(self, response):
|
|
||||||
"""添加CORS头信息"""
|
|
||||||
response.headers["Access-Control-Allow-Headers"] = (
|
|
||||||
"client-id, content-type, device-id"
|
|
||||||
)
|
|
||||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
|
||||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
||||||
|
|||||||
@@ -33,38 +33,60 @@ class SimpleHttpServer:
|
|||||||
return f"ws://{local_ip}:{port}/xiaozhi/v1/"
|
return f"ws://{local_ip}:{port}/xiaozhi/v1/"
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
server_config = self.config["server"]
|
try:
|
||||||
read_config_from_api = self.config.get("read_config_from_api", False)
|
server_config = self.config["server"]
|
||||||
host = server_config.get("ip", "0.0.0.0")
|
read_config_from_api = self.config.get("read_config_from_api", False)
|
||||||
port = int(server_config.get("http_port", 8003))
|
host = server_config.get("ip", "0.0.0.0")
|
||||||
|
port = int(server_config.get("http_port", 8003))
|
||||||
|
|
||||||
if port:
|
if port:
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
|
|
||||||
if not read_config_from_api:
|
if not read_config_from_api:
|
||||||
# 如果没有开启智控台,只是单模块运行,就需要再添加简单OTA接口,用于下发websocket接口
|
# 如果没有开启智控台,只是单模块运行,就需要再添加简单OTA接口,用于下发websocket接口
|
||||||
|
app.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/xiaozhi/ota/", self.ota_handler.handle_get),
|
||||||
|
web.post("/xiaozhi/ota/", self.ota_handler.handle_post),
|
||||||
|
web.options(
|
||||||
|
"/xiaozhi/ota/", self.ota_handler.handle_options
|
||||||
|
),
|
||||||
|
# 下载接口,仅提供 data/bin/*.bin 下载
|
||||||
|
web.get(
|
||||||
|
"/xiaozhi/ota/download/{filename}",
|
||||||
|
self.ota_handler.handle_download,
|
||||||
|
),
|
||||||
|
web.options(
|
||||||
|
"/xiaozhi/ota/download/{filename}",
|
||||||
|
self.ota_handler.handle_options,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# 添加路由
|
||||||
app.add_routes(
|
app.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/xiaozhi/ota/", self.ota_handler.handle_get),
|
web.get("/mcp/vision/explain", self.vision_handler.handle_get),
|
||||||
web.post("/xiaozhi/ota/", self.ota_handler.handle_post),
|
web.post(
|
||||||
web.options("/xiaozhi/ota/", self.ota_handler.handle_post),
|
"/mcp/vision/explain", self.vision_handler.handle_post
|
||||||
|
),
|
||||||
|
web.options(
|
||||||
|
"/mcp/vision/explain", self.vision_handler.handle_options
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
# 添加路由
|
|
||||||
app.add_routes(
|
|
||||||
[
|
|
||||||
web.get("/mcp/vision/explain", self.vision_handler.handle_get),
|
|
||||||
web.post("/mcp/vision/explain", self.vision_handler.handle_post),
|
|
||||||
web.options("/mcp/vision/explain", self.vision_handler.handle_post),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# 运行服务
|
# 运行服务
|
||||||
runner = web.AppRunner(app)
|
runner = web.AppRunner(app)
|
||||||
await runner.setup()
|
await runner.setup()
|
||||||
site = web.TCPSite(runner, host, port)
|
site = web.TCPSite(runner, host, port)
|
||||||
await site.start()
|
await site.start()
|
||||||
|
|
||||||
# 保持服务运行
|
# 保持服务运行
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(3600) # 每隔 1 小时检查一次
|
await asyncio.sleep(3600) # 每隔 1 小时检查一次
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"HTTP服务器启动失败: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
self.logger.bind(tag=TAG).error(f"错误堆栈: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|||||||
Reference in New Issue
Block a user