forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Prebuilt Release @ ab07000
This commit is contained in:
281
iqpilot/common/atlas_alerts.py
Normal file
281
iqpilot/common/atlas_alerts.py
Normal file
@@ -0,0 +1,281 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from bisect import insort
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from cereal import car, log
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
AlertStatus = log.SelfdriveState.AlertStatus
|
||||
VisualAlert = car.CarControl.HUDControl.VisualAlert
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
|
||||
def _frames_for(seconds: float) -> int:
|
||||
return int(seconds / DT_CTRL)
|
||||
|
||||
|
||||
class Tier(IntEnum):
|
||||
LOWEST = 0
|
||||
LOWER = 1
|
||||
LOW = 2
|
||||
MID = 3
|
||||
HIGH = 4
|
||||
HIGHEST = 5
|
||||
|
||||
|
||||
class Tags:
|
||||
ENABLE = "enable"
|
||||
PRE_ENABLE = "preEnable"
|
||||
OVERRIDE_LATERAL = "overrideLateral"
|
||||
OVERRIDE_LONGITUDINAL = "overrideLongitudinal"
|
||||
NO_ENTRY = "noEntry"
|
||||
WARNING = "warning"
|
||||
USER_DISABLE = "userDisable"
|
||||
SOFT_DISABLE = "softDisable"
|
||||
IMMEDIATE_DISABLE = "immediateDisable"
|
||||
PERMANENT = "permanent"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AlertCard:
|
||||
alert_text_1: str
|
||||
alert_text_2: str
|
||||
alert_status: log.SelfdriveState.AlertStatus
|
||||
alert_size: log.SelfdriveState.AlertSize
|
||||
priority: Tier
|
||||
visual_alert: car.CarControl.HUDControl.VisualAlert
|
||||
audible_alert: car.CarControl.HUDControl.AudibleAlert
|
||||
duration: int
|
||||
creation_delay: float = 0.0
|
||||
alert_type: str = field(default="", init=False)
|
||||
event_type: str | None = field(default=None, init=False)
|
||||
|
||||
def __init__(self,
|
||||
alert_text_1: str,
|
||||
alert_text_2: str,
|
||||
alert_status: log.SelfdriveState.AlertStatus,
|
||||
alert_size: log.SelfdriveState.AlertSize,
|
||||
priority: Tier,
|
||||
visual_alert: car.CarControl.HUDControl.VisualAlert,
|
||||
audible_alert: car.CarControl.HUDControl.AudibleAlert,
|
||||
duration: float,
|
||||
creation_delay: float = 0.0):
|
||||
self.alert_text_1 = alert_text_1
|
||||
self.alert_text_2 = alert_text_2
|
||||
self.alert_status = alert_status
|
||||
self.alert_size = alert_size
|
||||
self.priority = priority
|
||||
self.visual_alert = visual_alert
|
||||
self.audible_alert = audible_alert
|
||||
self.duration = _frames_for(duration)
|
||||
self.creation_delay = creation_delay
|
||||
self.alert_type = ""
|
||||
self.event_type = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}"
|
||||
|
||||
|
||||
AlertFactory = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], AlertCard]
|
||||
|
||||
|
||||
def car_mode_entry_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> AlertCard:
|
||||
del CS, sm, metric, soft_disable_time, personality
|
||||
headline = "Enable Adaptive Cruise to Engage"
|
||||
if CP.brand == "honda":
|
||||
headline = "Enable Main Switch to Engage"
|
||||
return NoEntryCard(headline)
|
||||
|
||||
|
||||
class EventBook(ABC):
|
||||
def __init__(self):
|
||||
self._live_names: list[int] = []
|
||||
self._latched_names: list[int] = []
|
||||
self.event_counters: dict[int, int] = {}
|
||||
|
||||
@property
|
||||
def events(self) -> list[int]:
|
||||
return self._live_names
|
||||
|
||||
@events.setter
|
||||
def events(self, values: list[int]) -> None:
|
||||
self._live_names = values
|
||||
|
||||
@property
|
||||
def static_events(self) -> list[int]:
|
||||
return self._latched_names
|
||||
|
||||
@static_events.setter
|
||||
def static_events(self, values: list[int]) -> None:
|
||||
self._latched_names = values
|
||||
|
||||
@property
|
||||
def names(self) -> list[int]:
|
||||
return list(self._live_names)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._live_names)
|
||||
|
||||
def add(self, event_name: int, static: bool = False) -> None:
|
||||
if static:
|
||||
insort(self._latched_names, event_name)
|
||||
insort(self._live_names, event_name)
|
||||
|
||||
def clear(self) -> None:
|
||||
refreshed: dict[int, int] = {}
|
||||
for event_name, frames_seen in self.event_counters.items():
|
||||
refreshed[event_name] = frames_seen + 1 if event_name in self._live_names else 0
|
||||
self.event_counters = refreshed
|
||||
self._live_names = list(self._latched_names)
|
||||
|
||||
def contains(self, event_type: str) -> bool:
|
||||
board = self.get_events_mapping()
|
||||
return any(event_type in board.get(event_name, {}) for event_name in self._live_names)
|
||||
|
||||
def has(self, event_name: int) -> bool:
|
||||
return event_name in self._live_names
|
||||
|
||||
def contains_in_list(self, events_list: list[int]) -> bool:
|
||||
return any(event_name in self._live_names for event_name in events_list)
|
||||
|
||||
def remove(self, event_name: int, static: bool = False) -> None:
|
||||
if static and event_name in self._latched_names:
|
||||
self._latched_names.remove(event_name)
|
||||
|
||||
if event_name in self._live_names:
|
||||
self.event_counters[event_name] = self.event_counters.get(event_name, 0) + 1
|
||||
self._live_names.remove(event_name)
|
||||
|
||||
def add_from_msg(self, events: Iterable) -> None:
|
||||
for event in events:
|
||||
insort(self._live_names, event.name.raw)
|
||||
|
||||
def to_msg(self):
|
||||
board = self.get_events_mapping()
|
||||
outbound = []
|
||||
for event_name in self._live_names:
|
||||
msg = self.get_event_msg_type().new_message()
|
||||
msg.name = event_name
|
||||
for event_kind in board.get(event_name, {}):
|
||||
setattr(msg, event_kind, True)
|
||||
outbound.append(msg)
|
||||
return outbound
|
||||
|
||||
def create_alerts(self, event_types: list[str], callback_args=None):
|
||||
callback_args = [] if callback_args is None else callback_args
|
||||
board = self.get_events_mapping()
|
||||
spawned: list[AlertCard] = []
|
||||
for event_name in self._live_names:
|
||||
variants = board.get(event_name, {})
|
||||
for event_type in event_types:
|
||||
chosen = variants.get(event_type)
|
||||
if chosen is None:
|
||||
continue
|
||||
alert = self._realize(chosen, callback_args)
|
||||
age_frames = self.event_counters.get(event_name, 0) + 1
|
||||
if age_frames * DT_CTRL < alert.creation_delay:
|
||||
continue
|
||||
alert.alert_type = f"{self.get_event_name(event_name)}/{event_type}"
|
||||
alert.event_type = event_type
|
||||
spawned.append(alert)
|
||||
return spawned
|
||||
|
||||
@staticmethod
|
||||
def _realize(candidate: AlertCard | AlertFactory, callback_args: list) -> AlertCard:
|
||||
return candidate if isinstance(candidate, AlertCard) else candidate(*callback_args)
|
||||
|
||||
@abstractmethod
|
||||
def get_events_mapping(self) -> dict[int, dict[str, AlertCard | AlertFactory]]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_event_name(self, event: int) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_event_msg_type(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _mici_reframe(primary: str, secondary: str) -> tuple[str, str, log.SelfdriveState.AlertSize]:
|
||||
if HARDWARE.get_device_type() == "mici":
|
||||
return secondary, primary, AlertSize.small
|
||||
return primary, secondary, AlertSize.mid
|
||||
|
||||
|
||||
class NoEntryCard(AlertCard):
|
||||
def __init__(self,
|
||||
alert_text_2: str,
|
||||
alert_text_1: str = "openpilot Unavailable",
|
||||
visual_alert: car.CarControl.HUDControl.VisualAlert = VisualAlert.none,
|
||||
priority: Tier = Tier.LOW):
|
||||
primary, secondary, size = _mici_reframe(alert_text_1, alert_text_2)
|
||||
super().__init__(primary, secondary, AlertStatus.normal, size, priority, visual_alert, AudibleAlert.refuse, 3.0)
|
||||
|
||||
|
||||
class GentleDisableCard(AlertCard):
|
||||
def __init__(self, alert_text_2: str):
|
||||
super().__init__(
|
||||
"TAKE CONTROL IMMEDIATELY",
|
||||
alert_text_2,
|
||||
AlertStatus.userPrompt,
|
||||
AlertSize.full,
|
||||
Tier.MID,
|
||||
VisualAlert.steerRequired,
|
||||
AudibleAlert.warningSoft,
|
||||
2.0,
|
||||
)
|
||||
|
||||
|
||||
class PendingDisableCard(GentleDisableCard):
|
||||
def __init__(self, alert_text_2: str):
|
||||
super().__init__(alert_text_2)
|
||||
self.alert_text_1 = "openpilot will disengage"
|
||||
|
||||
|
||||
class HardDisableCard(AlertCard):
|
||||
def __init__(self, alert_text_2: str):
|
||||
super().__init__(
|
||||
"TAKE CONTROL IMMEDIATELY",
|
||||
alert_text_2,
|
||||
AlertStatus.critical,
|
||||
AlertSize.full,
|
||||
Tier.HIGHEST,
|
||||
VisualAlert.steerRequired,
|
||||
AudibleAlert.warningImmediate,
|
||||
4.0,
|
||||
)
|
||||
|
||||
|
||||
class ChimeCard(AlertCard):
|
||||
def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert):
|
||||
super().__init__("", "", AlertStatus.normal, AlertSize.none, Tier.MID, VisualAlert.none, audible_alert, 0.2)
|
||||
|
||||
|
||||
class BannerCard(AlertCard):
|
||||
def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Tier = Tier.LOWER, creation_delay: float = 0.0):
|
||||
size = AlertSize.mid if alert_text_2 else AlertSize.small
|
||||
super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, size, priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay)
|
||||
|
||||
|
||||
class BootCard(AlertCard):
|
||||
def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal):
|
||||
if HARDWARE.get_device_type() == "mici":
|
||||
compact_secondary = "" if alert_text_2 == "Always keep hands on wheel and eyes on road" else alert_text_2
|
||||
super().__init__(alert_text_1, compact_secondary, alert_status, AlertSize.small, Tier.LOWER, VisualAlert.none, AudibleAlert.none, 5.0)
|
||||
else:
|
||||
super().__init__(alert_text_1, alert_text_2, alert_status, AlertSize.mid, Tier.LOWER, VisualAlert.none, AudibleAlert.none, 5.0)
|
||||
|
||||
|
||||
class AlertBase(AlertCard):
|
||||
pass
|
||||
|
||||
|
||||
NULL_ALERT = AlertCard("", "", AlertStatus.normal, AlertSize.none, Tier.LOWEST, VisualAlert.none, AudibleAlert.none, 0.0)
|
||||
58
iqpilot/common/auto_units.py
Normal file
58
iqpilot/common/auto_units.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import time
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.iqpilot.common.geo_regions import UNKNOWN_REGION, region_for_position, region_is_metric
|
||||
|
||||
CHECK_INTERVAL = 10.0
|
||||
CONFIRMATIONS = 3
|
||||
|
||||
|
||||
class AutoUnits:
|
||||
def __init__(self, params: Params | None = None):
|
||||
self.params = params or Params()
|
||||
self._next_check = 0.0
|
||||
self._candidate = UNKNOWN_REGION
|
||||
self._confirmations = 0
|
||||
|
||||
def _position(self) -> tuple[float, float, bool]:
|
||||
from openpilot.selfdrive.ui.lib.nav_helpers import current_or_last_gps_position
|
||||
|
||||
lat, lon, _, valid = current_or_last_gps_position(self.params)
|
||||
return lat, lon, valid
|
||||
|
||||
def update(self, now: float | None = None) -> None:
|
||||
if not self.params.get_bool("IQAutoUnits"):
|
||||
self._candidate = UNKNOWN_REGION
|
||||
self._confirmations = 0
|
||||
return
|
||||
|
||||
now = time.monotonic() if now is None else now
|
||||
if now < self._next_check:
|
||||
return
|
||||
self._next_check = now + CHECK_INTERVAL
|
||||
|
||||
lat, lon, valid = self._position()
|
||||
region = region_for_position(lat, lon) if valid else UNKNOWN_REGION
|
||||
if region == UNKNOWN_REGION:
|
||||
self._confirmations = 0
|
||||
return
|
||||
|
||||
if region != self._candidate:
|
||||
self._candidate = region
|
||||
self._confirmations = 1
|
||||
return
|
||||
|
||||
self._confirmations += 1
|
||||
if self._confirmations < CONFIRMATIONS:
|
||||
return
|
||||
|
||||
if region == self.params.get("IQAutoUnitsRegion"):
|
||||
return
|
||||
|
||||
self.params.put("IQAutoUnitsRegion", region)
|
||||
|
||||
metric = region_is_metric(region)
|
||||
if metric != self.params.get_bool("IsMetric"):
|
||||
self.params.put_bool("IsMetric", metric)
|
||||
cloudlog.warning(f"auto units: {region} detected, switching to {'km/h' if metric else 'mph'}")
|
||||
140
iqpilot/common/geo_regions.py
Normal file
140
iqpilot/common/geo_regions.py
Normal file
@@ -0,0 +1,140 @@
|
||||
MPH_REGIONS = ("US", "GB", "LR")
|
||||
METRIC_REGION = "METRIC"
|
||||
UNKNOWN_REGION = ""
|
||||
|
||||
_US_CONUS = [
|
||||
(-123.32, 49.00), (-117.03, 49.00), (-110.00, 49.00), (-104.05, 49.00), (-97.23, 49.00), (-95.15, 49.00),
|
||||
(-95.15, 49.38), (-94.82, 49.30), (-94.68, 48.77), (-93.85, 48.63), (-93.35, 48.62), (-92.72, 48.54),
|
||||
(-92.30, 48.24), (-91.55, 48.10), (-90.84, 48.24), (-89.99, 48.02), (-89.60, 48.02), (-89.10, 48.32),
|
||||
(-88.40, 48.30), (-87.00, 47.80), (-85.60, 47.15), (-84.60, 46.75), (-84.42, 46.56), (-84.30, 46.49),
|
||||
(-84.12, 46.28), (-83.90, 46.05), (-83.40, 45.75), (-82.90, 45.05), (-82.55, 44.00), (-82.42, 43.00),
|
||||
(-82.70, 42.47), (-82.93, 42.34), (-83.00, 42.33), (-83.05, 42.32), (-83.075, 42.312), (-83.13, 42.25),
|
||||
(-83.15, 42.18), (-83.11, 42.10), (-83.09, 42.02),
|
||||
(-82.50, 41.70), (-81.50, 42.00), (-80.20, 42.40), (-79.06, 42.85), (-79.05, 43.27), (-78.00, 43.45),
|
||||
(-77.00, 43.65), (-76.40, 44.10), (-75.80, 44.50), (-74.75, 45.00), (-73.35, 45.01), (-71.50, 45.01),
|
||||
(-71.29, 45.30), (-70.90, 45.30), (-70.72, 45.42), (-70.31, 45.86), (-70.05, 46.44), (-69.99, 46.70),
|
||||
(-69.24, 47.46), (-68.90, 47.20), (-68.38, 47.29), (-67.79, 47.07), (-67.78, 45.94), (-67.42, 45.60),
|
||||
(-67.03, 44.80), (-68.00, 44.30), (-69.06, 43.80), (-70.20, 43.60), (-70.80, 42.85), (-70.00, 41.90),
|
||||
(-70.00, 41.55), (-71.20, 41.30), (-72.00, 41.05), (-73.90, 40.55), (-74.20, 39.60), (-75.05, 38.45),
|
||||
(-75.90, 37.05), (-75.50, 35.20), (-78.50, 33.85), (-80.90, 32.00), (-81.40, 30.70), (-80.03, 26.80),
|
||||
(-80.15, 25.15), (-81.20, 24.55), (-82.00, 26.40), (-82.80, 27.80), (-83.00, 29.15), (-84.30, 29.90),
|
||||
(-85.30, 29.65), (-87.50, 30.25), (-89.00, 29.15), (-89.40, 28.95), (-91.30, 29.10), (-93.80, 29.65),
|
||||
(-95.00, 29.10), (-97.10, 27.80), (-97.14, 25.96), (-98.30, 26.05), (-99.10, 26.40), (-99.50, 27.60),
|
||||
(-100.40, 28.50), (-101.40, 29.77), (-102.30, 29.88), (-102.90, 29.30), (-103.30, 29.00), (-104.37, 29.56),
|
||||
(-104.68, 30.13), (-105.30, 30.80), (-105.85, 31.30), (-106.15, 31.50), (-106.30, 31.68), (-106.45, 31.755),
|
||||
(-106.53, 31.786), (-108.21, 31.783), (-108.21, 31.33), (-111.07, 31.33), (-114.72, 32.72),
|
||||
(-117.13, 32.53), (-118.40, 33.75), (-119.80, 34.40), (-120.65, 35.10), (-121.90, 36.60), (-122.52, 37.78),
|
||||
(-123.75, 39.40), (-124.20, 40.45), (-124.15, 42.00), (-124.05, 43.35), (-123.95, 46.25), (-124.75, 48.40),
|
||||
(-123.30, 48.25), (-123.15, 48.70),
|
||||
]
|
||||
|
||||
_US_ALASKA = [
|
||||
(-141.00, 70.20), (-141.00, 60.30), (-139.05, 60.35), (-137.45, 58.95), (-136.47, 59.63), (-135.03, 59.57),
|
||||
(-134.30, 58.90), (-133.40, 58.20), (-132.20, 56.90), (-130.60, 56.20), (-130.01, 54.80), (-131.80, 54.70),
|
||||
(-133.80, 55.90), (-136.60, 58.20), (-140.00, 59.70), (-145.00, 60.00), (-149.20, 59.10), (-152.30, 57.30),
|
||||
(-155.20, 55.60), (-160.00, 54.60), (-164.50, 54.40), (-162.00, 57.50), (-165.00, 60.20), (-167.50, 62.50),
|
||||
(-164.00, 64.50), (-168.10, 65.60), (-166.00, 68.30), (-161.00, 70.30), (-156.50, 71.40), (-150.00, 70.50),
|
||||
]
|
||||
|
||||
_US_ALEUTIANS_EAST = [(-180.00, 51.00), (-158.50, 51.00), (-158.50, 56.00), (-180.00, 56.00)]
|
||||
_US_ALEUTIANS_WEST = [(172.00, 51.00), (180.00, 51.00), (180.00, 54.00), (172.00, 54.00)]
|
||||
_US_HAWAII = [(-160.50, 18.80), (-154.70, 18.80), (-154.70, 22.30), (-160.50, 22.30)]
|
||||
_US_PUERTO_RICO = [(-67.35, 17.85), (-64.55, 17.85), (-64.55, 18.55), (-67.35, 18.55)]
|
||||
_US_MARIANAS = [(144.50, 13.10), (146.20, 13.10), (146.20, 20.60), (144.50, 20.60)]
|
||||
_US_SAMOA = [(-171.20, -14.60), (-168.10, -14.60), (-168.10, -11.00), (-171.20, -11.00)]
|
||||
|
||||
_GB_BRITAIN = [
|
||||
(-5.72, 50.07), (-4.20, 50.32), (-3.41, 50.62), (-2.45, 50.52), (-1.80, 50.72), (-0.90, 50.77),
|
||||
(0.58, 50.85), (1.35, 51.13), (1.38, 51.38), (1.15, 51.79), (1.35, 51.95), (1.75, 52.48),
|
||||
(1.30, 52.94), (0.49, 52.94), (0.34, 53.15), (-0.08, 53.57), (-0.08, 54.12), (-0.61, 54.49),
|
||||
(-1.18, 54.69), (-1.38, 54.91), (-1.50, 55.13), (-2.00, 55.77), (-2.52, 56.00), (-2.62, 56.28),
|
||||
(-2.47, 56.55), (-2.21, 56.96), (-2.08, 57.14), (-1.77, 57.50), (-2.00, 57.70), (-2.96, 57.68),
|
||||
(-3.90, 57.60), (-4.22, 57.48), (-4.05, 57.81), (-3.85, 58.01), (-3.65, 58.12), (-3.09, 58.44),
|
||||
(-3.01, 58.67), (-3.35, 58.62), (-3.52, 58.60), (-4.99, 58.62), (-5.05, 58.45), (-5.16, 57.90), (-5.70, 57.72),
|
||||
(-5.72, 57.28), (-5.83, 57.00), (-5.72, 56.65), (-5.47, 56.41), (-5.79, 55.60), (-5.62, 55.31),
|
||||
(-4.82, 55.64), (-4.63, 55.46), (-4.85, 55.24), (-5.12, 54.84), (-4.86, 54.63), (-4.44, 54.87),
|
||||
(-4.05, 54.83), (-3.26, 54.98), (-3.05, 54.90), (-3.50, 54.72), (-3.23, 54.07), (-3.05, 53.82),
|
||||
(-3.40, 53.34), (-3.83, 53.33), (-4.63, 53.42), (-4.72, 53.28), (-4.35, 53.12), (-4.76, 52.80),
|
||||
(-4.06, 52.72), (-4.09, 52.41), (-4.66, 52.09), (-5.31, 51.88), (-5.06, 51.70), (-4.70, 51.67),
|
||||
(-4.30, 51.62), (-3.95, 51.56), (-3.70, 51.48), (-3.17, 51.45), (-2.99, 51.55), (-2.67, 51.62),
|
||||
(-2.48, 51.72), (-2.30, 51.85), (-2.70, 51.50), (-2.98, 51.35), (-3.00, 51.20), (-3.47, 51.21),
|
||||
(-4.12, 51.21), (-4.55, 50.83), (-5.08, 50.42), (-5.48, 50.21),
|
||||
]
|
||||
|
||||
_GB_NORTHERN_IRELAND = [
|
||||
(-6.03, 54.05), (-6.28, 54.10), (-6.65, 54.17), (-6.86, 54.33), (-7.16, 54.34), (-7.31, 54.12),
|
||||
(-7.62, 54.14), (-8.00, 54.31), (-8.18, 54.47), (-8.20, 54.52), (-7.90, 54.55), (-7.85, 54.72), (-7.55, 54.75),
|
||||
(-7.44, 54.94), (-7.25, 55.06), (-6.95, 55.22), (-6.50, 55.25), (-6.25, 55.31), (-6.03, 55.22),
|
||||
(-5.43, 54.62), (-5.53, 54.24),
|
||||
]
|
||||
|
||||
_GB_ISLE_OF_MAN = [(-4.85, 54.03), (-4.30, 54.03), (-4.30, 54.42), (-4.85, 54.42)]
|
||||
_GB_CHANNEL_ISLANDS = [(-2.75, 49.15), (-1.95, 49.15), (-1.95, 49.80), (-2.75, 49.80)]
|
||||
_GB_ISLE_OF_WIGHT = [(-1.60, 50.55), (-1.05, 50.55), (-1.05, 50.80), (-1.60, 50.80)]
|
||||
_GB_OUTER_HEBRIDES = [(-7.75, 56.75), (-6.05, 56.75), (-6.05, 58.55), (-7.75, 58.55)]
|
||||
_GB_INNER_HEBRIDES = [(-7.00, 55.45), (-5.55, 55.45), (-5.55, 57.85), (-7.00, 57.85)]
|
||||
_GB_ORKNEY = [(-3.50, 58.70), (-2.35, 58.70), (-2.35, 59.45), (-3.50, 59.45)]
|
||||
_GB_SHETLAND = [(-1.85, 59.80), (-0.65, 59.80), (-0.65, 60.90), (-1.85, 60.90)]
|
||||
|
||||
_LR_LIBERIA = [
|
||||
(-11.46, 6.77), (-11.30, 6.95), (-11.16, 7.15), (-11.05, 7.40), (-10.85, 7.75), (-10.60, 8.00),
|
||||
(-10.28, 8.49), (-9.70, 8.54), (-9.35, 7.80),
|
||||
(-8.85, 7.40), (-8.48, 7.55), (-8.30, 6.90), (-7.95, 6.20), (-7.60, 5.20), (-7.40, 4.55),
|
||||
(-7.74, 4.33), (-8.46, 4.61), (-9.06, 4.97), (-9.52, 5.36), (-10.08, 5.85), (-10.40, 6.11),
|
||||
(-10.83, 6.27),
|
||||
]
|
||||
|
||||
_REGION_RINGS = {
|
||||
"US": (_US_CONUS, _US_ALASKA, _US_ALEUTIANS_EAST, _US_ALEUTIANS_WEST, _US_HAWAII, _US_PUERTO_RICO,
|
||||
_US_MARIANAS, _US_SAMOA),
|
||||
"GB": (_GB_BRITAIN, _GB_NORTHERN_IRELAND, _GB_ISLE_OF_MAN, _GB_CHANNEL_ISLANDS, _GB_ISLE_OF_WIGHT,
|
||||
_GB_OUTER_HEBRIDES, _GB_INNER_HEBRIDES, _GB_ORKNEY, _GB_SHETLAND),
|
||||
"LR": (_LR_LIBERIA,),
|
||||
}
|
||||
|
||||
|
||||
def _bounded(rings):
|
||||
out = []
|
||||
for ring in rings:
|
||||
lons = [p[0] for p in ring]
|
||||
lats = [p[1] for p in ring]
|
||||
out.append(((min(lons), min(lats), max(lons), max(lats)), ring))
|
||||
return tuple(out)
|
||||
|
||||
|
||||
_REGIONS = tuple((region, _bounded(rings)) for region, rings in _REGION_RINGS.items())
|
||||
|
||||
|
||||
def _point_in_ring(lat: float, lon: float, ring) -> bool:
|
||||
inside = False
|
||||
count = len(ring)
|
||||
j = count - 1
|
||||
for i in range(count):
|
||||
lon_i, lat_i = ring[i]
|
||||
lon_j, lat_j = ring[j]
|
||||
if (lat_i > lat) != (lat_j > lat):
|
||||
crossing = (lon_j - lon_i) * (lat - lat_i) / (lat_j - lat_i) + lon_i
|
||||
if lon < crossing:
|
||||
inside = not inside
|
||||
j = i
|
||||
return inside
|
||||
|
||||
|
||||
def valid_position(lat: float, lon: float) -> bool:
|
||||
return abs(lat) <= 90.0 and abs(lon) <= 180.0 and (abs(lat) > 1e-4 or abs(lon) > 1e-4)
|
||||
|
||||
|
||||
def region_for_position(lat: float, lon: float) -> str:
|
||||
if not valid_position(lat, lon):
|
||||
return UNKNOWN_REGION
|
||||
|
||||
for region, rings in _REGIONS:
|
||||
for (min_lon, min_lat, max_lon, max_lat), ring in rings:
|
||||
if min_lon <= lon <= max_lon and min_lat <= lat <= max_lat and _point_in_ring(lat, lon, ring):
|
||||
return region
|
||||
|
||||
return METRIC_REGION
|
||||
|
||||
|
||||
def region_is_metric(region: str) -> bool:
|
||||
return bool(region) and region not in MPH_REGIONS
|
||||
15
iqpilot/common/k3_slc_log.py
Normal file
15
iqpilot/common/k3_slc_log.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from datetime import datetime
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
K3_SLC_LOG_FILE = "/data/openpilot/k3_slc.txt"
|
||||
|
||||
|
||||
def k3_slc_log(message: str) -> None:
|
||||
try:
|
||||
with open(K3_SLC_LOG_FILE, "a") as f:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
f.write(f"[{timestamp}] {message}\n")
|
||||
f.flush()
|
||||
except Exception as e:
|
||||
cloudlog.error(f"[K3_SLC] Failed to write debug log: {e}")
|
||||
126
iqpilot/common/slc_utilities.py
Normal file
126
iqpilot/common/slc_utilities.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
requests = None
|
||||
|
||||
from openpilot.iqpilot.common.slc_variables import EARTH_RADIUS
|
||||
|
||||
|
||||
def calculate_bearing_offset(latitude, longitude, current_bearing, distance):
|
||||
"""
|
||||
Calculate new GPS coordinates given a starting point, bearing, and distance.
|
||||
Used for Mapbox API lookahead calculations.
|
||||
|
||||
Args:
|
||||
latitude: Starting latitude in degrees
|
||||
longitude: Starting longitude in degrees
|
||||
current_bearing: Bearing in degrees (0-360)
|
||||
distance: Distance to project in meters
|
||||
|
||||
Returns:
|
||||
Tuple of (new_latitude, new_longitude) in degrees
|
||||
"""
|
||||
bearing = math.radians(current_bearing)
|
||||
lat_rad = math.radians(latitude)
|
||||
lon_rad = math.radians(longitude)
|
||||
|
||||
delta = distance / EARTH_RADIUS
|
||||
|
||||
new_lat = math.asin(math.sin(lat_rad) * math.cos(delta) + math.cos(lat_rad) * math.sin(delta) * math.cos(bearing))
|
||||
new_lon = lon_rad + math.atan2(math.sin(bearing) * math.sin(delta) * math.cos(lat_rad), math.cos(delta) - math.sin(lat_rad) * math.sin(new_lat))
|
||||
return math.degrees(new_lat), math.degrees(new_lon)
|
||||
|
||||
|
||||
def calculate_distance_to_point(lat1, lon1, lat2, lon2):
|
||||
"""
|
||||
Calculate the great circle distance between two GPS points using the Haversine formula.
|
||||
|
||||
Args:
|
||||
lat1, lon1: First point coordinates in degrees
|
||||
lat2, lon2: Second point coordinates in degrees
|
||||
|
||||
Returns:
|
||||
Distance in meters
|
||||
"""
|
||||
lat1_rad = math.radians(lat1)
|
||||
lon1_rad = math.radians(lon1)
|
||||
lat2_rad = math.radians(lat2)
|
||||
lon2_rad = math.radians(lon2)
|
||||
|
||||
delta_lat = lat2_rad - lat1_rad
|
||||
delta_lon = lon2_rad - lon1_rad
|
||||
|
||||
a = (math.sin(delta_lat / 2) ** 2) + math.cos(lat1_rad) * math.cos(lat2_rad) * (math.sin(delta_lon / 2) ** 2)
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
return EARTH_RADIUS * c
|
||||
|
||||
|
||||
def calculate_lane_width(lane_line1, lane_line2, road_edge=None):
|
||||
"""
|
||||
Calculate the width of a lane based on lane line positions.
|
||||
Used for speed limit filler to determine road width.
|
||||
|
||||
Args:
|
||||
lane_line1: First lane line object with x, y coordinates
|
||||
lane_line2: Second lane line object with x, y coordinates
|
||||
road_edge: Optional road edge object with x, y coordinates
|
||||
|
||||
Returns:
|
||||
Lane width in meters
|
||||
"""
|
||||
lane_line1_x = np.asarray(lane_line1.x)
|
||||
lane_line1_y = np.asarray(lane_line1.y)
|
||||
|
||||
lane_line2_x = np.asarray(lane_line2.x)
|
||||
lane_line2_y = np.asarray(lane_line2.y)
|
||||
|
||||
lane_y_interp = np.interp(lane_line2_x, lane_line1_x, lane_line1_y)
|
||||
distance_to_lane = np.median(np.abs(lane_line2_y - lane_y_interp))
|
||||
|
||||
if road_edge is None:
|
||||
return distance_to_lane
|
||||
|
||||
road_edge_x = np.asarray(road_edge.x)
|
||||
road_edge_y = np.asarray(road_edge.y)
|
||||
|
||||
edge_y_interp = np.interp(lane_line2_x, road_edge_x, road_edge_y)
|
||||
distance_to_edge = np.median(np.abs(lane_line2_y - edge_y_interp))
|
||||
|
||||
return max(distance_to_lane, distance_to_edge)
|
||||
|
||||
|
||||
def is_url_pingable(url):
|
||||
"""
|
||||
Check if a URL is accessible and responding.
|
||||
Used to verify Mapbox/Overpass API availability before making requests.
|
||||
|
||||
Args:
|
||||
url: URL to ping
|
||||
|
||||
Returns:
|
||||
Boolean indicating if URL is accessible
|
||||
"""
|
||||
if not url:
|
||||
return False
|
||||
|
||||
if requests is None:
|
||||
return False
|
||||
|
||||
if not hasattr(is_url_pingable, "session"):
|
||||
is_url_pingable.session = requests.Session()
|
||||
is_url_pingable.session.headers.update({"User-Agent": "iqpilot-ping-test/1.0"})
|
||||
|
||||
try:
|
||||
response = is_url_pingable.session.head(url, timeout=10, allow_redirects=True)
|
||||
if response.status_code in (405, 501):
|
||||
response = is_url_pingable.session.get(url, timeout=10, allow_redirects=True, stream=True)
|
||||
|
||||
is_accessible = response.ok
|
||||
response.close()
|
||||
return is_accessible
|
||||
except Exception:
|
||||
return False
|
||||
35
iqpilot/common/slc_variables.py
Normal file
35
iqpilot/common/slc_variables.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Earth radius in meters (for GPS calculations)
|
||||
EARTH_RADIUS = 6378137
|
||||
|
||||
# Mapbox API limits
|
||||
FREE_MAPBOX_REQUESTS = 100_000
|
||||
|
||||
# Speed limit offset zones for different unit systems
|
||||
# Each entry is (min_speed_ms, max_speed_ms, param_name); the param value is a
|
||||
# percent offset applied to the resolved limit (e.g. 10 -> +10%), lower bound inclusive
|
||||
|
||||
OFFSET_PERCENT_MAX = 50.0
|
||||
|
||||
OFFSET_MAP_IMPERIAL = [
|
||||
(0, 8.94, "speed_limit_offset1"), # 0-20 mph
|
||||
(8.94, 17.88, "speed_limit_offset2"), # 20-40 mph
|
||||
(17.88, float("inf"), "speed_limit_offset3"), # 40+ mph
|
||||
]
|
||||
|
||||
OFFSET_MAP_METRIC = [
|
||||
(0, 8.33, "speed_limit_offset1"), # 0-30 km/h
|
||||
(8.33, 16.67, "speed_limit_offset2"), # 30-60 km/h
|
||||
(16.67, float("inf"), "speed_limit_offset3"), # 60+ km/h
|
||||
]
|
||||
|
||||
# Speed limit filler constants
|
||||
BOUNDING_BOX_RADIUS_DEGREE = 0.1
|
||||
MAX_ENTRIES = 1_000_000
|
||||
MAX_OVERPASS_DATA_BYTES = 1_073_741_824
|
||||
MAX_OVERPASS_REQUESTS = 10_000
|
||||
METERS_PER_DEG_LAT = 111_320
|
||||
VETTING_INTERVAL_DAYS = 7
|
||||
|
||||
# Overpass API URLs
|
||||
OVERPASS_API_URL = "https://overpass-api.de/api/interpreter"
|
||||
OVERPASS_STATUS_URL = "https://overpass-api.de/api/status"
|
||||
20
iqpilot/common/speed_assist_tiers.py
Normal file
20
iqpilot/common/speed_assist_tiers.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
Engagement tiers for the speed-assist feature. A tier is persisted as an integer
|
||||
under the "IQSpeedAssistMode" param; the ordinal IS the stored value and must remain
|
||||
stable (0..3), ordered by how much the tier is allowed to intervene.
|
||||
"""
|
||||
from enum import IntEnum
|
||||
|
||||
STORE_KEY = "IQSpeedAssistMode"
|
||||
|
||||
# none -> just display the limit -> highlight overspeed -> move the set speed
|
||||
SpeedAssistTier = IntEnum("SpeedAssistTier", "DISABLED ADVISORY ALERTING ACTUATING", start=0)
|
||||
|
||||
DEFAULT_TIER = SpeedAssistTier.ADVISORY
|
||||
|
||||
|
||||
def actuates_speed(tier) -> bool:
|
||||
"""Only the top tier is permitted to drive the cruise set speed."""
|
||||
return int(tier) == SpeedAssistTier.ACTUATING
|
||||
43
iqpilot/common/steer_delay.py
Normal file
43
iqpilot/common/steer_delay.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
|
||||
Chooses which steer-actuator delay the lateral controllers run with: the value the
|
||||
live estimator learned, or the driver's fixed software delay — gated by the
|
||||
"IQLiveSteerDelay" param. The pick is mirrored into "IQSteerDelayCache" so consumers that do
|
||||
not subscribe to liveDelay can still read the current value.
|
||||
"""
|
||||
from openpilot.common.params import Params
|
||||
|
||||
_ENABLE_KEY = "IQLiveSteerDelay"
|
||||
_FIXED_KEY = "IQSoftwareSteerDelay"
|
||||
_CACHE_KEY = "IQSteerDelayCache"
|
||||
|
||||
|
||||
def resolve_steer_delay(params, stock_delay):
|
||||
"""Learned lateral delay while live-learning is enabled, otherwise the stock delay."""
|
||||
if not params.get_bool(_ENABLE_KEY):
|
||||
return stock_delay
|
||||
return float(params.get(_CACHE_KEY, return_default=True))
|
||||
|
||||
|
||||
def cached_steer_delay():
|
||||
"""Last value SteerDelayPublisher mirrored into the param — usable without a
|
||||
liveDelay subscription (e.g. at process startup)."""
|
||||
return Params().get(_CACHE_KEY, return_default=True)
|
||||
|
||||
|
||||
class SteerDelayPublisher:
|
||||
"""Refreshes IQSteerDelayCache every lag message: the learned live delay when the
|
||||
toggle is on, else the actuator delay plus the driver's fixed software offset."""
|
||||
|
||||
def __init__(self, car_params):
|
||||
self._params = Params()
|
||||
self._actuator_delay = car_params.steerActuatorDelay
|
||||
|
||||
def _fixed_delay(self):
|
||||
return self._actuator_delay + self._params.get(_FIXED_KEY, return_default=True)
|
||||
|
||||
def update(self, lag_msg):
|
||||
live = self._params.get_bool(_ENABLE_KEY)
|
||||
value = lag_msg.liveDelay.lateralDelay if live else self._fixed_delay()
|
||||
self._params.put_nonblocking(_CACHE_KEY, value)
|
||||
1
iqpilot/common/version.h
Normal file
1
iqpilot/common/version.h
Normal file
@@ -0,0 +1 @@
|
||||
#define IQPILOT_VERSION "IQ.Pilot 1.0c"
|
||||
Reference in New Issue
Block a user