diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index ddc0160d4..aa78ce7dc 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -18,6 +18,7 @@ from openpilot.selfdrive.car import gen_empty_fingerprint from openpilot.system.version import get_build_metadata FRAME_FINGERPRINT = 100 # 1s +SOURCE_BRANCH_FILE = "/data/media/0/starpilot_source_branch" EventName = car.CarEvent.EventName FrogPilotEventName = custom.FrogPilotCarEvent.EventName @@ -189,6 +190,64 @@ def get_car_interface(CP, FPCP): CarInterface, CarController, CarState = interfaces[CP.carFingerprint] return CarInterface(CP, FPCP, CarController, CarState) +def get_cached_car_fingerprint(params: Params) -> str | None: + for key in ("CarParamsPersistent", "CarParamsCache", "CarParams"): + cp_bytes = params.get(key) + if cp_bytes is None: + continue + try: + with car.CarParams.from_bytes(cp_bytes) as cached_cp: + if cached_cp.carFingerprint: + return cached_cp.carFingerprint + except Exception: + continue + return None + +def clear_stale_car_params(params: Params, candidate: str) -> None: + cached_fingerprint = get_cached_car_fingerprint(params) + if cached_fingerprint is None or cached_fingerprint == candidate: + return + + stale_keys = ( + "CarParams", + "CarParamsCache", + "CarParamsPersistent", + "FrogPilotCarParams", + "FrogPilotCarParamsPersistent", + "CarModelName", + ) + for key in stale_keys: + params.remove(key) + + cloudlog.warning("cleared stale car params after fingerprint change: %s -> %s", cached_fingerprint, candidate) + +def migrate_legacy_bolt_candidate(candidate: str) -> str: + source_branch = "" + try: + with open(SOURCE_BRANCH_FILE, encoding="utf-8") as f: + source_branch = f.read().strip() + except OSError: + pass + + migration_branch = source_branch or get_build_metadata().channel + replacements = {} + if migration_branch in {"TorqueTune", "TorquePedal"}: + replacements = { + "CHEVROLET_BOLT_EUV": GM_CAR.CHEVROLET_BOLT_ACC_2022_2023, + "CHEVROLET_BOLT_CC": GM_CAR.CHEVROLET_BOLT_CC_2022_2023, + } + elif migration_branch in {"TotallyTune", "StarPilot-2017", "StarPilot 2017"}: + replacements = { + "CHEVROLET_BOLT_CC": GM_CAR.CHEVROLET_BOLT_CC_2017, + } + elif migration_branch in {"StarPilot"}: + replacements = { + "CHEVROLET_BOLT_CC": GM_CAR.CHEVROLET_BOLT_CC_2019_2021, + } + + normalized_candidate = candidate[4:] if candidate.startswith("CAR.") else candidate + return replacements.get(normalized_candidate, normalized_candidate) + def get_car(logcan, sendcan, experimental_long_allowed, params, num_pandas=1, frogpilot_toggles=None): candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(logcan, sendcan, num_pandas) @@ -203,9 +262,23 @@ def get_car(logcan, sendcan, experimental_long_allowed, params, num_pandas=1, fr params.put_nonblocking("CarMake", candidate.split('_')[0].title()) params.put_nonblocking("CarModel", candidate) + # Branch migration can leave legacy Bolt candidate names active in params/cache. + # Remap the selected candidate itself so fingerprint selection and params stay in sync. + migrated_candidate = migrate_legacy_bolt_candidate(candidate) + if candidate != migrated_candidate: + cloudlog.warning("legacy Bolt candidate migration: %s -> %s", candidate, migrated_candidate) + candidate = migrated_candidate + params.put_nonblocking("CarMake", candidate.split('_')[0].title()) + params.put_nonblocking("CarModel", candidate) + params.remove("CarModelName") + # VIN-based Bolt year mapping (selfdrive-only, bolt variants only) if not frogpilot_toggles.force_fingerprint and is_valid_vin(vin): bolt_variants = { + "CHEVROLET_BOLT_EUV", + "CHEVROLET_BOLT_CC", + "CAR.CHEVROLET_BOLT_EUV", + "CAR.CHEVROLET_BOLT_CC", GM_CAR.CHEVROLET_BOLT_ACC_2022_2023, GM_CAR.CHEVROLET_BOLT_CC_2022_2023, GM_CAR.CHEVROLET_BOLT_CC_2019_2021, @@ -235,12 +308,28 @@ def get_car(logcan, sendcan, experimental_long_allowed, params, num_pandas=1, fr candidate = vin_candidate params.put_nonblocking("CarMake", candidate.split('_')[0].title()) params.put_nonblocking("CarModel", candidate) + params.remove("CarModelName") cloudlog.warning("VIN Bolt override: %s -> %s", prev_candidate, candidate) + # Always prefer live fingerprint naming for Bolt variants to avoid stale manual labels. + if candidate in { + GM_CAR.CHEVROLET_BOLT_ACC_2022_2023, + GM_CAR.CHEVROLET_BOLT_CC_2022_2023, + GM_CAR.CHEVROLET_BOLT_CC_2019_2021, + GM_CAR.CHEVROLET_BOLT_CC_2017, + "CHEVROLET_BOLT_EUV", + "CHEVROLET_BOLT_CC", + "CAR.CHEVROLET_BOLT_EUV", + "CAR.CHEVROLET_BOLT_CC", + }: + params.remove("CarModelName") + if frogpilot_toggles.block_user: candidate = MOCK.MOCK sentry.capture_block() + clear_stale_car_params(params, candidate) + CarInterface, _, _ = interfaces[candidate] CP = CarInterface.get_params(candidate, fingerprints, car_fw, experimental_long_allowed, frogpilot_toggles, docs=False) FPCP = CarInterface.get_frogpilot_params(candidate, fingerprints, car_fw, CP, frogpilot_toggles) diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index 1fb496d04..517331323 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -172,6 +173,22 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { main_layout->addLayout(header_layout); + branch_merge_banner = new QLabel(this); + branch_merge_banner->setAlignment(Qt::AlignCenter); + branch_merge_banner->setWordWrap(true); + branch_merge_banner->setAttribute(Qt::WA_TransparentForMouseEvents, true); + branch_merge_banner->setVisible(false); + branch_merge_banner->setStyleSheet(R"( + background-color: #E22C2C; + color: white; + border-radius: 16px; + padding: 24px; + font-size: 44px; + font-weight: 700; + )"); + branch_merge_banner->setText(tr("Branch Merge Notice: This branch is deprecated and has been merged into StarPilot. Please switch to the StarPilot branch.")); + main_layout->addWidget(branch_merge_banner); + // main content main_layout->addSpacing(25); center_layout = new QStackedLayout(); @@ -268,10 +285,21 @@ void OffroadHome::hideEvent(QHideEvent *event) { } void OffroadHome::refresh() { + static const QSet deprecated_branches = { + "TorqueTune", + "TorquePedal", + "Kaofui", + "Red-Kao", + "TotallyTune", + "StarPilot-2017", + "TRX", + }; + date->setText(QLocale(uiState()->language.mid(5)).toString(QDateTime::currentDateTime(), "dddd, MMMM d")); date->setVisible(util::system_time_valid()); version->setText(getBrand() + " v" + getVersion().left(14).trimmed() + " - " + processModelName(frogpilotUIState()->frogpilot_toggles.value("model_name").toString())); + branch_merge_banner->setVisible(deprecated_branches.contains(QString::fromStdString(params.get("GitBranch")))); bool updateAvailable = update_widget->refresh(); int alerts = alerts_widget->refresh(); diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index d7e693579..f7468e656 100644 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -41,6 +41,7 @@ private: OffroadAlert* alerts_widget; QPushButton* alert_notif; QPushButton* update_notif; + QLabel* branch_merge_banner; // FrogPilot variables ElidedLabel* date; diff --git a/system/manager/manager.py b/system/manager/manager.py index cfa28165a..075eac381 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -92,6 +92,63 @@ def manager_init() -> None: params.put_bool("IsTestedBranch", build_metadata.tested_channel) params.put_bool("IsReleaseBranch", build_metadata.release_channel) + # Legacy Bolt fingerprint migration after branch consolidation + bolt_source_branch_file = "/data/media/0/starpilot_source_branch" + bolt_fingerprint_migration_flag_file = "/data/media/0/frogpilot_bolt_fingerprint_migrated.flag" + source_branch = "" + try: + if os.path.exists(bolt_source_branch_file): + with open(bolt_source_branch_file, encoding="utf-8") as f: + source_branch = f.read().strip() + except OSError: + cloudlog.exception("failed reading StarPilot source branch file") + + migration_branch = source_branch or build_metadata.channel + replacements = {} + if migration_branch in {"TorqueTune", "TorquePedal"}: + replacements = { + "CHEVROLET_BOLT_EUV": "CHEVROLET_BOLT_ACC_2022_2023", + "CHEVROLET_BOLT_CC": "CHEVROLET_BOLT_CC_2022_2023", + } + elif migration_branch in {"TotallyTune", "StarPilot-2017", "StarPilot 2017"}: + replacements = { + "CHEVROLET_BOLT_CC": "CHEVROLET_BOLT_CC_2017", + } + elif migration_branch in {"StarPilot"}: + replacements = { + "CHEVROLET_BOLT_CC": "CHEVROLET_BOLT_CC_2019_2021", + } + + migrated_values = [] + if replacements: + for param_key in ("CarModel", "CarModelName"): + current_value = params.get(param_key, encoding='utf-8') + normalized_value = current_value[4:] if current_value is not None and current_value.startswith("CAR.") else current_value + if normalized_value in replacements: + new_value = replacements[normalized_value] + params.put(param_key, new_value) + params_cache.put(param_key, new_value) + migrated_values.append(f"{param_key}: {current_value} -> {new_value}") + + if migrated_values: + cloudlog.info(f"migrated legacy bolt fingerprint values from branch '{migration_branch}' (source='{source_branch}'): {', '.join(migrated_values)}") + + # Keep Bolt display label aligned with live fingerprint selection + bolt_models = { + "CHEVROLET_BOLT_EUV", + "CHEVROLET_BOLT_CC", + "CHEVROLET_BOLT_ACC_2022_2023", + "CHEVROLET_BOLT_CC_2022_2023", + "CHEVROLET_BOLT_CC_2019_2021", + "CHEVROLET_BOLT_CC_2017", + } + if (params.get("CarModel", encoding='utf-8') or "") in bolt_models: + params.remove("CarModelName") + params_cache.remove("CarModelName") + + with open(bolt_fingerprint_migration_flag_file, "w") as f: + f.write(migration_branch or "unknown") + # One-time migration to align FrogPilot defaults after install frogpilot_migration_flag_file = "/data/media/0/frogpilot_migrated.flag" if not os.path.exists(frogpilot_migration_flag_file): diff --git a/system/updated/updated.py b/system/updated/updated.py index 7643d58d1..07da17b2b 100644 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -36,6 +36,19 @@ OVERLAY_INIT = Path(os.path.join(BASEDIR, ".overlay_init")) DAYS_NO_CONNECTIVITY_MAX = 14 # do not allow to engage after this many days DAYS_NO_CONNECTIVITY_PROMPT = 10 # send an offroad prompt after this many days +MIGRATED_TARGET_BRANCH = "StarPilot" +MIGRATION_DONE_FILE = "/data/starpilot_branch_migrated" +MIGRATION_SOURCE_BRANCH_FILE = "/data/media/0/starpilot_source_branch" +MIGRATION_EXCLUDED_BRANCHES = {"Dom"} +MIGRATION_SOURCE_BRANCHES = { + "TorqueTune", + "TorquePedal", + "Kaofui", + "Red-Kao", + "TotallyTune", + "StarPilot-2017", + "TRX", +} class UserRequest: NONE = 0 @@ -280,6 +293,7 @@ class Updater: self.params = Params() self.branches = defaultdict(str) self._has_internet: bool = False + self._migrate_target_branch() @property def has_internet(self) -> bool: @@ -292,6 +306,31 @@ class Updater: b = self.get_branch(BASEDIR) return b + def _migrate_target_branch(self) -> None: + target_branch: str | None = self.params.get("UpdaterTargetBranch", encoding='utf-8') + current_branch = self.get_branch(BASEDIR) + if current_branch in MIGRATION_EXCLUDED_BRANCHES or target_branch in MIGRATION_EXCLUDED_BRANCHES: + cloudlog.info(f"skipping StarPilot branch migration on excluded branch: current={current_branch}, target={target_branch}") + return + if current_branch not in MIGRATION_SOURCE_BRANCHES and target_branch not in MIGRATION_SOURCE_BRANCHES: + cloudlog.info(f"skipping StarPilot branch migration on unmanaged branch: current={current_branch}, target={target_branch}") + return + + if current_branch != MIGRATED_TARGET_BRANCH: + try: + Path(MIGRATION_SOURCE_BRANCH_FILE).write_text(current_branch, encoding='utf-8') + except OSError: + cloudlog.exception(f"failed to persist source branch for migration: {MIGRATION_SOURCE_BRANCH_FILE}") + + if target_branch != MIGRATED_TARGET_BRANCH or current_branch != MIGRATED_TARGET_BRANCH: + self.params.put("UpdaterTargetBranch", MIGRATED_TARGET_BRANCH) + cloudlog.info(f"migrated updater target branch to {MIGRATED_TARGET_BRANCH} from target={target_branch}, current={current_branch}") + + try: + Path(MIGRATION_DONE_FILE).touch() + except OSError: + cloudlog.exception(f"failed to write migration flag file: {MIGRATION_DONE_FILE}") + @property def update_ready(self) -> bool: consistent_file = Path(os.path.join(FINALIZED, ".overlay_consistent"))