Telemetry receive over serial, parsing and status flash

This commit is contained in:
2026-04-06 00:45:03 +02:00
parent 3f9a0126fa
commit 716c32b0ce
9 changed files with 146 additions and 10 deletions

BIN
src/dataflux.zip Normal file

Binary file not shown.

View File

@@ -2,8 +2,14 @@
# 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
from sys import base_exec_prefix
from threading import Thread
from serial import Serial from serial import Serial
import serial.tools.list_ports 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 from dataflux.state import AppState
@@ -23,11 +29,122 @@ def connect_serial(state: AppState, device: str) -> None:
state.serial_port = None state.serial_port = None
state.serial_port = Serial(port=device, baudrate=115200) 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: 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_port.close() state.serial_port.close()
state.serial_port = None 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

View File

@@ -5,6 +5,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from threading import Lock, Thread from threading import Lock, Thread
from serial import Serial from serial import Serial
from queue import Queue
@dataclass @dataclass
class AppState: class AppState:
@@ -12,7 +13,14 @@ class AppState:
serial_port: Serial | None = None serial_port: Serial | None = None
serial_thread: Thread | None = None serial_thread: Thread | None = None
serial_thread_running: bool = False
telemetry_thread: Thread | None = None 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) lock: Lock = field(default_factory=Lock)

View File

@@ -12,3 +12,4 @@ STATUS_SERIAL_STATUS_TEXT: str = "status_serial_status_text"
THEME_STATUS_DISCONNECTED: str = "theme_status_disconnected" THEME_STATUS_DISCONNECTED: str = "theme_status_disconnected"
THEME_STATUS_CONNECTED: str = "theme_status_connected" THEME_STATUS_CONNECTED: str = "theme_status_connected"
THEME_STATUS_CONNECTED_BRIGHT: str = "theme_status_connected_bigght"

View File

@@ -0,0 +1 @@
/home/hector/projects/Exergie/TelemetryCommon/python/

View File

@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
STATUS_RED_DARK = (140, 35, 35, 255) STATUS_RED_DARK = (140, 35, 35, 255)
STATUS_RED_BRIGHT = (255, 70, 70, 255) STATUS_RED_BRIGHT = (205, 85, 85, 255)
STATUS_ORANGE_DARK = (160, 90, 20, 255) STATUS_ORANGE_DARK = (160, 90, 20, 255)
STATUS_ORANGE_BRIGHT= (255, 165, 40, 255) STATUS_ORANGE_BRIGHT = (210, 140, 60, 255)
STATUS_GREEN_DARK = (40, 130, 55, 255) STATUS_GREEN_DARK = (40, 130, 55, 255)
STATUS_GREEN_BRIGHT = (70, 255, 110, 255) STATUS_GREEN_BRIGHT = (95, 185, 115, 255)

View File

@@ -4,7 +4,8 @@
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_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): def update_status_connection_status(state: AppState):
if state.serial_port is None: if state.serial_port is None:
@@ -13,3 +14,8 @@ def update_status_connection_status(state: AppState):
else: else:
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)
sleep(duration)
dpg.bind_item_theme(STATUS_SERIAL_STATUS_BOX, THEME_STATUS_CONNECTED)

View File

@@ -7,8 +7,8 @@ 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 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.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_DARK, STATUS_RED_DARK from dataflux.ui.colors import STATUS_GREEN_BRIGHT, STATUS_GREEN_DARK, STATUS_RED_DARK
def build_windows(state: AppState) -> None: def build_windows(state: AppState) -> None:
@@ -44,6 +44,10 @@ 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_GREEN_DARK) 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(tag=THEME_STATUS_DISCONNECTED):
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)

View File

@@ -1 +0,0 @@
/home/hector/projects/Exergie/TelemetryCommon/