# Copyright (C) 2026 Hector van der Aa # Copyright (C) 2026 Association Exergie # SPDX-License-Identifier: GPL-3.0-or-later import dearpygui.dearpygui as dpg 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, 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: with dpg.table_row(): with dpg.table_cell(): dpg.add_text(label) with dpg.table_cell(): dpg.add_text(value, tag=tag) with dpg.table_cell(): dpg.add_text(units) def build_windows(state: AppState) -> None: with dpg.window(label="DataFlux", tag="main_window", no_collapse=True): 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, 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 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.menu(label="Data"): with dpg.menu(label="Timeframe"): dpg.add_menu_item(label="30s") dpg.add_menu_item(label="60s") 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(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, ): dpg.add_table_column(width_fixed=True) dpg.add_table_column( width_stretch=True, init_width_or_weight=1.0 ) dpg.add_table_column(width_fixed=True) _add_live_data_row( "UTC Time", "no_data", LIVE_DATA_UTC_TIME_VALUE, "" ) _add_live_data_row( "Vehicle Time", "no_data", LIVE_DATA_VEHICLE_TIME_VALUE, "", ) _add_live_data_row( "Speed", "no_data", LIVE_DATA_SPEED_VALUE, "km/h" ) _add_live_data_row( "Battery Voltage", "no_data", LIVE_DATA_VBAT_VALUE, "V" ) _add_live_data_row( "Engine Temp", "no_data", LIVE_DATA_TENG_VALUE, "°C" ) with dpg.child_window( tag=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 ) 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_X_AXIS_SPEED, ymin=-30, ymax=0) dpg.add_line_series( [], [], parent=y_axis_speed, tag=GRAPH_SERIES_SPEED ) with dpg.plot( label="Battery Voltage", height=250, width=-1, no_inputs=True, ): dpg.add_plot_legend() dpg.add_plot_axis( dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_VBAT ) y_axis_vbat = dpg.add_plot_axis( dpg.mvYAxis, label="Voltage", tag=GRAPH_Y_AXIS_VBAT ) dpg.set_axis_limits(GRAPH_Y_AXIS_VBAT, ymin=0, ymax=20) dpg.set_axis_limits(GRAPH_X_AXIS_VBAT, ymin=-30, ymax=0) dpg.add_line_series( [], [], parent=y_axis_vbat, tag=GRAPH_SERIES_VBAT ) with dpg.plot( label="Engine Temp", height=250, width=-1, no_inputs=True ): dpg.add_plot_legend() dpg.add_plot_axis( dpg.mvXAxis, label="Time", tag=GRAPH_X_AXIS_TENG ) y_axis_teng = dpg.add_plot_axis( dpg.mvYAxis, label="Temperature", tag=GRAPH_Y_AXIS_TENG ) dpg.set_axis_limits(GRAPH_Y_AXIS_TENG, ymin=0, ymax=120) dpg.set_axis_limits(GRAPH_X_AXIS_TENG, ymin=-30, ymax=0) dpg.add_line_series( [], [], parent=y_axis_teng, tag=GRAPH_SERIES_TENG ) with dpg.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() with dpg.theme(tag=THEME_STATUS_CONNECTED): with dpg.theme_component(dpg.mvChildWindow): dpg.add_theme_color(dpg.mvThemeCol_ChildBg, STATUS_GREEN_DARK) with dpg.theme(tag=THEME_STATUS_CONNECTED_BRIGHT): with dpg.theme_component(dpg.mvChildWindow): dpg.add_theme_color(dpg.mvThemeCol_ChildBg, STATUS_GREEN_BRIGHT) with dpg.theme(tag=THEME_STATUS_DISCONNECTED): with dpg.theme_component(dpg.mvChildWindow): 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.group(horizontal=True): 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(): pass 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, ): dpg.add_combo([], tag=WINDOW_CONNECTION_MENU_COMBO) dpg.add_button( label="Connect", callback=dataflux.callbacks.serial.connection_window_connect_serial, user_data=state, ) with dpg.file_dialog( directory_selector=False, show=False, tag=WINDOW_FILE_DIALOG_DUMP_BUFFERS, width=700, height=400, modal=True, callback=dataflux.callbacks.menu.window_file_dialog_dump_buffers_ok, user_data=state, ): dpg.add_file_extension(".csv")