Files
MicrOBU/microbu-esp32c5/station-link/python/microbu_link/serial_transport.py
T
Ashin Walpola 0e9525162d Keep the colleague's microbu-esp32c5 tree in this repository
obu-firmware builds against vanetza-idf from microbu-esp32c5/external, but
that tree was gitignored, so a clone of this repository could not build the
firmware it ships. It is now committed here as ordinary files in its own
folder, microbu-esp32c5/: the colleague's commit cf4b99f plus the V2X2MAP
bridge's signature verification (--trust) used on the bench. Nothing is
fetched from or pushed to the colleague's repository; this repository and
its remotes carry everything. The folder's own .gitignore keeps build output,
downloaded components and private key material out, as it did there; the
committed file set is identical to that repository's tracked files.

The ESP32-C5 is still flashed from obu-firmware/, which only takes
vanetza-idf from microbu-esp32c5/, so the two stay separate folders.
FLASHING.md says how to take a newer version of the colleague's tree (copy
it over the folder, rebuild, test, commit).
2026-09-23 17:46:40 +02:00

137 lines
4.6 KiB
Python

"""Serial transport of the station-internal link (the app's Phase 03 framing).
``[AA][55][type][length LE][payload][crc16 LE]``, CRC-16/CCITT-FALSE over type+length+payload.
Frame types: 0x10 link message, 0x11 test channel, 0x7F log line. Requires pyserial.
"""
from __future__ import annotations
import enum
import struct
import threading
from typing import Callable, List, Tuple
MAXIMUM_PAYLOAD = 1536
class FrameType(enum.IntEnum):
LINK = 0x10
TEST = 0x11
LOG = 0x7F
def crc16_ccitt_false(data: bytes) -> int:
crc = 0xFFFF
for octet in data:
crc ^= octet << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
return crc
def encode_frame(frame_type: int, payload: bytes) -> bytes:
if len(payload) > MAXIMUM_PAYLOAD:
raise ValueError('frame payload exceeds %d octets' % MAXIMUM_PAYLOAD)
head = struct.pack('<BH', int(frame_type), len(payload)) + payload
return b'\xaa\x55' + head + struct.pack('<H', crc16_ccitt_false(head))
class FrameDecoder:
"""Byte-at-a-time state machine, the app's SerialFrameDecoder with the larger payload limit."""
def __init__(self):
self.state = 'SYNC0'
self.type = 0
self.length = 0
self.payload = bytearray()
self.crc = 0
self.crc_errors = 0
self.frames = 0
def feed(self, data: bytes) -> List[Tuple[int, bytes]]:
out = []
for b in data:
s = self.state
if s == 'SYNC0':
self.state = 'SYNC1' if b == 0xAA else 'SYNC0'
elif s == 'SYNC1':
self.state = 'TYPE' if b == 0x55 else ('SYNC1' if b == 0xAA else 'SYNC0')
elif s == 'TYPE':
self.type = b
self.state = 'LEN_LO'
elif s == 'LEN_LO':
self.length = b
self.state = 'LEN_HI'
elif s == 'LEN_HI':
self.length |= b << 8
self.payload = bytearray()
if self.length > MAXIMUM_PAYLOAD:
self.state = 'SYNC0'
else:
self.state = 'CRC_LO' if self.length == 0 else 'PAYLOAD'
elif s == 'PAYLOAD':
self.payload.append(b)
if len(self.payload) >= self.length:
self.state = 'CRC_LO'
elif s == 'CRC_LO':
self.crc = b
self.state = 'CRC_HI'
elif s == 'CRC_HI':
self.crc |= b << 8
head = struct.pack('<BH', self.type, self.length) + bytes(self.payload)
if crc16_ccitt_false(head) == self.crc:
self.frames += 1
out.append((self.type, bytes(self.payload)))
else:
self.crc_errors += 1
self.state = 'SYNC0'
return out
class SerialTransport:
"""Reader thread plus a locked writer over one pyserial port."""
def __init__(self, port: str, on_frame: Callable[[int, bytes], None], baudrate: int = 115200):
import serial
self.port = serial.Serial(port=None, baudrate=baudrate, timeout=0.05, write_timeout=2)
# do not toggle DTR/RTS: the USB Serial/JTAG controller would reset the board
self.port.dtr = False
self.port.rts = False
self.port.port = port
self.port.open()
self.on_frame = on_frame
self.decoder = FrameDecoder()
self.write_lock = threading.Lock()
self.running = True
self.thread = threading.Thread(target=self._reader, name='microbu-link-rx', daemon=True)
self.thread.start()
def _reader(self):
while self.running:
try:
data = self.port.read(4096)
except Exception:
if self.running:
continue
return
if data:
for frame_type, payload in self.decoder.feed(data):
try:
self.on_frame(frame_type, payload)
except Exception as error: # a handler failure must not stop the reader
print('frame handler error:', error)
def write(self, frame_type: int, payload: bytes):
frame = encode_frame(frame_type, payload)
with self.write_lock:
if self.port.write(frame) != len(frame):
raise IOError('incomplete serial write')
def close(self):
self.running = False
try:
self.port.cancel_read()
except Exception:
pass
self.thread.join(timeout=1)
self.port.close()