7 Commits

2 changed files with 217 additions and 1 deletions
+83
View File
@@ -0,0 +1,83 @@
# File: logs/view.py
# Copyright (C) 2026 Erick Ahmed
# SPDX-License-Identifier: AGPL-3.0-or-later
import pandas as pd
from dash import html, dash_table, dcc
import dash_bootstrap_components as dbc
PAGE_SIZE = 25000
def prepare_logs_data(df: pd.DataFrame) -> pd.DataFrame:
if df is None or df.empty:
return pd.DataFrame()
df = df.copy()
if 'j1939_metadata' in df.columns:
df['Priority'] = df['j1939_metadata'].apply(lambda x: x.get('Priority') if isinstance(x, dict) else None)
df['PF'] = df['j1939_metadata'].apply(lambda x: x.get('PF') if isinstance(x, dict) else None)
df['PS'] = df['j1939_metadata'].apply(lambda x: x.get('PS') if isinstance(x, dict) else None)
df['SA'] = df['j1939_metadata'].apply(lambda x: x.get('SA') if isinstance(x, dict) else None)
df['DA'] = df['j1939_metadata'].apply(lambda x: x.get('DA') if isinstance(x, dict) else None)
df['PGN'] = df['j1939_metadata'].apply(lambda x: x.get('PGN') if isinstance(x, dict) else None)
else:
for col in ['Priority', 'PF', 'PS', 'SA', 'DA', 'PGN']:
df[col] = None
for i in range(8):
col = f'b{i}'
if col in df.columns:
df[col] = df[col].apply(lambda x: f"{int(x):02X}" if pd.notna(x) else "")
else:
df[col] = ""
if 'ID' in df.columns:
df['ID'] = df['ID'].astype(str)
display_cols = ['Timestamp', 'ID', 'DLC', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'Priority', 'PF', 'PS', 'SA', 'DA', 'PGN']
display_df = df[[c for c in display_cols if c in df.columns]]
return display_df.fillna("")
def get_logs_table_component():
return html.Div([
html.Div(id='logs-info-text', className="text-muted mb-2"),
dash_table.DataTable(
id='logs-table',
virtualization=True,
page_action='none',
style_table={'overflowX': 'auto', 'height': '70vh', 'overflowY': 'auto'},
style_header={
'backgroundColor': '#1a1a1a',
'color': 'white',
'fontWeight': 'bold',
'textAlign': 'center',
'position': 'sticky',
'top': 0
},
style_data={
'backgroundColor': '#f8f9fa',
'color': '#2a2a2a',
'textAlign': 'center'
},
style_data_conditional=[
{
'if': {'row_index': 'odd'},
'backgroundColor': 'rgb(240, 240, 240)'
}
],
style_cell={
'minWidth': '80px',
'padding': '5px',
'textAlign': 'center',
'fontFamily': 'Segoe UI, Arial, sans-serif'
}
),
html.Div([
dbc.Button("Prev", id='logs-prev-btn', color="secondary", outline=True, size="sm", className="me-2"),
html.Div(id='logs-page-nav', className="d-inline-block", style={'verticalAlign': 'middle'}),
dbc.Button("Next", id='logs-next-btn', color="secondary", outline=True, size="sm", className="ms-2"),
], className="d-flex justify-content-center align-items-center mt-3"),
dcc.Store(id='logs-current-page', data=0),
])
+134 -1
View File
@@ -8,7 +8,7 @@ from concurrent.futures import ThreadPoolExecutor
import polars as pl import polars as pl
import dash import dash
from dash import dcc, html, Input, Output from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
import numpy as np import numpy as np
@@ -19,8 +19,10 @@ from stats.id_viewer import _format_can_id_vec, plot_bits
from stats.frequency import calculate_frequency, plot_frequency from stats.frequency import calculate_frequency, plot_frequency
from stats.correlation import calculate_correlation, plot_correlation_heatmap from stats.correlation import calculate_correlation, plot_correlation_heatmap
from stats.entropy import calculate_byte_entropy, plot_entropy_heatmap from stats.entropy import calculate_byte_entropy, plot_entropy_heatmap
from logs.view import get_logs_table_component, prepare_logs_data
RAW_LOG_DIR = "data/logs" RAW_LOG_DIR = "data/logs"
PAGE_SIZE = 25000
def parse_vehicle_from_filename(filename: str): def parse_vehicle_from_filename(filename: str):
stem = Path(filename).stem stem = Path(filename).stem
@@ -85,6 +87,7 @@ for log_file in Path(RAW_LOG_DIR).glob("*.txt"):
PRECOMPUTED_FIGURES = {} PRECOMPUTED_FIGURES = {}
DATA_BY_ID = {} DATA_BY_ID = {}
CORR_CACHE = {} CORR_CACHE = {}
PREPARED_LOGS_CACHE = {}
def process_bus_data(vehicle, bus, df): def process_bus_data(vehicle, bus, df):
precomp = {} precomp = {}
@@ -128,6 +131,25 @@ app.layout = dbc.Container([
dbc.Tab(label="Overview", tab_id="overview", children=[ dbc.Tab(label="Overview", tab_id="overview", children=[
html.Div(id="overview-content") html.Div(id="overview-content")
]), ]),
dbc.Tab(label="Logs", tab_id="logs", children=[
dbc.Row([
dbc.Col(html.Label("Select Vehicle:", className="mt-2"), width="auto"),
dbc.Col(dcc.Dropdown(
id='logs-vehicle-selector',
options=[{'label': v, 'value': v} for v in DATA.keys()],
value=list(DATA.keys())[0] if DATA else None,
clearable=False
), width=3, className="me-4"),
dbc.Col(html.Label("Select Bus:", className="mt-2"), width="auto"),
dbc.Col(dcc.Dropdown(
id='logs-bus-selector',
options=[{'label': 'Bus 1', 'value': 'Bus 1'}, {'label': 'Bus 2', 'value': 'Bus 2'}],
value='Bus 1',
clearable=False
), width=2),
], className="mb-3 mt-3", align="end"),
get_logs_table_component()
]),
dbc.Tab(label="Statistics", tab_id="statistics", children=[ dbc.Tab(label="Statistics", tab_id="statistics", children=[
dbc.Row([ dbc.Row([
dbc.Col(html.Label("Select Vehicle:", className="mt-2"), width="auto"), dbc.Col(html.Label("Select Vehicle:", className="mt-2"), width="auto"),
@@ -156,6 +178,117 @@ app.layout = dbc.Container([
], id="main-tabs", active_tab="statistics") ], id="main-tabs", active_tab="statistics")
], fluid=True) ], fluid=True)
def get_prepared_logs(vehicle, bus):
cache_key = (vehicle, bus)
if cache_key not in PREPARED_LOGS_CACHE:
df = DATA[vehicle][bus]
PREPARED_LOGS_CACHE[cache_key] = prepare_logs_data(df)
return PREPARED_LOGS_CACHE[cache_key]
def build_page_buttons(current_page: int, total_pages: int, max_buttons: int = 15) -> list:
buttons: list = []
if total_pages <= 1:
return buttons
half = max_buttons // 2
start = max(0, current_page - half)
end = min(total_pages, start + max_buttons)
if end - start < max_buttons:
start = max(0, end - max_buttons)
if start > 0:
buttons.append(
dbc.Button("1", id={'type': 'page-btn', 'index': 0}, color="secondary", outline=True, size="sm", className="me-1")
)
if start > 1:
buttons.append(html.Span("", className="mx-1 align-middle"))
for i in range(start, end):
is_current = (i == current_page)
buttons.append(
dbc.Button(
str(i + 1),
id={'type': 'page-btn', 'index': i},
size="sm",
color="primary" if is_current else "secondary",
outline=not is_current,
className="me-1",
disabled=is_current,
)
)
if end < total_pages:
if end < total_pages - 1:
buttons.append(html.Span("", className="mx-1 align-middle"))
buttons.append(
dbc.Button(
str(total_pages),
id={'type': 'page-btn', 'index': total_pages - 1},
color="secondary", outline=True, size="sm", className="me-1",
)
)
return buttons
@app.callback(
Output('logs-table', 'data'),
Output('logs-table', 'columns'),
Output('logs-info-text', 'children'),
Output('logs-page-nav', 'children'),
Output('logs-current-page', 'data'),
Input('logs-vehicle-selector', 'value'),
Input('logs-bus-selector', 'value'),
Input('logs-prev-btn', 'n_clicks'),
Input('logs-next-btn', 'n_clicks'),
Input({'type': 'page-btn', 'index': dash.ALL}, 'n_clicks'),
State('logs-current-page', 'data'),
)
def update_logs_table(vehicle, bus, prev_clicks, next_clicks, page_btn_clicks, current_page):
if (not vehicle or not bus or vehicle not in DATA or bus not in DATA[vehicle]):
return [], [], "No data available", [], 0
prepared_df = get_prepared_logs(vehicle, bus)
total_rows = len(prepared_df)
if total_rows == 0:
return [], [], "No data available", [], 0
total_pages = max(1, (total_rows + PAGE_SIZE - 1) // PAGE_SIZE)
ctx = dash.callback_context
triggered_id = ctx.triggered_id
current_page = current_page if current_page is not None else 0
if triggered_id in ('logs-vehicle-selector', 'logs-bus-selector'):
current_page = 0
elif triggered_id == 'logs-prev-btn':
current_page = max(0, current_page - 1)
elif triggered_id == 'logs-next-btn':
current_page = current_page + 1
elif isinstance(triggered_id, dict) and triggered_id.get('type') == 'page-btn':
if ctx.triggered and ctx.triggered[0]['value']:
current_page = triggered_id['index']
current_page = max(0, min(current_page, total_pages - 1))
start_idx = current_page * PAGE_SIZE
end_idx = min(start_idx + PAGE_SIZE, total_rows)
page_data = prepared_df.iloc[start_idx:end_idx].to_dict('records')
columns = [{"name": i, "id": i} for i in prepared_df.columns]
info_text = (f"Page {current_page + 1} of {total_pages} | "
f"Showing rows {start_idx + 1:,}{end_idx:,} "
f"of {total_rows:,} total frames")
page_buttons = build_page_buttons(current_page, total_pages)
return page_data, columns, info_text, page_buttons, current_page
@app.callback( @app.callback(
Output('tab-content', 'children'), Output('tab-content', 'children'),
Input('tabs', 'active_tab'), Input('tabs', 'active_tab'),