Compare commits

..

9 Commits

19 changed files with 1273 additions and 184 deletions

View File

@@ -2,7 +2,7 @@ APP_NAME = DataFlux
ENTRY = main.py ENTRY = main.py
build: build:
pyinstaller --onedir --name $(APP_NAME) --add-data "assets/fonts:assets/fonts" --add-data "assets/images:assets/images" $(ENTRY) uv run pyinstaller --onedir --windowed --name $(APP_NAME) --paths src --add-data "assets/fonts:assets/fonts" --add-data "assets/images:assets/images" $(ENTRY)
clean: clean:
rm -rf build dist *.spec rm -rf build dist *.spec

Binary file not shown.

View File

@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [
"dearpygui>=2.2", "dearpygui>=2.2",
"pyinstaller>=6.20.0",
"pyserial>=3.5", "pyserial>=3.5",
] ]

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 datetime import datetime
from pathlib import Path from pathlib import Path
import sys import sys
from threading import Thread from threading import Thread
@@ -9,6 +10,7 @@ import dearpygui.dearpygui as dpg
from dataflux.state import AppState from dataflux.state import AppState
import dataflux.config import dataflux.config
from dataflux.tags import TEXT_SERIAL_CONSOLE
import dataflux.ui.windows import dataflux.ui.windows
import dataflux.ui.worker import dataflux.ui.worker
import dataflux.services.telemetry import dataflux.services.telemetry
@@ -22,10 +24,12 @@ def _asset_path(relative_path: str) -> str:
if bundle_dir is not None: if bundle_dir is not None:
candidates.append(Path(bundle_dir) / path) candidates.append(Path(bundle_dir) / path)
candidates.extend(( candidates.extend(
(
Path.cwd() / path, Path.cwd() / path,
Path(__file__).resolve().parents[2] / path, Path(__file__).resolve().parents[2] / path,
)) )
)
for candidate in candidates: for candidate in candidates:
if candidate.exists(): if candidate.exists():
@@ -37,6 +41,7 @@ def _asset_path(relative_path: str) -> str:
def run() -> None: def run() -> None:
state: AppState = AppState() state: AppState = AppState()
state.start_time = datetime.now()
# Create application context and viewport # Create application context and viewport
dpg.create_context() dpg.create_context()
@@ -50,13 +55,20 @@ def run() -> None:
dataflux.config.MAP_IMAGE_HEIGHT = height dataflux.config.MAP_IMAGE_HEIGHT = height
with dpg.texture_registry(show=False): with dpg.texture_registry(show=False):
dpg.add_static_texture(width=width, height=height, default_value=data, tag="texture_tab") 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
with dpg.font_registry(): with dpg.font_registry():
app_font = dpg.add_font(_asset_path("assets/fonts/Inter-Regular.ttf"), 18 * 2) app_font = dpg.add_font(_asset_path("assets/fonts/Inter-Regular.ttf"), 18 * 2)
mono_font = dpg.add_font(
_asset_path("assets/fonts/JetBrainsMono-Regular.ttf"),
size=36,
label="mono_font",
)
dpg.bind_font(app_font) dpg.bind_font(app_font)
dataflux.ui.windows.build_windows(state) dataflux.ui.windows.build_windows(state)
@@ -64,22 +76,29 @@ def run() -> None:
dpg.setup_dearpygui() dpg.setup_dearpygui()
dpg.show_viewport() dpg.show_viewport()
vp_w = dpg.get_viewport_client_width() vp_w = dpg.get_viewport_client_width()
vp_h = dpg.get_viewport_client_height() vp_h = dpg.get_viewport_client_height()
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)
dpg.bind_item_font(TEXT_SERIAL_CONSOLE, mono_font)
state.ui_worker_thread = Thread(target=dataflux.ui.worker.ui_worker, args=(state,), daemon=True) state.ui_worker_thread = Thread(
target=dataflux.ui.worker.ui_worker, args=(state,), daemon=True
)
state.ui_worker_thread.start() state.ui_worker_thread.start()
state.telemetry_thread_running = True state.telemetry_thread_running = True
state.telemetry_thread = Thread(target=dataflux.services.telemetry.telemetry_worker, args=(state, ), daemon=True) state.telemetry_thread = Thread(
target=dataflux.services.telemetry.telemetry_worker, args=(state,), daemon=True
)
state.telemetry_thread.start() state.telemetry_thread.start()
state.ports_thread_running = True
state.ports_thread = Thread(
target=dataflux.ui.worker.ports_worker, args=(state,), daemon=True
)
state.ports_thread.start()
dpg.start_dearpygui() dpg.start_dearpygui()
dpg.destroy_context() dpg.destroy_context()

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
@@ -11,23 +12,95 @@ import dataflux.ui.routines.status
import dataflux.services.serial import dataflux.services.serial
import dataflux.services.telemetry import dataflux.services.telemetry
from dataflux.tags import WINDOW_CONNECTION_MENU, WINDOW_FILE_DIALOG_DUMP_BUFFERS from dataflux.tags import (
GRAPH_X_AXIS_SPEED,
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,
)
def open_connection_window(sender, app_data, user_data) -> None:
dataflux.ui.routines.windows.update_window_connection_menu_combo()
dpg.show_item(WINDOW_CONNECTION_MENU)
def menu_file_disconnect(sender, app_data, user_data) -> None: def open_lora_connection_window(sender, app_data, user_data: AppState) -> None:
dataflux.ui.routines.windows.update_window_lora_connection_menu_combo(user_data)
dpg.show_item(WINDOW_LORA_CONNECTION_MENU)
def open_serial_connection_window(sender, app_data, user_data: AppState) -> None:
dataflux.ui.routines.windows.update_window_serial_connection_menu_combo(user_data)
dpg.show_item(WINDOW_SERIAL_CONNECTION_MENU)
def menu_io_disconnect_lora(sender, app_data, user_data: AppState) -> None:
dataflux.services.serial.disconnect_lora(user_data)
update_global_connection_status(user_data)
def menu_io_disconnect_serial(sender, app_data, user_data: AppState) -> 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: 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:
dpg.stop_dearpygui()
def window_file_dialog_dump_buffers_ok(sender, app_data, user_data: AppState) -> None: 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 = Thread(
target=dataflux.services.telemetry.buffer_dump,
args=(user_data, app_data["file_path_name"]),
daemon=True,
)
user_data.buffer_dump_thread.start() user_data.buffer_dump_thread.start()
def menu_file_autosave_buffers(sender, app_state, user_data: AppState) -> None:
if user_data.autosave_enabled:
user_data.autosave_enabled = False
user_data.autosave_buffer_thread = None
else:
dpg.show_item(WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS)
def window_file_dialog_autosave_buffers_ok(
sender, app_data, user_data: AppState
) -> None:
user_data.autosave_enabled = True
user_data.autosave_buffer_thread = Thread(
target=dataflux.services.telemetry.autosave_worker,
args=(user_data, app_data["file_path_name"]),
daemon=True,
)
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)
def menu_data_timeframe(sender, app_data, user_data: tuple[AppState, int]) -> None:
app_state, timeframe = user_data
app_state.live_buffer_len = timeframe
dpg.set_axis_limits(GRAPH_X_AXIS_SPEED, ymin=-(timeframe), ymax=0)
dpg.set_axis_limits(GRAPH_X_AXIS_VBAT, ymin=-(timeframe), ymax=0)
dpg.set_axis_limits(GRAPH_X_AXIS_TENG, ymin=-(timeframe), ymax=0)

