mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-08 05:42:06 +08:00
Driving Model Selector
This commit is contained in:
@@ -6,6 +6,11 @@ sunnypilot - 0.9.5.2 (2023-xx-xx)
|
||||
* NEW❗: Neural Network Lateral Controller
|
||||
* Formerly known as "NNFF", this replaces the lateral "torque" controller with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy
|
||||
* Contact @twilsonco in the sunnypilot Discord server with feedback, or to provide log data for your car if your car is currently unsupported
|
||||
* NEW❗: Driving Model Selector
|
||||
* Easily switch between driving models without reinstalling branches. Offering immediate access to the latest models upon release
|
||||
* An internet connection is required for downloading models. Each model switch currently involves downloading the model again. Future updates may allow for offline switching
|
||||
* Warning is displayed for metered connections to avoid unexpected data usage if on cellular data
|
||||
* Change driving models via **Settings -> Software -> Current Driving Model**.
|
||||
* NEW❗: Hyundai CAN longitudinal:
|
||||
* NEW❗: Enable radar tracks for certain Santa Fe platforms
|
||||
* Internal Combustion Engine (ICE) 2021-23
|
||||
|
||||
@@ -226,6 +226,7 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"CarModelText", PERSISTENT},
|
||||
{"ChevronInfo", PERSISTENT},
|
||||
{"CustomBootScreen", PERSISTENT},
|
||||
{"CustomDrivingModel", PERSISTENT},
|
||||
{"CustomMapboxTokenPk", PERSISTENT},
|
||||
{"CustomMapboxTokenSk", PERSISTENT},
|
||||
{"CustomOffsets", PERSISTENT},
|
||||
@@ -234,6 +235,9 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"DevUIInfo", PERSISTENT},
|
||||
{"DisableOnroadUploads", PERSISTENT},
|
||||
{"DisengageLateralOnBrake", PERSISTENT},
|
||||
{"DrivingModelName", PERSISTENT},
|
||||
{"DrivingModelText", PERSISTENT},
|
||||
{"DrivingModelUrl", PERSISTENT},
|
||||
{"DynamicExperimentalControl", PERSISTENT},
|
||||
{"DynamicLaneProfile", PERSISTENT},
|
||||
{"EnableAmap", PERSISTENT},
|
||||
|
||||
@@ -32,6 +32,9 @@ MODEL_PATHS = {
|
||||
|
||||
METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl'
|
||||
|
||||
CUSTOM_MODEL_PATH = "/data/media/0/models"
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
frame_id: int = 0
|
||||
timestamp_sof: int = 0
|
||||
@@ -70,7 +73,15 @@ class ModelState:
|
||||
self.output = np.zeros(net_output_size, dtype=np.float32)
|
||||
self.parser = Parser()
|
||||
|
||||
self.model = ModelRunner(MODEL_PATHS, self.output, Runtime.GPU, False, context)
|
||||
self.param_s = Params()
|
||||
|
||||
if self.param_s.get_bool("CustomDrivingModel"):
|
||||
_model_name = self.param_s.get("DrivingModelText", encoding="utf8")
|
||||
_model_paths = {
|
||||
ModelRunner.THNEED: f"{CUSTOM_MODEL_PATH}/supercombo-{_model_name}.thneed"}
|
||||
else:
|
||||
_model_paths = MODEL_PATHS
|
||||
self.model = ModelRunner(_model_paths, self.output, Runtime.GPU, False, context)
|
||||
self.model.addInput("input_imgs", None)
|
||||
self.model.addInput("big_input_imgs", None)
|
||||
for k,v in self.inputs.items():
|
||||
|
||||
@@ -32,7 +32,8 @@ widgets_src += ["qt/offroad/sunnypilot/display_settings.cc", "qt/offroad/sunnypi
|
||||
"qt/offroad/sunnypilot/trips_settings.cc", "qt/offroad/sunnypilot/mads_settings.cc",
|
||||
"qt/offroad/sunnypilot/lane_change_settings.cc", "qt/offroad/sunnypilot/speed_limit_control_settings.cc",
|
||||
"qt/offroad/sunnypilot/monitoring_settings.cc", "qt/offroad/sunnypilot/osm_settings.cc",
|
||||
"qt/offroad/sunnypilot/custom_offsets_settings.cc", "qt/widgets/sunnypilot/drive_stats.cc"]
|
||||
"qt/offroad/sunnypilot/custom_offsets_settings.cc", "qt/widgets/sunnypilot/drive_stats.cc",
|
||||
"qt/offroad/sunnypilot/software_settings_sp.cc", "qt/offroad/sunnypilot/models_fetcher.cc"]
|
||||
|
||||
qt_env['CPPDEFINES'] = []
|
||||
if maps:
|
||||
|
||||
@@ -545,7 +545,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
|
||||
PanelInfo(" " + tr("Device"), device, "../assets/navigation/icon_home.svg"),
|
||||
PanelInfo(" " + tr("Network"), new Networking(this), "../assets/offroad/icon_network.png"),
|
||||
PanelInfo(" " + tr("Toggles"), toggles, "../assets/offroad/icon_toggle.png"),
|
||||
PanelInfo(" " + tr("Software"), new SoftwarePanel(this), "../assets/offroad/icon_software.png"),
|
||||
PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../assets/offroad/icon_software.png"),
|
||||
PanelInfo(" " + tr("sunnypilot"), new SunnypilotPanel(this), "../assets/offroad/icon_openpilot.png"),
|
||||
PanelInfo(" " + tr("OSM"), new OsmPanel(this), "../assets/offroad/icon_map.png"),
|
||||
PanelInfo(" " + tr("Monitoring"), new MonitoringPanel(this), "../assets/offroad/icon_monitoring.png"),
|
||||
|
||||
@@ -94,9 +94,9 @@ class SoftwarePanel : public ListWidget {
|
||||
public:
|
||||
explicit SoftwarePanel(QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void updateLabels();
|
||||
virtual void updateLabels();
|
||||
void checkForUpdates();
|
||||
|
||||
bool is_onroad = false;
|
||||
|
||||
@@ -22,7 +22,7 @@ void SoftwarePanel::checkForUpdates() {
|
||||
}
|
||||
|
||||
SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) {
|
||||
currentModelLbl = new LabelControl(tr("Current Driving Model"), CURRENT_MODEL);
|
||||
currentModelLbl = new LabelControl(tr("Driving Model"), CURRENT_MODEL);
|
||||
addItem(currentModelLbl);
|
||||
|
||||
onroadLbl = new QLabel(tr("Updates are only downloaded while the car is off."));
|
||||
|
||||
@@ -20,7 +20,7 @@ public:
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if(reply->error() != QNetworkReply::NoError) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Failed to fetch data from URL: " << reply->errorString();
|
||||
return QJsonObject();
|
||||
}
|
||||
@@ -32,4 +32,4 @@ public:
|
||||
reply->deleteLater();
|
||||
return json;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "selfdrive/ui/qt/offroad/sunnypilot/models_fetcher.h"
|
||||
|
||||
ModelsFetcher::ModelsFetcher(QObject* parent) : QObject(parent) {
|
||||
manager = new QNetworkAccessManager(this);
|
||||
}
|
||||
|
||||
void ModelsFetcher::download(const QUrl& url, const QString& filename, const QString& destinationPath) {
|
||||
if (!QDir(destinationPath).exists() && !QDir().mkpath(destinationPath)) {
|
||||
LOGE("Failed to create directory: [%s]", destinationPath.toStdString().c_str());
|
||||
}
|
||||
const QNetworkRequest request(url);
|
||||
QNetworkReply* reply = manager->get(request);
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, &ModelsFetcher::onDownloadProgress);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, destinationPath, filename]() {
|
||||
onFinished(reply, destinationPath, filename);
|
||||
});
|
||||
}
|
||||
|
||||
QString extractFileName(const QString& contentDisposition) {
|
||||
const QString filenameTag = "filename=";
|
||||
const int idx = contentDisposition.indexOf(filenameTag);
|
||||
if (idx < 0) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString filename = contentDisposition.mid(idx + filenameTag.length());
|
||||
if (filename.startsWith("\"") && filename.endsWith("\"")) {
|
||||
return filename.mid(1, filename.size() - 2);
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
void ModelsFetcher::onFinished(QNetworkReply* reply, const QString& destinationPath, const QString& filename) {
|
||||
// Handle download error
|
||||
if (reply->error()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray data = reply->readAll();
|
||||
|
||||
QString finalFilename = filename;
|
||||
if (finalFilename.isEmpty()) {
|
||||
finalFilename = extractFileName(reply->header(QNetworkRequest::ContentDispositionHeader).toString());
|
||||
}
|
||||
|
||||
QString finalPath = QDir(destinationPath).filePath(finalFilename);
|
||||
|
||||
// handle file open error
|
||||
QFile file(finalPath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
file.write(data);
|
||||
file.close();
|
||||
|
||||
emit downloadComplete(data);
|
||||
}
|
||||
|
||||
void ModelsFetcher::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
|
||||
const double progress = (bytesReceived * 100.0) / bytesTotal;
|
||||
emit downloadProgress(progress);
|
||||
}
|
||||
|
||||
std::vector<Model> ModelsFetcher::getModelsFromURL(const QUrl&url) {
|
||||
std::vector<Model> models;
|
||||
JsonFetcher fetcher;
|
||||
QJsonObject json = fetcher.getJsonFromURL(url.toString());
|
||||
for (auto it = json.begin(); it != json.end(); ++it) {
|
||||
models.push_back(Model(it.value().toObject()));
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
std::vector<Model> ModelsFetcher::getModelsFromURL(const QString&url) {
|
||||
return getModelsFromURL(QUrl(url));
|
||||
}
|
||||
|
||||
std::vector<Model> ModelsFetcher::getModelsFromURL() {
|
||||
return getModelsFromURL("https://docs.sunnypilot.ai/models.json");
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm> // for std::sort
|
||||
#include <cassert>
|
||||
#include <deque>
|
||||
#include <QDir>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/offroad/sunnypilot/json_fetcher.h"
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
static const QString MODELS_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/models/" : "/data/media/0/models/";
|
||||
|
||||
// New class ModelsFetcher with a new function that handles web requests and JSON parsing for the new JSON structure
|
||||
class Model {
|
||||
|
||||
public:
|
||||
explicit Model(const QJsonObject&json) {
|
||||
displayName = json["display_name"].toString();
|
||||
fullName = json["full_name"].toString();
|
||||
fileName = json["file_name"].toString();
|
||||
downloadUri = json["download_uri"].toString();
|
||||
}
|
||||
|
||||
QJsonObject toJson() const {
|
||||
QJsonObject json;
|
||||
json["display_name"] = displayName;
|
||||
json["full_name"] = fullName;
|
||||
json["file_name"] = fileName;
|
||||
json["download_uri"] = downloadUri;
|
||||
return json;
|
||||
}
|
||||
|
||||
QString displayName;
|
||||
QString fullName;
|
||||
QString fileName;
|
||||
QString downloadUri;
|
||||
};
|
||||
|
||||
class ModelsFetcher : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ModelsFetcher(QObject* parent = nullptr);
|
||||
void download(const QUrl&url, const QString& filename = "", const QString&destinationPath = MODELS_PATH);
|
||||
static std::vector<Model> getModelsFromURL(const QUrl&url);
|
||||
static std::vector<Model> getModelsFromURL(const QString&url);
|
||||
static std::vector<Model> getModelsFromURL();
|
||||
|
||||
signals:
|
||||
void downloadProgress(double percentage);
|
||||
void downloadComplete(const QByteArray&data);
|
||||
|
||||
private:
|
||||
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
void onFinished(QNetworkReply* reply, const QString&destinationPath, const QString&filename);
|
||||
|
||||
QNetworkAccessManager* manager;
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "selfdrive/ui/qt/offroad/sunnypilot/software_settings_sp.h"
|
||||
|
||||
SoftwarePanelSP::SoftwarePanelSP(QWidget* parent) : SoftwarePanel(parent) {
|
||||
// Get current model name and create new ButtonControl
|
||||
const auto current_model = GetModelName();
|
||||
currentModelLblBtn = new ButtonControl(tr("Driving Model"), tr("SELECT"), current_model);
|
||||
currentModelLblBtn->setValue(current_model);
|
||||
|
||||
// Connect downloadProgress from models_fetcher to local slot
|
||||
connect(&models_fetcher, &ModelsFetcher::downloadProgress, this, [=](const double progress) {
|
||||
modelDownloadProgress = progress;
|
||||
HandleModelDownloadProgressReport();
|
||||
});
|
||||
|
||||
// Connect click event from currentModelLblBtn to local slot
|
||||
connect(currentModelLblBtn, &ButtonControl::clicked, this, &SoftwarePanelSP::handleCurrentModelLblBtnClicked);
|
||||
|
||||
ReplaceOrAddWidget(currentModelLbl, currentModelLblBtn);
|
||||
}
|
||||
|
||||
QString SoftwarePanelSP::GetModelName() {
|
||||
if (selectedModelToDownload.has_value()) {
|
||||
return selectedModelToDownload->displayName;
|
||||
}
|
||||
|
||||
if (params.getBool("CustomDrivingModel")) {
|
||||
return QString::fromStdString(params.get("DrivingModelName"));
|
||||
}
|
||||
|
||||
return CURRENT_MODEL;
|
||||
}
|
||||
|
||||
void SoftwarePanelSP::HandleModelDownloadProgressReport() {
|
||||
const auto _progress_str = QString::number(modelDownloadProgress, 'f', 2);
|
||||
const auto description = isDownloadingModel() ? QString("Downloading [%1]... (%2%)") : QString("%1 downloaded");
|
||||
|
||||
// Update UI with new description
|
||||
currentModelLblBtn->setDescription(description.arg(GetModelName(), _progress_str));
|
||||
currentModelLblBtn->showDescription();
|
||||
currentModelLblBtn->setEnabled(!(is_onroad || isDownloadingModel()));
|
||||
|
||||
// If not downloading and there is a selected model, update parameters
|
||||
if (!isDownloadingModel() && selectedModelToDownload.has_value()) {
|
||||
params.put("DrivingModelText", selectedModelToDownload->fullName.toStdString());
|
||||
params.put("DrivingModelName", selectedModelToDownload->displayName.toStdString());
|
||||
//params.put("DrivingModelUrl", selectedModelToDownload->downloadUri.toStdString()); // TODO: Placeholder for future implementations
|
||||
selectedModelToDownload.reset();
|
||||
params.putBool("CustomDrivingModel", true);
|
||||
}
|
||||
}
|
||||
|
||||
void SoftwarePanelSP::handleCurrentModelLblBtnClicked() {
|
||||
// Disabling label button and displaying fetching message
|
||||
currentModelLblBtn->setEnabled(false);
|
||||
currentModelLblBtn->setValue("Fetching models...");
|
||||
|
||||
checkNetwork();
|
||||
const auto currentModelName = QString::fromStdString(params.get("DrivingModelName"));
|
||||
const auto models = models_fetcher.getModelsFromURL();
|
||||
QStringList modelNames;
|
||||
|
||||
// Collecting model names
|
||||
for (const auto &model: models) {
|
||||
modelNames.push_back(model.displayName);
|
||||
}
|
||||
|
||||
currentModelLblBtn->setEnabled(!is_onroad);
|
||||
currentModelLblBtn->setValue(GetModelName());
|
||||
|
||||
const QString selectedModelName = MultiOptionDialog::getSelection(tr("Select a Driving Model"), modelNames, currentModelName, this);
|
||||
|
||||
// Bail if the user doesn't want to continue while on metered
|
||||
if (!canContinueOnMeteredDialog()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Finding and setting the selected model
|
||||
for (auto &model: models) {
|
||||
if (model.displayName == selectedModelName) {
|
||||
selectedModelToDownload = model;
|
||||
params.putBool("CustomDrivingModel", false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If decision is to download and there is a selected model, update UI and begin downloading
|
||||
if (selectedModelToDownload.has_value()) {
|
||||
currentModelLblBtn->setValue(selectedModelToDownload->displayName);
|
||||
currentModelLblBtn->setDescription(selectedModelToDownload->displayName);
|
||||
models_fetcher.download(selectedModelToDownload->downloadUri, selectedModelToDownload->fileName);
|
||||
|
||||
// Disable select button until download completes
|
||||
currentModelLblBtn->setEnabled(false);
|
||||
showResetParamsDialog();
|
||||
}
|
||||
}
|
||||
|
||||
void SoftwarePanelSP::checkNetwork() {
|
||||
const SubMaster &sm = *(uiState()->sm);
|
||||
const auto device_state = sm["deviceState"].getDeviceState();
|
||||
const auto network_type = device_state.getNetworkType();
|
||||
is_wifi = network_type == cereal::DeviceState::NetworkType::WIFI;
|
||||
is_metered = device_state.getNetworkMetered();
|
||||
}
|
||||
|
||||
void SoftwarePanelSP::updateLabels() {
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkNetwork();
|
||||
currentModelLblBtn->setEnabled(!is_onroad);
|
||||
SoftwarePanel::updateLabels();
|
||||
}
|
||||
|
||||
void SoftwarePanelSP::showResetParamsDialog() {
|
||||
const auto confirmMsg = tr("Download has started in the background.\nWe STRONGLY suggest you to reset calibration, would you like to do that now?");
|
||||
const auto button_text = tr("Reset Calibration");
|
||||
|
||||
// If user confirms, remove specified parameters
|
||||
if (showConfirmationDialog(confirmMsg, button_text, false)) {
|
||||
params.remove("CalibrationParams");
|
||||
params.remove("LiveTorqueParameters");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/model.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||
#include "selfdrive/ui/qt/offroad/sunnypilot/models_fetcher.h"
|
||||
|
||||
class SoftwarePanelSP final : public SoftwarePanel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SoftwarePanelSP(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
QString GetModelName();
|
||||
void checkNetwork();
|
||||
bool isDownloadingModel() const {
|
||||
return selectedModelToDownload.has_value() && modelDownloadProgress > 0.0 && modelDownloadProgress < 100.0;
|
||||
}
|
||||
|
||||
// UI update related methods
|
||||
void updateLabels() override;
|
||||
void handleCurrentModelLblBtnClicked();
|
||||
void HandleModelDownloadProgressReport();
|
||||
void showResetParamsDialog();
|
||||
bool canContinueOnMeteredDialog() {
|
||||
if (!is_metered) return true;
|
||||
return showConfirmationDialog(QString(), QString(), is_metered);
|
||||
}
|
||||
|
||||
inline bool showConfirmationDialog(const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) {
|
||||
return showConfirmationDialog(this, message, confirmButtonText, show_metered_warning);
|
||||
}
|
||||
|
||||
static inline bool showConfirmationDialog(QWidget *parent, const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) {
|
||||
const QString warning_message = show_metered_warning ? tr("Warning: You are on a metered connection!") : QString();
|
||||
const QString final_message = QString("%1%2").arg(!message.isEmpty() ? message+"\n" : QString(), warning_message);
|
||||
const QString final_buttonText = !confirmButtonText.isEmpty() ? confirmButtonText : QString("Continue%1").arg(show_metered_warning ? " on Metered" : "");
|
||||
|
||||
return ConfirmationDialog::confirm(final_message, final_buttonText, parent);
|
||||
}
|
||||
|
||||
bool is_metered;
|
||||
bool is_wifi;
|
||||
double modelDownloadProgress = 0.0;
|
||||
std::optional<Model> selectedModelToDownload;
|
||||
ButtonControl *currentModelLblBtn;
|
||||
ModelsFetcher models_fetcher;
|
||||
};
|
||||
@@ -7,3 +7,4 @@
|
||||
#include "selfdrive/ui/qt/offroad/sunnypilot/trips_settings.h"
|
||||
#include "selfdrive/ui/qt/offroad/sunnypilot/monitoring_settings.h"
|
||||
#include "selfdrive/ui/qt/offroad/sunnypilot/osm_settings.h"
|
||||
#include "selfdrive/ui/qt/offroad/sunnypilot/software_settings_sp.h"
|
||||
|
||||
@@ -373,6 +373,23 @@ class ListWidget : public QWidget {
|
||||
inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); }
|
||||
inline void setSpacing(int spacing) { inner_layout.setSpacing(spacing); }
|
||||
|
||||
inline void AddWidgetAt(const int index, QWidget *new_widget) { inner_layout.insertWidget(index, new_widget); }
|
||||
inline void RemoveWidgetAt(const int index) {
|
||||
if (QLayoutItem* item; (item = inner_layout.takeAt(index)) != nullptr) {
|
||||
if(item->widget()) delete item->widget();
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
inline void ReplaceOrAddWidget(QWidget *old_widget, QWidget *new_widget) {
|
||||
if (const int index = inner_layout.indexOf(old_widget); index != -1) {
|
||||
RemoveWidgetAt(index);
|
||||
AddWidgetAt(index, new_widget);
|
||||
} else {
|
||||
addItem(new_widget);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *) override {
|
||||
QPainter p(this);
|
||||
|
||||
Reference in New Issue
Block a user