Technician tool v0.2
Imported from the Google Drive folder 004-technician_tool/v0.2.
This commit is contained in:
1 parent
e5a33a1c34
commit
e8b72b743d
17 files changed
+2410
-283
No files matched your search
@@ -0,0 +1,32 @@
|
||||
"""Load saved logs for offline analysis.
|
||||
|
||||
Accepts both raw serial captures and this tool's session logs, whose lines
|
||||
look like "[14:02:11.532] I (1234) TAG: msg" and whose host events look
|
||||
like "[14:02:11.532] --- reset issued by tool ---".
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from .logline import LineKind, LogLine, parse_line, split_glued
|
||||
|
||||
_SESSION_PREFIX = re.compile(r"^\[(\d\d):(\d\d):(\d\d)\.(\d{3})\] ")
|
||||
_HOST_EVENT = re.compile(r"^--- .* ---$")
|
||||
|
||||
|
||||
def read_log_file(path: Path) -> list[LogLine]:
|
||||
text = path.read_bytes().decode("utf-8", errors="replace")
|
||||
out: list[LogLine] = []
|
||||
for i, raw in enumerate(text.splitlines()):
|
||||
host_time = i * 0.001 # raw capture: keep order, no wall clock
|
||||
if m := _SESSION_PREFIX.match(raw):
|
||||
h, mi, s, ms = (int(g) for g in m.groups())
|
||||
host_time = h * 3600 + mi * 60 + s + ms / 1000
|
||||
raw = raw[m.end():]
|
||||
if _HOST_EVENT.match(raw):
|
||||
out.append(LogLine(raw, LineKind.HOST, host_time))
|
||||
continue
|
||||
out.extend(parse_line(part, host_time) for part in split_glued(raw))
|
||||
return out
|
||||
+36
-30
@@ -76,65 +76,71 @@ class LineFramer:
|
||||
|
||||
A partial line is released after `idle_flush_s` without new bytes so
|
||||
prompts / unterminated output are not held back forever.
|
||||
|
||||
Lines are also split where a new log line starts in the middle
|
||||
("I (2302) chip[DL]: I (2302) chip[DL]: Posting..."): the two S3 cores
|
||||
interleave output, and the USB-Serial-JTAG FIFO keeps the tail of an old
|
||||
line while no host reads, releasing it glued in front of the next live
|
||||
output. After a (re)open, the fragment in front of the first such split
|
||||
is stale and dropped. Timing cannot identify it, because the FIFO only
|
||||
flushes when the board next prints, possibly seconds later.
|
||||
"""
|
||||
|
||||
# Stale FIFO bytes arrive immediately after open; anything later is live.
|
||||
STALE_WINDOW_S = 0.25
|
||||
|
||||
def __init__(self, idle_flush_s: float = 0.3, max_line: int = 4096,
|
||||
drop_first_partial: bool = False, now: float | None = None):
|
||||
drop_first_partial: bool = False):
|
||||
self._buf = bytearray()
|
||||
self._last_rx = 0.0
|
||||
self.idle_flush_s = idle_flush_s
|
||||
self.max_line = max_line
|
||||
# The S3 USB-Serial-JTAG FIFO keeps the tail of an old line while no
|
||||
# host is reading and flushes it on open; drop bytes up to the first
|
||||
# newline after a (re)connect so it is not glued to a fresh line.
|
||||
# Only bytes arriving within STALE_WINDOW_S of open are considered
|
||||
# stale, otherwise an idle board's next boot banner would be lost.
|
||||
self._dropping = drop_first_partial
|
||||
self._drop_deadline = (time.monotonic() if now is None else now) + self.STALE_WINDOW_S
|
||||
self._first_line_pending = drop_first_partial
|
||||
|
||||
def cancel_drop(self) -> None:
|
||||
self._dropping = False
|
||||
self._first_line_pending = False
|
||||
|
||||
def feed(self, data: bytes, now: float | None = None) -> list[str]:
|
||||
now = time.monotonic() if now is None else now
|
||||
if self._dropping and now > self._drop_deadline:
|
||||
self._dropping = False
|
||||
if data:
|
||||
self._buf.extend(data)
|
||||
self._last_rx = now
|
||||
if self._dropping:
|
||||
i = self._buf.find(b"\n")
|
||||
if i < 0:
|
||||
return []
|
||||
del self._buf[: i + 1]
|
||||
self._dropping = False
|
||||
out: list[str] = []
|
||||
while (i := self._buf.find(b"\n")) >= 0:
|
||||
out.append(self._decode(self._buf[:i]))
|
||||
out += self._emit(self._buf[:i])
|
||||
del self._buf[: i + 1]
|
||||
if len(self._buf) >= self.max_line:
|
||||
out.append(self._decode(self._buf))
|
||||
out += self._emit(self._buf)
|
||||
self._buf.clear()
|
||||
return out
|
||||
|
||||
def poll_idle(self, now: float | None = None) -> list[str]:
|
||||
now = time.monotonic() if now is None else now
|
||||
if self._buf and now - self._last_rx >= self.idle_flush_s:
|
||||
line = self._decode(self._buf)
|
||||
self._buf.clear()
|
||||
return [line]
|
||||
return self.flush()
|
||||
return []
|
||||
|
||||
def flush(self) -> list[str]:
|
||||
if not self._buf:
|
||||
return []
|
||||
line = self._decode(self._buf)
|
||||
out = self._emit(self._buf)
|
||||
self._buf.clear()
|
||||
return [line]
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _decode(b: bytes | bytearray) -> str:
|
||||
return bytes(b).decode("utf-8", errors="replace")
|
||||
def _emit(self, raw: bytes | bytearray) -> list[str]:
|
||||
parts = split_glued(bytes(raw).decode("utf-8", errors="replace"))
|
||||
if self._first_line_pending:
|
||||
self._first_line_pending = False
|
||||
if len(parts) > 1:
|
||||
parts = parts[1:] # stale FIFO fragment from before the open
|
||||
return parts
|
||||
|
||||
|
||||
# Start of a log line (optionally ANSI-coloured) or ROM banner.
|
||||
_LINE_START = re.compile(r"(?:\x1b\[[0-9;]*m)?(?:[EWIDV] \(\d+\) \S|ESP-ROM:)")
|
||||
|
||||
|
||||
def split_glued(text: str) -> list[str]:
|
||||
# Left-to-right scan so an ANSI colour prefix stays with its line.
|
||||
cuts = [m.start() for m in _LINE_START.finditer(text) if m.start() > 0]
|
||||
if not cuts:
|
||||
return [text]
|
||||
bounds = [0, *cuts, len(text)]
|
||||
return [text[a:b] for a, b in zip(bounds, bounds[1:]) if a < b]
|
||||
@@ -33,6 +33,13 @@ class SessionLog:
|
||||
self._fh.write(f"[{ts}] {line.text}\n")
|
||||
self._fh.flush()
|
||||
|
||||
def write_report(self, text: str) -> None:
|
||||
"""Append the diagnosis at the end of the session (for the firmware team)."""
|
||||
if self._fh is None:
|
||||
return
|
||||
self._fh.write("\n===== Diagnosis at end of session =====\n" + text + "\n")
|
||||
self._fh.flush()
|
||||
|
||||
def stop(self) -> None:
|
||||
if self._fh is not None:
|
||||
self._fh.close()
|
||||
|
||||
Reference in new issue
Block a user