View File

@@ -7,12 +7,31 @@ import dataflux.services.serial
import dataflux.ui.routines import dataflux.ui.routines
from dataflux.state import AppState from dataflux.state import AppState
from dataflux.tags import WINDOW_CONNECTION_MENU, WINDOW_CONNECTION_MENU_COMBO from dataflux.tags import (
INPUT_SERIAL_CONSOLE,
WINDOW_LORA_CONNECTION_MENU,
WINDOW_LORA_CONNECTION_MENU_COMBO,
WINDOW_SERIAL_CONNECTION_MENU,
WINDOW_SERIAL_CONNECTION_MENU_COMBO,
)
def connection_window_connect_lora(sender, app_data, user_data: AppState) -> None:
device = dpg.get_value(WINDOW_LORA_CONNECTION_MENU_COMBO)
dataflux.services.serial.connect_lora(user_data, device)
dataflux.ui.routines.update_global_connection_status(user_data)
dpg.hide_item(WINDOW_LORA_CONNECTION_MENU)
def connection_window_connect_serial(sender, app_data, user_data: AppState) -> None: def connection_window_connect_serial(sender, app_data, user_data: AppState) -> None:
device = dpg.get_value(WINDOW_CONNECTION_MENU_COMBO) device = dpg.get_value(WINDOW_SERIAL_CONNECTION_MENU_COMBO)
dataflux.services.serial.connect_serial(user_data, device) dataflux.services.serial.connect_serial(user_data, device)
dataflux.ui.routines.update_global_connection_status(user_data) dataflux.ui.routines.update_global_connection_status(user_data)
dpg.hide_item(WINDOW_CONNECTION_MENU) dpg.hide_item(WINDOW_SERIAL_CONNECTION_MENU)
def serial_console_button_send(sender, app_data, user_data: AppState) -> None:
text = dpg.get_value(INPUT_SERIAL_CONSOLE)
dpg.set_value(INPUT_SERIAL_CONSOLE, "")
user_data.serial_send_queue.put(text)
print("Put into send queue: " + text)

View File

@@ -2,12 +2,21 @@
# 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 concurrent.futures import thread
import os
from queue import Empty from queue import Empty
from sys import base_exec_prefix from sys import base_exec_prefix
from threading import Thread from threading import Thread
import time
from serial import Serial from serial import Serial
import serial.tools.list_ports import serial.tools.list_ports
import dearpygui.dearpygui as dpg
from dataflux import telemetry_common from dataflux import telemetry_common
from dataflux.tags import (
STATUS_LORA_STATUS_BOX,
STATUS_SERIAL_STATUS_BOX,
TEXT_SERIAL_CONSOLE,
)
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 import dataflux.ui.routines
@@ -15,48 +24,134 @@ import dataflux.ui.routines
from dataflux.state import AppState from dataflux.state import AppState
def list_serial_ports() -> list[str]: def list_serial_ports(state: AppState) -> list[str]:
ports = serial.tools.list_ports.comports()
valid_ports: list[str] = [] valid_ports: list[str] = []
for port in ports:
for port in state.ports:
if port.vid is not None and port.pid is not None: if port.vid is not None and port.pid is not None:
valid_ports.append(port.device) valid_ports.append(port.device)
return valid_ports return valid_ports
def connect_lora(state: AppState, device: str) -> None:
if state.lora_port is not None:
state.lora_port.close()
state.lora_port = None
state.lora_port = Serial(port=device, baudrate=115200)
state.lora_thread = Thread(target=lora_reader_worker, args=(state,), daemon=True)
state.lora_status_thread = Thread(
target=lora_status_worker, args=(state,), daemon=True
)
state.lora_thread_running = True
state.lora_status_thread.start()
state.lora_thread.start()
def connect_serial(state: AppState, device: str) -> None: def connect_serial(state: AppState, device: str) -> None:
if state.serial_port is not None: if state.serial_port is not None:
state.serial_port.close() state.serial_port.close()
state.serial_port = None state.serial_port = None
state.serial_port = Serial(port=device, baudrate=115200) state.serial_port = Serial(
state.serial_thread = Thread(target=serial_reader_worker, args=(state,), daemon=True) port=device, baudrate=115200, timeout=0.05, write_timeout=0.1
state.serial_status_thread = Thread(target=serial_status_worker, args=(state,), daemon=True) )
state.serial_thread = Thread(target=serial_worker, args=(state,), daemon=True)
state.serial_status_thread = Thread(
target=serial_status_worker, args=(state,), daemon=True
)
state.serial_thread_running = True state.serial_thread_running = True
state.serial_status_thread.start() state.serial_status_thread.start()
state.serial_thread.start() state.serial_thread.start()
def disconnect_lora(state: AppState) -> None:
if state.lora_port is not None:
state.lora_thread_running = False
try:
state.lora_port.close()
except OSError:
pass
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
def lora_status_worker(state: AppState) -> None:
while state.lora_thread_running:
try:
duration = state.lora_status_queue.get_nowait()
except Empty:
continue
dataflux.ui.routines.status.flash_status_connection_status(
duration, STATUS_LORA_STATUS_BOX
)
def serial_worker(state: AppState) -> None:
while state.serial_thread_running:
port = state.serial_port
if port is None:
break
if port.closed:
print("Port closed")
break
if port.port is not None and not os.path.exists(port.port):
break
try:
line = port.readline()
except TypeError:
break
except serial.SerialException:
break
except OSError:
break
if line:
text = line.decode("utf-8", errors="replace")
state.serial_data_queue.put(text)
state.serial_status_queue.put(0.05)
if port.writable():
try:
data: str = state.serial_send_queue.get_nowait()
except Empty:
pass
else:
state.serial_data_queue.put(data + "\n")
state.serial_status_queue.put(0.05)
port.write(data.encode("utf-8"))
disconnect_serial(state)
dataflux.ui.routines.update_global_connection_status(state)
def serial_status_worker(state: AppState) -> None: def serial_status_worker(state: AppState) -> None:
while state.serial_thread_running: while state.serial_thread_running:
try: try:
duration = state.serial_status_queue.get(timeout=0.1) duration = state.serial_status_queue.get_nowait()
except Empty: except Empty:
continue continue
dataflux.ui.routines.status.flash_status_connection_status(duration) dataflux.ui.routines.status.flash_status_connection_status(
duration, STATUS_SERIAL_STATUS_BOX
)
def lora_reader_worker(state: AppState) -> None:
while state.lora_thread_running:
def serial_reader_worker(state: AppState) -> None: port = state.lora_port
while state.serial_thread_running:
port = state.serial_port
if port is None: if port is None:
break break
if port.closed: if port.closed:
@@ -71,12 +166,11 @@ def serial_reader_worker(state: AppState) -> None:
parsed = parse_uart_packet(packet) parsed = parse_uart_packet(packet)
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.lora_status_queue.put(0.1)
except Exception as e: except Exception:
print(f"Serial parser error: {e}")
break break
disconnect_serial(state) disconnect_lora(state)
dataflux.ui.routines.update_global_connection_status(state) dataflux.ui.routines.update_global_connection_status(state)
@@ -107,15 +201,20 @@ def read_one_uart_packet(port: Serial) -> bytes | None:
return body return body
def parse_uart_packet(body: bytes) -> dict | None: def parse_uart_packet(body: bytes) -> dict | None:
if len(body) < dataflux.telemetry_common.telemetry_common.LORA_HEADER_SIZE: if len(body) < dataflux.telemetry_common.telemetry_common.LORA_HEADER_SIZE:
return None return None
lora = dataflux.telemetry_common.telemetry_common.unpack_lora_header(body[:dataflux.telemetry_common.telemetry_common.LORA_HEADER_SIZE]) lora = dataflux.telemetry_common.telemetry_common.unpack_lora_header(
body[: dataflux.telemetry_common.telemetry_common.LORA_HEADER_SIZE]
)
payload = body[dataflux.telemetry_common.telemetry_common.LORA_HEADER_SIZE :] payload = body[dataflux.telemetry_common.telemetry_common.LORA_HEADER_SIZE :]
if lora.size != len(payload): if lora.size != len(payload):
print(f"Serial size mismatch header says {lora.size} actual payload is {len(payload)}") print(
f"Serial size mismatch header says {lora.size} actual payload is {len(payload)}"
)
return None return None
calc_crc = dataflux.telemetry_common.telemetry_common.crc16_ccitt(payload) calc_crc = dataflux.telemetry_common.telemetry_common.crc16_ccitt(payload)
@@ -135,7 +234,7 @@ def parse_uart_packet(body: bytes) -> dict | None:
return { return {
**base, **base,
"type": "packet1", "type": "packet1",
"ping": pkt.ping.decode("ascii", errors="replace") "ping": pkt.ping.decode("ascii", errors="replace"),
} }
if lora.version == 2: if lora.version == 2:
@@ -151,7 +250,15 @@ def parse_uart_packet(body: bytes) -> dict | None:
"speed": pkt.speed, "speed": pkt.speed,
} }
if lora.version == 3:
pkt = dataflux.telemetry_common.telemetry_common.unpack_packet3(payload)
return {
**base,
"type": "packet3",
"start_time": pkt.start_time,
"duration": pkt.duration,
"count": pkt.count,
}
print("Unknown payload") print("Unknown payload")
return None return None

