Telemetry receive over serial, parsing and status flash
This commit is contained in:
BIN
src/dataflux.zip
Normal file
BIN
src/dataflux.zip
Normal file
Binary file not shown.
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
1
src/dataflux/telemetry_common
Symbolic link
1
src/dataflux/telemetry_common
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/home/hector/projects/Exergie/TelemetryCommon/python/
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
# 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
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/home/hector/projects/Exergie/TelemetryCommon/
|
|
||||||
Reference in New Issue
Block a user