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).
137 lines
4.6 KiB
Python
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()
|