Added lap recap graphs and loading
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from ast import arg
|
||||
from threading import Thread
|
||||
import dearpygui.dearpygui as dpg
|
||||
from dataflux.state import AppState
|
||||
@@ -16,6 +17,7 @@ from dataflux.tags import (
|
||||
GRAPH_X_AXIS_TENG,
|
||||
GRAPH_X_AXIS_VBAT,
|
||||
WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS,
|
||||
WINDOW_FILE_DIALOG_LOAD_LAP,
|
||||
WINDOW_LORA_CONNECTION_MENU,
|
||||
WINDOW_FILE_DIALOG_DUMP_BUFFERS,
|
||||
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:
|
||||
print("Handling serial window open callback")
|
||||
dataflux.ui.routines.windows.update_window_serial_connection_menu_combo(user_data)
|
||||
print("Combo updated")
|
||||
dpg.show_item(WINDOW_SERIAL_CONNECTION_MENU)
|
||||
print("Window shown")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
dpg.stop_dearpygui()
|
||||
|
||||
@@ -82,6 +85,15 @@ def window_file_dialog_autosave_buffers_ok(
|
||||
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:
|
||||
dataflux.ui.routines.windows.toggle_window(user_data)
|
||||
|
||||
|
||||
@@ -72,14 +72,20 @@ def connect_serial(state: AppState, device: str) -> None:
|
||||
def disconnect_lora(state: AppState) -> None:
|
||||
if state.lora_port is not None:
|
||||
state.lora_thread_running = False
|
||||
state.lora_port.close()
|
||||
try:
|
||||
state.lora_port.close()
|
||||
except OSError:
|
||||
pass
|
||||
state.lora_port = None
|
||||
|
||||
|
||||
def disconnect_serial(state: AppState) -> None:
|
||||
if state.serial_port is not None:
|
||||
state.serial_thread_running = False
|
||||
state.serial_port.close()
|
||||
try:
|
||||
state.serial_port.close()
|
||||
except OSError:
|
||||
pass
|
||||
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):
|
||||
break
|
||||
|
||||
line = port.readline()
|
||||
try:
|
||||
line = port.readline()
|
||||
except TypeError:
|
||||
break
|
||||
except serial.SerialException:
|
||||
break
|
||||
except OSError:
|
||||
break
|
||||
|
||||
if line:
|
||||
text = line.decode("utf-8", errors="replace")
|
||||
@@ -155,8 +168,7 @@ def lora_reader_worker(state: AppState) -> None:
|
||||
state.packet_queue.put(parsed)
|
||||
state.lora_status_queue.put(0.1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Serial parser error: {e}")
|
||||
except Exception:
|
||||
break
|
||||
disconnect_lora(state)
|
||||
dataflux.ui.routines.update_global_connection_status(state)
|
||||
|
||||
@@ -184,6 +184,40 @@ def autosave_worker(state: AppState, path: str) -> None:
|
||||
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:
|
||||
if not values:
|
||||
raise ValueError("Cannot find closest index in an empty list")
|
||||
|
||||
@@ -68,6 +68,9 @@ class AppState:
|
||||
live_buffers_updated: bool = False
|
||||
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)
|
||||
new_laps: Queue = field(default_factory=Queue)
|
||||
laps: list[LapInfo] = field(default_factory=list)
|
||||
@@ -77,4 +80,6 @@ class AppState:
|
||||
autosave_enabled: bool = False
|
||||
autosave_path: Path | None = None
|
||||
|
||||
lap_loader_thread: Thread | None = None
|
||||
|
||||
lock: Lock = field(default_factory=Lock)
|
||||
|
||||
@@ -8,12 +8,14 @@ MENU_IO_DISCONNECT_LORA: str = "menu_io_disconnect_lora"
|
||||
MENU_IO_DISCONNECT_SERIAL: str = "menu_io_disconnect_serial"
|
||||
MENU_FILE_DUMP_BUFFERS: str = "menu_file_dump_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_SERIAL_CONNECTION_MENU: str = "window_serial_connection_menu"
|
||||
WINDOW_LORA_CONNECTION_MENU_COMBO: str = "window_lora_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_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"
|
||||
|
||||
@@ -49,10 +51,22 @@ GRAPH_X_AXIS_SPEED: str = "graph_x_axis_speed"
|
||||
GRAPH_Y_AXIS_SPEED: str = "graph_y_axis_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_Y_AXIS_VBAT: str = "graph_y_axis_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_Y_AXIS_TENG: str = "graph_y_axis_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"
|
||||
|
||||
@@ -24,6 +24,7 @@ def update_window_lora_connection_menu_combo(state: AppState) -> None:
|
||||
if port_name in ports:
|
||||
ports.remove(port_name)
|
||||
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:
|
||||
@@ -34,6 +35,7 @@ def update_window_serial_connection_menu_combo(state: AppState) -> None:
|
||||
if port_name in ports:
|
||||
ports.remove(port_name)
|
||||
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:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from operator import call
|
||||
import dearpygui.dearpygui as dpg
|
||||
import dataflux.callbacks.menu
|
||||
import dataflux.callbacks.serial
|
||||
@@ -11,14 +12,23 @@ from dataflux.tags import (
|
||||
BUTTON_SERIAL_CONSOLE_SEND,
|
||||
CHILD_WINDOW_SERIAL_CONSOLE,
|
||||
GRAPH_SERIES_SPEED,
|
||||
GRAPH_SERIES_SPEED_LR,
|
||||
GRAPH_SERIES_TENG,
|
||||
GRAPH_SERIES_TENG_LR,
|
||||
GRAPH_SERIES_VBAT,
|
||||
GRAPH_SERIES_VBAT_LR,
|
||||
GRAPH_X_AXIS_SPEED,
|
||||
GRAPH_X_AXIS_SPEED_LR,
|
||||
GRAPH_X_AXIS_TENG,
|
||||
GRAPH_X_AXIS_TENG_LR,
|
||||
GRAPH_X_AXIS_VBAT,
|
||||
GRAPH_X_AXIS_VBAT_LR,
|
||||
GRAPH_Y_AXIS_SPEED,
|
||||
GRAPH_Y_AXIS_SPEED_LR,
|
||||
GRAPH_Y_AXIS_TENG,
|
||||
GRAPH_Y_AXIS_TENG_LR,
|
||||
GRAPH_Y_AXIS_VBAT,
|
||||
GRAPH_Y_AXIS_VBAT_LR,
|
||||
INPUT_SERIAL_CONSOLE,
|
||||
LIVE_DATA_TENG_VALUE,
|
||||
LIVE_DATA_UTC_TIME_VALUE,
|
||||
@@ -26,6 +36,7 @@ from dataflux.tags import (
|
||||
LIVE_DATA_VEHICLE_TIME_VALUE,
|
||||
LIVE_DATA_SPEED_VALUE,
|
||||
MENU_FILE_AUTOSAVE_BUFFERS,
|
||||
MENU_FILE_LOAD_LAP,
|
||||
MENU_IO_CONNECT_LORA,
|
||||
MENU_IO_DISCONNECT_LORA,
|
||||
MENU_FILE_DUMP_BUFFERS,
|
||||
@@ -45,6 +56,7 @@ from dataflux.tags import (
|
||||
THEME_STATUS_CONNECTED_BRIGHT,
|
||||
THEME_STATUS_DISCONNECTED,
|
||||
WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS,
|
||||
WINDOW_FILE_DIALOG_LOAD_LAP,
|
||||
WINDOW_LORA_CONNECTION_MENU,
|
||||
WINDOW_LORA_CONNECTION_MENU_COMBO,
|
||||
WINDOW_FILE_DIALOG_DUMP_BUFFERS,
|
||||
@@ -85,6 +97,12 @@ def build_windows(state: AppState) -> None:
|
||||
callback=dataflux.callbacks.menu.menu_file_autosave_buffers,
|
||||
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(
|
||||
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_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.child_window(
|
||||
tag=CHILD_WINDOW_SERIAL_CONSOLE,
|
||||
@@ -452,3 +517,15 @@ def build_windows(state: AppState) -> None:
|
||||
user_data=state,
|
||||
):
|
||||
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")
|
||||
|
||||
@@ -12,13 +12,20 @@ import serial.tools.list_ports
|
||||
from dataflux.state import AppState
|
||||
from dataflux.tags import (
|
||||
GRAPH_SERIES_SPEED,
|
||||
GRAPH_SERIES_SPEED_LR,
|
||||
GRAPH_SERIES_TENG,
|
||||
GRAPH_SERIES_TENG_LR,
|
||||
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_UTC_TIME_VALUE,
|
||||
LIVE_DATA_VBAT_VALUE,
|
||||
LIVE_DATA_VEHICLE_TIME_VALUE,
|
||||
LIVE_DATA_SPEED_VALUE,
|
||||
MENU_FILE_AUTOSAVE_BUFFERS,
|
||||
)
|
||||
from dataflux.ui.routines.serial import append_text_to_console
|
||||
|
||||
@@ -37,10 +44,11 @@ def ui_worker(state: AppState):
|
||||
last_teng: str = ""
|
||||
no_data_written = False
|
||||
while state.running:
|
||||
# if state.autosave_enabled:
|
||||
# dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, True)
|
||||
# else:
|
||||
# dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, False)
|
||||
if state.autosave_enabled:
|
||||
dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, True)
|
||||
else:
|
||||
dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, False)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
formatted = now.strftime("%H:%M:%S")
|
||||
|
||||
@@ -56,6 +64,33 @@ def ui_worker(state: AppState):
|
||||
else:
|
||||
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:
|
||||
x_common: list[float] | None = None
|
||||
speed_y: list[float] | None = None
|
||||
|
||||
Reference in New Issue
Block a user