Added lap recap graphs and loading

This commit is contained in:
2026-05-20 19:30:13 +02:00
parent 91d841cf1c
commit 8f5ecf6d60
8 changed files with 203 additions and 12 deletions

View File

@@ -2,6 +2,7 @@
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com> # Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from ast import arg
from threading import Thread from threading import Thread
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
from dataflux.state import AppState from dataflux.state import AppState
@@ -16,6 +17,7 @@ from dataflux.tags import (
GRAPH_X_AXIS_TENG, GRAPH_X_AXIS_TENG,
GRAPH_X_AXIS_VBAT, GRAPH_X_AXIS_VBAT,
WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS, WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS,
WINDOW_FILE_DIALOG_LOAD_LAP,
WINDOW_LORA_CONNECTION_MENU, WINDOW_LORA_CONNECTION_MENU,
WINDOW_FILE_DIALOG_DUMP_BUFFERS, WINDOW_FILE_DIALOG_DUMP_BUFFERS,
WINDOW_SERIAL_CONNECTION_MENU, WINDOW_SERIAL_CONNECTION_MENU,
@@ -28,11 +30,8 @@ def open_lora_connection_window(sender, app_data, user_data: AppState) -> None:
def open_serial_connection_window(sender, app_data, user_data: AppState) -> None: def open_serial_connection_window(sender, app_data, user_data: AppState) -> None:
print("Handling serial window open callback")
dataflux.ui.routines.windows.update_window_serial_connection_menu_combo(user_data) dataflux.ui.routines.windows.update_window_serial_connection_menu_combo(user_data)
print("Combo updated")
dpg.show_item(WINDOW_SERIAL_CONNECTION_MENU) dpg.show_item(WINDOW_SERIAL_CONNECTION_MENU)
print("Window shown")
def menu_io_disconnect_lora(sender, app_data, user_data: AppState) -> None: def menu_io_disconnect_lora(sender, app_data, user_data: AppState) -> None:
@@ -49,6 +48,10 @@ def menu_file_dump_buffers(sender, app_data, user_data: AppState) -> None:
dpg.show_item(WINDOW_FILE_DIALOG_DUMP_BUFFERS) dpg.show_item(WINDOW_FILE_DIALOG_DUMP_BUFFERS)
def menu_file_load_lap(sender, app_data, user_data: AppState) -> None:
dpg.show_item(WINDOW_FILE_DIALOG_LOAD_LAP)
def menu_file_quit(sender, app_data, user_data) -> None: def menu_file_quit(sender, app_data, user_data) -> None:
dpg.stop_dearpygui() dpg.stop_dearpygui()
@@ -82,6 +85,15 @@ def window_file_dialog_autosave_buffers_ok(
user_data.autosave_buffer_thread.start() user_data.autosave_buffer_thread.start()
def window_file_dialog_load_lap_ok(sender, app_data, user_data: AppState) -> None:
user_data.lap_loader_thread = Thread(
target=dataflux.services.telemetry.lap_load_worker,
args=(user_data, app_data["file_path_name"]),
daemon=True,
)
user_data.lap_loader_thread.start()
def menu_window_select(sender, app_data, user_data: str) -> None: def menu_window_select(sender, app_data, user_data: str) -> None:
dataflux.ui.routines.windows.toggle_window(user_data) dataflux.ui.routines.windows.toggle_window(user_data)

View File

@@ -72,14 +72,20 @@ def connect_serial(state: AppState, device: str) -> None:
def disconnect_lora(state: AppState) -> None: def disconnect_lora(state: AppState) -> None:
if state.lora_port is not None: if state.lora_port is not None:
state.lora_thread_running = False state.lora_thread_running = False
try:
state.lora_port.close() state.lora_port.close()
except OSError:
pass
state.lora_port = None state.lora_port = None
def disconnect_serial(state: AppState) -> None: def disconnect_serial(state: AppState) -> None:
if state.serial_port is not None: if state.serial_port is not None:
state.serial_thread_running = False state.serial_thread_running = False
try:
state.serial_port.close() state.serial_port.close()
except OSError:
pass
state.serial_port = None state.serial_port = None
@@ -105,7 +111,14 @@ def serial_worker(state: AppState) -> None:
if port.port is not None and not os.path.exists(port.port): if port.port is not None and not os.path.exists(port.port):
break break
try:
line = port.readline() line = port.readline()
except TypeError:
break
except serial.SerialException:
break
except OSError:
break
if line: if line:
text = line.decode("utf-8", errors="replace") text = line.decode("utf-8", errors="replace")
@@ -155,8 +168,7 @@ def lora_reader_worker(state: AppState) -> None:
state.packet_queue.put(parsed) state.packet_queue.put(parsed)
state.lora_status_queue.put(0.1) state.lora_status_queue.put(0.1)
except Exception as e: except Exception:
print(f"Serial parser error: {e}")
break break
disconnect_lora(state) disconnect_lora(state)
dataflux.ui.routines.update_global_connection_status(state) dataflux.ui.routines.update_global_connection_status(state)

View File

@@ -184,6 +184,40 @@ def autosave_worker(state: AppState, path: str) -> None:
time.sleep(30) time.sleep(30)
def lap_load_worker(state: AppState, path: str) -> None:
state.lap_recap_buffers.timestamp.clear()
state.lap_recap_buffers.speed.clear()
state.lap_recap_buffers.vbat.clear()
state.lap_recap_buffers.teng.clear()
state.lap_recap_buffers.lat.clear()
state.lap_recap_buffers.lng.clear()
load_path = Path(path)
try:
with load_path.open("r", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
state.lap_recap_buffers.timestamp.append(int(row["timestamp"]))
state.lap_recap_buffers.speed.append(float(row["speed"]))
state.lap_recap_buffers.vbat.append(float(row["vbat"]))
state.lap_recap_buffers.teng.append(float(row["teng"]))
state.lap_recap_buffers.lat.append(float(row["lat"]))
state.lap_recap_buffers.lng.append(float(row["lng"]))
except FileNotFoundError:
pass
except KeyError:
pass
except ValueError:
pass
else:
state.lap_recap_updated = True
state.lap_loader_thread = None
def closest_idx(values: list[int], target: int) -> int: def closest_idx(values: list[int], target: int) -> int:
if not values: if not values:
raise ValueError("Cannot find closest index in an empty list") raise ValueError("Cannot find closest index in an empty list")

View File

@@ -68,6 +68,9 @@ class AppState:
live_buffers_updated: bool = False live_buffers_updated: bool = False
live_buffer_len: int = 30 live_buffer_len: int = 30
lap_recap_buffers: Buffers = field(default_factory=Buffers)
lap_recap_updated: bool = False
lap_lock: Lock = field(default_factory=Lock) lap_lock: Lock = field(default_factory=Lock)
new_laps: Queue = field(default_factory=Queue) new_laps: Queue = field(default_factory=Queue)
laps: list[LapInfo] = field(default_factory=list) laps: list[LapInfo] = field(default_factory=list)
@@ -77,4 +80,6 @@ class AppState:
autosave_enabled: bool = False autosave_enabled: bool = False
autosave_path: Path | None = None autosave_path: Path | None = None
lap_loader_thread: Thread | None = None
lock: Lock = field(default_factory=Lock) lock: Lock = field(default_factory=Lock)

View File

@@ -8,12 +8,14 @@ MENU_IO_DISCONNECT_LORA: str = "menu_io_disconnect_lora"
MENU_IO_DISCONNECT_SERIAL: str = "menu_io_disconnect_serial" MENU_IO_DISCONNECT_SERIAL: str = "menu_io_disconnect_serial"
MENU_FILE_DUMP_BUFFERS: str = "menu_file_dump_buffers" MENU_FILE_DUMP_BUFFERS: str = "menu_file_dump_buffers"
MENU_FILE_AUTOSAVE_BUFFERS: str = "menu_file_autosave_buffers" MENU_FILE_AUTOSAVE_BUFFERS: str = "menu_file_autosave_buffers"
MENU_FILE_LOAD_LAP: str = "menu_file_load_lap"
WINDOW_LORA_CONNECTION_MENU: str = "window_lora_connection_menu" WINDOW_LORA_CONNECTION_MENU: str = "window_lora_connection_menu"
WINDOW_SERIAL_CONNECTION_MENU: str = "window_serial_connection_menu" WINDOW_SERIAL_CONNECTION_MENU: str = "window_serial_connection_menu"
WINDOW_LORA_CONNECTION_MENU_COMBO: str = "window_lora_connection_menu_combo" WINDOW_LORA_CONNECTION_MENU_COMBO: str = "window_lora_connection_menu_combo"
WINDOW_SERIAL_CONNECTION_MENU_COMBO: str = "window_serial_connection_menu_combo" WINDOW_SERIAL_CONNECTION_MENU_COMBO: str = "window_serial_connection_menu_combo"
WINDOW_FILE_DIALOG_DUMP_BUFFERS: str = "window_file_dialog_dump_buffers" WINDOW_FILE_DIALOG_DUMP_BUFFERS: str = "window_file_dialog_dump_buffers"
WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS: str = "window_file_dialog_autosave_buffers" WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS: str = "window_file_dialog_autosave_buffers"
WINDOW_FILE_DIALOG_LOAD_LAP: str = "window_file_dialog_load_lap"
CHILD_WINDOW_SERIAL_CONSOLE: str = "child_window_serial_console" CHILD_WINDOW_SERIAL_CONSOLE: str = "child_window_serial_console"
@@ -49,10 +51,22 @@ GRAPH_X_AXIS_SPEED: str = "graph_x_axis_speed"
GRAPH_Y_AXIS_SPEED: str = "graph_y_axis_speed" GRAPH_Y_AXIS_SPEED: str = "graph_y_axis_speed"
GRAPH_SERIES_SPEED: str = "graph_series_speed" GRAPH_SERIES_SPEED: str = "graph_series_speed"
GRAPH_X_AXIS_SPEED_LR: str = "graph_x_axis_speed_lr"
GRAPH_Y_AXIS_SPEED_LR: str = "graph_y_axis_speed_lr"
GRAPH_SERIES_SPEED_LR: str = "graph_series_speed_lr"
GRAPH_X_AXIS_VBAT: str = "graph_x_axis_vbat" GRAPH_X_AXIS_VBAT: str = "graph_x_axis_vbat"
GRAPH_Y_AXIS_VBAT: str = "graph_y_axis_vbat" GRAPH_Y_AXIS_VBAT: str = "graph_y_axis_vbat"
GRAPH_SERIES_VBAT: str = "graph_series_vbat" GRAPH_SERIES_VBAT: str = "graph_series_vbat"
GRAPH_X_AXIS_VBAT_LR: str = "graph_x_axis_vbat_lr"
GRAPH_Y_AXIS_VBAT_LR: str = "graph_y_axis_vbat_lr"
GRAPH_SERIES_VBAT_LR: str = "graph_series_vbat_lr"
GRAPH_X_AXIS_TENG: str = "graph_x_axis_teng" GRAPH_X_AXIS_TENG: str = "graph_x_axis_teng"
GRAPH_Y_AXIS_TENG: str = "graph_y_axis_teng" GRAPH_Y_AXIS_TENG: str = "graph_y_axis_teng"
GRAPH_SERIES_TENG: str = "graph_series_teng" GRAPH_SERIES_TENG: str = "graph_series_teng"
GRAPH_X_AXIS_TENG_LR: str = "graph_x_axis_teng_lr"
GRAPH_Y_AXIS_TENG_LR: str = "graph_y_axis_teng_lr"
GRAPH_SERIES_TENG_LR: str = "graph_series_teng_lr"

View File

@@ -24,6 +24,7 @@ def update_window_lora_connection_menu_combo(state: AppState) -> None:
if port_name in ports: if port_name in ports:
ports.remove(port_name) ports.remove(port_name)
dpg.configure_item(WINDOW_LORA_CONNECTION_MENU_COMBO, items=ports) dpg.configure_item(WINDOW_LORA_CONNECTION_MENU_COMBO, items=ports)
dpg.set_value(WINDOW_LORA_CONNECTION_MENU_COMBO, "")
def update_window_serial_connection_menu_combo(state: AppState) -> None: def update_window_serial_connection_menu_combo(state: AppState) -> None:
@@ -34,6 +35,7 @@ def update_window_serial_connection_menu_combo(state: AppState) -> None:
if port_name in ports: if port_name in ports:
ports.remove(port_name) ports.remove(port_name)
dpg.configure_item(WINDOW_SERIAL_CONNECTION_MENU_COMBO, items=ports) dpg.configure_item(WINDOW_SERIAL_CONNECTION_MENU_COMBO, items=ports)
dpg.set_value(WINDOW_SERIAL_CONNECTION_MENU_COMBO, "")
def hide_all_but(tag: str) -> None: def hide_all_but(tag: str) -> None:

View File

@@ -2,6 +2,7 @@
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com> # Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from operator import call
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
import dataflux.callbacks.menu import dataflux.callbacks.menu
import dataflux.callbacks.serial import dataflux.callbacks.serial
@@ -11,14 +12,23 @@ from dataflux.tags import (
BUTTON_SERIAL_CONSOLE_SEND, BUTTON_SERIAL_CONSOLE_SEND,
CHILD_WINDOW_SERIAL_CONSOLE, CHILD_WINDOW_SERIAL_CONSOLE,
GRAPH_SERIES_SPEED, GRAPH_SERIES_SPEED,
GRAPH_SERIES_SPEED_LR,
GRAPH_SERIES_TENG, GRAPH_SERIES_TENG,
GRAPH_SERIES_TENG_LR,
GRAPH_SERIES_VBAT, GRAPH_SERIES_VBAT,
GRAPH_SERIES_VBAT_LR,
GRAPH_X_AXIS_SPEED, GRAPH_X_AXIS_SPEED,
GRAPH_X_AXIS_SPEED_LR,
GRAPH_X_AXIS_TENG, GRAPH_X_AXIS_TENG,
GRAPH_X_AXIS_TENG_LR,
GRAPH_X_AXIS_VBAT, GRAPH_X_AXIS_VBAT,
GRAPH_X_AXIS_VBAT_LR,
GRAPH_Y_AXIS_SPEED, GRAPH_Y_AXIS_SPEED,
GRAPH_Y_AXIS_SPEED_LR,
GRAPH_Y_AXIS_TENG, GRAPH_Y_AXIS_TENG,
GRAPH_Y_AXIS_TENG_LR,
GRAPH_Y_AXIS_VBAT, GRAPH_Y_AXIS_VBAT,
GRAPH_Y_AXIS_VBAT_LR,
INPUT_SERIAL_CONSOLE, INPUT_SERIAL_CONSOLE,
LIVE_DATA_TENG_VALUE, LIVE_DATA_TENG_VALUE,
LIVE_DATA_UTC_TIME_VALUE, LIVE_DATA_UTC_TIME_VALUE,
@@ -26,6 +36,7 @@ from dataflux.tags import (
LIVE_DATA_VEHICLE_TIME_VALUE, LIVE_DATA_VEHICLE_TIME_VALUE,
LIVE_DATA_SPEED_VALUE, LIVE_DATA_SPEED_VALUE,
MENU_FILE_AUTOSAVE_BUFFERS, MENU_FILE_AUTOSAVE_BUFFERS,
MENU_FILE_LOAD_LAP,
MENU_IO_CONNECT_LORA, MENU_IO_CONNECT_LORA,
MENU_IO_DISCONNECT_LORA, MENU_IO_DISCONNECT_LORA,
MENU_FILE_DUMP_BUFFERS, MENU_FILE_DUMP_BUFFERS,
@@ -45,6 +56,7 @@ from dataflux.tags import (
THEME_STATUS_CONNECTED_BRIGHT, THEME_STATUS_CONNECTED_BRIGHT,
THEME_STATUS_DISCONNECTED, THEME_STATUS_DISCONNECTED,
WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS, WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS,
WINDOW_FILE_DIALOG_LOAD_LAP,
WINDOW_LORA_CONNECTION_MENU, WINDOW_LORA_CONNECTION_MENU,
WINDOW_LORA_CONNECTION_MENU_COMBO, WINDOW_LORA_CONNECTION_MENU_COMBO,
WINDOW_FILE_DIALOG_DUMP_BUFFERS, WINDOW_FILE_DIALOG_DUMP_BUFFERS,
@@ -85,6 +97,12 @@ def build_windows(state: AppState) -> None:
callback=dataflux.callbacks.menu.menu_file_autosave_buffers, callback=dataflux.callbacks.menu.menu_file_autosave_buffers,
user_data=state, user_data=state,
) )
dpg.add_menu_item(
label="Load Lap",
enabled=True,
tag=MENU_FILE_LOAD_LAP,
callback=dataflux.callbacks.menu.menu_file_load_lap,
)
dpg.add_menu_item( dpg.add_menu_item(
label="Quit", callback=dataflux.callbacks.menu.menu_file_quit label="Quit", callback=dataflux.callbacks.menu.menu_file_quit
) )
@@ -300,6 +318,53 @@ def build_windows(state: AppState) -> None:
dpg.add_text("Lap Recap") dpg.add_text("Lap Recap")
dpg.add_separator() dpg.add_separator()
with dpg.plot(label="Speed", height=250, width=-1, no_inputs=True):
dpg.add_plot_legend()
dpg.add_plot_axis(
dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_SPEED_LR
)
y_axis_speed = dpg.add_plot_axis(
dpg.mvYAxis, label="Speed", tag=GRAPH_Y_AXIS_SPEED_LR
)
dpg.set_axis_limits(GRAPH_Y_AXIS_SPEED_LR, ymin=0, ymax=50)
dpg.set_axis_limits(GRAPH_X_AXIS_SPEED_LR, ymin=-30, ymax=0)
dpg.add_line_series(
[], [], parent=y_axis_speed, tag=GRAPH_SERIES_SPEED_LR
)
with dpg.plot(
label="Battery Voltage",
height=250,
width=-1,
no_inputs=True,
):
dpg.add_plot_legend()
dpg.add_plot_axis(
dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_VBAT_LR
)
y_axis_vbat = dpg.add_plot_axis(
dpg.mvYAxis, label="Voltage", tag=GRAPH_Y_AXIS_VBAT_LR
)
dpg.set_axis_limits(GRAPH_Y_AXIS_VBAT_LR, ymin=0, ymax=20)
dpg.set_axis_limits(GRAPH_X_AXIS_VBAT_LR, ymin=-30, ymax=0)
dpg.add_line_series(
[], [], parent=y_axis_vbat, tag=GRAPH_SERIES_VBAT_LR
)
with dpg.plot(
label="Engine Temp", height=250, width=-1, no_inputs=True
):
dpg.add_plot_legend()
dpg.add_plot_axis(
dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_TENG_LR
)
y_axis_teng = dpg.add_plot_axis(
dpg.mvYAxis, label="Temperature", tag=GRAPH_Y_AXIS_TENG_LR
)
dpg.set_axis_limits(GRAPH_Y_AXIS_TENG_LR, ymin=0, ymax=120)
dpg.set_axis_limits(GRAPH_X_AXIS_TENG_LR, ymin=-30, ymax=0)
dpg.add_line_series(
[], [], parent=y_axis_teng, tag=GRAPH_SERIES_TENG_LR
)
with dpg.group(tag=PAGE_SERIAL_CONSOLE, show=False): with dpg.group(tag=PAGE_SERIAL_CONSOLE, show=False):
with dpg.child_window( with dpg.child_window(
tag=CHILD_WINDOW_SERIAL_CONSOLE, tag=CHILD_WINDOW_SERIAL_CONSOLE,
@@ -452,3 +517,15 @@ def build_windows(state: AppState) -> None:
user_data=state, user_data=state,
): ):
pass pass
with dpg.file_dialog(
directory_selector=False,
show=False,
tag=WINDOW_FILE_DIALOG_LOAD_LAP,
width=700,
height=400,
modal=True,
callback=dataflux.callbacks.menu.window_file_dialog_load_lap_ok,
user_data=state,
):
dpg.add_file_extension(".csv")

View File

@@ -12,13 +12,20 @@ import serial.tools.list_ports
from dataflux.state import AppState from dataflux.state import AppState
from dataflux.tags import ( from dataflux.tags import (
GRAPH_SERIES_SPEED, GRAPH_SERIES_SPEED,
GRAPH_SERIES_SPEED_LR,
GRAPH_SERIES_TENG, GRAPH_SERIES_TENG,
GRAPH_SERIES_TENG_LR,
GRAPH_SERIES_VBAT, GRAPH_SERIES_VBAT,
GRAPH_SERIES_VBAT_LR,
GRAPH_X_AXIS_SPEED_LR,
GRAPH_X_AXIS_TENG_LR,
GRAPH_X_AXIS_VBAT_LR,
LIVE_DATA_TENG_VALUE, LIVE_DATA_TENG_VALUE,
LIVE_DATA_UTC_TIME_VALUE, LIVE_DATA_UTC_TIME_VALUE,
LIVE_DATA_VBAT_VALUE, LIVE_DATA_VBAT_VALUE,
LIVE_DATA_VEHICLE_TIME_VALUE, LIVE_DATA_VEHICLE_TIME_VALUE,
LIVE_DATA_SPEED_VALUE, LIVE_DATA_SPEED_VALUE,
MENU_FILE_AUTOSAVE_BUFFERS,
) )
from dataflux.ui.routines.serial import append_text_to_console from dataflux.ui.routines.serial import append_text_to_console
@@ -37,10 +44,11 @@ def ui_worker(state: AppState):
last_teng: str = "" last_teng: str = ""
no_data_written = False no_data_written = False
while state.running: while state.running:
# if state.autosave_enabled: if state.autosave_enabled:
# dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, True) dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, True)
# else: else:
# dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, False) dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, False)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
formatted = now.strftime("%H:%M:%S") formatted = now.strftime("%H:%M:%S")
@@ -56,6 +64,33 @@ def ui_worker(state: AppState):
else: else:
append_text_to_console(text) append_text_to_console(text)
if state.lap_recap_updated:
state.lap_recap_updated = False
timestamps = state.lap_recap_buffers.timestamp
if timestamps:
t0 = timestamps[0]
x_common = [(t - t0) / 100.0 for t in timestamps]
else:
x_common = []
dpg.set_value(
GRAPH_SERIES_SPEED_LR,
[x_common, state.lap_recap_buffers.speed],
)
dpg.set_value(
GRAPH_SERIES_VBAT_LR,
[x_common, state.lap_recap_buffers.vbat],
)
dpg.set_value(
GRAPH_SERIES_TENG_LR,
[x_common, state.lap_recap_buffers.teng],
)
axis_min = x_common[0]
axis_max = x_common[-1]
dpg.set_axis_limits(GRAPH_X_AXIS_SPEED_LR, ymin=axis_min, ymax=axis_max)
dpg.set_axis_limits(GRAPH_X_AXIS_VBAT_LR, ymin=axis_min, ymax=axis_max)
dpg.set_axis_limits(GRAPH_X_AXIS_TENG_LR, ymin=axis_min, ymax=axis_max)
if state.lora_thread_running and state.telemetry_valid: if state.lora_thread_running and state.telemetry_valid:
x_common: list[float] | None = None x_common: list[float] | None = None
speed_y: list[float] | None = None speed_y: list[float] | None = None