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 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,6 +19,7 @@ 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"
@@ -128,6 +129,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 +176,86 @@ app.layout = dbc.Container([
], id="main-tabs", active_tab="statistics") ], id="main-tabs", active_tab="statistics")
], fluid=True) ], 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( @app.callback(
Output('tab-content', 'children'), Output('tab-content', 'children'),
Input('tabs', 'active_tab'), 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) return plot_correlation_heatmap(corr_df, target_id=target_id, title=title)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=False) app.run(debug=False)