mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
130 lines
5.1 KiB
Python
130 lines
5.1 KiB
Python
"""One UI publisher for state-bound SLC requests.
|
|
|
|
The planner alone decides whether an action is accepted. This module never
|
|
changes a displayed limit, cruise target, Params value, or camera state.
|
|
"""
|
|
|
|
from collections.abc import Callable
|
|
import math
|
|
import time
|
|
from typing import Any
|
|
|
|
import openpilot.cereal.messaging as messaging
|
|
|
|
from openpilot.starpilot.ui.onroad_state import SlcActionKind, SlcUiRequest
|
|
|
|
|
|
_MAX_STATE_AGE_NS = 100_000_000 # two periods of the 20 Hz slcState service
|
|
_DUPLICATE_WINDOW_NS = 2_000_000_000
|
|
|
|
|
|
def _positive_number(value: Any) -> bool:
|
|
try:
|
|
number = float(value)
|
|
except (TypeError, ValueError, OverflowError):
|
|
return False
|
|
return math.isfinite(number) and number > 0
|
|
|
|
|
|
class SlcActionDispatcher:
|
|
"""Validate a fresh state again at release, then publish one typed request."""
|
|
|
|
def __init__(self, publisher: Any, read_current: Callable[[int], Any | None],
|
|
clock: Callable[[], int] = time.monotonic_ns):
|
|
self.publisher = publisher
|
|
self.read_current = read_current
|
|
self.clock = clock
|
|
self._session = ""
|
|
self._last_sequence = 0
|
|
self._last_context: tuple[str, int, int, SlcActionKind] | None = None
|
|
self._last_context_ns = 0
|
|
self._feedback_sequence = 0
|
|
self._feedback_started_ns = 0
|
|
|
|
def dispatch(self, request: SlcUiRequest) -> bool:
|
|
if not _positive_number(request.candidate_speed_mps):
|
|
return False
|
|
now_ns = self.clock()
|
|
state = self.read_current(now_ns)
|
|
if state is None:
|
|
return False
|
|
try:
|
|
session = str(state.sessionId)
|
|
frame_ns = int(state.frameMonoTime)
|
|
decision_id = int(state.decisionId)
|
|
presentation_id = int(state.presentationId)
|
|
if (not session or session != request.session_id or not bool(state.enabled) or
|
|
str(state.observationKind) != "valid" or str(state.source) == "none" or not 0 < frame_ns <= now_ns or
|
|
now_ns - frame_ns > _MAX_STATE_AGE_NS or
|
|
decision_id != request.decision_id or presentation_id != request.presentation_id):
|
|
return False
|
|
if request.kind in (SlcActionKind.ACCEPT, SlcActionKind.REJECT):
|
|
if (not bool(state.hasPending) or bool(getattr(state, 'automaticAdoptionPending', False)) or
|
|
decision_id <= 0 or not _positive_number(state.pendingSpeedLimit) or
|
|
not math.isclose(float(state.pendingSpeedLimit), request.candidate_speed_mps, abs_tol=0.001)):
|
|
return False
|
|
elif request.kind == SlcActionKind.ADOPT:
|
|
if (bool(state.hasPending) or presentation_id <= 0 or not _positive_number(state.speedLimit) or
|
|
not math.isclose(float(state.speedLimit), request.candidate_speed_mps, abs_tol=0.001)):
|
|
return False
|
|
else:
|
|
return False
|
|
acknowledged_sequence = int(state.actionSequenceId)
|
|
if acknowledged_sequence < 0:
|
|
return False
|
|
except (AttributeError, TypeError, ValueError, OverflowError):
|
|
return False
|
|
|
|
context = (session, decision_id, presentation_id, request.kind)
|
|
if (self._last_context is not None and context[:3] == self._last_context[:3] and
|
|
now_ns - self._last_context_ns < _DUPLICATE_WINDOW_NS and
|
|
(context == self._last_context or acknowledged_sequence < self._last_sequence)):
|
|
return False
|
|
if session != self._session:
|
|
self._session = session
|
|
self._last_sequence = 0
|
|
self._last_context = None
|
|
sequence = max(now_ns, acknowledged_sequence + 1, self._last_sequence + 1, 1)
|
|
try:
|
|
message = messaging.new_message("slcAction", valid=True)
|
|
message.logMonoTime = now_ns
|
|
action = message.slcAction
|
|
action.sessionId = session
|
|
action.sequenceId = sequence
|
|
action.decisionId = decision_id
|
|
action.presentationId = presentation_id
|
|
action.kind = request.kind.value
|
|
self.publisher.send("slcAction", message)
|
|
except (OSError, RuntimeError, ValueError):
|
|
return False
|
|
self._last_sequence = sequence
|
|
self._last_context = context
|
|
self._last_context_ns = now_ns
|
|
self._feedback_sequence = sequence
|
|
self._feedback_started_ns = now_ns
|
|
return True
|
|
|
|
def clear_feedback(self):
|
|
self._feedback_sequence = 0
|
|
|
|
def feedback(self, observation) -> str:
|
|
if observation.session_id != self._session:
|
|
self._feedback_sequence = 0
|
|
if not self._feedback_sequence:
|
|
return ''
|
|
if (observation.pending_speed_limit_mps is not None and self._last_context is not None and
|
|
observation.decision_id != self._last_context[1]):
|
|
self.clear_feedback()
|
|
return ''
|
|
age = self.clock() - self._feedback_started_ns
|
|
if age > 4_000_000_000:
|
|
self._feedback_sequence = 0
|
|
return ''
|
|
if observation.action_sequence_id != self._feedback_sequence:
|
|
return 'Waiting…' if age < 2_000_000_000 else 'Not confirmed'
|
|
if observation.command_action_sequence_id == self._feedback_sequence:
|
|
return {'issued': 'Applying…', 'applied': 'Applied', 'rejected': 'Setpoint not applied',
|
|
'expired': 'Setpoint not applied'}.get(observation.command_status, 'Not confirmed')
|
|
return {'accept': 'Limit accepted', 'reject': 'Set speed kept', 'adopt': 'Limit adopted'}.get(
|
|
observation.action_status, 'Not applied')
|