Driving Model Selector

This commit is contained in:
Jason Wen
2023-12-05 05:15:30 +00:00
parent f6977cc925
commit faa1ecbbbe
14 changed files with 368 additions and 8 deletions
+5
View File
@@ -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
+4
View File
@@ -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},
+12 -1
View File
@@ -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():
+2 -1
View File
@@ -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:
+1 -1
View File
@@ -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"),
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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"
+17
View File
@@ -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);