Files
SC-F001/logtool/gui_view.py
2026-03-30 11:39:04 -05:00

208 lines
7.4 KiB
Python

"""
Matplotlib GUI for SC-F001 logtool.
"""
from parser import LOG_TYPE_BAT, LOG_TYPE_CRASH, _ts_to_str
try:
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.animation import FuncAnimation
import numpy as np
_MPL_OK = True
except ImportError:
_MPL_OK = False
def _check_mpl():
if not _MPL_OK:
raise ImportError("'matplotlib' and 'numpy' required for GUI mode. Install: pip install matplotlib")
def _entries_to_arrays(entries: list) -> dict:
"""Split entries into typed arrays for plotting."""
fsm = [e for e in entries if 0 <= e.get('entry_type', -1) <= 12]
bat = [e for e in entries if e.get('entry_type') == LOG_TYPE_BAT]
crash = [e for e in entries if e.get('entry_type') == LOG_TYPE_CRASH]
return {'fsm': fsm, 'bat': bat, 'crash': crash}
def _ts_arr(entries, key='ts_ms'):
import numpy as np
return np.array([e.get(key, 0) / 1000.0 for e in entries])
def _val_arr(entries, key):
import numpy as np
return np.array([e.get(key, float('nan')) for e in entries])
def show_plots(entries: list, title: str = "SC-F001 Log"):
_check_mpl()
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np
from datetime import datetime
arrays = _entries_to_arrays(entries)
fsm = arrays['fsm']
bat = arrays['bat']
crash = arrays['crash']
crash_ts = [e.get('ts_ms', 0) / 1000.0 for e in crash]
fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True)
fig.suptitle(title)
def add_crash_lines(ax):
for ts in crash_ts:
ax.axvline(x=datetime.utcfromtimestamp(ts), color='red',
linestyle='--', linewidth=1.0, alpha=0.7)
def to_dt(ts_arr):
return [datetime.utcfromtimestamp(t) for t in ts_arr]
# 1. Battery voltage
ax0 = axes[0]
ax0.set_ylabel('Battery (V)')
all_bat_entries = fsm + bat
all_bat_entries.sort(key=lambda e: e.get('ts_ms', 0))
if all_bat_entries:
ts = to_dt(_ts_arr(all_bat_entries))
vs = _val_arr(all_bat_entries, 'bat_V')
ax0.plot(ts, vs, color='green', linewidth=1)
add_crash_lines(ax0)
ax0.grid(True, alpha=0.3)
# 2. Currents
ax1 = axes[1]
ax1.set_ylabel('Current (A)')
if fsm:
ts = to_dt(_ts_arr(fsm))
ax1.plot(ts, _val_arr(fsm, 'drive_A'), label='Drive', linewidth=1)
ax1.plot(ts, _val_arr(fsm, 'jack_A'), label='Jack', linewidth=1)
ax1.plot(ts, _val_arr(fsm, 'aux_A'), label='Aux', linewidth=1)
ax1.legend(fontsize=8, loc='upper right')
add_crash_lines(ax1)
ax1.grid(True, alpha=0.3)
# 3. FSM state
ax2 = axes[2]
ax2.set_ylabel('FSM State')
if fsm:
ts = to_dt(_ts_arr(fsm))
states = _val_arr(fsm, 'entry_type')
ax2.step(ts, states, where='post', linewidth=1, color='navy')
# y-tick labels: use state names from first entry if available
state_map = {}
for e in fsm:
state_map[e['entry_type']] = e.get('state_name', str(e['entry_type']))
yticks = sorted(state_map.keys())
ax2.set_yticks(yticks)
ax2.set_yticklabels([state_map[k] for k in yticks], fontsize=7)
add_crash_lines(ax2)
ax2.grid(True, alpha=0.3)
# 4. Thermal accumulators
ax3 = axes[3]
ax3.set_ylabel('Heat (I²t)')
if fsm:
ts = to_dt(_ts_arr(fsm))
ax3.plot(ts, _val_arr(fsm, 'drive_heat'), label='Drive', linewidth=1)
ax3.plot(ts, _val_arr(fsm, 'jack_heat'), label='Jack', linewidth=1)
ax3.plot(ts, _val_arr(fsm, 'aux_heat'), label='Aux', linewidth=1)
ax3.legend(fontsize=8, loc='upper right')
add_crash_lines(ax3)
ax3.grid(True, alpha=0.3)
ax3.xaxis.set_major_formatter(mdates.AutoDateFormatter(ax3.xaxis.get_major_locator()))
fig.autofmt_xdate()
plt.tight_layout()
plt.show()
def live_plot(url: str, interval_s: float = 2.0):
"""Live-updating matplotlib plot using FuncAnimation."""
_check_mpl()
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.animation import FuncAnimation
from datetime import datetime
import source as src
import parser as prs
all_entries = []
fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True)
fig.suptitle("SC-F001 Live Log")
labels = ['Battery (V)', 'Current (A)', 'FSM State', 'Heat (I²t)']
for ax, lbl in zip(axes, labels):
ax.set_ylabel(lbl)
ax.grid(True, alpha=0.3)
lines = {
'bat': axes[0].plot([], [], color='green', linewidth=1)[0],
'drive': axes[1].plot([], [], label='Drive', linewidth=1)[0],
'jack': axes[1].plot([], [], label='Jack', linewidth=1)[0],
'aux': axes[1].plot([], [], label='Aux', linewidth=1)[0],
'state': axes[2].step([], [], where='post', linewidth=1, color='navy')[0],
'drheat': axes[3].plot([], [], label='Drive', linewidth=1)[0],
'jkheat': axes[3].plot([], [], label='Jack', linewidth=1)[0],
'axheat': axes[3].plot([], [], label='Aux', linewidth=1)[0],
}
axes[1].legend(fontsize=8, loc='upper right')
axes[3].legend(fontsize=8, loc='upper right')
axes[3].xaxis.set_major_formatter(mdates.AutoDateFormatter(axes[3].xaxis.get_major_locator()))
state = {'current_tail': 0, 'first': True}
def to_dt(ts_list):
return [datetime.utcfromtimestamp(t / 1000.0) for t in ts_list]
def update(_frame):
try:
if state['first']:
blob = src.fetch_full(url)
meta, tail, head, new_entries = prs.autodetect_and_parse(blob)
state['current_tail'] = head or 0
state['first'] = False
else:
binary, new_head = src.fetch_incremental(url, state['current_tail'])
new_entries = prs.parse_entries(binary) if binary else []
state['current_tail'] = new_head
all_entries.extend(new_entries)
except Exception as exc:
print(f"[live_plot] fetch error: {exc}")
return
fsm = [e for e in all_entries if 0 <= e.get('entry_type', -1) <= 12]
bat = [e for e in all_entries if e.get('entry_type') in (LOG_TYPE_BAT,) or 'bat_V' in e]
crash = [e for e in all_entries if e.get('entry_type') == LOG_TYPE_CRASH]
if fsm:
ts = to_dt([e['ts_ms'] for e in fsm])
lines['drive'].set_data(ts, [e.get('drive_A', 0) for e in fsm])
lines['jack'].set_data( ts, [e.get('jack_A', 0) for e in fsm])
lines['aux'].set_data( ts, [e.get('aux_A', 0) for e in fsm])
lines['state'].set_data(ts, [e.get('entry_type', 0) for e in fsm])
lines['drheat'].set_data(ts, [e.get('drive_heat', 0) for e in fsm])
lines['jkheat'].set_data(ts, [e.get('jack_heat', 0) for e in fsm])
lines['axheat'].set_data(ts, [e.get('aux_heat', 0) for e in fsm])
all_bat = sorted(
[e for e in all_entries if 'bat_V' in e],
key=lambda e: e.get('ts_ms', 0)
)
if all_bat:
ts = to_dt([e['ts_ms'] for e in all_bat])
lines['bat'].set_data(ts, [e['bat_V'] for e in all_bat])
for ax in axes:
ax.relim()
ax.autoscale_view()
fig.autofmt_xdate()
ani = FuncAnimation(fig, update, interval=interval_s * 1000, cache_frame_data=False)
plt.tight_layout()
plt.show()