159 lines
6.1 KiB
Python
159 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Iterable, TextIO
|
|
|
|
|
|
DETECT_RE = re.compile(
|
|
r"^(?P<time>\d\d:\d\d:\d\d\.\d{3})\s+DETECT\s+"
|
|
r"(?:(?P<label>\S+)\s+)?(?P<frame>(?:[0-9A-Fa-f]{2}\s*){6})\s*$"
|
|
)
|
|
|
|
DEFAULT_IGNORED_LABELS = {
|
|
"heartbeat",
|
|
"connect_ok_path_response_candidate",
|
|
"connect_c0_path_response_candidate",
|
|
"table_readback_candidate",
|
|
"active_selector0_keepalive_report",
|
|
"gated_active_0004_response_candidate",
|
|
"gated_active_0004_transition_candidate",
|
|
}
|
|
|
|
KNOWN_FRAME_LABELS = {
|
|
"00 00 00 80 80 5A": "active_selector0_keepalive_report",
|
|
"00 00 6C 00 00 36": "copy_completion_exit_selector_006c_candidate",
|
|
"00 00 6D 00 00 37": "copy_in_progress_selector_006d_candidate",
|
|
"00 00 15 80 00 CF": "known_call_button_active_report",
|
|
"00 00 15 00 00 4F": "known_call_button_inactive_report",
|
|
"00 00 07 80 00 DD": "known_cam_power_button_report",
|
|
"00 00 13 00 00 49": "known_iris_mblack_link_clear_report_candidate",
|
|
"00 00 13 40 00 09": "known_iris_mblack_link_active_report_candidate",
|
|
"00 00 13 80 00 C9": "known_selector_0013_bit15_report_candidate",
|
|
"00 00 13 C0 00 89": "known_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
|
|
"01 00 13 00 00 48": "queued_iris_mblack_link_clear_report_candidate",
|
|
"02 00 13 00 00 4B": "queued_iris_mblack_link_clear_report_candidate",
|
|
"01 00 13 40 00 08": "queued_iris_mblack_link_active_report_candidate",
|
|
"02 00 13 40 00 0B": "queued_iris_mblack_link_active_report_candidate",
|
|
"01 00 13 80 00 C8": "queued_selector_0013_bit15_report_candidate",
|
|
"02 00 13 80 00 CB": "queued_selector_0013_bit15_report_candidate",
|
|
"01 00 13 C0 00 88": "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
|
|
"02 00 13 C0 00 8B": "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
|
|
"01 00 17 80 00 CC": "queued_bars_button_selector_0017_active_candidate",
|
|
"02 00 17 80 00 CF": "queued_bars_button_selector_0017_active_candidate",
|
|
"01 00 18 80 00 C3": "queued_bars_button_selector_0018_active_candidate",
|
|
"02 00 18 80 00 C0": "queued_bars_button_selector_0018_active_candidate",
|
|
"01 01 1A 08 00 48": "queued_iris_auto_button_selector_009a_active_candidate",
|
|
"02 01 1A 08 00 4B": "queued_iris_auto_button_selector_009a_active_candidate",
|
|
"01 00 04 00 00 5F": "gated_active_0004_response_candidate",
|
|
"02 00 04 00 00 5C": "gated_active_0004_transition_candidate",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DetectedFrame:
|
|
timestamp: str
|
|
label: str
|
|
frame: str
|
|
line_number: int
|
|
|
|
|
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description="Show unexpected DETECT frames from a serial_scenario capture log."
|
|
)
|
|
parser.add_argument("log", type=Path, help="serial_scenario capture log")
|
|
parser.add_argument(
|
|
"--include-refresh",
|
|
action="store_true",
|
|
help="include heartbeat, table-readback, and CONNECT OK refresh frames",
|
|
)
|
|
parser.add_argument(
|
|
"--show-all",
|
|
action="store_true",
|
|
help="print every matching DETECT frame after the summary",
|
|
)
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
|
|
args = build_arg_parser().parse_args(argv)
|
|
text = args.log.read_text(encoding="utf-8")
|
|
frames = parse_detected_frames(text.splitlines())
|
|
interesting = frames if args.include_refresh else filter_expected_refresh(frames)
|
|
print(format_report(frames, interesting, include_refresh=args.include_refresh, show_all=args.show_all), file=stdout)
|
|
return 0
|
|
|
|
|
|
def parse_detected_frames(lines: Iterable[str]) -> list[DetectedFrame]:
|
|
frames: list[DetectedFrame] = []
|
|
for line_number, line in enumerate(lines, start=1):
|
|
match = DETECT_RE.match(line.strip())
|
|
if not match:
|
|
continue
|
|
frame = " ".join(match.group("frame").upper().split())
|
|
label = KNOWN_FRAME_LABELS.get(frame, match.group("label") or "checksum_ok_unlabeled")
|
|
frames.append(
|
|
DetectedFrame(
|
|
timestamp=match.group("time"),
|
|
label=label,
|
|
frame=frame,
|
|
line_number=line_number,
|
|
)
|
|
)
|
|
return frames
|
|
|
|
|
|
def filter_expected_refresh(frames: Iterable[DetectedFrame]) -> list[DetectedFrame]:
|
|
return [frame for frame in frames if frame.label not in DEFAULT_IGNORED_LABELS]
|
|
|
|
|
|
def format_report(
|
|
frames: list[DetectedFrame],
|
|
interesting: list[DetectedFrame],
|
|
*,
|
|
include_refresh: bool,
|
|
show_all: bool,
|
|
) -> str:
|
|
lines = [
|
|
"Serial scenario unexpected-frame summary",
|
|
f"detected_frames={len(frames)} mode={'all' if include_refresh else 'unexpected-only'}",
|
|
]
|
|
label_counts = Counter(frame.label for frame in frames)
|
|
if label_counts:
|
|
lines.append("labels:")
|
|
for label, count in sorted(label_counts.items()):
|
|
lines.append(f" {label}: {count}")
|
|
ignored = len(frames) - len(interesting)
|
|
if not include_refresh:
|
|
lines.append(f"ignored_expected_refresh={ignored}")
|
|
lines.append(f"interesting_frames={len(interesting)}")
|
|
|
|
frame_counts = Counter((frame.frame, frame.label) for frame in interesting)
|
|
if frame_counts:
|
|
lines.append("interesting frame counts:")
|
|
for (frame_text, label), count in sorted(frame_counts.items(), key=lambda item: (-item[1], item[0][0])):
|
|
first = next(frame for frame in interesting if frame.frame == frame_text and frame.label == label)
|
|
lines.append(f" {count:4d}x {frame_text} label={label} first={first.timestamp} line={first.line_number}")
|
|
else:
|
|
lines.append("no unexpected checksum-valid frames found")
|
|
|
|
if show_all and interesting:
|
|
lines.append("timeline:")
|
|
for frame in interesting:
|
|
lines.append(f" {frame.timestamp} line={frame.line_number} {frame.frame} label={frame.label}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
__all__ = [
|
|
"DetectedFrame",
|
|
"filter_expected_refresh",
|
|
"format_report",
|
|
"main",
|
|
"parse_detected_frames",
|
|
]
|