View File

@@ -2,29 +2,65 @@
# 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 bisect import bisect_left
from datetime import datetime, timezone
from queue import Empty from queue import Empty
from dataflux.state import AppState, Buffers import stat
from tracemalloc import start
from dataflux.state import AppState, Buffers, LapInfo
import time import time
from pathlib import Path from pathlib import Path
import csv import csv
LIVE_BUFFER_WINDOW_CS = 30 * 100
def cs_to_datetime(date_utc: datetime, timestamp_cs: int) -> datetime:
if not 0 <= timestamp_cs < 24 * 60 * 60 * 100:
raise ValueError("timestamp_cs must be within one day")
hours, rem = divmod(timestamp_cs, 60 * 60 * 100)
minutes, rem = divmod(rem, 60 * 100)
seconds, cs = divmod(rem, 100)
return datetime(
date_utc.year,
date_utc.month,
date_utc.day,
hours,
minutes,
seconds,
cs * 10_000,
tzinfo=timezone.utc,
)
def datetime_to_cs(dt: datetime) -> int:
return (
dt.hour * 60 * 60 * 100
+ dt.minute * 60 * 100
+ dt.second * 100
+ dt.microsecond // 10_000
)
def telemetry_worker(state: AppState): def telemetry_worker(state: AppState):
while state.telemetry_thread_running: while state.telemetry_thread_running:
if state.serial_thread_running == False: if not state.lora_thread_running:
time.sleep(1) time.sleep(1)
continue continue
try: try:
dataframe = state.packet_queue.get(timeout=0.1) dataframe = state.packet_queue.get_nowait()
except Empty: except Empty:
continue continue
now = datetime.now(timezone.utc)
time_stamp = datetime_to_cs(now)
if (
dataframe["type"] == "packet2"
and abs(dataframe["time_stamp"] - time_stamp) <= 60 * 100
):
state.latest_telemetry = dataframe state.latest_telemetry = dataframe
state.telemetry_valid = True state.telemetry_valid = True
with state.lock: with state.lock:
state.raw_buffers.timestamp.append(dataframe["time_stamp"]) state.raw_buffers.timestamp.append(dataframe["time_stamp"])
state.raw_buffers.speed.append(dataframe["speed"]) state.raw_buffers.speed.append(dataframe["speed"])
@@ -45,12 +81,14 @@ def telemetry_worker(state: AppState):
return return
last_timestamp = state.raw_buffers.timestamp[-1] last_timestamp = state.raw_buffers.timestamp[-1]
cutoff = last_timestamp - LIVE_BUFFER_WINDOW_CS cutoff = last_timestamp - (state.live_buffer_len * 100)
i = len(state.raw_buffers.timestamp) - 1 i = len(state.raw_buffers.timestamp) - 1
while i >= 0 and state.raw_buffers.timestamp[i] >= cutoff: while i >= 0 and state.raw_buffers.timestamp[i] >= cutoff:
elapsed_seconds = (state.raw_buffers.timestamp[i] - last_timestamp) / 100.0 elapsed_seconds = (
state.raw_buffers.timestamp[i] - last_timestamp
) / 100.0
state.live_buffers.timestamp.append(elapsed_seconds) state.live_buffers.timestamp.append(elapsed_seconds)
state.live_buffers.speed.append(state.raw_buffers.speed[i]) state.live_buffers.speed.append(state.raw_buffers.speed[i])
state.live_buffers.vbat.append(state.raw_buffers.vbat[i]) state.live_buffers.vbat.append(state.raw_buffers.vbat[i])
@@ -65,8 +103,33 @@ def telemetry_worker(state: AppState):
state.live_buffers.teng.reverse() state.live_buffers.teng.reverse()
state.live_buffers.lat.reverse() state.live_buffers.lat.reverse()
state.live_buffers.lng.reverse() state.live_buffers.lng.reverse()
elif dataframe["type"] == "packet3":
start_time: int = dataframe["start_time"]
end_time: int = dataframe["duration"] + start_time
lap_count = dataframe["count"]
lap: LapInfo = LapInfo(start_time, end_time, lap_count)
state.laps.append(lap)
state.new_laps.put(lap)
def buffer_dump(state: AppState, path: str):
def save_lap(state: AppState, start_time: int, end_time: int, count: int) -> None:
time_str = cs_to_datetime(datetime.now(timezone.utc), start_time).strftime(
"%m_%d_%Y_%H_%M"
)
save_path = Path(state.autosave_path) / f"{time_str}_lap_{count}.csv"
data: Buffers = isolate_lap(state, start_time, end_time)
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(
data.timestamp, data.speed, data.vbat, data.teng, data.lat, data.lng
):
writer.writerow(row)
def buffer_dump(state: AppState, path: str) -> None:
save_path = Path(path) save_path = Path(path)
if save_path.is_dir(): if save_path.is_dir():
save_path = save_path / "output.csv" save_path = save_path / "output.csv"
@@ -86,9 +149,106 @@ def buffer_dump(state: AppState, path: str):
writer.writerow(["timestamp", "speed", "vbat", "teng", "lat", "lng"]) 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): 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) writer.writerow(row)
state.buffer_dump_thread = None state.buffer_dump_thread = None
def autosave_worker(state: AppState, path: str) -> None:
output_dir = Path(path)
state.autosave_path = output_dir
ctr: int = 0
while state.autosave_enabled:
date_str = state.start_time.strftime("%m_%d_%Y_%H_%M")
filename = date_str + ".csv"
save_path = output_dir / filename
buffer_dump(state, save_path)
print(f"Autosave {ctr} complete")
ctr += 1
try:
new_lap: LapInfo = state.new_laps.get_nowait()
except Empty:
pass
else:
save_lap(state, new_lap.start_time, new_lap.end_time, new_lap.count)
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")
pos = bisect_left(values, target)
if pos == 0:
return 0
if pos == len(values):
return len(values) - 1
before = pos - 1
after = pos
if abs(values[after] - target) < abs(values[before] - target):
return after
return before
def isolate_lap(state: AppState, start_time: int, end_time: int) -> Buffers:
output: Buffers = Buffers()
start_idx = closest_idx(state.raw_buffers.timestamp, start_time)
end_idx = closest_idx(state.raw_buffers.timestamp, end_time)
output.timestamp = state.raw_buffers.timestamp[start_idx : end_idx + 1]
output.speed = state.raw_buffers.speed[start_idx : end_idx + 1]
output.vbat = state.raw_buffers.vbat[start_idx : end_idx + 1]
output.teng = state.raw_buffers.teng[start_idx : end_idx + 1]
output.lat = state.raw_buffers.lat[start_idx : end_idx + 1]
output.lng = state.raw_buffers.lng[start_idx : end_idx + 1]
return output

