This commit is contained in:
firestar5683
2026-08-21 13:21:20 -05:00
parent 05e9dfd5d0
commit f63ee3375a
27 changed files with 527 additions and 53 deletions
+1
View File
@@ -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 {
@@ -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)
@@ -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
@@ -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)
+4 -3
View File
@@ -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]]:
+2
View File
@@ -1,2 +1,4 @@
fonts/*.fnt
fonts/*.png
!fonts/como-heavy.fnt
!fonts/como-heavy.png
+17
View File
@@ -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
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+5 -1
View File
@@ -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
@@ -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
@@ -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)
@@ -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():
+24 -5
View File
@@ -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()
+1 -1
View File
@@ -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)
@@ -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,
)
@@ -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
+51 -21
View File
@@ -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"
+3 -2
View File
@@ -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,
@@ -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",
@@ -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")
@@ -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):
+13
View File
@@ -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:
+5 -1
View File
@@ -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
@@ -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)
+6
View File
@@ -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
+32
View File
@@ -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