lateral tuning migration and dashcam mode

This commit is contained in:
firestar5683
2026-03-27 22:00:10 -05:00
parent 741131974c
commit 09929f2454
38 changed files with 173 additions and 10 deletions
+38
View File
@@ -52,10 +52,16 @@ LEGACY_FORCED_CANDIDATE_MAP = {
"CHEVROLET_BOLT_CC_2019_2021": "CHEVROLET_BOLT_CC_2018_2021",
}
GM_CANDIDATE_PREFIXES = ("CHEVROLET_", "GMC_", "CADILLAC_", "BUICK_", "HOLDEN_")
GM_CORE_FINGERPRINT_MSGS = frozenset((190, 201, 209, 211, 241))
def _normalize_forced_candidate(candidate: str | None) -> str | None:
if candidate is None:
return None
if isinstance(candidate, (bytes, bytearray)):
candidate = candidate.decode("utf-8", errors="ignore")
candidate = str(candidate)
return LEGACY_FORCED_CANDIDATE_MAP.get(candidate, candidate)
@@ -102,6 +108,25 @@ def _normalize_gm_bolt_candidate(candidate: str | None, fingerprints: dict[int,
return candidate
def _is_gm_candidate(candidate: str | None) -> bool:
return isinstance(candidate, str) and candidate.startswith(GM_CANDIDATE_PREFIXES)
def _get_gm_stored_candidate_fallback(fingerprints: dict[int, dict], stored_candidate: str | None,
cached_candidate: str | None) -> str | None:
pt = fingerprints.get(0, {})
# Several GM variants intentionally share FPv1 signatures. If live matching
# can't resolve them, reuse the last known GM platform instead of dropping to MOCK.
if len(GM_CORE_FINGERPRINT_MSGS.intersection(pt.keys())) < 4:
return None
for candidate in (_normalize_forced_candidate(stored_candidate), _normalize_forced_candidate(cached_candidate)):
if _is_gm_candidate(candidate) and candidate != MOCK.MOCK and candidate in interfaces:
return candidate
return None
def can_fingerprint(can_recv: CanRecvCallable) -> tuple[str | None, dict[int, dict]]:
finger = gen_empty_fingerprint()
candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1
@@ -217,6 +242,19 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip
candidate = _normalize_gm_bolt_candidate(candidate, fingerprints)
candidate = _normalize_forced_candidate(candidate)
fingerprinted_candidate = candidate
stored_candidate = _normalize_forced_candidate(params.get("CarModel"))
cached_candidate = _normalize_forced_candidate(getattr(cached_params, "carFingerprint", None))
if candidate is None:
gm_fallback_candidate = _get_gm_stored_candidate_fallback(fingerprints, stored_candidate, cached_candidate)
if gm_fallback_candidate is not None:
carlog.error({
"event": "using stored GM candidate after ambiguous live fingerprint",
"candidate": gm_fallback_candidate,
"stored_candidate": stored_candidate,
"cached_candidate": cached_candidate,
})
candidate = gm_fallback_candidate
if candidate is None or starpilot_toggles.force_fingerprint:
if starpilot_toggles.force_fingerprint:
@@ -1,6 +1,6 @@
import pytest
from opendbc.car.can_definitions import CanData
from opendbc.car.car_helpers import FRAME_FINGERPRINT, can_fingerprint
from opendbc.car.car_helpers import FRAME_FINGERPRINT, _get_gm_stored_candidate_fallback, can_fingerprint
from opendbc.car.fingerprints import _FINGERPRINTS as FINGERPRINTS
@@ -53,3 +53,24 @@ class TestCanFingerprint:
car_fingerprint, _ = can_fingerprint(can_recv)
assert car_fingerprint == car_model
assert frames == expected_frames + 2 # TODO: fix extra frames
def test_gm_stored_candidate_fallback_prefers_persisted_model(self):
fingerprints = {0: {190: 6, 201: 8, 209: 7, 211: 2, 241: 6}}
candidate = _get_gm_stored_candidate_fallback(fingerprints, "CHEVROLET_VOLT_CC", None)
assert candidate == "CHEVROLET_VOLT_CC"
def test_gm_stored_candidate_fallback_uses_cached_model(self):
fingerprints = {0: {190: 6, 201: 8, 209: 7, 211: 2, 241: 6}}
candidate = _get_gm_stored_candidate_fallback(fingerprints, "MOCK", "CHEVROLET_VOLT_CC")
assert candidate == "CHEVROLET_VOLT_CC"
def test_gm_stored_candidate_fallback_ignores_non_gm_fingerprint(self):
fingerprints = {0: {1: 1, 2: 2, 3: 3, 4: 4}}
candidate = _get_gm_stored_candidate_fallback(fingerprints, "CHEVROLET_VOLT_CC", "CHEVROLET_VOLT_CC")
assert candidate is None
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
extern const uint8_t gitversion[19];
const uint8_t gitversion[19] = "DEV-3d8af236-DEBUG";
const uint8_t gitversion[19] = "DEV-74113197-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
DEV-3d8af236-DEBUG
DEV-74113197-DEBUG
BIN
View File
Binary file not shown.
@@ -172,7 +172,15 @@ StarPilotSettingsWindow::StarPilotSettingsWindow(SettingsWindow *parent) : QFram
shownDescriptions = QJsonDocument::fromJson(QString::fromStdString(params.get("ShownToggleDescriptions")).toUtf8()).object();
QString className = this->metaObject()->className();
if (!shownDescriptions.value(className).toBool(false)) {
QString legacyClassName = className;
legacyClassName.replace("StarPilot", "FrogPilot");
bool alreadyShown = shownDescriptions.value(className).toBool(false);
bool legacyShown = legacyClassName != className && shownDescriptions.value(legacyClassName).toBool(false);
if (legacyShown && !alreadyShown) {
shownDescriptions.insert(className, true);
params.putNonBlocking("ShownToggleDescriptions", QJsonDocument(shownDescriptions).toJson(QJsonDocument::Compact).toStdString());
} else if (!alreadyShown) {
forceOpenDescriptions = true;
}
+23 -6
View File
@@ -136,6 +136,15 @@ def _load_first_available_param_value(params: Params, params_cache: Params, sour
return None
def _has_persisted_param_file(params: Params, key: str | bytes) -> bool:
try:
path = params.get_param_path(key)
except Exception:
return False
return bool(path) and os.path.isfile(path)
def migrate_starpilot_param_renames(params: Params, params_cache: Params) -> None:
if STARPILOT_PARAM_RENAME_MIGRATION_FLAG.exists():
return
@@ -320,6 +329,7 @@ def migrate_starpilot_default_parity(params: Params, params_cache: Params) -> No
if STARPILOT_DEFAULTS_PARITY_MIGRATION_FLAG.exists():
return
seeded_keys: list[str] = []
desired_bool_values = {
"AdvancedLateralTune": True,
"ForceAutoTuneOff": True,
@@ -330,27 +340,34 @@ def migrate_starpilot_default_parity(params: Params, params_cache: Params) -> No
}
for key, value in desired_bool_values.items():
if _has_persisted_param_file(params, key) or _has_persisted_param_file(params_cache, key):
continue
params.put_bool(key, value)
params_cache.put_bool(key, value)
seeded_keys.append(key)
params.put_float("CEModelStopTime", 7.0)
params_cache.put_float("CEModelStopTime", 7.0)
if not _has_persisted_param_file(params, "CEModelStopTime") and not _has_persisted_param_file(params_cache, "CEModelStopTime"):
params.put_float("CEModelStopTime", 7.0)
params_cache.put_float("CEModelStopTime", 7.0)
seeded_keys.append("CEModelStopTime")
# Rebase default regression fix:
# EVTuning must default to enabled on EV/direct-drive platforms to preserve
# StarPilot acceleration profile behavior.
# StarPilot acceleration profile behavior, but existing user overrides win.
carparams_blob = params.get("CarParamsPersistent") or params.get("CarParams")
if carparams_blob is not None:
try:
with car.CarParams.from_bytes(carparams_blob) as cp:
is_ev_platform = cp.transmissionType == car.CarParams.TransmissionType.direct
if is_ev_platform and not params.get_bool("TruckTuning"):
if is_ev_platform and not params.get_bool("TruckTuning") and not _has_persisted_param_file(params, "EVTuning") and not _has_persisted_param_file(params_cache, "EVTuning"):
params.put_bool("EVTuning", True)
params_cache.put_bool("EVTuning", True)
seeded_keys.append("EVTuning")
except Exception:
cloudlog.exception("Failed EVTuning EV default parity migration")
cloudlog.warning("Applied one-time StarPilot default parity migration for lateral/longitudinal toggles")
if seeded_keys:
cloudlog.warning(f"Applied one-time StarPilot default parity migration for {seeded_keys}")
try:
STARPILOT_DEFAULTS_PARITY_MIGRATION_FLAG.parent.mkdir(parents=True, exist_ok=True)
@@ -524,6 +541,7 @@ def manager_init() -> None:
# Canonicalize legacy string encodings (e.g. INT params stored as "26.000000")
# before bulk reads below to avoid repeated cast warnings and UI-side churn.
migrate_param_type_canonicalization(params)
migrate_starpilot_default_parity(params, params_cache)
# set unset params to their default value
for k in params.all_keys():
@@ -558,7 +576,6 @@ def manager_init() -> None:
# Branch migration: rename legacy Bolt fingerprint persisted in CarParams.
migrate_legacy_bolt_fingerprint(params)
migrate_starpilot_default_parity(params, params_cache)
# set dongle id
reg_res = register(show_spinner=True)
+79
View File
@@ -2,6 +2,8 @@ import os
import pytest
import signal
import time
from pathlib import Path
import json
from cereal import car
from openpilot.common.params import Params
@@ -16,6 +18,59 @@ MAX_STARTUP_TIME = 3
BLACKLIST_PROCS = ['manage_athenad', 'pandad', 'pigeond']
class FileBackedFakeParams:
def __init__(self, root: Path, values: dict[str, object] | None = None):
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
for key, value in (values or {}).items():
self.put(key, value)
def get_param_path(self, key):
return str(self.root / (key.decode() if isinstance(key, bytes) else str(key)))
def get(self, key):
path = Path(self.get_param_path(key))
if not path.is_file():
return None
raw = path.read_bytes()
try:
return raw.decode("utf-8")
except UnicodeDecodeError:
return raw
def get_bool(self, key):
value = self.get(key)
if value is None:
return False
if isinstance(value, bytes):
value = value.decode("utf-8", errors="ignore")
return str(value).strip().lower() in ("1", "true", "yes", "on")
def put(self, key, value):
path = Path(self.get_param_path(key))
path.parent.mkdir(parents=True, exist_ok=True)
if isinstance(value, bytes):
raw = value
elif isinstance(value, bool):
raw = b"1" if value else b"0"
elif isinstance(value, float):
raw = str(float(value)).encode("utf-8")
elif isinstance(value, (dict, list)):
raw = json.dumps(value, separators=(",", ":")).encode("utf-8")
else:
raw = str(value).encode("utf-8")
path.write_bytes(raw)
def put_bool(self, key, value):
self.put(key, bool(value))
def put_float(self, key, value):
self.put(key, float(value))
class TestManager:
def setup_method(self):
HARDWARE.set_power_save(False)
@@ -85,6 +140,30 @@ class TestManager:
assert params.get("ExperimentalLongitudinalEnabled") is None
assert params_cache.get("ExperimentalLongitudinalEnabled") is None
def test_migrate_starpilot_default_parity_preserves_existing_values(self, tmp_path, monkeypatch):
monkeypatch.setattr(manager, "STARPILOT_DEFAULTS_PARITY_MIGRATION_FLAG", tmp_path / "starpilot_defaults_parity_v1")
params = FileBackedFakeParams(tmp_path / "params", {
"AdvancedLateralTune": False,
"ForceAutoTuneOff": False,
"HumanAcceleration": True,
"CEModelStopTime": 3.5,
})
params_cache = FileBackedFakeParams(tmp_path / "cache", {
"NNFF": True,
})
manager.migrate_starpilot_default_parity(params, params_cache)
assert not params.get_bool("AdvancedLateralTune")
assert not params.get_bool("ForceAutoTuneOff")
assert params.get_bool("HumanAcceleration")
assert params.get("CEModelStopTime") == "3.5"
assert params_cache.get_bool("NNFF")
assert Path(params.get_param_path("HumanFollowing")).is_file()
assert not params.get_bool("HumanFollowing")
@pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad")
def test_clean_exit(self, subtests):
"""