Compare commits
2 Commits
95ba7376c5
...
f857a0a45c
| Author | SHA1 | Date | |
|---|---|---|---|
| f857a0a45c | |||
| 2d631f0669 |
@@ -2,16 +2,28 @@
|
|||||||
# 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 threading import Thread
|
||||||
import dearpygui.dearpygui as dpg
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
from dataflux.state import AppState
|
from dataflux.state import AppState
|
||||||
|
import dataflux.config
|
||||||
import dataflux.ui.windows
|
import dataflux.ui.windows
|
||||||
|
import dataflux.ui.worker
|
||||||
|
import dataflux.services.telemetry
|
||||||
|
|
||||||
def run() -> None:
|
def run() -> None:
|
||||||
state: AppState = AppState()
|
state: AppState = AppState()
|
||||||
|
|
||||||
# Create application context and viewport
|
# Create application context and viewport
|
||||||
dpg.create_context()
|
dpg.create_context()
|
||||||
|
|
||||||
|
width, height, channels, data = dpg.load_image("map.png")
|
||||||
|
dataflux.config.MAP_IMAGE_WIDTH = width
|
||||||
|
dataflux.config.MAP_IMAGE_HEIGHT = height
|
||||||
|
|
||||||
|
with dpg.texture_registry(show=False):
|
||||||
|
dpg.add_static_texture(width=width, height=height, default_value=data, tag="texture_tab")
|
||||||
|
|
||||||
dpg.create_viewport(title='DataFlux', width=600, height=600)
|
dpg.create_viewport(title='DataFlux', width=600, height=600)
|
||||||
|
|
||||||
# Add Inter font to registry and bind as main app font
|
# Add Inter font to registry and bind as main app font
|
||||||
@@ -30,6 +42,13 @@ def run() -> None:
|
|||||||
dpg.configure_item("main_window", pos=(0, 0), width=vp_w, height=vp_h)
|
dpg.configure_item("main_window", pos=(0, 0), width=vp_w, height=vp_h)
|
||||||
dpg.set_primary_window("main_window", True)
|
dpg.set_primary_window("main_window", True)
|
||||||
|
|
||||||
|
state.ui_worker_thread = Thread(target=dataflux.ui.worker.ui_worker, args=(state,), daemon=True)
|
||||||
|
state.ui_worker_thread.start()
|
||||||
|
|
||||||
|
state.telemetry_thread_running = True
|
||||||
|
state.telemetry_thread = Thread(target=dataflux.services.telemetry.telemetry_worker, args=(state, ), daemon=True)
|
||||||
|
state.telemetry_thread.start()
|
||||||
|
|
||||||
dpg.start_dearpygui()
|
dpg.start_dearpygui()
|
||||||
|
|
||||||
dpg.destroy_context()
|
dpg.destroy_context()
|
||||||
@@ -37,4 +56,3 @@ def run() -> None:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
# 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 threading import Thread
|
||||||
import dearpygui.dearpygui as dpg
|
import dearpygui.dearpygui as dpg
|
||||||
|
from dataflux.state import AppState
|
||||||
from dataflux.ui.routines import update_global_connection_status
|
from dataflux.ui.routines import update_global_connection_status
|
||||||
import dataflux.ui.routines.windows
|
import dataflux.ui.routines.windows
|
||||||
import dataflux.ui.routines.status
|
import dataflux.ui.routines.status
|
||||||
import dataflux.services.serial
|
import dataflux.services.serial
|
||||||
|
import dataflux.services.telemetry
|
||||||
|
|
||||||
from dataflux.tags import WINDOW_CONNECTION_MENU
|
from dataflux.tags import WINDOW_CONNECTION_MENU, WINDOW_FILE_DIALOG_DUMP_BUFFERS
|
||||||
|
|
||||||
def open_connection_window(sender, app_data, user_data) -> None:
|
def open_connection_window(sender, app_data, user_data) -> None:
|
||||||
dataflux.ui.routines.windows.update_window_connection_menu_combo()
|
dataflux.ui.routines.windows.update_window_connection_menu_combo()
|
||||||
@@ -17,3 +20,13 @@ def menu_file_disconnect(sender, app_data, user_data) -> None:
|
|||||||
dataflux.services.serial.disconnect_serial(user_data)
|
dataflux.services.serial.disconnect_serial(user_data)
|
||||||
update_global_connection_status(user_data)
|
update_global_connection_status(user_data)
|
||||||
|
|
||||||
|
def menu_file_dump_buffers(sender, app_data, user_data: AppState) -> None:
|
||||||
|
dpg.show_item(WINDOW_FILE_DIALOG_DUMP_BUFFERS)
|
||||||
|
|
||||||
|
def window_file_dialog_dump_buffers_ok(sender, app_data, user_data: AppState) -> None:
|
||||||
|
user_data.buffer_dump_thread = Thread(target=dataflux.services.telemetry.buffer_dump, args=(user_data, app_data["file_path_name"]), daemon=True)
|
||||||
|
user_data.buffer_dump_thread.start()
|
||||||
|
|
||||||
|
def menu_window_select(sender, app_data, user_data: str) -> None:
|
||||||
|
dataflux.ui.routines.windows.toggle_window(user_data)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
MAP_IMAGE_WIDTH: int = 1
|
||||||
|
MAP_IMAGE_HEIGHT: int = 1
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import serial.tools.list_ports
|
|||||||
from dataflux import telemetry_common
|
from dataflux import telemetry_common
|
||||||
import dataflux.telemetry_common.telemetry_common
|
import dataflux.telemetry_common.telemetry_common
|
||||||
import dataflux.ui.routines.status
|
import dataflux.ui.routines.status
|
||||||
|
import dataflux.ui.routines
|
||||||
|
|
||||||
from dataflux.state import AppState
|
from dataflux.state import AppState
|
||||||
|
|
||||||
@@ -58,6 +59,9 @@ def serial_reader_worker(state: AppState) -> None:
|
|||||||
port = state.serial_port
|
port = state.serial_port
|
||||||
if port is None:
|
if port is None:
|
||||||
break
|
break
|
||||||
|
if port.closed:
|
||||||
|
print("Port closed")
|
||||||
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
packet = read_one_uart_packet(port)
|
packet = read_one_uart_packet(port)
|
||||||
@@ -68,10 +72,13 @@ def serial_reader_worker(state: AppState) -> None:
|
|||||||
if parsed is not None:
|
if parsed is not None:
|
||||||
state.packet_queue.put(parsed)
|
state.packet_queue.put(parsed)
|
||||||
state.serial_status_queue.put(0.1)
|
state.serial_status_queue.put(0.1)
|
||||||
print(parsed)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Serial parser error: {e}")
|
print(f"Serial parser error: {e}")
|
||||||
|
break
|
||||||
|
disconnect_serial(state)
|
||||||
|
dataflux.ui.routines.update_global_connection_status(state)
|
||||||
|
|
||||||
|
|
||||||
def read_one_uart_packet(port: Serial) -> bytes | None:
|
def read_one_uart_packet(port: Serial) -> bytes | None:
|
||||||
first = port.read(1)
|
first = port.read(1)
|
||||||
|
|||||||
94
src/dataflux/services/telemetry/__init__.py
Normal file
94
src/dataflux/services/telemetry/__init__.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from queue import Empty
|
||||||
|
from dataflux.state import AppState, Buffers
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
import csv
|
||||||
|
|
||||||
|
LIVE_BUFFER_WINDOW_CS = 30 * 100
|
||||||
|
|
||||||
|
def telemetry_worker(state: AppState):
|
||||||
|
while state.telemetry_thread_running:
|
||||||
|
if state.serial_thread_running == False:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
dataframe = state.packet_queue.get(timeout=0.1)
|
||||||
|
except Empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
state.latest_telemetry = dataframe
|
||||||
|
state.telemetry_valid = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
with state.lock:
|
||||||
|
state.raw_buffers.timestamp.append(dataframe["time_stamp"])
|
||||||
|
state.raw_buffers.speed.append(dataframe["speed"])
|
||||||
|
state.raw_buffers.vbat.append(dataframe["vbat"])
|
||||||
|
state.raw_buffers.teng.append(dataframe["teng"])
|
||||||
|
state.raw_buffers.lat.append(dataframe["lat"])
|
||||||
|
state.raw_buffers.lng.append(dataframe["lng"])
|
||||||
|
|
||||||
|
state.live_buffers_updated = True
|
||||||
|
state.live_buffers.timestamp.clear()
|
||||||
|
state.live_buffers.speed.clear()
|
||||||
|
state.live_buffers.vbat.clear()
|
||||||
|
state.live_buffers.teng.clear()
|
||||||
|
state.live_buffers.lat.clear()
|
||||||
|
state.live_buffers.lng.clear()
|
||||||
|
|
||||||
|
if not state.raw_buffers.timestamp:
|
||||||
|
return
|
||||||
|
|
||||||
|
last_timestamp = state.raw_buffers.timestamp[-1]
|
||||||
|
cutoff = last_timestamp - LIVE_BUFFER_WINDOW_CS
|
||||||
|
|
||||||
|
i = len(state.raw_buffers.timestamp) - 1
|
||||||
|
|
||||||
|
while i >= 0 and state.raw_buffers.timestamp[i] >= cutoff:
|
||||||
|
elapsed_seconds = (state.raw_buffers.timestamp[i] - last_timestamp) / 100.0
|
||||||
|
state.live_buffers.timestamp.append(elapsed_seconds)
|
||||||
|
state.live_buffers.speed.append(state.raw_buffers.speed[i])
|
||||||
|
state.live_buffers.vbat.append(state.raw_buffers.vbat[i])
|
||||||
|
state.live_buffers.teng.append(state.raw_buffers.teng[i])
|
||||||
|
state.live_buffers.lat.append(state.raw_buffers.lat[i])
|
||||||
|
state.live_buffers.lng.append(state.raw_buffers.lng[i])
|
||||||
|
i -= 1
|
||||||
|
|
||||||
|
state.live_buffers.timestamp.reverse()
|
||||||
|
state.live_buffers.speed.reverse()
|
||||||
|
state.live_buffers.vbat.reverse()
|
||||||
|
state.live_buffers.teng.reverse()
|
||||||
|
state.live_buffers.lat.reverse()
|
||||||
|
state.live_buffers.lng.reverse()
|
||||||
|
|
||||||
|
def buffer_dump(state: AppState, path: str):
|
||||||
|
save_path = Path(path)
|
||||||
|
if save_path.is_dir():
|
||||||
|
save_path = save_path / "output.csv"
|
||||||
|
|
||||||
|
with state.lock:
|
||||||
|
local_raw_buffers = Buffers(
|
||||||
|
timestamp=list(state.raw_buffers.timestamp),
|
||||||
|
speed=list(state.raw_buffers.speed),
|
||||||
|
vbat=list(state.raw_buffers.vbat),
|
||||||
|
teng=list(state.raw_buffers.teng),
|
||||||
|
lat=list(state.raw_buffers.lat),
|
||||||
|
lng=list(state.raw_buffers.lng),
|
||||||
|
)
|
||||||
|
|
||||||
|
with save_path.open("w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
|
||||||
|
writer.writerow(["timestamp", "speed", "vbat", "teng", "lat", "lng"])
|
||||||
|
|
||||||
|
for row in zip(local_raw_buffers.timestamp, local_raw_buffers.speed, local_raw_buffers.vbat, local_raw_buffers.teng, local_raw_buffers.lat, local_raw_buffers.lng):
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
state.buffer_dump_thread = None
|
||||||
|
|
||||||
|
|
||||||
@@ -7,6 +7,15 @@ from threading import Lock, Thread
|
|||||||
from serial import Serial
|
from serial import Serial
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Buffers:
|
||||||
|
timestamp: list[int] = field(default_factory=list)
|
||||||
|
speed: list[float] = field(default_factory=list)
|
||||||
|
vbat: list[float] = field(default_factory=list)
|
||||||
|
teng: list[float] = field(default_factory=list)
|
||||||
|
lat: list[float] = field(default_factory=list)
|
||||||
|
lng: list[float] = field(default_factory=list)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AppState:
|
class AppState:
|
||||||
running: bool = True
|
running: bool = True
|
||||||
@@ -16,11 +25,21 @@ class AppState:
|
|||||||
serial_thread_running: bool = False
|
serial_thread_running: bool = False
|
||||||
|
|
||||||
telemetry_thread: Thread | None = None
|
telemetry_thread: Thread | None = None
|
||||||
|
telemetry_thread_running: bool = False
|
||||||
|
|
||||||
serial_status_thread: Thread | None = None
|
serial_status_thread: Thread | None = None
|
||||||
serial_status_queue: Queue = field(default_factory=Queue)
|
serial_status_queue: Queue = field(default_factory=Queue)
|
||||||
|
|
||||||
|
ui_worker_thread: Thread | None = None
|
||||||
|
|
||||||
packet_queue: Queue = field(default_factory=Queue)
|
packet_queue: Queue = field(default_factory=Queue)
|
||||||
latest_telemetry: dict = field(default_factory=dict)
|
latest_telemetry: dict = field(default_factory=dict)
|
||||||
|
telemetry_valid: bool = False
|
||||||
|
|
||||||
|
raw_buffers: Buffers = field(default_factory=Buffers)
|
||||||
|
live_buffers: Buffers = field(default_factory=Buffers)
|
||||||
|
live_buffers_updated: bool = False
|
||||||
|
|
||||||
|
buffer_dump_thread: Thread | None = None
|
||||||
|
|
||||||
lock: Lock = field(default_factory=Lock)
|
lock: Lock = field(default_factory=Lock)
|
||||||
|
|||||||
@@ -4,12 +4,38 @@
|
|||||||
|
|
||||||
MENU_FILE_CONNECT: str = "menu_file_connect"
|
MENU_FILE_CONNECT: str = "menu_file_connect"
|
||||||
MENU_FILE_DISCONNECT: str = "menu_file_disconnect"
|
MENU_FILE_DISCONNECT: str = "menu_file_disconnect"
|
||||||
|
MENU_FILE_DUMP_BUFFERS: str = "menu_file_dump_buffers"
|
||||||
WINDOW_CONNECTION_MENU: str = "window_connection_menu"
|
WINDOW_CONNECTION_MENU: str = "window_connection_menu"
|
||||||
WINDOW_CONNECTION_MENU_COMBO: str = "window_connection_menu_combo"
|
WINDOW_CONNECTION_MENU_COMBO: str = "window_connection_menu_combo"
|
||||||
|
WINDOW_FILE_DIALOG_DUMP_BUFFERS: str = "window_file_dialog_dump_buffers"
|
||||||
|
|
||||||
STATUS_SERIAL_STATUS_BOX: str = "status_serial_status_box"
|
STATUS_SERIAL_STATUS_BOX: str = "status_serial_status_box"
|
||||||
STATUS_SERIAL_STATUS_TEXT: str = "status_serial_status_text"
|
STATUS_SERIAL_STATUS_TEXT: str = "status_serial_status_text"
|
||||||
|
|
||||||
|
LIVE_DATA_UTC_TIME_VALUE: str = "live_data_utc_time_value"
|
||||||
|
LIVE_DATA_VEHICLE_TIME_VALUE: str = "live_data_vehicle_time_value"
|
||||||
|
LIVE_DATA_SPEED_VALUE: str = "live_data_speed_value"
|
||||||
|
LIVE_DATA_VBAT_VALUE: str = "live_data_vbat_value"
|
||||||
|
LIVE_DATA_TENG_VALUE: str = "live_data_teng_value"
|
||||||
|
|
||||||
|
PAGE_LIVE_DATA: str = "page_live_data"
|
||||||
|
PAGE_LAP_RECAP: str = "page_lap_recap"
|
||||||
|
|
||||||
|
SUB_PAGE_DATA_GRAPHS: str = "sub_page_data_graphs"
|
||||||
|
SUB_PAGE_MAP: str = "sub_page_map"
|
||||||
|
|
||||||
THEME_STATUS_DISCONNECTED: str = "theme_status_disconnected"
|
THEME_STATUS_DISCONNECTED: str = "theme_status_disconnected"
|
||||||
THEME_STATUS_CONNECTED: str = "theme_status_connected"
|
THEME_STATUS_CONNECTED: str = "theme_status_connected"
|
||||||
THEME_STATUS_CONNECTED_BRIGHT: str = "theme_status_connected_bigght"
|
THEME_STATUS_CONNECTED_BRIGHT: str = "theme_status_connected_bigght"
|
||||||
|
|
||||||
|
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_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_TENG: str = "graph_x_axis_teng"
|
||||||
|
GRAPH_Y_AXIS_TENG: str = "graph_y_axis_teng"
|
||||||
|
GRAPH_SERIES_TENG: str = "graph_series_teng"
|
||||||
|
|||||||
@@ -3,9 +3,34 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
import dearpygui.dearpygui as dpg
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
|
import dataflux.config
|
||||||
from dataflux.services.serial import list_serial_ports
|
from dataflux.services.serial import list_serial_ports
|
||||||
from dataflux.tags import WINDOW_CONNECTION_MENU_COMBO
|
from dataflux.tags import PAGE_LAP_RECAP, PAGE_LIVE_DATA, SUB_PAGE_DATA_GRAPHS, SUB_PAGE_MAP, WINDOW_CONNECTION_MENU_COMBO
|
||||||
|
|
||||||
def update_window_connection_menu_combo() -> None:
|
def update_window_connection_menu_combo() -> None:
|
||||||
ports: list[str] = list_serial_ports()
|
ports: list[str] = list_serial_ports()
|
||||||
dpg.configure_item(WINDOW_CONNECTION_MENU_COMBO, items=ports)
|
dpg.configure_item(WINDOW_CONNECTION_MENU_COMBO, items=ports)
|
||||||
|
|
||||||
|
|
||||||
|
def hide_all_but(tag: str) -> None:
|
||||||
|
arr = [PAGE_LIVE_DATA, PAGE_LAP_RECAP]
|
||||||
|
for item in arr:
|
||||||
|
if tag == item:
|
||||||
|
dpg.show_item(item)
|
||||||
|
else:
|
||||||
|
dpg.hide_item(item)
|
||||||
|
|
||||||
|
def toggle_window(tag: str) -> None:
|
||||||
|
if tag == SUB_PAGE_DATA_GRAPHS:
|
||||||
|
dpg.show_item(SUB_PAGE_DATA_GRAPHS)
|
||||||
|
dpg.hide_item(SUB_PAGE_MAP)
|
||||||
|
hide_all_but(PAGE_LIVE_DATA)
|
||||||
|
elif tag == SUB_PAGE_MAP:
|
||||||
|
dpg.show_item(SUB_PAGE_MAP)
|
||||||
|
dpg.hide_item(SUB_PAGE_DATA_GRAPHS)
|
||||||
|
hide_all_but(PAGE_LIVE_DATA)
|
||||||
|
else:
|
||||||
|
hide_all_but(tag)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,18 @@ import dataflux.callbacks.menu
|
|||||||
import dataflux.callbacks.serial
|
import dataflux.callbacks.serial
|
||||||
|
|
||||||
from dataflux.state import AppState
|
from dataflux.state import AppState
|
||||||
from dataflux.tags import MENU_FILE_CONNECT, MENU_FILE_DISCONNECT, STATUS_SERIAL_STATUS_BOX, STATUS_SERIAL_STATUS_TEXT, THEME_STATUS_CONNECTED, THEME_STATUS_CONNECTED_BRIGHT, THEME_STATUS_DISCONNECTED, WINDOW_CONNECTION_MENU, WINDOW_CONNECTION_MENU_COMBO
|
from dataflux.tags import GRAPH_SERIES_SPEED, GRAPH_SERIES_TENG, GRAPH_SERIES_VBAT, GRAPH_X_AXIS_SPEED, GRAPH_X_AXIS_TENG, GRAPH_X_AXIS_VBAT, GRAPH_Y_AXIS_SPEED, GRAPH_Y_AXIS_TENG, GRAPH_Y_AXIS_VBAT, LIVE_DATA_TENG_VALUE, LIVE_DATA_UTC_TIME_VALUE, LIVE_DATA_VBAT_VALUE, LIVE_DATA_VEHICLE_TIME_VALUE, LIVE_DATA_SPEED_VALUE, MENU_FILE_CONNECT, MENU_FILE_DISCONNECT, MENU_FILE_DUMP_BUFFERS, PAGE_LAP_RECAP, PAGE_LIVE_DATA, STATUS_SERIAL_STATUS_BOX, STATUS_SERIAL_STATUS_TEXT, SUB_PAGE_DATA_GRAPHS, SUB_PAGE_MAP, THEME_STATUS_CONNECTED, THEME_STATUS_CONNECTED_BRIGHT, THEME_STATUS_DISCONNECTED, WINDOW_CONNECTION_MENU, WINDOW_CONNECTION_MENU_COMBO, WINDOW_FILE_DIALOG_DUMP_BUFFERS
|
||||||
from dataflux.ui.colors import STATUS_GREEN_BRIGHT, STATUS_GREEN_DARK, STATUS_RED_DARK
|
from dataflux.ui.colors import STATUS_GREEN_BRIGHT, STATUS_GREEN_DARK, STATUS_RED_DARK
|
||||||
|
|
||||||
|
def _add_live_data_row(label: str, value: str, tag: str, units: str) -> None:
|
||||||
|
with dpg.table_row():
|
||||||
|
with dpg.table_cell():
|
||||||
|
dpg.add_text(label)
|
||||||
|
with dpg.table_cell():
|
||||||
|
dpg.add_text(value, tag=tag)
|
||||||
|
with dpg.table_cell():
|
||||||
|
dpg.add_text(units)
|
||||||
|
|
||||||
def build_windows(state: AppState) -> None:
|
def build_windows(state: AppState) -> None:
|
||||||
|
|
||||||
with dpg.window(label='DataFlux',tag="main_window", no_collapse=True):
|
with dpg.window(label='DataFlux',tag="main_window", no_collapse=True):
|
||||||
@@ -17,26 +26,56 @@ def build_windows(state: AppState) -> None:
|
|||||||
with dpg.menu(label='File'):
|
with dpg.menu(label='File'):
|
||||||
dpg.add_menu_item(label="Connect", enabled=True, tag=MENU_FILE_CONNECT, callback=dataflux.callbacks.menu.open_connection_window)
|
dpg.add_menu_item(label="Connect", enabled=True, tag=MENU_FILE_CONNECT, callback=dataflux.callbacks.menu.open_connection_window)
|
||||||
dpg.add_menu_item(label="Disonnect", enabled=False, tag=MENU_FILE_DISCONNECT, callback=dataflux.callbacks.menu.menu_file_disconnect, user_data=state)
|
dpg.add_menu_item(label="Disonnect", enabled=False, tag=MENU_FILE_DISCONNECT, callback=dataflux.callbacks.menu.menu_file_disconnect, user_data=state)
|
||||||
|
dpg.add_menu_item(label="Dump Buffers", enabled=True, tag=MENU_FILE_DUMP_BUFFERS, callback=dataflux.callbacks.menu.menu_file_dump_buffers)
|
||||||
dpg.add_menu_item(label="Quit")
|
dpg.add_menu_item(label="Quit")
|
||||||
with dpg.menu(label='Window'):
|
with dpg.menu(label='Window'):
|
||||||
dpg.add_menu_item(label="Live Data", user_data="page_live_data")
|
dpg.add_menu_item(label="Live Graphs", user_data=SUB_PAGE_DATA_GRAPHS, callback=dataflux.callbacks.menu.menu_window_select)
|
||||||
dpg.add_menu_item(label="Lap Recap", user_data="page_lap_recap")
|
dpg.add_menu_item(label="Live Map", user_data=SUB_PAGE_MAP, callback=dataflux.callbacks.menu.menu_window_select )
|
||||||
|
dpg.add_menu_item(label="Lap Recap", user_data=PAGE_LAP_RECAP, callback=dataflux.callbacks.menu.menu_window_select)
|
||||||
|
|
||||||
with dpg.child_window(tag="content_area", autosize_x=True, height=-32, border=False):
|
with dpg.child_window(tag="content_area", autosize_x=True, height=-32, border=False):
|
||||||
with dpg.group(tag="page_live_data", show=True):
|
with dpg.group(tag=PAGE_LIVE_DATA, show=True):
|
||||||
with dpg.group(horizontal=True):
|
with dpg.group(horizontal=True):
|
||||||
with dpg.child_window(tag="realtime_stats", width=250, autosize_y=True, border=True):
|
with dpg.child_window(tag="realtime_stats", width=260, autosize_y=True, border=True):
|
||||||
dpg.add_text("Speed: 25kmh")
|
with dpg.table(header_row=False, resizable=False, policy=dpg.mvTable_SizingFixedFit, borders_innerH=False, borders_innerV=False, borders_outerH=False, borders_outerV=False, no_host_extendX=True):
|
||||||
|
dpg.add_table_column(width_fixed=True)
|
||||||
|
dpg.add_table_column(width_stretch=True, init_width_or_weight=1.0)
|
||||||
|
dpg.add_table_column(width_fixed=True)
|
||||||
|
_add_live_data_row("UTC Time", "no_data", LIVE_DATA_UTC_TIME_VALUE,"")
|
||||||
|
_add_live_data_row("Vehicle Time", "no_data", LIVE_DATA_VEHICLE_TIME_VALUE,"")
|
||||||
|
_add_live_data_row("Speed", "no_data", LIVE_DATA_SPEED_VALUE,"km/h")
|
||||||
|
_add_live_data_row("Battery Voltage", "no_data", LIVE_DATA_VBAT_VALUE,"V")
|
||||||
|
_add_live_data_row("Engine Temp", "no_data", LIVE_DATA_TENG_VALUE,"°C")
|
||||||
|
|
||||||
with dpg.child_window(tag="data_graphs", autosize_x=True, autosize_y=True, border=True):
|
with dpg.child_window(tag=SUB_PAGE_DATA_GRAPHS, autosize_x=True, autosize_y=True, border=True, show=True):
|
||||||
with dpg.plot(label="Speed", height=250, width=-1, no_inputs=True):
|
with dpg.plot(label="Speed", height=250, width=-1, no_inputs=True):
|
||||||
dpg.add_plot_legend()
|
dpg.add_plot_legend()
|
||||||
dpg.add_plot_axis(dpg.mvXAxis, label="Time", tag="x_axis_speed")
|
dpg.add_plot_axis(dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_SPEED)
|
||||||
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Speed", tag="y_axis_speed")
|
y_axis_speed = dpg.add_plot_axis(dpg.mvYAxis, label="Speed", tag=GRAPH_Y_AXIS_SPEED)
|
||||||
dpg.set_axis_limits("y_axis_speed", 0, 50)
|
dpg.set_axis_limits(GRAPH_Y_AXIS_SPEED, ymin=0, ymax=50)
|
||||||
dpg.add_line_series([], [], parent=y_axis, tag="speed_series")
|
dpg.set_axis_limits(GRAPH_X_AXIS_SPEED, ymin=-30, ymax=0)
|
||||||
|
dpg.add_line_series([], [], parent=y_axis_speed, tag=GRAPH_SERIES_SPEED)
|
||||||
|
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)
|
||||||
|
y_axis_vbat = dpg.add_plot_axis(dpg.mvYAxis, label="Voltage", tag=GRAPH_Y_AXIS_VBAT)
|
||||||
|
dpg.set_axis_limits(GRAPH_Y_AXIS_VBAT, ymin=0, ymax=20)
|
||||||
|
dpg.set_axis_limits(GRAPH_X_AXIS_VBAT, ymin=-30, ymax=0)
|
||||||
|
dpg.add_line_series([], [], parent=y_axis_vbat, tag=GRAPH_SERIES_VBAT)
|
||||||
|
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)
|
||||||
|
y_axis_teng = dpg.add_plot_axis(dpg.mvYAxis, label="Temperature", tag=GRAPH_Y_AXIS_TENG)
|
||||||
|
dpg.set_axis_limits(GRAPH_Y_AXIS_TENG, ymin=0, ymax=120)
|
||||||
|
dpg.set_axis_limits(GRAPH_X_AXIS_TENG, ymin=-30, ymax=0)
|
||||||
|
dpg.add_line_series([], [], parent=y_axis_teng, tag=GRAPH_SERIES_TENG)
|
||||||
|
|
||||||
with dpg.group(tag="page_lap_recap", show=False):
|
with dpg.child_window(tag=SUB_PAGE_MAP, autosize_x=True, autosize_y=True, border=True, show=False, no_scrollbar=True):
|
||||||
|
with dpg.drawlist(width=500, height=500, tag="map_drawlist"):
|
||||||
|
dpg.draw_image("texture_tab", (0, 0), (500, 500))
|
||||||
|
dpg.draw_circle((0, 0), 10, color=(255, 0, 0, 255), fill=(255, 0, 0, 255))
|
||||||
|
|
||||||
|
with dpg.group(tag=PAGE_LAP_RECAP, show=False):
|
||||||
dpg.add_text("Lap Recap")
|
dpg.add_text("Lap Recap")
|
||||||
dpg.add_separator()
|
dpg.add_separator()
|
||||||
|
|
||||||
@@ -74,3 +113,15 @@ def build_windows(state: AppState) -> None:
|
|||||||
with dpg.window(label="Connection Menu", tag=WINDOW_CONNECTION_MENU, show=False, modal=True, no_collapse=True, width=300):
|
with dpg.window(label="Connection Menu", tag=WINDOW_CONNECTION_MENU, show=False, modal=True, no_collapse=True, width=300):
|
||||||
dpg.add_combo([], tag=WINDOW_CONNECTION_MENU_COMBO)
|
dpg.add_combo([], tag=WINDOW_CONNECTION_MENU_COMBO)
|
||||||
dpg.add_button(label="Connect", callback=dataflux.callbacks.serial.connection_window_connect_serial, user_data=state)
|
dpg.add_button(label="Connect", callback=dataflux.callbacks.serial.connection_window_connect_serial, user_data=state)
|
||||||
|
|
||||||
|
with dpg.file_dialog(
|
||||||
|
directory_selector=False,
|
||||||
|
show=False,
|
||||||
|
tag=WINDOW_FILE_DIALOG_DUMP_BUFFERS,
|
||||||
|
width=700,
|
||||||
|
height=400,
|
||||||
|
modal=True,
|
||||||
|
callback=dataflux.callbacks.menu.window_file_dialog_dump_buffers_ok,
|
||||||
|
user_data=state
|
||||||
|
):
|
||||||
|
dpg.add_file_extension(".csv")
|
||||||
|
|||||||
92
src/dataflux/ui/worker.py
Normal file
92
src/dataflux/ui/worker.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Copyright (C) 2026 Hector van der Aa <hector@h3cx.dev>
|
||||||
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from dataflux.state import AppState
|
||||||
|
from dataflux.tags import GRAPH_SERIES_SPEED, GRAPH_SERIES_TENG, GRAPH_SERIES_VBAT, GRAPH_X_AXIS_SPEED, LIVE_DATA_TENG_VALUE, LIVE_DATA_UTC_TIME_VALUE, LIVE_DATA_VBAT_VALUE, LIVE_DATA_VEHICLE_TIME_VALUE, LIVE_DATA_SPEED_VALUE
|
||||||
|
|
||||||
|
|
||||||
|
def ui_worker(state: AppState):
|
||||||
|
last_datetime: str = ""
|
||||||
|
last_veh_time: str = ""
|
||||||
|
last_veh_speed: str = ""
|
||||||
|
last_vbat: str = ""
|
||||||
|
last_teng: str = ""
|
||||||
|
no_data_written = False
|
||||||
|
while state.running:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
formatted = now.strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
if formatted != last_datetime:
|
||||||
|
dpg.set_value(LIVE_DATA_UTC_TIME_VALUE, formatted)
|
||||||
|
last_datetime = formatted
|
||||||
|
|
||||||
|
if state.serial_thread_running and state.telemetry_valid:
|
||||||
|
x_common: list[float] | None = None
|
||||||
|
speed_y: list[float] | None = None
|
||||||
|
vbat_y: list[float] | None = None
|
||||||
|
teng_y: list[float] | None = None
|
||||||
|
with state.lock:
|
||||||
|
# Vehicle Time
|
||||||
|
no_data_written = False
|
||||||
|
veh_time = state.latest_telemetry["time_stamp"]
|
||||||
|
veh_speed = state.latest_telemetry["speed"]
|
||||||
|
vbat = state.latest_telemetry["vbat"]
|
||||||
|
teng = state.latest_telemetry["teng"]
|
||||||
|
if state.live_buffers_updated:
|
||||||
|
x_common = list(state.live_buffers.timestamp)
|
||||||
|
speed_y = list(state.live_buffers.speed)
|
||||||
|
vbat_y = list(state.live_buffers.vbat)
|
||||||
|
teng_y = list(state.live_buffers.teng)
|
||||||
|
state.live_buffers_updated = False
|
||||||
|
|
||||||
|
hours = veh_time // 360000
|
||||||
|
minutes = (veh_time % 360000) // 6000
|
||||||
|
seconds = (veh_time % 6000) // 100
|
||||||
|
formatted = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||||
|
if formatted != last_veh_time:
|
||||||
|
dpg.set_value(LIVE_DATA_VEHICLE_TIME_VALUE, formatted)
|
||||||
|
last_veh_time = formatted
|
||||||
|
|
||||||
|
# Speed
|
||||||
|
formatted = f"{veh_speed:05.2f}"
|
||||||
|
if formatted != last_veh_speed:
|
||||||
|
dpg.set_value(LIVE_DATA_SPEED_VALUE, formatted)
|
||||||
|
last_veh_speed = formatted
|
||||||
|
|
||||||
|
|
||||||
|
# VBAT
|
||||||
|
formatted = f"{vbat:05.2f}"
|
||||||
|
if formatted != last_vbat:
|
||||||
|
dpg.set_value(LIVE_DATA_VBAT_VALUE, formatted)
|
||||||
|
last_vbat = formatted
|
||||||
|
|
||||||
|
# TENG
|
||||||
|
formatted = f"{teng:05.2f}"
|
||||||
|
if formatted != last_teng:
|
||||||
|
dpg.set_value(LIVE_DATA_TENG_VALUE, formatted)
|
||||||
|
last_teng = formatted
|
||||||
|
|
||||||
|
if x_common is not None:
|
||||||
|
dpg.set_value(GRAPH_SERIES_SPEED, [x_common, speed_y])
|
||||||
|
dpg.set_value(GRAPH_SERIES_VBAT, [x_common, vbat_y])
|
||||||
|
dpg.set_value(GRAPH_SERIES_TENG, [x_common, teng_y])
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not no_data_written:
|
||||||
|
dpg.set_value(LIVE_DATA_VEHICLE_TIME_VALUE, "no_data")
|
||||||
|
dpg.set_value(LIVE_DATA_SPEED_VALUE, "no_data")
|
||||||
|
dpg.set_value(LIVE_DATA_VBAT_VALUE, "no_data")
|
||||||
|
dpg.set_value(LIVE_DATA_TENG_VALUE, "no_data")
|
||||||
|
last_veh_time = last_veh_speed = last_vbat = last_teng = "no_data"
|
||||||
|
no_data_written = True
|
||||||
|
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user