mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 21:13:41 +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>
126 lines
4.8 KiB
Python
126 lines
4.8 KiB
Python
"""Read the owned replay publisher's clock without modifying recorded messages."""
|
|
|
|
from dataclasses import dataclass
|
|
import math
|
|
import mmap
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import struct
|
|
import sys
|
|
import time
|
|
|
|
|
|
MAGIC = 0x53505250434C4B31
|
|
VALID, PAUSED, SEEKING, BOOT_KNOWN = 1, 2, 4, 8
|
|
RECORD = struct.Struct("=QQIIQQQdqQQ")
|
|
HEARTBEAT_MAX_AGE_NS = 250_000_000
|
|
# Native camerad/pandad envelopes keep their recorded BOOTTIME domain.
|
|
# Python display services retain MONOTONIC; do not fabricate an offset.
|
|
BOOT_TIME_SERVICES = frozenset(('pandaStates', 'narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState',
|
|
'narrowRoadEncodeIdx', 'wideRoadEncodeIdx', 'cabinEncodeIdx',
|
|
'qNarrowRoadEncodeIdx'))
|
|
_PREFIX = re.compile(r"replay-[A-Za-z0-9_-]{1,48}\Z")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ClockSample:
|
|
now_ns: int | None
|
|
boot_ns: int | None
|
|
host_ns: int
|
|
epoch: int | None = None
|
|
paused: bool = False
|
|
valid: bool = False
|
|
|
|
|
|
class DisplayClockReader:
|
|
@staticmethod
|
|
def enabled(env=None) -> bool:
|
|
env = os.environ if env is None else env
|
|
prefix = env.get("OPENPILOT_PREFIX", "")
|
|
root = Path("/tmp" if sys.platform == "darwin" else "/dev/shm")
|
|
path = root / ("msgq_" + prefix) / "display-clock"
|
|
return ((env.get("SP_HOST_RUNTIME") == "1" or env.get("SP_REPLAY_CONTEXT") == "1") and bool(_PREFIX.fullmatch(prefix)) and
|
|
env.get("SP_REPLAY_CLOCK_PATH") == str(path))
|
|
|
|
def __init__(self, env=None):
|
|
env = os.environ if env is None else env
|
|
self._mapping = None
|
|
prefix = env.get("OPENPILOT_PREFIX", "")
|
|
root = Path("/tmp" if sys.platform == "darwin" else "/dev/shm")
|
|
parent = root / ("msgq_" + prefix)
|
|
if not self.enabled(env):
|
|
return
|
|
self._parent = parent
|
|
self._closed = False
|
|
self._next_open_ns = 0
|
|
self._open()
|
|
|
|
def _open(self):
|
|
parent = self._parent
|
|
directory = file = None
|
|
try:
|
|
directory = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC)
|
|
metadata = os.fstat(directory)
|
|
if metadata.st_uid != os.getuid() or stat.S_IMODE(metadata.st_mode) != 0o700:
|
|
return
|
|
file = os.open("display-clock", os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=directory)
|
|
metadata = os.fstat(file)
|
|
if (not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != os.getuid() or
|
|
stat.S_IMODE(metadata.st_mode) != 0o600 or metadata.st_nlink != 1 or metadata.st_size != RECORD.size):
|
|
return
|
|
self._mapping = mmap.mmap(file, RECORD.size, access=mmap.ACCESS_READ)
|
|
except (OSError, ValueError):
|
|
return
|
|
finally:
|
|
if file is not None:
|
|
os.close(file)
|
|
if directory is not None:
|
|
os.close(directory)
|
|
|
|
def close(self):
|
|
self._closed = True
|
|
if self._mapping is not None:
|
|
self._mapping.close()
|
|
self._mapping = None
|
|
|
|
def sample(self, host_ns: int | None = None) -> ClockSample:
|
|
implicit_host = host_ns is None
|
|
host_ns = time.monotonic_ns() if host_ns is None else host_ns
|
|
missing = ClockSample(None, None, host_ns)
|
|
if type(host_ns) is not int or host_ns <= 0:
|
|
return missing
|
|
if self._mapping is None and not getattr(self, "_closed", True) and host_ns >= self._next_open_ns:
|
|
self._next_open_ns = host_ns + 250_000_000
|
|
self._open()
|
|
if self._mapping is None:
|
|
return missing
|
|
for _ in range(4):
|
|
before = struct.unpack_from("=Q", self._mapping)[0]
|
|
if not before or before % 2:
|
|
continue
|
|
values = RECORD.unpack(self._mapping[:RECORD.size])
|
|
after = struct.unpack_from("=Q", self._mapping)[0]
|
|
if before == values[0] == after and not after % 2:
|
|
break
|
|
else:
|
|
return missing
|
|
# A heartbeat may advance while the record is copied; date the coherent snapshot after the read.
|
|
if implicit_host:
|
|
host_ns = time.monotonic_ns()
|
|
missing = ClockSample(None, None, host_ns)
|
|
_, magic, version, flags, epoch, route_anchor, host_anchor, speed, boot_offset, heartbeat, reserved = values
|
|
if (magic != MAGIC or version != 1 or flags & ~15 or reserved or not epoch or
|
|
not 0 < host_anchor <= heartbeat <= host_ns or host_ns - heartbeat > HEARTBEAT_MAX_AGE_NS or
|
|
not math.isfinite(speed) or speed <= 0 or flags & BOOT_KNOWN and boot_offset < 0):
|
|
return missing
|
|
paused = bool(flags & PAUSED)
|
|
if not flags & VALID or flags & SEEKING or not route_anchor:
|
|
return ClockSample(None, None, host_ns, epoch, paused)
|
|
now_ns = route_anchor if paused else route_anchor + int((host_ns - host_anchor) * speed)
|
|
boot_ns = now_ns + boot_offset if flags & BOOT_KNOWN else None
|
|
if not 0 < now_ns < 2**64 or boot_ns is not None and not 0 < boot_ns < 2**64:
|
|
return missing
|
|
return ClockSample(now_ns, boot_ns, host_ns, epoch, paused, True)
|