IQ.Pilot Release Commit @ 4fcea4d

This commit is contained in:
IQ.Lvbs CI [bot]
2026-07-20 11:06:57 -05:00
commit 7b20edda67
4602 changed files with 1122468 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""

View File

@@ -0,0 +1,82 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from cereal import car
from openpilot.common.constants import CV
from openpilot.common.params import Params
class SignalPauseEngine:
_KEY_ON = "IQBlinkerPauseLateral"
_KEY_UNIT = "IsMetric"
_KEY_GATE = "IQBlinkerMinLateralSpeed"
def __init__(self):
self._kv = Params()
self._state = {"on": False, "metric": False, "gate": 0.0}
self.reload_setup()
@staticmethod
def _one_signal(cs: car.CarState) -> bool:
return bool(cs.leftBlinker) ^ bool(cs.rightBlinker)
@staticmethod
def _as_float(raw) -> float:
try:
return float(raw) if raw is not None else 0.0
except (TypeError, ValueError):
return 0.0
def _pull_setup(self) -> None:
self._state["on"] = self._kv.get_bool(self._KEY_ON)
self._state["metric"] = self._kv.get_bool(self._KEY_UNIT)
self._state["gate"] = self._as_float(self._kv.get(self._KEY_GATE))
def _gate_mps(self) -> float:
factor = CV.KPH_TO_MS if self._state["metric"] else CV.MPH_TO_MS
return self._state["gate"] * factor
def reload_setup(self) -> None:
self._pull_setup()
def heartbeat(self) -> None:
self._pull_setup()
def is_paused(self, cs: car.CarState) -> bool:
return bool(self._state["on"] and self._one_signal(cs) and cs.vEgo < self._gate_mps())
@property
def enabled(self):
return self._state["on"]
@enabled.setter
def enabled(self, value):
self._state["on"] = bool(value)
@property
def is_metric(self):
return self._state["metric"]
@is_metric.setter
def is_metric(self, value):
self._state["metric"] = bool(value)
@property
def min_speed(self):
return self._state["gate"]
@min_speed.setter
def min_speed(self, value):
self._state["gate"] = float(value)
class IQSignalPauseController(SignalPauseEngine):
def __init__(self):
super().__init__()
def get_params(self) -> None:
self.reload_setup()
def update(self, cs: car.CarState) -> bool:
return self.is_paused(cs)

View File

@@ -0,0 +1,48 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
from numpy import clip, interp
from openpilot.common.realtime import DT_MDL
from openpilot.iqpilot.selfdrive.iqmodeld.config import ModelConstants
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, MAX_LATERAL_JERK, MIN_SPEED
def _sanitize_plan(headings, curvatures):
valid_shape = len(headings) == CONTROL_N and len(curvatures) >= CONTROL_N
if valid_shape:
return headings, curvatures
placeholder = [0.0] * CONTROL_N
return placeholder, placeholder
def _project_future_heading(delay_s: float, headings) -> float:
return float(interp(delay_s, ModelConstants.T_IDXS[:CONTROL_N], headings))
def _convert_heading_to_curvature(projected_heading: float, speed_mps: float, current_curvature: float, delay_s: float) -> float:
turning_arc = projected_heading / (speed_mps * delay_s)
return (2.0 * turning_arc) - current_curvature
def _limit_curvature_rate(target_curvature: float, current_curvature: float, speed_mps: float) -> float:
curvature_step = MAX_LATERAL_JERK / (speed_mps ** 2)
lower = current_curvature - (curvature_step * DT_MDL)
upper = current_curvature + (curvature_step * DT_MDL)
return float(clip(target_curvature, lower, upper))
def solve_lag_curvature(steer_delay, v_ego, psis, curvatures):
headings, curvature_track = _sanitize_plan(psis, curvatures)
speed_mps = max(MIN_SPEED, v_ego)
delay_s = max(float(steer_delay), 1e-3)
current_curvature = float(curvature_track[0])
projected_heading = _project_future_heading(delay_s, headings)
target_curvature = _convert_heading_to_curvature(projected_heading, speed_mps, current_curvature, delay_s)
return _limit_curvature_rate(target_curvature, current_curvature, speed_mps)
get_lag_adjusted_curvature = solve_lag_curvature

