Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54fd8ce74c | |||
| e0a4d098d9 | |||
| 42f8b844d9 |
@@ -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'})
|
||||
])
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user