199 lines
6.0 KiB
Bash
Executable File
199 lines
6.0 KiB
Bash
Executable File
#!/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
|