View File

@@ -0,0 +1,155 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from cereal import messaging, custom
from openpilot.common.realtime import DT_MDL
from openpilot.iqpilot.selfdrive.selfdrived.events import IQEvents
PATH_QUEUE_GATE = 30
LEAD_GROWTH_GATE = 1.0
TRIGGER_HOLD_S = 0.3
class TriggerPhase:
IDLE = 0
ARMED = 1
FIRED = 2
class StopAndLeadChimeEngine:
def __init__(self):
self._frame = -1
self._toggle = {"path_queue": False, "lead_depart": False}
self._trigger = {
"path_queue": {"phase": TriggerPhase.IDLE, "last_phase": TriggerPhase.IDLE, "fired": False, "hold_ticks": 0, "anchor": -1.0},
"lead_depart": {"phase": TriggerPhase.IDLE, "last_phase": TriggerPhase.IDLE, "fired": False, "hold_ticks": 0, "anchor": -1.0},
}
self._standstill_gate = {"open": False, "was_open": False, "moving_frame": -1}
self._lead_state = {"visible": False, "primed": False, "armed": False, "warmup_ticks": 0}
def _read_toggles(self) -> None:
self._toggle["path_queue"] = False
self._toggle["lead_depart"] = False
def _mark_motion(self, standstill: bool, speed_mps: float) -> bool:
is_moving = (not standstill) and speed_mps > 0.1
if is_moving:
self._standstill_gate["moving_frame"] = self._frame
return is_moving
def _within_motion_grace(self) -> bool:
stamp = self._standstill_gate["moving_frame"]
if stamp == -1:
return True
return (self._frame - stamp) * DT_MDL < 2.0
def _update_gate(self, standstill: bool, speed_mps: float, gas_pressed: bool, controls_enabled: bool) -> None:
moving = self._mark_motion(standstill, speed_mps)
self._standstill_gate["open"] = (not moving) and (not gas_pressed) and (not controls_enabled) and (not self._within_motion_grace())
@staticmethod
def _path_end_x(path_x):
return float(path_x[-1]) if len(path_x) else 0.0
def _path_queue_signal(self, path_x) -> bool:
node = self._trigger["path_queue"]
if node["phase"] != TriggerPhase.ARMED:
node["hold_ticks"] = 0
return False
node["hold_ticks"] = node["hold_ticks"] + 1 if self._path_end_x(path_x) > PATH_QUEUE_GATE else 0
return node["hold_ticks"] * DT_MDL > TRIGGER_HOLD_S
def _update_lead_prime(self, seen: bool, distance_m: float) -> None:
near_lead = seen and distance_m < 8.0
opened_now = self._standstill_gate["open"] and not self._standstill_gate["was_open"]
if opened_now and near_lead:
self._lead_state["primed"] = True
elif not self._standstill_gate["open"]:
self._lead_state["primed"] = False
if self._standstill_gate["open"] and self._lead_state["primed"] and near_lead:
self._lead_state["warmup_ticks"] += 1
self._lead_state["armed"] = self._lead_state["warmup_ticks"] * DT_MDL >= 1.0
return
self._lead_state["warmup_ticks"] = 0
self._lead_state["armed"] = False
def _lead_depart_signal(self, distance_m: float) -> bool:
node = self._trigger["lead_depart"]
if node["phase"] != TriggerPhase.ARMED:
node["anchor"] = -1.0
node["hold_ticks"] = 0
return False
if node["anchor"] == -1.0 or distance_m < node["anchor"]:
node["anchor"] = distance_m
grew = node["anchor"] != -1.0 and (distance_m - node["anchor"]) > LEAD_GROWTH_GATE
node["hold_ticks"] = node["hold_ticks"] + 1 if grew else 0
return node["hold_ticks"] * DT_MDL > TRIGGER_HOLD_S
def _sample_inputs(self, sm: messaging.SubMaster):
cs = sm['carState']
cc = sm['carControl']
lead_one = sm['radarState'].leadOne
self._lead_state["visible"] = lead_one.status
lead_distance = lead_one.dRel
self._update_gate(cs.standstill, cs.vEgo, cs.gasPressed, cc.enabled)
queue_fire = self._path_queue_signal(sm['modelV2'].position.x)
self._update_lead_prime(self._lead_state["visible"], lead_distance)
lead_fire = self._lead_depart_signal(lead_distance)
self._standstill_gate["was_open"] = self._standstill_gate["open"]
return queue_fire, lead_fire
@staticmethod
def _step_phase(phase: int, enabled: bool, allowed: bool, fired: bool) -> tuple[int, bool]:
if phase == TriggerPhase.IDLE:
return (TriggerPhase.ARMED, fired) if (allowed and enabled) else (phase, fired)
if not allowed or not enabled:
return TriggerPhase.IDLE, fired
if phase == TriggerPhase.ARMED and fired:
return TriggerPhase.FIRED, fired
return phase, fired
def _advance_trigger(self, key: str, enabled: bool, allowed: bool, fired: bool) -> None:
node = self._trigger[key]
node["last_phase"] = node["phase"]
node["phase"], node["fired"] = self._step_phase(node["phase"], enabled, allowed, fired)
def update(self, sm: messaging.SubMaster, iq_events: IQEvents) -> None:
self._read_toggles()
queue_fire, lead_fire = self._sample_inputs(sm)
self._advance_trigger("path_queue", self._toggle["path_queue"], self._standstill_gate["open"] and not self._lead_state["visible"], queue_fire)
self._advance_trigger("lead_depart", self._toggle["lead_depart"], self._standstill_gate["open"] and self._lead_state["armed"], lead_fire)
if self._trigger["path_queue"]["fired"] or self._trigger["lead_depart"]["fired"]:
iq_events.add(custom.IQOnroadEvent.EventName.e2eChime)
self._frame += 1
@property
def queue_alert(self):
return self._trigger["path_queue"]["fired"]
@queue_alert.setter
def queue_alert(self, payload):
self._trigger["path_queue"]["fired"] = bool(payload)
@property
def lead_alert(self):
return self._trigger["lead_depart"]["fired"]
@lead_alert.setter
def lead_alert(self, payload):
self._trigger["lead_depart"]["fired"] = bool(payload)
E2EAlertsHelper = StopAndLeadChimeEngine

