import iqpilot.cereal.messaging as messaging from iqpilot.cereal import log, car, custom from iqpilot.common.params import Params from iqpilot.common.constants import CV from iqpilot.common.atlas_alerts import EventBook as EventsBase, Tier as Priority, Tags as ET, AlertCard as Alert, \ NoEntryCard as NoEntryAlert, HardDisableCard as ImmediateDisableAlert, ChimeCard as EngagementAlert, \ BannerCard as NormalPermanentAlert, AlertFactory as AlertCallbackType, car_mode_entry_alert as wrong_car_mode_alert from iqpilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitAssistState AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus VisualAlert = car.CarControl.HUDControl.VisualAlert AudibleAlert = car.CarControl.HUDControl.AudibleAlert AudibleAlertIQ = custom.IQState.AudibleAlert EventNameIQ = custom.IQOnroadEvent.EventName # get event name from enum EVENT_NAME_IQ = {v: k for k, v in EventNameIQ.schema.enumerants.items()} def _get_longitudinal_plan_ext(sm: messaging.SubMaster): return sm['iqPlan'] def speed_limit_adjust_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: plan = _get_longitudinal_plan_ext(sm) resolver = plan.speedLimit.resolver assist = plan.speedLimit.assist speed_conv = CV.MS_TO_KPH if metric else CV.MS_TO_MPH speed = round(resolver.speedLimit * speed_conv) unit = "km/h" if metric else "mph" if assist.state == SpeedLimitAssistState.adapting: message = f"Speed Limit: Adjusting to {speed} {unit}" else: message = f"Speed Limit: Active at {speed} {unit}" return Alert( message, "", AlertStatus.normal, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlert.none, 4.) def speed_limit_pre_active_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: plan = _get_longitudinal_plan_ext(sm) resolver = plan.speedLimit.resolver speed_conv = CV.MS_TO_KPH if metric else CV.MS_TO_MPH unit = "km/h" if metric else "mph" pending_speed = round(resolver.speedLimit * speed_conv) last_speed = resolver.speedLimitFinalLast * speed_conv is_lower = pending_speed < last_speed or last_speed <= 0 confirm_hint = "SET" if is_lower else "RES" return Alert( f"Speed Limit: {pending_speed} {unit}", f"Press {confirm_hint} to apply", AlertStatus.normal, AlertSize.mid, Priority.LOW, VisualAlert.none, AudibleAlertIQ.promptSingleLow, .1) def speed_limit_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: resolver = _get_longitudinal_plan_ext(sm).speedLimit.resolver speed_conv = CV.MS_TO_KPH if metric else CV.MS_TO_MPH speed = round(resolver.speedLimit * speed_conv) unit = "km/h" if metric else "mph" return Alert( f"Speed Limit changed to {speed} {unit}", "", AlertStatus.normal, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlertIQ.promptSingleHigh, 3.) def construction_zone_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: resolver = _get_longitudinal_plan_ext(sm).speedLimit.resolver speed_conv = CV.MS_TO_KPH if metric else CV.MS_TO_MPH speed = round(resolver.speedLimit * speed_conv) unit = "KM/H" if metric else "MPH" return Alert( f"Construction Zone Detected: Speed {speed} {unit}", "", AlertStatus.userPrompt, AlertSize.small, Priority.MID, VisualAlert.none, AudibleAlertIQ.promptSingleHigh, 4.) _CAMERA_LABELS = { int(custom.IQNavState.CameraType.fixedSpeed): "Speed Camera", int(custom.IQNavState.CameraType.mobileSpeed): "Mobile Speed Camera", int(custom.IQNavState.CameraType.sectionStart): "Average-Speed Zone", int(custom.IQNavState.CameraType.sectionEnd): "Average-Speed Zone Ends", int(custom.IQNavState.CameraType.averageZone): "Average-Speed Zone", int(custom.IQNavState.CameraType.redLight): "Red-Light Camera", int(custom.IQNavState.CameraType.bump): "Speed Bump", int(custom.IQNavState.CameraType.alpr): "Flock / ALPR Camera", int(custom.IQNavState.CameraType.police): "Police Reported Ahead", } _POLICE_CHIMED_IDS: set[str] = set() _USA_REGION_CODES = frozenset(("US", "USA", "UNITED STATES", "UNITED STATES OF AMERICA")) def _configured_country_code() -> str: try: value = Params().get("OsmLocationName") except Exception: return "" if isinstance(value, bytes): value = value.decode("utf-8", "ignore") return str(value or "").strip().upper() def _alpr_alert_labels(country_code: str) -> tuple[str, str]: is_row = bool(country_code) and country_code not in _USA_REGION_CODES if is_row: return "Traffic / ALPR Camera", "Traffic / ALPR Camera Detected" return "Flock / ALPR Camera", "Flock Camera Detected" def speed_camera_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: nav = sm['iqNavState'] ctype = int(getattr(nav.cameraType, "raw", nav.cameraType)) label = _CAMERA_LABELS.get(ctype, "Speed Camera") distance = float(nav.cameraDistance) alpr_detected_label = "Flock Camera Detected" if ctype == int(custom.IQNavState.CameraType.alpr): label, alpr_detected_label = _alpr_alert_labels(_configured_country_code()) # RF (BLE/WiFi) Flock detection is a live proximity hit with no meaningful # distance — flockd/navd flag it with distance 0 on the alpr camera type. if ctype == int(custom.IQNavState.CameraType.alpr) and distance <= 0.0: return Alert( alpr_detected_label, "", AlertStatus.normal, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .2) if metric: dist_str = f"{distance:.0f} m" if distance < 1000.0 else f"{distance / 1000.0:.1f} km" else: feet = distance * 3.28084 dist_str = f"{int(round(feet / 10.0) * 10)} ft" if feet < 1000.0 else f"{distance * 0.000621371:.1f} mi" detail = dist_str if float(nav.cameraSpeedLimit) > 0.0: speed_conv = CV.MS_TO_KPH if metric else CV.MS_TO_MPH unit = "km/h" if metric else "mph" detail += f" • {round(float(nav.cameraSpeedLimit) * speed_conv)} {unit}" audible = AudibleAlert.prompt if ctype == int(custom.IQNavState.CameraType.police): report_id = str(getattr(nav, "cameraAlertId", "")) should_chime = bool(getattr(nav, "cameraChime", False)) and bool(report_id) and report_id not in _POLICE_CHIMED_IDS if should_chime: _POLICE_CHIMED_IDS.add(report_id) if len(_POLICE_CHIMED_IDS) > 256: _POLICE_CHIMED_IDS.clear() _POLICE_CHIMED_IDS.add(report_id) else: audible = AudibleAlert.none return Alert( f"{label} • {detail}", "", AlertStatus.normal, AlertSize.small, Priority.LOW if ctype == int(custom.IQNavState.CameraType.alpr) else Priority.HIGH, VisualAlert.none, audible, .2) class IQEvents(EventsBase): def __init__(self): super().__init__() self.event_counters = dict.fromkeys(EVENTS_IQ.keys(), 0) def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: return EVENTS_IQ def get_event_name(self, event: int): return EVENT_NAME_IQ[event] def get_event_msg_type(self): return custom.IQOnroadEvent.Event EVENTS_IQ_TYPE = dict[int, dict[str, Alert | AlertCallbackType]] _GUIDANCE_EVENTS: EVENTS_IQ_TYPE = { EventNameIQ.lateralEdgeBlocked: { ET.WARNING: Alert( "Lane Change Blocked", "Road edge detected", AlertStatus.userPrompt, AlertSize.mid, Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1), }, EventNameIQ.speedLimitActive: { ET.WARNING: speed_limit_adjust_alert, }, EventNameIQ.speedLimitPreActive: { ET.WARNING: speed_limit_pre_active_alert, }, EventNameIQ.speedLimitChanged: { ET.WARNING: speed_limit_changed_alert, }, EventNameIQ.speedCameraAhead: { ET.WARNING: speed_camera_alert, }, EventNameIQ.constructionZoneDetected: { ET.WARNING: construction_zone_alert, }, EventNameIQ.navExitLeft: { ET.WARNING: Alert( "Navigation: Exit Maneuver", "Nudge the wheel left to change lanes", AlertStatus.userPrompt, AlertSize.mid, Priority.MID, VisualAlert.none, AudibleAlert.prompt, 1.5), }, EventNameIQ.navExitRight: { ET.WARNING: Alert( "Navigation: Exit Maneuver", "Nudge the wheel right to change lanes", AlertStatus.userPrompt, AlertSize.mid, Priority.MID, VisualAlert.none, AudibleAlert.prompt, 1.5), }, EventNameIQ.navTurnLeft: { ET.WARNING: Alert( "Navigation: Turning Left", "", AlertStatus.normal, AlertSize.small, Priority.MID, VisualAlert.none, AudibleAlert.none, 1.5), }, EventNameIQ.navTurnRight: { ET.WARNING: Alert( "Navigation: Turning Right", "", AlertStatus.normal, AlertSize.small, Priority.MID, VisualAlert.none, AudibleAlert.none, 1.5), }, EventNameIQ.modelTurnLeft: { ET.WARNING: Alert( "Lane Turn Left", "", AlertStatus.normal, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), }, EventNameIQ.modelTurnRight: { ET.WARNING: Alert( "Lane Turn Right", "", AlertStatus.normal, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), }, } _ENGAGE_EVENTS: EVENTS_IQ_TYPE = { EventNameIQ.alcEngaged: { ET.ENABLE: EngagementAlert(AudibleAlert.engage), }, EventNameIQ.alcEngagedSilent: { ET.ENABLE: EngagementAlert(AudibleAlert.none), }, EventNameIQ.alcDisengaged: { ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), }, EventNameIQ.alcDisengagedSilent: { ET.USER_DISABLE: EngagementAlert(AudibleAlert.none), }, EventNameIQ.steerManually: { ET.USER_DISABLE: Alert( "Lane Centering Off", "Steer Manually", AlertStatus.normal, AlertSize.mid, Priority.LOW, VisualAlert.none, AudibleAlert.disengage, 1.), }, EventNameIQ.speedManually: { ET.WARNING: Alert( "Adaptive Cruise Off", "Control Speed Manually", AlertStatus.normal, AlertSize.mid, Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), }, EventNameIQ.steeringOverrideReengageAlc: { ET.WARNING: Alert( "Steering Overridden By Driver", "Double Tap SET or Cycle the Cruise Main to Re-Engage ALC", AlertStatus.userPrompt, AlertSize.mid, Priority.MID, VisualAlert.none, AudibleAlert.prompt, 2.0), }, EventNameIQ.latMismatch: { ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Lateral Controls Mismatch"), ET.NO_ENTRY: NoEntryAlert("Lateral Controls Mismatch"), }, } _CABIN_BLOCK_EVENTS: EVENTS_IQ_TYPE = { EventNameIQ.brakeHoldSilent: { ET.WARNING: EngagementAlert(AudibleAlert.none), ET.NO_ENTRY: NoEntryAlert("Brake Hold Engaged"), }, EventNameIQ.gearNotDriveSilent: { ET.WARNING: Alert( "", "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), ET.NO_ENTRY: Alert( "", "", AlertStatus.normal, AlertSize.none, Priority.LOW, VisualAlert.none, AudibleAlert.none, 0.), }, EventNameIQ.parkBrakeSilent: { ET.WARNING: Alert( "", "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), ET.NO_ENTRY: NoEntryAlert("Parking Brake On"), }, EventNameIQ.doorAjarSilent: { ET.WARNING: Alert( "", "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), ET.NO_ENTRY: NoEntryAlert("Door Ajar"), }, EventNameIQ.seatbeltUnbuckledSilent: { ET.WARNING: Alert( "", "", AlertStatus.normal, AlertSize.none, Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), ET.NO_ENTRY: NoEntryAlert("Seatbelt Unbuckled"), }, EventNameIQ.reverseSilent: { ET.PERMANENT: Alert( "In\nReverse", "", AlertStatus.normal, AlertSize.full, Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), ET.NO_ENTRY: NoEntryAlert("In Reverse"), }, } _NOTICE_EVENTS: EVENTS_IQ_TYPE = { EventNameIQ.carModeMismatchNotice: { ET.WARNING: wrong_car_mode_alert, }, EventNameIQ.pedalHeldNotice: { ET.WARNING: NoEntryAlert("Brake Pedal Held") }, EventNameIQ.experimentalToggled: { ET.WARNING: NormalPermanentAlert("Switched to IQ.Pilot End to End Control", duration=1.5) }, EventNameIQ.e2eChime: { ET.PERMANENT: Alert( "", "", AlertStatus.normal, AlertSize.none, Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.), }, EventNameIQ.wideCamFaulty: { ET.PERMANENT: NormalPermanentAlert("Wide Cam Faulty", "IQ.Pilot still available, degraded via road cam only", priority=Priority.LOW), }, EventNameIQ.modelUpdating: { ET.NO_ENTRY: NoEntryAlert("Update finishes while parked with internet", alert_text_1="Driving Model Updating", priority=Priority.MID), }, } EVENTS_IQ: EVENTS_IQ_TYPE = {**_GUIDANCE_EVENTS, **_ENGAGE_EVENTS, **_CABIN_BLOCK_EVENTS, **_NOTICE_EVENTS}