Compare commits
11 Commits
a9fc152724
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f5ecf6d60 | |||
| 91d841cf1c | |||
| a69b45ed27 | |||
| e5fd5cda89 | |||
| d56c7605a1 | |||
| 7ad0917237 | |||
| 5c840538fb | |||
| d61ad11428 | |||
| cc99d26f9e | |||
|
|
d4b9304d99 | ||
|
|
a9858e88f8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,3 +8,6 @@ wheels/
|
|||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
|
# Build generated files
|
||||||
|
*.spec
|
||||||
|
|||||||
8
Makefile
Normal file
8
Makefile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
APP_NAME = DataFlux
|
||||||
|
ENTRY = main.py
|
||||||
|
|
||||||
|
build:
|
||||||
|
uv run pyinstaller --onedir --windowed --name $(APP_NAME) --paths src --add-data "assets/fonts:assets/fonts" --add-data "assets/images:assets/images" $(ENTRY)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build dist *.spec
|
||||||
BIN
assets/fonts/JetBrainsMono-Regular.ttf
Normal file
BIN
assets/fonts/JetBrainsMono-Regular.ttf
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 MiB After Width: | Height: | Size: 8.9 MiB |
@@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,33 +2,73 @@
|
|||||||
# 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
|
||||||
|
import sys
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
def _asset_path(relative_path: str) -> str:
|
||||||
|
path = Path(relative_path)
|
||||||
|
candidates: list[Path] = []
|
||||||
|
|
||||||
|
bundle_dir = getattr(sys, "_MEIPASS", None)
|
||||||
|
if bundle_dir is not None:
|
||||||
|
candidates.append(Path(bundle_dir) / path)
|
||||||
|
|
||||||
|
candidates.extend(
|
||||||
|
(
|
||||||
|
Path.cwd() / path,
|
||||||
|
Path(__file__).resolve().parents[2] / path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.exists():
|
||||||
|
return str(candidate)
|
||||||
|
|
||||||
|
searched = ", ".join(str(candidate) for candidate in candidates)
|
||||||
|
raise FileNotFoundError(f"Missing asset {relative_path!r}. Searched: {searched}")
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
width, height, channels, data = dpg.load_image("map.png")
|
map_path = _asset_path("assets/images/map.png")
|
||||||
|
map_image = dpg.load_image(map_path)
|
||||||
|
if map_image is None:
|
||||||
|
raise RuntimeError(f"Failed to load map image: {map_path}")
|
||||||
|
width, height, channels, data = map_image
|
||||||
dataflux.config.MAP_IMAGE_WIDTH = width
|
dataflux.config.MAP_IMAGE_WIDTH = width
|
||||||
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("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)
|
||||||
@@ -36,23 +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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
/Users/hector/Projects/Exergie/TelemetryCommon/python
|
/home/hector/projects/Exergie/TelemetryCommon/python/
|
||||||
@@ -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)
|
||||||
|
|||||||
22
src/dataflux/ui/routines/serial.py
Normal file
22
src/dataflux/ui/routines/serial.py
Normal 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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
100
uv.lock
generated
@@ -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" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user