View File

@@ -0,0 +1,293 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from cereal import custom, log
from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
NAV_EXIT_COMMIT_DISTANCE = 500.0 # m before a route exit to begin moving into the exit lane
_ManeuverType = custom.IQNavState.ManeuverType
_NavDirection = custom.NavDirection
class LaneSwapPreset:
DISABLED = -1
STEERING_NUDGE = 0
DIRECT = 1
DELAY_HALF = 2
DELAY_ONE = 3
DELAY_TWO = 4
DELAY_THREE = 5
OFF = DISABLED
NUDGE = STEERING_NUDGE
NUDGELESS = DIRECT
HALF_SECOND = DELAY_HALF
ONE_SECOND = DELAY_ONE
TWO_SECONDS = DELAY_TWO
THREE_SECONDS = DELAY_THREE
PRESET_SECONDS = {
LaneSwapPreset.DISABLED: 0.0,
LaneSwapPreset.STEERING_NUDGE: 0.0,
LaneSwapPreset.DIRECT: 0.05,
LaneSwapPreset.DELAY_HALF: 0.5,
LaneSwapPreset.DELAY_ONE: 1.0,
LaneSwapPreset.DELAY_TWO: 2.0,
LaneSwapPreset.DELAY_THREE: 3.0,
}
LANE_SWAP_SECONDS = dict(PRESET_SECONDS)
BLINDSPOT_WAIT_OFFSET = -1
class LaneSwapEngine:
def __init__(self, desire_hub):
self._hub = desire_hub
self._kv = Params()
self._mem = {
"sec": 0.0,
"tick": 0,
"gate": 0.0,
"preset": self._kv.get("AutoLaneChangeTimer", return_default=True),
"bsm_hold": False,
"braked": False,
"ready": False,
"used": False,
}
self.reload_setup()
def _pull_setup(self) -> None:
self._mem["bsm_hold"] = self._kv.get_bool("AutoLaneChangeBsmDelay")
self._mem["preset"] = self._kv.get("AutoLaneChangeTimer", return_default=True)
def _idle_phase(self) -> bool:
return (
self._hub.lane_change_state == log.LaneChangeState.off and
self._hub.lane_change_direction == log.LaneChangeDirection.none
)
def _seconds_for_preset(self) -> float:
picked = self._mem["preset"]
return PRESET_SECONDS.get(picked, PRESET_SECONDS[LaneSwapPreset.STEERING_NUDGE])
def _auto_preset_active(self) -> bool:
picked = self._mem["preset"]
return picked not in (LaneSwapPreset.DISABLED, LaneSwapPreset.STEERING_NUDGE)
def _advance_clock(self, blindspot_now: bool) -> None:
wait_s = self._seconds_for_preset()
self._mem["gate"] = wait_s
self._mem["sec"] += DT_MDL
if self._mem["bsm_hold"] and blindspot_now and wait_s > 0.0:
if wait_s == PRESET_SECONDS[LaneSwapPreset.DIRECT]:
self._mem["sec"] = BLINDSPOT_WAIT_OFFSET
else:
self._mem["sec"] = wait_s + BLINDSPOT_WAIT_OFFSET
def _ready_to_fire(self) -> bool:
return (
self._auto_preset_active() and
(not self._mem["braked"]) and
(not self._mem["used"]) and
(self._mem["sec"] > self._mem["gate"])
)
def reload_setup(self) -> None:
self._pull_setup()
def heartbeat(self) -> None:
if (self._mem["tick"] % 50) == 0:
self._pull_setup()
self._mem["tick"] += 1
def sample(self, blindspot_now: bool = False, brake_now: bool = False, **legacy) -> None:
blindspot_now = bool(legacy.get("blindspot_detected", blindspot_now))
brake_now = bool(legacy.get("brake_pressed", brake_now))
self._mem["braked"] = self._mem["braked"] or brake_now
self._advance_clock(blindspot_now)
self._mem["ready"] = self._ready_to_fire()
def finalize(self) -> None:
started = self._hub.lane_change_state == log.LaneChangeState.laneChangeStarting
self._mem["used"] = self._mem["used"] or started
if self._idle_phase():
self._mem["sec"] = 0.0
self._mem["braked"] = False
self._mem["used"] = False
@property
def ready(self):
return self._mem["ready"]
@property
def delay(self):
return self._mem["gate"]
@property
def elapsed(self):
return self._mem["sec"]
@property
def preset(self):
return self._mem["preset"]
@preset.setter
def preset(self, value):
self._mem["preset"] = value
@property
def bsm_hold(self):
return self._mem["bsm_hold"]
@bsm_hold.setter
def bsm_hold(self, value):
self._mem["bsm_hold"] = bool(value)
@property
def braked(self):
return self._mem["braked"]
@braked.setter
def braked(self, value):
self._mem["braked"] = bool(value)
@property
def used(self):
return self._mem["used"]
@used.setter
def used(self, value):
self._mem["used"] = bool(value)
class NavExitLaneChangeController:
def __init__(self, enable_bsm: bool):
self._params = Params()
self._enable_bsm = bool(enable_bsm)
self.enabled = self._read_enabled()
self._tick = 0
self.active = False
self.direction = log.LaneChangeDirection.none
self.auto_allowed = False
def _read_enabled(self) -> bool:
try:
return self._params.get_bool("NavExitLaneChange")
except Exception:
return False
def update_params(self) -> None:
if self._tick % 50 == 0:
self.enabled = self._read_enabled()
self._tick += 1
@staticmethod
def _raw(value):
return getattr(value, "raw", value)
def update(self, nav_state, carstate) -> None:
self.active = False
self.direction = log.LaneChangeDirection.none
self.auto_allowed = False
if not self.enabled or nav_state is None or not getattr(nav_state, "active", False):
return
if not getattr(nav_state, "nextManeuverValid", False):
return
if self._raw(getattr(nav_state, "nextManeuverType", _ManeuverType.none)) != int(_ManeuverType.exit):
return
distance = float(getattr(nav_state, "nextManeuverDistance", 0.0))
if not 0.0 < distance <= NAV_EXIT_COMMIT_DISTANCE:
return
direction = self._raw(getattr(nav_state, "nextManeuverDirection", _NavDirection.none))
if direction == int(_NavDirection.left):
self.direction = log.LaneChangeDirection.left
elif direction == int(_NavDirection.right):
self.direction = log.LaneChangeDirection.right
else:
return
self.active = True
blindspot = carstate.leftBlindspot if self.direction == log.LaneChangeDirection.left else carstate.rightBlindspot
self.auto_allowed = (not blindspot) if self._enable_bsm else False
AutoLaneChangeMode = LaneSwapPreset
AUTO_LANE_CHANGE_TIMER = LANE_SWAP_SECONDS
ONE_SECOND_DELAY = BLINDSPOT_WAIT_OFFSET
class IQLaneSwapController(LaneSwapEngine):
def __init__(self, desire_helper):
super().__init__(desire_helper)
def reset(self) -> None:
self.finalize()
def update_params(self) -> None:
self.heartbeat()
def update_lane_change(self, blindspot_detected: bool, brake_pressed: bool) -> None:
self.sample(blindspot_now=blindspot_detected, brake_now=brake_pressed)
def update_state(self) -> None:
self.finalize()
@property
def lane_change_wait_timer(self):
return self.elapsed
@lane_change_wait_timer.setter
def lane_change_wait_timer(self, value):
self._mem["sec"] = float(value)
@property
def lane_change_delay(self):
return self.delay
@lane_change_delay.setter
def lane_change_delay(self, value):
self._mem["gate"] = float(value)
@property
def lane_change_set_timer(self):
return self.preset
@lane_change_set_timer.setter
def lane_change_set_timer(self, value):
self.preset = value
@property
def lane_change_bsm_delay(self):
return self.bsm_hold
@lane_change_bsm_delay.setter
def lane_change_bsm_delay(self, value):
self.bsm_hold = value
@property
def prev_brake_pressed(self):
return self.braked
@prev_brake_pressed.setter
def prev_brake_pressed(self, value):
self.braked = value
@property
def auto_lane_change_allowed(self):
return self.ready
@auto_lane_change_allowed.setter
def auto_lane_change_allowed(self, value):
self._mem["ready"] = bool(value)
@property
def prev_lane_change(self):
return self.used
@prev_lane_change.setter
def prev_lane_change(self, value):
self.used = value

