Refactor CANveyor dashboard architecture
- Replace `stats.utils.extractor` with dedicated `loader` and `converter` modules to improve code organization. - Implement explicit pipeline stages for ingestion, decoding, and precomputation with caching. - Standardize data loading and J1939 parsing logic across sub-modules. - Enhance dashboard responsiveness by pre-calculating figures and downsampling ID-grouped data. - Enforce strict typing and add docstrings to public components.
This commit is contained in:
+22
-14
@@ -1,25 +1,30 @@
|
||||
# File: id_viewer.py
|
||||
# File: stats/id_viewer.py
|
||||
# Copyright (C) 2026 Erick Ahmed
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
"""Interactive CAN bus byte-change visualizer."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
from plotly_resampler import FigureResampler
|
||||
from stats.utils.extractor import load_data
|
||||
|
||||
def _format_can_id_vec(s: pd.Series) -> pd.Series:
|
||||
s = s.astype('string').str.strip()
|
||||
s = s.str.replace(r'^0x', '', case=False, regex=True)
|
||||
s = s.str.upper()
|
||||
return s.fillna('UNKNOWN').replace('', 'UNKNOWN')
|
||||
from stats.utils.converter import format_can_id_vec as _format_can_id_vec
|
||||
from stats.utils.loader import load_data
|
||||
|
||||
def prepare_data(df, target_id):
|
||||
_BYTE_COLORS = [
|
||||
'#e41a1c', '#377eb8', '#4daf4a', '#984ea3',
|
||||
'#ff7f00', '#ffff33', '#a65628', '#f781bf',
|
||||
]
|
||||
|
||||
|
||||
def prepare_data(df: pd.DataFrame, target_id: str) -> Tuple[pd.DataFrame, List[str]]:
|
||||
can_id_col = 'ID' if 'ID' in df.columns else 'Identifier'
|
||||
formatted = _format_can_id_vec(df[can_id_col])
|
||||
df = df.assign(Formatted_ID=formatted)
|
||||
df = df.assign(Formatted_ID=_format_can_id_vec(df[can_id_col]))
|
||||
target_id_clean = _format_can_id_vec(pd.Series([target_id])).iloc[0]
|
||||
filtered = df[df['Formatted_ID'] == target_id_clean]
|
||||
|
||||
@@ -29,7 +34,9 @@ def prepare_data(df, target_id):
|
||||
|
||||
for col in byte_cols:
|
||||
if not pd.api.types.is_numeric_dtype(filtered[col]):
|
||||
filtered = filtered.assign(**{col: pd.to_numeric(filtered[col], errors='coerce').astype('float32')})
|
||||
filtered = filtered.assign(
|
||||
**{col: pd.to_numeric(filtered[col], errors='coerce').astype('float32')}
|
||||
)
|
||||
|
||||
filtered = filtered.sort_values('Timestamp', kind='stable')
|
||||
arr = filtered[byte_cols].to_numpy(dtype=np.float32, copy=False)
|
||||
@@ -40,12 +47,12 @@ def prepare_data(df, target_id):
|
||||
|
||||
return filtered, byte_cols
|
||||
|
||||
def plot_bits(df, byte_cols, can_id, title):
|
||||
|
||||
def plot_bits(df: pd.DataFrame, byte_cols: List[str], can_id: str, title: str) -> FigureResampler:
|
||||
fig = FigureResampler(
|
||||
resampled_trace_prefix_suffix=("", ""),
|
||||
show_mean_aggregation_size=False
|
||||
)
|
||||
colors = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628', '#f781bf']
|
||||
n = len(byte_cols)
|
||||
|
||||
x = df['Timestamp'].to_numpy() if not df.empty else np.array([])
|
||||
@@ -54,7 +61,7 @@ def plot_bits(df, byte_cols, can_id, title):
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
mode='lines',
|
||||
line=dict(shape='hv', width=2, color=colors[i % len(colors)]),
|
||||
line=dict(shape='hv', width=2, color=_BYTE_COLORS[i % len(_BYTE_COLORS)]),
|
||||
name=col.upper(),
|
||||
legendgroup=col.upper(),
|
||||
hovertemplate=f"<b>{col.upper()}</b><br>Time: %{{x}}<br>Value: %{{y}}<extra></extra>",
|
||||
@@ -121,6 +128,7 @@ def plot_bits(df, byte_cols, can_id, title):
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Visualize CAN bus byte changes over time")
|
||||
parser.add_argument("input", type=Path, help="Path to the input CAN log file")
|
||||
|
||||
Reference in New Issue
Block a user