diff --git a/assets/fonts/JetBrainsMono-Regular.ttf b/assets/fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..436c982 Binary files /dev/null and b/assets/fonts/JetBrainsMono-Regular.ttf differ diff --git a/src/dataflux/app.py b/src/dataflux/app.py index f3dcf97..a275e47 100644 --- a/src/dataflux/app.py +++ b/src/dataflux/app.py @@ -9,6 +9,7 @@ import dearpygui.dearpygui as dpg from dataflux.state import AppState import dataflux.config +from dataflux.tags import TEXT_SERIAL_CONSOLE import dataflux.ui.windows import dataflux.ui.worker import dataflux.services.telemetry @@ -34,8 +35,7 @@ def _asset_path(relative_path: str) -> str: return str(candidate) searched = ", ".join(str(candidate) for candidate in candidates) - raise FileNotFoundError( - f"Missing asset {relative_path!r}. Searched: {searched}") + raise FileNotFoundError(f"Missing asset {relative_path!r}. Searched: {searched}") def run() -> None: @@ -61,8 +61,12 @@ def run() -> None: # Add Inter font to registry and bind as main app font with dpg.font_registry(): - app_font = dpg.add_font(_asset_path( - "assets/fonts/Inter-Regular.ttf"), 18 * 2) + app_font = dpg.add_font(_asset_path("assets/fonts/Inter-Regular.ttf"), 18 * 2) + mono_font = dpg.add_font( + _asset_path("assets/fonts/JetBrainsMono-Regular.ttf"), + size=36, + label="mono_font", + ) dpg.bind_font(app_font) dataflux.ui.windows.build_windows(state) @@ -74,6 +78,7 @@ def run() -> None: vp_h = dpg.get_viewport_client_height() dpg.configure_item("main_window", pos=(0, 0), width=vp_w, height=vp_h) 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 @@ -82,8 +87,7 @@ def run() -> None: state.telemetry_thread_running = True state.telemetry_thread = Thread( - target=dataflux.services.telemetry.telemetry_worker, args=( - state,), daemon=True + target=dataflux.services.telemetry.telemetry_worker, args=(state,), daemon=True ) state.telemetry_thread.start() diff --git a/src/dataflux/callbacks/menu.py b/src/dataflux/callbacks/menu.py index f9ba794..0725589 100644 --- a/src/dataflux/callbacks/menu.py +++ b/src/dataflux/callbacks/menu.py @@ -15,17 +15,28 @@ from dataflux.tags import ( GRAPH_X_AXIS_SPEED, GRAPH_X_AXIS_TENG, GRAPH_X_AXIS_VBAT, - WINDOW_CONNECTION_MENU, + 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 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 menu_file_disconnect(sender, app_data, user_data) -> None: +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) update_global_connection_status(user_data) diff --git a/src/dataflux/callbacks/serial.py b/src/dataflux/callbacks/serial.py index b122f9d..138d0c0 100644 --- a/src/dataflux/callbacks/serial.py +++ b/src/dataflux/callbacks/serial.py @@ -7,12 +7,23 @@ import dataflux.services.serial import dataflux.ui.routines from dataflux.state import AppState -from dataflux.tags import WINDOW_CONNECTION_MENU, WINDOW_CONNECTION_MENU_COMBO +from dataflux.tags import ( + 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: - 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.ui.routines.update_global_connection_status(user_data) - dpg.hide_item(WINDOW_CONNECTION_MENU) - + dpg.hide_item(WINDOW_SERIAL_CONNECTION_MENU) diff --git a/src/dataflux/services/serial/__init__.py b/src/dataflux/services/serial/__init__.py index bc6d636..f2d04e1 100644 --- a/src/dataflux/services/serial/__init__.py +++ b/src/dataflux/services/serial/__init__.py @@ -2,12 +2,17 @@ # Copyright (C) 2026 Association Exergie # SPDX-License-Identifier: GPL-3.0-or-later +from concurrent.futures import thread +import os from queue import Empty from sys import base_exec_prefix from threading import Thread +import time from serial import Serial import serial.tools.list_ports +import dearpygui.dearpygui as dpg from dataflux import telemetry_common +from dataflux.tags import TEXT_SERIAL_CONSOLE import dataflux.telemetry_common.telemetry_common import dataflux.ui.routines.status import dataflux.ui.routines @@ -24,39 +29,96 @@ def list_serial_ports() -> list[str]: 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: if state.serial_port is not None: state.serial_port.close() state.serial_port = None state.serial_port = Serial(port=device, baudrate=115200) - state.serial_thread = Thread(target=serial_reader_worker, args=(state,), daemon=True) - state.serial_status_thread = Thread(target=serial_status_worker, args=(state,), daemon=True) + state.serial_thread = Thread( + target=serial_reader_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_status_thread.start() state.serial_thread.start() + +def disconnect_lora(state: AppState) -> None: + if state.lora_port is not None: + state.lora_thread_running = False + state.lora_port.close() + state.lora_port = None + + def disconnect_serial(state: AppState) -> None: if state.serial_port is not None: state.serial_thread_running = False state.serial_port.close() state.serial_port = None -def serial_status_worker(state: AppState) -> None: - while state.serial_thread_running: + +def lora_status_worker(state: AppState) -> None: + while state.lora_thread_running: try: - duration = state.serial_status_queue.get(timeout=0.1) + duration = state.lora_status_queue.get(timeout=0.1) except Empty: continue dataflux.ui.routines.status.flash_status_connection_status(duration) - - def serial_reader_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 + + line = port.readline() + + if line: + text = line.decode("utf-8", errors="replace") + + print(text, end="") + + old = dpg.get_value(TEXT_SERIAL_CONSOLE) + dpg.set_value(TEXT_SERIAL_CONSOLE, old + text) + disconnect_serial(state) + dataflux.ui.routines.update_global_connection_status(state) + + +def serial_status_worker(state: AppState) -> None: + while state.serial_thread_running: + time.sleep(1) + + +def lora_reader_worker(state: AppState) -> None: + while state.lora_thread_running: + port = state.lora_port if port is None: break if port.closed: @@ -71,14 +133,14 @@ def serial_reader_worker(state: AppState) -> None: parsed = parse_uart_packet(packet) if parsed is not None: state.packet_queue.put(parsed) - state.serial_status_queue.put(0.1) + state.lora_status_queue.put(0.1) except Exception as e: print(f"Serial parser error: {e}") break - disconnect_serial(state) + disconnect_lora(state) dataflux.ui.routines.update_global_connection_status(state) - + def read_one_uart_packet(port: Serial) -> bytes | None: first = port.read(1) @@ -107,19 +169,24 @@ def read_one_uart_packet(port: Serial) -> bytes | None: return body + def parse_uart_packet(body: bytes) -> dict | None: if len(body) < dataflux.telemetry_common.telemetry_common.LORA_HEADER_SIZE: return None - 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:] + 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 :] 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 calc_crc = dataflux.telemetry_common.telemetry_common.crc16_ccitt(payload) - + if calc_crc != lora.crc16: print("crc mismatch") return None @@ -129,13 +196,13 @@ def parse_uart_packet(body: bytes) -> dict | None: "dest": lora.dest, "version": lora.version, } - + if lora.version == 1: pkt = dataflux.telemetry_common.telemetry_common.unpack_packet1(payload) return { **base, "type": "packet1", - "ping": pkt.ping.decode("ascii", errors="replace") + "ping": pkt.ping.decode("ascii", errors="replace"), } if lora.version == 2: @@ -153,5 +220,3 @@ def parse_uart_packet(body: bytes) -> dict | None: print("Unknown payload") return None - - diff --git a/src/dataflux/services/telemetry/__init__.py b/src/dataflux/services/telemetry/__init__.py index 24e8235..087e69c 100644 --- a/src/dataflux/services/telemetry/__init__.py +++ b/src/dataflux/services/telemetry/__init__.py @@ -11,7 +11,7 @@ import csv def telemetry_worker(state: AppState): while state.telemetry_thread_running: - if not state.serial_thread_running: + if not state.lora_thread_running: time.sleep(1) continue try: diff --git a/src/dataflux/state.py b/src/dataflux/state.py index 340e1d6..dcc7ac9 100644 --- a/src/dataflux/state.py +++ b/src/dataflux/state.py @@ -22,6 +22,10 @@ class Buffers: class AppState: running: bool = True + lora_port: Serial | None = None + lora_thread: Thread | None = None + lora_thread_running: bool = False + serial_port: Serial | None = None serial_thread: Thread | None = None serial_thread_running: bool = False @@ -29,6 +33,9 @@ class AppState: telemetry_thread: Thread | None = None 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_queue: Queue = field(default_factory=Queue) diff --git a/src/dataflux/tags.py b/src/dataflux/tags.py index ad7cbb9..10c8eba 100644 --- a/src/dataflux/tags.py +++ b/src/dataflux/tags.py @@ -2,13 +2,19 @@ # Copyright (C) 2026 Association Exergie # SPDX-License-Identifier: GPL-3.0-or-later -MENU_FILE_CONNECT: str = "menu_file_connect" -MENU_FILE_DISCONNECT: str = "menu_file_disconnect" +MENU_IO_CONNECT_LORA: str = "menu_io_connect_lora" +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" -WINDOW_CONNECTION_MENU: str = "window_connection_menu" -WINDOW_CONNECTION_MENU_COMBO: str = "window_connection_menu_combo" +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" +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_TEXT: str = "status_serial_status_text" @@ -20,6 +26,9 @@ LIVE_DATA_TENG_VALUE: str = "live_data_teng_value" PAGE_LIVE_DATA: str = "page_live_data" PAGE_LAP_RECAP: str = "page_lap_recap" +PAGE_SERIAL_CONSOLE: str = "page_serial_console" + +TEXT_SERIAL_CONSOLE: str = "text_serial_console" SUB_PAGE_DATA_GRAPHS: str = "sub_page_data_graphs" SUB_PAGE_MAP: str = "sub_page_map" diff --git a/src/dataflux/ui/routines/menu.py b/src/dataflux/ui/routines/menu.py index a59c7eb..9be89ed 100644 --- a/src/dataflux/ui/routines/menu.py +++ b/src/dataflux/ui/routines/menu.py @@ -5,12 +5,25 @@ import dearpygui.dearpygui as dpg 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: - if state.serial_port is None: - dpg.enable_item(MENU_FILE_CONNECT) - dpg.disable_item(MENU_FILE_DISCONNECT) + if state.lora_port is None: + dpg.enable_item(MENU_IO_CONNECT_LORA) + dpg.disable_item(MENU_IO_DISCONNECT_LORA) else: - dpg.disable_item(MENU_FILE_CONNECT) - dpg.enable_item(MENU_FILE_DISCONNECT) + dpg.disable_item(MENU_IO_CONNECT_LORA) + 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) diff --git a/src/dataflux/ui/routines/status.py b/src/dataflux/ui/routines/status.py index 1eb0d0d..06f0c27 100644 --- a/src/dataflux/ui/routines/status.py +++ b/src/dataflux/ui/routines/status.py @@ -4,10 +4,26 @@ import dearpygui.dearpygui as dpg 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 + 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: dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_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.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) - sleep(duration) - dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_CONNECTED) + dpg.bind_item_theme(STATUS_LORA_STATUS_BOX, THEME_STATUS_CONNECTED_BRIGHT) + sleep(duration) + dpg.bind_item_theme(STATUS_LORA_STATUS_BOX, THEME_STATUS_CONNECTED) diff --git a/src/dataflux/ui/routines/windows.py b/src/dataflux/ui/routines/windows.py index 2af80be..b103ca0 100644 --- a/src/dataflux/ui/routines/windows.py +++ b/src/dataflux/ui/routines/windows.py @@ -5,21 +5,47 @@ import dearpygui.dearpygui as dpg import dataflux.config 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: + +def update_window_lora_connection_menu_combo(state: AppState) -> None: ports: list[str] = list_serial_ports() - dpg.configure_item(WINDOW_CONNECTION_MENU_COMBO, items=ports) + 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) + + +def update_window_serial_connection_menu_combo(state: AppState) -> None: + ports: list[str] = list_serial_ports() + 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) 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: if tag == item: dpg.show_item(item) else: dpg.hide_item(item) + def toggle_window(tag: str) -> None: if tag == SUB_PAGE_DATA_GRAPHS: dpg.show_item(SUB_PAGE_DATA_GRAPHS) @@ -31,6 +57,3 @@ def toggle_window(tag: str) -> None: hide_all_but(PAGE_LIVE_DATA) else: hide_all_but(tag) - - - diff --git a/src/dataflux/ui/windows.py b/src/dataflux/ui/windows.py index a0c285e..786deee 100644 --- a/src/dataflux/ui/windows.py +++ b/src/dataflux/ui/windows.py @@ -22,21 +22,29 @@ from dataflux.tags import ( LIVE_DATA_VBAT_VALUE, LIVE_DATA_VEHICLE_TIME_VALUE, LIVE_DATA_SPEED_VALUE, - MENU_FILE_CONNECT, - MENU_FILE_DISCONNECT, + 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_CONNECTION_MENU, - WINDOW_CONNECTION_MENU_COMBO, + 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 @@ -57,19 +65,6 @@ def build_windows(state: AppState) -> None: dpg.set_global_font_scale(0.5) with dpg.menu_bar(): with dpg.menu(label="File"): - dpg.add_menu_item( - label="Connect", - enabled=True, - tag=MENU_FILE_CONNECT, - callback=dataflux.callbacks.menu.open_connection_window, - ) - dpg.add_menu_item( - label="Disonnect", - enabled=False, - tag=MENU_FILE_DISCONNECT, - callback=dataflux.callbacks.menu.menu_file_disconnect, - user_data=state, - ) dpg.add_menu_item( label="Dump Buffers", enabled=True, @@ -77,6 +72,35 @@ def build_windows(state: AppState) -> None: callback=dataflux.callbacks.menu.menu_file_dump_buffers, ) dpg.add_menu_item(label="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", @@ -93,6 +117,11 @@ def build_windows(state: AppState) -> None: 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( @@ -255,6 +284,18 @@ def build_windows(state: AppState) -> None: dpg.add_text("Lap Recap") dpg.add_separator() + with dpg.group(tag=PAGE_SERIAL_CONSOLE, show=False): + with dpg.child_window( + 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(width=-100) + dpg.add_button(label="Send", width=100) + with dpg.theme(tag=THEME_STATUS_CONNECTED): with dpg.theme_component(dpg.mvChildWindow): dpg.add_theme_color(dpg.mvThemeCol_ChildBg, STATUS_GREEN_DARK) @@ -275,6 +316,33 @@ def build_windows(state: AppState) -> None: no_scrollbar=True, ): with dpg.group(horizontal=True): + with dpg.child_window( + 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(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( + "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 ): @@ -295,27 +363,43 @@ def build_windows(state: AppState) -> None: 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(): pass + dpg.bind_item_theme(STATUS_LORA_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, + label="LoRa Connection Menu", + tag=WINDOW_LORA_CONNECTION_MENU, show=False, modal=True, no_collapse=True, - width=300, + width=400, + no_resize=True, ): - dpg.add_combo([], tag=WINDOW_CONNECTION_MENU_COMBO) + 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, diff --git a/src/dataflux/ui/worker.py b/src/dataflux/ui/worker.py index 32de0a7..f8c5f15 100644 --- a/src/dataflux/ui/worker.py +++ b/src/dataflux/ui/worker.py @@ -8,7 +8,17 @@ import time from datetime import datetime, timezone from dataflux.state import AppState -from dataflux.tags import GRAPH_SERIES_SPEED, GRAPH_SERIES_TENG, GRAPH_SERIES_VBAT, GRAPH_X_AXIS_SPEED, LIVE_DATA_TENG_VALUE, LIVE_DATA_UTC_TIME_VALUE, LIVE_DATA_VBAT_VALUE, LIVE_DATA_VEHICLE_TIME_VALUE, LIVE_DATA_SPEED_VALUE +from dataflux.tags import ( + GRAPH_SERIES_SPEED, + GRAPH_SERIES_TENG, + GRAPH_SERIES_VBAT, + GRAPH_X_AXIS_SPEED, + LIVE_DATA_TENG_VALUE, + LIVE_DATA_UTC_TIME_VALUE, + LIVE_DATA_VBAT_VALUE, + LIVE_DATA_VEHICLE_TIME_VALUE, + LIVE_DATA_SPEED_VALUE, +) def ui_worker(state: AppState): @@ -18,15 +28,15 @@ def ui_worker(state: AppState): last_vbat: str = "" last_teng: str = "" no_data_written = False - while state.running: + while state.running: now = datetime.now(timezone.utc) formatted = now.strftime("%H:%M:%S") - + if formatted != last_datetime: dpg.set_value(LIVE_DATA_UTC_TIME_VALUE, formatted) last_datetime = formatted - - if state.serial_thread_running and state.telemetry_valid: + + if state.lora_thread_running and state.telemetry_valid: x_common: list[float] | None = None speed_y: list[float] | None = None vbat_y: list[float] | None = None @@ -59,7 +69,6 @@ def ui_worker(state: AppState): dpg.set_value(LIVE_DATA_SPEED_VALUE, formatted) last_veh_speed = formatted - # VBAT formatted = f"{vbat:05.2f}" if formatted != last_vbat: @@ -87,7 +96,3 @@ def ui_worker(state: AppState): no_data_written = True time.sleep(0.05) - - - -