From 89a9124a8331cbc6e402f27b2d0dd3a6477b2dba Mon Sep 17 00:00:00 2001 From: Erick Ahmed Date: Thu, 23 Jul 2026 15:37:50 +0200 Subject: [PATCH] Add custom plotting support for vehicle signal - Plot pie chart for engine load state --- main.py | 19 +++++++---- vehicle/komatsu.py | 83 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index 0038cd5..d65b600 100644 --- a/main.py +++ b/main.py @@ -410,12 +410,10 @@ def update_corr(method, target, vehicle, bus, tab): Input('vehicles-vehicle-selector', 'value') ) def render_vehicles(vehicle): - """Render small graph boxes for every decoded signal, combining both buses.""" if not vehicle or vehicle not in DATA: return html.Div("No data available", className="text-muted") brand = VEHICLE_META.get(vehicle, {}).get("brand", "") - vehicle_module = get_vehicle_module(brand) dfs = [] @@ -431,13 +429,22 @@ def render_vehicles(vehicle): df = df.sort_values('Timestamp', kind='stable').reset_index(drop=True) cards = [] + for nid, frame_def in vehicle_module.DECODER_RULES.items(): decoded = vehicle_module.decode_dataframe(df, frame_def.can_id) - for sig in frame_def.signals: - unit_str = f" ({sig.unit})" if sig.unit else "" - title = f"{frame_def.can_id} - {sig.name}{unit_str}" - fig = vehicle_module.plot_signal(decoded, sig.name, title=title, color=frame_def.color) + for item in frame_def.signals: + if hasattr(item, 'plot_func') and callable(item.plot_func): + title = f"{frame_def.can_id} - {item.name}" + fig = item.plot_func(decoded, frame_def.color) + else: + sig = item + if getattr(sig, 'skip_plot', False): + continue + + unit_str = f" ({sig.unit})" if sig.unit else "" + title = f"{frame_def.can_id} - {sig.name}{unit_str}" + fig = vehicle_module.plot_signal(decoded, sig.name, title=title, color=frame_def.color) card = dbc.Card([ dbc.CardBody([ diff --git a/vehicle/komatsu.py b/vehicle/komatsu.py index 121134e..f1ab977 100644 --- a/vehicle/komatsu.py +++ b/vehicle/komatsu.py @@ -2,10 +2,32 @@ # Copyright (C) 2026 Erick Ahmed # SPDX-License-Identifier: AGPL-3.0-or-later +import plotly.express as px +import pandas as pd +from dataclasses import dataclass +from typing import Callable +from copy import deepcopy from vehicle.base import ( SignalDef, FrameDef, normalize_id, decode_dataframe as _decode_dataframe, plot_signal ) +@dataclass +class CustomPlotDef: + name: str + plot_func: Callable + +load_state_sig = SignalDef( + name="Engine Load State", + bit_start=24, + bit_length=8, + factor=1, + offset=0.0, + is_signed=False, + byte_order="big", + unit="", +) +load_state_sig.skip_plot = True + DECODER_RULES = { normalize_id("0x011F"): FrameDef( can_id="0x011F", @@ -36,7 +58,7 @@ DECODER_RULES = { ), normalize_id("0x0CFF3300"): FrameDef( can_id="0x0CFF3300", - description="Engine temperature block", + description="Engine temperature and load state block", color="#0080fe", signals=[ SignalDef( @@ -59,9 +81,66 @@ DECODER_RULES = { byte_order="big", unit="℃", ), + load_state_sig, + CustomPlotDef( + name="Engine Load State", + plot_func=lambda decoded, color: plot_load_state_pie(decoded, color) + ), ], ), } +LOAD_STATE_MAP = { + 0: "Boot up", + 16: "Normal load", + 32: "High load" +} + +def plot_load_state_pie(decoded_df, color): + if decoded_df is None or decoded_df.empty or "Engine Load State" not in decoded_df.columns: + return px.pie(title="No data for Engine Load State") + + states = pd.to_numeric(decoded_df["Engine Load State"], errors='coerce').dropna().astype(int) + + labels = states.map(LOAD_STATE_MAP).fillna("Unknown") + counts = labels.value_counts().reset_index() + counts.columns = ['State', 'Count'] + + total = counts['Count'].sum() + counts['Percentage'] = (counts['Count'] / total * 100).round(1) + counts['Legend'] = counts['State'] + " (" + counts['Percentage'].astype(str) + "%)" + + fig = px.pie( + counts, + values='Count', + names='Legend', + color='State', + title='Engine Load State Distribution', + color_discrete_map={ + "Boot up": "#ff9900", + "Normal load": "#00cc00", + "High load": "#cc0000", + "Unknown": "#808080" + } + ) + + fig.update_traces( + textinfo='none', + hoverinfo='label+percent+value', + domain={'x': [0.05, 0.55], 'y': [0.05, 0.95]} + ) + fig.update_layout( + margin=dict(l=0, r=10, t=40, b=0), + legend=dict(x=0.6, y=0.5) + ) + return fig + def decode_dataframe(df, can_id): - return _decode_dataframe(df, can_id, DECODER_RULES) + filtered_rules = {} + for nid, frame in DECODER_RULES.items(): + filtered_signals = [sig for sig in frame.signals if isinstance(sig, SignalDef)] + new_frame = deepcopy(frame) + new_frame.signals = filtered_signals + filtered_rules[nid] = new_frame + + return _decode_dataframe(df, can_id, filtered_rules)