diff --git a/frogpilot/ui/qt/offroad/vehicle_settings.cc b/frogpilot/ui/qt/offroad/vehicle_settings.cc index e4c8afd3..0a3866d6 100644 --- a/frogpilot/ui/qt/offroad/vehicle_settings.cc +++ b/frogpilot/ui/qt/offroad/vehicle_settings.cc @@ -1,5 +1,95 @@ +#include + #include "frogpilot/ui/qt/offroad/vehicle_settings.h" +QStringList getCarNames(const QString &carMake, QMap &carModels) { + static const QHash makeToFolder = { + {"acura", "honda"}, + {"audi", "volkswagen"}, + {"buick", "gm"}, + {"cadillac", "gm"}, + {"chevrolet", "gm"}, + {"chrysler", "chrysler"}, + {"cupra", "volkswagen"}, + {"dodge", "chrysler"}, + {"ford", "ford"}, + {"genesis", "hyundai"}, + {"gmc", "gm"}, + {"holden", "gm"}, + {"honda", "honda"}, + {"hyundai", "hyundai"}, + {"jeep", "chrysler"}, + {"kia", "hyundai"}, + {"lexus", "toyota"}, + {"lincoln", "ford"}, + {"man", "volkswagen"}, + {"mazda", "mazda"}, + {"nissan", "nissan"}, + {"peugeot", "psa"}, + {"ram", "chrysler"}, + {"rivian", "rivian"}, + {"seat", "volkswagen"}, + {"škoda", "volkswagen"}, + {"subaru", "subaru"}, + {"tesla", "tesla"}, + {"toyota", "toyota"}, + {"volkswagen", "volkswagen"} + }; + + QStringList carNames; + + const QString folder = makeToFolder.value(carMake.toLower()); + if (folder.isEmpty()) { + return carNames; + } + + QFile file(QString("../../opendbc/car/%1/values.py").arg(folder)); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return carNames; + } + + QString content = file.readAll(); + file.close(); + + static const QRegularExpression commentRe("#[^\\n]*"); + static const QRegularExpression footnoteRe("footnotes=\\[[^\\]]*\\],\\s*"); + content.remove(commentRe).remove(footnoteRe); + + static const QRegularExpression platformRe("(\\w+)\\s*=\\s*\\w+\\s*\\("); + QRegularExpressionMatchIterator platformIt = platformRe.globalMatch(content); + + QVector> platforms; + while (platformIt.hasNext()) { + QRegularExpressionMatch match = platformIt.next(); + platforms.append({match.capturedStart(), match.captured(1)}); + } + platforms.append({content.length(), QString()}); + + static const QRegularExpression carNameRe("CarDocs\\w*\\s*\\(\\s*\"([^\"]+)\""); + const QString lowerMake = carMake.toLower(); + + for (int i = 0; i < platforms.size() - 1; ++i) { + int start = platforms[i].first; + int end = platforms[i + 1].first; + const QString &platformName = platforms[i].second; + + QRegularExpressionMatchIterator carIt = carNameRe.globalMatch( + content.mid(start, end - start) + ); + + while (carIt.hasNext()) { + QString carName = carIt.next().captured(1); + if (carName.startsWith(carMake, Qt::CaseInsensitive)) { + carModels[carName] = platformName; + carNames.append(carName); + } + } + } + + carNames.sort(Qt::CaseInsensitive); + return carNames; +} + FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { forceOpenDescriptions = forceOpen; @@ -12,6 +102,37 @@ FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent, vehiclesLayout->addWidget(vehiclesPanel); + QStringList makes = { + "Acura", "Audi", "Buick", "Cadillac", "Chevrolet", "Chrysler", "CUPRA", + "Dodge", "Ford", "Genesis", "GMC", "Holden", "Honda", "Hyundai", "Jeep", + "Kia", "Lexus", "Lincoln", "MAN", "Mazda", "Nissan", "Peugeot", "Ram", + "Rivian", "SEAT", "Škoda", "Subaru", "Tesla", "Toyota", "Volkswagen" + }; + + ButtonControl *selectMakeButton = new ButtonControl(tr("Car Make"), tr("SELECT")); + QObject::connect(selectMakeButton, &ButtonControl::clicked, [makes, selectMakeButton, this]() { + QString makeSelection = MultiOptionDialog::getSelection(tr("Choose your car make"), makes, "", this); + if (!makeSelection.isEmpty()) { + params.put("CarMake", makeSelection.toStdString()); + selectMakeButton->setValue(makeSelection); + } + }); + settingsList->addItem(selectMakeButton); + + ButtonControl *selectModelButton = new ButtonControl(tr("Car Model"), tr("SELECT")); + QObject::connect(selectModelButton, &ButtonControl::clicked, [selectModelButton, this]() { + QString modelSelection = MultiOptionDialog::getSelection(tr("Choose your car model"), getCarNames(QString::fromStdString(params.get("CarMake")).toLower(), carModels), "", this); + if (!modelSelection.isEmpty()) { + params.put("CarModel", carModels.value(modelSelection).toStdString()); + params.put("CarModelName", modelSelection.toStdString()); + selectModelButton->setValue(modelSelection); + } + }); + settingsList->addItem(selectModelButton); + + forceFingerprint = new ParamControl("ForceFingerprint", tr("Disable Automatic Fingerprint Detection"), tr("Force the selected fingerprint and prevent it from ever changing."), ""); + settingsList->addItem(forceFingerprint); + disableOpenpilotLong = new ParamControl("DisableOpenpilotLongitudinal", tr("Disable openpilot Longitudinal Control"), tr("Disable openpilot longitudinal and use the car's stock ACC instead."), ""); QObject::connect(disableOpenpilotLong, &ToggleControl::toggleFlipped, [parent, this](bool state) { if (state) { @@ -195,10 +316,18 @@ FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent, openDescriptions(forceOpenDescriptions, toggles); + QObject::connect(uiState(), &UIState::offroadTransition, [selectMakeButton, selectModelButton, this]() { + std::thread([selectMakeButton, selectModelButton, this]() { + selectMakeButton->setValue(QString::fromStdString(params.get("CarMake", true))); + selectModelButton->setValue(QString::fromStdString(params.get(params.get("CarModelName").empty() ? "CarModel" : "CarModelName", true))); + }).detach(); + }); + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [vehiclesLayout, vehiclesPanel, this] { if (forceOpenDescriptions) { openDescriptions(forceOpenDescriptions, toggles); disableOpenpilotLong->showDescription(); + forceFingerprint->showDescription(); } vehiclesLayout->setCurrentWidget(vehiclesPanel); }); @@ -208,6 +337,7 @@ FrogPilotVehiclesPanel::FrogPilotVehiclesPanel(FrogPilotSettingsWindow *parent, void FrogPilotVehiclesPanel::showEvent(QShowEvent *event) { if (forceOpenDescriptions) { disableOpenpilotLong->showDescription(); + forceFingerprint->showDescription(); } QStringList detected; @@ -298,6 +428,7 @@ void FrogPilotVehiclesPanel::updateToggles() { } disableOpenpilotLong->setVisible((parent->hasOpenpilotLongitudinal || parent->openpilotLongitudinalControlDisabled) && !parent->hasAlphaLongitudinal && parent->tuningLevel >= parent->frogpilotToggleLevels["DisableOpenpilotLongitudinal"].toBool()); + forceFingerprint->setVisible(parent->tuningLevel >= parent->frogpilotToggleLevels["ForceFingerprint"].toBool()); openDescriptions(forceOpenDescriptions, toggles); diff --git a/frogpilot/ui/qt/offroad/vehicle_settings.h b/frogpilot/ui/qt/offroad/vehicle_settings.h index 4494e56d..0c8487b7 100644 --- a/frogpilot/ui/qt/offroad/vehicle_settings.h +++ b/frogpilot/ui/qt/offroad/vehicle_settings.h @@ -35,6 +35,9 @@ private: FrogPilotSettingsWindow *parent; ParamControl *disableOpenpilotLong; + ParamControl *forceFingerprint; Params params; + + QMap carModels; }; diff --git a/opendbc_repo/opendbc/car/car_helpers.py b/opendbc_repo/opendbc/car/car_helpers.py index e2f1040b..2862bef4 100644 --- a/opendbc_repo/opendbc/car/car_helpers.py +++ b/opendbc_repo/opendbc/car/car_helpers.py @@ -14,6 +14,7 @@ from opendbc.car.mock.values import CAR as MOCK from opendbc.car.toyota.values import ToyotaFrogPilotFlags from opendbc.car.values import BRANDS from opendbc.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN +from openpilot.common.params import Params FRAME_FINGERPRINT = 100 # 1s @@ -156,12 +157,18 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, alpha_long_allowed: bool, - is_release: bool, num_pandas: int = 1, cached_params: CarParamsT | None = None, frogpilot_toggles: SimpleNamespace = None): + is_release: bool, params: Params, num_pandas: int = 1, cached_params: CarParamsT | None = None, frogpilot_toggles: SimpleNamespace = None): candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, num_pandas, cached_params) - if candidate is None: - carlog.error({"event": "car doesn't match any fingerprints", "fingerprints": repr(fingerprints)}) - candidate = "MOCK" + if candidate is None or frogpilot_toggles.force_fingerprint: + if frogpilot_toggles.force_fingerprint: + candidate = frogpilot_toggles.car_model + else: + carlog.error({"event": "car doesn't match any fingerprints", "fingerprints": repr(fingerprints)}) + candidate = "MOCK" + else: + params.put_nonblocking("CarMake", candidate.split('_')[0].title()) + params.put_nonblocking("CarModel", str(candidate)) if frogpilot_toggles.block_user: candidate = "MOCK" diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 77aba7e4..7195f4ab 100644 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -106,7 +106,7 @@ class Car: with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, cached_params, get_frogpilot_toggles()) + self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, self.params, num_pandas, cached_params, get_frogpilot_toggles()) self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP) self.CP = self.CI.CP