54e6124925
version: sunnypilot v2026.001.000 (dev) date: 2026-05-07T23:07:19 master commit: c28eb958740187620f2282023b8f1997cf90f583
191 lines
7.4 KiB
Python
191 lines
7.4 KiB
Python
"""
|
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
|
|
|
This file is part of sunnypilot and is licensed under the MIT License.
|
|
See the LICENSE.md file in the root directory for more details.
|
|
"""
|
|
import time
|
|
|
|
import cereal.messaging as messaging
|
|
from cereal import custom
|
|
from openpilot.common.constants import CV
|
|
from openpilot.common.gps import get_gps_location_service
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.realtime import DT_MDL
|
|
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD, get_sanitize_int_param
|
|
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC
|
|
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Policy, OffsetType
|
|
|
|
SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source
|
|
|
|
ALL_SOURCES = tuple(SpeedLimitSource.schema.enumerants.values())
|
|
|
|
|
|
class SpeedLimitResolver:
|
|
limit_solutions: dict[custom.LongitudinalPlanSP.SpeedLimit.Source, float]
|
|
distance_solutions: dict[custom.LongitudinalPlanSP.SpeedLimit.Source, float]
|
|
v_ego: float
|
|
speed_limit: float
|
|
speed_limit_last: float
|
|
speed_limit_final: float
|
|
speed_limit_final_last: float
|
|
distance: float
|
|
source: custom.LongitudinalPlanSP.SpeedLimit.Source
|
|
speed_limit_offset: float
|
|
|
|
def __init__(self):
|
|
self.params = Params()
|
|
self.frame = -1
|
|
|
|
self._gps_location_service = get_gps_location_service(self.params)
|
|
self.limit_solutions = {} # Store for speed limit solutions from different sources
|
|
self.distance_solutions = {} # Store for distance to current speed limit start for different sources
|
|
|
|
self.policy = self.params.get("SpeedLimitPolicy", return_default=True)
|
|
self.policy = get_sanitize_int_param(
|
|
"SpeedLimitPolicy",
|
|
Policy.min().value,
|
|
Policy.max().value,
|
|
self.params
|
|
)
|
|
self._policy_to_sources_map = {
|
|
Policy.car_state_only: [SpeedLimitSource.car],
|
|
Policy.map_data_only: [SpeedLimitSource.map],
|
|
Policy.car_state_priority: [SpeedLimitSource.car, SpeedLimitSource.map],
|
|
Policy.map_data_priority: [SpeedLimitSource.map, SpeedLimitSource.car],
|
|
Policy.combined: [SpeedLimitSource.car, SpeedLimitSource.map],
|
|
}
|
|
self.source = SpeedLimitSource.none
|
|
for source in ALL_SOURCES:
|
|
self._reset_limit_sources(source)
|
|
|
|
self.is_metric = self.params.get_bool("IsMetric")
|
|
self.offset_type = get_sanitize_int_param(
|
|
"SpeedLimitOffsetType",
|
|
OffsetType.min().value,
|
|
OffsetType.max().value,
|
|
self.params
|
|
)
|
|
self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True)
|
|
|
|
self.speed_limit = 0.
|
|
self.speed_limit_last = 0.
|
|
self.speed_limit_final = 0.
|
|
self.speed_limit_final_last = 0.
|
|
self.speed_limit_offset = 0.
|
|
|
|
def update_speed_limit_states(self) -> None:
|
|
self.speed_limit_final = self.speed_limit + self.speed_limit_offset
|
|
|
|
if self.speed_limit > 0.:
|
|
self.speed_limit_last = self.speed_limit
|
|
self.speed_limit_final_last = self.speed_limit_final
|
|
|
|
@property
|
|
def speed_limit_valid(self) -> bool:
|
|
return self.speed_limit > 0.
|
|
|
|
@property
|
|
def speed_limit_last_valid(self) -> bool:
|
|
return self.speed_limit_last > 0.
|
|
|
|
def update_params(self):
|
|
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
|
self.policy = self.params.get("SpeedLimitPolicy", return_default=True)
|
|
self.is_metric = self.params.get_bool("IsMetric")
|
|
self.offset_type = self.params.get("SpeedLimitOffsetType", return_default=True)
|
|
self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True)
|
|
|
|
def _get_speed_limit_offset(self) -> float:
|
|
if self.offset_type == OffsetType.off:
|
|
return 0
|
|
elif self.offset_type == OffsetType.fixed:
|
|
return float(self.offset_value * (CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS))
|
|
elif self.offset_type == OffsetType.percentage:
|
|
return float(self.offset_value * 0.01 * self.speed_limit)
|
|
else:
|
|
raise NotImplementedError("Offset not supported")
|
|
|
|
def _reset_limit_sources(self, source: custom.LongitudinalPlanSP.SpeedLimit.Source) -> None:
|
|
self.limit_solutions[source] = 0.
|
|
self.distance_solutions[source] = 0.
|
|
|
|
def _get_from_car_state(self, sm: messaging.SubMaster) -> None:
|
|
self._reset_limit_sources(SpeedLimitSource.car)
|
|
self.limit_solutions[SpeedLimitSource.car] = sm['carStateSP'].speedLimit
|
|
self.distance_solutions[SpeedLimitSource.car] = 0.
|
|
|
|
def _get_from_map_data(self, sm: messaging.SubMaster) -> None:
|
|
self._reset_limit_sources(SpeedLimitSource.map)
|
|
self._process_map_data(sm)
|
|
|
|
def _process_map_data(self, sm: messaging.SubMaster) -> None:
|
|
gps_data = sm[self._gps_location_service]
|
|
map_data = sm['liveMapDataSP']
|
|
|
|
gps_fix_age = time.monotonic() - gps_data.unixTimestampMillis * 1e-3
|
|
if gps_fix_age > LIMIT_MAX_MAP_DATA_AGE:
|
|
return
|
|
|
|
speed_limit = map_data.speedLimit if map_data.speedLimitValid else 0.
|
|
next_speed_limit = map_data.speedLimitAhead if map_data.speedLimitAheadValid else 0.
|
|
|
|
self._calculate_map_data_limits(sm, speed_limit, next_speed_limit)
|
|
|
|
def _calculate_map_data_limits(self, sm: messaging.SubMaster, speed_limit: float, next_speed_limit: float) -> None:
|
|
gps_data = sm[self._gps_location_service]
|
|
map_data = sm['liveMapDataSP']
|
|
|
|
distance_since_fix = self.v_ego * (time.monotonic() - gps_data.unixTimestampMillis * 1e-3)
|
|
distance_to_speed_limit_ahead = max(0., map_data.speedLimitAheadDistance - distance_since_fix)
|
|
|
|
self.limit_solutions[SpeedLimitSource.map] = speed_limit
|
|
self.distance_solutions[SpeedLimitSource.map] = 0.
|
|
|
|
# FIXME-SP: this is not working as expected
|
|
if 0. < next_speed_limit < self.v_ego:
|
|
adapt_time = (next_speed_limit - self.v_ego) / LIMIT_ADAPT_ACC
|
|
adapt_distance = self.v_ego * adapt_time + 0.5 * LIMIT_ADAPT_ACC * adapt_time ** 2
|
|
|
|
if distance_to_speed_limit_ahead <= adapt_distance:
|
|
self.limit_solutions[SpeedLimitSource.map] = next_speed_limit
|
|
self.distance_solutions[SpeedLimitSource.map] = distance_to_speed_limit_ahead
|
|
|
|
def _get_source_solution_according_to_policy(self) -> custom.LongitudinalPlanSP.SpeedLimit.Source:
|
|
sources_for_policy = self._policy_to_sources_map[self.policy]
|
|
|
|
if self.policy != Policy.combined:
|
|
# They are ordered in the order of preference, so we pick the first that's non-zero
|
|
for source in sources_for_policy:
|
|
if self.limit_solutions[source] > 0.:
|
|
return source
|
|
return SpeedLimitSource.none
|
|
|
|
sources_with_limits = [(s, limit) for s, limit in [(s, self.limit_solutions[s]) for s in sources_for_policy] if limit > 0.]
|
|
if sources_with_limits:
|
|
return min(sources_with_limits, key=lambda x: x[1])[0]
|
|
|
|
return SpeedLimitSource.none
|
|
|
|
def _resolve_limit_sources(self, sm: messaging.SubMaster) -> tuple[float, float, custom.LongitudinalPlanSP.SpeedLimit.Source]:
|
|
"""Get limit solutions from each data source"""
|
|
self._get_from_car_state(sm)
|
|
self._get_from_map_data(sm)
|
|
|
|
source = self._get_source_solution_according_to_policy()
|
|
speed_limit = self.limit_solutions[source] if source else 0.
|
|
distance = self.distance_solutions[source] if source else 0.
|
|
|
|
return speed_limit, distance, source
|
|
|
|
def update(self, v_ego: float, sm: messaging.SubMaster) -> None:
|
|
self.v_ego = v_ego
|
|
self.update_params()
|
|
|
|
self.speed_limit, self.distance, self.source = self._resolve_limit_sources(sm)
|
|
self.speed_limit_offset = self._get_speed_limit_offset()
|
|
|
|
self.update_speed_limit_states()
|
|
|
|
self.frame += 1
|