From 42f8b844d9330603d86afb84d0634829f7864201 Mon Sep 17 00:00:00 2001 From: Erick Ahmed Date: Wed, 22 Jul 2026 21:01:19 +0200 Subject: [PATCH] Add log visualization tab to dashboard --- logs/logs.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 30 +++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 logs/logs.py diff --git a/logs/logs.py b/logs/logs.py new file mode 100644 index 0000000..387f946 --- /dev/null +++ b/logs/logs.py @@ -0,0 +1,69 @@ +# File: logs.py +# Copyright (C) 2026 Erick Ahmed +# SPDX-License-Identifier: AGPL-3.0-or-later + +import pandas as pd +import plotly.graph_objects as go +from dash import html, dcc + +def render_logs_table(df: pd.DataFrame): + if df is None or df.empty: + return html.Div("No data available") + + 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]] + + display_df = display_df.fillna("") + + max_rows = 1000 + total_rows = len(display_df) + if total_rows > max_rows: + display_df = display_df.iloc[:max_rows] + + fig = go.Figure(data=[go.Table( + header=dict( + values=["" + str(c) + "" for c in display_df.columns], + fill_color='#1a1a1a', + font=dict(color='white', size=12), + align='center' + ), + cells=dict( + values=[display_df[col] for col in display_df.columns], + fill_color='#f8f9fa', + font=dict(color='#2a2a2a', size=11), + align='center' + ) + )]) + + fig.update_layout( + height=800, + margin=dict(l=0, r=0, t=10, b=0) + ) + + return html.Div([ + html.Div(f"Displaying first {len(display_df)} of {total_rows} total frames.", className="text-muted mb-2"), + dcc.Graph(figure=fig, style={'height': '80vh'}) + ]) diff --git a/main.py b/main.py index afd002c..ed62910 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,7 @@ from stats.id_viewer import _format_can_id_vec, plot_bits from stats.frequency import calculate_frequency, plot_frequency from stats.correlation import calculate_correlation, plot_correlation_heatmap from stats.entropy import calculate_byte_entropy, plot_entropy_heatmap +from logs import render_logs_table RAW_LOG_DIR = "data/logs" @@ -128,6 +129,25 @@ app.layout = dbc.Container([ dbc.Tab(label="Overview", tab_id="overview", children=[ 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"), + html.Div(id='logs-table-container') + ]), dbc.Tab(label="Statistics", tab_id="statistics", children=[ dbc.Row([ dbc.Col(html.Label("Select Vehicle:", className="mt-2"), width="auto"), @@ -156,6 +176,16 @@ app.layout = dbc.Container([ ], id="main-tabs", active_tab="statistics") ], fluid=True) +@app.callback( + Output('logs-table-container', 'children'), + Input('logs-vehicle-selector', 'value'), + Input('logs-bus-selector', 'value') +) +def update_logs_table(vehicle, bus): + if not vehicle or not bus or vehicle not in DATA or bus not in DATA[vehicle]: + return html.Div("No data available") + return render_logs_table(DATA[vehicle][bus]) + @app.callback( Output('tab-content', 'children'), Input('tabs', 'active_tab'),