diff --git a/scripts/speed_limit_vision/common.py b/scripts/speed_limit_vision/common.py index 5f4afb9d..6d6b67f5 100644 --- a/scripts/speed_limit_vision/common.py +++ b/scripts/speed_limit_vision/common.py @@ -193,8 +193,18 @@ BOOKMARK_MANIFEST_FIELDS = [ "candidate_confidence", "speed_limit_mph", "confidence", + "source_confidence", + "source_event", "published_speed_limit_mph", "published_confidence", + "map_source", + "map_current_speed_limit_mph", + "map_next_speed_limit_mph", + "map_next_speed_limit_distance_m", + "map_expected_speed_limit_mph", + "map_relation", + "previous_map_speed_limit_mph", + "review_bucket", "bookmark_count", "snapshot_path", "source_session_path", diff --git a/scripts/speed_limit_vision/import_debug_sessions.py b/scripts/speed_limit_vision/import_debug_sessions.py index aa4edfb6..aeccc3fb 100644 --- a/scripts/speed_limit_vision/import_debug_sessions.py +++ b/scripts/speed_limit_vision/import_debug_sessions.py @@ -41,7 +41,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--latest", type=int, default=1, help="How many latest sessions to import when no session ids are provided.") parser.add_argument("--mode", choices=("symlink", "copy"), default="symlink", help="How to place snapshots into the workspace review/images directory.") parser.add_argument("--force", action="store_true", help="Overwrite snapshot links/files if they already exist.") - parser.add_argument("--events", nargs="+", default=["bookmark", "auto_bookmark", "training_candidate", "publish", "candidate"], help="Event types to include in the manifest.") + parser.add_argument("--events", nargs="+", default=["bookmark", "auto_bookmark", "training_candidate", "map_transition_miss", "publish", "candidate"], help="Event types to include in the manifest.") return parser.parse_args() @@ -106,8 +106,18 @@ def event_row(event: dict, session_id: str, session_path: Path, event_index: int "candidate_confidence": str(event.get("candidateConfidence") or ""), "speed_limit_mph": str(event.get("speedLimitMph") or ""), "confidence": str(event.get("confidence") or ""), + "source_confidence": str(event.get("sourceConfidence") or ""), + "source_event": str(event.get("sourceEvent") or ""), "published_speed_limit_mph": str(event.get("publishedSpeedLimitMph") or ""), "published_confidence": str(event.get("publishedConfidence") or ""), + "map_source": str(event.get("mapSource") or ""), + "map_current_speed_limit_mph": str(event.get("mapCurrentSpeedLimitMph") or ""), + "map_next_speed_limit_mph": str(event.get("mapNextSpeedLimitMph") or ""), + "map_next_speed_limit_distance_m": str(event.get("mapNextSpeedLimitDistanceM") or ""), + "map_expected_speed_limit_mph": str(event.get("mapExpectedSpeedLimitMph") or ""), + "map_relation": str(event.get("mapRelation") or ""), + "previous_map_speed_limit_mph": str(event.get("previousMapSpeedLimitMph") or ""), + "review_bucket": str(event.get("reviewBucket") or ""), "bookmark_count": str(event.get("bookmarkCount") or ""), "snapshot_path": snapshot_path, "source_session_path": str(session_path), diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index 19744442..9d7fbde4 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -118,6 +118,8 @@ class HudRenderer(Widget): self._engaged: bool = False self._show_speed_limit: bool = False self._speed_limit: float = 0.0 + self._speed_limit_offset: float = 0.0 + self._show_speed_limit_offset: bool = False self._speed_limit_overridden: bool = False self._pending_speed_limit: float = 0.0 self._prompt_visible: bool = False @@ -194,6 +196,7 @@ class HudRenderer(Widget): if self._show_speed_limit: dashboard_speed_limit = sm["starpilotCarState"].dashboardSpeedLimit if sm.valid.get("starpilotCarState", False) else 0.0 vision_speed_limit = ui_state.params_memory.get_float("VisionSpeedLimit") if ui_state.params.get_bool("VisionSpeedLimitDetection") else 0.0 + self._show_speed_limit_offset = ui_state.params.get_bool("ShowSLCOffset") primary_priority = ui_state.params.get("SLCPriority1", encoding='utf-8') or "Map Data" secondary_priority = ui_state.params.get("SLCPriority2", encoding='utf-8') or "None" source_limits = { @@ -210,16 +213,23 @@ class HudRenderer(Widget): secondary_priority=secondary_priority, ) self._speed_limit = max(0.0, resolved_speed_limit * speed_conversion) + self._speed_limit_offset = starpilot_plan.slcSpeedLimitOffset * speed_conversion self._speed_limit_overridden = bool(starpilot_plan.slcOverriddenSpeed > 0 and starpilot_plan.slcSpeedLimit > 0) + if self._speed_limit > 0 and not self._speed_limit_overridden and not self._show_speed_limit_offset: + self._speed_limit += self._speed_limit_offset self._pending_speed_limit = max(0.0, starpilot_plan.unconfirmedSlcSpeedLimit * speed_conversion) else: self._speed_limit = 0.0 + self._speed_limit_offset = 0.0 + self._show_speed_limit_offset = False self._speed_limit_overridden = False self._pending_speed_limit = 0.0 self._prompt_visible = self._pending_speed_limit > 0 else: self._show_speed_limit = False self._speed_limit = 0.0 + self._speed_limit_offset = 0.0 + self._show_speed_limit_offset = False self._speed_limit_overridden = False self._pending_speed_limit = 0.0 self._prompt_visible = False @@ -335,6 +345,64 @@ class HudRenderer(Widget): max_color, ) + def _draw_us_speed_limit_sign( + self, + sign_rect: rl.Rectangle, + speed_text: str, + sign_alpha: int, + *, + border_thickness: float, + header_font_size: int, + header_gap: int, + speed_font_size: int, + header_top: float, + speed_top: float, + footer_text: str = "", + footer_font_size: int = 0, + footer_top: float = 0.0, + ) -> None: + border_color = rl.Color(255, 255, 255, sign_alpha) + text_color = rl.Color(255, 255, 255, sign_alpha) + + inner_border_rect = rl.Rectangle( + sign_rect.x + 8, + sign_rect.y + 8, + sign_rect.width - 16, + sign_rect.height - 16, + ) + rl.draw_rectangle_rounded_lines_ex(inner_border_rect, 0.14, 16, max(border_thickness - 2, 1), border_color) + + speed_label = tr("SPEED") + limit_label = tr("LIMIT") + speed_label_size = measure_text_cached(self._font_semi_bold, speed_label, header_font_size) + limit_label_size = measure_text_cached(self._font_semi_bold, limit_label, header_font_size) + + rl.draw_text_ex( + self._font_semi_bold, + speed_label, + rl.Vector2(sign_rect.x + sign_rect.width / 2 - speed_label_size.x / 2, sign_rect.y + header_top), + header_font_size, + 0, + text_color, + ) + rl.draw_text_ex( + self._font_semi_bold, + limit_label, + rl.Vector2(sign_rect.x + sign_rect.width / 2 - limit_label_size.x / 2, sign_rect.y + header_top + header_gap), + header_font_size, + 0, + text_color, + ) + + speed_text_size = measure_text_cached(self._font_bold, speed_text, speed_font_size) + speed_text_pos = rl.Vector2(sign_rect.x + sign_rect.width / 2 - speed_text_size.x / 2, sign_rect.y + speed_top) + rl.draw_text_ex(self._font_bold, speed_text, speed_text_pos, speed_font_size, 0, text_color) + + if footer_text and footer_font_size > 0: + footer_size = measure_text_cached(self._font_semi_bold, footer_text, footer_font_size) + footer_pos = rl.Vector2(sign_rect.x + sign_rect.width / 2 - footer_size.x / 2, sign_rect.y + footer_top) + rl.draw_text_ex(self._font_semi_bold, footer_text, footer_pos, footer_font_size, 0, text_color) + def _draw_speed_limit(self, rect: rl.Rectangle) -> None: if not self._show_speed_limit: return @@ -352,6 +420,10 @@ class HudRenderer(Widget): sign_y = rect.y + (28 if use_vienna_speed_limit else 20) speed_text = str(round(display_speed)) + offset_text = "" + if self._show_speed_limit_offset and not self._speed_limit_overridden: + rounded_offset = round(self._speed_limit_offset) + offset_text = "–" if rounded_offset == 0 else f"{rounded_offset:+d}" if use_vienna_speed_limit: center_x = sign_x + sign_width / 2 center_y = sign_y + sign_height / 2 @@ -370,30 +442,29 @@ class HudRenderer(Widget): font_size = 58 if len(speed_text) <= 2 else 48 text_size = measure_text_cached(self._font_bold, speed_text, font_size) - text_pos = rl.Vector2(center_x - text_size.x / 2, center_y - text_size.y / 2 + 4) + text_pos = rl.Vector2(center_x - text_size.x / 2, center_y - text_size.y / 2 + (-14 if offset_text else 4)) rl.draw_text_ex(self._font_bold, speed_text, text_pos, font_size, 0, rl.Color(0, 0, 0, sign_alpha)) + if offset_text: + offset_font_size = 34 + offset_size = measure_text_cached(self._font_semi_bold, offset_text, offset_font_size) + offset_pos = rl.Vector2(center_x - offset_size.x / 2, sign_y + sign_height - 42) + rl.draw_text_ex(self._font_semi_bold, offset_text, offset_pos, offset_font_size, 0, rl.Color(0, 0, 0, sign_alpha)) else: sign_rect = rl.Rectangle(sign_x, sign_y, sign_width, sign_height) - border_rect = rl.Rectangle(sign_x + 6, sign_y + 6, sign_width - 12, sign_height - 12) - rl.draw_rectangle_rounded(sign_rect, 0.18, 16, rl.Color(255, 255, 255, sign_alpha)) - rl.draw_rectangle_rounded_lines_ex(border_rect, 0.14, 16, 4, rl.Color(0, 0, 0, sign_alpha)) - - header_font_size = 20 - header_gap = 18 - speed_font_size = 50 if len(speed_text) <= 2 else 42 - - speed_label = tr("SPEED") - limit_label = tr("LIMIT") - speed_label_size = measure_text_cached(self._font_semi_bold, speed_label, header_font_size) - limit_label_size = measure_text_cached(self._font_semi_bold, limit_label, header_font_size) - speed_label_pos = rl.Vector2(sign_x + sign_width / 2 - speed_label_size.x / 2, sign_y + 18) - limit_label_pos = rl.Vector2(sign_x + sign_width / 2 - limit_label_size.x / 2, sign_y + 18 + header_gap) - rl.draw_text_ex(self._font_semi_bold, speed_label, speed_label_pos, header_font_size, 0, rl.Color(0, 0, 0, sign_alpha)) - rl.draw_text_ex(self._font_semi_bold, limit_label, limit_label_pos, header_font_size, 0, rl.Color(0, 0, 0, sign_alpha)) - - speed_text_size = measure_text_cached(self._font_bold, speed_text, speed_font_size) - speed_text_pos = rl.Vector2(sign_x + sign_width / 2 - speed_text_size.x / 2, sign_y + 66) - rl.draw_text_ex(self._font_bold, speed_text, speed_text_pos, speed_font_size, 0, rl.Color(0, 0, 0, sign_alpha)) + self._draw_us_speed_limit_sign( + sign_rect, + speed_text, + sign_alpha, + border_thickness=4, + header_font_size=20, + header_gap=18, + speed_font_size=44 if offset_text else (50 if len(speed_text) <= 2 else 42), + header_top=18, + speed_top=58 if offset_text else 66, + footer_text=offset_text, + footer_font_size=28 if offset_text else 0, + footer_top=100, + ) def _update_prompt_layout(self, rect: rl.Rectangle) -> None: if not self._prompt_visible: @@ -484,38 +555,17 @@ class HudRenderer(Widget): text_pos = rl.Vector2(center_x - text_size.x / 2, center_y - text_size.y / 2 + 4) rl.draw_text_ex(self._font_bold, speed_text, text_pos, font_size, 0, rl.BLACK) else: - border_rect = rl.Rectangle(sign_rect.x + 8, sign_rect.y + 8, sign_rect.width - 16, sign_rect.height - 16) - rl.draw_rectangle_rounded(sign_rect, 0.18, 16, rl.WHITE) - rl.draw_rectangle_rounded_lines_ex(border_rect, 0.14, 16, 5, rl.BLACK) - - header_font_size = 24 - header_gap = 20 - speed_font_size = 72 if len(speed_text) <= 2 else 60 - - speed_label = tr("SPEED") - limit_label = tr("LIMIT") - speed_label_size = measure_text_cached(self._font_semi_bold, speed_label, header_font_size) - limit_label_size = measure_text_cached(self._font_semi_bold, limit_label, header_font_size) - rl.draw_text_ex( - self._font_semi_bold, - speed_label, - rl.Vector2(sign_rect.x + sign_rect.width / 2 - speed_label_size.x / 2, sign_rect.y + 20), - header_font_size, - 0, - rl.BLACK, + self._draw_us_speed_limit_sign( + sign_rect, + speed_text, + 255, + border_thickness=5, + header_font_size=24, + header_gap=20, + speed_font_size=72 if len(speed_text) <= 2 else 60, + header_top=20, + speed_top=80, ) - rl.draw_text_ex( - self._font_semi_bold, - limit_label, - rl.Vector2(sign_rect.x + sign_rect.width / 2 - limit_label_size.x / 2, sign_rect.y + 20 + header_gap), - header_font_size, - 0, - rl.BLACK, - ) - - speed_size = measure_text_cached(self._font_bold, speed_text, speed_font_size) - speed_pos = rl.Vector2(sign_rect.x + sign_rect.width / 2 - speed_size.x / 2, sign_rect.y + 80) - rl.draw_text_ex(self._font_bold, speed_text, speed_pos, speed_font_size, 0, rl.BLACK) self._draw_prompt_button( self._prompt_deny_rect, @@ -532,7 +582,7 @@ class HudRenderer(Widget): False, ) - hint_text = tr("USE WHEEL - / +") + hint_text = tr("SET/+ TO CONFIRM RES/- TO DENY") hint_size = measure_text_cached(self._font_medium, hint_text, 24) hint_pos = rl.Vector2(card_rect.x + card_rect.width / 2 - hint_size.x / 2, card_rect.y + card_rect.height - 34) rl.draw_text_ex(self._font_medium, hint_text, hint_pos, 24, 0, rl.Color(255, 255, 255, 180)) diff --git a/starpilot/system/speed_limit_vision.py b/starpilot/system/speed_limit_vision.py index de0424a8..89377fcc 100644 --- a/starpilot/system/speed_limit_vision.py +++ b/starpilot/system/speed_limit_vision.py @@ -34,6 +34,9 @@ AUTO_BOOKMARK_MIN_CONFIDENCE = 0.62 TRAINING_COLLECTOR_CONFIRM_DELAY_SECONDS = 0.7 TRAINING_COLLECTOR_COOLDOWN_SECONDS = 2.5 TRAINING_COLLECTOR_MIN_CONFIDENCE = 0.40 +MAP_NEXT_REVIEW_DISTANCE_METERS = 120.0 +MAP_TRANSITION_MISS_CAPTURE_COOLDOWN_SECONDS = 8.0 +MAP_VISION_MATCH_WINDOW_SECONDS = 2.5 MODEL_PROPOSAL_MIN_CONFIDENCE = 0.0001 MODEL_PROPOSAL_MAX_COUNT = 16 MODEL_PROPOSAL_MAX_AREA_RATIO = 0.18 @@ -229,6 +232,9 @@ class SpeedLimitVisionDaemon: self.last_training_capture_at = 0.0 self.last_training_capture_speed_limit_mph = 0 self.pending_training_capture = None + self.last_map_speed_limit_mph = 0 + self.last_map_transition_miss_at = 0.0 + self.last_map_transition_miss_speed_limit_mph = 0 self.ignore_next_user_bookmark = False self.current_frame_bgr = None @@ -286,6 +292,120 @@ class SpeedLimitVisionDaemon: self.last_logged_status = "" self.last_logged_candidate = None + def _read_next_map_speed_limit(self): + if self.params_memory is None: + return {} + + next_map_speed_limit = self.params_memory.get("NextMapSpeedLimit") or {} + if isinstance(next_map_speed_limit, (bytes, str)): + try: + next_map_speed_limit = json.loads(next_map_speed_limit) + except Exception: + next_map_speed_limit = {} + return next_map_speed_limit if isinstance(next_map_speed_limit, dict) else {} + + def _get_map_context(self): + current_limit_ms = 0.0 + next_limit_ms = 0.0 + next_distance_m = 0.0 + source = "none" + + if self.sm is not None: + try: + current_limit_ms = float(self.sm["mapdOut"].speedLimit or 0.0) + next_limit_ms = float(self.sm["mapdOut"].nextSpeedLimit or 0.0) + next_distance_m = float(self.sm["mapdOut"].nextSpeedLimitDistance or 0.0) + if current_limit_ms > 0.0 or next_limit_ms > 0.0: + source = "mapd" + except Exception: + current_limit_ms = 0.0 + next_limit_ms = 0.0 + next_distance_m = 0.0 + + if self.params_memory is not None: + filler_current_limit_ms = float(self.params_memory.get_float("MapSpeedLimit") or 0.0) + next_map_speed_limit = self._read_next_map_speed_limit() + filler_next_limit_ms = float(next_map_speed_limit.get("speedlimit") or 0.0) + filler_next_distance_m = float(next_map_speed_limit.get("distance") or 0.0) + if filler_current_limit_ms > 0.0 or filler_next_limit_ms > 0.0: + current_limit_ms = filler_current_limit_ms if filler_current_limit_ms > 0.0 else current_limit_ms + next_limit_ms = filler_next_limit_ms if filler_next_limit_ms > 0.0 else next_limit_ms + next_distance_m = filler_next_distance_m if filler_next_distance_m > 0.0 else next_distance_m + source = "filler" + + current_limit_mph = int(round(current_limit_ms * CV.MS_TO_MPH)) if current_limit_ms > 0.0 else 0 + next_limit_mph = int(round(next_limit_ms * CV.MS_TO_MPH)) if next_limit_ms > 0.0 else 0 + next_distance_m = round(next_distance_m, 1) if next_distance_m > 0.0 else 0.0 + + return { + "source": source, + "current_speed_limit_mph": current_limit_mph, + "next_speed_limit_mph": next_limit_mph, + "next_speed_limit_distance_m": next_distance_m, + } + + def _map_fields(self, speed_limit_mph=0): + map_context = self._get_map_context() + current_limit_mph = int(map_context["current_speed_limit_mph"]) + next_limit_mph = int(map_context["next_speed_limit_mph"]) + next_distance_m = float(map_context["next_speed_limit_distance_m"]) + map_source = str(map_context["source"]) + + expected_speed_limit_mph = current_limit_mph if current_limit_mph > 0 else 0 + map_relation = "no_map" + review_bucket = "vision_only" + + next_is_relevant = next_limit_mph > 0 and next_limit_mph != current_limit_mph and 0.0 < next_distance_m <= MAP_NEXT_REVIEW_DISTANCE_METERS + + if current_limit_mph > 0: + if speed_limit_mph > 0 and speed_limit_mph == current_limit_mph: + map_relation = "agree_current" + review_bucket = "map_agreement" + else: + map_relation = "disagree_current" + review_bucket = "map_disagreement" + + if next_is_relevant: + if speed_limit_mph > 0 and speed_limit_mph == next_limit_mph: + expected_speed_limit_mph = next_limit_mph + map_relation = "agree_next" + review_bucket = "map_agreement" + elif current_limit_mph <= 0: + expected_speed_limit_mph = next_limit_mph + map_relation = "disagree_next" + review_bucket = "map_disagreement" + + if speed_limit_mph <= 0: + if next_is_relevant: + expected_speed_limit_mph = next_limit_mph + map_relation = "map_transition_pending" + review_bucket = "map_transition_review" + elif current_limit_mph > 0: + map_relation = "map_present" + review_bucket = "map_context_only" + + return { + "mapSource": map_source, + "mapCurrentSpeedLimitMph": current_limit_mph, + "mapNextSpeedLimitMph": next_limit_mph, + "mapNextSpeedLimitDistanceM": next_distance_m, + "mapExpectedSpeedLimitMph": expected_speed_limit_mph, + "mapRelation": map_relation, + "reviewBucket": review_bucket, + } + + def _vision_recently_supported(self, speed_limit_mph, now): + if speed_limit_mph <= 0: + return False + if self.last_candidate_speed_limit_mph == speed_limit_mph and now - self.last_candidate_at <= MAP_VISION_MATCH_WINDOW_SECONDS: + return True + if self.published_speed_limit_mph == speed_limit_mph and now - self.last_detection_at <= MAP_VISION_MATCH_WINDOW_SECONDS: + return True + return any( + entry.speed_limit_mph == speed_limit_mph and now - entry.created_at <= MAP_VISION_MATCH_WINDOW_SECONDS + for entry in self.history + ) + def _write_debug_event(self, event_type, frame_bgr=None, snapshot_prefix=None, **fields): if not self.use_runtime or self.params_memory is None: return @@ -307,6 +427,7 @@ class SpeedLimitVisionDaemon: } if self.debug_session_started_at > 0.0: event["sessionSeconds"] = round(max(time.monotonic() - self.debug_session_started_at, 0.0), 3) + event.update(self._map_fields(int(fields.get("speedLimitMph") or fields.get("candidateSpeedLimitMph") or 0))) event.update(fields) if frame_bgr is not None and self.debug_capture_dir is not None and snapshot_prefix: @@ -390,6 +511,24 @@ class SpeedLimitVisionDaemon: sourceEvent=source_event, ) + def _record_map_transition_miss(self, speed_limit_mph, previous_speed_limit_mph): + if not self.use_runtime or self.params_memory is None or not self.debug_log_path: + return + + self._write_debug_event( + "map_transition_miss", + frame_bgr=self.current_frame_bgr, + snapshot_prefix=f"map_transition_miss_{speed_limit_mph:03d}", + speedLimitMph=speed_limit_mph, + previousMapSpeedLimitMph=previous_speed_limit_mph, + candidateSpeedLimitMph=self.last_candidate_speed_limit_mph, + candidateConfidence=round(self.last_candidate_confidence, 4), + publishedSpeedLimitMph=self.published_speed_limit_mph, + publishedConfidence=round(self.published_confidence, 4), + reason="map_change_without_vision_support", + reviewBucket="map_transition_miss", + ) + def _schedule_auto_bookmark(self, speed_limit_mph, confidence, published_at): if not self.use_runtime or self.params is None: return @@ -503,6 +642,27 @@ class SpeedLimitVisionDaemon: self.last_training_capture_speed_limit_mph = speed_limit_mph self._record_training_candidate(speed_limit_mph, confidence, source_confidence, source_event) + def _maybe_capture_map_transition_miss(self, now): + map_context = self._get_map_context() + current_speed_limit_mph = int(map_context["current_speed_limit_mph"]) + previous_speed_limit_mph = self.last_map_speed_limit_mph + + if current_speed_limit_mph != previous_speed_limit_mph: + self.last_map_speed_limit_mph = current_speed_limit_mph + + if current_speed_limit_mph <= 0 or current_speed_limit_mph == previous_speed_limit_mph or previous_speed_limit_mph <= 0: + return + if self.current_frame_bgr is None: + return + if now - self.last_map_transition_miss_at < MAP_TRANSITION_MISS_CAPTURE_COOLDOWN_SECONDS and current_speed_limit_mph == self.last_map_transition_miss_speed_limit_mph: + return + if self._vision_recently_supported(current_speed_limit_mph, now): + return + + self.last_map_transition_miss_at = now + self.last_map_transition_miss_speed_limit_mph = current_speed_limit_mph + self._record_map_transition_miss(current_speed_limit_mph, previous_speed_limit_mph) + def _published_detection_stale(self, now): return self.published_speed_limit_mph > 0 and now - self.last_detection_at > PUBLISHED_HOLD_SECONDS @@ -1617,6 +1777,7 @@ class SpeedLimitVisionDaemon: self._maybe_commit_auto_bookmark(now) self._maybe_commit_training_capture(now) + self._maybe_capture_map_transition_miss(now) ratekeeper.keep_time() diff --git a/starpilot/system/the_pond/assets/components/tools/device_settings_layout.json b/starpilot/system/the_pond/assets/components/tools/device_settings_layout.json index 7ac59046..746c60b7 100644 --- a/starpilot/system/the_pond/assets/components/tools/device_settings_layout.json +++ b/starpilot/system/the_pond/assets/components/tools/device_settings_layout.json @@ -1181,8 +1181,25 @@ "description": "Ask before changing to a new speed limit. To accept, tap the flashing on-screen widget or press the Cruise Increase button. To deny, press the Cruise Decrease button or ignore the prompt for 30 seconds.", "data_type": "bool", "ui_type": "toggle", + "is_parent_toggle": true, "parent_key": "SpeedLimitController" }, + { + "key": "SLCConfirmationLower", + "label": "Confirm Lower Limits", + "description": "Require confirmation before applying a newly detected lower speed limit.", + "data_type": "bool", + "ui_type": "toggle", + "parent_key": "SLCConfirmation" + }, + { + "key": "SLCConfirmationHigher", + "label": "Confirm Higher Limits", + "description": "Require confirmation before applying a newly detected higher speed limit.", + "data_type": "bool", + "ui_type": "toggle", + "parent_key": "SLCConfirmation" + }, { "key": "SLCLookaheadHigher", "label": "Higher Limit Lookahead Time", @@ -1211,6 +1228,50 @@ "ui_type": "toggle", "parent_key": "SpeedLimitController" }, + { + "key": "SLCFallback", + "label": "Fallback Speed", + "description": "Choose the speed to use when no speed limit source is currently available.", + "data_type": "int", + "ui_type": "dropdown", + "options": [ + { + "value": 0, + "label": "Set Speed" + }, + { + "value": 1, + "label": "Experimental Mode" + }, + { + "value": 2, + "label": "Previous Limit" + } + ], + "parent_key": "SpeedLimitController" + }, + { + "key": "SLCOverride", + "label": "Override Speed", + "description": "Choose how SLC behaves after you manually drive faster than the posted speed limit.", + "data_type": "int", + "ui_type": "dropdown", + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Set With Gas Pedal" + }, + { + "value": 2, + "label": "Max Set Speed" + } + ], + "parent_key": "SpeedLimitController" + }, { "key": "SLCMapboxFiller", "label": "Use Mapbox as Fallback",