View File

@@ -0,0 +1,157 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
from dataclasses import dataclass
from cereal import custom
from openpilot.common.constants import CV
from openpilot.common.params import Params
TurnDirection = custom.IQTurnSignalDirection
TURN_TRIGGER_MPS = 20 * CV.MPH_TO_MS
TURN_SPEED_GATE_MPS = TURN_TRIGGER_MPS
LANE_CHANGE_SPEED_MIN = TURN_SPEED_GATE_MPS
@dataclass
class _TurnGateState:
active: bool = False
speed_limit_mps: float = TURN_TRIGGER_MPS
outcome: int = TurnDirection.none
refresh_tick: int = 0
def _mph_param_to_mps(raw_value) -> float:
try:
return float(raw_value) * CV.MPH_TO_MS
except (TypeError, ValueError):
return TURN_TRIGGER_MPS
def _resolve_signal_choice(speed_mps: float,
speed_limit_mps: float,
left_signal: bool,
right_signal: bool,
left_blocked: bool,
right_blocked: bool) -> int:
if speed_mps >= speed_limit_mps:
return TurnDirection.none
if left_signal and not right_signal and not left_blocked:
return TurnDirection.turnLeft
if right_signal and not left_signal and not right_blocked:
return TurnDirection.turnRight
return TurnDirection.none
class TurnSignalPlanner:
_REFRESH_STRIDE = 50
def __init__(self, desire_hub):
self._desire_hub = desire_hub
self._params = Params()
self._state = _TurnGateState()
self.reload_setup()
def _refresh_from_params(self) -> None:
requested_gate = _mph_param_to_mps(self._params.get("IQLaneTurnValue", return_default=True))
self._state.active = self._params.get_bool("IQLaneTurnDesire")
self._state.speed_limit_mps = min(TURN_TRIGGER_MPS, requested_gate)
def _consume_legacy_kwargs(self, **legacy) -> tuple[bool, bool, bool, bool, float]:
return (
bool(legacy.get("blindspot_left", False)),
bool(legacy.get("blindspot_right", False)),
bool(legacy.get("left_blinker", False)),
bool(legacy.get("right_blinker", False)),
float(legacy.get("v_ego", 0.0)),
)
def reload_setup(self):
self._refresh_from_params()
def heartbeat(self) -> None:
if self._state.refresh_tick % self._REFRESH_STRIDE == 0:
self._refresh_from_params()
self._state.refresh_tick += 1
def sample(self,
blocked_l: bool = False,
blocked_r: bool = False,
blink_l: bool = False,
blink_r: bool = False,
speed_mps: float = 0.0,
**legacy) -> None:
if legacy:
blocked_l, blocked_r, blink_l, blink_r, speed_mps = self._consume_legacy_kwargs(**legacy)
self._state.outcome = _resolve_signal_choice(speed_mps,
self._state.speed_limit_mps,
blink_l,
blink_r,
blocked_l,
blocked_r)
def output(self):
return self._state.outcome if self._state.active else TurnDirection.none
@property
def enabled(self):
return self._state.active
@enabled.setter
def enabled(self, value):
self._state.active = bool(value)
@property
def speed_gate(self):
return self._state.speed_limit_mps
@speed_gate.setter
def speed_gate(self, value):
self._state.speed_limit_mps = float(value)
@property
def turn_direction(self):
return self._state.outcome
@turn_direction.setter
def turn_direction(self, value):
self._state.outcome = value
class IQNavTurnController(TurnSignalPlanner):
def __init__(self, desire_helper):
super().__init__(desire_helper)
def read_params(self):
self.reload_setup()
def update_params(self) -> None:
self.heartbeat()
def update_lane_turn(self,
blindspot_left: bool,
blindspot_right: bool,
left_blinker: bool,
right_blinker: bool,
v_ego: float) -> None:
self.sample(blocked_l=blindspot_left,
blocked_r=blindspot_right,
blink_l=left_blinker,
blink_r=right_blinker,
speed_mps=v_ego)
def get_turn_direction(self):
return self.output()
@property
def lane_turn_value(self):
return self.speed_gate
@lane_turn_value.setter
def lane_turn_value(self, value):
self.speed_gate = value