View File

@@ -3,10 +3,15 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from threading import Lock, Thread from threading import Lock, Thread
from serial import Serial from serial import Serial
from queue import Queue from queue import Queue
from serial.tools.list_ports_common import ListPortInfo
@dataclass @dataclass
class Buffers: class Buffers:
timestamp: list[int] = field(default_factory=list) timestamp: list[int] = field(default_factory=list)
@@ -16,17 +21,39 @@ class Buffers:
lat: list[float] = field(default_factory=list) lat: list[float] = field(default_factory=list)
lng: list[float] = field(default_factory=list) lng: list[float] = field(default_factory=list)
@dataclass
class LapInfo:
start_time: int = field(default_factory=int)
end_time: int = field(default_factory=int)
count: int = field(default_factory=int)
@dataclass @dataclass
class AppState: class AppState:
running: bool = True running: bool = True
start_time: datetime = datetime.now()
ports: list[ListPortInfo] = field(default_factory=list)
ports_thread: Thread | None = None
ports_thread_running: bool = False
lora_port: Serial | None = None
lora_thread: Thread | None = None
lora_thread_running: bool = False
serial_port: Serial | None = None serial_port: Serial | None = None
serial_thread: Thread | None = None serial_thread: Thread | None = None
serial_data_queue: Queue | None = field(default_factory=Queue)
serial_send_queue: Queue | None = field(default_factory=Queue)
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 telemetry_thread_running: bool = False
lora_status_thread: Thread | None = None
lora_status_queue: Queue = field(default_factory=Queue)
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)
@@ -39,7 +66,20 @@ class AppState:
raw_buffers: Buffers = field(default_factory=Buffers) raw_buffers: Buffers = field(default_factory=Buffers)
live_buffers: Buffers = field(default_factory=Buffers) live_buffers: Buffers = field(default_factory=Buffers)
live_buffers_updated: bool = False 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)
buffer_dump_thread: Thread | None = None buffer_dump_thread: Thread | None = None
autosave_buffer_thread: Thread | None = None
autosave_enabled: bool = False
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

@@ -2,13 +2,25 @@
# 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
MENU_FILE_CONNECT: str = "menu_file_connect" MENU_IO_CONNECT_LORA: str = "menu_io_connect_lora"
MENU_FILE_DISCONNECT: str = "menu_file_disconnect" MENU_IO_CONNECT_SERIAL: str = "menu_io_connect_serial"
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_DUMP_BUFFERS: str = "menu_file_dump_buffers"
WINDOW_CONNECTION_MENU: str = "window_connection_menu" MENU_FILE_AUTOSAVE_BUFFERS: str = "menu_file_autosave_buffers"
WINDOW_CONNECTION_MENU_COMBO: str = "window_connection_menu_combo" 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_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"
STATUS_LORA_STATUS_BOX: str = "status_lora_status_box"
STATUS_LORA_STATUS_TEXT: str = "status_lora_status_text"
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"
@@ -20,6 +32,13 @@ LIVE_DATA_TENG_VALUE: str = "live_data_teng_value"
PAGE_LIVE_DATA: str = "page_live_data" PAGE_LIVE_DATA: str = "page_live_data"
PAGE_LAP_RECAP: str = "page_lap_recap" PAGE_LAP_RECAP: str = "page_lap_recap"
PAGE_SERIAL_CONSOLE: str = "page_serial_console"
TEXT_SERIAL_CONSOLE: str = "text_serial_console"
BUTTON_SERIAL_CONSOLE_SEND: str = "button_serial_console_send"
INPUT_SERIAL_CONSOLE: str = "input_serial_console"
SUB_PAGE_DATA_GRAPHS: str = "sub_page_data_graphs" SUB_PAGE_DATA_GRAPHS: str = "sub_page_data_graphs"
SUB_PAGE_MAP: str = "sub_page_map" SUB_PAGE_MAP: str = "sub_page_map"
@@ -32,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

@@ -1 +1 @@
/Users/hector/Projects/Exergie/TelemetryCommon/python /home/hector/projects/Exergie/TelemetryCommon/python/

View File

@@ -5,12 +5,25 @@
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
from dataflux.state import AppState from dataflux.state import AppState
from dataflux.tags import MENU_FILE_CONNECT, MENU_FILE_DISCONNECT from dataflux.tags import (
MENU_IO_CONNECT_LORA,
MENU_IO_CONNECT_SERIAL,
MENU_IO_DISCONNECT_LORA,
MENU_IO_DISCONNECT_SERIAL,
)
def update_menu_file_connection_status(state: AppState) -> None: def update_menu_file_connection_status(state: AppState) -> None:
if state.serial_port is None: if state.lora_port is None:
dpg.enable_item(MENU_FILE_CONNECT) dpg.enable_item(MENU_IO_CONNECT_LORA)
dpg.disable_item(MENU_FILE_DISCONNECT) dpg.disable_item(MENU_IO_DISCONNECT_LORA)
else: else:
dpg.disable_item(MENU_FILE_CONNECT) dpg.disable_item(MENU_IO_CONNECT_LORA)
dpg.enable_item(MENU_FILE_DISCONNECT) dpg.enable_item(MENU_IO_DISCONNECT_LORA)
if state.serial_port is None:
dpg.enable_item(MENU_IO_CONNECT_SERIAL)
dpg.disable_item(MENU_IO_DISCONNECT_SERIAL)
else:
dpg.disable_item(MENU_IO_CONNECT_SERIAL)
dpg.enable_item(MENU_IO_DISCONNECT_SERIAL)

View File

