This commit is contained in:
Thaddeus Hughes
2026-03-04 17:41:58 -06:00
parent 6ce5dae3a4
commit e2451fce78
21 changed files with 9013 additions and 7 deletions

157
logtool/cli_view.py Normal file
View File

@@ -0,0 +1,157 @@
"""
CLI table output for SC-F001 logtool.
"""
from parser import LOG_TYPE_BAT, LOG_TYPE_CRASH, LOG_TYPE_BOOT, LOG_TYPE_TIME_SET
try:
from tabulate import tabulate
_TABULATE_OK = True
except ImportError:
_TABULATE_OK = False
_SENSOR_BITS = ['SAFETY', 'JACK', 'DRIVE', 'AUX2']
def _sensor_str(nibble: int) -> str:
active = [_SENSOR_BITS[i] for i in range(4) if (nibble >> i) & 1]
return '+'.join(active) if active else '-'
def _row(e: dict) -> list:
t = e.get('entry_type', -1)
name = e.get('state_name', '?')
if 0 <= t <= 12:
return [
e.get('time_str', ''),
name,
f"{e.get('bat_V', 0):.3f}",
f"{e.get('drive_A', 0):.2f}",
f"{e.get('jack_A', 0):.2f}",
f"{e.get('aux_A', 0):.2f}",
str(e.get('counter', 0)),
_sensor_str(e.get('sensors_stable', 0)),
_sensor_str(e.get('sensors_raw', 0)),
f"{e.get('drive_heat', 0):.1f}",
f"{e.get('jack_heat', 0):.1f}",
f"{e.get('aux_heat', 0):.1f}",
]
elif t == LOG_TYPE_BAT:
return [
e.get('time_str', ''),
'BAT',
f"{e.get('bat_V', 0):.3f}",
'', '', '', '', '', '', '', '', '',
]
elif t == LOG_TYPE_CRASH:
return [
e.get('time_str', ''),
f"*** CRASH: {e.get('reason_str', '?')}",
'', '', '', '', '', '', '', '', '', '',
]
elif t == LOG_TYPE_BOOT:
return [
e.get('time_str', ''),
f"BOOT rst={e.get('reason_str', '?')} wake={e.get('wake_str', '?')}",
'', '', '', '', '', '', '', '', '', '',
]
elif t == LOG_TYPE_TIME_SET:
return [
e.get('time_str', ''),
'TIME_SET',
'', '', '', '', '', '', '', '', '', '',
]
else:
return [
e.get('time_str', ''),
name,
'', '', '', '', '', '', '', '', '', '',
]
_HEADERS = ['Time', 'State', 'Bat(V)', 'Drive(A)', 'Jack(A)', 'Aux(A)',
'Counter', 'Stable', 'Raw', 'DrHeat', 'JkHeat', 'AxHeat']
def print_table(entries: list, type_filter: str = None):
"""Print a tabulate table of log entries to stdout."""
if type_filter:
tf = type_filter.lower()
if tf == 'fsm':
entries = [e for e in entries if 0 <= e.get('entry_type', -1) <= 12]
elif tf == 'bat':
entries = [e for e in entries if e.get('entry_type') == LOG_TYPE_BAT]
elif tf == 'crash':
entries = [e for e in entries if e.get('entry_type') == LOG_TYPE_CRASH]
rows = [_row(e) for e in entries]
if not rows:
print("(no entries)")
return
if _TABULATE_OK:
print(tabulate(rows, headers=_HEADERS, tablefmt='simple'))
else:
# Manual fallback
widths = [max(len(str(r[i])) for r in [_HEADERS] + rows) for i in range(len(_HEADERS))]
fmt = ' '.join(f'{{:<{w}}}' for w in widths)
print(fmt.format(*_HEADERS))
print(' '.join('-' * w for w in widths))
for row in rows:
print(fmt.format(*row))
def print_summary(entries: list):
"""Print a brief summary: time range, entry counts, voltage range."""
if not entries:
print("(empty log)")
return
fsm_entries = [e for e in entries if 0 <= e.get('entry_type', -1) <= 12]
bat_entries = [e for e in entries if e.get('entry_type') == LOG_TYPE_BAT]
crash_entries = [e for e in entries if e.get('entry_type') == LOG_TYPE_CRASH]
boot_entries = [e for e in entries if e.get('entry_type') == LOG_TYPE_BOOT]
time_set_entries = [e for e in entries if e.get('entry_type') == LOG_TYPE_TIME_SET]
all_ts = [e.get('ts_ms', 0) for e in entries if e.get('ts_ms')]
ts_min = min(all_ts) if all_ts else 0
ts_max = max(all_ts) if all_ts else 0
all_bat = [e['bat_V'] for e in entries if 'bat_V' in e]
print(f"Entries : {len(entries)} total "
f"({len(fsm_entries)} FSM, {len(bat_entries)} BAT, "
f"{len(crash_entries)} CRASH, {len(boot_entries)} BOOT, "
f"{len(time_set_entries)} TIME_SET)")
if all_ts:
from parser import _ts_to_str
print(f"Time : {_ts_to_str(ts_min)}{_ts_to_str(ts_max)}")
dur_s = (ts_max - ts_min) / 1000
print(f"Duration: {dur_s:.1f} s ({dur_s/60:.1f} min)")
if all_bat:
print(f"Battery : {min(all_bat):.3f} V {max(all_bat):.3f} V")
if boot_entries:
print(f"\nBOOT events:")
for e in boot_entries:
print(f" {e.get('time_str', '?')} rst={e.get('reason_str', '?')} wake={e.get('wake_str', '?')}")
if crash_entries:
print(f"\nCRASH events:")
for e in crash_entries:
print(f" {e.get('time_str', '?')} reason={e.get('reason_str', '?')}")
if time_set_entries:
print(f"\nTIME_SET events:")
for e in time_set_entries:
print(f" {e.get('time_str', '?')}")
def append_rows(new_entries: list):
"""Print new rows without header (for streaming append mode)."""
for e in new_entries:
row = _row(e)
if _TABULATE_OK:
print(tabulate([row], tablefmt='plain'))
else:
print(' '.join(str(c) for c in row))