From d6b2f52c30561af4619bb6c38b6a0de241978a71 Mon Sep 17 00:00:00 2001 From: Sileya Date: Tue, 7 Apr 2026 00:25:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9AESP32=20=E7=83=A7?= =?UTF-8?q?=E5=BD=95=E6=8C=87=E5=8D=97=20+=20=E5=AE=9E=E6=97=B6=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=9B=91=E6=8E=A7=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 条日志 --- ESP32_FLASH_GUIDE.md | 233 +++++++++++++++++++++++++++++++++++++++++ xiaozhi_log_monitor.py | 200 +++++++++++++++++++++++++++++++++++ 2 files changed, 433 insertions(+) create mode 100644 ESP32_FLASH_GUIDE.md create mode 100644 xiaozhi_log_monitor.py diff --git a/ESP32_FLASH_GUIDE.md b/ESP32_FLASH_GUIDE.md new file mode 100644 index 0000000..e647f21 --- /dev/null +++ b/ESP32_FLASH_GUIDE.md @@ -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://: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 +``` diff --git a/xiaozhi_log_monitor.py b/xiaozhi_log_monitor.py new file mode 100644 index 0000000..916230e --- /dev/null +++ b/xiaozhi_log_monitor.py @@ -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)