@@ -0,0 +1,22 @@
# 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
from dataflux.tags import CHILD_WINDOW_SERIAL_CONSOLE, TEXT_SERIAL_CONSOLE
def append_text_to_console(text: str) -> None:
old = dpg.get_value(TEXT_SERIAL_CONSOLE)
dpg.set_value(TEXT_SERIAL_CONSOLE, old + text)
def scroll_to_bottom() -> None:
dpg.set_y_scroll(
CHILD_WINDOW_SERIAL_CONSOLE,
dpg.get_y_scroll_max(CHILD_WINDOW_SERIAL_CONSOLE),
)
frame = dpg.get_frame_count()
dpg.set_frame_callback(frame + 1, scroll_to_bottom)
dpg.set_frame_callback(frame + 2, scroll_to_bottom)

View File

@@ -4,10 +4,26 @@
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
from dataflux.state import AppState from dataflux.state import AppState
from dataflux.tags import STATUS_SERIAL_STATUS_BOX, STATUS_SERIAL_STATUS_TEXT, THEME_STATUS_CONNECTED, THEME_STATUS_CONNECTED_BRIGHT, THEME_STATUS_DISCONNECTED from dataflux.tags import (
STATUS_LORA_STATUS_BOX,
STATUS_LORA_STATUS_TEXT,
STATUS_SERIAL_STATUS_BOX,
STATUS_SERIAL_STATUS_TEXT,
THEME_STATUS_CONNECTED,
THEME_STATUS_CONNECTED_BRIGHT,
THEME_STATUS_DISCONNECTED,
)
from time import sleep from time import sleep
def update_status_connection_status(state: AppState): def update_status_connection_status(state: AppState):
if state.lora_port is None:
dpg.bind_item_theme(STATUS_LORA_STATUS_BOX, THEME_STATUS_DISCONNECTED)
dpg.set_value(STATUS_LORA_STATUS_TEXT, "LoRa: Disconnected")
else:
dpg.bind_item_theme(STATUS_LORA_STATUS_BOX, THEME_STATUS_CONNECTED)
dpg.set_value(STATUS_LORA_STATUS_TEXT, "LoRa: Connected")
if state.serial_port is None: if state.serial_port is None:
dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_DISCONNECTED) dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_DISCONNECTED)
dpg.set_value(STATUS_SERIAL_STATUS_TEXT, "Serial: Disconnected") dpg.set_value(STATUS_SERIAL_STATUS_TEXT, "Serial: Disconnected")
@@ -15,7 +31,8 @@ def update_status_connection_status(state: AppState):
dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_CONNECTED) dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_CONNECTED)
dpg.set_value(STATUS_SERIAL_STATUS_TEXT, "Serial: Connected") dpg.set_value(STATUS_SERIAL_STATUS_TEXT, "Serial: Connected")
def flash_status_connection_status(duration: float) -> None:
dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_CONNECTED_BRIGHT) def flash_status_connection_status(duration: float, tag: str) -> None:
dpg.bind_item_theme(tag, THEME_STATUS_CONNECTED_BRIGHT)
sleep(duration) sleep(duration)
dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_CONNECTED) dpg.bind_item_theme(tag, THEME_STATUS_CONNECTED)

View File

@@ -3,23 +3,50 @@
# 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 PAGE_LAP_RECAP, PAGE_LIVE_DATA, SUB_PAGE_DATA_GRAPHS, SUB_PAGE_MAP, WINDOW_CONNECTION_MENU_COMBO from dataflux.state import AppState
from dataflux.tags import (
PAGE_LAP_RECAP,
PAGE_LIVE_DATA,
PAGE_SERIAL_CONSOLE,
SUB_PAGE_DATA_GRAPHS,
SUB_PAGE_MAP,
WINDOW_LORA_CONNECTION_MENU_COMBO,
WINDOW_SERIAL_CONNECTION_MENU_COMBO,
)
def update_window_connection_menu_combo() -> None:
ports: list[str] = list_serial_ports() def update_window_lora_connection_menu_combo(state: AppState) -> None:
dpg.configure_item(WINDOW_CONNECTION_MENU_COMBO, items=ports) ports: list[str] = list_serial_ports(state)
if state.serial_port is not None and state.serial_thread_running:
port_name = state.serial_port.name
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:
ports: list[str] = list_serial_ports(state)
if state.lora_port is not None and state.lora_thread_running:
port_name = state.lora_port.name
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: def hide_all_but(tag: str) -> None:
arr = [PAGE_LIVE_DATA, PAGE_LAP_RECAP] arr = [PAGE_LIVE_DATA, PAGE_LAP_RECAP, PAGE_SERIAL_CONSOLE]
for item in arr: for item in arr:
if tag == item: if tag == item:
dpg.show_item(item) dpg.show_item(item)
else: else:
dpg.hide_item(item) dpg.hide_item(item)
def toggle_window(tag: str) -> None: def toggle_window(tag: str) -> None:
if tag == SUB_PAGE_DATA_GRAPHS: if tag == SUB_PAGE_DATA_GRAPHS:
dpg.show_item(SUB_PAGE_DATA_GRAPHS) dpg.show_item(SUB_PAGE_DATA_GRAPHS)
@@ -31,6 +58,3 @@ def toggle_window(tag: str) -> None:
hide_all_but(PAGE_LIVE_DATA) hide_all_but(PAGE_LIVE_DATA)
else: else:
hide_all_but(tag) hide_all_but(tag)

View File

