logtool
This commit is contained in:
207
logtool/gui_view.py
Normal file
207
logtool/gui_view.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
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.DateFormatter('%H:%M:%S'))
|
||||
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.DateFormatter('%H:%M:%S'))
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user