diff --git a/map.png b/map.png new file mode 100644 index 0000000..a728d0f Binary files /dev/null and b/map.png differ diff --git a/src/dataflux/app.py b/src/dataflux/app.py index 1669bee..b2162dc 100644 --- a/src/dataflux/app.py +++ b/src/dataflux/app.py @@ -6,6 +6,7 @@ from threading import Thread import dearpygui.dearpygui as dpg from dataflux.state import AppState +import dataflux.config import dataflux.ui.windows import dataflux.ui.worker import dataflux.services.telemetry @@ -15,6 +16,14 @@ def run() -> None: # Create application context and viewport dpg.create_context() + + width, height, channels, data = dpg.load_image("map.png") + dataflux.config.MAP_IMAGE_WIDTH = width + dataflux.config.MAP_IMAGE_HEIGHT = height + + with dpg.texture_registry(show=False): + dpg.add_static_texture(width=width, height=height, default_value=data, tag="texture_tab") + dpg.create_viewport(title='DataFlux', width=600, height=600) # Add Inter font to registry and bind as main app font @@ -47,4 +56,3 @@ def run() -> None: - diff --git a/src/dataflux/callbacks/menu.py b/src/dataflux/callbacks/menu.py index b97ceac..60abfd4 100644 --- a/src/dataflux/callbacks/menu.py +++ b/src/dataflux/callbacks/menu.py @@ -27,3 +27,6 @@ def window_file_dialog_dump_buffers_ok(sender, app_data, user_data: AppState) -> user_data.buffer_dump_thread = Thread(target=dataflux.services.telemetry.buffer_dump, args=(user_data, app_data["file_path_name"]), daemon=True) user_data.buffer_dump_thread.start() +def menu_window_select(sender, app_data, user_data: str) -> None: + dataflux.ui.routines.windows.toggle_window(user_data) + diff --git a/src/dataflux/config.py b/src/dataflux/config.py index e69de29..1c94706 100644 --- a/src/dataflux/config.py +++ b/src/dataflux/config.py @@ -0,0 +1,2 @@ +MAP_IMAGE_WIDTH: int = 1 +MAP_IMAGE_HEIGHT: int = 1 diff --git a/src/dataflux/services/telemetry/__init__.py b/src/dataflux/services/telemetry/__init__.py index dd6474c..412af46 100644 --- a/src/dataflux/services/telemetry/__init__.py +++ b/src/dataflux/services/telemetry/__init__.py @@ -3,18 +3,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later from queue import Empty -from threading import local from dataflux.state import AppState, Buffers import time from pathlib import Path import csv -def hhmmsscc_to_day_seconds(value: int) -> int: - hours = value // 1000000 - minutes = (value // 10000) % 100 - seconds = (value // 100) % 100 - - return (hours * 3600) + (minutes * 60) + seconds +LIVE_BUFFER_WINDOW_CS = 30 * 100 def telemetry_worker(state: AppState): while state.telemetry_thread_running: @@ -32,7 +26,7 @@ def telemetry_worker(state: AppState): with state.lock: - state.raw_buffers.timestamp.append(hhmmsscc_to_day_seconds(dataframe["time_stamp"])) + state.raw_buffers.timestamp.append(dataframe["time_stamp"]) state.raw_buffers.speed.append(dataframe["speed"]) state.raw_buffers.vbat.append(dataframe["vbat"]) state.raw_buffers.teng.append(dataframe["teng"]) @@ -51,12 +45,13 @@ def telemetry_worker(state: AppState): return last_timestamp = state.raw_buffers.timestamp[-1] - cutoff = last_timestamp - 30 + cutoff = last_timestamp - LIVE_BUFFER_WINDOW_CS i = len(state.raw_buffers.timestamp) - 1 while i >= 0 and state.raw_buffers.timestamp[i] >= cutoff: - state.live_buffers.timestamp.append(state.raw_buffers.timestamp[i] - last_timestamp) + elapsed_seconds = (state.raw_buffers.timestamp[i] - last_timestamp) / 100.0 + state.live_buffers.timestamp.append(elapsed_seconds) state.live_buffers.speed.append(state.raw_buffers.speed[i]) state.live_buffers.vbat.append(state.raw_buffers.vbat[i]) state.live_buffers.teng.append(state.raw_buffers.teng[i]) @@ -72,7 +67,6 @@ def telemetry_worker(state: AppState): state.live_buffers.lng.reverse() def buffer_dump(state: AppState, path: str): - print(path) save_path = Path(path) if save_path.is_dir(): save_path = save_path / "output.csv" @@ -87,8 +81,6 @@ def buffer_dump(state: AppState, path: str): lng=list(state.raw_buffers.lng), ) - print(local_raw_buffers.timestamp) - with save_path.open("w", newline="", encoding="utf-8") as f: writer = csv.writer(f) @@ -100,4 +92,3 @@ def buffer_dump(state: AppState, path: str): state.buffer_dump_thread = None - diff --git a/src/dataflux/tags.py b/src/dataflux/tags.py index df431d1..ad7cbb9 100644 --- a/src/dataflux/tags.py +++ b/src/dataflux/tags.py @@ -18,6 +18,12 @@ LIVE_DATA_SPEED_VALUE: str = "live_data_speed_value" LIVE_DATA_VBAT_VALUE: str = "live_data_vbat_value" LIVE_DATA_TENG_VALUE: str = "live_data_teng_value" +PAGE_LIVE_DATA: str = "page_live_data" +PAGE_LAP_RECAP: str = "page_lap_recap" + +SUB_PAGE_DATA_GRAPHS: str = "sub_page_data_graphs" +SUB_PAGE_MAP: str = "sub_page_map" + THEME_STATUS_DISCONNECTED: str = "theme_status_disconnected" THEME_STATUS_CONNECTED: str = "theme_status_connected" THEME_STATUS_CONNECTED_BRIGHT: str = "theme_status_connected_bigght" diff --git a/src/dataflux/ui/routines/windows.py b/src/dataflux/ui/routines/windows.py index 53b4178..2af80be 100644 --- a/src/dataflux/ui/routines/windows.py +++ b/src/dataflux/ui/routines/windows.py @@ -3,9 +3,34 @@ # SPDX-License-Identifier: GPL-3.0-or-later import dearpygui.dearpygui as dpg +import dataflux.config from dataflux.services.serial import list_serial_ports -from dataflux.tags import WINDOW_CONNECTION_MENU_COMBO +from dataflux.tags import PAGE_LAP_RECAP, PAGE_LIVE_DATA, SUB_PAGE_DATA_GRAPHS, SUB_PAGE_MAP, WINDOW_CONNECTION_MENU_COMBO def update_window_connection_menu_combo() -> None: ports: list[str] = list_serial_ports() dpg.configure_item(WINDOW_CONNECTION_MENU_COMBO, items=ports) + + +def hide_all_but(tag: str) -> None: + arr = [PAGE_LIVE_DATA, PAGE_LAP_RECAP] + for item in arr: + if tag == item: + dpg.show_item(item) + else: + dpg.hide_item(item) + +def toggle_window(tag: str) -> None: + if tag == SUB_PAGE_DATA_GRAPHS: + dpg.show_item(SUB_PAGE_DATA_GRAPHS) + dpg.hide_item(SUB_PAGE_MAP) + hide_all_but(PAGE_LIVE_DATA) + elif tag == SUB_PAGE_MAP: + dpg.show_item(SUB_PAGE_MAP) + dpg.hide_item(SUB_PAGE_DATA_GRAPHS) + hide_all_but(PAGE_LIVE_DATA) + else: + hide_all_but(tag) + + + diff --git a/src/dataflux/ui/windows.py b/src/dataflux/ui/windows.py index bb73cb8..96b0bc4 100644 --- a/src/dataflux/ui/windows.py +++ b/src/dataflux/ui/windows.py @@ -7,7 +7,7 @@ import dataflux.callbacks.menu import dataflux.callbacks.serial 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, STATUS_SERIAL_STATUS_BOX, STATUS_SERIAL_STATUS_TEXT, THEME_STATUS_CONNECTED, THEME_STATUS_CONNECTED_BRIGHT, THEME_STATUS_DISCONNECTED, WINDOW_CONNECTION_MENU, WINDOW_CONNECTION_MENU_COMBO, WINDOW_FILE_DIALOG_DUMP_BUFFERS +from dataflux.tags import GRAPH_SERIES_SPEED, GRAPH_SERIES_TENG, GRAPH_SERIES_VBAT, GRAPH_X_AXIS_SPEED, GRAPH_X_AXIS_TENG, GRAPH_X_AXIS_VBAT, GRAPH_Y_AXIS_SPEED, GRAPH_Y_AXIS_TENG, GRAPH_Y_AXIS_VBAT, LIVE_DATA_TENG_VALUE, LIVE_DATA_UTC_TIME_VALUE, LIVE_DATA_VBAT_VALUE, LIVE_DATA_VEHICLE_TIME_VALUE, LIVE_DATA_SPEED_VALUE, MENU_FILE_CONNECT, MENU_FILE_DISCONNECT, MENU_FILE_DUMP_BUFFERS, PAGE_LAP_RECAP, PAGE_LIVE_DATA, STATUS_SERIAL_STATUS_BOX, STATUS_SERIAL_STATUS_TEXT, SUB_PAGE_DATA_GRAPHS, SUB_PAGE_MAP, THEME_STATUS_CONNECTED, THEME_STATUS_CONNECTED_BRIGHT, THEME_STATUS_DISCONNECTED, WINDOW_CONNECTION_MENU, WINDOW_CONNECTION_MENU_COMBO, WINDOW_FILE_DIALOG_DUMP_BUFFERS from dataflux.ui.colors import STATUS_GREEN_BRIGHT, STATUS_GREEN_DARK, STATUS_RED_DARK def _add_live_data_row(label: str, value: str, tag: str, units: str) -> None: @@ -29,11 +29,12 @@ def build_windows(state: AppState) -> None: dpg.add_menu_item(label="Dump Buffers", enabled=True, tag=MENU_FILE_DUMP_BUFFERS, callback=dataflux.callbacks.menu.menu_file_dump_buffers) dpg.add_menu_item(label="Quit") with dpg.menu(label='Window'): - dpg.add_menu_item(label="Live Data", user_data="page_live_data") - dpg.add_menu_item(label="Lap Recap", user_data="page_lap_recap") + 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) 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.child_window(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): @@ -46,7 +47,7 @@ def build_windows(state: AppState) -> None: _add_live_data_row("Battery Voltage", "no_data", LIVE_DATA_VBAT_VALUE,"V") _add_live_data_row("Engine Temp", "no_data", LIVE_DATA_TENG_VALUE,"°C") - with dpg.child_window(tag="data_graphs", autosize_x=True, autosize_y=True, border=True): + with dpg.child_window(tag=SUB_PAGE_DATA_GRAPHS, autosize_x=True, autosize_y=True, border=True, show=True): with dpg.plot(label="Speed", height=250, width=-1, no_inputs=True): dpg.add_plot_legend() dpg.add_plot_axis(dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_SPEED) @@ -69,7 +70,12 @@ def build_windows(state: AppState) -> None: dpg.set_axis_limits(GRAPH_X_AXIS_TENG, ymin=-30, ymax=0) dpg.add_line_series([], [], parent=y_axis_teng, tag=GRAPH_SERIES_TENG) - with dpg.group(tag="page_lap_recap", show=False): + with dpg.child_window(tag=SUB_PAGE_MAP, autosize_x=True, autosize_y=True, border=True, show=False, no_scrollbar=True): + with dpg.drawlist(width=500, height=500, tag="map_drawlist"): + dpg.draw_image("texture_tab", (0, 0), (500, 500)) + dpg.draw_circle((0, 0), 10, color=(255, 0, 0, 255), fill=(255, 0, 0, 255)) + + with dpg.group(tag=PAGE_LAP_RECAP, show=False): dpg.add_text("Lap Recap") dpg.add_separator() diff --git a/src/dataflux/ui/worker.py b/src/dataflux/ui/worker.py index 65bc96e..ce48a51 100644 --- a/src/dataflux/ui/worker.py +++ b/src/dataflux/ui/worker.py @@ -26,6 +26,10 @@ def ui_worker(state: AppState): last_datetime = formatted if state.serial_thread_running and state.telemetry_valid: + x_common: list[float] | None = None + speed_y: list[float] | None = None + vbat_y: list[float] | None = None + teng_y: list[float] | None = None with state.lock: # Vehicle Time no_data_written = False @@ -33,14 +37,16 @@ def ui_worker(state: AppState): veh_speed = state.latest_telemetry["speed"] vbat = state.latest_telemetry["vbat"] teng = state.latest_telemetry["teng"] - x_common = state.live_buffers.timestamp - speed_y = state.live_buffers.speed - vbat_y = state.live_buffers.vbat - teng_y = state.live_buffers.teng + if state.live_buffers_updated: + x_common = list(state.live_buffers.timestamp) + speed_y = list(state.live_buffers.speed) + vbat_y = list(state.live_buffers.vbat) + teng_y = list(state.live_buffers.teng) + state.live_buffers_updated = False - hours = veh_time // 1000000 - minutes = (veh_time // 10000) % 100 - seconds = (veh_time // 100) % 100 + hours = veh_time // 360000 + minutes = (veh_time % 360000) // 6000 + seconds = (veh_time % 6000) // 100 formatted = f"{hours:02d}:{minutes:02d}:{seconds:02d}" if formatted != last_veh_time: dpg.set_value(LIVE_DATA_VEHICLE_TIME_VALUE, formatted) @@ -65,12 +71,10 @@ def ui_worker(state: AppState): dpg.set_value(LIVE_DATA_TENG_VALUE, formatted) last_teng = formatted - if state.live_buffers_updated: + if x_common is not None: dpg.set_value(GRAPH_SERIES_SPEED, [x_common, speed_y]) dpg.set_value(GRAPH_SERIES_VBAT, [x_common, vbat_y]) dpg.set_value(GRAPH_SERIES_TENG, [x_common, teng_y]) - with state.lock: - state.live_buffers_updated = False else: if not no_data_written: