CEM / NO CAMERA / UI / REDNECK

This commit is contained in:
firestar5683
2026-04-22 00:15:36 -05:00
parent 4362a2bce4
commit 920c6832d3
8 changed files with 308 additions and 93 deletions
+18 -3
View File
@@ -5,7 +5,10 @@ from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, DT_CTRL, create_gas_in
from opendbc.car.lateral import apply_driver_steer_torque_limits
from opendbc.car.gm import gmcan
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.gm.values import ASCM_INT, CAR, CC_ONLY_CAR, CC_REGEN_PADDLE_CAR, DBC, EV_CAR, SDGM_CAR, AccState, CanBus, CarControllerParams, CruiseButtons, GMFlags
from opendbc.car.gm.values import (
ASCM_INT, CAR, CC_ONLY_CAR, CC_REGEN_PADDLE_CAR, DBC, EV_CAR, SDGM_CAR, AccState, CanBus, CarControllerParams,
CruiseButtons, GMFlags,
)
from opendbc.car.interfaces import CarControllerBase
from openpilot.common.params import Params, UnknownKeyName
from openpilot.starpilot.common.testing_grounds import testing_ground
@@ -66,6 +69,14 @@ def should_spoof_ecm_cruise_status(CP):
)
def should_send_cc_button_spam(CP, CC, CS):
return (
bool(CP.flags & GMFlags.CC_LONG.value) and
CC.longActive and
CS.out.vEgo > CP.minEnableSpeed
)
def get_testing_ground_1_brake_switch_bias(v_ego: float) -> int:
return int(round(np.interp(v_ego, [0.0, 6.0, 15.0, 30.0], [40.0, 85.0, 130.0, 170.0])))
@@ -478,7 +489,11 @@ class CarController(CarControllerBase):
interceptor_gas_cmd, press_regen_paddle = self.calc_pedal_command(actuators.accel, CC.longActive, CS.out.vEgo)
maneuver_sng_launch = self.longitudinal_maneuver_mode and self.is_volt
if self.CP.enableGasInterceptorDEPRECATED and self.apply_gas > self.params.INACTIVE_REGEN and use_interceptor_sng_launch(self.CP, CS, maneuver_sng_launch):
if (
self.CP.enableGasInterceptorDEPRECATED and
self.apply_gas > self.params.INACTIVE_REGEN and
use_interceptor_sng_launch(self.CP, CS, maneuver_sng_launch)
):
interceptor_gas_cmd = self.params.SNG_INTERCEPTOR_GAS
if maneuver_sng_launch:
interceptor_gas_cmd = max(interceptor_gas_cmd, float(np.interp(actuators.accel, [0.0, 1.0, 2.0], [self.params.SNG_INTERCEPTOR_GAS, 0.11, 0.16])))
@@ -501,7 +516,7 @@ class CarController(CarControllerBase):
can_sends.append(gmcan.create_regen_paddle_command(self.packer_pt, CanBus.POWERTRAIN, paddle_spoof_pressed))
if self.CP.flags & GMFlags.CC_LONG.value:
if CC.longActive and CS.out.cruiseState.enabled and CS.out.vEgo > self.CP.minEnableSpeed:
if should_send_cc_button_spam(self.CP, CC, CS):
# Using extend instead of append since the message is only sent intermittently
can_sends.extend(gmcan.create_gm_cc_spam_command(self.packer_pt, self, CS, actuators, starpilot_toggles))
elif (CS.out.cruiseState.enabled and CC.enabled and self.frame % 52 == 0 and
+8 -2
View File
@@ -1,7 +1,7 @@
from opendbc.car import DT_CTRL
from opendbc.car.can_definitions import CanData
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.gm.values import CAR, CanBus, CruiseButtons
from opendbc.car.gm.values import CAR, CanBus, CruiseButtons, GMFlags
MALIBU_BUTTON_TABLE = {
0: [0x10FF, 0x15EE, 0x1ADD, 0x1FCC],
@@ -314,7 +314,13 @@ def create_gm_cc_spam_command(packer, controller, CS, actuators, starpilot_toggl
controller.malibu_button_phase = (controller.malibu_button_phase + 1) % 4
return msgs
idx = (CS.buttons_counter + 1) % 4 # Need to predict the next idx for '22-23 EUV
return [create_buttons(packer, CanBus.POWERTRAIN, idx, cruise_btn)]
msgs = [create_buttons(packer, CanBus.POWERTRAIN, idx, cruise_btn)]
# Flashed camera-forward Volt CC installs also need the button spoof on the
# camera side. Removed-camera installs set NO_CAMERA and keep this PT-only.
if CS.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC and not (CS.CP.flags & GMFlags.NO_CAMERA.value):
msgs.append(create_buttons(packer, CanBus.CAMERA, idx, cruise_btn))
return msgs
else:
return []
+3 -1
View File
@@ -612,7 +612,9 @@ class CarInterface(CarInterfaceBase):
ret.flags |= GMFlags.FORCE_BRAKE_C9.value
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_FORCE_BRAKE_C9.value
if (ret.networkLocation == NetworkLocation.fwdCamera or candidate in CC_ONLY_CAR) and CAM_MSG not in fingerprint[CanBus.CAMERA] and candidate not in SDGM_CAR:
# Exception for flashed cars, or cars whose camera was removed.
missing_camera_msg = CAM_MSG not in fingerprint.get(CanBus.CAMERA, {})
if (ret.networkLocation == NetworkLocation.fwdCamera or candidate in CC_ONLY_CAR) and missing_camera_msg and candidate not in SDGM_CAR:
ret.flags |= GMFlags.NO_CAMERA.value
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_NO_CAMERA.value
+95 -2
View File
@@ -2,12 +2,15 @@ import pytest
from types import SimpleNamespace
from parameterized import parameterized
from opendbc.can import CANPacker
from opendbc.car import Bus, DT_CTRL
from opendbc.car.car_helpers import interfaces
from opendbc.car.gm.carcontroller import should_spoof_dash_speed
from opendbc.car.gm import gmcan
from opendbc.car.gm.carcontroller import should_send_cc_button_spam, should_spoof_dash_speed
import opendbc.car.gm.interface as gm_interface
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.gm.fingerprints import FINGERPRINTS
from opendbc.car.gm.values import CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, GM_RX_OFFSET
from opendbc.car.gm.values import CAMERA_ACC_CAR, CAR, CC_ONLY_CAR, DBC, GM_RX_OFFSET, GMFlags, GMSafetyFlags
CAMERA_DIAGNOSTIC_ADDRESS = 0x24b
VOLT_CARS = (
@@ -78,6 +81,19 @@ class TestGMInterface:
assert car_params.startingState
assert car_params.startAccel == pytest.approx(1.15)
def test_volt_cc_sparse_fingerprint_without_camera_sets_no_camera(self):
CarInterface = interfaces[CAR.CHEVROLET_VOLT_CC]
fingerprint = {
0: FINGERPRINTS[CAR.CHEVROLET_VOLT][0].copy(),
1: {},
}
car_params = CarInterface.get_params(CAR.CHEVROLET_VOLT_CC, fingerprint, [], alpha_long=False, is_release=False, docs=False,
starpilot_toggles=_test_starpilot_toggles())
assert car_params.flags & GMFlags.NO_CAMERA.value
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_NO_CAMERA.value
class TestGMCarController:
def test_dash_speed_spoof_respects_live_stock_acc_toggles(self):
@@ -91,3 +107,80 @@ class TestGMCarController:
cp = SimpleNamespace(openpilotLongitudinalControl=True, enableGasInterceptorDEPRECATED=False)
assert should_spoof_dash_speed(cp, SimpleNamespace(disable_openpilot_long=False))
def test_cc_button_spam_does_not_require_stock_cruise_enabled(self):
cp = SimpleNamespace(flags=GMFlags.CC_LONG.value, minEnableSpeed=10.0)
cc = SimpleNamespace(longActive=True)
cs = SimpleNamespace(out=SimpleNamespace(vEgo=11.0, cruiseState=SimpleNamespace(enabled=False)))
assert should_send_cc_button_spam(cp, cc, cs)
def test_cc_button_spam_requires_cc_long_and_speed(self):
cc = SimpleNamespace(longActive=True)
cs = SimpleNamespace(out=SimpleNamespace(vEgo=9.0, cruiseState=SimpleNamespace(enabled=True)))
assert not should_send_cc_button_spam(SimpleNamespace(flags=GMFlags.CC_LONG.value, minEnableSpeed=10.0), cc, cs)
assert not should_send_cc_button_spam(SimpleNamespace(flags=0, minEnableSpeed=10.0), cc, cs)
def test_volt_cc_redneck_spam_is_mirrored_to_camera_bus(self):
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
controller = SimpleNamespace(frame=int(0.3 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
cs = SimpleNamespace(
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT_CC,
flags=0,
minEnableSpeed=24 * CV.MPH_TO_MS,
),
buttons_counter=2,
out=SimpleNamespace(
vEgo=25.0,
cruiseState=SimpleNamespace(speed=20.0),
),
)
actuators = SimpleNamespace(accel=1.0)
msgs = gmcan.create_gm_cc_spam_command(packer, controller, cs, actuators, SimpleNamespace(is_metric=False))
assert [msg[2] for msg in msgs] == [0, 2]
def test_volt_cc_no_camera_redneck_spam_stays_on_powertrain_bus(self):
packer = CANPacker(DBC[CAR.CHEVROLET_VOLT_CC][Bus.pt])
controller = SimpleNamespace(frame=int(0.3 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
cs = SimpleNamespace(
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT_CC,
flags=GMFlags.NO_CAMERA.value,
minEnableSpeed=24 * CV.MPH_TO_MS,
),
buttons_counter=2,
out=SimpleNamespace(
vEgo=25.0,
cruiseState=SimpleNamespace(speed=20.0),
),
)
actuators = SimpleNamespace(accel=1.0)
msgs = gmcan.create_gm_cc_spam_command(packer, controller, cs, actuators, SimpleNamespace(is_metric=False))
assert [msg[2] for msg in msgs] == [0]
def test_non_volt_cc_redneck_spam_stays_on_powertrain_bus(self):
packer = CANPacker(DBC[CAR.CHEVROLET_BOLT_CC_2018_2021][Bus.pt])
controller = SimpleNamespace(frame=int(0.3 / DT_CTRL), last_button_frame=0, apply_speed=0, malibu_button_phase=0)
cs = SimpleNamespace(
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_BOLT_CC_2018_2021,
flags=GMFlags.CC_LONG.value,
minEnableSpeed=24 * CV.MPH_TO_MS,
),
buttons_counter=2,
out=SimpleNamespace(
vEgo=25.0,
cruiseState=SimpleNamespace(speed=20.0),
),
)
actuators = SimpleNamespace(accel=1.0)
msgs = gmcan.create_gm_cc_spam_command(packer, controller, cs, actuators, SimpleNamespace(is_metric=False))
assert [msg[2] for msg in msgs] == [0]
@@ -7,13 +7,15 @@ import openpilot.starpilot.controls.starpilot_planner as starpilot_planner_modul
from openpilot.starpilot.controls.lib.conditional_experimental_mode import ConditionalExperimentalMode
def make_cem(*, model_length: float, model_stopped: bool = False, tracking_lead: bool = False):
def make_cem(*, model_length: float, model_stopped: bool = False, tracking_lead: bool = False,
lead_status: bool = False, lead_d_rel: float = float("inf")):
planner = SimpleNamespace(
params=None,
params_memory=None,
model_length=model_length,
model_stopped=model_stopped,
tracking_lead=tracking_lead,
lead_one=SimpleNamespace(status=lead_status, dRel=lead_d_rel),
)
return ConditionalExperimentalMode(planner)
@@ -24,6 +26,12 @@ def make_sm(traffic_mode_enabled: bool = False):
}
def run_stop_light_detector(cem, v_ego, *, steps: int, tracking_lead: bool = False):
for _ in range(steps):
cem.starpilot_planner.tracking_lead = tracking_lead
cem.stop_sign_and_light(v_ego, make_sm(), model_time=7.0)
def test_low_speed_cruise_does_not_trigger_stop_light_from_model_stopped():
v_ego = 10 * CV.MPH_TO_MS
model_length = v_ego * 10.0
@@ -39,7 +47,41 @@ def test_predicted_stop_within_threshold_triggers_stop_light():
model_length = v_ego * 4.0
cem = make_cem(model_length=model_length)
cem.stop_sign_and_light(v_ego, make_sm(), model_time=7.0)
run_stop_light_detector(cem, v_ego, steps=20)
assert cem.stop_light_detected
def test_chattering_lead_does_not_trigger_stop_light():
v_ego = 22 * CV.MPH_TO_MS
model_length = v_ego * 4.0
cem = make_cem(model_length=model_length)
for i in range(30):
cem.starpilot_planner.tracking_lead = (i % 2 == 0)
cem.starpilot_planner.lead_one.status = (i % 2 == 0)
cem.starpilot_planner.lead_one.dRel = model_length + 5.0
cem.stop_sign_and_light(v_ego, make_sm(), model_time=7.0)
assert not cem.stop_light_detected
def test_close_visible_but_untracked_lead_blocks_stop_light():
v_ego = 22 * CV.MPH_TO_MS
model_length = v_ego * 4.0
cem = make_cem(model_length=model_length, lead_status=True, lead_d_rel=model_length + 5.0)
run_stop_light_detector(cem, v_ego, steps=30)
assert not cem.stop_light_detected
def test_far_visible_lead_does_not_block_stop_light():
v_ego = 22 * CV.MPH_TO_MS
model_length = v_ego * 4.0
cem = make_cem(model_length=model_length, lead_status=True, lead_d_rel=v_ego * 7.0 + 30.0)
run_stop_light_detector(cem, v_ego, steps=30)
assert cem.stop_light_detected
@@ -110,6 +152,7 @@ def test_starpilot_planner_updates_cem_with_current_frame_state(monkeypatch):
planner.gps_location_service: SimpleNamespace(latitude=1.0, longitude=1.0, bearingDeg=90.0),
}
planner.tracking_lead_filter.x = 1.0
planner.update(0.0, False, sm, starpilot_toggles)
assert seen == {
+60 -41
View File
@@ -21,9 +21,10 @@ MIN_DRAW_DISTANCE = 10.0
MAX_DRAW_DISTANCE = 100.0
RAINBOW_GRADIENT_COLOR_COUNT = 19
RAINBOW_SCROLL_SPEED_DEG_PER_SEC = 60.0
ACCEL_PATH_MIN_LIGHTNESS = 0.78
ACCEL_PATH_MIN_SATURATION = 0.50
STOCK_LANE_LINES_COLOR = rl.Color(255, 255, 255, 255)
DEFAULT_LANE_LINES_WIDTH = 4.0
DEFAULT_PATH_WIDTH = 6.1
DEFAULT_ROAD_EDGES_WIDTH = 2.0
LANE_LINE_COLORS = {
UIStatus.DISENGAGED: rl.Color(200, 200, 200, 255),
UIStatus.OVERRIDE: rl.Color(255, 255, 255, 255),
@@ -199,14 +200,13 @@ class ModelRenderer(Widget):
def _update_model(self, lead, path_x_array):
"""Update model visualization data based on model message"""
model_ui_enabled = self._params.get_bool("ModelUI", default=True)
if model_ui_enabled:
path_width = self._path_width_to_half_m(self._params.get_float("PathWidth", return_default=True, default=6.1))
lane_line_width = self._small_distance_to_half_m(self._params.get_float("LaneLinesWidth", return_default=True, default=4.0))
road_edge_width = self._small_distance_to_half_m(self._params.get_float("RoadEdgesWidth", return_default=True, default=2.0))
else:
path_width = 0.9
lane_line_width = 0.025
road_edge_width = 0.025
custom_path_width = model_ui_enabled and self._param_float_changed("PathWidth", DEFAULT_PATH_WIDTH)
custom_lane_line_width = model_ui_enabled and self._param_float_changed("LaneLinesWidth", DEFAULT_LANE_LINES_WIDTH)
custom_road_edge_width = model_ui_enabled and self._param_float_changed("RoadEdgesWidth", DEFAULT_ROAD_EDGES_WIDTH)
path_width = self._path_width_to_half_m(self._params.get_float("PathWidth", default=DEFAULT_PATH_WIDTH)) if custom_path_width else 0.9
lane_line_width = self._small_distance_to_half_m(self._params.get_float("LaneLinesWidth", default=DEFAULT_LANE_LINES_WIDTH)) if custom_lane_line_width else None
road_edge_width = self._small_distance_to_half_m(self._params.get_float("RoadEdgesWidth", default=DEFAULT_ROAD_EDGES_WIDTH)) if custom_road_edge_width else None
if model_ui_enabled and self._params.get_bool("DynamicPathWidth", default=False):
if ui_state.status == UIStatus.ENGAGED:
@@ -220,14 +220,19 @@ class ModelRenderer(Widget):
max_idx = self._get_path_length_idx(self._lane_lines[0].raw_points[:, 0], max_distance)
# Update lane lines using raw points
line_width_factor = 0.12
for i, lane_line in enumerate(self._lane_lines):
if i in (1, 2):
line_width_factor = 0.16
line_width = lane_line_width if lane_line_width is not None else line_width_factor
lane_line.projected_points = self._map_line_to_polygon(
lane_line.raw_points, lane_line_width * self._lane_line_probs[i], 0.0, max_idx
lane_line.raw_points, line_width * self._lane_line_probs[i], 0.0, max_idx
)
# Update road edges using raw points
edge_width = road_edge_width if road_edge_width is not None else line_width_factor
for road_edge in self._road_edges:
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, road_edge_width, 0.0, max_idx)
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, edge_width, 0.0, max_idx)
# Update path using raw points
if lead and lead.status:
@@ -255,17 +260,15 @@ class ModelRenderer(Widget):
def _update_experimental_gradient(self):
"""Pre-calculate experimental mode gradient colors"""
use_rainbow = self._params.get_bool("RainbowPath", default=False)
use_acceleration = not use_rainbow and (self._experimental_mode or self._params.get_bool("AccelerationPath", default=True))
if not (use_rainbow or use_acceleration):
return
max_len = min(len(self._path.projected_points) // 2, len(self._acceleration_x))
if use_rainbow:
gradient_bottom, gradient_top = self._get_visible_gradient_bounds()
self._exp_gradient = self._build_rainbow_gradient(gradient_bottom, gradient_top)
return
if not self._experimental_mode or not self._params.get_bool("AccelerationPath", default=True):
return
max_len = min(len(self._path.projected_points) // 2, len(self._acceleration_x))
segment_colors = []
gradient_stops = []
@@ -282,9 +285,9 @@ class ModelRenderer(Widget):
# speed up: 120, slow down: 0
path_hue = np.clip(60 + self._acceleration_x[i] * 35, 0, 120)
accel_magnitude = np.clip(abs(self._acceleration_x[i]) * 1.5, 0.0, 1.0)
saturation = np.interp(accel_magnitude, [0.0, 1.0], [ACCEL_PATH_MIN_SATURATION, 1.0])
lightness = np.interp(accel_magnitude, [0.0, 1.0], [ACCEL_PATH_MIN_LIGHTNESS, 0.62])
saturation = min(abs(self._acceleration_x[i] * 1.5), 1)
lightness = np.interp(saturation, [0.0, 1.0], [0.95, 0.62])
alpha = np.interp(lin_grad_point, [0.75 / 2.0, 0.75], [0.4, 0.0])
# Use HSL to RGB conversion
@@ -361,12 +364,13 @@ class ModelRenderer(Widget):
def _get_ll_color(self, prob: float, adjacent: bool, left: bool):
alpha = np.clip(prob, 0.0, 0.7)
stock_scheme = is_stock_color_scheme(self._params)
line_status = UIStatus.ENGAGED if ui_state.status == UIStatus.DISENGAGED and ui_state.always_on_lateral_active else ui_state.status
if adjacent:
override = get_param_color(self._params, "PathEdgesColor", 255)
if override is not None:
color = with_alpha(override, int(alpha * override.a))
elif stock_scheme:
base_color = LANE_LINE_COLORS.get(ui_state.status, LANE_LINE_COLORS[UIStatus.DISENGAGED])
base_color = LANE_LINE_COLORS.get(line_status, LANE_LINE_COLORS[UIStatus.DISENGAGED])
color = rl.Color(base_color.r, base_color.g, base_color.b, int(alpha * 255))
else:
base_color = get_theme_color("PathEdge", rl.Color(0, 255, 64, 255))
@@ -391,7 +395,7 @@ class ModelRenderer(Widget):
lane_lines_color = get_theme_color("LaneLines", STOCK_LANE_LINES_COLOR)
color = with_alpha(lane_lines_color, int(alpha * lane_lines_color.a))
if stock_scheme and ui_state.status == UIStatus.DISENGAGED:
if stock_scheme and ui_state.status == UIStatus.DISENGAGED and not ui_state.always_on_lateral_active:
color = rl.Color(0, 0, 0, int(alpha * 255))
return color
@@ -424,17 +428,36 @@ class ModelRenderer(Widget):
self._blend_filter.update(int(allow_throttle))
use_rainbow = self._params.get_bool("RainbowPath", default=False)
use_accel_path = not use_rainbow and self._params.get_bool("AccelerationPath", default=True)
path_override = get_param_color(self._params, "PathColor", 255)
custom_theme_selected = (self._params.get("ColorScheme", encoding="utf-8", default="stock") or "stock").lower() != "stock"
if use_rainbow or self._experimental_mode or use_accel_path:
# Draw with acceleration coloring
if use_rainbow:
if len(self._exp_gradient.colors) > 1:
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else:
fallback = get_border_color(ui_state)
draw_polygon(self._rect, self._path.projected_points, rl.Color(fallback.r, fallback.g, fallback.b, 90))
elif path_override is not None or custom_theme_selected:
elif use_accel_path:
if self._experimental_mode:
if len(self._exp_gradient.colors) > 1:
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else:
fallback = get_border_color(ui_state)
draw_polygon(self._rect, self._path.projected_points, rl.Color(fallback.r, fallback.g, fallback.b, 90))
else:
blend_factor = round(self._blend_filter.x * 100) / 100
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
if lateral_ui_active and blend_factor < 1.0:
blended_colors = self._blend_colors(blended_colors, THROTTLE_COLORS, 0.65)
gradient = Gradient(
start=(0.0, 1.0),
end=(0.0, 0.0),
colors=blended_colors,
stops=[0.0, 0.5, 1.0],
)
if ui_state.status == UIStatus.DISENGAGED and not ui_state.always_on_lateral_active:
draw_polygon(self._rect, self._path.projected_points, rl.Color(0, 0, 0, 90))
else:
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
else:
path_color = get_visual_color(self._params, "PathColor", "Path", rl.Color(48, 255, 156, 255))
gradient = Gradient(
start=(0.0, 1.0),
@@ -447,19 +470,6 @@ class ModelRenderer(Widget):
stops=[0.0, 0.5, 1.0],
)
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
else:
# Blend throttle/no throttle colors based on transition
blend_factor = round(self._blend_filter.x * 100) / 100
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
if lateral_ui_active and blend_factor < 1.0:
blended_colors = self._blend_colors(blended_colors, THROTTLE_COLORS, 0.65)
gradient = Gradient(
start=(0.0, 1.0), # Bottom of path
end=(0.0, 0.0), # Top of path
colors=blended_colors,
stops=[0.0, 0.5, 1.0],
)
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
def _draw_lead_indicator(self):
# Draw lead vehicles if available
@@ -599,3 +609,12 @@ class ModelRenderer(Widget):
if self._params.get_bool("IsMetric"):
return value / 2.0
return value * CV.FOOT_TO_METER / 2.0
def _param_float_changed(self, key: str, default: float) -> bool:
value = self._params.get(key, encoding="utf-8")
if value in (None, ""):
return False
try:
return not np.isclose(float(value), default)
except (TypeError, ValueError):
return False
+48 -38
View File
@@ -19,9 +19,11 @@ MIN_DRAW_DISTANCE = 10.0
MAX_DRAW_DISTANCE = 100.0
RAINBOW_GRADIENT_COLOR_COUNT = 19
RAINBOW_SCROLL_SPEED_DEG_PER_SEC = 60.0
ACCEL_PATH_MIN_LIGHTNESS = 0.78
ACCEL_PATH_MIN_SATURATION = 0.50
STOCK_LANE_LINES_COLOR = rl.Color(255, 255, 255, 255)
DEFAULT_LANE_LINES_WIDTH = 4.0
DEFAULT_PATH_EDGE_WIDTH = 20.0
DEFAULT_PATH_WIDTH = 6.1
DEFAULT_ROAD_EDGES_WIDTH = 2.0
THROTTLE_COLORS = [
rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4)
@@ -35,7 +37,6 @@ NO_THROTTLE_COLORS = [
rl.Color(242, 242, 242, 0), # HSLF(112/360, 0.0, 0.95, 0.0)
]
@dataclass
class ModelPoints:
raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32))
@@ -189,16 +190,15 @@ class ModelRenderer(Widget):
def _update_model(self, lead, path_x_array):
"""Update model visualization data based on model message"""
model_ui_enabled = self._params.get_bool('ModelUI', default=True)
if model_ui_enabled:
path_width = self._path_width_to_half_m(self._params.get_float('PathWidth', return_default=True, default=6.1))
lane_line_width_m = self._small_distance_to_half_m(self._params.get_float('LaneLinesWidth', return_default=True, default=4.0))
road_edge_width_m = self._small_distance_to_half_m(self._params.get_float('RoadEdgesWidth', return_default=True, default=2.0))
path_edge_width_pct = np.clip(self._params.get_float('PathEdgeWidth', return_default=True, default=20.0) / 100.0, 0.0, 1.0)
else:
path_width = 0.9
lane_line_width_m = 0.025
road_edge_width_m = 0.025
path_edge_width_pct = 0.0
custom_path_width = model_ui_enabled and self._param_float_changed('PathWidth', DEFAULT_PATH_WIDTH)
custom_lane_line_width = model_ui_enabled and self._param_float_changed('LaneLinesWidth', DEFAULT_LANE_LINES_WIDTH)
custom_road_edge_width = model_ui_enabled and self._param_float_changed('RoadEdgesWidth', DEFAULT_ROAD_EDGES_WIDTH)
custom_path_edge_width = model_ui_enabled and self._param_float_changed('PathEdgeWidth', DEFAULT_PATH_EDGE_WIDTH)
path_width = self._path_width_to_half_m(self._params.get_float('PathWidth', default=DEFAULT_PATH_WIDTH)) if custom_path_width else 0.9
lane_line_width_m = self._small_distance_to_half_m(self._params.get_float('LaneLinesWidth', default=DEFAULT_LANE_LINES_WIDTH)) if custom_lane_line_width else 0.025
road_edge_width_m = self._small_distance_to_half_m(self._params.get_float('RoadEdgesWidth', default=DEFAULT_ROAD_EDGES_WIDTH)) if custom_road_edge_width else 0.025
path_edge_width_pct = np.clip(self._params.get_float('PathEdgeWidth', default=DEFAULT_PATH_EDGE_WIDTH) / 100.0, 0.0, 1.0) if custom_path_edge_width else 0.0
# Dynamic path width
if model_ui_enabled and self._params.get_bool('DynamicPathWidth', default=False):
@@ -246,17 +246,15 @@ class ModelRenderer(Widget):
def _update_experimental_gradient(self):
"""Pre-calculate experimental mode gradient colors"""
use_rainbow = self._params.get_bool('RainbowPath', default=False)
use_acceleration = not use_rainbow and (self._experimental_mode or self._params.get_bool('AccelerationPath', default=True))
if not use_acceleration and not use_rainbow:
return
max_len = min(len(self._path.projected_points) // 2, len(self._acceleration_x))
if use_rainbow:
gradient_bottom, gradient_top = self._get_visible_gradient_bounds()
self._exp_gradient = self._build_rainbow_gradient(gradient_bottom, gradient_top)
return
if not self._experimental_mode or not self._params.get_bool('AccelerationPath', default=True):
return
max_len = min(len(self._path.projected_points) // 2, len(self._acceleration_x))
segment_colors = []
gradient_stops = []
@@ -273,9 +271,9 @@ class ModelRenderer(Widget):
# speed up: 120, slow down: 0
path_hue = np.clip(60 + self._acceleration_x[i] * 35, 0, 120)
accel_magnitude = np.clip(abs(self._acceleration_x[i]) * 1.5, 0.0, 1.0)
saturation = np.interp(accel_magnitude, [0.0, 1.0], [ACCEL_PATH_MIN_SATURATION, 1.0])
lightness = np.interp(accel_magnitude, [0.0, 1.0], [ACCEL_PATH_MIN_LIGHTNESS, 0.62])
saturation = min(abs(self._acceleration_x[i] * 1.5), 1)
lightness = np.interp(saturation, [0.0, 1.0], [0.95, 0.62])
alpha = np.interp(lin_grad_point, [0.75 / 2.0, 0.75], [0.4, 0.0])
# Use HSL to RGB conversion
@@ -387,14 +385,28 @@ class ModelRenderer(Widget):
use_rainbow = self._params.get_bool('RainbowPath', default=False)
use_accel_path = not use_rainbow and self._params.get_bool('AccelerationPath', default=True)
path_override = get_param_color(self._params, 'PathColor', 255)
custom_theme_selected = (self._params.get('ColorScheme', encoding='utf-8', default='stock') or 'stock').lower() != 'stock'
if use_rainbow or self._experimental_mode or use_accel_path:
if use_rainbow:
if len(self._exp_gradient.colors) > 1:
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else:
draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30))
elif path_override is not None or custom_theme_selected:
draw_polygon(self._rect, self._path.projected_points, rl.Color(48, 255, 156, 90))
elif use_accel_path:
if self._experimental_mode:
if len(self._exp_gradient.colors) > 1:
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
else:
draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30))
else:
blend_factor = round(self._blend_filter.x * 100) / 100
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
gradient = Gradient(
start=(0.0, 1.0),
end=(0.0, 0.0),
colors=blended_colors,
stops=[0.0, 0.5, 1.0],
)
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
else:
path_color = get_visual_color(self._params, "PathColor", "Path", rl.Color(48, 255, 156, 255))
gradient = Gradient(
start=(0.0, 1.0),
@@ -407,17 +419,6 @@ class ModelRenderer(Widget):
stops=[0.0, 0.5, 1.0],
)
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
else:
# Blend throttle/no throttle colors based on transition
blend_factor = round(self._blend_filter.x * 100) / 100
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
gradient = Gradient(
start=(0.0, 1.0), # Bottom of path
end=(0.0, 0.0), # Top of path
colors=blended_colors,
stops=[0.0, 0.5, 1.0],
)
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
def _draw_lead_indicator(self):
# Draw lead vehicles if available
@@ -683,6 +684,15 @@ class ModelRenderer(Widget):
return value / 2.0
return value * CV.FOOT_TO_METER / 2.0
def _param_float_changed(self, key: str, default: float) -> bool:
value = self._params.get(key, encoding="utf-8")
if value in (None, ""):
return False
try:
return not np.isclose(float(value), default)
except (TypeError, ValueError):
return False
@staticmethod
def _blend_colors(begin_colors, end_colors, t):
if t >= 1.0:
@@ -38,6 +38,12 @@ class ConditionalExperimentalMode:
LIGHT_SPEED_LOW = 50 * CV.MPH_TO_MS # 50 mph threshold
LIGHT_SPEED_HIGH = 60 * CV.MPH_TO_MS # 60 mph threshold
LIGHT_MAX_TIME = 9 # Balanced max time preserving city performance
LOW_SPEED_LIGHT_FILTER_TIME = 0.35
LEAD_CLEAR_FILTER_TIME_LOW = 0.6
LEAD_CLEAR_FILTER_TIME_HIGH = 0.35
STOP_LIGHT_ON_MARGIN = 2.5
STOP_LIGHT_OFF_MARGIN = 4.0
STOP_LIGHT_LEAD_BLOCK_MARGIN = 15.0
# ===== END TUNING PARAMETERS =====
@@ -66,11 +72,13 @@ class ConditionalExperimentalMode:
self.curvature_filter = FirstOrderFilter(0, self.FILTER_TIME_CURVE, DT_MDL)
self.slow_lead_filter = FirstOrderFilter(0, self.FILTER_TIME_LEAD, DT_MDL)
self.stop_light_filter = FirstOrderFilter(0, self.FILTER_TIME_LIGHT, DT_MDL)
self.lead_clear_filter = FirstOrderFilter(0, self.LEAD_CLEAR_FILTER_TIME_LOW, DT_MDL)
self.curve_detected = False
self.slow_lead_detected = False
self.experimental_mode = False
self.stop_light_detected = False
self.stop_light_model_detected = False
self.prev_experimental_mode = False # For hysteresis
self.mode_hold_until = 0.0
self.mode_false_since = 0.0
@@ -177,7 +185,8 @@ class ConditionalExperimentalMode:
filter_time_curves = interp(speed_mph, bp, [low_filter_time, low_filter_time, tuned_filter_time_curves])
filter_time_leads = interp(speed_mph, bp, [low_filter_time, low_filter_time, tuned_filter_time_leads])
filter_time_lights = interp(speed_mph, bp, [low_filter_time, low_filter_time, tuned_filter_time_lights])
filter_time_lights = interp(speed_mph, bp, [self.LOW_SPEED_LIGHT_FILTER_TIME, self.LOW_SPEED_LIGHT_FILTER_TIME, tuned_filter_time_lights])
lead_clear_filter_time = interp(speed_mph, bp, [self.LEAD_CLEAR_FILTER_TIME_LOW, self.LEAD_CLEAR_FILTER_TIME_LOW, self.LEAD_CLEAR_FILTER_TIME_HIGH])
light_boost = interp(speed_mph, bp, [low_boost, low_boost, tuned_boost])
cap_factor = interp(speed_mph, bp, [low_cap_factor, low_cap_factor, tuned_cap_factor])
@@ -185,11 +194,14 @@ class ConditionalExperimentalMode:
self.curvature_filter = FirstOrderFilter(self.curvature_filter.x, filter_time_curves, DT_MDL)
self.slow_lead_filter = FirstOrderFilter(self.slow_lead_filter.x, filter_time_leads, DT_MDL)
self.stop_light_filter = FirstOrderFilter(self.stop_light_filter.x, filter_time_lights, DT_MDL)
self.lead_clear_filter.update_alpha(lead_clear_filter_time)
# Disable stoplight detection at very high speeds to prevent false positives
if speed_mph > 75: # Disable above 75 mph
self.stop_light_filter.x = 0
self.stop_light_detected = False
self.stop_light_model_detected = False
self.lead_clear_filter.x = 0
return
# Adjust model time with interp boost and gradual cap
@@ -197,15 +209,30 @@ class ConditionalExperimentalMode:
if cap_factor > 0:
adjusted_model_time = min(adjusted_model_time, self.LIGHT_MAX_TIME * cap_factor + model_time * (1 - cap_factor)) # Gradual cap
model_stopping = self.starpilot_planner.model_length < v_ego * adjusted_model_time
stop_threshold = max(v_ego * adjusted_model_time, 0.0)
if self.stop_light_model_detected:
model_stopping = self.starpilot_planner.model_length < stop_threshold + self.STOP_LIGHT_OFF_MARGIN
else:
model_stopping = self.starpilot_planner.model_length < max(stop_threshold - self.STOP_LIGHT_ON_MARGIN, 0.0)
self.stop_light_model_detected = model_stopping
# `model_stopped` is a coarse horizon-length check (< 50 m with current constants)
# used elsewhere for force-stop/green-light behavior. Reusing it here causes
# ordinary low-speed cruising to look like a stop prediction and can latch the
# STOP_LIGHT CEM trigger. For the CEM detector, key strictly off the configured
# "predicted stop within N seconds" threshold.
self.stop_light_filter.update(model_stopping)
self.stop_light_detected = bool(self.stop_light_filter.x >= THRESHOLD**2 and not self.starpilot_planner.tracking_lead)
# Key off relevant raw lead presence, not trackingLead. Vision-only GM can
# flap trackingLead around the model-length threshold while leadOne remains
# present; far/stale leads should not suppress true stop-light detection.
lead = getattr(self.starpilot_planner, "lead_one", None)
lead_distance = float(getattr(lead, "dRel", float("inf")))
lead_relevant = bool(getattr(lead, "status", False)) and lead_distance < stop_threshold + self.STOP_LIGHT_LEAD_BLOCK_MARGIN
self.lead_clear_filter.update(not lead_relevant)
lead_cleared = self.lead_clear_filter.x >= THRESHOLD
self.stop_light_filter.update(model_stopping and lead_cleared)
self.stop_light_detected = bool(self.stop_light_filter.x >= THRESHOLD**2 and lead_cleared)
else:
self.stop_light_filter.x = 0
self.stop_light_detected = False
self.stop_light_model_detected = False
self.lead_clear_filter.x = 0