#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" LOGFS_OFFSET="0x210000" LOGFS_SIZE="0x40000" LOGFS_BLOCK_SIZE="4096" pause_before_exit() { if [[ -t 0 ]]; then printf '\n按回车键关闭窗口...' read -r _ fi } fail() { printf '\n错误:%s\n' "$1" >&2 pause_before_exit exit 1 } find_serial_port() { local requested_port="${1:-}" if [[ -n "$requested_port" ]]; then [[ -e "$requested_port" ]] || fail "串口不存在:$requested_port" printf '%s' "$requested_port" return fi local ports=() local candidate for candidate in /dev/cu.usbmodem* /dev/cu.usbserial* \ /dev/cu.SLAB_USBtoUART* /dev/cu.wchusbserial*; do [[ -e "$candidate" ]] && ports+=("$candidate") done if [[ ${#ports[@]} -eq 0 ]]; then fail "没有检测到设备串口。请连接设备后重试。" fi if [[ ${#ports[@]} -eq 1 ]]; then printf '%s' "${ports[0]}" return fi printf '检测到多个串口:\n' >&2 local index for ((index = 0; index < ${#ports[@]}; index++)); do printf ' %d) %s\n' "$((index + 1))" "${ports[index]}" >&2 done printf '请选择序号:' >&2 local selection read -r selection [[ "$selection" =~ ^[0-9]+$ ]] || fail "输入的序号无效。" ((selection >= 1 && selection <= ${#ports[@]})) || fail "输入的序号超出范围。" printf '%s' "${ports[selection - 1]}" } find_build_tool() { local relative_path="$1" local candidate="$PROJECT_DIR/$relative_path" if [[ -x "$candidate" ]]; then printf '%s' "$candidate" return fi return 1 } find_idf_value() { local key="$1" local cache="$PROJECT_DIR/build/CMakeCache.txt" [[ -f "$cache" ]] || return 1 sed -n "s|^${key}:[^=]*=||p" "$cache" | head -n 1 } find_idf_python() { local candidate candidate="$(find_idf_value PYTHON || true)" if [[ -n "$candidate" && -x "$candidate" ]]; then printf '%s' "$candidate" return fi for candidate in "$HOME"/.espressif/tools/python/*/venv/bin/python; do if [[ -x "$candidate" ]]; then printf '%s' "$candidate" return fi done return 1 } find_esptool() { local idf_path idf_path="$(find_idf_value esp-idf_SOURCE_DIR || true)" local candidate="$idf_path/components/esptool_py/esptool/esptool.py" if [[ -n "$idf_path" && -f "$candidate" ]]; then printf '%s' "$candidate" return fi for candidate in "$HOME"/.espressif/v*/esp-idf/components/esptool_py/esptool/esptool.py; do if [[ -f "$candidate" ]]; then printf '%s' "$candidate" return fi done return 1 } PORT="$(find_serial_port "${1:-}")" IDF_PYTHON="$(find_idf_python || true)" ESPTOOL="$(find_esptool || true)" LITTLEFS_TOOL="$(find_build_tool build/littlefs_py_venv/bin/littlefs-python || true)" [[ -n "$IDF_PYTHON" ]] || fail "找不到 ESP-IDF Python 环境,请先成功构建一次工程。" [[ -n "$ESPTOOL" ]] || fail "找不到 esptool.py,请检查 ESP-IDF 是否已安装。" [[ -n "$LITTLEFS_TOOL" ]] || fail "找不到 LittleFS 解包工具,请先成功构建一次工程。" TIMESTAMP="$(date '+%Y%m%d-%H%M%S')" OUTPUT_DIR="$PROJECT_DIR/flash_log_dump/$TIMESTAMP" IMAGE_PATH="$OUTPUT_DIR/logfs.bin" EXTRACT_DIR="$OUTPUT_DIR/extracted" REPORT_PATH="$OUTPUT_DIR/remote_logs.txt" mkdir -p "$EXTRACT_DIR" printf '设备串口:%s\n' "$PORT" printf '输出目录:%s\n' "$OUTPUT_DIR" printf '\n注意:请先关闭正在使用该串口的 monitor。\n' printf '读取 Flash 会让设备进入下载模式,完成后设备会自动重启。\n' printf '正在读取 logfs 分区,请勿拔线...\n\n' if ! "$IDF_PYTHON" "$ESPTOOL" \ --chip esp32s3 \ --port "$PORT" \ --baud 460800 \ --before default_reset \ --after hard_reset \ read_flash "$LOGFS_OFFSET" "$LOGFS_SIZE" "$IMAGE_PATH"; then fail "读取 Flash 失败。请关闭 monitor、检查 USB 连接后重试。" fi printf '\n正在解包 LittleFS...\n' if ! "$LITTLEFS_TOOL" extract \ --block-size "$LOGFS_BLOCK_SIZE" \ "$IMAGE_PATH" "$EXTRACT_DIR"; then fail "LittleFS 解包失败,请确认设备使用的是当前 16m.csv 分区表。" fi if ! "$IDF_PYTHON" - "$EXTRACT_DIR" "$REPORT_PATH" <<'PY' import json import sys from pathlib import Path extract_dir = Path(sys.argv[1]) report_path = Path(sys.argv[2]) log_dir = extract_dir / "remote_logs" log_files = sorted(log_dir.glob("log_*.jsonl")) if log_dir.is_dir() else [] with report_path.open("w", encoding="utf-8") as report: report.write("TalkingQ cached remote logs\n") report.write(f"Source: {log_dir}\n") report.write(f"Files: {len(log_files)}\n\n") if not log_files: report.write("No pending remote log files were found.\n") for log_file in log_files: report.write(f"===== {log_file.name} =====\n") with log_file.open("r", encoding="utf-8", errors="replace") as source: for raw_line in source: line = raw_line.strip() if not line: continue try: entry = json.loads(line) timestamp = entry.get("ts_ms", "?") level = entry.get("level", "?") truncated = " [truncated]" if entry.get("truncated") else "" message = entry.get("message", "") report.write(f"[{timestamp:>10} ms] {level}{truncated} {message}\n") except (json.JSONDecodeError, TypeError, ValueError): report.write(f"[invalid JSON] {line}\n") report.write("\n") PY then fail "生成可读日志文件失败。" fi printf '\n读取完成:%s\n' "$REPORT_PATH" if command -v open >/dev/null 2>&1; then open "$REPORT_PATH" else printf '当前系统无法自动打开文件,请手动查看上述路径。\n' fi pause_before_exit