Files
2026-04-24 08:30:50 -05:00

486 lines
17 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 time
import numpy as np
from concurrent.futures import ThreadPoolExecutor
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
class SpeedLimitController:
def __init__(self, params):
self.params = params
self.calling_mapbox = False
self.override_slc = False
self.denied_target = 0
self.map_speed_limit = 0
self.mapbox_limit = 0
self.next_speed_limit = 0
self.overridden_speed = 0
self.segment_distance = 0
self.speed_limit_changed_timer = 0
self.target = 0
self.unconfirmed_speed_limit = 0
self.previous_source = "None"
self.source = "None"
self.active_source = "None"
self.active_target = 0.0
self.speed_limit_accepted = False
self.gps_valid = False
self.gps_position = {
"bearing": 0,
"latitude": 0,
"longitude": 0
}
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:
if isinstance(mapbox_requests_raw, bytes):
self.mapbox_requests = json.loads(mapbox_requests_raw.decode('utf-8'))
elif isinstance(mapbox_requests_raw, str):
self.mapbox_requests = json.loads(mapbox_requests_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:
if isinstance(previous_limit, bytes):
self.previous_target = float(previous_limit.decode('utf-8'))
else:
self.previous_target = float(previous_limit)
except (ValueError, AttributeError):
self.previous_target = 0.0
else:
self.previous_target = 0.0
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"})
@staticmethod
def _is_alive(sm, key: str) -> bool:
if hasattr(sm, "alive"):
return bool(sm.alive.get(key, False))
return isinstance(sm, dict) and key in sm
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.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
return 0
def _log_mapbox_diag(self, message: str, force: bool = False) -> None:
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 update_gps(self, sm):
llk_valid = False
if self._is_alive(sm, "liveLocationKalman"):
llk = sm["liveLocationKalman"]
llk_valid = bool(getattr(llk, "gpsOK", 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 llk_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)
}
else:
self.gps_valid = False
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
self.segment_distance = 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(
"SLC Mapbox skipped: "
f"gps_valid={self.gps_valid} token={bool(self.mapbox_token)} steer_angle={round(float(steer_angle), 2)}"
)
self.mapbox_limit = 0
self.segment_distance = 0
return
if v_ego < 1:
self._log_mapbox_diag(f"SLC Mapbox skipped: low_speed v_ego={round(float(v_ego), 2)}")
return
if self.segment_distance > 0:
self._log_mapbox_diag(
"SLC Mapbox deferred: "
f"segment_distance={round(float(self.segment_distance), 2)} v_ego={round(float(v_ego), 2)}"
)
self.segment_distance -= v_ego * DT_MDL
return
if self.calling_mapbox:
self._log_mapbox_diag("SLC Mapbox deferred: request already in flight")
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)
current_bearing = self.gps_position.get("bearing")
current_latitude = self.gps_position.get("latitude")
current_longitude = self.gps_position.get("longitude")
future_latitude, future_longitude = calculate_bearing_offset(current_latitude, current_longitude, current_bearing, v_ego)
self._log_mapbox_diag(
"SLC Mapbox request: "
f"lat={round(float(current_latitude), 6)} lon={round(float(current_longitude), 6)} "
f"bearing={round(float(current_bearing), 2)} v_ego={round(float(v_ego), 2)} "
f"future_lat={round(float(future_latitude), 6)} future_lon={round(float(future_longitude), 6)}",
force=True,
)
url = (
f"{self.mapbox_host}/matching/v5/mapbox/driving/"
f"{current_longitude},{current_latitude};"
f"{future_longitude},{future_latitude}.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
message = f"SLC Mapbox request failed: {exception}"
cloudlog.warning(message)
k3_slc_log(message)
finally:
self.calling_mapbox = False
if not successful:
self.mapbox_limit = 0
self.segment_distance = v_ego
return None
def complete_request(future):
try:
data = future.result()
if data:
matchings = data.get("matchings") or []
if not matchings:
self._log_mapbox_diag("SLC Mapbox callback: no matchings", force=True)
self.mapbox_limit = 0
self.segment_distance = v_ego
return
legs = (matchings[0] or {}).get("legs") or []
if not legs:
self._log_mapbox_diag("SLC Mapbox callback: no legs", force=True)
self.mapbox_limit = 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_segment_speed = speed_data[0]
speed_limit_kph = (first_segment_speed.get("speed") if first_segment_speed.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(
"SLC Mapbox callback: "
f"speed_limit_kph={round(float(speed_limit_kph), 2)} segment_distance={round(float(segment_distance), 2)}",
force=True,
)
return
self._log_mapbox_diag(
"SLC Mapbox callback: "
f"no usable maxspeed speed_data_len={len(speed_data)} segment_distance={round(float(segment_distance), 2)}",
force=True,
)
self.mapbox_limit = 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
message = f"SLC Mapbox callback failed: {exception}"
cloudlog.warning(message)
k3_slc_log(message)
self.mapbox_limit = 0
self.segment_distance = v_ego
future = self.executor.submit(make_request)
future.add_done_callback(complete_request)
def handle_limit_change(self, desired_source, desired_target, sm, slc_params):
self.speed_limit_changed_timer += DT_MDL
car_state_iq = sm["iqCarState"]
accel_pressed = getattr(car_state_iq, "accelPressed", False)
speed_limit_accepted = (accel_pressed and sm["carControl"].longActive) or self.speed_limit_accepted
decel_pressed = getattr(car_state_iq, "decelPressed", False)
speed_limit_denied = decel_pressed or (self.speed_limit_changed_timer >= 30)
if speed_limit_accepted:
self.overridden_speed = 0
self.source = desired_source
self.target = desired_target
self.speed_limit_accepted = False
elif speed_limit_denied:
self.denied_target = desired_target
self.previous_source = desired_source
self.previous_target = desired_target
elif desired_target < self.target and not slc_params.get("speed_limit_confirmation_lower", False):
self.source = desired_source
self.target = desired_target
elif desired_target > self.target and not slc_params.get("speed_limit_confirmation_higher", False):
self.source = desired_source
self.target = desired_target
else:
self.source = "None"
self.unconfirmed_speed_limit = desired_target
if self.target != self.previous_target and self.target > 0 and not speed_limit_denied:
self.denied_target = 0
self.previous_source = self.source
self.previous_target = self.target
self.params.put_nonblocking("PreviousSpeedLimit", float(self.target))
def update_limits(self, dashboard_speed_limit, now, time_validated, v_cruise, v_ego, sm, slc_params):
self.update_gps(sm)
self.update_map_speed_limit(v_ego, sm, slc_params)
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
self.segment_distance = 0
limits = {
"Dashboard": dashboard_speed_limit,
"Mapbox": self.mapbox_limit,
"Map Data": self.map_speed_limit
}
filtered_limits = {source: limit for source, limit in limits.items() if limit >= 1}
priority_highest = slc_params.get("speed_limit_priority_highest", False)
priority_lowest = slc_params.get("speed_limit_priority_lowest", False)
priority1 = slc_params.get("speed_limit_priority1", "Dashboard")
priority2 = slc_params.get("speed_limit_priority2", "Mapbox")
priority3 = slc_params.get("speed_limit_priority3", "Map Data")
if priority_highest:
desired_source = max(filtered_limits, key=filtered_limits.get, default="None")
desired_target = filtered_limits.get(desired_source, 0)
elif priority_lowest:
desired_source = min(filtered_limits, key=filtered_limits.get, default="None")
desired_target = filtered_limits.get(desired_source, 0)
elif filtered_limits:
for priority in [priority1, priority2, priority3]:
if priority in filtered_limits:
desired_source = priority
desired_target = filtered_limits[desired_source]
break
else:
desired_source = "None"
desired_target = 0
else:
desired_source = "None"
desired_target = 0
if desired_target == 0 or self.target == 0:
if self.denied_target != self.previous_target > 0 and slc_params.get("slc_fallback_previous_speed_limit", False):
desired_source = self.previous_source
desired_target = self.previous_target
self.target = desired_target
elif sm["selfdriveState"].enabled and slc_params.get("slc_fallback_set_speed", False):
desired_source = "None"
desired_target = v_cruise
self.active_source = desired_source
self.active_target = float(desired_target)
if abs(desired_target - self.previous_target) >= 1:
self.handle_limit_change(desired_source, desired_target, sm, slc_params)
elif desired_source != self.source and abs(desired_target - self.target) < 1:
self.source = desired_source
else:
self.speed_limit_changed_timer = 0
self.unconfirmed_speed_limit = 0
def update_map_speed_limit(self, v_ego, sm, slc_params):
if self._is_alive(sm, "iqLiveData"):
map_data = sm["iqLiveData"]
self.map_speed_limit = getattr(map_data, "speedLimit", 0) if getattr(map_data, "speedLimitValid", False) else 0
self.next_speed_limit = getattr(map_data, "speedLimitAhead", 0) if getattr(map_data, "speedLimitAheadValid", False) else 0
if self.next_speed_limit > 0:
if self.map_speed_limit < self.next_speed_limit:
lookahead_higher = slc_params.get("map_speed_lookahead_higher", 5)
max_lookahead = lookahead_higher * v_ego
elif self.map_speed_limit > self.next_speed_limit:
lookahead_lower = slc_params.get("map_speed_lookahead_lower", 5)
max_lookahead = lookahead_lower * v_ego
else:
max_lookahead = 0
next_distance = getattr(map_data, "speedLimitAheadDistance", 0)
if next_distance < max_lookahead:
self.map_speed_limit = self.next_speed_limit
else:
self.map_speed_limit = 0
self.next_speed_limit = 0
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)
self.override_slc = self.overridden_speed > self.target + offset > 0
self.override_slc |= sm["carState"].gasPressed and v_ego > self.target + offset > 0
self.override_slc &= sm["selfdriveState"].enabled
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, self.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
self.source = "None"
else:
self.overridden_speed = 0