@@ -2,14 +2,70 @@
# 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
from dataflux.state import AppState from dataflux.state import AppState
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.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,
LIVE_DATA_VBAT_VALUE,
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,
MENU_IO_CONNECT_SERIAL,
MENU_IO_DISCONNECT_SERIAL,
PAGE_LAP_RECAP,
PAGE_LIVE_DATA,
PAGE_SERIAL_CONSOLE,
STATUS_LORA_STATUS_BOX,
STATUS_LORA_STATUS_TEXT,
STATUS_SERIAL_STATUS_BOX,
STATUS_SERIAL_STATUS_TEXT,
SUB_PAGE_DATA_GRAPHS,
SUB_PAGE_MAP,
TEXT_SERIAL_CONSOLE,
THEME_STATUS_CONNECTED,
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,
WINDOW_SERIAL_CONNECTION_MENU,
WINDOW_SERIAL_CONNECTION_MENU_COMBO,
)
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: def _add_live_data_row(label: str, value: str, tag: str, units: str) -> None:
with dpg.table_row(): with dpg.table_row():
with dpg.table_cell(): with dpg.table_cell():
@@ -19,67 +75,315 @@ def _add_live_data_row(label: str, value: str, tag: str, units: str) -> None:
with dpg.table_cell(): with dpg.table_cell():
dpg.add_text(units) 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):
dpg.set_global_font_scale(0.5) dpg.set_global_font_scale(0.5)
with dpg.menu_bar(): with dpg.menu_bar():
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(
dpg.add_menu_item(label="Disonnect", enabled=False, tag=MENU_FILE_DISCONNECT, callback=dataflux.callbacks.menu.menu_file_disconnect, user_data=state) label="Dump Buffers",
dpg.add_menu_item(label="Dump Buffers", enabled=True, tag=MENU_FILE_DUMP_BUFFERS, callback=dataflux.callbacks.menu.menu_file_dump_buffers) enabled=True,
dpg.add_menu_item(label="Quit") tag=MENU_FILE_DUMP_BUFFERS,
with dpg.menu(label='Window'): callback=dataflux.callbacks.menu.menu_file_dump_buffers,
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="Live Map", user_data=SUB_PAGE_MAP, callback=dataflux.callbacks.menu.menu_window_select ) dpg.add_menu_item(
dpg.add_menu_item(label="Lap Recap", user_data=PAGE_LAP_RECAP, callback=dataflux.callbacks.menu.menu_window_select) label="Autosave Buffers",
enabled=True,
check=True,
default_value=False,
tag=MENU_FILE_AUTOSAVE_BUFFERS,
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
)
with dpg.menu(label="IO"):
dpg.add_menu_item(
label="Connect LoRa",
enabled=True,
tag=MENU_IO_CONNECT_LORA,
callback=dataflux.callbacks.menu.open_lora_connection_window,
user_data=state,
)
dpg.add_menu_item(
label="Disonnect LoRa",
enabled=False,
tag=MENU_IO_DISCONNECT_LORA,
callback=dataflux.callbacks.menu.menu_io_disconnect_lora,
user_data=state,
)
dpg.add_menu_item(
label="Connect Serial",
enabled=True,
tag=MENU_IO_CONNECT_SERIAL,
callback=dataflux.callbacks.menu.open_serial_connection_window,
user_data=state,
)
dpg.add_menu_item(
label="Disonnect Serial",
enabled=False,
tag=MENU_IO_DISCONNECT_SERIAL,
callback=dataflux.callbacks.menu.menu_io_disconnect_serial,
user_data=state,
)
with dpg.menu(label="Window"):
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="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,
)
dpg.add_menu_item(
label="Serial Console",
user_data=PAGE_SERIAL_CONSOLE,
callback=dataflux.callbacks.menu.menu_window_select,
)
with dpg.menu(label="Data"):
with dpg.menu(label="Timeframe"):
dpg.add_menu_item(
label="30s",
user_data=(state, 30),
callback=dataflux.callbacks.menu.menu_data_timeframe,
)
dpg.add_menu_item(
label="60s",
user_data=(state, 60),
callback=dataflux.callbacks.menu.menu_data_timeframe,
)
dpg.add_menu_item(
label="5m",
user_data=(state, 60 * 5),
callback=dataflux.callbacks.menu.menu_data_timeframe,
)
dpg.add_menu_item(
label="10m",
user_data=(state, 60 * 10),
callback=dataflux.callbacks.menu.menu_data_timeframe,
)
dpg.add_menu_item(
label="15m",
user_data=(state, 60 * 15),
callback=dataflux.callbacks.menu.menu_data_timeframe,
)
dpg.add_menu_item(
label="30m",
user_data=(state, 60 * 30),
callback=dataflux.callbacks.menu.menu_data_timeframe,
)
dpg.add_menu_item(
label="1h",
user_data=(state, 60 * 60),
callback=dataflux.callbacks.menu.menu_data_timeframe,
)
dpg.add_menu_item(
label="2h",
user_data=(state, 60 * 120),
callback=dataflux.callbacks.menu.menu_data_timeframe,
)
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=260, autosize_y=True, border=True): with dpg.child_window(
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): tag="realtime_stats", width=260, autosize_y=True, border=True
):
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_fixed=True)
dpg.add_table_column(width_stretch=True, init_width_or_weight=1.0) dpg.add_table_column(
width_stretch=True, init_width_or_weight=1.0
)
dpg.add_table_column(width_fixed=True) dpg.add_table_column(width_fixed=True)
_add_live_data_row("UTC Time", "no_data", LIVE_DATA_UTC_TIME_VALUE,"") _add_live_data_row(
_add_live_data_row("Vehicle Time", "no_data", LIVE_DATA_VEHICLE_TIME_VALUE,"") "UTC Time", "no_data", LIVE_DATA_UTC_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(
_add_live_data_row("Engine Temp", "no_data", LIVE_DATA_TENG_VALUE,"°C") "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=SUB_PAGE_DATA_GRAPHS, autosize_x=True, autosize_y=True, border=True, show=True): with dpg.child_window(
with dpg.plot(label="Speed", height=250, width=-1, no_inputs=True): 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
):
dpg.add_plot_legend() dpg.add_plot_legend()
dpg.add_plot_axis(dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_SPEED) dpg.add_plot_axis(
y_axis_speed = dpg.add_plot_axis(dpg.mvYAxis, label="Speed", tag=GRAPH_Y_AXIS_SPEED) dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_SPEED
)
y_axis_speed = dpg.add_plot_axis(
dpg.mvYAxis, label="Speed", tag=GRAPH_Y_AXIS_SPEED
)
dpg.set_axis_limits(GRAPH_Y_AXIS_SPEED, ymin=0, ymax=50) dpg.set_axis_limits(GRAPH_Y_AXIS_SPEED, ymin=0, ymax=50)
dpg.set_axis_limits(GRAPH_X_AXIS_SPEED, ymin=-30, ymax=0) dpg.set_axis_limits(GRAPH_X_AXIS_SPEED, ymin=-30, ymax=0)
dpg.add_line_series([], [], parent=y_axis_speed, tag=GRAPH_SERIES_SPEED) dpg.add_line_series(
with dpg.plot(label="Battery Voltage", height=250, width=-1, no_inputs=True): [], [], 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_legend()
dpg.add_plot_axis(dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_VBAT) dpg.add_plot_axis(
y_axis_vbat = dpg.add_plot_axis(dpg.mvYAxis, label="Voltage", tag=GRAPH_Y_AXIS_VBAT) 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_Y_AXIS_VBAT, ymin=0, ymax=20)
dpg.set_axis_limits(GRAPH_X_AXIS_VBAT, ymin=-30, ymax=0) dpg.set_axis_limits(GRAPH_X_AXIS_VBAT, ymin=-30, ymax=0)
dpg.add_line_series([], [], parent=y_axis_vbat, tag=GRAPH_SERIES_VBAT) dpg.add_line_series(
with dpg.plot(label="Engine Temp", height=250, width=-1, no_inputs=True): [], [], 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_legend()
dpg.add_plot_axis(dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_TENG) dpg.add_plot_axis(
y_axis_teng = dpg.add_plot_axis(dpg.mvYAxis, label="Temperature", tag=GRAPH_Y_AXIS_TENG) 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_Y_AXIS_TENG, ymin=0, ymax=120)
dpg.set_axis_limits(GRAPH_X_AXIS_TENG, ymin=-30, ymax=0) dpg.set_axis_limits(GRAPH_X_AXIS_TENG, ymin=-30, ymax=0)
dpg.add_line_series([], [], parent=y_axis_teng, tag=GRAPH_SERIES_TENG) dpg.add_line_series(
[], [], parent=y_axis_teng, tag=GRAPH_SERIES_TENG
)
with dpg.child_window(tag=SUB_PAGE_MAP, autosize_x=True, autosize_y=True, border=True, show=False, no_scrollbar=True): 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"): with dpg.drawlist(width=500, height=500, tag="map_drawlist"):
dpg.draw_image("texture_tab", (0, 0), (500, 500)) 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)) 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): 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()
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,
width=-1,
height=-40,
border=True,
horizontal_scrollbar=False,
):
dpg.add_text(tag=TEXT_SERIAL_CONSOLE, wrap=0)
with dpg.group(horizontal=True):
dpg.add_input_text(tag=INPUT_SERIAL_CONSOLE, width=-100)
dpg.add_button(
tag=BUTTON_SERIAL_CONSOLE_SEND,
label="Send",
width=100,
callback=dataflux.callbacks.serial.serial_console_button_send,
user_data=state,
)
with dpg.theme(tag=THEME_STATUS_CONNECTED): with dpg.theme(tag=THEME_STATUS_CONNECTED):
with dpg.theme_component(dpg.mvChildWindow): with dpg.theme_component(dpg.mvChildWindow):
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, STATUS_GREEN_DARK) dpg.add_theme_color(dpg.mvThemeCol_ChildBg, STATUS_GREEN_DARK)
@@ -92,28 +396,103 @@ def build_windows(state: AppState) -> None:
with dpg.theme_component(dpg.mvChildWindow): with dpg.theme_component(dpg.mvChildWindow):
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, STATUS_RED_DARK) dpg.add_theme_color(dpg.mvThemeCol_ChildBg, STATUS_RED_DARK)
with dpg.child_window(tag="footer_bar", autosize_x=True, height=28, border=False, no_scrollbar=True): with dpg.child_window(
tag="footer_bar",
autosize_x=True,
height=28,
border=False,
no_scrollbar=True,
):
with dpg.group(horizontal=True): with dpg.group(horizontal=True):
with dpg.child_window(width=200, height=28, border=False, tag=STATUS_SERIAL_STATUS_BOX): with dpg.child_window(
with dpg.table(header_row=False, resizable=False, policy=dpg.mvTable_SizingStretchProp, borders_innerV=False, borders_innerH=False, borders_outerH=False, borders_outerV=False, no_host_extendX=False, no_pad_innerX=True): width=200, height=28, border=False, tag=STATUS_LORA_STATUS_BOX
):
with dpg.table(
header_row=False,
resizable=False,
policy=dpg.mvTable_SizingStretchProp,
borders_innerV=False,
borders_innerH=False,
borders_outerH=False,
borders_outerV=False,
no_host_extendX=False,
no_pad_innerX=True,
):
dpg.add_table_column(init_width_or_weight=1.0) dpg.add_table_column(init_width_or_weight=1.0)
dpg.add_table_column(width_fixed=True) dpg.add_table_column(width_fixed=True)
dpg.add_table_column(init_width_or_weight=1.0) dpg.add_table_column(init_width_or_weight=1.0)
with dpg.table_row(): with dpg.table_row():
with dpg.table_cell(): with dpg.table_cell():
pass pass
with dpg.table_cell(): with dpg.table_cell():
dpg.add_text("Serial: Disconnected", tag=STATUS_SERIAL_STATUS_TEXT) dpg.add_text(
"LoRa: Disconnected",
tag=STATUS_LORA_STATUS_TEXT,
)
with dpg.table_cell():
pass
with dpg.child_window(
width=200, height=28, border=False, tag=STATUS_SERIAL_STATUS_BOX
):
with dpg.table(
header_row=False,
resizable=False,
policy=dpg.mvTable_SizingStretchProp,
borders_innerV=False,
borders_innerH=False,
borders_outerH=False,
borders_outerV=False,
no_host_extendX=False,
no_pad_innerX=True,
):
dpg.add_table_column(init_width_or_weight=1.0)
dpg.add_table_column(width_fixed=True)
dpg.add_table_column(init_width_or_weight=1.0)
with dpg.table_row():
with dpg.table_cell():
pass
with dpg.table_cell():
dpg.add_text(
"Serial: Disconnected",
tag=STATUS_SERIAL_STATUS_TEXT,
)
with dpg.table_cell(): with dpg.table_cell():
pass pass
dpg.bind_item_theme(STATUS_LORA_STATUS_BOX, THEME_STATUS_DISCONNECTED)
dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_DISCONNECTED) dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_DISCONNECTED)
with dpg.window(label="Connection Menu", tag=WINDOW_CONNECTION_MENU, show=False, modal=True, no_collapse=True, width=300): with dpg.window(
dpg.add_combo([], tag=WINDOW_CONNECTION_MENU_COMBO) label="LoRa Connection Menu",
dpg.add_button(label="Connect", callback=dataflux.callbacks.serial.connection_window_connect_serial, user_data=state) tag=WINDOW_LORA_CONNECTION_MENU,
show=False,
modal=True,
no_collapse=True,
width=400,
no_resize=True,
):
dpg.add_combo([], tag=WINDOW_LORA_CONNECTION_MENU_COMBO)
dpg.add_button(
label="Connect",
callback=dataflux.callbacks.serial.connection_window_connect_lora,
user_data=state,
)
with dpg.window(
label="Serial Connection Menu",
tag=WINDOW_SERIAL_CONNECTION_MENU,
show=False,
modal=True,
no_collapse=True,
width=400,
no_resize=True,
):
dpg.add_combo([], tag=WINDOW_SERIAL_CONNECTION_MENU_COMBO)
dpg.add_button(
label="Connect",
callback=dataflux.callbacks.serial.connection_window_connect_serial,
user_data=state,
)
with dpg.file_dialog( with dpg.file_dialog(
directory_selector=False, directory_selector=False,
@@ -123,6 +502,30 @@ def build_windows(state: AppState) -> None:
height=400, height=400,
modal=True, modal=True,
callback=dataflux.callbacks.menu.window_file_dialog_dump_buffers_ok, callback=dataflux.callbacks.menu.window_file_dialog_dump_buffers_ok,
user_data=state user_data=state,
):
dpg.add_file_extension(".csv")
with dpg.file_dialog(
directory_selector=True,
show=False,
tag=WINDOW_FILE_DIALOG_AUTOSAVE_BUFFERS,
width=700,
height=400,
modal=True,
callback=dataflux.callbacks.menu.window_file_dialog_autosave_buffers_ok,
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") dpg.add_file_extension(".csv")

View File

@@ -2,13 +2,38 @@
# 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 queue import Empty
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
import datetime import datetime
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
import serial.tools.list_ports
from dataflux.state import AppState 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 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
def ports_worker(state: AppState) -> None:
while state.ports_thread_running:
state.ports = serial.tools.list_ports.comports()
time.sleep(5)
def ui_worker(state: AppState): def ui_worker(state: AppState):
@@ -19,6 +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:
dpg.set_value(MENU_FILE_AUTOSAVE_BUFFERS, True)
else:
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")
@@ -26,7 +56,42 @@ def ui_worker(state: AppState):
dpg.set_value(LIVE_DATA_UTC_TIME_VALUE, formatted) dpg.set_value(LIVE_DATA_UTC_TIME_VALUE, formatted)
last_datetime = formatted last_datetime = formatted
if state.serial_thread_running and state.telemetry_valid: if state.serial_thread_running:
try:
text = state.serial_data_queue.get_nowait()
except Empty:
pass
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 x_common: list[float] | None = None
speed_y: list[float] | None = None speed_y: list[float] | None = None
vbat_y: list[float] | None = None vbat_y: list[float] | None = None
@@ -59,7 +124,6 @@ def ui_worker(state: AppState):
dpg.set_value(LIVE_DATA_SPEED_VALUE, formatted) dpg.set_value(LIVE_DATA_SPEED_VALUE, formatted)
last_veh_speed = formatted last_veh_speed = formatted
# VBAT # VBAT
formatted = f"{vbat:05.2f}" formatted = f"{vbat:05.2f}"
if formatted != last_vbat: if formatted != last_vbat:
@@ -87,7 +151,3 @@ def ui_worker(state: AppState):
no_data_written = True no_data_written = True
time.sleep(0.05) time.sleep(0.05)

100
uv.lock generated
View File

@@ -2,18 +2,29 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.14" requires-python = ">=3.14"
[[package]]
name = "altgraph"
version = "0.17.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
]
[[package]] [[package]]
name = "dataflux" name = "dataflux"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "dearpygui" }, { name = "dearpygui" },
{ name = "pyinstaller" },
{ name = "pyserial" }, { name = "pyserial" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "dearpygui", specifier = ">=2.2" }, { name = "dearpygui", specifier = ">=2.2" },
{ name = "pyinstaller", specifier = ">=6.20.0" },
{ name = "pyserial", specifier = ">=3.5" }, { name = "pyserial", specifier = ">=3.5" },
] ]
@@ -27,6 +38,77 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/03/aeb4ebe09a0240c8c9337018d2ac3e087fd911f6051a3bb0131248fbd942/dearpygui-2.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe3c8dc37be3ddce0356afb0c16721c0e485a4c94a831886935a0692bb9a9966", size = 1889279, upload-time = "2026-02-17T14:21:46.16Z" }, { url = "https://files.pythonhosted.org/packages/f8/03/aeb4ebe09a0240c8c9337018d2ac3e087fd911f6051a3bb0131248fbd942/dearpygui-2.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe3c8dc37be3ddce0356afb0c16721c0e485a4c94a831886935a0692bb9a9966", size = 1889279, upload-time = "2026-02-17T14:21:46.16Z" },
] ]
[[package]]
name = "macholib"
version = "1.16.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pefile"
version = "2024.8.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
]
[[package]]
name = "pyinstaller"
version = "6.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "macholib", marker = "sys_platform == 'darwin'" },
{ name = "packaging" },
{ name = "pefile", marker = "sys_platform == 'win32'" },
{ name = "pyinstaller-hooks-contrib" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/60/d03d52e6690d4e9caf333dcd14550cde634ce6c118b3bc8fa3112c3186fd/pyinstaller-6.20.0.tar.gz", hash = "sha256:95c5c7e03d5d61e9dfb8ef259c699cf492bb1041beb6dbe83696608cec07347a", size = 4048728, upload-time = "2026-04-22T20:59:36.96Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/e4/e228d6d1bbb7fd62dc660a8fb202a583b023d3a3624ca95d1a9290ee4d6a/pyinstaller-6.20.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:bf3be4e1284ee78ddccba5e29f99443a12a7b4673168288ffc4c9d38c6f7b90e", size = 1047642, upload-time = "2026-04-22T20:58:32.006Z" },
{ url = "https://files.pythonhosted.org/packages/ce/bd/afb631bcb3f9040efebd4f6d067f0828b51710818f69fb41a2d4b7787f52/pyinstaller-6.20.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72ae9c1fdea134afa791f58bdc9a1934d5c7609753c111e0026bfc272b32b712", size = 742494, upload-time = "2026-04-22T20:58:36.285Z" },
{ url = "https://files.pythonhosted.org/packages/76/08/0729a5bac14754150e5d83b39d87d842eb42b0bffcaa03dbad6252e23a39/pyinstaller-6.20.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1031bcc307f3fbeffd4e162723e64d46dbf591c82dd0997413afb2a07328b941", size = 754191, upload-time = "2026-04-22T20:58:40.603Z" },
{ url = "https://files.pythonhosted.org/packages/e6/82/bc0ee4c7b97db1958eb651e0da9fb1e672e5ae53ca8867fd97701de52906/pyinstaller-6.20.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:8df3b3f347659fa2562d8d193a98ad4600133b8b8d07c268df89e4154376750e", size = 751902, upload-time = "2026-04-22T20:58:44.7Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e7/770002d6aaa54173881cb2c49bb195ba67b97bf39bac1cdf320f28401629/pyinstaller-6.20.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b0d3cc9dd8120d448459bd3880a12e2f9774c51443af49047801446377999a59", size = 748634, upload-time = "2026-04-22T20:58:48.579Z" },
{ url = "https://files.pythonhosted.org/packages/fe/db/68ba1fccb71278b2124fb90b37b7c8c0bc4c1173fba45b94466df3d9cb7f/pyinstaller-6.20.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03696bb6350177c6bc23bcaf78e71a33c4a89b6754dd90d1be2f318e978c918b", size = 748490, upload-time = "2026-04-22T20:58:52.749Z" },
{ url = "https://files.pythonhosted.org/packages/03/0f/ac77ffa996a56be3d5c8f85734a007f8347240691657f9704e7de2527fa3/pyinstaller-6.20.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:6357f1699f6af84f37e7367f031d4f68abdba65543b83990c9e8f5a4cebed0b7", size = 747650, upload-time = "2026-04-22T20:58:57.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/56/1ee91c3a2bc10ca1f36da10a6fd55ff7efc4dec367171eb25992a827874f/pyinstaller-6.20.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0ab39c690abad26ba148e8f664f0478acc82a733997f4f22e757774832802da9", size = 747413, upload-time = "2026-04-22T20:59:01.174Z" },
{ url = "https://files.pythonhosted.org/packages/d7/55/ae264339996953c4cdf9d89d916a0a8fa26a83cf917a742fff8b9d5f3fe8/pyinstaller-6.20.0-py3-none-win32.whl", hash = "sha256:9a7637e8e44b4387b13667fdcaac86ab6b29c446c16d34d8401539b81838759c", size = 1331584, upload-time = "2026-04-22T20:59:07.201Z" },
{ url = "https://files.pythonhosted.org/packages/76/8c/300f57578882cce259bfb5ae56fda3b69caa3fe9df40a176c719920ea6e2/pyinstaller-6.20.0-py3-none-win_amd64.whl", hash = "sha256:d588844e890ee80c4365867f98146636e1849bbca8e4284bbf0c809aff0f161a", size = 1391851, upload-time = "2026-04-22T20:59:14.024Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ea/b2f8e1642aecda78c0b75c7321f708e49e10bb3c00dd4f148c40761a1527/pyinstaller-6.20.0-py3-none-win_arm64.whl", hash = "sha256:bd53282c0a73e5c95573e1ddc8e5d564d4932bec91efbaed4dc5fdff9c2ae7f2", size = 1332259, upload-time = "2026-04-22T20:59:20.509Z" },
]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2026.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/67/f4452d68793fb15beba4f19ef39a38a8822f0da7452b503c400d5a21f5c1/pyinstaller_hooks_contrib-2026.5.tar.gz", hash = "sha256:f066dfca8f7c45ff6336c9cf9fe25b4e48bfeb322a1aa24faaedfb8a8d1b0b08", size = 173689, upload-time = "2026-05-04T22:36:55.124Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/5c/fd465d11da4d12b50d7eb5d2ee2ceb780d8d049dbb489f3828d131e387af/pyinstaller_hooks_contrib-2026.5-py3-none-any.whl", hash = "sha256:ea1535783fbdac4626351709e83f3ea80b681d3a4745763ebb407b5e27342eb9", size = 457314, upload-time = "2026-05-04T22:36:53.598Z" },
]
[[package]] [[package]]
name = "pyserial" name = "pyserial"
version = "3.5" version = "3.5"
@@ -35,3 +117,21 @@ sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
] ]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
[[package]]
name = "setuptools"
version = "82.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
]