diff --git a/src/dataflux.zip b/src/dataflux.zip new file mode 100644 index 0000000..b4e749f Binary files /dev/null and b/src/dataflux.zip differ diff --git a/src/dataflux/services/serial/__init__.py b/src/dataflux/services/serial/__init__.py index 7e2f0b7..57f44f4 100644 --- a/src/dataflux/services/serial/__init__.py +++ b/src/dataflux/services/serial/__init__.py @@ -2,8 +2,14 @@ # Copyright (C) 2026 Association Exergie # SPDX-License-Identifier: GPL-3.0-or-later +from queue import Empty +from sys import base_exec_prefix +from threading import Thread from serial import Serial import serial.tools.list_ports +from dataflux import telemetry_common +import dataflux.telemetry_common.telemetry_common +import dataflux.ui.routines.status from dataflux.state import AppState @@ -23,11 +29,122 @@ def connect_serial(state: AppState, device: str) -> None: 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_running = True + state.serial_status_thread.start() + state.serial_thread.start() 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: + try: + duration = state.serial_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 + + try: + packet = read_one_uart_packet(port) + if packet is None: + continue + + parsed = parse_uart_packet(packet) + if parsed is not None: + state.packet_queue.put(parsed) + state.serial_status_queue.put(0.1) + print(parsed) + + except Exception as e: + print(f"Serial parser error: {e}") + +def read_one_uart_packet(port: Serial) -> bytes | None: + first = port.read(1) + if not first: + return None + + if first != dataflux.telemetry_common.telemetry_common.UART_MAGIC[:1]: + return None + + rest_magic = port.read(3) + if len(rest_magic) != 3: + return None + + if first + rest_magic != dataflux.telemetry_common.telemetry_common.UART_MAGIC: + return None + + size_bytes = port.read(1) + if len(size_bytes) != 1: + return None + + body_size = size_bytes[0] + + body = port.read(body_size) + if len(body) != body_size: + return 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:] + + if lora.size != 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 + + base = { + "source": lora.source, + "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") + } + + if lora.version == 2: + pkt = dataflux.telemetry_common.telemetry_common.unpack_packet2(payload) + return { + **base, + "type": "packet2", + "time_stamp": pkt.time_stamp, + "vbat": pkt.vbat, + "teng": pkt.teng, + "lat": pkt.lat, + "lng": pkt.lng, + "speed": pkt.speed, + } + + print("Unknown payload") + return None diff --git a/src/dataflux/state.py b/src/dataflux/state.py index eddd313..89cec5b 100644 --- a/src/dataflux/state.py +++ b/src/dataflux/state.py @@ -5,6 +5,7 @@ from dataclasses import dataclass, field from threading import Lock, Thread from serial import Serial +from queue import Queue @dataclass class AppState: @@ -12,7 +13,14 @@ class AppState: serial_port: Serial | None = None serial_thread: Thread | None = None + serial_thread_running: bool = False telemetry_thread: Thread | None = None + serial_status_thread: Thread | None = None + serial_status_queue: Queue = field(default_factory=Queue) + + packet_queue: Queue = field(default_factory=Queue) + latest_telemetry: dict = field(default_factory=dict) + lock: Lock = field(default_factory=Lock) diff --git a/src/dataflux/tags.py b/src/dataflux/tags.py index 7097382..57cfb1c 100644 --- a/src/dataflux/tags.py +++ b/src/dataflux/tags.py @@ -12,3 +12,4 @@ STATUS_SERIAL_STATUS_TEXT: str = "status_serial_status_text" 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/telemetry_common b/src/dataflux/telemetry_common new file mode 120000 index 0000000..edc567c --- /dev/null +++ b/src/dataflux/telemetry_common @@ -0,0 +1 @@ +/home/hector/projects/Exergie/TelemetryCommon/python/ \ No newline at end of file diff --git a/src/dataflux/ui/colors.py b/src/dataflux/ui/colors.py index bebb68b..6fe3e23 100644 --- a/src/dataflux/ui/colors.py +++ b/src/dataflux/ui/colors.py @@ -2,11 +2,11 @@ # Copyright (C) 2026 Association Exergie # SPDX-License-Identifier: GPL-3.0-or-later -STATUS_RED_DARK = (140, 35, 35, 255) -STATUS_RED_BRIGHT = (255, 70, 70, 255) +STATUS_RED_DARK = (140, 35, 35, 255) +STATUS_RED_BRIGHT = (205, 85, 85, 255) -STATUS_ORANGE_DARK = (160, 90, 20, 255) -STATUS_ORANGE_BRIGHT= (255, 165, 40, 255) +STATUS_ORANGE_DARK = (160, 90, 20, 255) +STATUS_ORANGE_BRIGHT = (210, 140, 60, 255) -STATUS_GREEN_DARK = (40, 130, 55, 255) -STATUS_GREEN_BRIGHT = (70, 255, 110, 255) +STATUS_GREEN_DARK = (40, 130, 55, 255) +STATUS_GREEN_BRIGHT = (95, 185, 115, 255) diff --git a/src/dataflux/ui/routines/status.py b/src/dataflux/ui/routines/status.py index b4b6e66..1eb0d0d 100644 --- a/src/dataflux/ui/routines/status.py +++ b/src/dataflux/ui/routines/status.py @@ -4,7 +4,8 @@ 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_DISCONNECTED +from dataflux.tags import 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.serial_port is None: @@ -13,3 +14,8 @@ def update_status_connection_status(state: AppState): else: 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) diff --git a/src/dataflux/ui/windows.py b/src/dataflux/ui/windows.py index bad3f7a..0d59605 100644 --- a/src/dataflux/ui/windows.py +++ b/src/dataflux/ui/windows.py @@ -7,8 +7,8 @@ import dataflux.callbacks.menu import dataflux.callbacks.serial from dataflux.state import AppState -from dataflux.tags import MENU_FILE_CONNECT, MENU_FILE_DISCONNECT, STATUS_SERIAL_STATUS_BOX, STATUS_SERIAL_STATUS_TEXT, THEME_STATUS_CONNECTED, THEME_STATUS_DISCONNECTED, WINDOW_CONNECTION_MENU, WINDOW_CONNECTION_MENU_COMBO -from dataflux.ui.colors import STATUS_GREEN_DARK, STATUS_RED_DARK +from dataflux.tags import MENU_FILE_CONNECT, MENU_FILE_DISCONNECT, 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 +from dataflux.ui.colors import STATUS_GREEN_BRIGHT, STATUS_GREEN_DARK, STATUS_RED_DARK def build_windows(state: AppState) -> None: @@ -44,6 +44,10 @@ def build_windows(state: AppState) -> None: 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) diff --git a/telemetry_common b/telemetry_common deleted file mode 120000 index 66ae1b3..0000000 --- a/telemetry_common +++ /dev/null @@ -1 +0,0 @@ -/home/hector/projects/Exergie/TelemetryCommon/ \ No newline at end of file