diff --git a/cereal/custom.capnp b/cereal/custom.capnp index c577ad906..a8ba066a5 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -220,6 +220,7 @@ struct StarPilotPlan @0xf98d843bfd7004a3 { disableThrottle @35 :Bool; trackingLead @36 :Bool; stopSignConfirmed @37 :Bool; + pulseGlideCoasting @38 :Bool; # developer-only P&G phase for on-road status UI } struct StarPilotRadarState @0xb86e6369214c01c8 { diff --git a/opendbc_repo/opendbc/car/hyundai/carcontroller.py b/opendbc_repo/opendbc/car/hyundai/carcontroller.py index 32a4c1097..670a150c0 100644 --- a/opendbc_repo/opendbc/car/hyundai/carcontroller.py +++ b/opendbc_repo/opendbc/car/hyundai/carcontroller.py @@ -9,10 +9,12 @@ from opendbc.car.common.conversions import Conversions as CV from opendbc.car.hyundai import hyundaicanfd, hyundaican from opendbc.car.hyundai.hyundaicanfd import CanBus from opendbc.car.hyundai.values import HyundaiFlags, Buttons, CarControllerParams, CAR, CANFD_ANGLE_LONGITUDINAL_CAR, \ - CANFD_RADAR_LIVE_LONGITUDINAL_CAR, kia_ev6_gt_line_longitudinal_tuning + CANFD_RADAR_LIVE_LONGITUDINAL_CAR, kia_ev6_gt_line_longitudinal_tuning, \ + KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID from opendbc.car.interfaces import CarControllerBase from opendbc.car.vehicle_model import VehicleModel from openpilot.common.params import Params +from openpilot.starpilot.common.testing_grounds import testing_ground VisualAlert = structs.CarControl.HUDControl.VisualAlert LongCtrlState = structs.CarControl.Actuators.LongControlState @@ -79,7 +81,10 @@ BLINDSPOT_WARNING_SOUND_SAMPLES = 36 def egmp_dynamic_longitudinal_tuning(CP) -> bool: return CP.carFingerprint in (CAR.HYUNDAI_IONIQ_6, CAR.KIA_EV9, CAR.HYUNDAI_IONIQ_5_PE) or \ - kia_ev6_gt_line_longitudinal_tuning(CP.carFingerprint, getattr(CP, "carVin", "")) + kia_ev6_gt_line_longitudinal_tuning( + CP.carFingerprint, getattr(CP, "carVin", ""), + testing_ground.use(KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID), + ) def get_canfd_scc_decel_step(CP) -> float: @@ -87,7 +92,10 @@ def get_canfd_scc_decel_step(CP) -> float: def should_reset_ev6_gt_line_longitudinal_tuning(CP, long_control_state: LongCtrlState) -> bool: - return kia_ev6_gt_line_longitudinal_tuning(CP.carFingerprint, getattr(CP, "carVin", "")) and \ + return kia_ev6_gt_line_longitudinal_tuning( + CP.carFingerprint, getattr(CP, "carVin", ""), + testing_ground.use(KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID), + ) and \ long_control_state == LongCtrlState.off @@ -631,7 +639,10 @@ class CarController(CarControllerBase): use_egmp_dynamic_long_tuning = egmp_dynamic_longitudinal_tuning(self.CP) and self.long_active_ecu and \ actuators.longControlState in (LongCtrlState.starting, LongCtrlState.pid, LongCtrlState.stopping) - is_ev6_gt_line = kia_ev6_gt_line_longitudinal_tuning(self.CP.carFingerprint, getattr(self.CP, "carVin", "")) + is_ev6_gt_line = kia_ev6_gt_line_longitudinal_tuning( + self.CP.carFingerprint, getattr(self.CP, "carVin", ""), + testing_ground.use(KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID), + ) is_ccnc_angle_long = self.CP.carFingerprint in CANFD_ANGLE_LONGITUDINAL_CAR if is_ccnc_angle_long and (self._ev9_long_tuning.stop_request or not CC.enabled or CC.cruiseControl.override): self._ioniq_6_long_tuning = reset_egmp_longitudinal_tuning(self._ioniq_6_long_tuning) diff --git a/opendbc_repo/opendbc/car/hyundai/interface.py b/opendbc_repo/opendbc/car/hyundai/interface.py index b75082688..c9aaffdec 100644 --- a/opendbc_repo/opendbc/car/hyundai/interface.py +++ b/opendbc_repo/opendbc/car/hyundai/interface.py @@ -12,13 +12,15 @@ from opendbc.car.hyundai.values import HyundaiFlags, CAR, CarControllerParams, \ CAN_CANFD_BLENDED_HDA2_LONGITUDINAL_CAR, \ HyundaiStarPilotSafetyFlags, \ hyundai_cancel_button_enables_cruise, \ - kia_ev6_gt_line_longitudinal_tuning + kia_ev6_gt_line_longitudinal_tuning, \ + KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID from opendbc.car.hyundai.radar_interface import get_radar_track_config, radar_tracks_available from opendbc.car.interfaces import CarInterfaceBase, ACCEL_MIN from opendbc.car.disable_ecu import disable_ecu, ecu_log from opendbc.car.hyundai.carcontroller import CarController from opendbc.car.hyundai.carstate import CarState from opendbc.car.hyundai.radar_interface import RadarInterface +from openpilot.starpilot.common.testing_grounds import testing_ground ButtonType = structs.CarState.ButtonEvent.Type Ecu = structs.CarParams.Ecu @@ -106,7 +108,8 @@ class CarInterface(CarInterfaceBase): @staticmethod def apply_post_fingerprint_params(CP: structs.CarParams, candidate, fingerprint, car_fw) -> None: - if kia_ev6_gt_line_longitudinal_tuning(CP.carFingerprint, CP.carVin): + gt_line_testing_ground = testing_ground.use(KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID) + if kia_ev6_gt_line_longitudinal_tuning(CP.carFingerprint, CP.carVin, gt_line_testing_ground): apply_kia_ev6_gt_line_longitudinal_params(CP) @staticmethod diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index 1670b0117..93dd1073c 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -1043,6 +1043,24 @@ class TestHyundaiFingerprint: assert reset_state.accel_last == pytest.approx(0.0) assert reset_state.long_control_state_last == LongCtrlState.off + def test_kia_ev6_gt_line_testing_ground_longitudinal_params(self, monkeypatch): + toggles = get_test_toggles() + CP = CarInterface.get_params(CAR.KIA_EV6, gen_empty_fingerprint(), [], True, False, False, toggles) + CP.carVin = "00000000000000000" + + monkeypatch.setattr( + "opendbc.car.hyundai.interface.testing_ground", + SimpleNamespace(use=lambda slot_id: slot_id == "5"), + ) + CarInterface.apply_post_fingerprint_params(CP, CAR.KIA_EV6, gen_empty_fingerprint(), []) + + assert CP.startAccel == pytest.approx(1.4) + assert CP.vEgoStarting == pytest.approx(0.5) + assert CP.longitudinalActuatorDelay == pytest.approx(0.35) + + assert kia_ev6_gt_line_longitudinal_tuning(CP.carFingerprint, CP.carVin, testing_ground_active=True) + assert not kia_ev6_gt_line_longitudinal_tuning(CAR.KIA_EV6_2025, CP.carVin, testing_ground_active=True) + def test_kia_ev6_non_gt_line_keeps_family_longitudinal_params(self): toggles = get_test_toggles() CP = CarInterface.get_params(CAR.KIA_EV6, gen_empty_fingerprint(), [], True, False, False, toggles) diff --git a/opendbc_repo/opendbc/car/hyundai/values.py b/opendbc_repo/opendbc/car/hyundai/values.py index eecc2ec7a..16eb84388 100644 --- a/opendbc_repo/opendbc/car/hyundai/values.py +++ b/opendbc_repo/opendbc/car/hyundai/values.py @@ -975,6 +975,7 @@ CAN_CANFD_BLENDED_HDA2_LONGITUDINAL_CAR = frozenset({ KIA_EV6_GT_LINE_LONG_TUNING_VDS_PREFIXES = frozenset({ "C4DLC", }) +KIA_EV6_GT_LINE_LONG_TUNING_TESTING_GROUND_ID = "5" ALT_BUS_LDA_BUTTON_CARS = frozenset() @@ -985,9 +986,9 @@ def hyundai_cancel_button_enables_cruise(car_fingerprint) -> bool: return car_fingerprint in CANCEL_BUTTON_ENABLE_CARS -def kia_ev6_gt_line_longitudinal_tuning(car_fingerprint, vin: str) -> bool: - return car_fingerprint == CAR.KIA_EV6 and isinstance(vin, str) and \ - len(vin) == 17 and vin[3:8] in KIA_EV6_GT_LINE_LONG_TUNING_VDS_PREFIXES +def kia_ev6_gt_line_longitudinal_tuning(car_fingerprint, vin: str, testing_ground_active: bool = False) -> bool: + vin_match = isinstance(vin, str) and len(vin) == 17 and vin[3:8] in KIA_EV6_GT_LINE_LONG_TUNING_VDS_PREFIXES + return car_fingerprint == CAR.KIA_EV6 and (vin_match or testing_ground_active) def get_platform_codes(fw_versions: list[bytes]) -> set[tuple[bytes, bytes | None]]: diff --git a/selfdrive/assets/.gitignore b/selfdrive/assets/.gitignore index 2d97f8b11..342359088 100644 --- a/selfdrive/assets/.gitignore +++ b/selfdrive/assets/.gitignore @@ -1,2 +1,4 @@ fonts/*.fnt fonts/*.png +!fonts/como-heavy.fnt +!fonts/como-heavy.png diff --git a/selfdrive/assets/fonts/como-heavy.fnt b/selfdrive/assets/fonts/como-heavy.fnt new file mode 100644 index 000000000..018abbb1b --- /dev/null +++ b/selfdrive/assets/fonts/como-heavy.fnt @@ -0,0 +1,17 @@ +info face="como-heavy" size=-200 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0 +common lineHeight=200 base=200 scaleW=1024 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4 +page id=0 file="como-heavy.png" +chars count=13 +char id=32 x=6 y=6 width=39 height=200 xoffset=0 yoffset=0 xadvance=39 page=0 chnl=15 +char id=63 x=57 y=6 width=81 height=115 xoffset=0 yoffset=47 xadvance=83 page=0 chnl=15 +char id=80 x=150 y=6 width=100 height=113 xoffset=8 yoffset=49 xadvance=110 page=0 chnl=15 +char id=83 x=262 y=6 width=101 height=116 xoffset=4 yoffset=47 xadvance=108 page=0 chnl=15 +char id=97 x=375 y=6 width=88 height=85 xoffset=5 yoffset=78 xadvance=100 page=0 chnl=15 +char id=105 x=475 y=6 width=31 height=121 xoffset=6 yoffset=41 xadvance=43 page=0 chnl=15 +char id=108 x=518 y=6 width=29 height=119 xoffset=7 yoffset=43 xadvance=43 page=0 chnl=15 +char id=111 x=559 y=6 width=88 height=85 xoffset=5 yoffset=78 xadvance=98 page=0 chnl=15 +char id=112 x=659 y=6 width=88 height=115 xoffset=7 yoffset=78 xadvance=100 page=0 chnl=15 +char id=114 x=759 y=6 width=63 height=83 xoffset=7 yoffset=79 xadvance=69 page=0 chnl=15 +char id=115 x=834 y=6 width=77 height=85 xoffset=4 yoffset=78 xadvance=84 page=0 chnl=15 +char id=116 x=923 y=6 width=63 height=111 xoffset=6 yoffset=52 xadvance=70 page=0 chnl=15 +char id=122 x=6 y=218 width=79 height=81 xoffset=3 yoffset=80 xadvance=85 page=0 chnl=15 diff --git a/selfdrive/assets/fonts/como-heavy.otf b/selfdrive/assets/fonts/como-heavy.otf new file mode 100644 index 000000000..426cb86ae Binary files /dev/null and b/selfdrive/assets/fonts/como-heavy.otf differ diff --git a/selfdrive/assets/fonts/como-heavy.png b/selfdrive/assets/fonts/como-heavy.png new file mode 100644 index 000000000..28b9b3584 Binary files /dev/null and b/selfdrive/assets/fonts/como-heavy.png differ diff --git a/selfdrive/assets/fonts/process.py b/selfdrive/assets/fonts/process.py index 2a08ff150..e8f31ad16 100644 --- a/selfdrive/assets/fonts/process.py +++ b/selfdrive/assets/fonts/process.py @@ -12,6 +12,7 @@ LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json" GLYPH_PADDING = 6 EXTRA_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥²⚠ⓘ" UNIFONT_LANGUAGES = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"} +BRAND_FONT_CHARS = " StarPilotstarpilot?z" def _languages(): @@ -127,7 +128,10 @@ def main(): for font in fonts: if "emoji" in font.name.lower(): continue - glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp + if font.name == "como-heavy.otf": + glyphs = tuple(sorted({ord(c) for c in BRAND_FONT_CHARS})) + else: + glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp _process_font(font, glyphs) return 0 diff --git a/selfdrive/controls/lib/longitudinal_vehicle_tunes.py b/selfdrive/controls/lib/longitudinal_vehicle_tunes.py index 9a3f866fd..e110dea73 100644 --- a/selfdrive/controls/lib/longitudinal_vehicle_tunes.py +++ b/selfdrive/controls/lib/longitudinal_vehicle_tunes.py @@ -34,6 +34,8 @@ TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MAX_DISTANCE = 100.0 TOYOTA_RAV4_TSS2_RADAR_FOLLOW_DISTANCE_TIME = 4.5 TOYOTA_RAV4_TSS2_RADAR_FOLLOW_DISTANCE_OFFSET = 32.0 TOYOTA_RAV4_TSS2_RADAR_FOLLOW_MAX_LATERAL_OFFSET = 1.75 +TOYOTA_RAV4_TSS2_FAR_FOLLOW_BRAKE_SLEW_RATE = 2.5 +TOYOTA_RAV4_TSS2_FAR_FOLLOW_RELEASE_SLEW_RATE = 1.75 TOYOTA_CAMRY_TSS2_FORCE_STOP_HANDOFF_M = 4.5 # The Camry's force-stop path otherwise consumes the model endpoint before the # normal MPC stop-distance margin can be applied. Keep it within the forward @@ -132,6 +134,11 @@ def get_far_follow_output_slew_rates(CP): HONDA_HRV_3G_FAR_FOLLOW_BRAKE_SLEW_RATE, HONDA_HRV_3G_FAR_FOLLOW_RELEASE_SLEW_RATE, ) + if is_toyota_rav4_tss2_post_departure_tune(CP): + return ( + TOYOTA_RAV4_TSS2_FAR_FOLLOW_BRAKE_SLEW_RATE, + TOYOTA_RAV4_TSS2_FAR_FOLLOW_RELEASE_SLEW_RATE, + ) return 0.0, 0.0 diff --git a/selfdrive/controls/tests/test_longitudinal_planner.py b/selfdrive/controls/tests/test_longitudinal_planner.py index e17dd75ab..6cd533bf8 100644 --- a/selfdrive/controls/tests/test_longitudinal_planner.py +++ b/selfdrive/controls/tests/test_longitudinal_planner.py @@ -25,6 +25,7 @@ from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import ( from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from openpilot.selfdrive.controls.lib.longitudinal_vehicle_tunes import ( allow_radar_standstill_gap_settle, + get_far_follow_output_slew_rates, get_follow_prebrake_min_headway, get_toyota_rav4_tss2_early_lead_cap, get_toyota_sienna_post_departure_restop_cap, @@ -239,6 +240,28 @@ def test_non_hrv_has_no_vehicle_far_follow_output_slew(): assert target == pytest.approx(-1.0) +def test_rav4_far_follow_output_slew_damps_vision_lead_chatter(): + v_ego = 24.0 + CP = ToyotaCarInterface.get_non_essential_params(TOYOTA_CAR.TOYOTA_RAV4_TSS2_2023) + planner = LongitudinalPlanner(CP, init_v=v_ego) + planner.lead_one = make_lead(status=True, d_rel=58.0, v_lead=20.0, model_prob=0.99) + planner.lead_two = make_lead(status=False) + + brake_rate, release_rate = get_far_follow_output_slew_rates(CP) + assert brake_rate > 0.0 + assert release_rate > 0.0 + + initial = planner.get_vehicle_far_follow_slew_target( + v_ego, prev_target=0.0, target=-0.6, output_should_stop=False, panic_bypass=False, + ) + smoothed = planner.get_vehicle_far_follow_slew_target( + v_ego, prev_target=initial, target=0.4, output_should_stop=False, panic_bypass=False, + ) + + assert initial == pytest.approx(-0.6) + assert smoothed == pytest.approx(initial + release_rate * planner.dt) + + def test_depart_release_hold_rejects_nearby_stopped_lead_conflict(): CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC) planner = LongitudinalPlanner(CP) diff --git a/selfdrive/controls/tests/test_starpilot_acceleration.py b/selfdrive/controls/tests/test_starpilot_acceleration.py index e29fa451e..23836a5ec 100644 --- a/selfdrive/controls/tests/test_starpilot_acceleration.py +++ b/selfdrive/controls/tests/test_starpilot_acceleration.py @@ -1,3 +1,4 @@ +import math from types import SimpleNamespace import pytest @@ -8,6 +9,7 @@ from openpilot.starpilot.common.accel_profile import A_CRUISE_MAX_BP_CUSTOM, ACC from openpilot.starpilot.controls.lib.starpilot_acceleration import ( A_CRUISE_MIN_ECO, A_CRUISE_MIN_TRAFFIC, + PULSE_GLIDE_COAST_MIN_ACCEL, StarPilotAcceleration, get_max_accel_eco, get_max_accel_standard, @@ -55,9 +57,11 @@ def make_lead(status=False, d_rel=150.0, v_lead=0.0, a_lead_k=0.0): def make_sm(*, set_speed_kph=100.0, lead_one=None, lead_two=None, standstill=False, force_decel=False, - eco_gear=False, sport_gear=False, force_coast=False, pulse_and_glide=False, traffic_mode=False, v_ego_cluster=0.0): + eco_gear=False, sport_gear=False, force_coast=False, pulse_and_glide=False, traffic_mode=False, + v_ego_cluster=0.0, pitch=0.0): return { "carState": SimpleNamespace(vCruise=set_speed_kph, standstill=standstill, vEgoCluster=v_ego_cluster), + "carControl": SimpleNamespace(orientationNED=[0.0, pitch, 0.0]), "controlsState": SimpleNamespace(forceDecel=force_decel), "radarState": SimpleNamespace( leadOne=lead_one or make_lead(), @@ -222,7 +226,7 @@ def test_pulse_and_glide_coasts_at_set_speed_then_resumes_below_delta(): accel.update(set_speed, make_sm(set_speed_kph=100.0, pulse_and_glide=True), toggles) assert accel.pulse_glide_coasting is True assert accel.pulse_glide_target == pytest.approx(set_speed - delta) - assert accel.min_accel == pytest.approx(A_CRUISE_MIN_ECO) + assert accel.min_accel == pytest.approx(PULSE_GLIDE_COAST_MIN_ACCEL) accel.update((90.0 * CV.KPH_TO_MS) - 0.05, make_sm(set_speed_kph=100.0, pulse_and_glide=True), toggles) assert accel.pulse_glide_coasting is False @@ -232,7 +236,23 @@ def test_pulse_and_glide_coasts_at_set_speed_then_resumes_below_delta(): accel.update(99.8 * CV.KPH_TO_MS, make_sm(set_speed_kph=100.0, pulse_and_glide=True), toggles) assert accel.pulse_glide_coasting is True assert accel.pulse_glide_target == pytest.approx(set_speed - delta) - assert accel.min_accel == pytest.approx(A_CRUISE_MIN_ECO) + assert accel.min_accel == pytest.approx(PULSE_GLIDE_COAST_MIN_ACCEL) + + +def test_pulse_and_glide_pauses_on_steep_grade_then_resumes(): + set_speed = 100.0 * CV.KPH_TO_MS + delta = 10.0 * CV.KPH_TO_MS + accel = StarPilotAcceleration(FakePlanner(v_cruise=set_speed)) + toggles = make_toggles(pulse_glide_speed_delta=delta) + + accel.update(set_speed, make_sm(set_speed_kph=100.0, pulse_and_glide=True, pitch=math.radians(4.0)), toggles) + assert accel.pulse_glide_coasting is False + assert accel.pulse_glide_hill_paused is True + + accel.update(set_speed, make_sm(set_speed_kph=100.0, pulse_and_glide=True, pitch=math.radians(2.0)), toggles) + assert accel.pulse_glide_coasting is True + assert accel.pulse_glide_hill_paused is False + assert accel.min_accel == pytest.approx(PULSE_GLIDE_COAST_MIN_ACCEL) def test_pulse_and_glide_is_inert_when_disabled(): diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py index bac83f2b7..5037b8a57 100644 --- a/selfdrive/ui/layouts/home.py +++ b/selfdrive/ui/layouts/home.py @@ -181,12 +181,31 @@ class HomeLayout(Widget): version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y, version_text_width, self.header_rect.height) + brand_text = "StarPilot" + detail_text = self._version_text.removeprefix(brand_text) + brand_font = gui_app.font(FontWeight.BRAND) version_font_size = 48 - version_text_size = measure_text_cached(font, self._version_text, version_font_size) - if version_text_size.x > version_rect.width: - version_font_size = max(32, int(version_font_size * version_rect.width / version_text_size.x)) - gui_label(version_rect, self._version_text, version_font_size, rl.WHITE, font_weight=FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) + + def _measure_header(font_size: int) -> tuple[rl.Vector2, rl.Vector2]: + return (measure_text_cached(brand_font, brand_text, font_size + 2), + measure_text_cached(font, detail_text, font_size)) + + brand_size, detail_size = _measure_header(version_font_size) + total_width = brand_size.x + detail_size.x + if total_width > version_rect.width: + version_font_size = max(32, int(version_font_size * version_rect.width / total_width)) + brand_size, detail_size = _measure_header(version_font_size) + total_width = brand_size.x + detail_size.x + + rendered_width = min(total_width, version_rect.width) + text_x = version_rect.x + version_rect.width - rendered_width + brand_rect = rl.Rectangle(text_x, version_rect.y, min(brand_size.x, rendered_width), version_rect.height) + gui_label(brand_rect, brand_text, version_font_size + 2, rl.WHITE, font_weight=FontWeight.BRAND, elide_right=False) + + detail_width = max(0.0, rendered_width - brand_rect.width) + if detail_text and detail_width > 0: + detail_rect = rl.Rectangle(brand_rect.x + brand_rect.width, version_rect.y, detail_width, version_rect.height) + gui_label(detail_rect, detail_text, version_font_size, rl.WHITE, font_weight=FontWeight.MEDIUM) def _render_home_content(self): self._render_left_column() diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index dbe126734..3b3ca354a 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -173,7 +173,7 @@ class MiciHomeLayout(Widget): self._mic_icon, ], spacing=18) - self._openpilot_label = UnifiedLabel("starpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False) + self._openpilot_label = UnifiedLabel("StarPilot", font_size=96, font_weight=FontWeight.BRAND, max_width=480, wrap_text=False) self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) diff --git a/selfdrive/ui/onroad/starpilot/pulse_glide.py b/selfdrive/ui/onroad/starpilot/pulse_glide.py new file mode 100644 index 000000000..fe90fc023 --- /dev/null +++ b/selfdrive/ui/onroad/starpilot/pulse_glide.py @@ -0,0 +1,27 @@ +import pyray as rl + +from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.text_measure import draw_text_with_shadow, measure_text_cached + + +PULSE_COLOR = rl.Color(52, 190, 112, 255) +GLIDE_COLOR = rl.Color(65, 155, 235, 255) + + +def render_pulse_glide(rect: rl.Rectangle, coasting: bool) -> None: + """Render the developer-only P&G phase badge beside the standard HUD badges.""" + border = GLIDE_COLOR if coasting else PULSE_COLOR + label = "GLIDE" if coasting else "PULSE" + font = gui_app.font(FontWeight.BOLD) + font_size = 27 + text_size = measure_text_cached(font, label, font_size) + + rl.draw_rectangle_rounded(rect, 0.3, 10, rl.Color(0, 0, 0, 166)) + rl.draw_rectangle_rounded_lines_ex(rect, 0.3, 10, 4, border) + draw_text_with_shadow( + font, + label, + rl.Vector2(rect.x + (rect.width - text_size.x) / 2, rect.y + (rect.height - text_size.y) / 2), + font_size, + rl.WHITE, + ) diff --git a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py index 5aca1c42b..0125c3cf6 100644 --- a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py +++ b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py @@ -14,6 +14,7 @@ from openpilot.selfdrive.ui.onroad.starpilot.widgets import ( ) from openpilot.selfdrive.ui.onroad.starpilot.stopping_point import render_stopping_point from openpilot.selfdrive.ui.onroad.starpilot.pause_indicators import render_lateral_paused, render_longitudinal_paused +from openpilot.selfdrive.ui.onroad.starpilot.pulse_glide import render_pulse_glide from openpilot.selfdrive.ui.onroad.starpilot.pip_sidecam import PipSideCamera from openpilot.selfdrive.ui.onroad.starpilot.favorite_radial_menu import FavoriteRadialMenu from openpilot.selfdrive.ui.onroad.starpilot.weather_icon import render_weather_icon @@ -342,6 +343,7 @@ class StarPilotOnroadView(AugmentedRoadView): # Check pause/CEM states starpilot_car_state = ui_state.sm["starpilotCarState"] if ui_state.sm.valid.get("starpilotCarState", False) else None + plan = ui_state.sm["starpilotPlan"] if ui_state.sm.valid.get("starpilotPlan", False) else None lateral_paused = starpilot_car_state.pauseLateral if starpilot_car_state else False longitudinal_paused = (starpilot_car_state.pauseLongitudinal or starpilot_car_state.forceCoast) if starpilot_car_state else False @@ -352,6 +354,8 @@ class StarPilotOnroadView(AugmentedRoadView): active_badges.append("lateral_paused") if longitudinal_paused: active_badges.append("longitudinal_paused") + if starpilot_car_state and starpilot_car_state.pulseAndGlide: + active_badges.append("pulse_glide") # Dimensions badge_w = 120 @@ -377,9 +381,11 @@ class StarPilotOnroadView(AugmentedRoadView): render_lateral_paused(badge_rect) elif badge == "longitudinal_paused": render_longitudinal_paused(badge_rect) + elif badge == "pulse_glide": + pulse_glide_coasting = bool(getattr(plan, "pulseGlideCoasting", False)) if plan else False + render_pulse_glide(badge_rect, pulse_glide_coasting) # 2. Render Weather (on the opposite side of DM icon) - plan = ui_state.sm["starpilotPlan"] if ui_state.sm.valid.get("starpilotPlan", False) else None if plan and plan.weatherId != 0: weather_w = 120 weather_h = 120 diff --git a/starpilot/common/starpilot_functions.py b/starpilot/common/starpilot_functions.py index 0d6724680..603d398c3 100644 --- a/starpilot/common/starpilot_functions.py +++ b/starpilot/common/starpilot_functions.py @@ -2,6 +2,7 @@ import dataclasses import json import requests +import tempfile import threading import time @@ -34,6 +35,10 @@ from openpilot.starpilot.common.starpilot_variables import ( StarPilotVariables, get_starpilot_toggles ) +BOOT_LOGO_JPEG_PATH = Path("/usr/comma/bg.jpg") +BOOT_LOGO_PNG_PATH = Path("/usr/comma/bg.png") +BOOT_LOGO_MAGIC_PATH = Path("/usr/comma/magic.py") + def seed_desktop_theme_assets(): params = Params() @@ -163,8 +168,6 @@ def update_boot_logo(starpilot=False, stock=False, selected_logo=None): if HARDWARE.get_device_type() == "pc": return - boot_logo_location = Path("/usr/comma/bg.jpg") - if starpilot: target_logo = Path(BASEDIR) / "starpilot/assets/other_images/starpilot_boot_logo.jpg" if selected_logo: @@ -184,30 +187,57 @@ def update_boot_logo(starpilot=False, stock=False, selected_logo=None): print(f"Error: Target logo file not found at {target_logo}") return - source_logo = target_logo - staged_logo = Path("/tmp/starpilot_boot_logo.jpg") try: from PIL import Image - with Image.open(target_logo) as img: - # weston.service always writes a JPEG copy of /usr/comma/bg.jpg; make sure - # the source image is already RGB JPEG to avoid startup failure on RGBA assets. - if img.format != "JPEG" or img.mode != "RGB": - img.convert("RGB").save(staged_logo, format="JPEG", quality=95) - source_logo = staged_logo + with tempfile.TemporaryDirectory(prefix="starpilot_boot_logo_") as staging_dir: + staging_path = Path(staging_dir) + staged_jpeg = staging_path / "bg.jpg" + staged_png = staging_path / "bg.png" + + with Image.open(target_logo) as img: + normalized_logo = img.convert("RGB") + if normalized_logo.width >= normalized_logo.height: + landscape_logo = normalized_logo + weston_logo = normalized_logo.transpose(Image.Transpose.ROTATE_270) + else: + weston_logo = normalized_logo + landscape_logo = normalized_logo.transpose(Image.Transpose.ROTATE_90) + + magic_uses_jpeg = False + try: + magic_uses_jpeg = BOOT_LOGO_JPEG_PATH.as_posix() in BOOT_LOGO_MAGIC_PATH.read_text() + except OSError: + pass + + (landscape_logo if magic_uses_jpeg else weston_logo).save(staged_jpeg, format="JPEG", quality=95) + landscape_logo.save(staged_png, format="PNG") + + logo_variants = [(staged_jpeg, BOOT_LOGO_JPEG_PATH)] + if BOOT_LOGO_PNG_PATH.is_file(): + logo_variants.append((staged_png, BOOT_LOGO_PNG_PATH)) + + pending_updates = [ + (source, destination) + for source, destination in logo_variants + if not destination.is_file() or destination.read_bytes() != source.read_bytes() + ] + if not pending_updates: + return + + mount_options = run_cmd(["findmnt", "-n", "-o", "OPTIONS", "/"], "Successfully retrieved mount options", "Failed to retrieve mount options") + if mount_options is None: + return + if run_cmd(["sudo", "mount", "-o", "remount,rw", "/"], "Successfully remounted / as read-write", "Failed to remount /") is None: + return + + try: + for source, destination in pending_updates: + run_cmd(["sudo", "cp", source, destination], f"Successfully replaced boot logo at {destination}", f"Failed to replace boot logo at {destination}") + finally: + run_cmd(["sudo", "mount", "-o", f"remount,{mount_options}", "/"], "Successfully restored / mount options", "Failed to restore / mount options") except Exception as error: print(f"Error normalizing boot logo {target_logo}: {error}") - if target_logo.suffix.lower() not in {".jpg", ".jpeg"}: - print("Skipping boot logo update to keep weston startup stable.") - return - - current_logo = boot_logo_location.read_bytes() if boot_logo_location.is_file() else b"" - desired_logo = source_logo.read_bytes() - if current_logo != desired_logo: - mount_options = run_cmd(["findmnt", "-n", "-o", "OPTIONS", "/"], "Successfully retrieved mount options", "Failed to retrieve mount options") - run_cmd(["sudo", "mount", "-o", "remount,rw", "/"], "Successfully remounted / as read-write", "Failed to remount /") - run_cmd(["sudo", "cp", source_logo, boot_logo_location], "Successfully replaced boot logo", "Failed to replace boot logo") - run_cmd(["sudo", "mount", "-o", f"remount,{mount_options}", "/"], "Successfully restored / mount options", "Failed to restore / mount options") MAPS_DOWNLOAD_PROGRESS_PARAM = "MapsDownloadProgress" diff --git a/starpilot/common/testing_grounds.py b/starpilot/common/testing_grounds.py index de8f65ee1..8047cb1dd 100644 --- a/starpilot/common/testing_grounds.py +++ b/starpilot/common/testing_grounds.py @@ -63,9 +63,10 @@ TESTING_GROUNDS_SLOT_DEFINITIONS = ( }, { "id": TESTING_GROUND_5, - "name": "Unused", - "description": "Unused slot.", + "name": "EV6 GT-Line Long", + "description": "Kia EV6 GT-Line longitudinal tuning sandbox.", "aLabel": "A - Installed tune", + "bLabel": "B - EV6 GT-Line long tune", }, { "id": TESTING_GROUND_6, diff --git a/starpilot/common/tests/test_starpilot_functions.py b/starpilot/common/tests/test_starpilot_functions.py index 39025b7f5..8e19f9896 100644 --- a/starpilot/common/tests/test_starpilot_functions.py +++ b/starpilot/common/tests/test_starpilot_functions.py @@ -1,3 +1,8 @@ +import shutil + +import pytest +from PIL import Image + from openpilot.starpilot.common import connect_server as cs from openpilot.starpilot.common import starpilot_functions as sf @@ -27,6 +32,157 @@ class FakeThreadManager: return False +@pytest.mark.parametrize("extension,image_format", [("jpg", "JPEG"), ("png", "PNG")]) +def test_update_boot_logo_writes_agnos_jpeg_and_png(monkeypatch, tmp_path, extension, image_format): + themes_path = tmp_path / "themes" + custom_logo = themes_path / "bootlogos" / f"custom.{extension}" + custom_logo.parent.mkdir(parents=True) + Image.new("RGBA" if image_format == "PNG" else "RGB", (24, 12), (12, 34, 56, 255)).save(custom_logo, format=image_format) + + jpeg_destination = tmp_path / "usr" / "comma" / "bg.jpg" + png_destination = tmp_path / "usr" / "comma" / "bg.png" + jpeg_destination.parent.mkdir(parents=True) + jpeg_destination.write_bytes(b"old jpeg") + png_destination.write_bytes(b"old png") + + commands = [] + + def fake_run_cmd(command, *_args, **_kwargs): + commands.append(command) + if command[0] == "findmnt": + return "ro,relatime" + if command[:2] == ["sudo", "cp"]: + shutil.copy2(command[2], command[3]) + return "" + + monkeypatch.setattr(sf.HARDWARE, "get_device_type", lambda: "mici") + monkeypatch.setattr(sf, "THEME_SAVE_PATH", themes_path) + monkeypatch.setattr(sf, "BOOT_LOGO_JPEG_PATH", jpeg_destination) + monkeypatch.setattr(sf, "BOOT_LOGO_PNG_PATH", png_destination) + monkeypatch.setattr(sf, "BOOT_LOGO_MAGIC_PATH", tmp_path / "usr" / "comma" / "magic.py") + monkeypatch.setattr(sf, "run_cmd", fake_run_cmd) + + sf.update_boot_logo(starpilot=True, selected_logo="custom") + + with Image.open(jpeg_destination) as jpeg_logo: + assert jpeg_logo.format == "JPEG" + assert jpeg_logo.mode == "RGB" + assert jpeg_logo.size == (12, 24) + with Image.open(png_destination) as png_logo: + assert png_logo.format == "PNG" + assert png_logo.mode == "RGB" + assert png_logo.size == (24, 12) + + assert [command for command in commands if command[:2] == ["sudo", "mount"]] == [ + ["sudo", "mount", "-o", "remount,rw", "/"], + ["sudo", "mount", "-o", "remount,ro,relatime", "/"], + ] + assert len([command for command in commands if command[:2] == ["sudo", "cp"]]) == 2 + + +def test_update_boot_logo_does_not_create_png_on_legacy_agnos(monkeypatch, tmp_path): + themes_path = tmp_path / "themes" + custom_logo = themes_path / "bootlogos" / "custom.png" + custom_logo.parent.mkdir(parents=True) + Image.new("RGB", (24, 12), (12, 34, 56)).save(custom_logo, format="PNG") + + jpeg_destination = tmp_path / "usr" / "comma" / "bg.jpg" + png_destination = tmp_path / "usr" / "comma" / "bg.png" + jpeg_destination.parent.mkdir(parents=True) + jpeg_destination.write_bytes(b"old jpeg") + + def fake_run_cmd(command, *_args, **_kwargs): + if command[0] == "findmnt": + return "ro,relatime" + if command[:2] == ["sudo", "cp"]: + shutil.copy2(command[2], command[3]) + return "" + + monkeypatch.setattr(sf.HARDWARE, "get_device_type", lambda: "tici") + monkeypatch.setattr(sf, "THEME_SAVE_PATH", themes_path) + monkeypatch.setattr(sf, "BOOT_LOGO_JPEG_PATH", jpeg_destination) + monkeypatch.setattr(sf, "BOOT_LOGO_PNG_PATH", png_destination) + monkeypatch.setattr(sf, "BOOT_LOGO_MAGIC_PATH", tmp_path / "usr" / "comma" / "magic.py") + monkeypatch.setattr(sf, "run_cmd", fake_run_cmd) + + sf.update_boot_logo(starpilot=True, selected_logo="custom") + + with Image.open(jpeg_destination) as jpeg_logo: + assert jpeg_logo.format == "JPEG" + assert jpeg_logo.size == (12, 24) + assert not png_destination.exists() + + +def test_update_boot_logo_rotates_legacy_stock_for_raylib(monkeypatch, tmp_path): + stock_logo = tmp_path / "starpilot" / "assets" / "other_images" / "stock_bg.jpg" + stock_logo.parent.mkdir(parents=True) + Image.new("RGB", (12, 24), (12, 34, 56)).save(stock_logo, format="JPEG") + + comma_path = tmp_path / "usr" / "comma" + jpeg_destination = comma_path / "bg.jpg" + png_destination = comma_path / "bg.png" + comma_path.mkdir(parents=True) + jpeg_destination.write_bytes(b"old jpeg") + png_destination.write_bytes(b"old png") + + def fake_run_cmd(command, *_args, **_kwargs): + if command[0] == "findmnt": + return "ro,relatime" + if command[:2] == ["sudo", "cp"]: + shutil.copy2(command[2], command[3]) + return "" + + monkeypatch.setattr(sf.HARDWARE, "get_device_type", lambda: "mici") + monkeypatch.setattr(sf, "BASEDIR", str(tmp_path)) + monkeypatch.setattr(sf, "BOOT_LOGO_JPEG_PATH", jpeg_destination) + monkeypatch.setattr(sf, "BOOT_LOGO_PNG_PATH", png_destination) + monkeypatch.setattr(sf, "BOOT_LOGO_MAGIC_PATH", comma_path / "magic.py") + monkeypatch.setattr(sf, "run_cmd", fake_run_cmd) + + sf.update_boot_logo(stock=True) + + with Image.open(jpeg_destination) as jpeg_logo: + assert jpeg_logo.size == (12, 24) + with Image.open(png_destination) as png_logo: + assert png_logo.size == (24, 12) + + +def test_update_boot_logo_uses_landscape_jpeg_for_current_magic(monkeypatch, tmp_path): + themes_path = tmp_path / "themes" + custom_logo = themes_path / "bootlogos" / "custom.png" + custom_logo.parent.mkdir(parents=True) + Image.new("RGB", (12, 24), (12, 34, 56)).save(custom_logo, format="PNG") + + comma_path = tmp_path / "usr" / "comma" + jpeg_destination = comma_path / "bg.jpg" + png_destination = comma_path / "bg.png" + magic_path = comma_path / "magic.py" + comma_path.mkdir(parents=True) + jpeg_destination.write_bytes(b"old jpeg") + magic_path.write_text(f'BACKGROUND = "{jpeg_destination.as_posix()}"\n') + + def fake_run_cmd(command, *_args, **_kwargs): + if command[0] == "findmnt": + return "ro,relatime" + if command[:2] == ["sudo", "cp"]: + shutil.copy2(command[2], command[3]) + return "" + + monkeypatch.setattr(sf.HARDWARE, "get_device_type", lambda: "mici") + monkeypatch.setattr(sf, "THEME_SAVE_PATH", themes_path) + monkeypatch.setattr(sf, "BOOT_LOGO_JPEG_PATH", jpeg_destination) + monkeypatch.setattr(sf, "BOOT_LOGO_PNG_PATH", png_destination) + monkeypatch.setattr(sf, "BOOT_LOGO_MAGIC_PATH", magic_path) + monkeypatch.setattr(sf, "run_cmd", fake_run_cmd) + + sf.update_boot_logo(starpilot=True, selected_logo="custom") + + with Image.open(jpeg_destination) as jpeg_logo: + assert jpeg_logo.format == "JPEG" + assert jpeg_logo.size == (24, 12) + assert not png_destination.exists() + + def test_automatic_update_requests_guarded_reboot(monkeypatch): params = FakeParams({ "UpdaterState": "idle", diff --git a/starpilot/common/tests/test_testing_grounds.py b/starpilot/common/tests/test_testing_grounds.py index db4447e06..d457f5626 100644 --- a/starpilot/common/tests/test_testing_grounds.py +++ b/starpilot/common/tests/test_testing_grounds.py @@ -1,23 +1,21 @@ import json -import pytest - from openpilot.starpilot.common import testing_grounds as tg -@pytest.mark.parametrize("hidden_slot_id", [tg.TESTING_GROUND_5]) -def test_hidden_testing_ground_selection_is_migrated(tmp_path, monkeypatch, hidden_slot_id): +def test_invalid_testing_ground_selection_is_migrated(tmp_path, monkeypatch): + invalid_slot_id = "99" state_path = tmp_path / "slots.json" state_path.write_text(json.dumps({ "schemaVersion": tg.TESTING_GROUNDS_SCHEMA_VERSION, - "activeSlot": hidden_slot_id, + "activeSlot": invalid_slot_id, "activeVariant": tg.TESTING_GROUND_TEST_VARIANT, }), encoding="utf-8") monkeypatch.setattr(tg, "TESTING_GROUNDS_STATE_PATH", state_path) monkeypatch.setattr(tg, "_CACHE_LAST_REFRESH", 0.0) monkeypatch.setattr(tg, "_CACHE_LAST_MTIME_NS", -1) - monkeypatch.setattr(tg, "_VISIBLE_TESTING_GROUND_IDS", tuple(slot_id for slot_id in tg.TESTING_GROUND_IDS if slot_id != hidden_slot_id)) + monkeypatch.setattr(tg, "_VISIBLE_TESTING_GROUND_IDS", tuple(tg.TESTING_GROUND_IDS)) monkeypatch.setattr(tg, "_DEFAULT_ACTIVE_SLOT", tg.TESTING_GROUND_1) monkeypatch.setattr(tg, "_CACHE_ACTIVE_SLOT", tg._DEFAULT_ACTIVE_SLOT) monkeypatch.setattr(tg, "_CACHE_ACTIVE_VARIANT", tg.DEFAULT_TESTING_GROUND_VARIANT) @@ -32,11 +30,11 @@ def test_hidden_testing_ground_selection_is_migrated(tmp_path, monkeypatch, hidd assert payload["activeVariant"] == tg.DEFAULT_TESTING_GROUND_VARIANT -def test_hidden_slot_invalid_variant_is_migrated_off_slot(tmp_path, monkeypatch): +def test_invalid_slot_variant_is_migrated_off_slot(tmp_path, monkeypatch): state_path = tmp_path / "slots.json" state_path.write_text(json.dumps({ "schemaVersion": tg.TESTING_GROUNDS_SCHEMA_VERSION, - "activeSlot": tg.TESTING_GROUND_5, + "activeSlot": "99", "activeVariant": "C", }), encoding="utf-8") diff --git a/starpilot/controls/lib/starpilot_acceleration.py b/starpilot/controls/lib/starpilot_acceleration.py index d359a08df..f3c4ee05f 100644 --- a/starpilot/controls/lib/starpilot_acceleration.py +++ b/starpilot/controls/lib/starpilot_acceleration.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +import math + import numpy as np from openpilot.common.constants import CV @@ -79,6 +81,9 @@ RELEVANT_LEAD_MIN_BRAKE = -0.4 PULSE_GLIDE_MIN_TARGET_SPEED = 5.0 PULSE_GLIDE_MIN_LOWER_SPEED = 3.0 PULSE_GLIDE_HYSTERESIS = 0.25 +PULSE_GLIDE_COAST_MIN_ACCEL = -0.03 +PULSE_GLIDE_HILL_ENTER_PITCH = math.radians(3.0) +PULSE_GLIDE_HILL_EXIT_PITCH = math.radians(2.5) # Drive mode -> profile mapping used by the map_acceleration / map_deceleration toggles. GEAR_STATE_PROFILES = { @@ -153,11 +158,37 @@ class StarPilotAcceleration: self.last_gear_state = "init" self.pulse_glide_coasting = False self.pulse_glide_target = None + self.pulse_glide_hill_paused = False + + def _update_pulse_glide_hill_pause(self, sm): + try: + orientation_ned = sm["carControl"].orientationNED + if len(orientation_ned) < 2: + return self.pulse_glide_hill_paused + abs_pitch = abs(float(orientation_ned[1])) + except (KeyError, IndexError, TypeError, ValueError, AttributeError): + return self.pulse_glide_hill_paused + + if not math.isfinite(abs_pitch): + return self.pulse_glide_hill_paused + + if self.pulse_glide_hill_paused: + if abs_pitch <= PULSE_GLIDE_HILL_EXIT_PITCH: + self.pulse_glide_hill_paused = False + elif abs_pitch >= PULSE_GLIDE_HILL_ENTER_PITCH: + self.pulse_glide_hill_paused = True + + return self.pulse_glide_hill_paused def _update_pulse_glide(self, v_ego, sm, starpilot_toggles): self.pulse_glide_target = None pulse_glide_enabled = bool(getattr(sm["starpilotCarState"], "pulseAndGlide", False)) if not pulse_glide_enabled: + self.pulse_glide_coasting = False + self.pulse_glide_hill_paused = False + return False + + if self._update_pulse_glide_hill_pause(sm): self.pulse_glide_coasting = False return False @@ -254,8 +285,10 @@ class StarPilotAcceleration: self.max_accel -= self.max_accel * self.starpilot_planner.starpilot_weather.reduce_acceleration pulse_glide_coasting = self._update_pulse_glide(v_ego, sm, starpilot_toggles) - if sm["starpilotCarState"].forceCoast or pulse_glide_coasting: + if sm["starpilotCarState"].forceCoast: self.min_accel = A_CRUISE_MIN_ECO + elif pulse_glide_coasting: + self.min_accel = PULSE_GLIDE_COAST_MIN_ACCEL elif sm["starpilotCarState"].trafficModeEnabled: self.min_accel = A_CRUISE_MIN_TRAFFIC elif starpilot_toggles.map_deceleration and (eco_gear or sport_gear): diff --git a/starpilot/controls/starpilot_card.py b/starpilot/controls/starpilot_card.py index afd3a5a05..0d24a6401 100644 --- a/starpilot/controls/starpilot_card.py +++ b/starpilot/controls/starpilot_card.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from opendbc.car import structs from opendbc.car.chrysler.values import pacifica_hybrid_aol_requires_set_press from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, HyundaiFlags from opendbc.safety import ALTERNATIVE_EXPERIENCE @@ -276,6 +277,18 @@ class StarPilotCard: elif not self.distancePressed_previously: self.gap_counter = 0 + distance_released = not starpilotCarState.distancePressed and self.distancePressed_previously + has_distance_release = any( + self._button_type_raw(be) == int(ButtonType.gapAdjustCruise) and not be.pressed + for be in carState.buttonEvents + ) + if getattr(self.CP, "carFingerprint", None) == HYUNDAI_CAR.HYUNDAI_ELANTRA_HEV_2024 and \ + distance_released and not has_distance_release: + carState.buttonEvents = [ + *carState.buttonEvents, + structs.CarState.ButtonEvent(pressed=False, type=ButtonType.gapAdjustCruise), + ] + self.distancePressed_previously = starpilotCarState.distancePressed if not starpilotCarState.distancePressed and 1 <= self.gap_counter < self.long_press_threshold: diff --git a/starpilot/controls/starpilot_planner.py b/starpilot/controls/starpilot_planner.py index e9b37bc6e..650bdecb1 100644 --- a/starpilot/controls/starpilot_planner.py +++ b/starpilot/controls/starpilot_planner.py @@ -329,7 +329,11 @@ class StarPilotPlanner: starpilotPlan.cscTraining = self.starpilot_vcruise.csc.enable_training starpilotPlan.desiredFollowDistance = int(self.starpilot_following.desired_follow_distance) - starpilotPlan.disableThrottle = self.starpilot_following.disable_throttle + starpilotPlan.disableThrottle = ( + self.starpilot_following.disable_throttle or + self.starpilot_acceleration.pulse_glide_coasting + ) + starpilotPlan.pulseGlideCoasting = self.starpilot_acceleration.pulse_glide_coasting starpilotPlan.trackingLead = self.tracking_lead conditional_experimental_mode = False diff --git a/starpilot/controls/tests/test_starpilot_card.py b/starpilot/controls/tests/test_starpilot_card.py index 0666c0cae..c390c2273 100644 --- a/starpilot/controls/tests/test_starpilot_card.py +++ b/starpilot/controls/tests/test_starpilot_card.py @@ -219,6 +219,48 @@ def make_wrapped_button_event(button_type, pressed): return SimpleNamespace(type=SimpleNamespace(raw=int(button_type)), pressed=pressed) +@pytest.mark.parametrize( + ("car_fingerprint", "expect_normalized_release"), + ( + (spc.HYUNDAI_CAR.HYUNDAI_ELANTRA_HEV_2024, True), + (spc.HYUNDAI_CAR.HYUNDAI_ELANTRA_2024, False), + ), +) +def test_distance_release_normalization_is_limited_to_reported_elantra_hybrid( + monkeypatch, tmp_path, car_fingerprint, expect_normalized_release, +): + monkeypatch.setattr(spc, "Params", FakeParams) + monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False) + monkeypatch.setattr(spc, "ERROR_LOGS_PATH", tmp_path) + + card = spc.StarPilotCard( + SimpleNamespace(brand="hyundai", carFingerprint=car_fingerprint), + SimpleNamespace(alternativeExperience=0), + ) + toggles = make_toggles( + experimental_mode_via_distance=False, + bookmark_via_distance=False, + force_coast_via_distance=False, + pulse_and_glide_via_distance=False, + pause_lateral_via_distance=False, + pause_longitudinal_via_distance=False, + switchback_mode_via_distance=False, + ) + sm = make_sm() + starpilot_car_state = SimpleNamespace(distancePressed=True) + + card.update(make_car_state(), starpilot_car_state, sm, toggles) + + starpilot_car_state.distancePressed = False + car_state = make_car_state(button_events=[SimpleNamespace(type=spc.ButtonType.unknown, pressed=False)]) + card.update(car_state, starpilot_car_state, sm, toggles) + + assert any( + be.type == spc.ButtonType.gapAdjustCruise and not be.pressed + for be in car_state.buttonEvents + ) is expect_normalized_release + + def test_honda_lkas_button_can_toggle_always_on_lateral(monkeypatch, tmp_path): monkeypatch.setattr(spc, "Params", FakeParams) monkeypatch.setattr(spc, "is_FrogsGoMoo", lambda: False) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index cacc59869..1c13c4015 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -155,6 +155,7 @@ class FontWeight(StrEnum): BOLD = "Inter-Bold.fnt" SEMI_BOLD = "Inter-SemiBold.fnt" UNIFONT = "unifont.fnt" + BRAND = "como-heavy.fnt" # Small UI fonts DISPLAY_REGULAR = "Inter-Regular.fnt" @@ -165,6 +166,11 @@ class FontWeight(StrEnum): def font_fallback(font: rl.Font) -> rl.Font: """Fall back to unifont for languages that require it.""" if multilang.requires_unifont(): + try: + if font.texture.id == gui_app.font(FontWeight.BRAND).texture.id: + return font + except (AttributeError, KeyError): + pass return gui_app.font(FontWeight.UNIFONT) return font diff --git a/system/ui/lib/tests/test_application.py b/system/ui/lib/tests/test_application.py index d8e5e0458..511701737 100644 --- a/system/ui/lib/tests/test_application.py +++ b/system/ui/lib/tests/test_application.py @@ -1,3 +1,6 @@ +from importlib.resources import as_file +from types import SimpleNamespace + from openpilot.system.ui.lib import application @@ -35,3 +38,32 @@ def test_burn_in_shift_transitions_between_positions(monkeypatch): midpoint = app._burn_in_shift(109.0) assert midpoint == (-1.0, 0.0) assert app._burn_in_shift(110.0) == (-2.0, 0.0) + + +def test_brand_font_assets_include_wordmark_glyphs(): + with as_file(application.FONT_DIR.joinpath("como-heavy.fnt")) as font_path: + lines = font_path.read_text().splitlines() + + glyphs = {} + for line in lines: + if not line.startswith("char id="): + continue + fields = dict(field.split("=", 1) for field in line.split() if "=" in field) + glyphs[int(fields["id"])] = (int(fields["width"]), int(fields["height"])) + + for char in set("StarPilot"): + assert glyphs[ord(char)][0] > 0 + assert glyphs[ord(char)][1] > 0 + + +def test_brand_font_is_not_replaced_by_language_fallback(monkeypatch): + brand_font = SimpleNamespace(texture=SimpleNamespace(id=1)) + unifont = SimpleNamespace(texture=SimpleNamespace(id=2)) + monkeypatch.setattr(application.multilang, "requires_unifont", lambda: True) + monkeypatch.setattr(application.gui_app, "font", lambda weight: { + application.FontWeight.BRAND: brand_font, + application.FontWeight.UNIFONT: unifont, + }[weight]) + + assert application.font_fallback(brand_font) is brand_font + assert application.font_fallback(SimpleNamespace(texture=SimpleNamespace(id=3))) is unifont