Correct sign size/sign size + offset sizes

This commit is contained in:
firestarsdog
2026-11-11 15:55:00 -05:00
parent 267c2bb2ce
commit fda184225d
12 changed files with 202 additions and 25 deletions
@@ -28,6 +28,7 @@ from openpilot.starpilot.ui.onroad_dm import draw_monitor_art, monitor_bounds, m
from openpilot.starpilot.ui.onroad_stopped_timer import PREVIEW_DURATION_S
from openpilot.starpilot.ui.layout_preview_sidebar import SIDEBAR_WIDTH, render_sidebar
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile, default_font_directory
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
from openpilot.starpilot.ui.rainbow_path import RainbowPath
from openpilot.starpilot.ui.road_colors import edge_gradient, lane_color, path_mode, solid_gradient
from openpilot.starpilot.ui.onroad_customization import ROAD_COLORS
@@ -78,6 +79,7 @@ def sample_state(scene: str, document: dict) -> OnroadState:
from openpilot.starpilot.ui.weather import WeatherReading
state = OnroadState(weather=WeatherReading(21, 2, True), engaged=lateral or longitudinal, camera_available=False,
speed_mps=0.0 if scene in ("cem_lead", "stopped") else 22.0, cruise_kph=88.0,
slc_offset_schedule=OffsetSchedule((OffsetBand(0., None, 2.),)),
stopped_duration_s=PREVIEW_DURATION_S if scene == "stopped" else None,
speed_limit=SpeedLimitObservation(kind=ObservationKind.VALID, source="dashboard",
speed_limit_mps=22.0 if pending else 20.0,
+2
View File
@@ -23,6 +23,7 @@ from openpilot.starpilot.ui.traffic_status import TrafficDisplay
from openpilot.starpilot.ui.developer_preview import OnroadVisualPreview
from openpilot.starpilot.ui.navigation_state import NavigationDisplay
from openpilot.starpilot.ui.live_developer_sidebar import DeveloperMetric
from openpilot.starpilot.speed_limits.speed_domain import OffsetSchedule
@@ -244,6 +245,7 @@ class OnroadState:
weather: WeatherReading | None = None
ford_rear_fog_on: bool = False
ford_rear_fog_source_ns: int = 0
slc_offset_schedule: OffsetSchedule | None = None
def __post_init__(self) -> None:
if self.speed_mps is not None and (not math.isfinite(self.speed_mps) or self.speed_mps < 0):
+10 -1
View File
@@ -55,6 +55,8 @@ from openpilot.starpilot.ui.settings_state import Destination, DestinationAvaila
from openpilot.starpilot.ui.shell import ShellMode, ShellSnapshot
from openpilot.starpilot.ui.software_state import DownloadLabel, SoftwareRequest, SoftwareState
from openpilot.starpilot.ui.toggles_state import Personality, ToggleKey, TogglesState
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.speed_limits import offset_document
SLC_SIGN_HOLD_NS = 150_000_000
@@ -314,6 +316,7 @@ class RuntimeSnapshotAdapter:
self._customization_value = default_document()
self._appearance_value = OnroadAppearance()
self._appearance_read_ns: int | None = None
self._slc_offset_schedule = None
# A second screen (Android Auto) swaps in its own display choices before anything uses them.
self.appearance_override: Callable[[OnroadAppearance], OnroadAppearance] | None = None
self._last_mode: ShellMode | None = None
@@ -442,6 +445,12 @@ class RuntimeSnapshotAdapter:
self._appearance_value = onroad_appearance(self.ui_state.params)
self._navigation_enabled = _flag(self.ui_state.params, "NavigationEnabled")
self._show_curve_status = _flag(self.ui_state.params, "ShowCSCStatus")
try:
raw, readable = read_saved(self.ui_state.params, "SLCOffsetSchedule", offset_document.MAX_DOCUMENT_BYTES)
offsets = offset_document.decode(raw) if readable and raw is not None else None
self._slc_offset_schedule = offsets.schedule() if isinstance(offsets, offset_document.OffsetDocument) else None
except (AttributeError, OSError, TypeError, ValueError, OverflowError):
self._slc_offset_schedule = None
if self.appearance_override is not None:
self._appearance_value = self.appearance_override(self._appearance_value)
self._appearance_read_ns = now_ns
@@ -720,7 +729,7 @@ class RuntimeSnapshotAdapter:
engaged=display_lat_active or display_long_active or longitudinal_overridden, camera_available=started,
speed_mps=raw_speed, cruise_kph=cruise, speed_limit=observation,
appearance=self._appearance_value, customization=self._customization_value,
alert=alert, metric=bool(ui.is_metric),
alert=alert, metric=bool(ui.is_metric), slc_offset_schedule=self._slc_offset_schedule,
experimental_available=bool(ui.has_longitudinal_control),
experimental_enabled=observed_experimental,
personality=list(Personality).index(persona),
+9 -7
View File
@@ -49,26 +49,28 @@ class SpeedLimitShapes:
if image.data is not None:
rl.unload_image(image)
def _draw(self, name, x, y, color, rotation=0, *, scale=None):
def _draw(self, name, x, y, color, rotation=0, *, scale=None, width_scale=1):
self._load()
source = self.sources[name]
origin = self.origins[name] if scale is None else rl.Vector2(source.width * scale / SPRITE_DENSITIES.get(name, 1) / 2,
source.height * scale / SPRITE_DENSITIES.get(name, 1) / 2)
scale = (self.scale if scale is None else scale) / SPRITE_DENSITIES.get(name, 1)
rl.draw_texture_pro(self.texture, source, rl.Rectangle(x, y, source.width * scale, source.height * scale),
if width_scale != 1:
origin = rl.Vector2(origin.x * width_scale, origin.y)
rl.draw_texture_pro(self.texture, source, rl.Rectangle(x, y, source.width * scale * width_scale, source.height * scale),
origin, rotation, color)
def circle(self, x, y, color):
self._outlined('ring', x, y, color)
def _outlined(self, name, x, y, color, *, scale=None):
def _outlined(self, name, x, y, color, *, scale=None, width_scale=1):
radius = self.scale if scale is None else scale
for dx, dy in ((-radius, 0), (radius, 0), (0, -radius), (0, radius)):
self._draw(name, x + dx, y + dy, rl.Color(6, 14, 10, 230), scale=scale)
self._draw(name, x, y, color, scale=scale)
self._draw(name, x + dx, y + dy, rl.Color(6, 14, 10, 230), scale=scale, width_scale=width_scale)
self._draw(name, x, y, color, scale=scale, width_scale=width_scale)
def outline(self, x, y, color, *, scale=None):
self._outlined('outline', x, y, color, scale=scale)
def outline(self, x, y, color, *, scale=None, width_scale=1):
self._outlined('outline', x, y, color, scale=scale, width_scale=width_scale)
def button(self, x, y, color, *, profile, scale, pressed=False):
self._draw(f'button_{profile}' + ('_pressed' if pressed else ''), x, y, color, scale=scale)
+16 -11
View File
@@ -312,12 +312,17 @@ class SpeedLimitWidget:
return
color = white
# Clamping the controller target does not configure an offset for this sign.
offset_text = shown.offset_text if vienna or shown.offset_configured else None
if vienna:
x, top = x + 5 * scale, top + 5 * scale
center_x = 59
self.shapes.circle(x + 59 * scale, top + sign_center_y * scale, self.pulse.color(rl.Color(201, 34, 49, 255), now))
else:
self.shapes.outline(x + center_x * scale, top + sign_center_y * scale, IN_USE if shown.limit_in_use else white)
# Keep 4:5 proportions and the original text sizes inside the existing tap area.
sign_scale = (160 if offset_text else 150) / SIGN_HEIGHT
self.shapes.outline(x + center_x * scale, top + (SIGN_HEIGHT * sign_scale / 2 - 9) * scale, IN_USE if shown.limit_in_use else white,
scale=scale * sign_scale, width_scale=SIGN_HEIGHT * 4 / 5 / SIGN_WIDTH)
heading_size = HEADING_SIZE if not vienna else 18
if vienna and self.profile == Profile.LARGE and not shown.pending:
@@ -327,8 +332,8 @@ class SpeedLimitWidget:
heading_size *= 108 / (right - left)
left, right, a, b = ink(heading, FontRole.SEMI_BOLD, heading_size)
# Lift the Vienna stack to keep the offset clear of the bottom rim.
lift = (12 if shown.limit_in_use else 7 if shown.offset_text else 2) if vienna else 0
header_y = 47 - lift if vienna else HEADING_ROW
lift = (12 if shown.limit_in_use else 7 if offset_text else 2) if vienna else 0
header_y = 47 - lift if vienna else 21 if offset_text else 26
arrow_scale = 1.
if vienna:
# Fit the heading and arrow inside the circle at the top of their ink.
@@ -352,15 +357,15 @@ class SpeedLimitWidget:
role = FontRole.BOLD if vienna else FontRole.SET_SPEED
size = 58 if vienna else NUMBER_SIZE
offset_size = 30 if vienna else 27
number_center = (73 if shown.offset_text else 80) - lift if vienna else NUMBER_ROW
number_center = (73 if offset_text else 80) - lift if vienna else 76 if offset_text else 90
while True:
left, right, a, b = ink(shown.posted_text, role, size)
width, height = right - left, b - a
number_top = number_center - height / 2
if vienna:
fits = number_top >= 57 - lift and circle_fits(59 - width / 2, 59 + width / 2, number_top, number_top + height)
if shown.offset_text:
left, right, a, b = ink(shown.offset_text, FontRole.MEDIUM, offset_size)
if offset_text:
left, right, a, b = ink(offset_text, FontRole.MEDIUM, offset_size)
offset_top = number_top + height + 4
fits = fits and circle_fits(59 - (right - left) / 2, 59 + (right - left) / 2, offset_top, offset_top + b - a)
else:
@@ -374,12 +379,12 @@ class SpeedLimitWidget:
centered(shown.posted_text, role, size, number_top, color)
else:
draw_speed_row(self.fonts, shown.posted_text, role, size * self.text_scale, x + center_x * scale,
top + NUMBER_ROW * scale, color, '0123456789')
if shown.offset_text:
top + number_center * scale, color, '0123456789')
if offset_text:
if not vienna:
left, right, a, b = ink(shown.offset_text, FontRole.MEDIUM, offset_size)
left, right, a, b = ink(offset_text, FontRole.MEDIUM, offset_size)
offset_size *= min(1, 108 / (right - left), 24 / (b - a))
offset_top = 105
centered(shown.offset_text, FontRole.MEDIUM, offset_size, offset_top, color if vienna else secondary)
offset_top = 120
centered(offset_text, FontRole.MEDIUM, offset_size, offset_top, color if vienna else secondary)
if shown.limit_in_use:
centered('IN USE', FontRole.SEMI_BOLD, 15 if vienna else 22, 98 if vienna else 117, IN_USE)
@@ -26,7 +26,7 @@ def historical_outline():
@pytest.mark.parametrize('profile', list(Profile))
@pytest.mark.parametrize('offset', [False, True])
@pytest.mark.parametrize('vienna', [False, True])
def test_both_sign_styles_keep_their_height_with_or_without_offset(profile, offset, vienna, historical_outline):
def test_sign_layout_and_tap_area_stay_fixed_with_or_without_offset(profile, offset, vienna, historical_outline):
current = state(show_slc_offset=offset)
current = replace(current, appearance=replace(current.appearance, use_vienna_sign=vienna))
widget = SpeedLimitWidget(fonts(profile), profile)
@@ -46,8 +46,10 @@ def test_both_sign_styles_keep_their_height_with_or_without_offset(profile, offs
else:
historical_outline.assert_not_called()
widget.shapes.outline.assert_called_once()
assert widget.shapes.outline.call_args.args[:2] == (bounds[0]+12+SIGN_WIDTH/2*scale, bounds[1]+12+(SIGN_HEIGHT/2-7)*scale)
assert widget.shapes.outline.call_args.kwargs == {}
sign_scale = (160 if offset else 150) / SIGN_HEIGHT
assert widget.shapes.outline.call_args.args[:2] == (bounds[0]+12+SIGN_WIDTH/2*scale,
bounds[1]+12+(SIGN_HEIGHT*sign_scale/2-9)*scale)
assert widget.shapes.outline.call_args.kwargs == {'scale': scale * sign_scale, 'width_scale': .9}
assert ('+5' in [call.args[0] for call in foreground_calls(widget.fonts.draw)]) == offset
@@ -103,3 +105,84 @@ def test_small_rectangular_no_offset_regression(historical_outline):
assert bounds is not None and bounds[3] - bounds[1] == SIGN_HEIGHT + 20
historical_outline.assert_not_called()
widget.shapes.outline.assert_called_once()
@pytest.mark.parametrize('profile', list(Profile))
@pytest.mark.parametrize('metric', (False, True))
@pytest.mark.parametrize('posted_mps', (1 / 3.6, 20., 40., 160 / 3.6))
@pytest.mark.parametrize('configured_offset', (0, 4, -4))
def test_only_the_displayed_ranges_configured_offset_expands_the_sign(profile, metric, posted_mps, configured_offset):
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule, adjusted_limit
factor = 3.6 if metric else 2.2369362921
bands = ((0., 10.), (10., 40.), (40., None))
schedule = OffsetSchedule(tuple(OffsetBand(lower, upper,
(configured_offset if lower <= posted_mps and (upper is None or posted_mps < upper) else 7) / factor)
for lower, upper in bands))
target = adjusted_limit(posted_mps, schedule)
assert target is not None
current = state(metric=metric, speed_mps=5., slc_offset_schedule=schedule)
current = replace(current, speed_limit=SpeedLimitObservation(ObservationKind.VALID, 'vision', posted_mps,
offset_mps=target - posted_mps, presentation_adjusted_limit_mps=target, accepted_speed_limit_mps=15.))
shown = resolve_unified_speed(current)
assert shown.offset_configured == (configured_offset != 0)
assert shown.limit_text == str(round(target * factor))
if configured_offset == 0 and posted_mps in (1 / 3.6, 160 / 3.6):
assert shown.offset_text is not None # The effective adjustment is clamping, not a configured offset.
widget = SpeedLimitWidget(fonts(profile), profile)
widget.shapes = Mock()
widget.render(current)
visible_offset = configured_offset != 0 and shown.offset_text is not None
assert widget.shapes.outline.call_args.kwargs['scale'] == pytest.approx(widget.scale * (160 if visible_offset else 150) / SIGN_HEIGHT)
assert [call.args[0] for call in foreground_calls(widget.fonts.draw)] == (
['VISION', shown.posted_text] + ([shown.offset_text] if visible_offset else []))
@pytest.mark.parametrize('profile', list(Profile))
def test_mutcd_preserves_text_sizes_and_fits_both_sizes_with_real_fonts(profile):
from itertools import product
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, default_font_directory
owns_context = not rl.is_window_ready()
if owns_context:
rl.init_window(128, 128, 'MUTCD spacing')
try:
with BitmapFonts(profile, default_font_directory()) as font:
widget = SpeedLimitWidget(font, profile)
for value, adjustment, stage in product((65, 120, 199), (0, 4, 10, -10), ('live', 'pending', 'retained')):
current = state(metric=True, slc_offset_schedule=OffsetSchedule((OffsetBand(0., None, adjustment / 3.6),)))
current = replace(current, speed_mps=(value + adjustment + 2) / 3.6,
speed_limit=SpeedLimitObservation(ObservationKind.VALID, 'vision', value / 3.6,
offset_mps=adjustment / 3.6, action_enabled=False,
pending_speed_limit_mps=value / 3.6 if stage == 'pending' else None))
if stage == 'retained':
current = replace(current, speed_limit=replace(current.speed_limit, kind=ObservationKind.STALE, speed_limit_mps=None,
last_received_speed_limit_mps=value / 3.6, last_received_adjusted_limit_mps=(value + adjustment) / 3.6,
last_received_source='vision', session_id='drive'))
with patch.object(font, 'draw') as drawn, patch.object(widget.shapes, 'outline') as outline:
widget.render(current)
scale = widget.scale * (160 if adjustment else 150) / SIGN_HEIGHT
cx, cy = outline.call_args.args[:2]
half_width, half_height = (SIGN_WIDTH / 2 - 4) * .9 * scale, (SIGN_HEIGHT / 2 - 4) * scale
boxes = []
for call in foreground_calls(drawn):
text, role, size, x, y, _ = call.args
left, right = font.horizontal_ink(text, role, size)
top, bottom = font.vertical_ink(text, role, size)
if text.startswith('LAST '):
assert right - left == pytest.approx(108 * widget.scale)
else:
assert size == pytest.approx({FontRole.SEMI_BOLD: 26, FontRole.SET_SPEED: 70, FontRole.MEDIUM: 27}[role] * widget.text_scale)
boxes.append((x + left, y + top, x + right, y + bottom))
assert cx - half_width + 1 <= min(box[0] for box in boxes)
assert max(box[2] for box in boxes) <= cx + half_width - 1
assert cy - half_height + 1 <= boxes[0][1]
assert boxes[-1][3] <= cy + half_height - 1
assert all(a[3] + 4 * widget.scale <= b[1] for a, b in zip(boxes, boxes[1:], strict=False))
bounds = speed_limit_bounds(profile, current)
outer_width, outer_height = (SIGN_WIDTH / 2 - 2) * .9 * scale + scale, (SIGN_HEIGHT / 2 - 2) * scale + scale
assert bounds[0] <= cx - outer_width and cx + outer_width <= bounds[2]
assert bounds[1] <= cy - outer_height and cy + outer_height <= bounds[3]
widget.close()
finally:
if owns_context:
rl.close_window()
@@ -619,9 +619,9 @@ def test_compact_us_sign_keeps_camera_visible_at_saved_position(speed):
hud._speed_limit_sign(state)
fill.assert_not_called()
outline.assert_called_once()
assert outline.call_args.args[:2] == (264, 95)
assert outline.call_args.args[:2] == (264, 96)
assert [call.args[0] for call in foreground_calls(fonts.draw)] == ['SPEED', str(speed)]
assert foreground_calls(fonts.draw)[0].args[3:5] == (259, 38)
assert foreground_calls(fonts.draw)[0].args[3:5] == (259, 47)
def test_compact_drag_keeps_animation_state_and_separate_origins():
@@ -100,6 +100,43 @@ def ui_fake():
has_longitudinal_control=True, CP=None, recording_audio=False)
def test_slc_offset_schedule_uses_cached_validated_settings(tmp_path):
from pathlib import Path
from openpilot.common.params import Params
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.speed_limits import offset_document as od
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
ui = ui_fake()
ui.params = Params(str(tmp_path))
document = od.OffsetDocument((0., 10., 15., 20., 25., 30., 35., 40.), (7., 7., 7., 0., 7., 7., 7.))
ui.params.put('SLCOffsetSchedule', od.to_value(document), block=True)
adapter = RuntimeSnapshotAdapter(ui)
with patch('openpilot.starpilot.ui.runtime_snapshot.read_saved', wraps=read_saved) as reads:
first = adapter.build(ShellMode.ONROAD, now_ns=NOW).onroad
assert first.slc_offset_schedule == document.schedule()
assert not resolve_unified_speed(first).offset_configured
document = od.OffsetDocument(document.bounds_mps, (7.,) * 7)
ui.params.put('SLCOffsetSchedule', od.to_value(document), block=True)
cached = adapter.build(ShellMode.ONROAD, now_ns=NOW + 50_000_000).onroad
assert cached.slc_offset_schedule is first.slc_offset_schedule
reads.assert_called_once()
adapter.invalidate_appearance()
updated = adapter.build(ShellMode.ONROAD, now_ns=NOW + 100_000_000).onroad
assert updated.slc_offset_schedule == document.schedule()
assert resolve_unified_speed(updated).offset_configured
assert reads.call_count == 2
path = Path(ui.params.get_param_path('SLCOffsetSchedule'))
for raw in (b'{bad', od.encode(od.NEEDS_REVIEW), b'x' * (od.MAX_DOCUMENT_BYTES + 1), None):
if raw is None:
path.unlink()
else:
path.write_bytes(raw)
adapter.invalidate_appearance()
invalid = adapter.build(ShellMode.ONROAD, now_ns=NOW + 100_000_000).onroad
assert invalid.slc_offset_schedule is None
assert not resolve_unified_speed(invalid).offset_configured
class TestRuntimeSnapshot(unittest.TestCase):
def test_home_uses_shared_statistics_source_without_onroad_polling(self):
@@ -78,6 +78,21 @@ def test_bad_atlas_dimensions_release_image_without_uploading(gpu):
gpu['load_texture_from_image'].assert_not_called()
@pytest.mark.parametrize('scale', (1., 8 / 9))
def test_narrow_outline_reuses_the_atlas_and_keeps_its_center(gpu, scale):
owner = shapes.SpeedLimitShapes()
owner.outline(64, 65, shapes.rl.WHITE, scale=scale, width_scale=.9)
assert gpu['draw_texture_pro'].call_count == 5
_, source, destination, origin, _, _ = gpu['draw_texture_pro'].call_args.args
assert (source.width, source.height) == (136, 152)
assert (destination.width, destination.height) == pytest.approx((136 * scale * .9, 152 * scale))
assert (origin.x, origin.y) == pytest.approx((destination.width / 2, destination.height / 2))
owner.outline(64, 65, shapes.rl.WHITE)
assert gpu['draw_texture_pro'].call_args.args[3].x == 68
gpu['load_texture_from_image'].assert_called_once()
owner.close()
def test_large_sign_scale_changes_destination_and_rotation_origin_only(gpu):
owner = shapes.SpeedLimitShapes(1.25)
owner.arrow('compact', 345.75, 111.25, -1, shapes.rl.BLACK)
@@ -33,12 +33,14 @@ def drawing(monkeypatch):
def state(**changes):
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
observation = SpeedLimitObservation(ObservationKind.VALID, 'map', 50 / 3.6,
accepted_speed_limit_mps=40 / 3.6, accepted_adjusted_limit_mps=45 / 3.6,
pending_speed_limit_mps=50 / 3.6, pending_adjusted_limit_mps=55 / 3.6,
presentation_adjusted_limit_mps=55 / 3.6, accepted_source='dashboard', action_enabled=True,
session_id='drive', presentation_id=2, decision_id=1, pending_since_ns=10_000_000_000)
return replace(OnroadState(True, False, 45 / 3.6, 100., observation, metric=True, longitudinal_active=True,
slc_offset_schedule=OffsetSchedule((OffsetBand(0., None, 5 / 3.6),)),
observed_ns=10_000_000_000), **changes)
@@ -105,6 +105,21 @@ def test_hidden_max_and_offset_preferences_keep_the_latest_sign():
assert (shown.mode, shown.posted_text, shown.offset_text) == ('limit_only', '108', None)
@pytest.mark.parametrize('stage,expected', [('live', True), ('confirmation', False), ('retained', False), ('last_received', True)])
def test_configured_offset_follows_the_resolved_posted_limit(stage, expected):
from openpilot.starpilot.speed_limits.speed_domain import OffsetBand, OffsetSchedule
current = state(slc_offset_schedule=OffsetSchedule((OffsetBand(0., 25., 0.), OffsetBand(25., None, 2.))))
if stage == 'confirmation':
current = replace(current, speed_limit=replace(current.speed_limit, pending_speed_limit_mps=20., pending_adjusted_limit_mps=20.))
elif stage in ('retained', 'last_received'):
current = replace(current, speed_limit=replace(current.speed_limit, kind=ObservationKind.STALE, speed_limit_mps=None,
accepted_adjusted_limit_mps=20., last_received_speed_limit_mps=30., last_received_adjusted_limit_mps=32.,
effective_cluster_target_mps=20. if stage == 'retained' else None, effective_cap_mps=20.))
shown = resolve_unified_speed(current, confirmation=stage == 'confirmation')
assert shown.posted_text == ('108' if expected else '72')
assert shown.offset_configured == expected
@pytest.mark.parametrize('pending,adjusted,presentation,offset,expected', [
(30., 33., 32., 2., '119'),
(30., None, 32., 2., '115'),
@@ -3,6 +3,8 @@
from dataclasses import dataclass
import math
from openpilot.starpilot.speed_limits.speed_domain import offset_for_limit
@dataclass(frozen=True)
class UnifiedSpeedPresentation:
@@ -23,6 +25,7 @@ class UnifiedSpeedPresentation:
limit_in_use: bool = False
set_limited: bool = False
show_last: bool = False
offset_configured: bool = False
@property
def max_status(self) -> str:
@@ -159,4 +162,6 @@ def resolve_unified_speed(state, *, confirmation=False) -> UnifiedSpeedPresentat
obs.override_basis != 'pedal' and
positive(obs.effective_cluster_target_mps) else None,
speed(active_target) if slc_limiting and positive(active_target) else None,
posted_retained, limit_in_use, set_limited, show_last)
posted_retained, limit_in_use, set_limited, show_last,
offset_configured=state.slc_offset_schedule is not None and
offset_for_limit(posted, state.slc_offset_schedule) not in (None, 0.))