3 Commits

2 changed files with 178 additions and 2 deletions
+76
View File
@@ -0,0 +1,76 @@
# 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
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': '75vh', '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.Button("Load More", id="load-more-logs-btn", n_clicks=0, style={'display': 'none'}),
html.Div(id='dummy-output', style={'display': 'none'})
])
+102 -2
View File
@@ -8,7 +8,7 @@ from concurrent.futures import ThreadPoolExecutor
import polars as pl
import dash
from dash import dcc, html, Input, Output
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
import numpy as np
@@ -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.view import get_logs_table_component, prepare_logs_data
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"),
get_logs_table_component()
]),
dbc.Tab(label="Statistics", tab_id="statistics", children=[
dbc.Row([
dbc.Col(html.Label("Select Vehicle:", className="mt-2"), width="auto"),
@@ -156,6 +176,86 @@ app.layout = dbc.Container([
], id="main-tabs", active_tab="statistics")
], fluid=True)
app.clientside_callback(
"""
function(data) {
if (!data) return '';
setTimeout(function() {
const btn = document.getElementById('load-more-logs-btn');
const info = document.getElementById('logs-info-text');
if (!btn || !info || info.innerText.toLowerCase().includes('all')) {
return;
}
btn.dataset.loading = "false";
const containers = document.querySelectorAll('.dash-spreadsheet-container, .dash-spreadsheet-inner');
if (containers.length === 0) return;
containers.forEach(container => {
container.onscroll = function() {
if (container.scrollHeight - container.scrollTop - container.clientHeight < 200) {
if (btn && btn.dataset.loading === "false") {
btn.dataset.loading = "true";
btn.click();
}
}
};
if (container.scrollHeight - container.scrollTop - container.clientHeight < 200) {
if (btn && btn.dataset.loading === "false") {
btn.dataset.loading = "true";
btn.click();
}
}
});
}, 200);
return '';
}
""",
Output('dummy-output', 'children'),
Input('logs-table', 'data')
)
@app.callback(
Output('logs-table', 'data'),
Output('logs-table', 'columns'),
Output('logs-info-text', 'children'),
Input('logs-vehicle-selector', 'value'),
Input('logs-bus-selector', 'value'),
Input('load-more-logs-btn', 'n_clicks'),
State('logs-table', 'data'),
)
def update_logs_table(vehicle, bus, n_clicks, current_data):
ctx = dash.callback_context
trigger_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else ''
if not vehicle or not bus or vehicle not in DATA or bus not in DATA[vehicle]:
return [], [], "No data available"
df = DATA[vehicle][bus]
prepared_df = prepare_logs_data(df)
total_rows = len(prepared_df)
columns = [{"name": i, "id": i} for i in prepared_df.columns]
if trigger_id in ['logs-vehicle-selector', 'logs-bus-selector', '']:
current_data = []
offset = len(current_data) if current_data else 0
chunk_size = 1000
if offset >= total_rows:
return current_data, columns, f"Displaying all {total_rows} total frames."
next_chunk = prepared_df.iloc[offset:offset + chunk_size].to_dict('records')
new_data = current_data + next_chunk
new_offset = len(new_data)
info_text = f"Displaying {new_offset} of {total_rows} total frames."
return new_data, columns, info_text
@app.callback(
Output('tab-content', 'children'),
Input('tabs', 'active_tab'),
@@ -259,4 +359,4 @@ def update_corr(method, target, vehicle, bus, tab):
return plot_correlation_heatmap(corr_df, target_id=target_id, title=title)
if __name__ == '__main__':
app.run(debug=False)
app.run(debug=False)