View File

@@ -0,0 +1,83 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
Short, decaying steering-torque nudges that lean the car through navigation
turns and highway exits. This is a lateral-control add-on driven by iqNavState;
it is independent of the feed-forward model and is off by default.
"""
import numpy as np
import cereal.messaging as messaging
from cereal import custom
TURN_NUDGE_TORQUE = 0.8
EXIT_NUDGE_TORQUE = 0.6
TURN_PULSE_FRAMES = 50
EXIT_PULSE_FRAMES = 75
# Master switch — nav torque influence is experimental and shipped off.
IQP_NAV_TORQUE_INFLUENCE_ENABLED = False
_LEFT = 1 # turnDesireDirection / lanePositioningDirection: 1 == left
class NavTorquePulseBrain:
def __init__(self, lac_torque):
self._controller = lac_torque
self._nav_sm = messaging.SubMaster(["iqNavState"], poll="iqNavState")
self._nav_key = ""
self._nav_pulse_sign = 0.0
self._nav_pulse_frames = 0
def _lookup_nav_pulse(self):
if not IQP_NAV_TORQUE_INFLUENCE_ENABLED:
return "", 0.0, 0
self._nav_sm.update(0)
nav_state = self._nav_sm["iqNavState"]
phase = getattr(nav_state, "maneuverPhase", custom.IQNavState.ManeuverPhase.none)
maneuver_direction = getattr(nav_state, "maneuverDirection", custom.NavDirection.none)
# left nudges negative, otherwise positive
def turn(tag, direction):
return f"turn{tag}:{direction}", -TURN_NUDGE_TORQUE if direction == _LEFT else TURN_NUDGE_TORQUE, TURN_PULSE_FRAMES
def keep(tag, direction):
return f"{tag}:{direction}", -EXIT_NUDGE_TORQUE if direction == _LEFT else EXIT_NUDGE_TORQUE, EXIT_PULSE_FRAMES
if phase == custom.IQNavState.ManeuverPhase.turnActive:
return turn("-phase", getattr(nav_state, "turnDesireDirection", 0))
if phase == custom.IQNavState.ManeuverPhase.highwayCommit and maneuver_direction in (custom.NavDirection.left, custom.NavDirection.right):
return keep("highway-phase", getattr(nav_state, "lanePositioningDirection", 0))
if getattr(nav_state, "shouldSendTurnDesire", False):
return turn("", getattr(nav_state, "turnDesireDirection", 0))
if getattr(nav_state, "shouldSendLanePositioning", False):
return keep("keep", getattr(nav_state, "lanePositioningDirection", 0))
return "", 0.0, 0
def nudge_output_torque(self, active: bool, car_state, output_torque: float) -> float:
if not IQP_NAV_TORQUE_INFLUENCE_ENABLED:
self._nav_pulse_frames = 0
self._nav_key = ""
return output_torque
nav_key, pulse_sign, pulse_frames = self._lookup_nav_pulse()
if not active or getattr(car_state, "steeringPressed", False):
self._nav_pulse_frames = 0
if not nav_key:
self._nav_key = ""
return output_torque
if nav_key and nav_key != self._nav_key:
self._nav_key = nav_key
self._nav_pulse_sign = pulse_sign
self._nav_pulse_frames = pulse_frames
elif not nav_key and self._nav_pulse_frames == 0:
self._nav_key = ""
if self._nav_pulse_frames > 0:
self._nav_pulse_frames -= 1
steer_max = float(getattr(self._controller, "steer_max", 1.0))
output_torque = float(np.clip(output_torque + self._nav_pulse_sign, -steer_max, steer_max))
return output_torque

View File

@@ -0,0 +1,3 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""

View File

@@ -0,0 +1,105 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from types import SimpleNamespace
import numpy as np
import pytest
from cereal import custom
import openpilot.iqpilot.selfdrive.controls.lib.helpers.nav_torque_pulse as nav_pulse
from openpilot.iqpilot.selfdrive.controls.lib.helpers.nav_torque_pulse import (
NavTorquePulseBrain, TURN_PULSE_FRAMES, EXIT_PULSE_FRAMES)
@pytest.fixture
def influence_on():
nav_pulse.IQP_NAV_TORQUE_INFLUENCE_ENABLED = True
try:
yield
finally:
nav_pulse.IQP_NAV_TORQUE_INFLUENCE_ENABLED = False
def _fixed_nav_sm(**fields):
nav = SimpleNamespace(
maneuverPhase=custom.IQNavState.ManeuverPhase.none,
maneuverDirection=custom.NavDirection.none,
shouldSendTurnDesire=False,
turnDesireDirection=0,
shouldSendLanePositioning=False,
lanePositioningDirection=0,
)
for k, v in fields.items():
setattr(nav, k, v)
class SM:
def update(self, _):
return None
def __getitem__(self, _):
return nav
return SM()
def _brain(nav_sm=None, steer_max=1.0):
brain = NavTorquePulseBrain(SimpleNamespace(steer_max=steer_max))
if nav_sm is not None:
brain._nav_sm = nav_sm
return brain
def test_passthrough_when_disabled():
brain = _brain()
cs = SimpleNamespace(steeringPressed=False)
assert brain.nudge_output_torque(True, cs, 0.42) == 0.42
class TestPulseSign:
@pytest.mark.parametrize("direction,expect_negative", [(1, True), (2, False)])
def test_turn_desire_direction(self, influence_on, direction, expect_negative):
brain = _brain(_fixed_nav_sm(shouldSendTurnDesire=True, turnDesireDirection=direction))
cs = SimpleNamespace(steeringPressed=False)
first = brain.nudge_output_torque(True, cs, 0.0)
assert (first < 0.0) == expect_negative
def test_turn_active_phase_uses_turn_frames(self, influence_on):
brain = _brain(_fixed_nav_sm(maneuverPhase=custom.IQNavState.ManeuverPhase.turnActive,
turnDesireDirection=1))
cs = SimpleNamespace(steeringPressed=False)
outs = [brain.nudge_output_torque(True, cs, 0.0) for _ in range(TURN_PULSE_FRAMES + 2)]
assert outs[TURN_PULSE_FRAMES - 1] < 0.0
assert outs[TURN_PULSE_FRAMES] == 0.0
class TestPulseLifecycle:
def test_pulse_expires_after_its_frame_count(self, influence_on):
brain = _brain(_fixed_nav_sm(shouldSendTurnDesire=True, turnDesireDirection=1))
cs = SimpleNamespace(steeringPressed=False)
outs = [brain.nudge_output_torque(True, cs, 0.0) for _ in range(TURN_PULSE_FRAMES + 3)]
assert all(o < 0.0 for o in outs[:TURN_PULSE_FRAMES])
assert all(o == 0.0 for o in outs[TURN_PULSE_FRAMES:])
assert all(np.isfinite(o) for o in outs)
def test_steering_press_suppresses_pulse(self, influence_on):
brain = _brain(_fixed_nav_sm(shouldSendTurnDesire=True, turnDesireDirection=1))
cs = SimpleNamespace(steeringPressed=True)
assert brain.nudge_output_torque(True, cs, 0.4) == 0.4
def test_inactive_suppresses_pulse(self, influence_on):
brain = _brain(_fixed_nav_sm(shouldSendTurnDesire=True, turnDesireDirection=1))
cs = SimpleNamespace(steeringPressed=False)
assert brain.nudge_output_torque(False, cs, 0.4) == 0.4
def test_output_clamped_to_steer_max(self, influence_on):
brain = _brain(_fixed_nav_sm(shouldSendTurnDesire=True, turnDesireDirection=2), steer_max=0.5)
cs = SimpleNamespace(steeringPressed=False)
out = brain.nudge_output_torque(True, cs, 0.4) # 0.4 + 0.8 nudge, clamped to 0.5
assert out == pytest.approx(0.5)
def test_lane_positioning_uses_exit_frames(self, influence_on):
brain = _brain(_fixed_nav_sm(shouldSendLanePositioning=True, lanePositioningDirection=1))
cs = SimpleNamespace(steeringPressed=False)
outs = [brain.nudge_output_torque(True, cs, 0.0) for _ in range(EXIT_PULSE_FRAMES + 2)]
assert outs[EXIT_PULSE_FRAMES - 1] < 0.0
assert outs[EXIT_PULSE_FRAMES] == 0.0