mirror of
https://gitlvb.teallvbs.xyz/IQ.Lvbs/IQ.Pilot.git
synced 2026-07-25 05:22:11 +08:00
258 lines
12 KiB
Python
258 lines
12 KiB
Python
"""
|
|
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
|
"""
|
|
from datetime import datetime
|
|
|
|
import numpy as np
|
|
|
|
from cereal import messaging, custom
|
|
from iqdbc.car import structs
|
|
from openpilot.common.constants import CV
|
|
from openpilot.common.realtime import DT_MDL
|
|
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
|
|
from openpilot.iqpilot.selfdrive.controls.lib.custom_stop_distance import CustomStopDistance
|
|
from openpilot.iqpilot.selfdrive.controls.lib.iq_dynamic.engine import IQDynamicController
|
|
from openpilot.iqpilot.selfdrive.controls.lib.iq_dynamic.imahelper import IQConstants
|
|
from openpilot.iqpilot.selfdrive.controls.lib.helpers.e2e_alerts import E2EAlertsHelper
|
|
from openpilot.iqpilot.selfdrive.controls.lib.slc_vcruise import SLCVCruise
|
|
from openpilot.iqpilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_ADAPT_ACC
|
|
from openpilot.iqpilot.selfdrive.selfdrived.events import IQEvents
|
|
from openpilot.iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle
|
|
|
|
IQDynamicState = custom.IQPlan.IQDynamicControl.IQDynamicControlState
|
|
LongitudinalPlanSource = custom.IQPlan.LongitudinalPlanSource
|
|
SpeedLimitAssistState = custom.IQPlan.SpeedLimit.AssistState
|
|
SpeedLimitSource = custom.IQPlan.SpeedLimit.Source
|
|
NavProvider = custom.IQNavState.LongitudinalProvider
|
|
NavLongitudinalState = custom.IQNavState.LongitudinalState
|
|
|
|
class LongitudinalPlannerIQ:
|
|
def __init__(self, CP: structs.CarParams, CP_IQ: structs.IQCarParams, mpc):
|
|
self.events_iq = IQEvents()
|
|
self.iq_dynamic = IQDynamicController(CP, mpc)
|
|
self.custom_stop_distance = CustomStopDistance()
|
|
self.slimit = SLCVCruise()
|
|
self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None
|
|
self.source = LongitudinalPlanSource.cruise
|
|
self.iqmodeloutput = E2EAlertsHelper()
|
|
self.output_v_target = 0.
|
|
self.output_a_target = 0.
|
|
self.speed_limit_last = 0.
|
|
self.speed_limit_final_last = 0.
|
|
self.speed_limit_source = SpeedLimitSource.none
|
|
self.nav_engaged = False
|
|
self.nav_provider = NavProvider.none
|
|
self.nav_state = NavLongitudinalState.disabled
|
|
self.nav_speed_target = 0.
|
|
self.nav_accel_target = 0.
|
|
self.nav_valid = False
|
|
self.force_stop_timer = 0.0
|
|
self.forcing_stop = False
|
|
self.override_force_stop = False
|
|
self.override_force_stop_timer = 0.0
|
|
self.tracked_model_length = 0.0
|
|
|
|
def is_e2e(self, sm: messaging.SubMaster) -> bool:
|
|
experimental_mode = sm['selfdriveState'].experimentalMode
|
|
if not self.iq_dynamic.active():
|
|
return experimental_mode
|
|
|
|
return experimental_mode and self.iq_dynamic.mode() == "blended"
|
|
|
|
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
|
CS = sm['carState']
|
|
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
|
v_cruise_cluster = v_cruise_cluster_kph * CV.KPH_TO_MS
|
|
# SLC should apply whenever IQ.Pilot is engaged, even on stock-longitudinal cars
|
|
# where carControl.longActive stays false.
|
|
slc_apply_enabled = bool(getattr(sm['selfdriveState'], "enabled", False))
|
|
|
|
nav_state = sm['iqNavState']
|
|
self.nav_engaged = bool(getattr(nav_state, "longitudinalEngaged", False))
|
|
self.nav_provider = getattr(nav_state, "longitudinalProvider", NavProvider.none)
|
|
self.nav_state = getattr(nav_state, "longitudinalState", NavLongitudinalState.disabled)
|
|
self.nav_speed_target = float(getattr(nav_state, "speedTarget", 0.0))
|
|
self.nav_accel_target = float(getattr(nav_state, "accelTarget", 0.0))
|
|
self.nav_valid = bool(getattr(nav_state, "valid", False) and self.nav_engaged)
|
|
|
|
# IQ.Pilot custom Speed Limit Controller
|
|
now = datetime.now()
|
|
if hasattr(sm, "alive"):
|
|
time_validated = sm.alive.get('clocks', False) and getattr(sm['clocks'], 'timeValid', False)
|
|
else:
|
|
clocks = sm.get('clocks', None) if isinstance(sm, dict) else None
|
|
time_validated = bool(getattr(clocks, 'timeValid', False))
|
|
slc_v_cruise = self.slimit.update(slc_apply_enabled, now, time_validated, v_cruise, v_ego, sm)
|
|
self.iq_dynamic.set_slc_experimental_mode(self.slimit.slc_experimental_mode)
|
|
self.iq_dynamic.update(sm)
|
|
# Prefer confirmed controller output for UI/planner rendering.
|
|
# Fall back to active (policy-resolved) target/source when confirmed is unavailable.
|
|
display_speed_limit = self.slimit.slc_target if self.slimit.slc_target > 0 else self.slimit.slc_active_target
|
|
display_source = self.slimit.slc_source if self.slimit.slc_source != "None" else self.slimit.slc_active_source
|
|
|
|
if display_speed_limit > 0:
|
|
self.speed_limit_last = display_speed_limit
|
|
self.speed_limit_final_last = display_speed_limit + self.slimit.slc_offset
|
|
elif display_source == "None":
|
|
self.speed_limit_last = 0.0
|
|
self.speed_limit_final_last = 0.0
|
|
# Respect user-defined max cruise speed when applying SLC.
|
|
if v_cruise_cluster > 0 and self.speed_limit_final_last > 0:
|
|
self.speed_limit_final_last = min(self.speed_limit_final_last, v_cruise_cluster)
|
|
source_map = {
|
|
"Dashboard": SpeedLimitSource.car,
|
|
"Map Data": SpeedLimitSource.map,
|
|
"Mapbox": SpeedLimitSource.map,
|
|
"None": SpeedLimitSource.none,
|
|
}
|
|
self.speed_limit_source = source_map.get(display_source, SpeedLimitSource.none)
|
|
|
|
targets = {
|
|
LongitudinalPlanSource.cruise: (v_cruise, a_ego),
|
|
LongitudinalPlanSource.speedLimitAssist: (slc_v_cruise, a_ego),
|
|
}
|
|
if self.nav_valid:
|
|
targets[LongitudinalPlanSource.nav] = (self.nav_speed_target, self.nav_accel_target)
|
|
|
|
self.source = min(targets, key=lambda k: targets[k][0])
|
|
self.output_v_target, self.output_a_target = targets[self.source]
|
|
self.output_v_target = self._apply_force_stop(self.output_v_target, v_ego, sm, slc_apply_enabled)
|
|
# envelope shaping only in Assist mode: info/warn must never change the plan
|
|
self._envelope_enabled = (slc_apply_enabled and bool(getattr(self.slimit, "controller_enabled", False))
|
|
and bool(getattr(self.slimit, "mode_assist", False)))
|
|
return self.output_v_target, self.output_a_target
|
|
|
|
def cruise_envelope(self, v_target: float, v_ego: float, t_idxs) -> np.ndarray:
|
|
"""Per-timestep cruise speed over the MPC horizon: the scalar target, shaped down
|
|
ahead of an upcoming lower speed limit so the solver decelerates before the sign
|
|
instead of at it."""
|
|
env = np.full(len(t_idxs), max(float(v_target), 0.0))
|
|
if not getattr(self, "_envelope_enabled", False):
|
|
return env
|
|
slc = getattr(self.slimit, "slc", None)
|
|
next_limit = float(getattr(slc, "next_speed_limit", 0.0) or 0.0)
|
|
next_dist = float(getattr(slc, "next_speed_distance", 0.0) or 0.0)
|
|
if next_limit <= 0.0 or next_dist <= 0.0:
|
|
return env
|
|
next_target = max(next_limit + float(getattr(self.slimit, "slc_offset", 0.0) or 0.0), 0.0)
|
|
if next_target >= env[0]:
|
|
return env
|
|
travel = np.maximum(v_ego, 1.0) * np.asarray(t_idxs)
|
|
v_allowed = np.sqrt(np.maximum(next_target ** 2 + 2.0 * abs(LIMIT_ADAPT_ACC) * (next_dist - travel), next_target ** 2))
|
|
return np.minimum(env, v_allowed)
|
|
|
|
def update(self, sm: messaging.SubMaster) -> None:
|
|
self.events_iq.clear()
|
|
for event_name in getattr(self.slimit, 'pending_events', []):
|
|
self.events_iq.add(event_name)
|
|
self.custom_stop_distance.update()
|
|
self.iqmodeloutput.update(sm, self.events_iq)
|
|
if bool(getattr(sm["iqCarState"], "alcOverrideAlert", False)):
|
|
self.events_iq.add(custom.IQOnroadEvent.EventName.steeringOverrideReengageAlc)
|
|
|
|
def apply_e2e_stop_distance(self, sm: messaging.SubMaster, v_ego: float, a_target: float, should_stop: bool) -> tuple[float, bool]:
|
|
if not self.is_e2e(sm):
|
|
return a_target, should_stop
|
|
return self.custom_stop_distance.adjust_e2e_stop(a_target, should_stop, v_ego, sm['modelV2'])
|
|
|
|
def _apply_force_stop(self, v_target: float, v_ego: float, sm: messaging.SubMaster, apply_enabled: bool) -> float:
|
|
force_stop = self.iq_dynamic.force_stop_requested() and apply_enabled and self.override_force_stop_timer <= 0.0
|
|
self.force_stop_timer = self.force_stop_timer + DT_MDL if force_stop else 0.0
|
|
force_stop_enabled = self.force_stop_timer >= 1.0
|
|
force_stop_ramp_time = max(float(getattr(self.iq_dynamic, "model_stop_time", IQConstants.FORCE_STOP_PLANNER_TIME)), DT_MDL)
|
|
|
|
accel_pressed = bool(getattr(sm["iqCarState"], "accelPressed", False))
|
|
self.override_force_stop |= sm["carState"].gasPressed or accel_pressed
|
|
self.override_force_stop &= force_stop_enabled
|
|
|
|
if self.override_force_stop:
|
|
self.override_force_stop_timer = 10.0
|
|
elif self.override_force_stop_timer > 0.0:
|
|
self.override_force_stop_timer = max(0.0, self.override_force_stop_timer - DT_MDL)
|
|
else:
|
|
self.override_force_stop = False
|
|
|
|
if force_stop_enabled and not self.override_force_stop:
|
|
self.forcing_stop = True
|
|
self.tracked_model_length = max(self.tracked_model_length - (v_ego * DT_MDL), 0.0)
|
|
if sm["carState"].standstill:
|
|
return 0.0
|
|
return min(self.tracked_model_length / force_stop_ramp_time, v_target)
|
|
|
|
self.forcing_stop = False
|
|
self.tracked_model_length = max(
|
|
float(getattr(self.iq_dynamic, "model_length", 0.0)),
|
|
float(getattr(self.iq_dynamic, "minimum_force_stop_length", 0.0)),
|
|
0.0,
|
|
)
|
|
return v_target
|
|
|
|
def publish_longitudinal_plan_iq(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
|
def fill_plan(plan_msg) -> None:
|
|
plan_msg.longitudinalPlanSource = self.source
|
|
plan_msg.vTarget = float(self.output_v_target)
|
|
plan_msg.aTarget = float(self.output_a_target)
|
|
plan_msg.events = self.events_iq.to_msg()
|
|
|
|
# IQ.Dynamic control state
|
|
iq_dynamic = plan_msg.iqDynamic
|
|
iq_dynamic.state = IQDynamicState.blended if self.iq_dynamic.mode() == 'blended' else IQDynamicState.acc
|
|
iq_dynamic.enabled = self.iq_dynamic.enabled()
|
|
iq_dynamic.active = self.iq_dynamic.active()
|
|
|
|
nav_summary = plan_msg.iqNavState.nav
|
|
nav_summary.engaged = self.nav_engaged
|
|
nav_summary.provider = self.nav_provider
|
|
nav_summary.state = self.nav_state
|
|
nav_summary.speedTarget = float(self.nav_speed_target)
|
|
nav_summary.accelTarget = float(self.nav_accel_target)
|
|
nav_summary.valid = self.nav_valid
|
|
|
|
# Speed Limit
|
|
speedLimit = plan_msg.speedLimit
|
|
resolver = speedLimit.resolver
|
|
speed_limit = float(self.slimit.slc_target if self.slimit.slc_target > 0 else self.slimit.slc_active_target)
|
|
speed_limit_offset = float(self.slimit.slc_offset)
|
|
speed_limit_final = speed_limit + speed_limit_offset if speed_limit > 0 else 0.
|
|
speed_limit_valid = speed_limit > 0.
|
|
speed_limit_last_valid = self.speed_limit_last > 0.
|
|
|
|
resolver.speedLimit = speed_limit
|
|
resolver.speedLimitLast = float(self.speed_limit_last)
|
|
resolver.speedLimitFinal = float(speed_limit_final)
|
|
resolver.speedLimitFinalLast = float(self.speed_limit_final_last)
|
|
resolver.speedLimitValid = speed_limit_valid
|
|
resolver.speedLimitLastValid = speed_limit_last_valid
|
|
resolver.speedLimitOffset = speed_limit_offset
|
|
resolver.distToSpeedLimit = 0.
|
|
resolver.source = self.speed_limit_source
|
|
|
|
assist = speedLimit.assist
|
|
slc_assist_state = self.slimit.assist_state
|
|
assist.enabled = bool(self.slimit.slc_target > 0 or self.slimit.slc_unconfirmed > 0)
|
|
assist.active = self.source == LongitudinalPlanSource.speedLimitAssist and self.slimit.slc_target > 0
|
|
if slc_assist_state is not None:
|
|
assist.state = slc_assist_state
|
|
elif not assist.enabled:
|
|
assist.state = SpeedLimitAssistState.disabled
|
|
elif self.slimit.slc_unconfirmed > 0:
|
|
assist.state = SpeedLimitAssistState.preActive
|
|
elif assist.active:
|
|
assist.state = SpeedLimitAssistState.active
|
|
else:
|
|
assist.state = SpeedLimitAssistState.inactive
|
|
assist.vTarget = float(self.output_v_target if assist.active else 255.)
|
|
assist.aTarget = float(self.slimit.slc_a_target if assist.active else 0.)
|
|
|
|
# E2E Alerts
|
|
e2eAlerts = plan_msg.e2eAlerts
|
|
e2eAlerts.greenLightAlert = self.iqmodeloutput.queue_alert
|
|
e2eAlerts.leadDepartAlert = self.iqmodeloutput.lead_alert
|
|
|
|
valid = sm.all_checks(service_list=['carState', 'controlsState'])
|
|
|
|
plan_iq_send = messaging.new_message('iqPlan')
|
|
plan_iq_send.valid = valid
|
|
fill_plan(plan_iq_send.iqPlan)
|
|
pm.send('iqPlan', plan_iq_send)
|