新增:ESP32 烧录指南 + 实时日志监控工具
- ESP32_FLASH_GUIDE.md: 固件烧录/OTA/配置/测试完整指南 - 推荐固件:v2.2.4 / v1.9.2 - ESP Launchpad 在线烧录方案 - 首次配置流程(WiFi + OTA 地址) - OTA 升级步骤 - 调试和问题排查 - xiaozhi_log_monitor.py: 实时日志监控工具 - 彩色输出,关键事件分类高亮 - 过滤器:llm / mcp / device / error / tts / stt - 支持回放最近 N 条日志main
parent
c4d5caf931
commit
d6b2f52c30
|
|
@ -0,0 +1,233 @@
|
|||
# ESP32 玩偶固件烧录与测试指南
|
||||
|
||||
> **目标平台:** 小智 AI 玩偶(ESP32-S3/C3/P4)
|
||||
> **固件来源:** https://github.com/78/xiaozhi-esp32/releases
|
||||
> **固件版本:** v2.2.4(最新,2026-03-09)
|
||||
> **服务器:** xiaozhi-esp32-server 全模块(本地 MacBook Pro)
|
||||
> **生成时间:** 2026-04-07
|
||||
|
||||
---
|
||||
|
||||
## 一、固件获取
|
||||
|
||||
### 推荐版本
|
||||
|
||||
| 版本 | 固件名 | 状态 | 推荐理由 |
|
||||
|------|--------|------|---------|
|
||||
| **v2.2.4** | `xiaozhi-esp32s3-2.2.4.bin` | ✅ 官方最新 | 功能最全,但与 v1 不兼容 |
|
||||
| **v1.9.2** | `xiaozhi-esp32s3-1.9.2.bin` | ✅ 稳定维护 | 分区表与 v2 不同,需全量烧录 |
|
||||
|
||||
**下载链接:** https://github.com/78/xiaozhi-esp32/releases
|
||||
|
||||
> ⚠️ v1 和 v2 分区表不同,**不能从 v1 OTA 升级到 v2**,必须全量烧录。
|
||||
|
||||
---
|
||||
|
||||
## 二、烧录工具选择
|
||||
|
||||
### 方案 A(推荐):ESP Launchpad(在线,无需安装)
|
||||
|
||||
最适合零配置用户,无需 IDF 环境。
|
||||
|
||||
1. 打开:https://espressif.github.io/esp-launchpad/
|
||||
2. 选择对应固件
|
||||
3. 连接 ESP32(USB-C 数据线)
|
||||
4. 点击 "Flash" 烧录
|
||||
|
||||
### 方案 B:esptool.py(命令行)
|
||||
|
||||
```bash
|
||||
# 安装 esptool
|
||||
pip3 install esptool
|
||||
|
||||
# 全量烧录(地址固定 0x0)
|
||||
esptool.py --chip esp32s3 \
|
||||
--port /dev/tty.usbserial-* \
|
||||
write_flash 0x0 xiaozhi-esp32s3-2.2.4.bin
|
||||
|
||||
# 查找端口
|
||||
ls /dev/tty.usbserial-*
|
||||
```
|
||||
|
||||
### 方案 C:Thonny IDE(适合 Windows/Mac 入门用户)
|
||||
|
||||
https://thonny.org/
|
||||
|
||||
---
|
||||
|
||||
## 三、首次配置流程
|
||||
|
||||
固件烧录完成后,上电初始化配置:
|
||||
|
||||
### Step 1:连接配置 WiFi
|
||||
|
||||
1. 玩偶上电,等待启动
|
||||
2. 手机/电脑搜索 WiFi:**Xiaozhi-XXXXXX**(6位随机字符)
|
||||
3. 浏览器打开:**http://192.168.4.1**
|
||||
4. 填写:
|
||||
- **WiFi SSID**:你的 2.4GHz WiFi 名称
|
||||
- **WiFi 密码**:你的 WiFi 密码
|
||||
- **OTA 地址**:`http://<MacBook-IP>:8002/xiaozhi/ota/`
|
||||
- 查询本机 IP:`ipconfig getifaddr en0`(Mac)
|
||||
- 注意:必须是 **8002**(全模块),不是 8003
|
||||
5. 点击保存,设备自动重启
|
||||
|
||||
### Step 2:确认连接成功
|
||||
|
||||
```bash
|
||||
# 查看日志
|
||||
python3 xiaozhi_log_monitor.py --filter device -r 50
|
||||
|
||||
# 或直接查看服务器日志
|
||||
docker logs -f xiaozhi-esp32-server
|
||||
```
|
||||
|
||||
**成功标志:**
|
||||
```
|
||||
[设备] 连接成功
|
||||
Websocket地址是 ws://192.168.x.x:8000/xiaozhi/v1/
|
||||
MCP接入点连接成功
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、OTA 固件升级
|
||||
|
||||
### 通过服务器 OTA(无需拆机)
|
||||
|
||||
1. 下载新版本固件(如 `xiaozhi-esp32s3-2.2.4.bin`)
|
||||
2. 放入服务器目录:
|
||||
```bash
|
||||
cp ~/Downloads/xiaozhi-esp32s3-2.2.4.bin \
|
||||
/Users/bigemon/WorkSpace/xiaozhi-server/data/bin/
|
||||
```
|
||||
3. 重启玩偶或发送唤醒词,设备自动检测并下载新固件
|
||||
|
||||
### 本地测试固件
|
||||
|
||||
如果需要测试自己编译的固件:
|
||||
```bash
|
||||
# 用 esptool 烧录(需 USB 连接)
|
||||
esptool.py --chip esp32s3 --port /dev/tty.usbserial-* \
|
||||
write_flash 0x0 your-custom-firmware.bin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、测试验证
|
||||
|
||||
### 唤醒词
|
||||
**"你好,小智"**(默认)
|
||||
|
||||
### 测试流程
|
||||
|
||||
1. **连接验证**
|
||||
```bash
|
||||
python3 xiaozhi_log_monitor.py -r 50
|
||||
# 确认看到 "连接成功"
|
||||
```
|
||||
|
||||
2. **语音对话测试**
|
||||
```
|
||||
你:你好,小智
|
||||
玩偶:你好!我是小智,很高兴认识你(中文 TTS)
|
||||
```
|
||||
|
||||
3. **心理场景测试**(模拟)
|
||||
```
|
||||
你:今天小朋友打我,我好害怕
|
||||
玩偶:[触发心理筛查 MCP] → 回应欺凌场景
|
||||
```
|
||||
|
||||
4. **日志过滤命令**
|
||||
```bash
|
||||
# 只看 LLM/MCP 链路
|
||||
python3 xiaozhi_log_monitor.py -f llm
|
||||
|
||||
# 只看 MCP 工具调用
|
||||
python3 xiaozhi_log_monitor.py -f mcp
|
||||
|
||||
# 只看设备连接
|
||||
python3 xiaozhi_log_monitor.py -f device
|
||||
|
||||
# 只看错误
|
||||
python3 xiaozhi_log_monitor.py -f error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、调试和问题排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 症状 | 原因 | 解决方法 |
|
||||
|------|------|---------|
|
||||
| 玩偶 WiFi 连接失败 | 输入了 5GHz WiFi | 固件只支持 2.4GHz |
|
||||
| OTA 地址填错 | 填了 8003 | 应填 8002(全模块智控台端口)|
|
||||
| 设备反复重启 | WiFi 密码错误 | 重新进入 192.168.4.1 配置 |
|
||||
| 设备不说话 | TTS 未生成音频 | 检查 EdgeTTS 配置 |
|
||||
| MCP 工具不触发 | 服务器 MCP 未加载 | `docker logs` 确认 MCP 初始化 |
|
||||
|
||||
### 串口日志(需要 USB 连接)
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
screen /dev/tty.usbserial-* 115200
|
||||
|
||||
# 退出 screen
|
||||
Ctrl+A → K → Y
|
||||
```
|
||||
|
||||
### 服务器日志
|
||||
|
||||
```bash
|
||||
# 实时监控(彩色)
|
||||
python3 xiaozhi_log_monitor.py
|
||||
|
||||
# 回放最近 200 条
|
||||
python3 xiaozhi_log_monitor.py -r 200
|
||||
|
||||
# 只看错误
|
||||
python3 xiaozhi_log_monitor.py -f error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、服务器配置确认
|
||||
|
||||
确保全模块服务器的 OTA 地址配置正确:
|
||||
|
||||
```bash
|
||||
# 检查服务器 OTA 端点
|
||||
curl -I http://127.0.0.1:8002/xiaozhi/ota/ 2>&1 | head -3
|
||||
|
||||
# OTA 文件目录
|
||||
ls /Users/bigemon/WorkSpace/xiaozhi-server/data/bin/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、快速参考命令卡
|
||||
|
||||
```bash
|
||||
# 1. 启动日志监控
|
||||
python3 xiaozhi_log_monitor.py
|
||||
|
||||
# 2. 查看设备连接日志
|
||||
python3 xiaozhi_log_monitor.py -f device -r 50
|
||||
|
||||
# 3. 查看 MCP 工具调用
|
||||
python3 xiaozhi_log_monitor.py -f mcp
|
||||
|
||||
# 4. 查看 LLM 调用
|
||||
python3 xiaozhi_log_monitor.py -f llm
|
||||
|
||||
# 5. 重启服务器
|
||||
docker restart xiaozhi-esp32-server
|
||||
|
||||
# 6. 查看容器状态
|
||||
docker ps --format "{{.Names}}\t{{.Status}}"
|
||||
|
||||
# 7. 查询本机 IP(OTA 地址用)
|
||||
ipconfig getifaddr en0
|
||||
```
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
xiaozhi-esp32-server 实时日志监控器
|
||||
|
||||
用法:
|
||||
python xiaozhi_log_monitor.py # 全部日志
|
||||
python xiaozhi_log_monitor.py --filter llm # 只看 LLM
|
||||
python xiaozhi_log_monitor.py --filter mcp # 只看 MCP
|
||||
python xiaozhi_log_monitor.py --filter device # 只看设备连接
|
||||
python xiaozhi_log_monitor.py --filter error # 只看错误
|
||||
|
||||
按 Ctrl+C 退出
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# ANSI 颜色码
|
||||
COLORS = {
|
||||
"reset": "\033[0m",
|
||||
"bold": "\033[1m",
|
||||
"red": "\033[91m",
|
||||
"green": "\033[92m",
|
||||
"yellow": "\033[93m",
|
||||
"blue": "\033[94m",
|
||||
"magenta": "\033[95m",
|
||||
"cyan": "\033[96m",
|
||||
"gray": "\033[90m",
|
||||
"white": "\033[97m",
|
||||
}
|
||||
|
||||
# 日志类型配色
|
||||
LOG_COLORS = {
|
||||
"ERROR": COLORS["red"] + COLORS["bold"],
|
||||
"error": COLORS["red"],
|
||||
"WARN": COLORS["yellow"] + COLORS["bold"],
|
||||
"warn": COLORS["yellow"],
|
||||
"LLM": COLORS["cyan"] + COLORS["bold"],
|
||||
"llm": COLORS["cyan"],
|
||||
"MCP": COLORS["magenta"] + COLORS["bold"],
|
||||
"mcp": COLORS["magenta"],
|
||||
"device": COLORS["green"],
|
||||
"DEVICE": COLORS["green"] + COLORS["bold"],
|
||||
"TTS": COLORS["blue"] + COLORS["bold"],
|
||||
"tts": COLORS["blue"],
|
||||
"STT": COLORS["blue"],
|
||||
"ASR": COLORS["blue"],
|
||||
"VAD": COLORS["gray"],
|
||||
"websocket": COLORS["yellow"],
|
||||
"MCP接入点": COLORS["magenta"],
|
||||
"连接成功": COLORS["green"],
|
||||
"启动": COLORS["cyan"],
|
||||
"初始化": COLORS["cyan"],
|
||||
"function_call": COLORS["yellow"],
|
||||
}
|
||||
|
||||
# 过滤器模式
|
||||
FILTER_PATTERNS = {
|
||||
"llm": re.compile(r"(LLM|llm|大模型|MiniMax|abab|chatglm|openai_api|function_call|psycho_screen)", re.IGNORECASE),
|
||||
"mcp": re.compile(r"(MCP|mcp|接入点|tool|call|psycho_screen)", re.IGNORECASE),
|
||||
"device": re.compile(r"(device|设备|连接|断开|认证|websocket)", re.IGNORECASE),
|
||||
"error": re.compile(r"(ERROR|error|错误|失败|异常|401|400|500|exception)", re.IGNORECASE),
|
||||
"tts": re.compile(r"(TTS|tts|语音|音频|EdgeTTS|生成)", re.IGNORECASE),
|
||||
"stt": re.compile(r"(STT|ASR|stt|识别|转文字)", re.IGNORECASE),
|
||||
"all": re.compile(r".*"),
|
||||
}
|
||||
|
||||
# 时间戳提取(格式: 20260406 22:50:12)
|
||||
TIMESTAMP_RE = re.compile(r"(\d{8}\s+\d{2}:\d{2}:\d{2})")
|
||||
|
||||
|
||||
def colorize(line: str) -> str:
|
||||
"""为日志行上色"""
|
||||
for keyword, color in LOG_COLORS.items():
|
||||
if keyword in line:
|
||||
return color + line + COLORS["reset"]
|
||||
return line
|
||||
|
||||
|
||||
def format_line(line: str) -> str:
|
||||
"""格式化单行日志"""
|
||||
line = line.rstrip("\n\r")
|
||||
if not line:
|
||||
return ""
|
||||
|
||||
# 提取时间戳
|
||||
ts_match = TIMESTAMP_RE.search(line)
|
||||
ts = ts_match.group(1) if ts_match else ""
|
||||
|
||||
# 上色
|
||||
colored = colorize(line)
|
||||
|
||||
# 高亮时间戳
|
||||
if ts:
|
||||
return f"{COLORS['gray']}{ts}{COLORS['reset']} {colored[len(ts):]}"
|
||||
return colored
|
||||
|
||||
|
||||
def matches_filter(line: str, filter_name: str) -> bool:
|
||||
"""检查日志行是否匹配过滤器"""
|
||||
pattern = FILTER_PATTERNS.get(filter_name, FILTER_PATTERNS["all"])
|
||||
return bool(pattern.search(line))
|
||||
|
||||
|
||||
def monitor(filter_name: str = "all", container: str = "xiaozhi-esp32-server"):
|
||||
"""主监控循环"""
|
||||
print(f"\n{COLORS['cyan']}{COLORS['bold']}=== xiaozhi-esp32-server 实时日志监控 ==={COLORS['reset']}")
|
||||
print(f"过滤器: {filter_name} | 容器: {container}")
|
||||
print(f"按 {COLORS['yellow']}Ctrl+C{COLORS['reset']} 退出\n")
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
["docker", "logs", "-f", "--since", "0s", container],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=False,
|
||||
bufsize=1,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"{COLORS['red']}启动日志失败: {e}{COLORS['reset']}")
|
||||
sys.exit(1)
|
||||
|
||||
line_count = 0
|
||||
shown_lines = []
|
||||
|
||||
try:
|
||||
for raw_line in iter(proc.stdout.readline, b""):
|
||||
try:
|
||||
line = raw_line.decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
line = raw_line.decode("latin-1", errors="replace")
|
||||
|
||||
if not matches_filter(line, filter_name):
|
||||
continue
|
||||
|
||||
line_count += 1
|
||||
shown_lines.append(line)
|
||||
|
||||
# 只保留最后 5000 行
|
||||
if len(shown_lines) > 5000:
|
||||
shown_lines.pop(0)
|
||||
|
||||
print(format_line(line))
|
||||
sys.stdout.flush()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n\n{COLORS['gray']}已停止。共显示了 {line_count} 条日志。{COLORS['reset']}")
|
||||
finally:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
|
||||
|
||||
def replay_recent(filter_name: str = "all", container: str = "xiaozhi-esp32-server", lines: int = 200):
|
||||
"""回放最近 N 条日志"""
|
||||
print(f"\n{COLORS['cyan']}=== 回放最近 {lines} 条日志 ==={COLORS['reset']}\n")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["docker", "logs", "--tail", str(lines), container],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=False,
|
||||
timeout=10,
|
||||
)
|
||||
for raw_line in iter(proc.stdout.readline, b""):
|
||||
line = raw_line.decode("utf-8", errors="replace")
|
||||
if matches_filter(line, filter_name):
|
||||
print(format_line(line))
|
||||
except Exception as e:
|
||||
print(f"{COLORS['red']}回放失败: {e}{COLORS['reset']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="xiaozhi-esp32-server 实时日志监控器")
|
||||
parser.add_argument(
|
||||
"--filter", "-f",
|
||||
choices=["all", "llm", "mcp", "device", "error", "tts", "stt"],
|
||||
default="all",
|
||||
help="日志过滤器 (default: all)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--container", "-c",
|
||||
default="xiaozhi-esp32-server",
|
||||
help="Docker 容器名 (default: xiaozhi-esp32-server)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--replay", "-r",
|
||||
type=int,
|
||||
metavar="N",
|
||||
help="回放最近 N 条日志后退出(不持续监控)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.replay:
|
||||
replay_recent(filter_name=args.filter, container=args.container, lines=args.replay)
|
||||
else:
|
||||
monitor(filter_name=args.filter, container=args.container)
|
||||
Loading…
Reference in New Issue