diff --git a/common/params_keys.h b/common/params_keys.h index 6d62e387a..fb6985a7c 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -584,6 +584,7 @@ inline static std::unordered_map keys = { {"SteerOffsetStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"SteerRatio", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"SteerRatioStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, + {"StockConfidenceBallWidget", {PERSISTENT, BOOL, "0", "0", 0}}, {"StockDongleId", {PERSISTENT, STRING, "", ""}}, {"StopAccel", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, {"StopAccelStock", {PERSISTENT, FLOAT, "0.0", "0.0", 3}}, diff --git a/opendbc_repo/opendbc/car/gm/carcontroller.py b/opendbc_repo/opendbc/car/gm/carcontroller.py index d5d8d85b8..c91e67b5f 100644 --- a/opendbc_repo/opendbc/car/gm/carcontroller.py +++ b/opendbc_repo/opendbc/car/gm/carcontroller.py @@ -327,9 +327,18 @@ def get_bolt_acc_pedal_friction_brake(apply_brake, full_brake_accel, v_ego, para full_brake_accel = min(full_brake_accel, -0.1) legacy_full_scale = max(-params.ACCEL_MIN, 0.1) corrected_scale = legacy_full_scale / max(-full_brake_accel, 0.1) - speed_gain = float(np.interp(v_ego, [0.0, 10.0, 25.0], [1.0, 1.15, 1.3])) + speed_gain = float(np.interp(v_ego, [0.0, 8.0, 15.0, 25.0], [1.0, 1.08, 1.2, 1.35])) + onset_gain = float(np.interp( + apply_brake, + [0.0, 5.0, 20.0, 60.0, 120.0, 240.0, params.MAX_BRAKE], + [0.0, 1.8, 1.65, 1.4, 1.22, 1.08, 1.0], + )) - return int(round(np.clip(apply_brake * corrected_scale * speed_gain, 0, params.MAX_BRAKE))) + shaped_brake = apply_brake * corrected_scale * speed_gain * onset_gain + minimum_brake = float(np.interp(v_ego, [0.0, 6.0, 8.0, 12.0, 18.0, 25.0], [0.0, 0.0, 4.0, 10.0, 20.0, 28.0])) + shaped_brake = max(shaped_brake, minimum_brake) + + return int(round(np.clip(shaped_brake, 0, params.MAX_BRAKE))) def shape_bolt_acc_pedal_low_speed_friction(apply_brake: int, v_ego: float, stopping: bool, active: bool): @@ -383,6 +392,16 @@ def get_bolt_acc_pedal_friction_command_state(apply_brake: int, cruise_main_on: return command_brake, release_frames, should_send +def get_interceptor_sng_gas_cmd(CP, interceptor_gas_cmd: float, accel: float, params, maneuver_mode: bool) -> float: + if maneuver_mode: + return max(interceptor_gas_cmd, float(np.interp(accel, [0.0, 1.0, 2.0], [params.SNG_INTERCEPTOR_GAS, 0.11, 0.16]))) + + if supports_bolt_acc_pedal_friction_experiment(CP): + return max(interceptor_gas_cmd, params.SNG_INTERCEPTOR_GAS) + + return params.SNG_INTERCEPTOR_GAS + + def should_use_fixed_stopping_brake(CP, near_stop: bool, stopping: bool, resume: bool) -> bool: if not (near_stop and stopping and not resume): return False @@ -971,9 +990,9 @@ class CarController(CarControllerBase): 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]))) + interceptor_gas_cmd = get_interceptor_sng_gas_cmd( + self.CP, interceptor_gas_cmd, actuators.accel, self.params, maneuver_sng_launch, + ) self.apply_brake = 0 self.apply_gas = self.params.INACTIVE_REGEN diff --git a/opendbc_repo/opendbc/car/gm/tests/test_carcontroller.py b/opendbc_repo/opendbc/car/gm/tests/test_carcontroller.py index 4c94c3d9c..2c0588c6e 100644 --- a/opendbc_repo/opendbc/car/gm/tests/test_carcontroller.py +++ b/opendbc_repo/opendbc/car/gm/tests/test_carcontroller.py @@ -47,6 +47,7 @@ from opendbc.car.gm.carcontroller import ( get_bolt_acc_pedal_effective_brake_switch, get_bolt_acc_pedal_planner_brake_switch, get_bolt_pedal_long_accel_limit, + get_interceptor_sng_gas_cmd, get_lka_steering_cmd_counter, get_volt_one_pedal_target_decel, get_testing_ground_1_brake_switch_bias, @@ -164,6 +165,18 @@ def test_bolt_acc_pedal_friction_blend_biases_small_commands_upward_at_speed(): assert high_speed > low_speed +def test_bolt_acc_pedal_friction_blend_applies_a_minimum_pre_stop_command_at_speed(): + params = SimpleNamespace(ACCEL_MIN=-4.0, MAX_BRAKE=400) + + assert get_bolt_acc_pedal_friction_brake(2, -2.86, 17.0, params) >= 18 + + +def test_bolt_acc_pedal_friction_blend_boosts_midrange_commands_before_stopping_phase(): + params = SimpleNamespace(ACCEL_MIN=-4.0, MAX_BRAKE=400) + + assert get_bolt_acc_pedal_friction_brake(40, -2.86, 15.0, params) >= 80 + + def test_bolt_acc_pedal_low_speed_friction_ignores_tiny_inactive_brake_requests(): apply_brake, active = shape_bolt_acc_pedal_low_speed_friction(9, 5.0, False, False) @@ -818,6 +831,30 @@ def test_bolt_acc_pedal_sng_launch_uses_physical_standstill_without_stock_acc_bi assert not use_interceptor_sng_launch(CP, _sng_cs(1.2, False, False)) +def test_bolt_acc_pedal_sng_launch_preserves_stronger_computed_pedal(): + CP = SimpleNamespace( + carFingerprint=CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, + openpilotLongitudinalControl=True, + enableGasInterceptorDEPRECATED=True, + flags=GMFlags.PEDAL_LONG.value, + ) + params = SimpleNamespace(SNG_INTERCEPTOR_GAS=18. / 255.) + + assert get_interceptor_sng_gas_cmd(CP, 0.2, 0.54, params, False) == pytest.approx(0.2) + + +def test_other_pedal_sng_launch_keeps_fixed_floor_behavior(): + CP = SimpleNamespace( + carFingerprint=CAR.CHEVROLET_BOLT_CC_2018_2021, + openpilotLongitudinalControl=True, + enableGasInterceptorDEPRECATED=True, + flags=GMFlags.PEDAL_LONG.value, + ) + params = SimpleNamespace(SNG_INTERCEPTOR_GAS=18. / 255.) + + assert get_interceptor_sng_gas_cmd(CP, 0.2, 0.54, params, False) == pytest.approx(18. / 255.) + + def test_use_interceptor_sng_launch_extends_for_maneuver_mode(): CP = SimpleNamespace(vEgoStarting=0.25) diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index b513d2352..000ac2163 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -66,6 +66,7 @@ REDNECK_BUTTON_COPIES_TIME_METRIC = [REDNECK_BUTTON_COPIES_TIME, 40] ANGLE_SAFETY_BASELINE_MODEL = str(CAR.KIA_SPORTAGE_HEV_2026) DEFAULT_ANGLE_SMOOTHING_VEGO_BP = [5.0, 10.0, 20.0] DEFAULT_ANGLE_SMOOTHING_ALPHA_V = [0.2, 0.1, 0.0] +EV9_HIGH_ANGLE_CONTROL_LIMIT = MAX_ANGLE def egmp_dynamic_longitudinal_tuning(CP) -> bool: @@ -394,6 +395,7 @@ class CarController(CarControllerBase): if self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: v_ego_raw = CS.out.vEgoRaw + ev9_high_angle_inhibit = self.CP.carFingerprint == CAR.KIA_EV9 and abs(CS.out.steeringAngleDeg) >= EV9_HIGH_ANGLE_CONTROL_LIMIT desired_angle = float(np.clip(actuators.steeringAngleDeg, -self.params.ANGLE_LIMITS.STEER_ANGLE_MAX, self.params.ANGLE_LIMITS.STEER_ANGLE_MAX)) @@ -418,6 +420,14 @@ class CarController(CarControllerBase): apply_angle = CS.out.steeringAngleDeg apply_steer_req = False + if ev9_high_angle_inhibit: + apply_torque = 0 + apply_angle = float(np.clip(CS.out.steeringAngleDeg, + -self.params.ANGLE_LIMITS.STEER_ANGLE_MAX, + self.params.ANGLE_LIMITS.STEER_ANGLE_MAX)) + apply_steer_req = False + self.angle_filter.x = apply_angle + self.apply_angle_last = apply_angle if not CC.latActive: self.apply_angle_last = float(np.clip(CS.out.steeringAngleDeg, @@ -617,8 +627,8 @@ class CarController(CarControllerBase): steering_msg_active = apply_steer_req if self.CP.carFingerprint == CAR.KIA_EV9 and self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: # EV9 faults if the angle-steering status drops inactive during torque limiting. - # Hold the angle path active while lateral is active; gain/angle are already limited above. - steering_msg_active = CC.latActive + # Hold the angle path active while lateral is active, except during high-angle maneuvers. + steering_msg_active = CC.latActive and abs(CS.out.steeringAngleDeg) < EV9_HIGH_ANGLE_CONTROL_LIMIT can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, steering_msg_active, apply_torque, apply_angle, @@ -627,7 +637,10 @@ class CarController(CarControllerBase): lka_icon=lka_icon)) # prevent LFA from activating on LKA steering cars by sending "no lane lines detected" to ADAS ECU - if self.frame % 5 == 0 and lka_steering: + suppress_lfa = bool(lka_steering) + if self.CP.carFingerprint == CAR.KIA_EV9 and self.CP.flags & HyundaiFlags.CANFD_ANGLE_STEERING: + suppress_lfa = bool(steering_msg_active) + if self.frame % 5 == 0 and suppress_lfa: can_sends.append(hyundaicanfd.create_suppress_lfa(self.packer, self.CAN, CS.lfa_block_msg, self.CP.flags & HyundaiFlags.CANFD_LKA_STEERING_ALT)) diff --git a/opendbc_repo/opendbc/car/hyundai/carstate.py b/opendbc_repo/opendbc/car/hyundai/carstate.py index 27652e1cd..aa29e673e 100644 --- a/opendbc_repo/opendbc/car/hyundai/carstate.py +++ b/opendbc_repo/opendbc/car/hyundai/carstate.py @@ -75,6 +75,12 @@ class CarState(CarStateBase): self.cruise_buttons: deque = deque([Buttons.NONE] * PREV_BUTTON_SAMPLES, maxlen=PREV_BUTTON_SAMPLES) self.main_buttons: deque = deque([Buttons.NONE] * PREV_BUTTON_SAMPLES, maxlen=PREV_BUTTON_SAMPLES) self.lda_button = 0 + self.sonata_hybrid_lkas_source = None + self.sonata_hybrid_lkas_sources = { + "bcm": 0, + "clu13": 0, + "swl_stat": 0, + } self.lda_button_raw = 0 self.lda_button_raw_initialized = False self.lda_button_last_raw_rise_ts_nanos = 0 @@ -204,10 +210,8 @@ class CarState(CarStateBase): return button_events def create_lkas_button_events(self, cp: CANParser, prev_lda_button: int) -> list[structs.CarState.ButtonEvent]: - if self.CP.carFingerprint == CAR.HYUNDAI_SONATA_HYBRID and cp.ts_nanos["BCM_PO_11"]["LDA_BTN"] > 0: - # Route-proven: late-model Sonata Hybrid publishes a live LKAS button on BCM_PO_11 - # while CLU13 is present on the main bus but does not carry the LKAS state. - self.lda_button = int(cp.vl["BCM_PO_11"]["LDA_BTN"]) + if self.CP.carFingerprint == CAR.HYUNDAI_SONATA_HYBRID: + self.lda_button = self.get_sonata_hybrid_lkas_button_state(cp) # Some classic HKG platforms publish the LKAS button on the cluster bus instead of BCM_PO_11. elif cp.ts_nanos["CLU13"]["CF_Clu_LdwsLkasSW"] > 0: self.lda_button = int(cp.vl["CLU13"]["CF_Clu_LdwsLkasSW"]) @@ -218,6 +222,32 @@ class CarState(CarStateBase): return create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas}) + def get_sonata_hybrid_lkas_button_state(self, cp: CANParser) -> int: + source_states = { + "bcm": int(cp.vl["BCM_PO_11"]["LDA_BTN"]) if cp.ts_nanos["BCM_PO_11"]["LDA_BTN"] > 0 else 0, + "clu13": int(cp.vl["CLU13"]["CF_Clu_LdwsLkasSW"]) if cp.ts_nanos["CLU13"]["CF_Clu_LdwsLkasSW"] > 0 else 0, + "swl_stat": int(cp.vl["CLU13"]["CF_Clu_SWL_Stat"] == 4) if cp.ts_nanos["CLU13"]["CF_Clu_SWL_Stat"] > 0 else 0, + } + + changed_sources = [source for source, state in source_states.items() if state != self.sonata_hybrid_lkas_sources[source]] + active_sources = [source for source, state in source_states.items() if state] + + selected_source = None + if self.sonata_hybrid_lkas_source in changed_sources: + selected_source = self.sonata_hybrid_lkas_source + elif active_sources: + selected_source = active_sources[0] + elif changed_sources: + selected_source = changed_sources[0] + elif self.sonata_hybrid_lkas_source is not None: + selected_source = self.sonata_hybrid_lkas_source + + self.sonata_hybrid_lkas_sources.update(source_states) + if selected_source is not None: + self.sonata_hybrid_lkas_source = selected_source + return source_states[selected_source] + return 0 + def update(self, can_parsers, starpilot_toggles) -> structs.CarState: cp = can_parsers[Bus.pt] cp_cam = can_parsers[Bus.cam] diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index 2d740078f..4fad7ea8f 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -879,6 +879,69 @@ class TestHyundaiFingerprint: ret = update(0, 0, 3) assert any(be.type == ButtonType.lkas and not be.pressed for be in ret.buttonEvents) + def test_sonata_hybrid_falls_back_to_main_bus_clu13_lkas_button_when_bcm_stays_dead(self): + toggles = get_test_toggles() + fingerprint = gen_empty_fingerprint() + fingerprint[0][0x391] = 8 + fingerprint[1][0x50C] = 8 + CP = CarInterface.get_params(CAR.HYUNDAI_SONATA_HYBRID, fingerprint, [], False, False, False, toggles) + FPCP = CarInterface.get_starpilot_params(CAR.HYUNDAI_SONATA_HYBRID, fingerprint, [], CP, toggles) + + car_state = CarState(CP, FPCP) + can_parsers = car_state.get_can_parsers(CP) + packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) + + def update(clu13_lkas_button: int, bcm_lkas_button: int, frame: int): + msgs = [ + packer.make_can_msg("CLU13", 0, { + "CF_Clu_LdwsLkasSW": clu13_lkas_button, + }), + packer.make_can_msg("BCM_PO_11", 0, { + "LDA_BTN": bcm_lkas_button, + }), + ] + can_parsers[Bus.pt].update([(frame, msgs)]) + return car_state.update(can_parsers, toggles)[0] + + update(0, 0, 1) + ret = update(1, 0, 2) + assert any(be.type == ButtonType.lkas and be.pressed for be in ret.buttonEvents) + + ret = update(0, 0, 3) + assert any(be.type == ButtonType.lkas and not be.pressed for be in ret.buttonEvents) + + def test_sonata_hybrid_falls_back_to_main_bus_clu13_swl_stat_lkas_button_when_other_sources_are_dead(self): + toggles = get_test_toggles() + fingerprint = gen_empty_fingerprint() + fingerprint[0][0x391] = 8 + fingerprint[1][0x50C] = 8 + CP = CarInterface.get_params(CAR.HYUNDAI_SONATA_HYBRID, fingerprint, [], False, False, False, toggles) + FPCP = CarInterface.get_starpilot_params(CAR.HYUNDAI_SONATA_HYBRID, fingerprint, [], CP, toggles) + + car_state = CarState(CP, FPCP) + can_parsers = car_state.get_can_parsers(CP) + packer = CANPacker(DBC[CP.carFingerprint][Bus.pt]) + + def update(swl_stat: int, frame: int): + msgs = [ + packer.make_can_msg("CLU13", 0, { + "CF_Clu_LdwsLkasSW": 0, + "CF_Clu_SWL_Stat": swl_stat, + }), + packer.make_can_msg("BCM_PO_11", 0, { + "LDA_BTN": 0, + }), + ] + can_parsers[Bus.pt].update([(frame, msgs)]) + return car_state.update(can_parsers, toggles)[0] + + update(0, 1) + ret = update(4, 2) + assert any(be.type == ButtonType.lkas and be.pressed for be in ret.buttonEvents) + + ret = update(0, 3) + assert any(be.type == ButtonType.lkas and not be.pressed for be in ret.buttonEvents) + def test_sonata_hybrid_ignores_noisy_alt_bus_clu13_lkas_button(self): toggles = get_test_toggles() fingerprint = gen_empty_fingerprint() @@ -1327,7 +1390,8 @@ class TestHyundaiFingerprint: } cc = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(longControlState=LongCtrlState.off), leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace()) - cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg=stock_lkas) + cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg=stock_lkas, + out=SimpleNamespace(steeringAngleDeg=0.0)) msgs = controller.create_canfd_msgs(0, False, 0.0, 8.5, 0.0, 0.0, False, cc.hudControl, cs, cc, get_test_toggles(), lka_icon=2, lfa_icon=2) @@ -1341,6 +1405,56 @@ class TestHyundaiFingerprint: assert parser.vl["LKAS_ALT"]["ADAS_ACIAnglTqRedcGainVal"] == pytest.approx(0.0) assert parser.vl["LKAS_ALT"]["ADAS_StrAnglReqVal"] == pytest.approx(8.5) + def test_ev9_high_steering_angle_inhibits_angle_control_and_lfa_suppress(self): + CP = CarParams.new_message() + CP.carFingerprint = CAR.KIA_EV9 + CP.flags = int(HyundaiFlags.CANFD | HyundaiFlags.EV | HyundaiFlags.CANFD_ANGLE_STEERING | + HyundaiFlags.CANFD_LKA_STEERING | HyundaiFlags.CANFD_LKA_STEERING_ALT) + CP.openpilotLongitudinalControl = False + + controller = CarController(DBC[CP.carFingerprint], CP) + controller.frame = 5 + can_bus = CanBus(CP) + parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("LKAS_ALT", 0)], can_bus.ACAN) + stock_lkas = { + "CHECKSUM": 1234, + "COUNTER": 42, + "LKA_MODE": 2, + "LKA_AVAILABLE": 3, + "LKA_WARNING": 1, + "LKA_ICON": 1, + "FCA_SYSWARN": 1, + "TORQUE_REQUEST": 17, + "STEER_REQ": 1, + "LFA_BUTTON": 1, + "LKA_ASSIST": 1, + "STEER_MODE": 5, + "NEW_SIGNAL_2": 0, + "LKAS_ANGLE_ACTIVE": 1, + "HAS_LANE_SAFETY": 1, + "ADAS_StrAnglReqVal": 12.3, + "ADAS_ACIAnglTqRedcGainVal": 0.42, + "DAMP_FACTOR": 0, + } + cc = SimpleNamespace(enabled=True, latActive=True, actuators=SimpleNamespace(longControlState=LongCtrlState.off), + leftBlinker=False, rightBlinker=False, hudControl=SimpleNamespace()) + cs = SimpleNamespace(stock_lfa_msg=None, stock_lkas_msg=stock_lkas, lfa_block_msg={}, + out=SimpleNamespace(steeringAngleDeg=120.0)) + + msgs = controller.create_canfd_msgs(0, True, 0.44, 120.0, 0.0, 0.0, False, cc.hudControl, cs, cc, + get_test_toggles(), lka_icon=2, lfa_icon=2) + lkas_msgs = [msg for msg in msgs if msg[0] == 0x110] + suppress_msgs = [msg for msg in msgs if msg[0] == 0x362] + assert len(lkas_msgs) == 1 + assert len(suppress_msgs) == 0 + + parser.update([(1, lkas_msgs)]) + + assert parser.can_valid + assert parser.vl["LKAS_ALT"]["LKAS_ANGLE_ACTIVE"] == 1 + assert parser.vl["LKAS_ALT"]["ADAS_ACIAnglTqRedcGainVal"] == pytest.approx(0.0) + assert parser.vl["LKAS_ALT"]["ADAS_StrAnglReqVal"] == pytest.approx(120.0) + def test_can_acc_commands_use_default_values(self): CP = CarParams.new_message() CP.carFingerprint = CAR.GENESIS_G90 diff --git a/scripts/launch_onroad_desktop.sh b/scripts/launch_onroad_desktop.sh index f3490a7bb..ab011dda9 100755 --- a/scripts/launch_onroad_desktop.sh +++ b/scripts/launch_onroad_desktop.sh @@ -27,12 +27,13 @@ env_var_truthy() { usage() { cat <<'EOF' Usage: - ./onroad [jobs] (--c3 | --c4 | --raybig | --all | --replay-only) [-nav] [--prefix name] + ./onroad [jobs] (--c3 | --c4 | --raybig | --all | --replay-only) [-nav] [--mici-widget-demo] [--prefix name] Examples: ./onroad --c3 ./onroad --c4 --start 30 ./onroad --c4 -nav + ./onroad --c4 --mici-widget-demo --demo ./onroad --all ./onroad --replay-only --demo --no-vipc --no-loop @@ -41,6 +42,7 @@ Notes: - A private comma connect route still requires tools/lib/auth.py before replay can download it. - Use multiple UI flags together if you want more than one desktop UI at once. - -nav injects a fake navigation demo stream and blocks replay from publishing navInstruction/navRoute. + - --mici-widget-demo makes the small C4 sidebar cycle through widget states for visual review. EOF } @@ -56,6 +58,7 @@ UI_TARGETS=() LEGACY_UI_SELECTION="" REPLAY_ONLY=0 NAV_DEMO=0 +MICI_WIDGET_DEMO=0 REPLAY_PID="" NAV_PID="" UI_PIDS=() @@ -91,6 +94,10 @@ parse_args() { NAV_DEMO=1 shift ;; + --mici-widget-demo|--widget-demo) + MICI_WIDGET_DEMO=1 + shift + ;; --ui) if [[ $# -lt 2 ]]; then echo "Missing value for --ui" >&2 @@ -315,6 +322,7 @@ prepare_env() { export SP_RAYBIG_FAKE_WIFI=0 export SP_ALLOW_DESKTOP_FAKE_WIFI=0 export SP_ONROAD_NAV_DEMO="${NAV_DEMO}" + export SP_MICI_WIDGET_DEMO="${MICI_WIDGET_DEMO}" if [[ "$(uname -s)" == "Darwin" ]] || env_var_truthy "${ZMQ:-0}"; then export OPENPILOT_ZMQ_NAMESPACE="${PREFIX_ARG:-${OPENPILOT_ZMQ_NAMESPACE:-desktop-onroad-$$}}" diff --git a/selfdrive/ui/layouts/settings/starpilot/appearance.py b/selfdrive/ui/layouts/settings/starpilot/appearance.py index 325af2d0d..a3cf1f633 100644 --- a/selfdrive/ui/layouts/settings/starpilot/appearance.py +++ b/selfdrive/ui/layouts/settings/starpilot/appearance.py @@ -464,6 +464,10 @@ class StarPilotAppearanceLayout(_SettingsPage): subtitle="", get_state=lambda: self._params.get_bool("DriverCamera"), set_state=lambda s: self._params.put_bool("DriverCamera", s)), + SettingRow("StockConfidenceBallWidget", "toggle", tr_noop("Stock Confidence Ball"), + subtitle=tr_noop("Use the original moving confidence ball on the small comma 4 UI."), + get_state=lambda: self._params.get_bool("StockConfidenceBallWidget"), + set_state=lambda s: self._params.put_bool("StockConfidenceBallWidget", s)), SettingRow("BootLogo", "value", tr_noop("Boot Logo"), subtitle="", get_value=lambda: self._get_theme_value("BootLogo"), diff --git a/selfdrive/ui/mici/layouts/settings/visuals.py b/selfdrive/ui/mici/layouts/settings/visuals.py index 77b47bfb4..a8b4bc032 100644 --- a/selfdrive/ui/mici/layouts/settings/visuals.py +++ b/selfdrive/ui/mici/layouts/settings/visuals.py @@ -60,6 +60,7 @@ class VisualsLayoutMici(NavScroller): self._camera_view_btn = CameraViewBigButton() self._driver_camera_btn = BigParamControl("driver camera on reverse", "DriverCamera") self._stopped_timer_btn = BigParamControl("stopped timer", "StoppedTimer") + self._stock_confidence_ball_btn = BigParamControl("stock confidence ball", "StockConfidenceBallWidget") self._rainbow_path_btn = BigParamControl("rainbow road", "RainbowPath") self._lead_indicator_btn = LeadIndicatorBigButton() self._speed_limit_signs_btn = BigParamControl("speed limit signs", "ShowSpeedLimits") @@ -71,6 +72,7 @@ class VisualsLayoutMici(NavScroller): self._camera_view_btn, self._driver_camera_btn, self._stopped_timer_btn, + self._stock_confidence_ball_btn, self._rainbow_path_btn, self._lead_indicator_btn, self._speed_limit_signs_btn, diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 8bfaa8276..bb2d03724 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -12,6 +12,7 @@ from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall +from openpilot.selfdrive.ui.mici.onroad.sidebar_widgets import MiciSidebarWidgets from openpilot.selfdrive.ui.mici.onroad.starpilot_status import ( ENGAGED_COLOR, EXPERIMENTAL_COLOR, @@ -483,6 +484,7 @@ class AugmentedRoadView(CameraView): self._alert_renderer = AlertRenderer() self._driver_state_renderer = DriverStateRenderer() self._confidence_ball = ConfidenceBall() + self._sidebar_widgets = MiciSidebarWidgets(self._confidence_ball) self._min_steer_speed_banner = MinSteerSpeedBanner() self._standstill_timer = StandstillTimerOverlay() self._favorite_slots = self._child(FavoriteSlotsOverlay()) @@ -623,7 +625,10 @@ class AugmentedRoadView(CameraView): # Custom UI extension point - add custom overlays here # Use self._content_rect for positioning within camera bounds if draw_road_overlays: - self._confidence_ball.render(self.rect) + if ui_state.params.get_bool("StockConfidenceBallWidget") and not self._sidebar_widgets.demo_active: + self._confidence_ball.render(self.rect) + else: + self._sidebar_widgets.render(self.rect) if draw_hud_controls and (camera_view_none or is_driver_stream or not in_reverse): self._favorite_slots.render(self._content_rect) if camera_view_none or is_driver_stream or not in_reverse: diff --git a/selfdrive/ui/mici/onroad/confidence_ball.py b/selfdrive/ui/mici/onroad/confidence_ball.py index 62f14c561..63e255437 100644 --- a/selfdrive/ui/mici/onroad/confidence_ball.py +++ b/selfdrive/ui/mici/onroad/confidence_ball.py @@ -43,19 +43,7 @@ class ConfidenceBall(Widget): self._confidence_filter.update((1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs or [1])) * (1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs or [1]))) - def _render(self, _): - content_rect = rl.Rectangle( - self.rect.x + self.rect.width - SIDE_PANEL_WIDTH, - self.rect.y, - SIDE_PANEL_WIDTH, - self.rect.height, - ) - - status_dot_radius = 24 - dot_height = (1 - self._confidence_filter.x) * (content_rect.height - 2 * status_dot_radius) + status_dot_radius - dot_height = self._rect.y + dot_height - - # confidence zones + def _dot_colors(self) -> tuple[rl.Color, rl.Color]: if ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active or self._demo: if self._confidence_filter.x > 0.5: top_dot_color = rl.Color(0, 255, 204, 255) @@ -75,6 +63,31 @@ class ConfidenceBall(Widget): top_dot_color = rl.Color(50, 50, 50, 255) bottom_dot_color = rl.Color(13, 13, 13, 255) + return top_dot_color, bottom_dot_color + + def render_static(self, rect: rl.Rectangle, radius: int = 20) -> None: + self._update_state() + top_dot_color, bottom_dot_color = self._dot_colors() + draw_circle_gradient(rect.x + rect.width / 2, + rect.y + rect.height / 2, + radius, + top_dot_color, + bottom_dot_color) + + def _render(self, _): + content_rect = rl.Rectangle( + self.rect.x + self.rect.width - SIDE_PANEL_WIDTH, + self.rect.y, + SIDE_PANEL_WIDTH, + self.rect.height, + ) + + status_dot_radius = 24 + dot_height = (1 - self._confidence_filter.x) * (content_rect.height - 2 * status_dot_radius) + status_dot_radius + dot_height = self._rect.y + dot_height + + top_dot_color, bottom_dot_color = self._dot_colors() + draw_circle_gradient(content_rect.x + content_rect.width - status_dot_radius, dot_height, status_dot_radius, top_dot_color, bottom_dot_color) diff --git a/selfdrive/ui/mici/onroad/sidebar_widgets.py b/selfdrive/ui/mici/onroad/sidebar_widgets.py new file mode 100644 index 000000000..17ef1dc71 --- /dev/null +++ b/selfdrive/ui/mici/onroad/sidebar_widgets.py @@ -0,0 +1,360 @@ +import os +import pyray as rl +from cereal import log +from openpilot.common.constants import CV +from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH +from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.starpilot.common.experimental_state import CCStatus, CEStatus +from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget + +PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants +DEMO_HOLD_SECONDS = 1.5 +DEMO_REASONS = ("chill", "lead", "stop", "curve", "speed") +DEMO_PERSONALITIES = ( + (False, 0), # aggressive + (False, 1), # standard + (False, 2), # relaxed + (True, 0), # traffic +) + +WHITE = rl.Color(255, 255, 255, 255) +WHITE_DIM = rl.Color(255, 255, 255, 170) +BLACK = rl.Color(0, 0, 0, 255) +PERSONALITY_BLUE = rl.Color(112, 192, 216, 255) +CEM_BLUE = PERSONALITY_BLUE +TRAFFIC_RED = rl.Color(200, 32, 48, 255) + + +def _with_alpha(color: rl.Color, alpha: int) -> rl.Color: + return rl.Color(color.r, color.g, color.b, max(0, min(255, alpha))) + + +def _env_truthy(name: str) -> bool: + return os.getenv(name, "").lower() in {"1", "true", "yes", "on"} + + +def _v(x: float, y: float) -> rl.Vector2: + return rl.Vector2(float(x), float(y)) + + +def _draw_line(x1: float, y1: float, x2: float, y2: float, thickness: float, color: rl.Color) -> None: + rl.draw_line_ex(_v(x1, y1), _v(x2, y2), thickness, color) + + +def _draw_tri(p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], color: rl.Color) -> None: + rl.draw_triangle(_v(*p1), _v(*p2), _v(*p3), color) + + +def _draw_quad(points: tuple[tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float]], color: rl.Color) -> None: + p1, p2, p3, p4 = points + rl.draw_triangle(_v(*p1), _v(*p2), _v(*p3), color) + rl.draw_triangle(_v(*p1), _v(*p3), _v(*p4), color) + + +def _draw_quad_outline(points: tuple[tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float]], thickness: float, color: rl.Color) -> None: + for idx, start in enumerate(points): + end = points[(idx + 1) % len(points)] + _draw_line(start[0], start[1], end[0], end[1], thickness, color) + + +def _draw_bezier(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], thickness: float, color: rl.Color) -> None: + rl.draw_spline_segment_bezier_cubic(_v(*p0), _v(*p1), _v(*p2), _v(*p3), thickness, color) + + +def _bezier_point(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], t: float) -> tuple[float, float]: + inv_t = 1.0 - t + x = inv_t ** 3 * p0[0] + 3 * inv_t ** 2 * t * p1[0] + 3 * inv_t * t ** 2 * p2[0] + t ** 3 * p3[0] + y = inv_t ** 3 * p0[1] + 3 * inv_t ** 2 * t * p1[1] + 3 * inv_t * t ** 2 * p2[1] + t ** 3 * p3[1] + return x, y + + +def _draw_dashed_bezier(p0: tuple[float, float], p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float], thickness: float, color: rl.Color) -> None: + for start_t, end_t in ((0.18, 0.28), (0.39, 0.50), (0.61, 0.72)): + start = _bezier_point(p0, p1, p2, p3, start_t) + end = _bezier_point(p0, p1, p2, p3, end_t) + _draw_line(start[0], start[1], end[0], end[1], thickness, color) + + +class MiciSidebarWidgets(Widget): + def __init__(self, confidence_ball: ConfidenceBall): + super().__init__() + self._confidence_ball = confidence_ball + self._font_bold = gui_app.font(FontWeight.BOLD) + self._font_semi_bold = gui_app.font(FontWeight.SEMI_BOLD) + self._demo = _env_truthy("SP_MICI_WIDGET_DEMO") + + @property + def demo_active(self) -> bool: + return self._demo + + def _render(self, rect: rl.Rectangle) -> None: + sidebar = rl.Rectangle( + rect.x + rect.width - SIDE_PANEL_WIDTH, + rect.y, + SIDE_PANEL_WIDTH, + rect.height, + ) + rl.draw_rectangle(int(sidebar.x), int(sidebar.y), int(sidebar.width), int(sidebar.height), BLACK) + + slot_height = sidebar.height / 3 + confidence_slot = rl.Rectangle(sidebar.x, sidebar.y, sidebar.width, slot_height) + cem_slot = rl.Rectangle(sidebar.x, sidebar.y + slot_height, sidebar.width, slot_height) + personality_slot = rl.Rectangle(sidebar.x, sidebar.y + slot_height * 2, sidebar.width, sidebar.height - slot_height * 2) + + self._confidence_ball.render_static(confidence_slot, max(16, min(20, int(slot_height * 0.28)))) + self._draw_cem_widget(cem_slot) + self._draw_personality_widget(personality_slot) + + def _draw_personality_widget(self, rect: rl.Rectangle) -> None: + center_x = rect.x + rect.width / 2 + center_y = rect.y + rect.height / 2 + icon_height = min(rect.height - 6, 64) + bottom_y = center_y + icon_height * 0.42 + lane_top_y = center_y - icon_height * 0.38 + + traffic_mode, personality = self._personality_state() + + active_color = TRAFFIC_RED if traffic_mode else PERSONALITY_BLUE + stack_count = 1 if traffic_mode else max(1, min(3, personality + 1)) + + _draw_line(center_x - 23, bottom_y, center_x - 14, lane_top_y, 3, WHITE) + _draw_line(center_x + 23, bottom_y, center_x + 14, lane_top_y, 3, WHITE) + + car_top_y = bottom_y - 4 + car_bottom_y = bottom_y + 8 + car_outline = ( + (center_x - 8, car_top_y), + (center_x + 8, car_top_y), + (center_x + 13, car_bottom_y), + (center_x - 13, car_bottom_y), + ) + _draw_quad_outline(car_outline, 3, WHITE) + + bar_h = max(6, min(9, int(icon_height * 0.14))) + gap = max(3, min(5, int(icon_height * 0.08))) + y = bottom_y - bar_h - 4 + widths = ((36, 30), (28, 23), (20, 16)) + for idx in range(stack_count): + bottom_w, top_w = widths[idx] + top_y = y + bottom = y + bar_h + points = ( + (center_x - top_w / 2, top_y), + (center_x + top_w / 2, top_y), + (center_x + bottom_w / 2, bottom), + (center_x - bottom_w / 2, bottom), + ) + _draw_quad(points, _with_alpha(active_color, 60)) + _draw_quad_outline(points, 3, active_color) + y -= bar_h + gap + + def _personality_state(self) -> tuple[bool, int]: + if self._demo: + return DEMO_PERSONALITIES[int(rl.get_time() / DEMO_HOLD_SECONDS) % len(DEMO_PERSONALITIES)] + + personality = 1 + try: + if ui_state.sm.valid.get("selfdriveState", False): + personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality] + elif isinstance(ui_state.personality, int): + personality = ui_state.personality + else: + personality = PERSONALITY_TO_INT[ui_state.personality] + except Exception: + personality = 1 + return ui_state.traffic_mode_enabled, personality + + def _draw_cem_widget(self, rect: rl.Rectangle) -> None: + reason, color = self._cem_reason() + if reason == "chill": + self._draw_chill_icon(rect) + elif reason == "lead": + self._draw_lead_icon(rect, color) + elif reason == "stop": + self._draw_stop_light_icon(rect) + elif reason == "curve": + self._draw_curve_icon(rect, color) + elif reason == "turn": + self._draw_turn_icon(rect, color) + elif reason == "speed": + self._draw_speed_icon(rect, color) + else: + self._draw_chill_icon(rect) + + def _model_stop_active(self) -> bool: + try: + if ui_state.sm.recv_frame["starpilotPlan"] >= ui_state.started_frame: + starpilot_plan = ui_state.sm["starpilotPlan"] + return bool(starpilot_plan.redLight or starpilot_plan.forcingStop) + except Exception: + pass + return False + + def _cem_reason(self) -> tuple[str, rl.Color]: + if self._demo: + reason = DEMO_REASONS[int(rl.get_time() / DEMO_HOLD_SECONDS) % len(DEMO_REASONS)] + if reason == "chill": + return reason, WHITE + if reason == "stop": + return reason, TRAFFIC_RED + return reason, CEM_BLUE + + conditional_experimental = ui_state.params.get_bool("ConditionalExperimental") + conditional_chill = ui_state.params.get_bool("ConditionalChill") and not conditional_experimental + if conditional_chill: + status = ui_state.params_memory.get_int("CCStatus", default=CCStatus["OFF"]) + if status == CCStatus["LEAD"]: + return "lead", CEM_BLUE + if status == CCStatus["SPEED"]: + return "speed", CEM_BLUE + if status == CCStatus["USER_EXPERIMENTAL"]: + return "chill", WHITE + if status == CCStatus["USER_CHILL"]: + return "chill", WHITE + return "chill", WHITE + + status = ui_state.params_memory.get_int("CEStatus", default=CEStatus["OFF"]) if conditional_experimental else CEStatus["OFF"] + if status == CEStatus["CURVATURE"]: + return "curve", CEM_BLUE + if status == CEStatus["LEAD"]: + return "lead", CEM_BLUE + if status == CEStatus["SIGNAL"]: + return "turn", CEM_BLUE + if status in (CEStatus["SPEED"], CEStatus["SPEED_LIMIT"]): + return "speed", CEM_BLUE + if status == CEStatus["STOP_LIGHT"]: + return "stop", TRAFFIC_RED + if status == CEStatus["USER_DISABLED"]: + return "chill", WHITE + if status == CEStatus["USER_OVERRIDDEN"]: + return "chill", WHITE + if self._model_stop_active(): + return "stop", TRAFFIC_RED + return "chill", WHITE + + def _draw_chill_icon(self, rect: rl.Rectangle) -> None: + cx = rect.x + rect.width / 2 + cy = rect.y + rect.height / 2 + stroke = 3 + + _draw_line(cx - 14, cy - 15, cx + 14, cy - 15, stroke, WHITE) + _draw_line(cx - 16, cy - 13, cx - 16, cy + 1, stroke, WHITE) + _draw_line(cx + 16, cy - 13, cx + 16, cy + 1, stroke, WHITE) + _draw_line(cx - 19, cy + 3, cx + 19, cy + 3, stroke, WHITE) + _draw_line(cx - 21, cy - 3, cx - 21, cy + 10, stroke, WHITE) + _draw_line(cx + 21, cy - 3, cx + 21, cy + 10, stroke, WHITE) + _draw_line(cx - 21, cy + 10, cx + 21, cy + 10, stroke, WHITE) + _draw_line(cx - 14, cy + 6, cx + 14, cy + 6, 2, CEM_BLUE) + _draw_line(cx - 14, cy + 12, cx - 17, cy + 19, stroke, WHITE) + _draw_line(cx + 14, cy + 12, cx + 17, cy + 19, stroke, WHITE) + + def _draw_lead_icon(self, rect: rl.Rectangle, color: rl.Color) -> None: + cx = rect.x + rect.width / 2 + cy = rect.y + rect.height / 2 + self._draw_car(cx, cy - 13, 31, 18, WHITE, TRAFFIC_RED) + self._draw_car(cx, cy + 12, 43, 22, WHITE, TRAFFIC_RED) + _draw_line(cx - 11, cy - 1, cx - 11, cy + 3, 2, _with_alpha(WHITE, 130)) + _draw_line(cx + 11, cy - 1, cx + 11, cy + 3, 2, _with_alpha(WHITE, 130)) + + def _draw_car(self, cx: float, cy: float, width: float, height: float, color: rl.Color, accent: rl.Color) -> None: + top = cy - height / 2 + bottom = cy + height / 2 + body = ( + (cx - width * 0.34, top + height * 0.10), + (cx + width * 0.34, top + height * 0.10), + (cx + width * 0.50, bottom - height * 0.04), + (cx - width * 0.50, bottom - height * 0.04), + ) + windshield = ( + (cx - width * 0.22, top + height * 0.24), + (cx + width * 0.22, top + height * 0.24), + (cx + width * 0.32, top + height * 0.50), + (cx - width * 0.32, top + height * 0.50), + ) + _draw_quad_outline(body, 3, color) + _draw_quad_outline(windshield, 2, color) + _draw_line(cx - width * 0.56, top + height * 0.46, cx - width * 0.50, top + height * 0.46, 2, color) + _draw_line(cx + width * 0.50, top + height * 0.46, cx + width * 0.56, top + height * 0.46, 2, color) + _draw_line(cx - width * 0.32, bottom - height * 0.22, cx - width * 0.16, bottom - height * 0.22, 2, accent) + _draw_line(cx + width * 0.16, bottom - height * 0.22, cx + width * 0.32, bottom - height * 0.22, 2, accent) + + def _draw_stop_light_icon(self, rect: rl.Rectangle) -> None: + cx = rect.x + rect.width / 2 + cy = rect.y + rect.height / 2 + housing = rl.Rectangle(cx - 12, cy - 27, 24, 54) + rl.draw_rectangle_rounded_lines_ex(housing, 0.35, 8, 3, WHITE) + for bulb_y in (cy - 16, cy, cy + 16): + rl.draw_circle_lines(int(cx), int(bulb_y), 6, WHITE) + rl.draw_circle_lines(int(cx), int(cy - 16), 7, TRAFFIC_RED) + rl.draw_circle(int(cx), int(cy - 16), 4, TRAFFIC_RED) + + def _draw_curve_icon(self, rect: rl.Rectangle, color: rl.Color) -> None: + cx = rect.x + rect.width / 2 + cy = rect.y + rect.height / 2 + def road_curve_points(x_offset: float) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float]]: + return ( + (cx + x_offset, cy + 24), + (cx + x_offset - 1, cy + 9), + (cx + x_offset + 1, cy - 9), + (cx + x_offset + 6, cy - 24), + ) + + for points in (road_curve_points(-18), road_curve_points(14)): + _draw_bezier(*points, 3, WHITE) + rl.draw_circle(int(points[0][0]), int(points[0][1]), 2, WHITE) + rl.draw_circle(int(points[3][0]), int(points[3][1]), 2, WHITE) + _draw_dashed_bezier(*road_curve_points(-2), 4, color) + + def _draw_turn_icon(self, rect: rl.Rectangle, color: rl.Color) -> None: + cx = rect.x + rect.width / 2 + cy = rect.y + rect.height / 2 + left = False + right = True + try: + left = ui_state.sm["carState"].leftBlinker + right = ui_state.sm["carState"].rightBlinker + except Exception: + pass + direction = -1 if left or not right else 1 + shaft = ( + (cx - direction * 10, cy + 25), + (cx - direction * 13, cy + 6), + (cx - direction * 4, cy - 8), + (cx + direction * 18, cy - 20), + ) + _draw_bezier(*shaft, 4, color) + tip = (cx + direction * 24, cy - 26) + _draw_tri( + tip, + (cx + direction * 4, cy - 24), + (cx + direction * 19, cy - 7), + color, + ) + + def _draw_speed_icon(self, rect: rl.Rectangle, color: rl.Color) -> None: + cx = rect.x + rect.width / 2 + cy = rect.y + rect.height / 2 + radius = min(rect.width, rect.height) * 0.34 + text = self._speed_text() + + rl.draw_circle(int(cx), int(cy), radius, WHITE) + rl.draw_ring(_v(cx, cy), radius - 5, radius, 0, 360, 48, color) + + font_size = 19 if len(text) <= 2 else 16 + text_size = measure_text_cached(self._font_bold, text, font_size) + rl.draw_text_ex(self._font_bold, text, rl.Vector2(cx - text_size.x / 2, cy - text_size.y / 2 + 1), font_size, 0, BLACK) + + def _speed_text(self) -> str: + if ui_state.sm.recv_frame["starpilotPlan"] < ui_state.started_frame: + return "S" + + speed_limit = ui_state.sm["starpilotPlan"].slcSpeedLimit + if speed_limit <= 0: + return "S" + + conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH + speed_text = str(round(speed_limit * conversion)) + return speed_text[:3] diff --git a/starpilot/common/starpilot_variables.py b/starpilot/common/starpilot_variables.py index d7a67971e..262c61a96 100644 --- a/starpilot/common/starpilot_variables.py +++ b/starpilot/common/starpilot_variables.py @@ -1223,6 +1223,7 @@ class StarPilotVariables: toggle.driver_camera_in_reverse = self.get_value("DriverCamera", condition=quality_of_life_visuals) toggle.onroad_distance_button = toggle.openpilot_longitudinal and (self.get_value("OnroadDistanceButton", condition=quality_of_life_visuals) or toggle.debug_mode) toggle.stopped_timer = self.get_value("StoppedTimer", condition=quality_of_life_visuals) + toggle.stock_confidence_ball_widget = self.get_value("StockConfidenceBallWidget", condition=quality_of_life_visuals) toggle.rainbow_path = self.get_value("RainbowPath", condition=not toggle.debug_mode) diff --git a/starpilot/controls/starpilot_card.py b/starpilot/controls/starpilot_card.py index 03951671b..4f5400e4c 100644 --- a/starpilot/controls/starpilot_card.py +++ b/starpilot/controls/starpilot_card.py @@ -114,18 +114,20 @@ class StarPilotCard: def update(self, carState, starpilotCarState, sm, starpilot_toggles): self.switchback_mode_enabled = self.params_memory.get_bool("SwitchbackModeEnabled") button_event_types = [self._button_type_raw(be) for be in carState.buttonEvents] + sonata_hybrid_cruise_ready = self.hyundai_lkas_aol_requires_engagement and carState.cruiseState.available hyundai_lkas_aol_can_toggle = ( not self.hyundai_lkas_aol_requires_engagement or self.hyundai_aol_ready or sm["selfdriveState"].active or - carState.cruiseState.enabled + carState.cruiseState.enabled or + sonata_hybrid_cruise_ready ) if self.hyundai_aol_needs_engagement: if carState.gearShifter in NON_DRIVING_GEARS: self.hyundai_aol_ready = False self.always_on_lateral_allowed = False - elif sm["selfdriveState"].active or carState.cruiseState.enabled: + elif sm["selfdriveState"].active or carState.cruiseState.enabled or sonata_hybrid_cruise_ready: self.hyundai_aol_ready = True if self.CP.brand == "hyundai" or starpilot_toggles.lkas_allowed_for_aol: diff --git a/starpilot/controls/tests/test_starpilot_card.py b/starpilot/controls/tests/test_starpilot_card.py index 4ba715c52..0dd0b08e9 100644 --- a/starpilot/controls/tests/test_starpilot_card.py +++ b/starpilot/controls/tests/test_starpilot_card.py @@ -181,8 +181,8 @@ def test_sonata_hybrid_lkas_button_can_toggle_aol_after_engagement(monkeypatch, sm = make_sm() toggles = make_toggles(always_on_lateral=True, always_on_lateral_lkas=True) - engaged_state = make_car_state(available=True, enabled=True) - card.update(engaged_state, starpilot_car_state, sm, toggles) + cruise_main_state = make_car_state(available=True, enabled=False) + card.update(cruise_main_state, starpilot_car_state, sm, toggles) lkas_state = make_car_state(available=False, enabled=False, button_events=[SimpleNamespace(type=spc.ButtonType.lkas, pressed=True)]) ret = card.update(lkas_state, starpilot_car_state, sm, toggles) diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json index 7b2eed841..54016ca65 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json @@ -2105,6 +2105,14 @@ "ui_type": "toggle", "parent_key": "QOLVisuals" }, + { + "key": "StockConfidenceBallWidget", + "label": "Stock Confidence Ball Widget", + "description": "Use the original moving confidence ball on the small comma 4 UI instead of the fixed confidence, CEM/CCM, and personality sidebar.", + "data_type": "bool", + "ui_type": "toggle", + "parent_key": "QOLVisuals" + }, { "key": "SimpleMode", "label": "Simple Mode", diff --git a/starpilot/ui/qt/offroad/visual_settings.cc b/starpilot/ui/qt/offroad/visual_settings.cc index 7e7e88dc0..af9b20ab4 100644 --- a/starpilot/ui/qt/offroad/visual_settings.cc +++ b/starpilot/ui/qt/offroad/visual_settings.cc @@ -71,6 +71,7 @@ StarPilotVisualsPanel::StarPilotVisualsPanel(StarPilotSettingsWindow *parent, bo {"CameraView", tr("Camera View"), tr("Select the active camera view. This is purely a visual change and doesn't impact how openpilot drives!"), ""}, {"DriverCamera", tr("Show Driver Camera When In Reverse"), tr("Show the driver camera feed when the vehicle is in reverse."), ""}, {"StoppedTimer", tr("Stopped Timer"), tr("Show a timer when stopped in place of the current speed to indicate how long the vehicle has been stopped."), ""}, + {"StockConfidenceBallWidget", tr("Stock Confidence Ball Widget"), tr("Use the original moving confidence ball on the small comma 4 UI instead of the fixed confidence, CEM/CCM, and personality sidebar."), ""}, {"DisableWideRoad", tr("Disable Wide Road Camera"), QString("%1

%2").arg(tr("Only enable this if the wide camera is broken or for development!")).arg(tr("Disabling the wide camera may degrade driving performance and cause instability.

Requires a reboot to take effect.")), "../../starpilot/assets/toggle_icons/icon_advanced_device.png"} }; diff --git a/starpilot/ui/qt/offroad/visual_settings.h b/starpilot/ui/qt/offroad/visual_settings.h index 8ca32657f..615b05bb3 100644 --- a/starpilot/ui/qt/offroad/visual_settings.h +++ b/starpilot/ui/qt/offroad/visual_settings.h @@ -27,7 +27,7 @@ private: QSet customOnroadUIKeys = {"AccelerationPath", "AdjacentPath", "BlindSpotPath", "Compass", "OnroadDistanceButton", "PedalsOnUI", "RotatingWheel"}; QSet modelUIKeys = {"DynamicPathWidth", "LaneLinesWidth", "PathEdgeWidth", "PathWidth", "RoadEdgesWidth"}; QSet navigationUIKeys = {"RoadNameUI", "ShowSpeedLimits", "SLCMapboxFiller", "UseVienna"}; - QSet qualityOfLifeKeys = {"CameraView", "DriverCamera", "StoppedTimer"}; + QSet qualityOfLifeKeys = {"CameraView", "DriverCamera", "StoppedTimer", "StockConfidenceBallWidget"}; QSet parentKeys; diff --git a/tools/StarPilot/feasibleparams.txt b/tools/StarPilot/feasibleparams.txt index da0a161bd..7b72c9510 100644 --- a/tools/StarPilot/feasibleparams.txt +++ b/tools/StarPilot/feasibleparams.txt @@ -339,6 +339,7 @@ SteerLatAccel SteerLatAccelStock SteerRatio SteerRatioStock +StockConfidenceBallWidget StopAccel StopAccelStock StopDistance diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index 75abb8417..23c7a6597 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -92,7 +92,7 @@ void LogReader::migrateOldEvents() { new_state.setActive(old_state.getActiveDEPRECATED()); new_state.setAlertSize(old_state.getAlertSizeDEPRECATED()); - new_state.setAlertSound(old_state.getAlertSound2DEPRECATED()); + new_state.setAlertSound(static_cast(old_state.getAlertSound2DEPRECATED())); new_state.setAlertStatus(old_state.getAlertStatusDEPRECATED()); new_state.setAlertText1(old_state.getAlertText1DEPRECATED()); new_state.setAlertText2(old_state.getAlertText2DEPRECATED());