mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
e9dd458699
Provide route replay, log migration and recorded vehicle fixtures for regression checks. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com> Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Kacper Rączy <gfw.kra@gmail.com> Co-authored-by: YassineYousfi <yyousfi1@binghamton.edu> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
"""One renderer-owned RGBA slot; device viewers only read it."""
|
|
from dataclasses import dataclass
|
|
import fcntl
|
|
import math
|
|
import mmap
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import struct
|
|
import time
|
|
|
|
MAGIC = 0x5350525046524D31
|
|
VERSION = 1
|
|
HEADER_SIZE = 4096
|
|
MAX_WIDTH, MAX_HEIGHT = 1600, 800
|
|
HEADER = struct.Struct('=QQIIIIQQQd64s')
|
|
SEQ = struct.Struct('=Q')
|
|
SESSION = re.compile(r'[a-f0-9]{32}\Z')
|
|
PROFILES = {'large': 1, 'compact': 2}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReplayFrame:
|
|
data: bytes
|
|
width: int
|
|
height: int
|
|
session_id: str
|
|
epoch: int
|
|
position: float
|
|
frame_id: int
|
|
captured_ns: int
|
|
|
|
|
|
class ReplayFramePublisher:
|
|
def __init__(self, path: Path, session_id: str, profile: str, width: int, height: int):
|
|
if SESSION.fullmatch(session_id) is None or profile not in PROFILES or not 0 < width <= MAX_WIDTH or not 0 < height <= MAX_HEIGHT:
|
|
raise ValueError('Invalid replay frame geometry or identity')
|
|
self.path, self.session_id, self.profile = Path(path), session_id, profile
|
|
self.width, self.height = width, height
|
|
self.size = HEADER_SIZE + width * height * 4
|
|
self.frame_id = 0
|
|
fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600)
|
|
try:
|
|
os.ftruncate(fd, self.size)
|
|
self.mm = mmap.mmap(fd, self.size)
|
|
except BaseException:
|
|
os.close(fd)
|
|
raise
|
|
self.fd = fd
|
|
|
|
def publish(self, pixels, *, epoch: int, position: float, captured_ns: int):
|
|
if type(epoch) is not int or epoch <= 0 or not math.isfinite(position) or position < 0 or captured_ns <= 0:
|
|
raise ValueError('Invalid replay frame clock')
|
|
if len(pixels) != self.width * self.height * 4:
|
|
raise ValueError('Invalid RGBA replay frame')
|
|
# Kernel locks also establish ordering on ARM; a Python mmap seqlock alone
|
|
# cannot provide acquire/release barriers while a reader copies the pixels.
|
|
deadline = time.monotonic() + .1
|
|
while True:
|
|
try:
|
|
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
break
|
|
except BlockingIOError:
|
|
if time.monotonic() >= deadline:
|
|
raise RuntimeError('Replay frame consumer stalled') from None
|
|
time.sleep(.001)
|
|
try:
|
|
self._publish(pixels, epoch=epoch, position=position, captured_ns=captured_ns)
|
|
finally:
|
|
fcntl.flock(self.fd, fcntl.LOCK_UN)
|
|
|
|
def _publish(self, pixels, *, epoch, position, captured_ns):
|
|
seq = SEQ.unpack_from(self.mm, 8)[0]
|
|
SEQ.pack_into(self.mm, 8, seq | 1)
|
|
self.mm[HEADER_SIZE:] = pixels
|
|
self.frame_id += 1
|
|
HEADER.pack_into(self.mm, 0, MAGIC, seq | 1, VERSION, self.width, self.height, PROFILES[self.profile],
|
|
epoch, self.frame_id, captured_ns, position, self.session_id.encode())
|
|
SEQ.pack_into(self.mm, 8, (seq | 1) + 1)
|
|
|
|
def close(self):
|
|
self.mm.close()
|
|
os.close(self.fd)
|
|
self.path.unlink(missing_ok=True)
|
|
|
|
|
|
class ReplayFrameReader:
|
|
def __init__(self, path: Path, session_id: str, profile: str | None = None):
|
|
if SESSION.fullmatch(session_id) is None or profile is not None and profile not in PROFILES:
|
|
raise ValueError('Invalid replay frame identity')
|
|
self.session_id, self.profile, self.last_key = session_id, profile, None
|
|
self.mm = None
|
|
self.fd = None
|
|
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC)
|
|
try:
|
|
info = os.fstat(fd)
|
|
if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600 or
|
|
info.st_nlink != 1 or not HEADER_SIZE < info.st_size <= HEADER_SIZE + MAX_WIDTH * MAX_HEIGHT * 4):
|
|
raise ValueError('Unsafe replay frame file')
|
|
self.mm = mmap.mmap(fd, info.st_size, access=mmap.ACCESS_READ)
|
|
except BaseException:
|
|
os.close(fd)
|
|
raise
|
|
self.fd = fd
|
|
|
|
def latest(self) -> ReplayFrame | None:
|
|
mm, fd = self.mm, self.fd
|
|
if mm is None or fd is None:
|
|
return None
|
|
try:
|
|
fcntl.flock(fd, fcntl.LOCK_SH | fcntl.LOCK_NB)
|
|
except BlockingIOError:
|
|
return None
|
|
try:
|
|
return self._latest(mm)
|
|
finally:
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
|
|
def _latest(self, mm: mmap.mmap):
|
|
for _ in range(3):
|
|
seq = SEQ.unpack_from(mm, 8)[0]
|
|
if not seq or seq & 1:
|
|
continue
|
|
magic, stamp, version, width, height, profile, epoch, frame_id, captured_ns, position, session = HEADER.unpack_from(mm)
|
|
if (magic != MAGIC or stamp != seq or version != VERSION or profile not in PROFILES.values() or
|
|
self.profile is not None and profile != PROFILES[self.profile] or
|
|
not 0 < width <= MAX_WIDTH or not 0 < height <= MAX_HEIGHT or
|
|
len(mm) != HEADER_SIZE + width * height * 4 or session.rstrip(b'\0') != self.session_id.encode() or
|
|
epoch <= 0 or frame_id <= 0 or captured_ns <= 0 or not math.isfinite(position) or position < 0):
|
|
return None
|
|
key = epoch, frame_id
|
|
if key == self.last_key:
|
|
return None
|
|
data = mm[HEADER_SIZE:]
|
|
if SEQ.unpack_from(mm, 8)[0] != seq:
|
|
continue
|
|
self.last_key = key
|
|
return ReplayFrame(data, width, height, self.session_id, epoch, position, frame_id, captured_ns)
|
|
return None
|
|
|
|
def close(self):
|
|
if self.mm is not None:
|
|
self.mm.close()
|
|
self.mm = None
|
|
if self.fd is not None:
|
|
os.close(self.fd)
|
|
self.fd = None
|