Files
MoreTore/common/realtime.py
T
2025-06-01 20:34:03 -05:00

181 lines
4.7 KiB
Python

"""Utilities for reading real time clocks and keeping soft real time constraints."""
import gc
import os
import time
from collections import deque
from setproctitle import getproctitle
from openpilot.system.hardware import PC
# time step for each process
DT_CTRL = 0.01 # controlsd
DT_MDL = 0.05 # model
DT_HW = 0.5 # hardwared and manager
DT_DMON = 0.05 # driver monitoring
class Priority:
# CORE 2
# - modeld = 55
# - camerad = 54
CTRL_LOW = 51 # plannerd & radard
# CORE 3
# - pandad = 55
CTRL_HIGH = 53
def set_realtime_priority(level: int) -> None:
if not PC:
os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(level))
def set_core_affinity(cores: list[int]) -> None:
if not PC:
os.sched_setaffinity(0, cores)
def config_realtime_process(cores: int | list[int], priority: int) -> None:
gc.disable()
set_realtime_priority(priority)
c = cores if isinstance(cores, list) else [cores, ]
set_core_affinity(c)
class Ratekeeper:
def __init__(self, rate: float, print_delay_threshold: float | None = 0.0) -> None:
"""Rate in Hz for ratekeeping. print_delay_threshold must be nonnegative."""
self._interval = 1. / rate
self._next_frame_time = time.monotonic() + self._interval
self._print_delay_threshold = print_delay_threshold
self._frame = 0
self._remaining = 0.0
self._process_name = getproctitle()
self._dts = deque([self._interval], maxlen=100)
self._last_monitor_time = time.monotonic()
@property
def frame(self) -> int:
return self._frame
@property
def remaining(self) -> float:
return self._remaining
@property
def lagging(self) -> bool:
avg_dt = sum(self._dts) / len(self._dts)
expected_dt = self._interval * (1 / 0.9)
return avg_dt > expected_dt
# Maintain loop rate by calling this at the end of each loop
def keep_time(self) -> bool:
lagged = self.monitor_time()
if self._remaining > 0:
time.sleep(self._remaining)
return lagged
# Monitors the cumulative lag, but does not enforce a rate
def monitor_time(self) -> bool:
prev = self._last_monitor_time
self._last_monitor_time = time.monotonic()
self._dts.append(self._last_monitor_time - prev)
lagged = False
remaining = self._next_frame_time - time.monotonic()
self._next_frame_time += self._interval
if self._print_delay_threshold is not None and remaining < -self._print_delay_threshold:
print(f"{self._process_name} lagging by {-remaining * 1000:.2f} ms")
lagged = True
self._frame += 1
self._remaining = remaining
return lagged
class DurationTimer:
def __init__(self, duration=0, step=DT_CTRL) -> None:
self.step = step
self.duration = duration
self.was_reset = False
self.timer = 0
self.min = float("-inf") # type: float
self.max = float("inf") # type: float
def tick_obj(self) -> None:
self.timer += self.step
# reset on overflow
self.timer = 0 if (self.timer == (self.max or self.min)) else self.timer
def reset(self) -> None:
"""Resets this objects timer"""
self.timer = 0
self.was_reset = True
def active(self) -> bool:
"""Returns true if time since last reset is less than duration"""
return bool(round(self.timer,2) < self.duration)
def adjust(self, duration) -> None:
"""Adjusts the duration of the timer"""
self.duration = duration
def once_after_reset(self) -> bool:
"""Returns true only one time after calling reset()"""
ret = self.was_reset
self.was_reset = False
return ret
@staticmethod
def interval_obj(rate, frame) -> bool:
if frame % rate == 0: # Highlighting shows "frame" in white
return True
return False
class ModelTimer(DurationTimer):
frame: int = 0
objects: list = []
def __init__(self, duration=0) -> None:
self.step = DT_MDL
super().__init__(duration, self.step)
self.__class__.objects.append(self)
@classmethod
def tick(cls) -> None:
cls.frame += 1
for obj in cls.objects:
ModelTimer.tick_obj(obj)
@classmethod
def reset_all(cls) -> None:
for obj in cls.objects:
obj.reset()
@classmethod
def interval(cls, rate) -> bool:
return ModelTimer.interval_obj(rate, cls.frame)
class ControlsTimer(DurationTimer):
frame = 0
objects = [] # type: list[DurationTimer]
def __init__(self, duration=0) -> None:
self.step = DT_CTRL
super().__init__(duration=duration, step=self.step)
self.__class__.objects.append(self)
@classmethod
def tick(cls) -> None:
cls.frame += 1
for obj in cls.objects:
ControlsTimer.tick_obj(obj)
@classmethod
def reset_all(cls) -> None:
for obj in cls.objects:
obj.reset()
@classmethod
def interval(cls, rate) -> bool:
return ControlsTimer.interval_obj(rate, cls.frame)