mirror of
https://gitlvb.teallvbs.xyz/IQ.Lvbs/IQ.Pilot.git
synced 2026-07-24 21:12:05 +08:00
808 lines
29 KiB
Python
808 lines
29 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
|
"""
|
|
|
|
import calendar
|
|
import json
|
|
import math
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
import numpy as np
|
|
|
|
from cereal import car, custom
|
|
from openpilot.common.constants import CV
|
|
from openpilot.common.realtime import DT_MDL
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.iqpilot.common.k3_slc_log import k3_slc_log
|
|
from openpilot.iqpilot.common.slc_utilities import calculate_bearing_offset, is_url_pingable
|
|
from openpilot.iqpilot.common.slc_variables import FREE_MAPBOX_REQUESTS, OFFSET_MAP_IMPERIAL, OFFSET_MAP_METRIC
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
requests = None
|
|
|
|
ButtonType = car.CarState.ButtonEvent.Type
|
|
SpeedLimitAssistState = custom.IQPlan.SpeedLimit.AssistState
|
|
EventNameIQ = custom.IQOnroadEvent.EventName
|
|
|
|
LIMIT_MIN_ACC = -1.5
|
|
LIMIT_MAX_ACC = 1.0
|
|
LIMIT_MIN_SPEED = 8.33
|
|
LIMIT_SPEED_OFFSET_TH = -1.0
|
|
LIMIT_ADAPT_ACC = -1.0
|
|
CONTROL_HORIZON = 10.0
|
|
|
|
AUTO_CONFIRM_PERIOD = 5.0
|
|
AUTO_DENY_PERIOD = 30.0
|
|
|
|
POLICY_MAP_DATA_ONLY = 0
|
|
POLICY_MAP_DATA_PRIORITY = 1
|
|
POLICY_COMBINED = 2
|
|
|
|
CONFIRM_LOWER_BUTTONS = frozenset({ButtonType.decelCruise, ButtonType.setCruise})
|
|
CONFIRM_HIGHER_BUTTONS = frozenset({ButtonType.accelCruise, ButtonType.resumeCruise})
|
|
|
|
|
|
class IQSpeedLimitResolver:
|
|
def __init__(self):
|
|
self.map_speed_limit = 0.0
|
|
self.next_speed_limit = 0.0
|
|
self.next_speed_distance = 0.0
|
|
|
|
@staticmethod
|
|
def _is_alive(sm, key):
|
|
if hasattr(sm, "alive"):
|
|
return bool(sm.alive.get(key, False))
|
|
return False
|
|
|
|
def update_map_data(self, v_ego, sm, lookahead_lower, lookahead_higher):
|
|
if not self._is_alive(sm, "iqLiveData"):
|
|
self.map_speed_limit = 0.0
|
|
self.next_speed_limit = 0.0
|
|
self.next_speed_distance = 0.0
|
|
return
|
|
|
|
map_data = sm["iqLiveData"]
|
|
current_limit = float(getattr(map_data, "speedLimit", 0)) if getattr(map_data, "speedLimitValid", False) else 0.0
|
|
ahead_limit = float(getattr(map_data, "speedLimitAhead", 0)) if getattr(map_data, "speedLimitAheadValid", False) else 0.0
|
|
ahead_distance = float(getattr(map_data, "speedLimitAheadDistance", 0))
|
|
|
|
self.next_speed_limit = ahead_limit
|
|
self.next_speed_distance = ahead_distance
|
|
|
|
if ahead_limit > 0 and ahead_distance > 0:
|
|
if ahead_limit < v_ego:
|
|
adapt_time = (ahead_limit - v_ego) / LIMIT_ADAPT_ACC # positive (LIMIT_ADAPT_ACC negative)
|
|
adapt_distance = v_ego * adapt_time + 0.5 * LIMIT_ADAPT_ACC * adapt_time**2
|
|
comfort_distance = lookahead_lower * v_ego
|
|
if ahead_distance <= max(adapt_distance, comfort_distance):
|
|
self.map_speed_limit = ahead_limit
|
|
return
|
|
elif ahead_limit > current_limit:
|
|
if ahead_distance <= lookahead_higher * v_ego:
|
|
self.map_speed_limit = ahead_limit
|
|
return
|
|
|
|
self.map_speed_limit = current_limit
|
|
|
|
def resolve(self, dashboard_limit, mapbox_limit, slc_params):
|
|
policy = slc_params.get("slc_policy", POLICY_MAP_DATA_PRIORITY)
|
|
|
|
sources = {}
|
|
if dashboard_limit >= LIMIT_MIN_SPEED:
|
|
sources["Dashboard"] = dashboard_limit
|
|
if mapbox_limit >= LIMIT_MIN_SPEED:
|
|
sources["Mapbox"] = mapbox_limit
|
|
if self.map_speed_limit >= LIMIT_MIN_SPEED:
|
|
sources["Map Data"] = self.map_speed_limit
|
|
|
|
if policy == POLICY_MAP_DATA_ONLY:
|
|
if "Map Data" in sources:
|
|
return sources["Map Data"], "Map Data"
|
|
return 0.0, "None"
|
|
|
|
if policy == POLICY_MAP_DATA_PRIORITY:
|
|
for src in ("Map Data", "Dashboard", "Mapbox"):
|
|
if src in sources:
|
|
return sources[src], src
|
|
return 0.0, "None"
|
|
|
|
if policy == POLICY_COMBINED:
|
|
if sources:
|
|
src = min(sources, key=sources.get)
|
|
return sources[src], src
|
|
return 0.0, "None"
|
|
|
|
return 0.0, "None"
|
|
|
|
|
|
class IQSpeedLimitAssist:
|
|
def __init__(self, params):
|
|
self._params = params
|
|
self._state = SpeedLimitAssistState.inactive
|
|
self._prev_state = SpeedLimitAssistState.inactive
|
|
|
|
self.target = 0.0
|
|
self.source = "None"
|
|
|
|
self.unconfirmed_limit = 0.0
|
|
self.unconfirmed_source = "None"
|
|
|
|
self.previous_target = 0.0
|
|
self.previous_source = "None"
|
|
self.denied_target = 0.0
|
|
|
|
self._pre_active_timer = 0.0
|
|
|
|
self.pending_events = []
|
|
|
|
self.output_a_target = 0.0
|
|
|
|
self.just_confirmed = False
|
|
|
|
@property
|
|
def state(self):
|
|
return self._state
|
|
|
|
def update(self, enabled, v_ego, resolved_limit, resolved_source, slc_params, sm):
|
|
self.pending_events = []
|
|
self.just_confirmed = False
|
|
self._prev_state = self._state
|
|
|
|
if not enabled:
|
|
if self._state != SpeedLimitAssistState.disabled:
|
|
self._state = SpeedLimitAssistState.disabled
|
|
self._reset_confirmed()
|
|
self._reset_unconfirmed()
|
|
self.output_a_target = 0.0
|
|
self._fire_transition_events()
|
|
return
|
|
|
|
if self._state == SpeedLimitAssistState.disabled:
|
|
self._state = SpeedLimitAssistState.inactive
|
|
|
|
has_limit = resolved_limit >= LIMIT_MIN_SPEED
|
|
v_offset = self.target - v_ego if self.target > 0 else 0.0
|
|
|
|
if self._state == SpeedLimitAssistState.inactive:
|
|
if has_limit:
|
|
if self._needs_confirmation(resolved_limit, slc_params):
|
|
self._enter_pre_active(resolved_limit, resolved_source)
|
|
else:
|
|
self._apply_limit(resolved_limit, resolved_source, v_ego, fire_changed_event=True)
|
|
|
|
elif self._state == SpeedLimitAssistState.preActive:
|
|
self._pre_active_timer += DT_MDL
|
|
confirmed, denied = self._check_confirmation(sm, slc_params)
|
|
|
|
if denied:
|
|
self.denied_target = self.unconfirmed_limit
|
|
self.previous_source = self.unconfirmed_source
|
|
self.previous_target = self.unconfirmed_limit
|
|
self._reset_unconfirmed()
|
|
self._state = SpeedLimitAssistState.inactive
|
|
elif confirmed:
|
|
self._confirm(v_ego)
|
|
elif not has_limit:
|
|
self._reset_unconfirmed()
|
|
self._state = SpeedLimitAssistState.inactive
|
|
|
|
elif self._state in (SpeedLimitAssistState.active, SpeedLimitAssistState.adapting):
|
|
if not has_limit:
|
|
if self.target > 0:
|
|
self.previous_target = self.target
|
|
self.previous_source = self.source
|
|
self._reset_confirmed()
|
|
self._state = SpeedLimitAssistState.inactive
|
|
elif abs(resolved_limit - self.target) >= 1.0:
|
|
if self._needs_confirmation(resolved_limit, slc_params):
|
|
self._enter_pre_active(resolved_limit, resolved_source)
|
|
else:
|
|
self._apply_limit(resolved_limit, resolved_source, v_ego, fire_changed_event=True)
|
|
elif self._state == SpeedLimitAssistState.adapting:
|
|
if v_offset >= LIMIT_SPEED_OFFSET_TH:
|
|
self._state = SpeedLimitAssistState.active
|
|
elif self._state == SpeedLimitAssistState.active:
|
|
if v_offset < LIMIT_SPEED_OFFSET_TH:
|
|
self._state = SpeedLimitAssistState.adapting
|
|
|
|
self._update_a_target(v_ego)
|
|
self._fire_transition_events()
|
|
|
|
def _enter_pre_active(self, limit, source):
|
|
self.unconfirmed_limit = limit
|
|
self.unconfirmed_source = source
|
|
self._state = SpeedLimitAssistState.preActive
|
|
self._pre_active_timer = 0.0
|
|
|
|
def _confirm(self, v_ego):
|
|
self.target = self.unconfirmed_limit
|
|
self.source = self.unconfirmed_source
|
|
self.previous_target = self.target
|
|
self.previous_source = self.source
|
|
self.denied_target = 0.0
|
|
self._reset_unconfirmed()
|
|
self._params.put_nonblocking("PreviousSpeedLimit", float(self.target))
|
|
self.just_confirmed = True
|
|
v_offset = self.target - v_ego
|
|
self._state = SpeedLimitAssistState.adapting if v_offset < LIMIT_SPEED_OFFSET_TH else SpeedLimitAssistState.active
|
|
|
|
def _apply_limit(self, limit, source, v_ego, fire_changed_event=False):
|
|
self.target = limit
|
|
self.source = source
|
|
self.previous_target = self.target
|
|
self.previous_source = self.source
|
|
self._params.put_nonblocking("PreviousSpeedLimit", float(self.target))
|
|
if fire_changed_event:
|
|
self.pending_events.append(EventNameIQ.speedLimitChanged)
|
|
v_offset = self.target - v_ego
|
|
self._state = SpeedLimitAssistState.adapting if v_offset < LIMIT_SPEED_OFFSET_TH else SpeedLimitAssistState.active
|
|
|
|
def _needs_confirmation(self, new_limit, slc_params):
|
|
if new_limit < self.target:
|
|
return slc_params.get("speed_limit_confirmation_lower", False)
|
|
return slc_params.get("speed_limit_confirmation_higher", False)
|
|
|
|
def _check_confirmation(self, sm, slc_params):
|
|
confirmed = False
|
|
denied = False
|
|
|
|
if slc_params.get("slc_auto_confirm", False) and self._pre_active_timer >= AUTO_CONFIRM_PERIOD:
|
|
return True, False
|
|
|
|
if self._pre_active_timer >= AUTO_DENY_PERIOD:
|
|
return False, True
|
|
|
|
is_lower = (self.target <= 0) or (self.unconfirmed_limit <= self.target)
|
|
try:
|
|
for btn in sm["carState"].buttonEvents:
|
|
if btn.pressed:
|
|
continue
|
|
if is_lower and btn.type in CONFIRM_LOWER_BUTTONS:
|
|
confirmed = True
|
|
break
|
|
elif not is_lower and btn.type in CONFIRM_HIGHER_BUTTONS:
|
|
confirmed = True
|
|
break
|
|
except (AttributeError, TypeError):
|
|
pass
|
|
|
|
return confirmed, denied
|
|
|
|
def _update_a_target(self, v_ego):
|
|
if self._state in (SpeedLimitAssistState.adapting, SpeedLimitAssistState.active) and self.target > 0:
|
|
v_offset = self.target - v_ego
|
|
self.output_a_target = float(np.clip(v_offset / CONTROL_HORIZON, LIMIT_MIN_ACC, LIMIT_MAX_ACC))
|
|
else:
|
|
self.output_a_target = 0.0
|
|
|
|
def _fire_transition_events(self):
|
|
prev = self._prev_state
|
|
curr = self._state
|
|
if prev == curr:
|
|
return
|
|
if curr == SpeedLimitAssistState.preActive:
|
|
self.pending_events.append(EventNameIQ.speedLimitPreActive)
|
|
elif curr in (SpeedLimitAssistState.adapting, SpeedLimitAssistState.active):
|
|
if prev not in (SpeedLimitAssistState.adapting, SpeedLimitAssistState.active):
|
|
self.pending_events.append(EventNameIQ.speedLimitActive)
|
|
|
|
def _reset_confirmed(self):
|
|
self.target = 0.0
|
|
self.source = "None"
|
|
|
|
def _reset_unconfirmed(self):
|
|
self.unconfirmed_limit = 0.0
|
|
self.unconfirmed_source = "None"
|
|
|
|
|
|
class SpeedLimitController:
|
|
def __init__(self, params):
|
|
self.params = params
|
|
self._resolver = IQSpeedLimitResolver()
|
|
self._assist = IQSpeedLimitAssist(params)
|
|
|
|
self.calling_mapbox = False
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = 0.0
|
|
|
|
self.gps_valid = False
|
|
self.gps_position = {"bearing": 0, "latitude": 0, "longitude": 0}
|
|
|
|
self.override_slc = False
|
|
self.overridden_speed = 0.0
|
|
|
|
self._resolved_limit = 0.0
|
|
self._resolved_source = "None"
|
|
self._czone_was_limiting = False
|
|
|
|
self.pending_events = []
|
|
|
|
mapbox_requests_raw = self.params.get("MapBoxRequests")
|
|
if isinstance(mapbox_requests_raw, dict):
|
|
self.mapbox_requests = mapbox_requests_raw
|
|
elif mapbox_requests_raw is not None:
|
|
try:
|
|
raw = mapbox_requests_raw
|
|
if isinstance(raw, bytes):
|
|
self.mapbox_requests = json.loads(raw.decode("utf-8"))
|
|
elif isinstance(raw, str):
|
|
self.mapbox_requests = json.loads(raw)
|
|
else:
|
|
self.mapbox_requests = {}
|
|
except (json.JSONDecodeError, AttributeError, TypeError):
|
|
self.mapbox_requests = {}
|
|
else:
|
|
self.mapbox_requests = {}
|
|
self.mapbox_requests.setdefault("total_requests", 0)
|
|
self.mapbox_requests.setdefault("max_requests", FREE_MAPBOX_REQUESTS - (28 * 100))
|
|
|
|
self.mapbox_host = "https://api.mapbox.com"
|
|
self.mapbox_token = self.params.get("MapboxToken")
|
|
if self.mapbox_token is not None and isinstance(self.mapbox_token, bytes):
|
|
self.mapbox_token = self.mapbox_token.decode("utf-8")
|
|
|
|
previous_limit = self.params.get("PreviousSpeedLimit")
|
|
if previous_limit is not None:
|
|
try:
|
|
val = previous_limit
|
|
self._assist.previous_target = float(val.decode("utf-8") if isinstance(val, bytes) else val)
|
|
except (ValueError, AttributeError):
|
|
pass
|
|
|
|
self.executor = ThreadPoolExecutor(max_workers=1)
|
|
self._last_mapbox_log_t = 0.0
|
|
self._last_mapbox_diag_t = 0.0
|
|
self._last_mapbox_diag_message = None
|
|
|
|
self.session = requests.Session() if requests is not None else None
|
|
if self.session is not None:
|
|
self.session.headers.update({"Accept-Language": "en"})
|
|
self.session.headers.update({"User-Agent": "iqpilot-mapbox-speed-limit-retriever/1.0"})
|
|
|
|
self.tomtom_host = "https://api.tomtom.com"
|
|
self.tomtom_token = self._resolve_tomtom_token()
|
|
self.tomtom_limit = 0.0
|
|
self.tomtom_segment_distance = 0.0
|
|
self.calling_tomtom = False
|
|
self.tomtom_consecutive_failures = 0
|
|
self.tomtom_backoff_until = 0.0
|
|
|
|
def _resolve_tomtom_token(self) -> str:
|
|
try:
|
|
from openpilot.iqpilot.navd.runtime_common import resolve_tomtom_token
|
|
return resolve_tomtom_token(self.params) or ""
|
|
except Exception:
|
|
tok = self.params.get("TomTomToken")
|
|
return (tok.decode("utf-8") if isinstance(tok, bytes) else (tok or "")).strip()
|
|
|
|
@property
|
|
def target(self):
|
|
return self._assist.target
|
|
|
|
@property
|
|
def source(self):
|
|
return self._assist.source
|
|
|
|
@property
|
|
def active_target(self):
|
|
return self._resolved_limit
|
|
|
|
@property
|
|
def active_source(self):
|
|
return self._resolved_source
|
|
|
|
@property
|
|
def unconfirmed_speed_limit(self):
|
|
return self._assist.unconfirmed_limit
|
|
|
|
@property
|
|
def map_speed_limit(self):
|
|
return self._resolver.map_speed_limit
|
|
|
|
@property
|
|
def next_speed_limit(self):
|
|
return self._resolver.next_speed_limit
|
|
|
|
@property
|
|
def assist_state(self):
|
|
return self._assist.state
|
|
|
|
@property
|
|
def output_a_target(self):
|
|
return self._assist.output_a_target
|
|
|
|
def get_offset(self, is_metric):
|
|
offset_map = OFFSET_MAP_METRIC if is_metric else OFFSET_MAP_IMPERIAL
|
|
for low, high, offset_param in offset_map:
|
|
if low < self._assist.target < high:
|
|
offset_value = self.params.get(offset_param)
|
|
if offset_value is not None:
|
|
if isinstance(offset_value, bytes):
|
|
return float(offset_value.decode("utf-8"))
|
|
return float(offset_value)
|
|
return 0.0
|
|
return 0.0
|
|
|
|
@staticmethod
|
|
def _is_alive(sm, key):
|
|
if hasattr(sm, "alive"):
|
|
return bool(sm.alive.get(key, False))
|
|
return False
|
|
|
|
def update_gps(self, sm):
|
|
iq_loc_valid = False
|
|
iq_loc = None
|
|
if self._is_alive(sm, "iqLiveLocation"):
|
|
iq_loc = sm["iqLiveLocation"]
|
|
iq_loc_valid = bool(getattr(iq_loc, "gpsHealthy", False))
|
|
|
|
if self._is_alive(sm, "gpsLocationExternal"):
|
|
gps_location = sm["gpsLocationExternal"]
|
|
elif self._is_alive(sm, "gpsLocation"):
|
|
gps_location = sm["gpsLocation"]
|
|
else:
|
|
gps_location = None
|
|
|
|
gps_has_fix = False
|
|
if gps_location is not None:
|
|
gps_has_fix = bool(getattr(gps_location, "hasFix", False))
|
|
gps_has_fix |= bool(getattr(gps_location, "flags", 0) > 0)
|
|
|
|
if gps_location and (gps_has_fix or iq_loc_valid):
|
|
self.gps_valid = True
|
|
self.gps_position = {
|
|
"bearing": getattr(gps_location, "bearingDeg", 0),
|
|
"latitude": getattr(gps_location, "latitude", 0),
|
|
"longitude": getattr(gps_location, "longitude", 0),
|
|
}
|
|
elif iq_loc_valid and iq_loc is not None and getattr(iq_loc, "geodeticPosition", None) and iq_loc.geodeticPosition.isValid:
|
|
self.gps_valid = True
|
|
self.gps_position = {
|
|
"bearing": math.degrees(iq_loc.alignedOrientationNed.values[2]) if getattr(iq_loc, "alignedOrientationNed", None) else 0,
|
|
"latitude": iq_loc.geodeticPosition.values[0],
|
|
"longitude": iq_loc.geodeticPosition.values[1],
|
|
}
|
|
else:
|
|
self.gps_valid = False
|
|
|
|
def _log_mapbox_diag(self, message, force=False):
|
|
now_mono = time.monotonic()
|
|
if not force and message == self._last_mapbox_diag_message and now_mono - self._last_mapbox_diag_t < 5.0:
|
|
return
|
|
if not force and now_mono - self._last_mapbox_diag_t < 2.0:
|
|
return
|
|
self._last_mapbox_diag_t = now_mono
|
|
self._last_mapbox_diag_message = message
|
|
cloudlog.info(message)
|
|
k3_slc_log(message)
|
|
|
|
def get_mapbox_speed_limit(self, now, time_validated, v_ego, sm):
|
|
if requests is None or self.session is None:
|
|
self._log_mapbox_diag("SLC Mapbox skipped: requests session unavailable")
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = 0.0
|
|
return
|
|
|
|
steer_angle = sm["carState"].steeringAngleDeg - sm["liveParameters"].angleOffsetDeg
|
|
if not self.gps_valid or not self.mapbox_token or steer_angle >= 45:
|
|
self._log_mapbox_diag(f"SLC Mapbox skipped: gps_valid={self.gps_valid} token={bool(self.mapbox_token)} steer_angle={round(float(steer_angle), 2)}")
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = 0.0
|
|
return
|
|
|
|
if v_ego < 1:
|
|
return
|
|
|
|
if self.segment_distance > 0:
|
|
self.segment_distance -= v_ego * DT_MDL
|
|
return
|
|
|
|
if self.calling_mapbox:
|
|
self.segment_distance = v_ego
|
|
return
|
|
|
|
def make_request():
|
|
try:
|
|
self.calling_mapbox = True
|
|
successful = False
|
|
|
|
if not is_url_pingable(self.mapbox_host):
|
|
self._log_mapbox_diag("SLC Mapbox skipped: host not pingable", force=True)
|
|
self.segment_distance = 1000
|
|
return None
|
|
|
|
if time_validated:
|
|
current_month = now.month
|
|
if current_month != self.mapbox_requests.get("month"):
|
|
self.mapbox_requests.update(
|
|
{
|
|
"month": current_month,
|
|
"total_requests": 0,
|
|
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(now.year, current_month)[1] * 100,
|
|
}
|
|
)
|
|
|
|
self.mapbox_requests["total_requests"] += 1
|
|
self.params.put_nonblocking("MapBoxRequests", self.mapbox_requests)
|
|
|
|
lat = self.gps_position.get("latitude")
|
|
lon = self.gps_position.get("longitude")
|
|
bearing = self.gps_position.get("bearing")
|
|
future_lat, future_lon = calculate_bearing_offset(lat, lon, bearing, v_ego)
|
|
|
|
self._log_mapbox_diag(
|
|
f"SLC Mapbox request: lat={round(float(lat), 6)} lon={round(float(lon), 6)} bearing={round(float(bearing), 2)} v_ego={round(float(v_ego), 2)}",
|
|
force=True,
|
|
)
|
|
|
|
url = f"{self.mapbox_host}/matching/v5/mapbox/driving/{lon},{lat};{future_lon},{future_lat}.json"
|
|
mapbox_params = {
|
|
"access_token": self.mapbox_token,
|
|
"annotations": "maxspeed,distance",
|
|
"geometries": "polyline6",
|
|
"overview": "full",
|
|
"steps": "false",
|
|
"radiuses": "10;10",
|
|
"tidy": "true",
|
|
}
|
|
|
|
response = self.session.get(url, params=mapbox_params, timeout=10)
|
|
response.raise_for_status()
|
|
successful = True
|
|
return response.json()
|
|
except Exception as exception:
|
|
now_mono = time.monotonic()
|
|
if now_mono - self._last_mapbox_log_t >= 5.0:
|
|
self._last_mapbox_log_t = now_mono
|
|
msg = f"SLC Mapbox request failed: {exception}"
|
|
cloudlog.warning(msg)
|
|
k3_slc_log(msg)
|
|
finally:
|
|
self.calling_mapbox = False
|
|
if not successful:
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = v_ego
|
|
|
|
def complete_request(future):
|
|
try:
|
|
data = future.result()
|
|
if data:
|
|
matchings = data.get("matchings") or []
|
|
if not matchings:
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = v_ego
|
|
return
|
|
legs = (matchings[0] or {}).get("legs") or []
|
|
if not legs:
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = v_ego
|
|
return
|
|
annotation = legs[0].get("annotation") or {}
|
|
distances = annotation.get("distance") or [v_ego]
|
|
segment_distance = distances[0]
|
|
speed_data = annotation.get("maxspeed", [])
|
|
speed_limit_kph = 0
|
|
if speed_data:
|
|
first = speed_data[0]
|
|
speed_limit_kph = (first.get("speed") if first.get("speed") != "none" else 0) or 0
|
|
if speed_limit_kph > 0:
|
|
self.mapbox_limit = speed_limit_kph * CV.KPH_TO_MS
|
|
self.segment_distance = segment_distance
|
|
self._log_mapbox_diag(
|
|
f"SLC Mapbox callback: speed_limit_kph={round(float(speed_limit_kph), 2)} segment_distance={round(float(segment_distance), 2)}",
|
|
force=True,
|
|
)
|
|
return
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = v_ego
|
|
except Exception as exception:
|
|
now_mono = time.monotonic()
|
|
if now_mono - self._last_mapbox_log_t >= 5.0:
|
|
self._last_mapbox_log_t = now_mono
|
|
msg = f"SLC Mapbox callback failed: {exception}"
|
|
cloudlog.warning(msg)
|
|
k3_slc_log(msg)
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = v_ego
|
|
|
|
future = self.executor.submit(make_request)
|
|
future.add_done_callback(complete_request)
|
|
|
|
def get_tomtom_speed_limit(self, now, time_validated, v_ego, sm):
|
|
if requests is None or self.session is None or not self.tomtom_token:
|
|
self.tomtom_limit = 0.0
|
|
self.tomtom_segment_distance = 0.0
|
|
return
|
|
|
|
# backoff: an exhausted-quota key (HTTP 403 InsufficientFunds) otherwise gets
|
|
# hammered every 250 m for the rest of the drive
|
|
if time.monotonic() < self.tomtom_backoff_until:
|
|
self.tomtom_limit = 0.0
|
|
return
|
|
|
|
steer_angle = sm["carState"].steeringAngleDeg - sm["liveParameters"].angleOffsetDeg
|
|
if not self.gps_valid or steer_angle >= 45 or v_ego < 1:
|
|
self.tomtom_limit = 0.0
|
|
return
|
|
|
|
# re-query at most once per ~250 m of travel
|
|
if self.tomtom_segment_distance > 0:
|
|
self.tomtom_segment_distance -= v_ego * DT_MDL
|
|
return
|
|
if self.calling_tomtom:
|
|
self.tomtom_segment_distance = v_ego
|
|
return
|
|
|
|
lat = self.gps_position.get("latitude")
|
|
lon = self.gps_position.get("longitude")
|
|
bearing = self.gps_position.get("bearing")
|
|
future_lat, future_lon = calculate_bearing_offset(lat, lon, bearing, max(v_ego, 12.0) * 12.0)
|
|
|
|
def make_request():
|
|
successful = False
|
|
try:
|
|
self.calling_tomtom = True
|
|
url = f"{self.tomtom_host}/routing/1/calculateRoute/{lat},{lon}:{future_lat},{future_lon}/json"
|
|
self._log_mapbox_diag(
|
|
f"SLC TomTom request: lat={round(float(lat), 6)} lon={round(float(lon), 6)} bearing={round(float(bearing), 2)} v_ego={round(float(v_ego), 2)}",
|
|
force=True,
|
|
)
|
|
response = self.session.get(url, params={"key": self.tomtom_token, "sectionType": "speedLimit", "traffic": "false"}, timeout=10)
|
|
response.raise_for_status()
|
|
successful = True
|
|
self.tomtom_consecutive_failures = 0
|
|
return response.json()
|
|
except Exception as exception:
|
|
status = getattr(getattr(exception, "response", None), "status_code", None)
|
|
if status in (401, 403, 429):
|
|
# dead/exhausted key: retry hourly in case credits refill, not every 250 m
|
|
self.tomtom_backoff_until = time.monotonic() + 3600.0
|
|
else:
|
|
self.tomtom_consecutive_failures += 1
|
|
self.tomtom_backoff_until = time.monotonic() + min(600.0, 10.0 * (2 ** min(self.tomtom_consecutive_failures, 6)))
|
|
now_mono = time.monotonic()
|
|
if now_mono - self._last_mapbox_log_t >= 5.0:
|
|
self._last_mapbox_log_t = now_mono
|
|
msg = f"SLC TomTom request failed (backoff {max(0.0, self.tomtom_backoff_until - now_mono):.0f}s): {exception}"
|
|
cloudlog.warning(msg)
|
|
k3_slc_log(msg)
|
|
finally:
|
|
self.calling_tomtom = False
|
|
if not successful:
|
|
self.tomtom_limit = 0.0
|
|
self.tomtom_segment_distance = v_ego
|
|
|
|
def complete_request(future):
|
|
try:
|
|
data = future.result()
|
|
kmh = 0
|
|
if data:
|
|
sections = ((data.get("routes") or [{}])[0]).get("sections") or []
|
|
speed_secs = [s for s in sections if s.get("sectionType") == "SPEED_LIMIT"]
|
|
at_start = next((s for s in speed_secs if s.get("startPointIndex") == 0), None)
|
|
chosen = at_start or (speed_secs[0] if speed_secs else None)
|
|
if chosen:
|
|
kmh = chosen.get("maxSpeedLimitInKmh") or 0
|
|
if kmh and kmh > 0:
|
|
self.tomtom_limit = float(kmh) * CV.KPH_TO_MS
|
|
self._log_mapbox_diag(
|
|
f"SLC TomTom callback: speed_limit_kph={round(float(kmh), 2)}",
|
|
force=True,
|
|
)
|
|
else:
|
|
self.tomtom_limit = 0.0
|
|
except Exception as exception:
|
|
now_mono = time.monotonic()
|
|
if now_mono - self._last_mapbox_log_t >= 5.0:
|
|
self._last_mapbox_log_t = now_mono
|
|
cloudlog.warning(f"SLC TomTom callback failed: {exception}")
|
|
self.tomtom_limit = 0.0
|
|
finally:
|
|
self.tomtom_segment_distance = 250.0
|
|
|
|
future = self.executor.submit(make_request)
|
|
future.add_done_callback(complete_request)
|
|
|
|
def _construction_zone_limit(self, sm, slc_params):
|
|
if not slc_params.get("construction_zone_assist", False):
|
|
return 0.0
|
|
if not self._is_alive(sm, "iqConstructionZone"):
|
|
return 0.0
|
|
if not bool(getattr(sm["iqConstructionZone"], "active", False)):
|
|
return 0.0
|
|
speed = slc_params.get("construction_zone_speed", 60.0)
|
|
unit = CV.KPH_TO_MS if slc_params.get("is_metric", False) else CV.MPH_TO_MS
|
|
return max(float(speed), 0.0) * unit
|
|
|
|
def _maybe_reset_mapbox_quota(self, now, time_validated):
|
|
if time_validated:
|
|
current_month = now.month
|
|
if current_month != self.mapbox_requests.get("month"):
|
|
self.mapbox_requests.update(
|
|
{
|
|
"month": current_month,
|
|
"total_requests": 0,
|
|
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(now.year, current_month)[1] * 100,
|
|
}
|
|
)
|
|
self.params.put_nonblocking("MapBoxRequests", self.mapbox_requests)
|
|
|
|
def update_limits(self, dashboard_speed_limit, now, time_validated, v_cruise, v_ego, sm, slc_params):
|
|
self.update_gps(sm)
|
|
|
|
lookahead_lower = slc_params.get("map_speed_lookahead_lower", 5.0)
|
|
lookahead_higher = slc_params.get("map_speed_lookahead_higher", 5.0)
|
|
self._resolver.update_map_data(v_ego, sm, lookahead_lower, lookahead_higher)
|
|
|
|
use_online = slc_params.get("slc_online_filler", False)
|
|
if use_online:
|
|
self._maybe_reset_mapbox_quota(now, time_validated)
|
|
if self.mapbox_requests["total_requests"] < self.mapbox_requests["max_requests"]:
|
|
self.get_mapbox_speed_limit(now, time_validated, v_ego, sm)
|
|
else:
|
|
self.mapbox_limit = 0.0
|
|
self.segment_distance = 0.0
|
|
self.get_tomtom_speed_limit(now, time_validated, v_ego, sm)
|
|
else:
|
|
self.mapbox_limit = 0.0
|
|
self.tomtom_limit = 0.0
|
|
self.segment_distance = 0.0
|
|
self.tomtom_segment_distance = 0.0
|
|
|
|
online_limit = self.tomtom_limit if self.tomtom_limit > 0 else self.mapbox_limit
|
|
|
|
dashboard_limit = float(dashboard_speed_limit) if dashboard_speed_limit else 0.0
|
|
resolved_limit, resolved_source = self._resolver.resolve(dashboard_limit, online_limit, slc_params)
|
|
|
|
enabled = bool(getattr(sm["selfdriveState"], "enabled", False))
|
|
if resolved_limit <= 0:
|
|
if self._assist.denied_target != self._assist.previous_target > 0 and slc_params.get("slc_fallback_previous_speed_limit", False):
|
|
resolved_limit = self._assist.previous_target
|
|
resolved_source = self._assist.previous_source
|
|
elif enabled and slc_params.get("slc_fallback_set_speed", False):
|
|
resolved_limit = v_cruise
|
|
resolved_source = "None"
|
|
|
|
# work-zone clamp: only ever lowers the resolved limit
|
|
czone_limit = self._construction_zone_limit(sm, slc_params)
|
|
if czone_limit > 0 and (resolved_limit <= 0 or resolved_limit > czone_limit):
|
|
resolved_limit = czone_limit
|
|
resolved_source = "Construction"
|
|
|
|
self._resolved_limit = float(resolved_limit)
|
|
self._resolved_source = resolved_source
|
|
|
|
self._assist.update(enabled, v_ego, resolved_limit, resolved_source, slc_params, sm)
|
|
|
|
if self._assist.just_confirmed:
|
|
self.overridden_speed = 0.0
|
|
|
|
self.pending_events = list(self._assist.pending_events)
|
|
|
|
czone_limiting = resolved_source == "Construction"
|
|
if czone_limiting and not self._czone_was_limiting:
|
|
self.pending_events.append(EventNameIQ.constructionZoneDetected)
|
|
self._czone_was_limiting = czone_limiting
|
|
|
|
def update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm, slc_params, is_metric):
|
|
offset = self.get_offset(is_metric)
|
|
target = self._assist.target
|
|
|
|
self.override_slc = self.overridden_speed > target + offset > 0
|
|
self.override_slc |= sm["carState"].gasPressed and v_ego > target + offset > 0
|
|
self.override_slc &= bool(getattr(sm["selfdriveState"], "enabled", False))
|
|
|
|
if self.override_slc:
|
|
if slc_params.get("speed_limit_controller_override_manual", False):
|
|
if sm["carState"].gasPressed:
|
|
self.overridden_speed = max(v_ego + v_ego_diff, self.overridden_speed)
|
|
self.overridden_speed = float(np.clip(self.overridden_speed, target + offset, v_cruise + v_cruise_diff))
|
|
elif slc_params.get("speed_limit_controller_override_set_speed", False):
|
|
self.overridden_speed = v_cruise + v_cruise_diff
|
|
else:
|
|
self.overridden_speed = 0.0
|