chore: archive diagnostics and project documentation
This commit is contained in:
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FIRMWARE_PATH="${1:-${ROOT_DIR}/build/TQ_Cat_Code.bin}"
|
||||
VERSION_FILE="${ROOT_DIR}/version.txt"
|
||||
|
||||
fail() {
|
||||
echo "Error: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ ! -f "${FIRMWARE_PATH}" ]]; then
|
||||
fail "firmware binary not found: ${FIRMWARE_PATH}"
|
||||
fi
|
||||
|
||||
# ESP-IDF places esp_app_desc_t at image offset 0x20. Its magic word is
|
||||
# 0xABCD5432 and its 32-byte application version starts at offset 0x30.
|
||||
APP_DESC_MAGIC="$(od -An -tx1 -j 32 -N 4 "${FIRMWARE_PATH}" | tr -d '[:space:]')"
|
||||
if [[ "${APP_DESC_MAGIC}" != "3254cdab" ]]; then
|
||||
fail "not a supported ESP-IDF application image: ${FIRMWARE_PATH}"
|
||||
fi
|
||||
|
||||
FIRMWARE_VERSION="$(
|
||||
dd if="${FIRMWARE_PATH}" bs=1 skip=48 count=32 2>/dev/null |
|
||||
LC_ALL=C tr -d '\000'
|
||||
)"
|
||||
if [[ -z "${FIRMWARE_VERSION}" ]]; then
|
||||
fail "firmware version is empty: ${FIRMWARE_PATH}"
|
||||
fi
|
||||
|
||||
SIZE_BYTES="$(wc -c < "${FIRMWARE_PATH}" | tr -d '[:space:]')"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
SHA256="$(sha256sum "${FIRMWARE_PATH}" | awk '{print $1}')"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
SHA256="$(shasum -a 256 "${FIRMWARE_PATH}" | awk '{print $1}')"
|
||||
else
|
||||
fail "neither sha256sum nor shasum is available"
|
||||
fi
|
||||
|
||||
echo "version=${FIRMWARE_VERSION}"
|
||||
echo "sizeBytes=${SIZE_BYTES}"
|
||||
echo "sha256=${SHA256}"
|
||||
|
||||
if [[ -f "${VERSION_FILE}" ]]; then
|
||||
SOURCE_VERSION="$(tr -d '\r\n' < "${VERSION_FILE}")"
|
||||
if [[ -n "${SOURCE_VERSION}" && "${SOURCE_VERSION}" != "${FIRMWARE_VERSION}" ]]; then
|
||||
echo "Warning: version.txt=${SOURCE_VERSION}, but the binary contains ${FIRMWARE_VERSION}; rebuild the firmware." >&2
|
||||
fi
|
||||
fi
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/zsh
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="${0:A:h}"
|
||||
PLAYER="$SCRIPT_DIR/play_prompt_opus.py"
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
PROMPT_FILE="$1"
|
||||
else
|
||||
PROMPT_FILE="$(osascript <<'APPLESCRIPT'
|
||||
try
|
||||
POSIX path of (choose file with prompt "Choose a TQ_CatN prompt Opus file")
|
||||
on error number -128
|
||||
return ""
|
||||
end try
|
||||
APPLESCRIPT
|
||||
)"
|
||||
fi
|
||||
|
||||
if [[ -z "$PROMPT_FILE" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 "$PLAYER" "$PROMPT_FILE"
|
||||
RESULT=$?
|
||||
if [[ $RESULT -ne 0 ]]; then
|
||||
echo
|
||||
echo "Playback failed. Install FFmpeg with: brew install ffmpeg"
|
||||
echo "Press Enter to close."
|
||||
read -r
|
||||
fi
|
||||
exit $RESULT
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Play or convert the firmware's length-prefixed raw Opus prompt files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
MAX_PACKET_BYTES = 2_048
|
||||
OGG_SIGNATURE = b"OggS"
|
||||
OPUS_CLOCK_HZ = 48_000
|
||||
PROMPT_FRAME_MS = 60
|
||||
OGG_STREAM_SERIAL = 0x54514341
|
||||
|
||||
|
||||
class PromptError(RuntimeError):
|
||||
"""Raised when a prompt file does not match the firmware format."""
|
||||
|
||||
|
||||
def read_prompt_packets(path: Path) -> list[bytes]:
|
||||
packets: list[bytes] = []
|
||||
with path.open("rb") as stream:
|
||||
while True:
|
||||
header = stream.read(4)
|
||||
if not header:
|
||||
break
|
||||
if len(header) != 4:
|
||||
raise PromptError("truncated 4-byte packet header")
|
||||
packet_size = struct.unpack(">I", header)[0]
|
||||
if packet_size == 0 or packet_size > MAX_PACKET_BYTES:
|
||||
raise PromptError(f"invalid Opus packet size: {packet_size}")
|
||||
packet = stream.read(packet_size)
|
||||
if len(packet) != packet_size:
|
||||
raise PromptError("truncated Opus packet payload")
|
||||
packets.append(packet)
|
||||
if not packets:
|
||||
raise PromptError("prompt contains no Opus packets")
|
||||
return packets
|
||||
|
||||
|
||||
def ogg_crc(data: bytes) -> int:
|
||||
crc = 0
|
||||
for value in data:
|
||||
crc ^= value << 24
|
||||
for _ in range(8):
|
||||
crc = ((crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if crc & 0x80000000 else (crc << 1) & 0xFFFFFFFF
|
||||
return crc
|
||||
|
||||
|
||||
def packet_lacing(packet_size: int) -> bytes:
|
||||
segments = bytearray()
|
||||
remaining = packet_size
|
||||
while remaining >= 255:
|
||||
segments.append(255)
|
||||
remaining -= 255
|
||||
segments.append(remaining)
|
||||
return bytes(segments)
|
||||
|
||||
|
||||
def make_ogg_page(packet: bytes, flags: int, granule: int, sequence: int) -> bytes:
|
||||
lacing = packet_lacing(len(packet))
|
||||
if len(lacing) > 255:
|
||||
raise PromptError("Opus packet requires too many Ogg segments")
|
||||
header = bytearray()
|
||||
header.extend(OGG_SIGNATURE)
|
||||
header.extend((0, flags))
|
||||
header.extend(struct.pack("<Q", granule))
|
||||
header.extend(struct.pack("<I", OGG_STREAM_SERIAL))
|
||||
header.extend(struct.pack("<I", sequence))
|
||||
header.extend(b"\x00\x00\x00\x00")
|
||||
header.append(len(lacing))
|
||||
header.extend(lacing)
|
||||
page = header + packet
|
||||
struct.pack_into("<I", page, 22, ogg_crc(page))
|
||||
return bytes(page)
|
||||
|
||||
|
||||
def write_ogg(path: Path, packets: list[bytes]) -> None:
|
||||
opus_head = b"OpusHead" + struct.pack("<BBHIhB", 1, 1, 0, 16_000, 0, 0)
|
||||
vendor = b"TQ_CatN prompt player"
|
||||
opus_tags = b"OpusTags" + struct.pack("<I", len(vendor)) + vendor + struct.pack("<I", 0)
|
||||
samples_per_packet = OPUS_CLOCK_HZ * PROMPT_FRAME_MS // 1_000
|
||||
|
||||
with path.open("wb") as output:
|
||||
output.write(make_ogg_page(opus_head, flags=0x02, granule=0, sequence=0))
|
||||
output.write(make_ogg_page(opus_tags, flags=0, granule=0, sequence=1))
|
||||
granule = 0
|
||||
for index, packet in enumerate(packets):
|
||||
granule += samples_per_packet
|
||||
flags = 0x04 if index == len(packets) - 1 else 0
|
||||
output.write(make_ogg_page(packet, flags=flags, granule=granule, sequence=index + 2))
|
||||
|
||||
|
||||
def convert_prompt(input_path: Path, output_path: Path) -> None:
|
||||
if not input_path.is_file():
|
||||
raise PromptError(f"file not found: {input_path}")
|
||||
if input_path.read_bytes()[:4] == OGG_SIGNATURE:
|
||||
shutil.copyfile(input_path, output_path)
|
||||
return
|
||||
write_ogg(output_path, read_prompt_packets(input_path))
|
||||
|
||||
|
||||
def play_prompt(input_path: Path, ffplay: str) -> None:
|
||||
executable = shutil.which(ffplay)
|
||||
if executable is None:
|
||||
raise PromptError("ffplay is not installed; run: brew install ffmpeg")
|
||||
with tempfile.TemporaryDirectory(prefix="tq_prompt_player_") as temp_dir:
|
||||
ogg_path = Path(temp_dir) / f"{input_path.stem}.ogg"
|
||||
convert_prompt(input_path, ogg_path)
|
||||
subprocess.run(
|
||||
[executable, "-hide_banner", "-loglevel", "warning", "-nodisp", "-autoexit", str(ogg_path)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input", type=Path, help="firmware prompt .opus file")
|
||||
parser.add_argument("--output", type=Path, help="write standard Ogg Opus instead of playing")
|
||||
parser.add_argument("--ffplay", default="ffplay", help="ffplay executable name or path")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.output is not None:
|
||||
convert_prompt(args.input, args.output)
|
||||
print(f"Created standard Ogg Opus: {args.output}")
|
||||
else:
|
||||
play_prompt(args.input, args.ffplay)
|
||||
except (PromptError, OSError, subprocess.CalledProcessError) as error:
|
||||
print(f"Error: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user