long manuevers / model manager

This commit is contained in:
firestar5683
2026-04-06 18:34:30 -05:00
parent 19068fc2cc
commit deff72c5e4
6 changed files with 164 additions and 44 deletions
+6 -3
View File
@@ -203,9 +203,10 @@ def pond_thread():
parked = sm["starpilotCarState"].isParked
started = sm["deviceState"].started
long_maneuver_mode = params.get_bool("LongitudinalManeuverMode")
state_changed = started != previous_started or parked != previous_parked
if params.get_bool("PondPaired"):
if params.get_bool("PondPaired") and not long_maneuver_mode:
presence_interval = POND_PRESENCE_INTERVAL_ACTIVE if started or pond_active else POND_PRESENCE_INTERVAL_IDLE
ping_pond_presence(presence_interval, parked, started, state_changed)
@@ -219,13 +220,15 @@ def pond_thread():
if state_changed and parked:
next_toggle_check_at = 0.0
if boot_sync_complete and now >= next_toggle_check_at:
if long_maneuver_mode:
next_toggle_check_at = max(next_toggle_check_at, now + REMOTE_TOGGLE_CHECK_INTERVAL_IDLE)
elif boot_sync_complete and now >= next_toggle_check_at:
latest_pond_active = check_toggles(started, params, sm)
if latest_pond_active is not None:
pond_active = latest_pond_active
next_toggle_check_at = now + REMOTE_TOGGLE_CHECK_INTERVAL_ACTIVE if pond_active else REMOTE_TOGGLE_CHECK_INTERVAL_IDLE
if params.get_bool("PondUploadPending"):
if params.get_bool("PondUploadPending") and not long_maneuver_mode:
if not params.get_bool("PondPaired"):
params.put_bool("PondUploadPending", False)
elif upload_toggles(params):
@@ -9,7 +9,7 @@ const state = reactive({
let initialized = false
let pollHandle = null
const POLL_INTERVAL_MS = 1000
const POLL_INTERVAL_MS = 3000
function isLongitudinalManeuversRouteActive() {
return window.location.pathname === "/longitudinal_maneuvers"
@@ -432,7 +432,9 @@ function renderActions(model) {
if (model.installed) {
return html`
<button class="mm-btn mm-btn-secondary" data-mm-action="select" data-model="${modelKey}">Set Active</button>
<button class="mm-btn mm-btn-danger" data-mm-action="delete" data-model="${modelKey}">Delete</button>
${model.builtin
? ""
: html`<button class="mm-btn mm-btn-danger" data-mm-action="delete" data-model="${modelKey}">Delete</button>`}
`;
}
@@ -451,6 +453,7 @@ function renderModelRow(model) {
</div>
<div class="mm-row-meta">
<span class="mm-chip">${key}</span>
${model.builtin ? html`<span class="mm-chip">Built-in</span>` : ""}
${state.sortMode === "release_date" ? "" : model.series ? html`<span class="mm-chip">${safeText(model.series)}</span>` : ""}
${model.version ? html`<span class="mm-chip">Version ${safeText(model.version)}</span>` : ""}
${model.released ? html`<span class="mm-chip">Released ${safeText(model.released)}</span>` : ""}
+121 -32
View File
@@ -43,6 +43,7 @@ from openpilot.system.version import get_build_metadata
from openpilot.tools.longitudinal_maneuvers.capabilities import get_longitudinal_maneuver_support
from panda import Panda
from openpilot.starpilot.assets.model_manager import canonical_model_key, is_builtin_model_key, model_key_aliases
from openpilot.starpilot.assets.theme_manager import HOLIDAY_THEME_PATH, THEME_COMPONENT_PARAMS
from openpilot.starpilot.common.accel_profile import (
CUSTOM_ACCEL_PROFILE_INITIALIZED_KEY,
@@ -60,7 +61,7 @@ from openpilot.starpilot.common.maps_catalog import (
schedule_param_value,
)
from openpilot.starpilot.common.starpilot_utilities import delete_file, get_lock_status, run_cmd
from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, LEGACY_STARPILOT_PARAM_RENAMES, MAPS_PATH, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, STOCK_THEME_PATH, THEME_SAVE_PATH,\
from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, LEGACY_STARPILOT_PARAM_RENAMES, MAPS_PATH, MODELS_PATH, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, STOCK_THEME_PATH, THEME_SAVE_PATH,\
default_ev_tuning_enabled, update_starpilot_toggles
from openpilot.starpilot.common.testing_grounds import (
DEFAULT_TESTING_GROUND_VARIANT as SHARED_DEFAULT_TESTING_GROUND_VARIANT,
@@ -1971,7 +1972,10 @@ def _get_current_param_value(key, value_type, defaults_lookup=None):
if defaults_lookup is None:
defaults_lookup = _get_default_param_values()
raw_value = defaults_lookup.get(key)
return _coerce_param_value(raw_value, value_type)
value = _coerce_param_value(raw_value, value_type)
if key in ("Model", "DrivingModel") and isinstance(value, str):
return canonical_model_key(value)
return value
def _get_custom_accel_profile_initialized():
@@ -3355,7 +3359,7 @@ def setup(app):
}), 200
if key in ("Model", "DrivingModel"):
selected_model = str_val.strip()
selected_model = canonical_model_key(str_val.strip())
if not selected_model:
return jsonify({"error": "Driving model cannot be empty."}), 400
@@ -3366,25 +3370,40 @@ def setup(app):
available_names = [entry.strip() for entry in (params.get("AvailableModelNames", encoding="utf-8") or "").split(",")]
model_versions = [entry.strip() for entry in (params.get("ModelVersions", encoding="utf-8") or "").split(",")]
if selected_model in available_models:
selected_index = available_models.index(selected_model)
selected_index = next((i for i, model_key in enumerate(available_models) if canonical_model_key(model_key) == selected_model), -1)
if selected_index != -1:
if selected_index < len(available_names) and available_names[selected_index]:
params.put("DrivingModelName", available_names[selected_index])
elif is_builtin_model_key(selected_model):
params.put("DrivingModelName", _default_model_name())
if selected_index < len(model_versions) and model_versions[selected_index]:
resolved_version = model_versions[selected_index]
params.put("ModelVersion", resolved_version)
params.put("DrivingModelVersion", resolved_version)
elif is_builtin_model_key(selected_model):
resolved_version = _default_model_version()
params.put("ModelVersion", resolved_version)
params.put("DrivingModelVersion", resolved_version)
elif is_builtin_model_key(selected_model):
params.put("DrivingModelName", _default_model_name())
resolved_version = _default_model_version()
params.put("ModelVersion", resolved_version)
params.put("DrivingModelVersion", resolved_version)
else:
# Fallback to cached version map if this model isn't in the current manifest list yet.
try:
with open("/data/models/.model_versions.json", "r") as f:
with open(MODELS_PATH / ".model_versions.json", "r") as f:
versions = json.load(f)
if selected_model in versions:
resolved_version = str(versions[selected_model]).strip()
for alias in model_key_aliases(selected_model):
if alias not in versions:
continue
resolved_version = str(versions[alias]).strip()
if resolved_version:
params.put("ModelVersion", resolved_version)
params.put("DrivingModelVersion", resolved_version)
break
except Exception:
pass
elif key in ("ModelVersion", "DrivingModelVersion"):
@@ -3434,7 +3453,12 @@ def setup(app):
return _serialize_param_write_value(defaults_lookup.get(request_key)), 200
if request_key == CUSTOM_ACCEL_PROFILE_INITIALIZED_KEY:
return _serialize_param_write_value(_get_custom_accel_profile_initialized()), 200
return params.get(request_key) or "", 200
value = params.get(request_key) or ""
if request_key in ("Model", "DrivingModel"):
if isinstance(value, bytes):
value = value.decode("utf-8", errors="replace")
return canonical_model_key(str(value).strip()), 200
return value, 200
@app.route("/api/params/all", methods=["GET"])
def get_all_params():
@@ -3517,7 +3541,7 @@ def setup(app):
installed = [{"value": model["value"], "label": model["label"]} for model in catalog if model["installed"]]
# Keep current model selectable even if local files are currently inconsistent.
current_model = params.get("Model", encoding="utf-8") or ""
current_model = _current_model_key()
if current_model and all(model["value"] != current_model for model in installed):
for model in catalog:
if model["value"] == current_model:
@@ -3531,7 +3555,7 @@ def setup(app):
models = get_model_catalog()
return jsonify({
"models": models,
"currentModel": params.get("Model", encoding="utf-8") or "",
"currentModel": _current_model_key(),
"summary": {
"installed": sum(1 for model in models if model["installed"]),
"missing": sum(1 for model in models if not model["installed"]),
@@ -3572,13 +3596,13 @@ def setup(app):
@app.route("/api/models/status", methods=["GET"])
def get_models_status():
models = get_model_catalog()
model_to_download = params_memory.get(MODEL_DOWNLOAD_PARAM, encoding="utf-8") or ""
model_to_download = canonical_model_key(params_memory.get(MODEL_DOWNLOAD_PARAM, encoding="utf-8") or "")
download_all = params_memory.get_bool(MODEL_DOWNLOAD_ALL_PARAM)
progress = params_memory.get(MODEL_DOWNLOAD_PROGRESS_PARAM, encoding="utf-8") or ""
cancelling = params_memory.get_bool(MODEL_CANCEL_DOWNLOAD_PARAM)
downloading = bool(model_to_download) or download_all
current_model = params.get("Model", encoding="utf-8") or ""
current_model = _current_model_key()
sort_mode = read_legacy_param_file(MODEL_SORT_MODE_PARAM, "alphabetical")
terminal = progress in ("Downloaded!", "All models downloaded!") or bool(re.search(r"cancelled|exists|failed|offline|invalid|error", progress, re.IGNORECASE))
summary = {
@@ -3665,7 +3689,7 @@ def setup(app):
return jsonify({"error": "A model download is already in progress."}), 409
data = request.get_json() or {}
model_key = (data.get("model") or "").strip()
model_key = canonical_model_key((data.get("model") or "").strip())
if not model_key:
return jsonify({"error": "Missing model key."}), 400
@@ -3722,11 +3746,11 @@ def setup(app):
return jsonify({"error": "Cannot delete model files while a download is in progress."}), 409
data = request.get_json() or {}
model_key = (data.get("model") or "").strip()
model_key = canonical_model_key((data.get("model") or "").strip())
if not model_key:
return jsonify({"error": "Missing model key."}), 400
current_model = params.get("Model", encoding="utf-8") or ""
current_model = _current_model_key()
if model_key == current_model:
return jsonify({"error": "Cannot delete the currently active model."}), 409
@@ -3734,8 +3758,10 @@ def setup(app):
model = catalog.get(model_key)
if model is None:
return jsonify({"error": f"Unknown model '{model_key}'."}), 404
if model.get("builtin"):
return jsonify({"error": "Cannot delete the built-in default model."}), 409
models_dir = Path("/data/models")
models_dir = MODELS_PATH
if not models_dir.is_dir():
return jsonify({"message": "No model directory exists yet."}), 200
@@ -3882,7 +3908,32 @@ def setup(app):
def get_param_memory():
return params_memory.get(request.args.get("key")) or "", 200
def _param_text(value):
if value is None:
return ""
if isinstance(value, bytes):
return value.decode("utf-8", errors="ignore").strip()
return str(value).strip()
def _default_model_key():
default_key = _param_text(params.get_default_value("Model") or params.get_default_value("DrivingModel"))
return canonical_model_key(default_key) or "sc2"
def _default_model_name():
return _param_text(params.get_default_value("DrivingModelName")) or "South Carolina"
def _default_model_version():
default_version = _param_text(params.get_default_value("ModelVersion") or params.get_default_value("DrivingModelVersion"))
return default_version or "v11"
def _current_model_key():
current_model = _param_text(params.get("Model", encoding="utf-8") or params.get("DrivingModel", encoding="utf-8"))
return canonical_model_key(current_model) or _default_model_key()
def is_model_installed(model_key, model_version, on_disk_files):
if is_builtin_model_key(model_key):
return True
if f"{model_key}.thneed" in on_disk_files:
return True
@@ -3913,18 +3964,18 @@ def setup(app):
versions = [entry.strip() for entry in (params.get("ModelVersions", encoding="utf-8") or "").split(",")]
released_dates = [entry.strip() for entry in (params.get("ModelReleasedDates", encoding="utf-8") or "").split(",")]
community_favorites = {entry.strip() for entry in (params.get("CommunityFavorites", encoding="utf-8") or "").split(",") if entry.strip()}
user_favorites = {entry.strip() for entry in (params.get(MODEL_USER_FAVORITES_PARAM, encoding="utf-8") or "").split(",") if entry.strip()}
community_favorites = {canonical_model_key(entry.strip()) for entry in (params.get("CommunityFavorites", encoding="utf-8") or "").split(",") if entry.strip()}
user_favorites = {canonical_model_key(entry.strip()) for entry in (params.get(MODEL_USER_FAVORITES_PARAM, encoding="utf-8") or "").split(",") if entry.strip()}
models_dir = "/data/models"
try:
on_disk_files = set(os.listdir(models_dir)) if os.path.isdir(models_dir) else set()
on_disk_files = {entry.name for entry in MODELS_PATH.iterdir()} if MODELS_PATH.is_dir() else set()
except Exception:
on_disk_files = set()
models = []
models_by_key = {}
for i, key in enumerate(available):
if not key:
canonical_key = canonical_model_key(key)
if not canonical_key:
continue
label = names[i] if i < len(names) and names[i] else key
@@ -3932,19 +3983,57 @@ def setup(app):
model_series = series[i] if i < len(series) and series[i] else "Custom Series"
released = released_dates[i] if i < len(released_dates) else ""
installed = is_model_installed(key, model_version, on_disk_files)
partial = not installed and any(file.startswith(f"{key}.") or file.startswith(f"{key}_") for file in on_disk_files)
existing = models_by_key.get(canonical_key)
if existing is None:
models_by_key[canonical_key] = {
"value": canonical_key,
"label": label,
"series": model_series,
"version": model_version,
"released": released,
"builtin": is_builtin_model_key(canonical_key),
"communityFavorite": canonical_key in community_favorites,
"userFavorite": canonical_key in user_favorites,
}
continue
if (not existing["label"] or existing["label"] == existing["value"]) and label:
existing["label"] = label
if (not existing["series"] or existing["series"] == "Custom Series") and model_series:
existing["series"] = model_series
if not existing["version"] and model_version:
existing["version"] = model_version
if not existing["released"] and released:
existing["released"] = released
existing["builtin"] = existing["builtin"] or is_builtin_model_key(canonical_key)
existing["communityFavorite"] = existing["communityFavorite"] or canonical_key in community_favorites
existing["userFavorite"] = existing["userFavorite"] or canonical_key in user_favorites
default_key = _default_model_key()
default_entry = models_by_key.setdefault(default_key, {
"value": default_key,
"label": _default_model_name(),
"series": "Custom Series",
"version": _default_model_version(),
"released": "",
"builtin": True,
"communityFavorite": default_key in community_favorites,
"userFavorite": default_key in user_favorites,
})
default_entry["builtin"] = True
if not default_entry["label"] or default_entry["label"] == default_entry["value"]:
default_entry["label"] = _default_model_name()
if not default_entry["version"]:
default_entry["version"] = _default_model_version()
models = []
for key, model in models_by_key.items():
installed = is_model_installed(key, model["version"], on_disk_files)
partial = (not model["builtin"]) and (not installed) and any(file.startswith(f"{key}.") or file.startswith(f"{key}_") for file in on_disk_files)
models.append({
"value": key,
"label": label,
"series": model_series,
"version": model_version,
"released": released,
**model,
"installed": installed,
"partial": partial,
"communityFavorite": key in community_favorites,
"userFavorite": key in user_favorites,
})
models.sort(key=lambda model: (model["series"].lower(), model["label"].lower()))
+24 -3
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any
from opendbc.car.gm.values import GMFlags
LOW_SPEED_MANEUVER_DESCRIPTIONS = (
"come to stop",
@@ -31,12 +33,27 @@ def get_longitudinal_maneuver_support(CP: Any) -> LongitudinalManeuverSupport:
openpilot_longitudinal = bool(getattr(CP, "openpilotLongitudinalControl", False))
auto_resume_supported = bool(getattr(CP, "autoResumeSng", False))
brand = str(getattr(CP, "brand", "") or getattr(CP, "carName", "") or "").lower()
fingerprint = str(getattr(CP, "carFingerprint", "") or "")
flags = int(getattr(CP, "flags", 0) or 0)
has_pedal = bool(getattr(CP, "enableGasInterceptorDEPRECATED", False))
min_enable_speed = float(getattr(CP, "minEnableSpeed", 0.0) or 0.0)
stop_accel = float(getattr(CP, "stopAccel", 0.0) or 0.0)
# CarParams does not expose a dedicated "won't fully brake to zero" flag,
# so low-speed engagement support is the closest reliable proxy.
is_gm = brand == "gm"
is_volt = fingerprint.startswith("CHEVROLET_VOLT")
has_sascm = is_gm and bool(flags & GMFlags.SASCM.value)
# CarParams does not expose a dedicated "won't fully brake to zero" flag.
# For most platforms, low-speed engagement support is the best proxy.
full_stop_and_go = min_enable_speed <= 0.0
# GM Volt without pedal can often engage below 0 mph but still creep instead of
# reliably achieving a true stop in these canned maneuvers, especially on SASCM paths.
if is_volt and not has_pedal:
full_stop_and_go = False
auto_resume_from_stop = full_stop_and_go and auto_resume_supported
expected_to_reach_zero = full_stop_and_go
requires_resume_assist = expected_to_reach_zero and not auto_resume_from_stop
@@ -45,10 +62,14 @@ def get_longitudinal_maneuver_support(CP: Any) -> LongitudinalManeuverSupport:
if not openpilot_longitudinal:
caveats.append("openpilot longitudinal is disabled, so the maneuver suite cannot drive longitudinal tests on this platform.")
if not expected_to_reach_zero:
if is_volt and not has_pedal:
caveats.append("Volt without pedal is not expected to reach a true standstill in the maneuver suite. Stop, start, and creep maneuvers will be skipped.")
elif not expected_to_reach_zero:
caveats.append("This car is not expected to reach a true standstill in the suite. Stop, start, and creep maneuvers will be skipped.")
elif requires_resume_assist:
caveats.append("This car can reach a stop, but restart-from-stop needs resume assistance. Zero-speed maneuvers will allow cruise standstill instead of treating it as setup failure.")
elif has_sascm and full_stop_and_go:
caveats.append("SASCM is present on this GM platform. If a real standstill is reached, restart behavior can still depend on resume handling.")
skipped_maneuvers = LOW_SPEED_MANEUVER_DESCRIPTIONS if not expected_to_reach_zero else ()
+8 -4
View File
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from cereal import messaging, car
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.common.realtime import DT_MDL, Priority, config_realtime_process
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.tools.longitudinal_maneuvers.capabilities import (
@@ -134,6 +134,8 @@ def build_maneuvers():
def main():
config_realtime_process(5, Priority.CTRL_LOW)
params = Params()
cloudlog.info("joystickd is waiting for CarParams")
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
@@ -147,7 +149,7 @@ def main():
continue
supported_maneuvers.append(maneuver)
sm = messaging.SubMaster(['carState', 'carControl', 'controlsState', 'selfdriveState', 'modelV2'], poll='modelV2')
sm = messaging.SubMaster(['carState', 'carControl', 'modelV2'], poll='modelV2')
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'alertDebug'])
maneuvers = iter(supported_maneuvers)
@@ -163,7 +165,7 @@ def main():
alert_msg.valid = True
plan_send = messaging.new_message('longitudinalPlan')
plan_send.valid = sm.all_checks()
plan_send.valid = sm.all_checks(['carState', 'carControl', 'modelV2'])
longitudinalPlan = plan_send.longitudinalPlan
accel = 0
@@ -184,6 +186,8 @@ def main():
longitudinalPlan.aTarget = accel
longitudinalPlan.shouldStop = v_ego < CP.vEgoStopping and accel < 1e-2
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
longitudinalPlan.processingDelay = (plan_send.logMonoTime / 1e9) - sm.logMonoTime['modelV2']
longitudinalPlan.allowBrake = True
longitudinalPlan.allowThrottle = True
@@ -194,7 +198,7 @@ def main():
pm.send('longitudinalPlan', plan_send)
assistance_send = messaging.new_message('driverAssistance')
assistance_send.valid = True
assistance_send.valid = sm.all_checks(['carState', 'carControl', 'modelV2'])
pm.send('driverAssistance', assistance_send)
if maneuver is not None and maneuver.finished: