feat(app): merge multi-copy work — modules, auth, UI amiga shell, scenarios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
EXIT_CODES = {
|
||||
"normal_boot_ok": 0,
|
||||
"safe_diagnostic_ok": 10,
|
||||
"build_blocked": 20,
|
||||
"runtime_regression": 30,
|
||||
}
|
||||
|
||||
|
||||
def load_text(path: Path | None) -> str:
|
||||
if path is None or not path.exists():
|
||||
return ""
|
||||
return path.read_text(encoding="utf-8", errors="ignore")
|
||||
|
||||
|
||||
def relevant_segment(text: str) -> str:
|
||||
for marker in ("[HOST] ", "[MAIN] Freenove all-in-one boot", "[BOOT] safe diagnostic mode enabled"):
|
||||
idx = text.rfind(marker if marker != "[HOST] " else "SENT RESET")
|
||||
if idx != -1:
|
||||
return text[idx:]
|
||||
return text
|
||||
|
||||
|
||||
def has_any(text: str, needles: list[str]) -> bool:
|
||||
return any(needle in text for needle in needles)
|
||||
|
||||
|
||||
def classify(text: str, build_ok: bool, upload_ok: bool) -> tuple[str, list[str]]:
|
||||
evidence: list[str] = []
|
||||
if not build_ok or not upload_ok:
|
||||
if not build_ok:
|
||||
evidence.append("build_ok=0")
|
||||
if not upload_ok:
|
||||
evidence.append("upload_ok=0")
|
||||
return "build_blocked", evidence
|
||||
|
||||
segment = relevant_segment(text)
|
||||
psram_match = re.findall(r"psram_found=(\d)", segment)
|
||||
if psram_match:
|
||||
evidence.append(f"psram_found={psram_match[-1]}")
|
||||
|
||||
safe_markers = [
|
||||
"[BOOT] safe diagnostic mode enabled: PSRAM required, app stack disabled",
|
||||
"[SAFE] boot path: storage + serial + display + buttons only",
|
||||
]
|
||||
safe_forbidden = [
|
||||
"[NET]",
|
||||
"[WEB]",
|
||||
"[CAM] boot start",
|
||||
"wifi:alloc pp wdev funcs fail",
|
||||
"tag=fx_",
|
||||
"tag=fx_sprite",
|
||||
]
|
||||
fatal_markers = [
|
||||
"Guru Meditation",
|
||||
"Backtrace:",
|
||||
"abort()",
|
||||
"panic",
|
||||
"wifi:alloc pp wdev funcs fail",
|
||||
"[MEM] alloc_fail",
|
||||
]
|
||||
|
||||
if has_any(segment, safe_markers):
|
||||
evidence.extend(marker for marker in safe_markers if marker in segment)
|
||||
forbidden = [marker for marker in safe_forbidden if marker in segment]
|
||||
if not forbidden and "STATUS mode=safe_diagnostic" in segment:
|
||||
evidence.append("STATUS mode=safe_diagnostic")
|
||||
return "safe_diagnostic_ok", evidence
|
||||
evidence.extend(f"forbidden={marker}" for marker in forbidden)
|
||||
return "runtime_regression", evidence
|
||||
|
||||
normal_markers = [
|
||||
"psram_found=1",
|
||||
"LVGL + display ready",
|
||||
"PONG",
|
||||
]
|
||||
if has_any(segment, normal_markers):
|
||||
evidence.extend(marker for marker in normal_markers if marker in segment)
|
||||
fatals = [marker for marker in fatal_markers if marker in segment]
|
||||
if "psram_found=1" in segment and "LVGL + display ready" in segment and not fatals:
|
||||
return "normal_boot_ok", evidence
|
||||
evidence.extend(f"fatal={marker}" for marker in fatals)
|
||||
return "runtime_regression", evidence
|
||||
|
||||
if segment.strip():
|
||||
evidence.append("boot segment captured but acceptance markers missing")
|
||||
else:
|
||||
evidence.append("boot log empty")
|
||||
return "runtime_regression", evidence
|
||||
|
||||
|
||||
def write_summary(path: Path, state: str, evidence: list[str], log_path: Path | None) -> None:
|
||||
lines = [
|
||||
f"state={state}",
|
||||
]
|
||||
if log_path is not None:
|
||||
lines.append(f"log={log_path}")
|
||||
for item in evidence:
|
||||
lines.append(f"evidence={item}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Classify ESP32 boot logs.")
|
||||
parser.add_argument("--log")
|
||||
parser.add_argument("--summary", required=True)
|
||||
parser.add_argument("--build-ok", type=int, choices=(0, 1), default=1)
|
||||
parser.add_argument("--upload-ok", type=int, choices=(0, 1), default=1)
|
||||
args = parser.parse_args()
|
||||
|
||||
log_path = Path(args.log) if args.log else None
|
||||
summary_path = Path(args.summary)
|
||||
text = load_text(log_path)
|
||||
state, evidence = classify(text, build_ok=bool(args.build_ok), upload_ok=bool(args.upload_ok))
|
||||
write_summary(summary_path, state, evidence, log_path)
|
||||
print(state)
|
||||
return EXIT_CODES[state]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||
VENV_DIR="${ZACUS_VENV_DIR:-$ROOT_DIR/.venv}"
|
||||
|
||||
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
|
||||
echo "[BOOTSTRAP] python3 not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "[BOOTSTRAP] creating virtualenv at $VENV_DIR"
|
||||
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install platformio esptool pyserial
|
||||
|
||||
echo "[BOOTSTRAP] tool versions"
|
||||
python -m platformio --version
|
||||
python -m esptool version
|
||||
python - <<'PY'
|
||||
import serial
|
||||
print(f"pyserial {serial.VERSION}")
|
||||
PY
|
||||
|
||||
cat <<EOF
|
||||
|
||||
[BOOTSTRAP] next steps
|
||||
source "$VENV_DIR/bin/activate"
|
||||
python -m platformio run -e freenove_esp32s3_full_with_ui
|
||||
python -m platformio run -e freenove_esp32s3_full_with_ui -t buildfs
|
||||
python -m platformio run -e freenove_esp32s3_full_with_ui -t uploadfs --upload-port <PORT>
|
||||
python -m platformio run -e freenove_esp32s3_full_with_ui -t upload --upload-port <PORT>
|
||||
python -m platformio device monitor -b 115200 --port <PORT>
|
||||
EOF
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||
STATUS=0
|
||||
|
||||
ok() {
|
||||
printf '[OK] %s\n' "$1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '[WARN] %s\n' "$1"
|
||||
}
|
||||
|
||||
err() {
|
||||
printf '[ERR] %s\n' "$1" >&2
|
||||
STATUS=1
|
||||
}
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
for cmd in bash rg "$PYTHON_BIN"; do
|
||||
if command -v "$cmd" >/dev/null 2>&1; then
|
||||
ok "tool available: $cmd"
|
||||
else
|
||||
err "missing tool: $cmd"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v pio >/dev/null 2>&1; then
|
||||
ok "pio available: $(pio --version | head -n 1)"
|
||||
elif [ -x "$ROOT_DIR/.venv/bin/python" ] && "$ROOT_DIR/.venv/bin/python" -m platformio --version >/dev/null 2>&1; then
|
||||
ok "platformio available in .venv: $("$ROOT_DIR/.venv/bin/python" -m platformio --version | head -n 1)"
|
||||
else
|
||||
warn "PlatformIO not found in PATH or .venv"
|
||||
fi
|
||||
|
||||
platform_pin="$(rg -n '^platform = ' platformio.ini | sed 's/^[0-9]*://')"
|
||||
ok "platform pin: ${platform_pin:-missing}"
|
||||
|
||||
echo "[INFO] supported envs"
|
||||
rg '^\[env:' platformio.ini | sed 's/^\[env://; s/\]$//'
|
||||
|
||||
for path in \
|
||||
README.md \
|
||||
README_ESP32_ZACUS.md \
|
||||
ui_freenove_allinone/README.md \
|
||||
docs/QUICKSTART.md \
|
||||
docs/FNK0102H_SOURCE_OF_TRUTH.md \
|
||||
boards/freenove_esp32_s3_wroom.json \
|
||||
scripts/bootstrap_platformio.sh \
|
||||
scripts/doctor_repo.sh \
|
||||
scripts/flash_monitor_audit.sh \
|
||||
scripts/serial_boot_capture.py \
|
||||
scripts/audit_boot_log.py \
|
||||
tests/sprint1_utility_contract.py \
|
||||
tests/sprint2_capture_contract.py \
|
||||
tests/sprint3_audio_contract.py \
|
||||
tests/phase9_ui_validation.py; do
|
||||
if [ -e "$path" ]; then
|
||||
ok "path present: $path"
|
||||
else
|
||||
err "path missing: $path"
|
||||
fi
|
||||
done
|
||||
|
||||
"$PYTHON_BIN" - <<'PY' || STATUS=1
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
root = Path.cwd()
|
||||
files = [
|
||||
root / "README.md",
|
||||
root / "README_ESP32_ZACUS.md",
|
||||
root / "ui_freenove_allinone" / "README.md",
|
||||
root / "docs" / "QUICKSTART.md",
|
||||
root / "docs" / "FNK0102H_SOURCE_OF_TRUTH.md",
|
||||
]
|
||||
legacy_needles = ("hardware/firmware", "tools/dev/", "protocol/", "../hardware/")
|
||||
status = 0
|
||||
|
||||
for path in files:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for match in re.finditer(r"\[[^\]]+\]\(([^)]+)\)", text):
|
||||
target = match.group(1).strip()
|
||||
if not target or target.startswith(("http://", "https://", "#", "mailto:")):
|
||||
continue
|
||||
target = target.split("#", 1)[0]
|
||||
if not (path.parent / target).resolve().exists():
|
||||
print(f"[ERR] broken markdown link in {path.relative_to(root)} -> {target}")
|
||||
status = 1
|
||||
for needle in legacy_needles:
|
||||
if needle in text:
|
||||
print(f"[ERR] stale legacy reference in {path.relative_to(root)} -> {needle}")
|
||||
status = 1
|
||||
|
||||
if status == 0:
|
||||
print("[OK] markdown links and legacy path scan clean")
|
||||
sys.exit(status)
|
||||
PY
|
||||
|
||||
if [ "$STATUS" -ne 0 ]; then
|
||||
err "doctor detected blocking issues"
|
||||
else
|
||||
ok "doctor completed without blocking issues"
|
||||
fi
|
||||
|
||||
exit "$STATUS"
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_NAME="${ZACUS_ENV:-freenove_esp32s3_full_with_ui}"
|
||||
MONITOR_SECONDS="${ZACUS_MONITOR_SECONDS:-90}"
|
||||
DEFAULT_PORT="/dev/cu.usbmodem5AB90753301"
|
||||
|
||||
if [ -x "$ROOT_DIR/.venv/bin/python" ]; then
|
||||
PYTHON_BIN="$ROOT_DIR/.venv/bin/python"
|
||||
else
|
||||
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||
fi
|
||||
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
BUILD_ARTIFACT_DIR="$ROOT_DIR/artifacts/build"
|
||||
MONITOR_ARTIFACT_DIR="$ROOT_DIR/artifacts/monitor"
|
||||
AUDIT_ARTIFACT_DIR="$ROOT_DIR/artifacts/audit"
|
||||
TEST_ARTIFACT_DIR="$ROOT_DIR/artifacts/tests"
|
||||
SUMMARY_FILE="$AUDIT_ARTIFACT_DIR/${TIMESTAMP}_summary.txt"
|
||||
MONITOR_LOG="$MONITOR_ARTIFACT_DIR/${TIMESTAMP}_boot.log"
|
||||
ESPTOOL_LOG="$AUDIT_ARTIFACT_DIR/${TIMESTAMP}_esptool.log"
|
||||
|
||||
mkdir -p "$BUILD_ARTIFACT_DIR" "$MONITOR_ARTIFACT_DIR" "$AUDIT_ARTIFACT_DIR" "$TEST_ARTIFACT_DIR"
|
||||
|
||||
log() {
|
||||
printf '[CHAIN] %s\n' "$1"
|
||||
}
|
||||
|
||||
detect_port() {
|
||||
if [ -n "${ZACUS_SERIAL_PORT:-}" ] && [ -e "${ZACUS_SERIAL_PORT}" ]; then
|
||||
printf '%s\n' "${ZACUS_SERIAL_PORT}"
|
||||
return 0
|
||||
fi
|
||||
if [ -e "$DEFAULT_PORT" ]; then
|
||||
printf '%s\n' "$DEFAULT_PORT"
|
||||
return 0
|
||||
fi
|
||||
mapfile -t ports < <(find /dev -maxdepth 1 -name 'cu.usbmodem*' | sort)
|
||||
if [ "${#ports[@]}" -eq 1 ]; then
|
||||
printf '%s\n' "${ports[0]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
run_stage() {
|
||||
local stage="$1"
|
||||
shift
|
||||
local logfile="$BUILD_ARTIFACT_DIR/${TIMESTAMP}_${stage}.log"
|
||||
log "stage=$stage"
|
||||
if "$@" 2>&1 | tee "$logfile"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
write_blocked_summary() {
|
||||
local build_ok="$1"
|
||||
local upload_ok="$2"
|
||||
"$PYTHON_BIN" "$ROOT_DIR/scripts/audit_boot_log.py" \
|
||||
--summary "$SUMMARY_FILE" \
|
||||
--build-ok "$build_ok" \
|
||||
--upload-ok "$upload_ok" >/dev/null
|
||||
}
|
||||
|
||||
run_upload_target() {
|
||||
local target="$1"
|
||||
local port="$2"
|
||||
run_stage "$target" "$PYTHON_BIN" -m platformio run -e "$ENV_NAME" -t "$target" --upload-port "$port"
|
||||
}
|
||||
|
||||
run_upload_with_retry() {
|
||||
local result_var="$1"
|
||||
local target="$2"
|
||||
local port="$3"
|
||||
if run_upload_target "$target" "$port"; then
|
||||
printf -v "$result_var" '%s' "$port"
|
||||
return 0
|
||||
fi
|
||||
log "retry stage=$target port=$port"
|
||||
if [ -e "$port" ] && run_upload_target "$target" "$port"; then
|
||||
printf -v "$result_var" '%s' "$port"
|
||||
return 0
|
||||
fi
|
||||
local fallback_port
|
||||
fallback_port="$(detect_port)" || return 1
|
||||
if [ "$fallback_port" != "$port" ] && run_upload_target "$target" "$fallback_port"; then
|
||||
printf -v "$result_var" '%s' "$fallback_port"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
run_test() {
|
||||
local name="$1"
|
||||
shift
|
||||
local logfile="$TEST_ARTIFACT_DIR/${TIMESTAMP}_${name}.log"
|
||||
log "test=$name"
|
||||
"$@" 2>&1 | tee "$logfile"
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT_DIR"
|
||||
local port
|
||||
port="$(detect_port)" || {
|
||||
printf 'state=hardware_unreachable\nreason=no_serial_port\n' >"$SUMMARY_FILE"
|
||||
log "no serial port detected"
|
||||
return 40
|
||||
}
|
||||
|
||||
export ZACUS_SERIAL_PORT="$port"
|
||||
|
||||
if ! run_stage build "$PYTHON_BIN" -m platformio run -e "$ENV_NAME"; then
|
||||
write_blocked_summary 0 0
|
||||
return 20
|
||||
fi
|
||||
if ! run_stage buildfs "$PYTHON_BIN" -m platformio run -e "$ENV_NAME" -t buildfs; then
|
||||
write_blocked_summary 0 0
|
||||
return 20
|
||||
fi
|
||||
|
||||
if ! run_upload_with_retry port uploadfs "$port"; then
|
||||
write_blocked_summary 1 0
|
||||
return 20
|
||||
fi
|
||||
export ZACUS_SERIAL_PORT="$port"
|
||||
|
||||
if ! run_upload_with_retry port upload "$port"; then
|
||||
write_blocked_summary 1 0
|
||||
return 20
|
||||
fi
|
||||
export ZACUS_SERIAL_PORT="$port"
|
||||
|
||||
if ! "$PYTHON_BIN" "$ROOT_DIR/scripts/serial_boot_capture.py" \
|
||||
--port "$port" \
|
||||
--baud 115200 \
|
||||
--seconds "$MONITOR_SECONDS" \
|
||||
--log "$MONITOR_LOG"; then
|
||||
printf 'state=hardware_unreachable\nreason=serial_capture_failed\nport=%s\nlog=%s\n' "$port" "$MONITOR_LOG" >"$SUMMARY_FILE"
|
||||
return 40
|
||||
fi
|
||||
|
||||
local audit_state
|
||||
set +e
|
||||
audit_state="$("$PYTHON_BIN" "$ROOT_DIR/scripts/audit_boot_log.py" \
|
||||
--log "$MONITOR_LOG" \
|
||||
--summary "$SUMMARY_FILE" \
|
||||
--build-ok 1 \
|
||||
--upload-ok 1)"
|
||||
local audit_rc=$?
|
||||
set -e
|
||||
|
||||
if [ "$audit_state" = "safe_diagnostic_ok" ]; then
|
||||
{
|
||||
printf '[ESPTOOL] flash_id\n'
|
||||
"$PYTHON_BIN" -m esptool --chip esp32s3 --port "$port" flash_id
|
||||
printf '\n[ESPTOOL] read_mac\n'
|
||||
"$PYTHON_BIN" -m esptool --chip esp32s3 --port "$port" read_mac
|
||||
} 2>&1 | tee "$ESPTOOL_LOG"
|
||||
{
|
||||
printf 'finding=containment_logic_ok_root_cause_probable_hardware_or_memory_config\n'
|
||||
printf 'expected_board=FNK0102H/ESP32-S3-WROOM-1-N16R8\n'
|
||||
printf 'esptool_log=%s\n' "$ESPTOOL_LOG"
|
||||
} >>"$SUMMARY_FILE"
|
||||
return "$audit_rc"
|
||||
fi
|
||||
|
||||
if [ "$audit_state" = "normal_boot_ok" ]; then
|
||||
run_test sprint1 "$PYTHON_BIN" "$ROOT_DIR/tests/sprint1_utility_contract.py" --mode serial --cycles 1 --port "$port"
|
||||
run_test sprint2 "$PYTHON_BIN" "$ROOT_DIR/tests/sprint2_capture_contract.py" --mode serial --cycles 1 --port "$port"
|
||||
run_test sprint3 "$PYTHON_BIN" "$ROOT_DIR/tests/sprint3_audio_contract.py" --mode serial --cycles 1 --port "$port"
|
||||
run_test phase9 "$PYTHON_BIN" "$ROOT_DIR/tests/phase9_ui_validation.py" --port "$port"
|
||||
printf 'tests=serial_smoke_pass\n' >>"$SUMMARY_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return "$audit_rc"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+175
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import serial
|
||||
|
||||
|
||||
BOOT_BANNER = "[MAIN] Freenove all-in-one boot"
|
||||
|
||||
|
||||
def host_line(text: str) -> str:
|
||||
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return f"[HOST] {stamp} {text}"
|
||||
|
||||
|
||||
def open_serial(port: str, baud: int, timeout: float) -> serial.Serial:
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = baud
|
||||
ser.timeout = timeout
|
||||
ser.write_timeout = 1.0
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
ser.open()
|
||||
time.sleep(0.25)
|
||||
return ser
|
||||
|
||||
|
||||
def flush_lines(ser: serial.Serial, log_handle, deadline: float) -> list[str]:
|
||||
lines: list[str] = []
|
||||
pending = ""
|
||||
while time.time() < deadline:
|
||||
chunk = ser.read(ser.in_waiting or 1)
|
||||
if not chunk:
|
||||
continue
|
||||
pending += chunk.decode("utf-8", errors="ignore")
|
||||
while "\n" in pending:
|
||||
raw_line, pending = pending.split("\n", 1)
|
||||
line = raw_line.rstrip("\r")
|
||||
log_handle.write(line + "\n")
|
||||
log_handle.flush()
|
||||
lines.append(line)
|
||||
if pending:
|
||||
line = pending.rstrip("\r")
|
||||
log_handle.write(line + "\n")
|
||||
log_handle.flush()
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def send_command(ser: serial.Serial, log_handle, command: str, wait_s: float) -> list[str]:
|
||||
log_handle.write(host_line(f"SENT {command}") + "\n")
|
||||
log_handle.flush()
|
||||
ser.write((command + "\n").encode("utf-8"))
|
||||
ser.flush()
|
||||
return flush_lines(ser, log_handle, time.time() + wait_s)
|
||||
|
||||
|
||||
def wait_for_command_channel(ser: serial.Serial, log_handle, timeout_s: float) -> bool:
|
||||
deadline = time.time() + timeout_s
|
||||
while time.time() < deadline:
|
||||
flush_lines(ser, log_handle, time.time() + 0.35)
|
||||
response = send_command(ser, log_handle, "PING", wait_s=1.2)
|
||||
if any("PONG" in line or "UNKNOWN PING" in line for line in response):
|
||||
log_handle.write(host_line("COMMAND CHANNEL READY") + "\n")
|
||||
log_handle.flush()
|
||||
return True
|
||||
time.sleep(0.4)
|
||||
log_handle.write(host_line("COMMAND CHANNEL NOT READY") + "\n")
|
||||
log_handle.flush()
|
||||
return False
|
||||
|
||||
|
||||
def wait_for_quiet_period(
|
||||
ser: serial.Serial,
|
||||
log_handle,
|
||||
timeout_s: float,
|
||||
quiet_s: float,
|
||||
) -> tuple[bool, float]:
|
||||
deadline = time.time() + timeout_s
|
||||
last_rx_at = time.time()
|
||||
while time.time() < deadline:
|
||||
lines = flush_lines(ser, log_handle, time.time() + 0.25)
|
||||
if lines:
|
||||
last_rx_at = time.time()
|
||||
elif (time.time() - last_rx_at) >= quiet_s:
|
||||
return True, last_rx_at
|
||||
return False, last_rx_at
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Capture a rebooted ESP32 boot log over serial.")
|
||||
parser.add_argument("--port", required=True)
|
||||
parser.add_argument("--baud", type=int, default=115200)
|
||||
parser.add_argument("--timeout", type=float, default=0.2)
|
||||
parser.add_argument("--seconds", type=float, default=90.0)
|
||||
parser.add_argument("--log", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
log_path = Path(args.log)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
with log_path.open("w", encoding="utf-8") as log_handle:
|
||||
log_handle.write(host_line(f"OPEN port={args.port} baud={args.baud}") + "\n")
|
||||
log_handle.flush()
|
||||
|
||||
ser = open_serial(args.port, args.baud, args.timeout)
|
||||
try:
|
||||
ready = wait_for_command_channel(ser, log_handle, timeout_s=20.0)
|
||||
if ready:
|
||||
ser.close()
|
||||
time.sleep(1.2)
|
||||
ser = open_serial(args.port, args.baud, args.timeout)
|
||||
log_handle.write(host_line("REOPEN FOR BOOT CAPTURE") + "\n")
|
||||
log_handle.flush()
|
||||
|
||||
capture_deadline = time.time() + args.seconds
|
||||
boot_seen = False
|
||||
post_boot_commands_sent = False
|
||||
boot_seen_at = 0.0
|
||||
last_rx_at = time.time()
|
||||
phase_start = time.time()
|
||||
|
||||
while time.time() < capture_deadline:
|
||||
lines = flush_lines(ser, log_handle, time.time() + 0.25)
|
||||
if lines:
|
||||
last_rx_at = time.time()
|
||||
for line in lines:
|
||||
if BOOT_BANNER in line:
|
||||
boot_seen = True
|
||||
boot_seen_at = time.time()
|
||||
|
||||
should_probe = False
|
||||
if (
|
||||
boot_seen
|
||||
and not post_boot_commands_sent
|
||||
and (
|
||||
(time.time() - last_rx_at) >= 1.8
|
||||
or (time.time() - boot_seen_at) >= 18.0
|
||||
)
|
||||
):
|
||||
should_probe = True
|
||||
if (
|
||||
not boot_seen
|
||||
and not post_boot_commands_sent
|
||||
and (time.time() - phase_start) >= 18.0
|
||||
):
|
||||
should_probe = True
|
||||
|
||||
if should_probe:
|
||||
send_command(ser, log_handle, "PING", wait_s=2.0)
|
||||
send_command(ser, log_handle, "STATUS", wait_s=2.4)
|
||||
post_boot_commands_sent = True
|
||||
|
||||
log_handle.write(host_line("CAPTURE COMPLETE") + "\n")
|
||||
log_handle.flush()
|
||||
finally:
|
||||
try:
|
||||
ser.close()
|
||||
except Exception:
|
||||
pass
|
||||
except serial.SerialException as exc:
|
||||
print(f"serial capture failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user