7 Commits

4 changed files with 63 additions and 29 deletions
+22 -5
View File
@@ -4,6 +4,7 @@
import os import os
from pathlib import Path from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import polars as pl import polars as pl
import dash import dash
@@ -59,9 +60,10 @@ PRECOMPUTED_FIGURES = {}
DATA_BY_ID = {} DATA_BY_ID = {}
CORR_CACHE = {} CORR_CACHE = {}
for bus, df in DATA.items(): def process_bus_data(bus, df):
PRECOMPUTED_FIGURES[f"{bus}_freq"] = plot_frequency(calculate_frequency(df), title=f"{bus} Frequency") precomp = {}
PRECOMPUTED_FIGURES[f"{bus}_entropy"] = plot_entropy_heatmap(calculate_byte_entropy(df), title=f"{bus} Byte-Level Entropy") precomp[f"{bus}_freq"] = plot_frequency(calculate_frequency(df), title=f"{bus} Frequency")
precomp[f"{bus}_entropy"] = plot_entropy_heatmap(calculate_byte_entropy(df), title=f"{bus} Byte-Level Entropy")
can_id_col = 'ID' if 'ID' in df.columns else 'Identifier' can_id_col = 'ID' if 'ID' in df.columns else 'Identifier'
formatted = _format_can_id_vec(df[can_id_col]) formatted = _format_can_id_vec(df[can_id_col])
@@ -80,13 +82,26 @@ for bus, df in DATA.items():
group = group.iloc[keep] group = group.iloc[keep]
grouped[can_id] = (group, byte_cols) grouped[can_id] = (group, byte_cols)
return precomp, grouped
with ThreadPoolExecutor() as executor:
futures = {executor.submit(process_bus_data, bus, df): bus for bus, df in DATA.items()}
for future in futures:
bus = futures[future]
precomp, grouped = future.result()
PRECOMPUTED_FIGURES.update(precomp)
DATA_BY_ID[bus] = grouped DATA_BY_ID[bus] = grouped
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.config.suppress_callback_exceptions = True app.config.suppress_callback_exceptions = True
app.layout = dbc.Container([ app.layout = dbc.Container([
html.H1("CAN Bus Analyzer", className="my-4"), html.H1("CANveyor", className="my-4"),
dbc.Tabs([
dbc.Tab(label="Overview", tab_id="overview", children=[
html.Div(id="overview-content")
]),
dbc.Tab(label="Statistics", tab_id="statistics", children=[
dbc.Row([ dbc.Row([
dbc.Col(html.Label("Select Bus:"), width=1, className="mt-2"), dbc.Col(html.Label("Select Bus:"), width=1, className="mt-2"),
dbc.Col(dcc.Dropdown( dbc.Col(dcc.Dropdown(
@@ -95,7 +110,7 @@ app.layout = dbc.Container([
value='Bus 1', value='Bus 1',
clearable=False clearable=False
), width=2), ), width=2),
], className="mb-3"), ], className="mb-3 mt-3"),
dbc.Tabs([ dbc.Tabs([
dbc.Tab(label="Frequency", tab_id="freq"), dbc.Tab(label="Frequency", tab_id="freq"),
dbc.Tab(label="ID Viewer", tab_id="id_viewer"), dbc.Tab(label="ID Viewer", tab_id="id_viewer"),
@@ -103,6 +118,8 @@ app.layout = dbc.Container([
dbc.Tab(label="Entropy", tab_id="entropy"), dbc.Tab(label="Entropy", tab_id="entropy"),
], id="tabs", active_tab="freq"), ], id="tabs", active_tab="freq"),
html.Div(id="tab-content", className="mt-3") html.Div(id="tab-content", className="mt-3")
])
], id="main-tabs", active_tab="statistics")
], fluid=True) ], fluid=True)
@app.callback( @app.callback(
+11 -3
View File
@@ -1,7 +1,15 @@
[project] [project]
name = "CANveyor" name = "CANveyor"
version = "0.0.4" version = "0.1.0"
description = "J1939 CAN bus parser that works in pair with CANdigger" description = "J1939 CAN bus parser that works in pair with CANdigger"
readme = "README.md" readme = "README.md"
requires-python = ">=3.14" requires-python = ">=3.10"
dependencies = ["polars", "pathlib", "typing"] dependencies = [
"polars",
"dash",
"dash-bootstrap-components",
"numpy",
"pandas",
"plotly",
"plotly-resampler"
]
+7 -3
View File
@@ -4,6 +4,7 @@
import argparse import argparse
from pathlib import Path from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -69,8 +70,7 @@ def calculate_correlation(df: pd.DataFrame, method: str, target_id: str | None =
else: else:
groups = [] groups = []
out = np.zeros((len(unique_ids), n_cols), dtype=np.float64) def _process_group(sub):
for gi, sub in enumerate(groups):
mask = ~np.isnan(sub).any(axis=1) mask = ~np.isnan(sub).any(axis=1)
sub = sub[mask] sub = sub[mask]
if len(sub) > 1: if len(sub) > 1:
@@ -80,7 +80,11 @@ def calculate_correlation(df: pd.DataFrame, method: str, target_id: str | None =
c = np.abs(np.corrcoef(sub, rowvar=False)) c = np.abs(np.corrcoef(sub, rowvar=False))
np.nan_to_num(c, copy=False, nan=0.0) np.nan_to_num(c, copy=False, nan=0.0)
np.fill_diagonal(c, 0.0) np.fill_diagonal(c, 0.0)
out[gi] = c.max(axis=0) return c.max(axis=0)
return np.zeros(n_cols, dtype=np.float64)
with ThreadPoolExecutor() as executor:
out = np.array(list(executor.map(_process_group, groups)))
result = pd.DataFrame(out, index=unique_ids, columns=available_cols) result = pd.DataFrame(out, index=unique_ids, columns=available_cols)
result.index.name = 'Identifier' result.index.name = 'Identifier'
+8 -3
View File
@@ -4,6 +4,7 @@
import argparse import argparse
from pathlib import Path from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -63,10 +64,14 @@ def calculate_byte_entropy(df: pd.DataFrame) -> pd.DataFrame:
else: else:
groups = [] groups = []
out = np.zeros((len(unique_ids), n_cols), dtype=np.float64) def _process_group(sub):
for gi, sub in enumerate(groups): res = np.zeros(n_cols, dtype=np.float64)
for ci in range(n_cols): for ci in range(n_cols):
out[gi, ci] = _entropy_col(sub[:, ci]) res[ci] = _entropy_col(sub[:, ci])
return res
with ThreadPoolExecutor() as executor:
out = np.array(list(executor.map(_process_group, groups)))
result = pd.DataFrame(out, index=unique_ids, columns=available_cols) result = pd.DataFrame(out, index=unique_ids, columns=available_cols)
result.index.name = 'Identifier' result.index.name = 'Identifier'