158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
"""
|
||
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))
|