mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
168 lines
6.1 KiB
Python
168 lines
6.1 KiB
Python
"""One lifecycle-owned message collector for Galaxy's fresh authority readers."""
|
|
|
|
import threading
|
|
import time
|
|
from types import MappingProxyType, SimpleNamespace
|
|
|
|
from openpilot.starpilot.parked_evidence import RESUME_SKEW_NS
|
|
|
|
|
|
class EvidenceSnapshot(SimpleNamespace):
|
|
def __setattr__(self, name, value):
|
|
if getattr(self, '_frozen', False):
|
|
raise AttributeError('Evidence snapshot is immutable')
|
|
super().__setattr__(name, value)
|
|
|
|
def __getitem__(self, name):
|
|
return self.data[name]
|
|
|
|
|
|
class EvidenceSource:
|
|
SERVICES = ('deviceState', 'pandaStates', 'carState', 'selfdriveState', 'carParams', 'peripheralState')
|
|
|
|
def __init__(self, messages=None, *, mono=time.monotonic_ns,
|
|
boot=lambda: time.clock_gettime_ns(getattr(time, 'CLOCK_BOOTTIME', time.CLOCK_MONOTONIC))):
|
|
if messages is None:
|
|
from openpilot.cereal import messaging
|
|
messages = messaging.SubMaster(list(self.SERVICES))
|
|
self.messages, self.mono, self.boot = messages, mono, boot
|
|
self.after_mono_ns = mono()
|
|
self.offset = self.device_offset_ns = None
|
|
self.lock = threading.Lock()
|
|
self.changed = threading.Condition(self.lock)
|
|
self.wake = threading.Event()
|
|
self.stop = threading.Event()
|
|
self.thread = None
|
|
self.closed = self.failed = False
|
|
self.polls = 0
|
|
self.observations = dict.fromkeys(('deviceState', 'pandaStates'), 0)
|
|
|
|
def _clock_pair(self):
|
|
# Discard preempted measurements before deciding whether the device resumed.
|
|
for _ in range(3):
|
|
before, boot, now = self.mono(), self.boot(), self.mono()
|
|
if 0 <= now - before <= RESUME_SKEW_NS:
|
|
break
|
|
return before, boot, now
|
|
|
|
def poll(self):
|
|
with self.lock:
|
|
if self.closed:
|
|
return
|
|
self.polls += 1
|
|
try:
|
|
self.messages.update(0)
|
|
except (OSError, RuntimeError, AttributeError, KeyError, TypeError, ValueError):
|
|
self.after_mono_ns, self.device_offset_ns, self.failed = self.mono(), None, True
|
|
self.changed.notify_all()
|
|
return
|
|
self.failed = False
|
|
before, boot, now = self._clock_pair()
|
|
offset = boot - (before + now) // 2
|
|
if not 0 <= now - before <= RESUME_SKEW_NS or self.offset is not None and abs(offset - self.offset) > RESUME_SKEW_NS:
|
|
self.after_mono_ns, self.device_offset_ns = now, None
|
|
self.offset = offset
|
|
if self.messages.updated['deviceState'] and int(self.messages.logMonoTime['deviceState']) > self.after_mono_ns:
|
|
self.device_offset_ns = offset
|
|
for name in self.observations:
|
|
if self.messages.updated[name]:
|
|
self.observations[name] += 1
|
|
self.changed.notify_all()
|
|
|
|
def refresh(self, timeout_ms=750):
|
|
deadline = time.monotonic() + timeout_ms / 1000
|
|
with self.changed:
|
|
if self.closed or self.thread is None or not self.thread.is_alive():
|
|
return False
|
|
before, boot, now = self._clock_pair()
|
|
if not 0 <= now - before <= RESUME_SKEW_NS:
|
|
return False
|
|
offset = boot - (before + now) // 2
|
|
barrier, polls = self.after_mono_ns, self.polls
|
|
observations = dict(self.observations)
|
|
self.wake.set()
|
|
while True:
|
|
if (self.closed or not self.thread.is_alive() or self.after_mono_ns != barrier or
|
|
self.offset is not None and abs(self.offset - offset) > RESUME_SKEW_NS):
|
|
return False
|
|
if self.polls > polls:
|
|
if self.failed:
|
|
return False
|
|
if (all(self.observations[name] > count for name, count in observations.items()) and
|
|
int(self.messages.logMonoTime['deviceState']) > now and
|
|
int(self.messages.logMonoTime['pandaStates']) > boot):
|
|
return True
|
|
remaining = deadline - time.monotonic()
|
|
if remaining <= 0:
|
|
return False
|
|
self.changed.wait(remaining)
|
|
|
|
def start(self):
|
|
with self.lock:
|
|
if self.closed:
|
|
raise RuntimeError('Galaxy evidence reader is closed')
|
|
if self.thread is not None:
|
|
return self
|
|
self.poll()
|
|
def run():
|
|
while not self.stop.is_set():
|
|
self.wake.wait(0.1)
|
|
self.wake.clear()
|
|
if self.stop.is_set():
|
|
break
|
|
try:
|
|
self.poll()
|
|
except (OSError, RuntimeError, AttributeError, KeyError, TypeError, ValueError):
|
|
with self.lock:
|
|
self.after_mono_ns, self.device_offset_ns = self.mono(), None
|
|
self.failed = True
|
|
self.changed.notify_all()
|
|
self.thread = threading.Thread(target=run, name='galaxy-evidence', daemon=True)
|
|
try:
|
|
self.thread.start()
|
|
except BaseException:
|
|
self.close()
|
|
raise
|
|
return self
|
|
|
|
def snapshot(self):
|
|
with self.lock:
|
|
if self.closed or self.failed:
|
|
return None
|
|
before, boot, now = self._clock_pair()
|
|
offset = boot - (before + now) // 2
|
|
if (not 0 <= now - before <= RESUME_SKEW_NS or self.offset is None or
|
|
abs(offset - self.offset) > RESUME_SKEW_NS):
|
|
self.after_mono_ns, self.device_offset_ns, self.offset = now, None, offset
|
|
return None
|
|
if self.device_offset_ns is None:
|
|
return None
|
|
sm = self.messages
|
|
values = {name: sm[name] for name in self.SERVICES}
|
|
metadata = {name: MappingProxyType(dict(getattr(sm, name)))
|
|
for name in ('seen', 'alive', 'valid', 'updated', 'logMonoTime', 'recv_time')}
|
|
captured = EvidenceSnapshot(**metadata,
|
|
after_mono_ns=self.after_mono_ns, device_offset_ns=self.device_offset_ns)
|
|
captured.data = MappingProxyType(values)
|
|
captured._frozen = True
|
|
return captured
|
|
|
|
def close(self):
|
|
self.stop.set()
|
|
self.wake.set()
|
|
with self.lock:
|
|
if self.closed:
|
|
return
|
|
self.closed = True
|
|
self.changed.notify_all()
|
|
if self.thread is not None and self.thread.ident is not None:
|
|
self.thread.join(timeout=1)
|
|
if self.thread.is_alive():
|
|
raise RuntimeError('Galaxy evidence reader did not stop')
|
|
with self.lock:
|
|
self.closed = True
|
|
for socket in getattr(self.messages, 'sock', {}).values():
|
|
close = getattr(socket, 'close', None)
|
|
if close is not None:
|
|
close()
|