Merge branch 'master-new' into hkg-angle-steering-2025

# Conflicts:
#	common/params_keys.h
This commit is contained in:
DevTekVE
2025-06-07 15:04:16 +02:00
44 changed files with 1960 additions and 51 deletions
+7 -1
View File
@@ -226,7 +226,13 @@ struct BackupManagerSP @0xf98d843bfd7004a3 {
struct CarStateSP @0xb86e6369214c01c8 {
}
struct CustomReserved8 @0xf416ec09499d9d19 {
struct LiveMapDataSP @0xf416ec09499d9d19 {
speedLimitValid @0 :Bool;
speedLimit @1 :Float32;
speedLimitAheadValid @2 :Bool;
speedLimitAhead @3 :Float32;
speedLimitAheadDistance @4 :Float32;
roadName @5 :Text;
}
struct CustomReserved9 @0xa1680744031fdb2d {
+1 -1
View File
@@ -2610,7 +2610,7 @@ struct Event {
carControlSP @112 :Custom.CarControlSP;
backupManagerSP @113 :Custom.BackupManagerSP;
carStateSP @114 :Custom.CarStateSP;
customReserved8 @115 :Custom.CustomReserved8;
liveMapDataSP @115 :Custom.LiveMapDataSP;
customReserved9 @116 :Custom.CustomReserved9;
customReserved10 @136 :Custom.CustomReserved10;
customReserved11 @137 :Custom.CustomReserved11;
+1
View File
@@ -84,6 +84,7 @@ _services: dict[str, tuple] = {
"carParamsSP": (True, 0.02, 1),
"carControlSP": (True, 100., 10),
"carStateSP": (True, 100., 10),
"liveMapDataSP": (True, 1., 1),
# debug
"uiDebug": (True, 0., 1),
+21 -1
View File
@@ -131,12 +131,12 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"CarParamsSPCache", CLEAR_ON_MANAGER_START},
{"CarParamsSPPersistent", PERSISTENT},
{"CarPlatformBundle", PERSISTENT},
{"DeviceBootMode", PERSISTENT | BACKUP},
{"EnableGithubRunner", PERSISTENT | BACKUP},
{"MaxTimeOffroad", PERSISTENT | BACKUP},
{"Brightness", PERSISTENT | BACKUP},
{"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
{"OffroadMode", CLEAR_ON_MANAGER_START},
{"OffroadMode_Status", CLEAR_ON_MANAGER_START},
{"QuietMode", PERSISTENT | BACKUP},
// MADS params
@@ -176,6 +176,26 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
// model panel params
{"LagdToggle", PERSISTENT | BACKUP},
// mapd
{"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"MapdVersion", PERSISTENT},
{"MapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"NextMapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"Offroad_OSMUpdateRequired", CLEAR_ON_MANAGER_START},
{"OsmDbUpdatesCheck", CLEAR_ON_MANAGER_START}, // mapd database update happens with device ON, reset on boot
{"OSMDownloadBounds", PERSISTENT},
{"OsmDownloadedDate", PERSISTENT},
{"OSMDownloadLocations", PERSISTENT},
{"OSMDownloadProgress", CLEAR_ON_MANAGER_START},
{"OsmLocal", PERSISTENT},
{"OsmLocationName", PERSISTENT},
{"OsmLocationTitle", PERSISTENT},
{"OsmLocationUrl", PERSISTENT},
{"OsmStateName", PERSISTENT},
{"OsmStateTitle", PERSISTENT},
{"OsmWayTest", PERSISTENT},
{"RoadName", CLEAR_ON_ONROAD_TRANSITION},
// Tuning keys
{"HkgTuningAngleSmoothingFactor", PERSISTENT | BACKUP},
{"HkgTuningAngleMinTorque", PERSISTENT | BACKUP},
+3 -3
View File
@@ -45,8 +45,8 @@
"text": "sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.",
"severity": 0
},
"OffroadMode_Status": {
"text": "sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to \"Settings\" -> \"Device\" to exit Always Offroad mode.",
"severity": 1
"Offroad_OSMUpdateRequired": {
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
"severity": 0
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
main_layout->setContentsMargins(40, 40, 40, 40);
// top header
QHBoxLayout* header_layout = new QHBoxLayout();
header_layout = new QHBoxLayout();
header_layout->setContentsMargins(0, 0, 0, 0);
header_layout->setSpacing(16);
+4 -2
View File
@@ -39,11 +39,13 @@ public:
protected:
QHBoxLayout *home_layout;
QHBoxLayout *header_layout;
void showEvent(QShowEvent *event) override;
void refresh();
private:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void refresh();
Params params;
+8 -1
View File
@@ -19,6 +19,7 @@ qt_src = [
"sunnypilot/qt/sidebar.cc",
"sunnypilot/qt/window.cc",
"sunnypilot/qt/home.cc",
"sunnypilot/qt/offroad/exit_offroad_button.cc",
"sunnypilot/qt/offroad/offroad_home.cc",
"sunnypilot/qt/offroad/settings/device_panel.cc",
"sunnypilot/qt/offroad/settings/lateral_panel.cc",
@@ -26,6 +27,7 @@ qt_src = [
"sunnypilot/qt/offroad/settings/max_time_offroad.cc",
"sunnypilot/qt/offroad/settings/brightness.cc",
"sunnypilot/qt/offroad/settings/models_panel.cc",
"sunnypilot/qt/offroad/settings/osm_panel.cc",
"sunnypilot/qt/offroad/settings/settings.cc",
"sunnypilot/qt/offroad/settings/software_panel.cc",
"sunnypilot/qt/offroad/settings/sunnylink_panel.cc",
@@ -55,6 +57,10 @@ network_src = [
"sunnypilot/qt/network/sunnylink/services/user_service.cc",
]
osm_panel_qt_src = [
"sunnypilot/qt/offroad/settings/osm/models_fetcher.cc",
]
vehicle_panel_qt_src = [
"sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc",
"sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc",
@@ -76,8 +82,9 @@ brand_settings_qt_src = [
"sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc",
]
sp_widgets_src = widgets_src + network_src
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src
sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + osm_panel_qt_src
sp_qt_util = qt_util
Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util")
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QEventLoop>
class JsonFetcher {
public:
static QJsonObject getJsonFromURL(const QString &url) {
const auto qurl = QUrl(url);
QNetworkAccessManager manager;
const QNetworkRequest request(qurl);
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
// Send GET request
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Failed to fetch data from URL: " << reply->errorString();
return QJsonObject();
}
const QByteArray responseData = reply->readAll();
const QJsonDocument doc = QJsonDocument::fromJson(responseData);
QJsonObject json = doc.object();
reply->deleteLater();
return json;
}
};
@@ -0,0 +1,100 @@
#include <QDebug>
#include <QHBoxLayout>
#include <QPainter>
#include <QPainterPath>
#include <QStyle>
#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/widgets/input.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h"
ExitOffroadButton::ExitOffroadButton(QWidget *parent) : QPushButton(parent), glowTimer(new QTimer(this)) {
setMouseTracking(true);
connect(glowTimer, &QTimer::timeout, this, [this]() {
// Pulse alpha up and down
glowAlpha += glowDelta;
if (glowAlpha > 220 || glowAlpha < 10) {
glowDelta *= -1;
}
update(); // trigger repaint
});
glowTimer->start(45);
pixmap = QPixmap("../../sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png").scaledToWidth(img_width, Qt::SmoothTransformation);
// go to toggles and expand experimental mode description
connect(this, &QPushButton::clicked, [=]() {
if (ConfirmationDialog::confirm(tr("Are you sure you want to exit Always Offroad mode?"), tr("Confirm"), this)) {
params.remove("OffroadMode");
}
});
setFixedHeight(125);
QHBoxLayout *main_layout = new QHBoxLayout;
main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0);
mode_label = new QLabel(tr("EXIT ALWAYS OFFROAD MODE"));
mode_icon = new QLabel;
mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
mode_icon->setPixmap(pixmap);
main_layout->addWidget(mode_label, 1, Qt::AlignLeft);
main_layout->addWidget(mode_icon, 0, Qt::AlignRight);
setLayout(main_layout);
setStyleSheet(R"(
QPushButton {
border: none;
}
QLabel {
font-size: 45px;
font-weight: 300;
text-align: left;
font-family: JetBrainsMono;
color: #000000;
}
)");
}
void drawPulsingGlowOverlay(QPainter &p, QPainterPath path, int glowAlpha) {
// Draw pulsing glow effect clipped to button area
p.save();
p.setClipPath(path);
p.setCompositionMode(QPainter::CompositionMode_HardLight);
const QColor animatedGlowColor(255, 255, 255, std::min(255, glowAlpha));
QPen glowPen(animatedGlowColor, 8);
glowPen.setJoinStyle(Qt::RoundJoin);
p.setPen(glowPen);
p.drawPath(path);
p.restore();
}
void ExitOffroadButton::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
QPainterPath path;
path.addRoundedRect(rect(), 10, 10);
// gradient
bool pressed = isDown();
QLinearGradient gradient(rect().left(), 0, rect().right(), 0);
gradient.setColorAt(0, QColor(35, 149, 255, pressed ? 0xcc : 0xff));
gradient.setColorAt(0.3, QColor(35, 149, 255, pressed ? 0xcc : 0xff));
gradient.setColorAt(1, QColor(20, 255, 171, pressed ? 0xcc : 0xff));
p.fillPath(path, gradient);
drawPulsingGlowOverlay(p, path, glowAlpha);
// vertical line
p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine));
int line_x = rect().right() - img_width - (2 * horizontal_padding);
p.drawLine(line_x, rect().bottom(), line_x, rect().top());
}
@@ -0,0 +1,29 @@
#pragma once
#include <QLabel>
#include <QPushButton>
#include "common/params.h"
class ExitOffroadButton : public QPushButton {
Q_OBJECT
private:
QTimer *glowTimer;
int glowAlpha = 100; // Current alpha of glow
int glowDelta = 10; // Change per tick
public:
explicit ExitOffroadButton(QWidget* parent = 0);
Params params;
bool offroad_mode;
int img_width = 100;
int horizontal_padding = 30;
QPixmap pixmap;
QLabel *mode_label;
QLabel *mode_icon;
protected:
void paintEvent(QPaintEvent *event) override;
};
@@ -5,16 +5,45 @@
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h"
#include <QStackedWidget>
#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h"
OffroadHomeSP::OffroadHomeSP(QWidget *parent) : OffroadHome(parent) {
QStackedWidget *left_widget = new QStackedWidget(this);
left_widget->addWidget(new DriveStats(this));
QFrame *left_widget = new QFrame(this);
QVBoxLayout *left_layout = new QVBoxLayout(left_widget);
left_layout->setContentsMargins(0, 0, 0, 0);
left_layout->setSpacing(30);
btn_exit_offroad = new ExitOffroadButton(this);
QObject::connect(btn_exit_offroad, &ExitOffroadButton::clicked, [=]() {
refreshOffroadStatus();
});
left_layout->addWidget(btn_exit_offroad);
left_layout->addWidget(new DriveStats(this));
left_widget->setStyleSheet("border-radius: 10px;");
home_layout->insertWidget(0, left_widget);
offroad_notif = new QPushButton(tr("ALWAYS OFFROAD ACTIVE"));
offroad_notif->setVisible(false);
offroad_notif->setStyleSheet("background-color: #E22C2C;");
header_layout->insertWidget(0, offroad_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
QObject::connect(deviceSP(), &DeviceSP::displayPowerChanged, this, &OffroadHomeSP::refreshOffroadStatus);
}
void OffroadHomeSP::showEvent(QShowEvent *event) {
refreshOffroadStatus();
OffroadHome::showEvent(event);
}
void OffroadHomeSP::refreshOffroadStatus() {
bool is_offroad = params.getBool("OffroadMode");
btn_exit_offroad->setVisible(is_offroad);
offroad_notif->setVisible(is_offroad);
OffroadHome::refresh();
}
@@ -8,10 +8,19 @@
#pragma once
#include "selfdrive/ui/qt/offroad/offroad_home.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h"
class OffroadHomeSP : public OffroadHome {
Q_OBJECT
public:
explicit OffroadHomeSP(QWidget *parent = 0);
private:
ExitOffroadButton *btn_exit_offroad;
QPushButton *offroad_notif;
Params params;
void showEvent(QShowEvent *event) override;
void refreshOffroadStatus();
};
@@ -80,6 +80,14 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
connect(maxTimeOffroad, &OptionControlSP::updateLabels, maxTimeOffroad, &MaxTimeOffroad::refresh);
addItem(maxTimeOffroad);
toggleDeviceBootMode = new ButtonParamControlSP("DeviceBootMode", tr("Wake-Up Behavior"), "", "", {"Default", "Offroad"}, 375, true);
addItem(toggleDeviceBootMode);
connect(toggleDeviceBootMode, &ButtonParamControlSP::buttonClicked, this, [=](int index) {
params.put("DeviceBootMode", QString::number(index).toStdString());
updateState();
});
// Brightness
brightness = new Brightness();
connect(brightness, &OptionControlSP::updateLabels, brightness, &Brightness::refresh);
@@ -184,4 +192,10 @@ void DevicePanelSP::updateState() {
bool offroad_mode_param = params.getBool("OffroadMode");
offroadBtn->setText(offroad_mode_param ? tr("Exit Always Offroad") : tr("Always Offroad"));
offroadBtn->setStyleSheet(offroad_mode_param ? alwaysOffroadStyle : autoOffroadStyle);
DeviceSleepModeStatus currStatus = DeviceSleepModeStatus::DEFAULT;
if (params.get("DeviceBootMode") == "1") {
currStatus = DeviceSleepModeStatus::OFFROAD;
}
toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus));
}
@@ -12,6 +12,11 @@
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
enum class DeviceSleepModeStatus {
DEFAULT,
OFFROAD,
};
class DevicePanelSP : public DevicePanel {
Q_OBJECT
@@ -26,6 +31,7 @@ private:
std::map<QString, PushButtonSP*> buttons;
PushButtonSP *offroadBtn;
MaxTimeOffroad *maxTimeOffroad;
ButtonParamControlSP *toggleDeviceBootMode;
Brightness *brightness;
const QString alwaysOffroadStyle = R"(
@@ -87,4 +93,20 @@ private:
background-color: #FF2424;
}
)";
static QString deviceSleepModeDescription(DeviceSleepModeStatus status = DeviceSleepModeStatus::DEFAULT) {
QString def_str = tr("⁍ Default: Device will boot/wake-up normally & will be ready to engage.");
QString offrd_str = tr("⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up.");
if (status == DeviceSleepModeStatus::DEFAULT) {
def_str = "<font color='white'><b>" + def_str + "</b></font>";
} else if (status == DeviceSleepModeStatus::OFFROAD) {
offrd_str = "<font color='white'><b>" + offrd_str + "</b></font>";
}
return QString("%1<br><br>%2<br>%3")
.arg(tr("Controls state of the device after boot/sleep."))
.arg(def_str)
.arg(offrd_str);
}
};
@@ -24,17 +24,7 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) {
currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model);
currentModelLblBtn->setValue(current_model);
connect(currentModelLblBtn, &ButtonControlSP::clicked, [=]() {
InputDialog d(tr("Search Model"), this, tr("Enter search keywords, or leave blank to list all models."), false);
d.setMinLength(0);
const int ret = d.exec();
if (ret) {
handleCurrentModelLblBtnClicked(d.text());
}
});
connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked);
connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
is_onroad = !offroad;
updateLabels();
@@ -144,7 +134,7 @@ void ModelsPanel::updateModelManagerState() {
* @brief Handles the model bundle selection button click
* Displays available bundles, allows selection, and initiates download
*/
void ModelsPanel::handleCurrentModelLblBtnClicked(const QString &query) {
void ModelsPanel::handleCurrentModelLblBtnClicked() {
currentModelLblBtn->setEnabled(false);
currentModelLblBtn->setValue(tr("Fetching models..."));
@@ -152,8 +142,7 @@ void ModelsPanel::handleCurrentModelLblBtnClicked(const QString &query) {
QMap<uint32_t, QString> index_to_bundle;
const auto bundles = model_manager.getAvailableBundles();
for (const auto &bundle: bundles) {
std::string bundleStr = std::string(bundle.getDisplayName()) + " $SNAME$ " + std::string(bundle.getInternalName());
index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundleStr));
index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName()));
}
// Sort bundles by index in descending order
@@ -167,24 +156,10 @@ void ModelsPanel::handleCurrentModelLblBtnClicked(const QString &query) {
bundleNames.append(index_to_bundle[index]);
}
QStringList filteredBundleNames = searchFromList(query, bundleNames);
currentModelLblBtn->setValue(GetActiveModelName());
if (filteredBundleNames.isEmpty()) {
ConfirmationDialog::alert(tr("No model found for keywords: %1").arg(query), this);
return;
}
for (const QString &bundleName: filteredBundleNames) {
int index = bundleName.indexOf("$SNAME$");
if (index != -1) {
filteredBundleNames.replace(filteredBundleNames.indexOf(bundleName), bundleName.left(index).trimmed());
}
}
const QString selectedBundleName = MultiOptionDialog::getSelection(
tr("Select a Model"), filteredBundleNames, GetActiveModelName(), this);
tr("Select a Model"), bundleNames, GetActiveModelName(), this);
if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) {
return;
@@ -7,7 +7,6 @@
#pragma once
#include "selfdrive/ui/sunnypilot/qt/util.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
class ModelsPanel : public QWidget {
@@ -31,7 +30,7 @@ private:
// UI update related methods
void updateLabels();
void handleCurrentModelLblBtnClicked(const QString &query);
void handleCurrentModelLblBtnClicked();
void handleBundleDownloadProgress();
void showResetParamsDialog();
cereal::ModelManagerSP::Reader model_manager;
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include <algorithm> // for std::sort
#include <deque>
#include <vector>
#include <tuple>
#include <QDir>
#include <QJsonObject>
#include "selfdrive/ui/sunnypilot/qt/common/json_fetcher.h"
static const std::tuple<QString, QString> defaultLocation = std::make_tuple("== None ==", "");
// New class LocationsFetcher that handles web requests and JSON parsing
class LocationsFetcher {
public:
inline std::vector<std::tuple<QString, QString, QString, QString> >
getLocationsFromURL(const QUrl &url, const std::tuple<QString, QString> &customLocation = defaultLocation) const {
// Initialize an empty vector to hold the locations
std::vector<std::tuple<QString, QString, QString, QString> > locations;
JsonFetcher fetcher;
QJsonObject json = fetcher.getJsonFromURL(url.toString());
for (auto it = json.begin(); it != json.end(); ++it) {
QString code = it.key();
QJsonObject obj = it.value().toObject();
QString fullName = obj["full_name"].toString();
locations.push_back(std::make_tuple(fullName, code, QString(), QString()));
}
// Sort locations by full name
std::sort(locations.begin(), locations.end(), [](const auto &lhs, const auto &rhs) {
return std::get<0>(lhs) < std::get<0>(rhs); // Compare full names
});
// Optionally, you can now add defaultName entry at the beginning
locations.insert(locations.begin(), std::tuple_cat(customLocation, std::make_tuple("", "")));
return locations;
}
inline std::vector<std::tuple<QString, QString, QString, QString> >
getLocationsFromURL(const QString &url, const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL(QUrl(url), customLocation);
}
inline std::vector<std::tuple<QString, QString, QString, QString> >
getOsmLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL( "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/nation_bounding_boxes.json", customLocation);
}
inline std::vector<std::tuple<QString, QString, QString, QString> >
getUsStatesLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL( "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/us_states_bounding_boxes.json", customLocation);
}
};
@@ -0,0 +1,158 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h"
#include <QThread>
ModelsFetcher::ModelsFetcher(QObject *parent) : QObject(parent) {
manager = new QNetworkAccessManager(this);
}
QByteArray ModelsFetcher::verifyFileHash(const QString &filePath, const QString &expectedHash, bool &hashMatches) {
hashMatches = false; // Default to false
QByteArray fileData;
if (expectedHash.isEmpty()) {
// If no hash is provided, assume verification isn't required but return the file data
hashMatches = true;
} else {
QFile file(filePath);
if (file.open(QIODevice::ReadOnly)) {
QCryptographicHash hash(QCryptographicHash::Sha256); // Or your chosen algorithm
fileData = file.readAll(); // Read the file data once
hash.addData(fileData);
file.close();
QString currentHash = QString(hash.result().toHex());
hashMatches = (currentHash == expectedHash);
}
}
// Return the file data if hash matches or no hash was provided; empty otherwise
return hashMatches ? fileData : QByteArray();
}
void ModelsFetcher::download(const DownloadInfo &downloadInfo, const QString &filename, const QString &destinationPath) {
QString fullPath = destinationPath + "/" + filename;
QFileInfo fileInfo(fullPath);
bool hashMatches = false;
QByteArray data = verifyFileHash(fullPath, downloadInfo.sha256, hashMatches);
if (fileInfo.exists() && hashMatches) {
// Hash matches or no hash provided, and we have the file data
LOGD("File already downloaded and verified: %s", filename.toStdString().c_str());
emit downloadProgress(100);
emit downloadComplete(data, true); // Use the data returned from verifyFileHash
return; // Exit early
}
// Proceed with download if file does not exist or hash verification failed
QNetworkRequest request(downloadInfo.url);
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::downloadProgress, this, &ModelsFetcher::onDownloadProgress);
connect(reply, &QNetworkReply::finished, this, [this, reply, destinationPath, filename, downloadInfo]() {
onFinished(reply, destinationPath, filename, downloadInfo.sha256);
});
}
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, const QString &expectedHash) {
// Handle download error
if (reply->error()) {
return; // Possibly emit a signal or log an error as per your error handling policy
}
const QByteArray data = reply->readAll();
QString finalFilename = filename;
if (finalFilename.isEmpty()) {
finalFilename = extractFileName(reply->header(QNetworkRequest::ContentDispositionHeader).toString());
}
QString finalPath = QDir(destinationPath).filePath(finalFilename);
// Save the downloaded file
QFile file(finalPath);
//ensure if the path exists and if not create it
if (!QDir().mkpath(destinationPath)) {
LOGE("Unable to create directory: %s", destinationPath.toStdString().c_str());
emit downloadFailed(filename);
return; // Stop further processing
}
//Retry the file open and write 3 times with a little delay between each retry
for (int i = 0; i < 3; i++) {
if (file.isOpen()) break;
file.open(QIODevice::WriteOnly);
if (!file.isOpen()) QThread::msleep(100);
}
// If the file is still not open, log an error and emit a failure signal
if (!file.isOpen()) {
LOGE("Unable to open file for writing: %s", finalPath.toStdString().c_str());
emit downloadFailed(filename);
return; // Stop further processing
}
file.write(data);
file.close();
bool hashMatches = false;
verifyFileHash(finalPath, expectedHash, hashMatches);
// Verify the file hash if expectedHash is provided
if (!expectedHash.isEmpty() && !hashMatches) {
LOGE("The downloaded file didn't pass the hash validation!: %s", filename.toStdString().c_str());
// Hash verification failed, handle accordingly
// This could involve deleting the file, logging an error, or emitting a failure signal
QFile::remove(finalPath); // Example action: Remove the invalid file
emit downloadFailed(filename);
return; // Stop further processing
}
emit downloadComplete(data, false); // Emit your success signal
}
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_v5.json");
}
@@ -0,0 +1,143 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include <algorithm> // for std::sort
#include <cassert>
#include <deque>
#include <vector>
#include <QDir>
#include <QJsonObject>
#include <QTimer>
#include "common/swaglog.h"
#include "common/util.h"
#include "selfdrive/ui/sunnypilot/ui.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/sunnypilot/qt/common/json_fetcher.h"
#ifdef SUNNYPILOT
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
#else
#include "selfdrive/ui/qt/widgets/controls.h"
#endif
#include "system/hardware/hw.h"
static const QString MODELS_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/models/" : "/data/media/0/models/";
struct DownloadInfo {
QString url;
QString sha256;
};
// 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();
// Parse downloadUri as an object
QJsonObject downloadUriObj = json["download_uri"].toObject();
downloadUri.url = downloadUriObj["url"].toString();
downloadUri.sha256 = downloadUriObj["sha256"].toString();
fullNameNav = json["full_name_nav"].toString();
fileNameNav = json["file_name_nav"].toString();
// Parse downloadUriNav as an object
QJsonObject downloadUriNavObj = json["download_uri_nav"].toObject();
downloadUriNav.url = downloadUriNavObj["url"].toString();
downloadUriNav.sha256 = downloadUriNavObj["sha256"].toString();
fullNameMetadata = json["full_name_metadata"].toString();
fileNameMetadata = json["file_name_metadata"].toString();
// Parse downloadUriMetadata as an object
QJsonObject downloadUriMetadataObj = json["download_uri_metadata"].toObject();
downloadUriMetadata.url = downloadUriMetadataObj["url"].toString();
downloadUriMetadata.sha256 = downloadUriMetadataObj["sha256"].toString();
index = json["index"].toString();
environment = json["environment"].toString();
generation = json["generation"].toString();
}
// Method to convert model back to QJsonObject, if needed
QJsonObject toJson() const {
QJsonObject json;
json["display_name"] = displayName;
json["full_name"] = fullName;
json["file_name"] = fileName;
QJsonObject uriObj;
uriObj["url"] = downloadUri.url;
uriObj["sha256"] = downloadUri.sha256;
json["download_uri"] = uriObj;
QJsonObject uriNavObj;
uriNavObj["url"] = downloadUriNav.url;
uriNavObj["sha256"] = downloadUriNav.sha256;
json["download_uri_nav"] = uriNavObj;
QJsonObject uriMetadataObj;
uriMetadataObj["url"] = downloadUriMetadata.url;
uriMetadataObj["sha256"] = downloadUriMetadata.sha256;
json["download_uri_metadata"] = uriMetadataObj;
json["full_name_nav"] = fullNameNav;
json["file_name_nav"] = fileNameNav;
json["full_name_metadata"] = fullNameMetadata;
json["file_name_metadata"] = fileNameMetadata;
json["index"] = index;
json["environment"] = environment;
json["generation"] = generation;
return json;
}
QString displayName;
QString fullName;
QString fileName;
DownloadInfo downloadUri;
DownloadInfo downloadUriNav;
DownloadInfo downloadUriMetadata;
QString fullNameNav;
QString fileNameNav;
QString fullNameMetadata;
QString fileNameMetadata;
QString index;
QString environment;
QString generation;
};
class ModelsFetcher : public QObject {
Q_OBJECT
public:
explicit ModelsFetcher(QObject *parent = nullptr);
void download(const DownloadInfo &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, bool fromCache = false);
void downloadFailed(const QString &filename);
private:
// static bool verifyFileHash(const QString& filePath, const QString& expectedHash);
static QByteArray verifyFileHash(const QString &filePath, const QString &expectedHash, bool &hashMatches);
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void onFinished(QNetworkReply *reply, const QString &destinationPath, const QString &filename,
const QString &expectedHash);
QNetworkAccessManager *manager;
};
@@ -0,0 +1,283 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h"
#include <tuple>
#include <vector>
#include <string>
#include "common/swaglog.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
OsmPanel::OsmPanel(QWidget *parent) : QFrame(parent) {
main_layout = new QStackedLayout(this);
const auto list = new ListWidgetSP(this, false);
list->addItem(mapdVersion = new LabelControlSP(tr("Mapd Version"), "Loading..."));
list->addItem(setupOsmDeleteMapsButton(parent));
list->addItem(offlineMapsETA = new LabelControlSP(tr("Offline Maps ETA"), ""));
list->addItem(offlineMapsElapsed = new LabelControlSP(tr("Time Elapsed"), ""));
list->addItem(setupOsmUpdateButton(parent));
list->addItem(setupOsmDownloadButton(parent));
list->addItem(setupUsStatesButton(parent));
connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) {
updateLabels();
});
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, QOverload<>::of(&OsmPanel::updateLabels));
timer->start(FAST_REFRESH_INTERVAL); // Time specified in milliseconds.
updateLabels();
osmScreen = new QWidget(this);
auto *vlayout = new QVBoxLayout(osmScreen);
vlayout->setContentsMargins(50, 20, 50, 20);
vlayout->addWidget(new ScrollViewSP(list, this), 1);
main_layout->addWidget(osmScreen);
}
ButtonControlSP *OsmPanel::setupOsmDeleteMapsButton(QWidget *parent) {
osmDeleteMapsBtn = new ButtonControlSP(tr("Downloaded Maps"), tr("DELETE")); // Updated on updateLabels()
connect(osmDeleteMapsBtn, &ButtonControlSP::clicked, [=]() {
if (showConfirmationDialog(parent, tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all the maps?"), tr("Yes, delete all the maps."))) {
QtConcurrent::run([=]() {
QDir dir(MAP_PATH);
osmDeleteMapsBtn->setEnabled(false);
osmDeleteMapsBtn->setText("");
dir.removeRecursively();
updateMapSize();
osmDeleteMapsBtn->setEnabled(true);
osmDeleteMapsBtn->setText(tr("DELETE"));
});
updateLabels();
}
});
return osmDeleteMapsBtn;
}
ButtonControlSP *OsmPanel::setupOsmUpdateButton(QWidget *parent) {
osmUpdateBtn = new ButtonControlSP(tr("Database Update"), tr("CHECK")); // Updated on updateLabels()
connect(osmUpdateBtn, &ButtonControlSP::clicked, [=]() {
if (osm_download_in_progress && !download_failed_state) {
updateLabels();
} else if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
});
return osmUpdateBtn;
}
ButtonControlSP *OsmPanel::setupOsmDownloadButton(QWidget *parent) {
osmDownloadBtn = new ButtonControlSP(tr("Country"), tr("SELECT"));
connect(osmDownloadBtn, &ButtonControlSP::clicked, [=]() {
osmDownloadBtn->setEnabled(false);
osmDownloadBtn->setValue(tr("Fetching Country list..."));
const std::vector<std::tuple<QString, QString, QString, QString> > locations = getOsmLocations();
osmDownloadBtn->setEnabled(true);
osmDownloadBtn->setValue("");
const QString initTitle = QString::fromStdString(params.get("OsmLocationTitle"));
const QString currentTitle = ((initTitle == "== None ==") || (initTitle.length() == 0)) ? "== None ==" : initTitle;
QStringList locationTitles;
for (auto &loc: locations) {
locationTitles.push_back(std::get<0>(loc));
}
const QString selection = MultiOptionDialog::getSelection(tr("Country"), locationTitles, currentTitle, this);
if (!selection.isEmpty()) {
params.put("OsmLocal", "1");
params.put("OsmLocationTitle", selection.toStdString());
for (auto &loc: locations) {
if (std::get<0>(loc) == selection) {
params.put("OsmLocationName", std::get<1>(loc).toStdString());
break;
}
}
if (params.get("OsmLocationName") == "US") {
usStatesBtn->click();
return;
} else if (selection != "== None ==") {
if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
}
}
updateLabels();
});
return osmDownloadBtn;
}
ButtonControlSP *OsmPanel::setupUsStatesButton(QWidget *parent) {
usStatesBtn = new ButtonControlSP(tr("State"), tr("SELECT"));
connect(usStatesBtn, &ButtonControlSP::clicked, [=]() {
const std::tuple<QString, QString> allStatesOption = std::make_tuple("All States (~4.8 GB)", "All");
usStatesBtn->setEnabled(false);
usStatesBtn->setValue(tr("Fetching State list..."));
const std::vector<std::tuple<QString, QString, QString, QString> > locations =
getUsStatesLocations(allStatesOption);
usStatesBtn->setEnabled(true);
usStatesBtn->setValue("");
const QString initTitle = QString::fromStdString(params.get("OsmStateTitle"));
const QString currentTitle = ((initTitle == std::get<0>(allStatesOption)) || (initTitle.length() == 0)) ? tr("All") : initTitle;
QStringList locationTitles;
for (auto &loc: locations) {
locationTitles.push_back(std::get<0>(loc));
}
const QString selection = MultiOptionDialog::getSelection(tr("State"), locationTitles, currentTitle, this);
if (!selection.isEmpty()) {
params.put("OsmStateTitle", selection.toStdString());
for (auto &loc: locations) {
if (std::get<0>(loc) == selection) {
params.put("OsmStateName", std::get<1>(loc).toStdString());
break;
}
}
usStatesBtn->setValue(selection);
if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
}
updateLabels();
});
usStatesBtn->setVisible(false); // initially hidden
return usStatesBtn;
}
void OsmPanel::showEvent(QShowEvent *event) {
updateLabels(); // For snappier feeling
if (!timer->isActive()) {
timer->start(FAST_REFRESH_INTERVAL);
}
}
void OsmPanel::hideEvent(QHideEvent *event) {
if (timer->isActive()) {
timer->stop();
}
}
void OsmPanel::updateLabels() {
if (!isVisible()) {
return;
}
mapd_version = params.get("MapdVersion");
mapdVersion->setText(mapd_version.c_str());
updateMapSize();
osm_download_locations = mem_params.get("OSMDownloadLocations");
osm_download_in_progress = !osm_download_locations.empty();
timer->setInterval(osm_download_in_progress ? FAST_REFRESH_INTERVAL : SLOW_REFRESH_INTERVAL);
LOGT("Timer Interval %d", timer->interval());
const std::string osmLastDownloadTimeStr = params.get("OsmDownloadedDate");
if (!lastDownloadedTimePoint.has_value() && !osmLastDownloadTimeStr.empty()) {
const double osmLastDownloadTime = std::stod(osmLastDownloadTimeStr);
lastDownloadedTimePoint = std::chrono::system_clock::from_time_t(static_cast<std::time_t>(osmLastDownloadTime));
}
osmDownloadBtn->setEnabled(!osm_download_in_progress);
usStatesBtn->setEnabled(!osm_download_in_progress);
updateDownloadProgress();
const QString locationName = QString::fromStdString(params.get("OsmLocationName"));
const bool isUs = !locationName.isEmpty() && locationName == "US";
usStatesBtn->setVisible(isUs);
if (!locationName.isEmpty()) {
if (!isUs) {
params.remove("OsmStateName");
params.remove("OsmStateTitle");
}
osmUpdateBtn->setVisible(true);
} else {
params.remove("OsmLocal");
params.remove("OsmLocationName");
params.remove("OsmLocationTitle");
params.remove("OsmStateName");
params.remove("OsmStateTitle");
osmUpdateBtn->setVisible(false);
usStatesBtn->setVisible(false);
}
osmDownloadBtn->setValue(QString::fromStdString(params.get("OsmLocationTitle")));
usStatesBtn->setValue(QString::fromStdString(params.get("OsmStateTitle")));
update();
}
void OsmPanel::updateDownloadProgress() {
const auto pending_update_check = params.getBool("OsmDbUpdatesCheck");
const QJsonObject osmDownloadProgress = QJsonDocument::fromJson(params.get("OSMDownloadProgress").c_str()).object();
if (osm_download_in_progress && lastDownloadedTimePoint.has_value()) {
offlineMapsETA->setVisible(true);
offlineMapsElapsed->setVisible(true);
offlineMapsETA->setText(calculateETA(osmDownloadProgress, lastDownloadedTimePoint.value()));
offlineMapsElapsed->setText(calculateElapsedTime(osmDownloadProgress, lastDownloadedTimePoint.value()));
} else {
offlineMapsETA->setVisible(false);
offlineMapsElapsed->setVisible(false);
}
const int total_files = extractIntFromJson(osmDownloadProgress, "total_files");
const int downloaded_files = extractIntFromJson(osmDownloadProgress, "downloaded_files");
download_failed_state = total_files && osm_download_in_progress && !lastDownloadedTimePoint.has_value() && downloaded_files < total_files;
QString updateButtonText = processUpdateStatus(pending_update_check, total_files, downloaded_files, osmDownloadProgress, download_failed_state);
osmUpdateBtn->setValue(updateButtonText);
osmUpdateBtn->setText(osm_download_in_progress && !download_failed_state ? tr("REFRESH") : tr("UPDATE"));
osmDeleteMapsBtn->setValue(formatSize(mapsDirSize));
}
int OsmPanel::extractIntFromJson(const QJsonObject &json, const QString &key) {
return (json.contains(key)) ? json[key].toInt() : 0;
}
QString OsmPanel::processUpdateStatus(bool pending_update, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state) {
if (pending_update && !osm_download_in_progress && !total_files) {
lastDownloadedTimePoint.reset();
return tr("Download starting...");
} else if (failed_state) {
return tr("Error: Invalid download. Retry.");
} else if (osm_download_in_progress && total_files > downloaded_files) {
return formatDownloadStatus(json);
} else if (osm_download_in_progress && downloaded_files >= total_files) {
osm_download_in_progress = false;
lastDownloadedTimePoint.reset();
return tr("Download complete!");
}
if (lastDownloadedTimePoint.has_value()) {
QDateTime dateTime = QDateTime::fromTime_t(std::chrono::system_clock::to_time_t(lastDownloadedTimePoint.value())); //fromMSecsSinceEpoch(duration);
dateTime = dateTime.toLocalTime();
return QString("%1").arg(dateTime.toString("yyyy-MM-dd HH:mm:ss"));
}
return "";
}
void OsmPanel::updateMapSize() {
if (mapSizeFuture.has_value() && mapSizeFuture.value().isFinished()) {
mapsDirSize = mapSizeFuture.value().result();
}
if (!mapSizeFuture.has_value() || !mapSizeFuture.value().isRunning()) {
mapSizeFuture = QtConcurrent::run(getDirSize, MAP_PATH);
}
}
@@ -0,0 +1,232 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include <deque>
#include <chrono>
#include <map>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
#include <QDir>
#include <QFileInfo>
#include <QtConcurrent/QtConcurrent>
#include "selfdrive/ui/qt/network/wifi_manager.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/sunnypilot/ui.h"
#include "system/hardware/hw.h"
constexpr int FAST_REFRESH_INTERVAL = 1000; // ms
constexpr int SLOW_REFRESH_INTERVAL = 5000; // ms
static const QString MAP_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/osm/offline/" : "/data/media/0/osm/offline/";
class OsmPanel : public QFrame {
Q_OBJECT
public:
explicit OsmPanel(QWidget *parent = nullptr);
private:
QStackedLayout *main_layout = nullptr;
QWidget *osmScreen = nullptr;
Params params;
Params mem_params{Hardware::PC() ? "" : "/dev/shm/params"};
std::map<std::string, ParamControlSP *> toggles;
std::optional<QFuture<quint64> > mapSizeFuture;
const SubMaster &sm = *uiStateSP()->sm;
bool is_onroad = false;
std::string mapd_version;
bool isWifi() const { return sm["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; }
bool isMetered() const { return sm["deviceState"].getDeviceState().getNetworkMetered(); }
bool osm_download_in_progress = false;
bool download_failed_state = false;
quint64 mapsDirSize = 0;
QLabel *osmUpdateLbl;
ButtonControlSP *osmDownloadBtn;
ButtonControlSP *osmUpdateBtn;
ButtonControlSP *usStatesBtn;
ButtonControlSP *osmDeleteMapsBtn;
ButtonControlSP *setupOsmDeleteMapsButton(QWidget *parent);;
ButtonControlSP *setupOsmUpdateButton(QWidget *parent);
ButtonControlSP *setupOsmDownloadButton(QWidget *parent);
ButtonControlSP *setupUsStatesButton(QWidget *parent);
QTimer *timer;
std::string osm_download_locations;
// void updateButtonControlSP(ButtonControlSP *btnControl, QWidget *parent, const QString &initTitle, const QString &allStatesOption);
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void updateLabels();
void updateDownloadProgress();
static int extractIntFromJson(const QJsonObject &json, const QString &key);
QString processUpdateStatus(bool pending_update_check, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state);
ConfirmationDialog *confirmationDialog;
LabelControlSP *mapdVersion;
LabelControlSP *offlineMapsStatus;
LabelControlSP *offlineMapsETA;
LabelControlSP *offlineMapsElapsed;
std::optional<std::chrono::system_clock::time_point> lastDownloadedTimePoint;
LocationsFetcher locationsFetcher;
void updateMapSize();
bool showConfirmationDialog(QWidget *parent,
const QString &message = QString(),
const QString &confirmButtonText = QString()) const {
const auto _is_metered = isMetered();
const QString warning_message = _is_metered ? tr("\n\nWarning: You are on a metered connection!") : QString();
QString final_message = message.isEmpty() ? tr("This will start the download process and it might take a while to complete.") : message;
final_message += warning_message; // Append the warning message if the connection is metered
const QString final_buttonText = confirmButtonText.isEmpty() ? (_is_metered ? tr("Continue on Metered") : tr("Start Download")) : confirmButtonText;
return ConfirmationDialog::confirm(final_message, final_buttonText, parent);
}
// Refactored methods
std::vector<std::tuple<QString, QString, QString, QString> > getOsmLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return locationsFetcher.getOsmLocations(customLocation);
}
std::vector<std::tuple<QString, QString, QString, QString> > getUsStatesLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return locationsFetcher.getUsStatesLocations(customLocation);
}
static QString formatTime(const long timeInSeconds) {
const long minutes = timeInSeconds / 60;
const long seconds = timeInSeconds % 60;
QString formattedTime;
if (minutes > 0) {
formattedTime = QString::number(minutes) + tr("m ");
}
formattedTime += QString::number(seconds) + tr("s");
return formattedTime;
}
static QString calculateElapsedTime(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) {
using namespace std::chrono;
if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files"))
return tr("Calculating...");
const int totalFiles = jsonData["total_files"].toInt();
const int downloadedFiles = jsonData["downloaded_files"].toInt();
if (downloadedFiles >= totalFiles || totalFiles <= 0) return tr("Downloaded");
const long elapsed = duration_cast<seconds>(system_clock::now() - startTime).count();
if (elapsed == 0 || downloadedFiles == 0) return tr("Calculating...");
return formatTime(elapsed);
}
static QString calculateETA(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) {
using namespace std::chrono;
static steady_clock::time_point lastUpdateTime = steady_clock::now();
static std::deque<double> rateHistory;
constexpr int minDataPoints = 3;
constexpr int historySize = 10;
static QString lastETA = tr("Calculating ETA...");
if (duration_cast<seconds>(steady_clock::now() - lastUpdateTime).count() < 1) {
return lastETA;
}
if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files"))
return lastETA;
const int totalFiles = jsonData["total_files"].toInt();
const int downloadedFiles = jsonData["downloaded_files"].toInt();
if (totalFiles <= 0 || downloadedFiles >= totalFiles) {
return totalFiles <= 0 ? tr("Ready") : tr("Downloaded");
}
const long elapsed = duration_cast<seconds>(system_clock::now() - startTime).count();
if (elapsed == 0 || downloadedFiles == 0) return lastETA;
const double rate = downloadedFiles / static_cast<double>(elapsed);
if (rateHistory.size() >= historySize) rateHistory.pop_front();
rateHistory.push_back(rate);
if (rateHistory.size() < minDataPoints) return lastETA;
double weightedSum = 0;
for (int i = 0, weight = 1; i < rateHistory.size(); ++i, ++weight) {
weightedSum += rateHistory[i] * weight;
}
const double avgRate = 2 * weightedSum / (rateHistory.size() * (rateHistory.size() + 1));
const long remainingTime = static_cast<long>((totalFiles - downloadedFiles) / avgRate);
if (remainingTime <= 0) return lastETA;
lastETA = tr("Time remaining: ") + formatTime(remainingTime);
lastUpdateTime = steady_clock::now();
return lastETA;
}
static QString formatDownloadStatus(const QJsonObject &json) {
if (!json.contains("total_files") || !json.contains("downloaded_files"))
return "";
const int total_files = json["total_files"].toInt();
const int downloaded_files = json["downloaded_files"].toInt();
if (total_files <= 0) return tr("Ready");
if (downloaded_files >= total_files) return tr("Downloaded");
const int percentage = static_cast<int>(100.0 * downloaded_files / total_files);
return QString::asprintf("%d/%d (%d%%)", downloaded_files, total_files, percentage);
}
QString formatSize(quint64 size) const {
if (size == 0 && (!mapSizeFuture.has_value() || mapSizeFuture.value().isRunning())) {
return tr("Calculating...");
}
constexpr qint64 kb = 1024;
constexpr qint64 mb = 1024 * kb;
constexpr qint64 gb = 1024 * mb;
if (size < gb) {
const double sizeMB = size / static_cast<double>(mb);
return QString::number(sizeMB, 'f', 2) + " MB";
} else {
const double sizeGB = size / static_cast<double>(gb);
return QString::number(sizeGB, 'f', 2) + " GB";
}
}
static quint64 getDirSize(QString dirPath) {
quint64 size = 0;
const QString actualDirPath = dirPath.startsWith("~") ? dirPath.replace(0, 1, QDir::homePath()) : dirPath;
QDirIterator it(actualDirPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
if (it.fileInfo().isFile()) {
size += it.fileInfo().size();
}
}
return size;
}
};
@@ -18,6 +18,7 @@
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h"
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h"
@@ -85,6 +86,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) {
PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
PanelInfo(" " + tr("Cruise"), new LongitudinalPanel(this), "../assets/icons/speed_limit.png"),
PanelInfo(" " + tr("Visuals"), new VisualsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"),
PanelInfo(" " + tr("OSM"), new OsmPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_map.png"),
PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"),
@@ -247,14 +247,11 @@ public:
if (inline_layout) {
button_param_layout->setMargin(0);
button_param_layout->setSpacing(0);
spacingItem = nullptr;
if (!title.isEmpty()) {
main_layout->removeWidget(title_label);
hlayout->addWidget(title_label, 1);
}
if (spacingItem != nullptr && main_layout->indexOf(spacingItem) != -1) {
main_layout->removeItem(spacingItem);
spacingItem = nullptr;
}
}
button_group = new QButtonGroup(this);
@@ -34,7 +34,7 @@ DriveStats::DriveStats(QWidget* parent) : QFrame(parent) {
int row = 0;
grid_layout->addWidget(newLabel(title, "title"), row++, 0, 1, 3);
grid_layout->addItem(new QSpacerItem(0, 50), row++, 0, 1, 1);
grid_layout->addItem(new QSpacerItem(0, 30), row++, 0, 1, 1);
grid_layout->addWidget(labels.routes = newLabel("0", "number"), row, 0, Qt::AlignLeft);
grid_layout->addWidget(labels.distance = newLabel("0", "number"), row, 1, Qt::AlignLeft);
+8
View File
@@ -41,6 +41,7 @@ void UIStateSP::update() {
DeviceSP::DeviceSP(QObject *parent) : Device(parent) {
QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update);
QObject::connect(this, &Device::displayPowerChanged, this, &DeviceSP::handleDisplayPowerChanged);
}
UIStateSP *uiStateSP() {
@@ -62,3 +63,10 @@ DeviceSP *deviceSP() {
static DeviceSP _device;
return &_device;
}
void DeviceSP::handleDisplayPowerChanged(bool on) {
// if enabled, trigger offroad mode when device goes to sleep
if (params.get("DeviceBootMode") == "1" && not on) {
params.putBool("OffroadMode", true);
}
}
+4
View File
@@ -84,6 +84,10 @@ class DeviceSP : public Device {
public:
DeviceSP(QObject *parent = 0);
private:
Params params;
void handleDisplayPowerChanged(bool on);
};
DeviceSP *deviceSP();
View File
+22
View File
@@ -0,0 +1,22 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.common.swaglog import cloudlog
LOOK_AHEAD_HORIZON_TIME = 15. # s. Time horizon for look ahead of turn speed sections to provide on liveMapDataSP msg.
_DEBUG = False
_CLOUDLOG_DEBUG = False
ROAD_NAME_TIMEOUT = 30 # secs
R = 6373000.0 # approximate radius of earth in mts
QUERY_RADIUS = 3000 # mts. Radius to use on OSM data queries.
QUERY_RADIUS_OFFLINE = 2250 # mts. Radius to use on offline OSM data queries.
def get_debug(msg, log_to_cloud=True):
if _CLOUDLOG_DEBUG and log_to_cloud:
cloudlog.debug(msg)
if _DEBUG:
print(msg)
@@ -0,0 +1,76 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import time
from abc import abstractmethod, ABC
from cereal import messaging
from openpilot.common.gps import get_gps_location_service
from openpilot.common.params import Params
from openpilot.sunnypilot.navd.helpers import Coordinate, coordinate_from_param
class BaseMapData(ABC):
def __init__(self):
self.params = Params()
self.gps_location_service = get_gps_location_service(self.params)
self.sm = messaging.SubMaster(['livePose', 'carControl'] + [self.gps_location_service])
self.pm = messaging.PubMaster(['liveMapDataSP'])
self.last_position = coordinate_from_param("LastGPSPosition", self.params)
self.last_altitude = None
@abstractmethod
def update_location(self) -> None:
pass
@abstractmethod
def get_current_speed_limit(self) -> float:
pass
@abstractmethod
def get_next_speed_limit_and_distance(self) -> tuple[float, float]:
pass
@abstractmethod
def get_current_road_name(self) -> str:
pass
def get_current_location(self) -> None:
gps = self.sm[self.gps_location_service]
# ignore the message if the fix is invalid
gps_ok = self.sm.updated[self.gps_location_service] or (time.monotonic() - self.sm.logMonoTime[self.gps_location_service] / 1e9) > 2.0
if not gps_ok and self.sm['livePose'].inputsOK:
return None
# livePose has these data, but aren't on cereal
self.last_position = Coordinate(gps.latitude, gps.longitude)
self.last_altitude = gps.altitude
def publish(self) -> None:
speed_limit = self.get_current_speed_limit()
next_speed_limit, next_speed_limit_distance = self.get_next_speed_limit_and_distance()
mapd_sp_send = messaging.new_message('liveMapDataSP')
mapd_sp_send.valid = self.sm.all_checks(service_list=[self.gps_location_service, 'livePose'])
live_map_data = mapd_sp_send.liveMapDataSP
live_map_data.speedLimitValid = bool(speed_limit > 0)
live_map_data.speedLimit = speed_limit
live_map_data.speedLimitAheadValid = bool(next_speed_limit > 0)
live_map_data.speedLimitAhead = next_speed_limit
live_map_data.speedLimitAheadDistance = next_speed_limit_distance
live_map_data.roadName = self.get_current_road_name()
self.pm.send('liveMapDataSP', mapd_sp_send)
def tick(self) -> None:
self.sm.update()
self.get_current_location()
self.update_location()
self.publish()
+57
View File
@@ -0,0 +1,57 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
# DISCLAIMER: This code is intended principally for development and debugging purposes.
# Although it provides a standalone entry point to the program, users should refer
# to the actual implementations for consumption. Usage outside of development scenarios
# is not advised and could lead to unpredictable results.
import threading
import traceback
from cereal import messaging
from openpilot.common.gps import get_gps_location_service
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver
from openpilot.sunnypilot.mapd.live_map_data import get_debug
def excepthook(args):
get_debug(f'MapD: Threading exception:\n{args}')
traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback)
def live_map_data_sp_thread():
config_realtime_process([0, 1, 2, 3], 5)
params = Params()
gps_location_service = get_gps_location_service(params)
while True:
live_map_data_sp_thread_debug(gps_location_service)
def live_map_data_sp_thread_debug(gps_location_service):
_sub_master = messaging.SubMaster(['carState', 'livePose', 'liveMapDataSP', 'longitudinalPlanSP', gps_location_service])
_sub_master.update()
v_ego = _sub_master['carState'].vEgo
long_spl = _sub_master['longitudinalPlanSP'].speedLimit
_policy = Policy.car_state_priority
_resolver = SpeedLimitResolver(_policy)
_speed_limit, _distance, _source = _resolver.resolve(v_ego, long_spl, _sub_master)
print(_speed_limit, _distance, _source, " <-> ", long_spl)
def main():
threading.excepthook = excepthook
live_map_data_sp_thread()
if __name__ == "__main__":
main()
@@ -0,0 +1,51 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
import platform
from openpilot.common.params import Params
from openpilot.sunnypilot.mapd.live_map_data.base_map_data import BaseMapData
from openpilot.sunnypilot.navd.helpers import Coordinate
class OsmMapData(BaseMapData):
def __init__(self):
super().__init__()
self.params = Params()
self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params
def update_location(self) -> None:
if self.last_position is None or self.last_altitude is None:
return
params = {
"latitude": self.last_position.latitude,
"longitude": self.last_position.longitude,
"altitude": self.last_altitude,
}
self.mem_params.put("LastGPSPosition", json.dumps(params))
def get_current_speed_limit(self) -> float:
return float(self.mem_params.get("MapSpeedLimit", encoding='utf8') or 0.0)
def get_current_road_name(self) -> str:
return self.mem_params.get("RoadName", encoding='utf8') or ""
def get_next_speed_limit_and_distance(self) -> tuple[float, float]:
next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit", encoding='utf8')
next_speed_limit_section = json.loads(next_speed_limit_section_str) if next_speed_limit_section_str else {}
next_speed_limit = next_speed_limit_section.get('speedlimit', 0.0)
next_speed_limit_latitude = next_speed_limit_section.get('latitude')
next_speed_limit_longitude = next_speed_limit_section.get('longitude')
next_speed_limit_distance = 0.0
if next_speed_limit_latitude and next_speed_limit_longitude:
next_speed_limit_coordinates = Coordinate(next_speed_limit_latitude, next_speed_limit_longitude)
next_speed_limit_distance = (self.last_position or Coordinate(0, 0)).distance_to(next_speed_limit_coordinates)
return next_speed_limit, next_speed_limit_distance
@@ -0,0 +1,42 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
# DISCLAIMER: This code is intended principally for development and debugging purposes.
# Although it provides a standalone entry point to the program, users should refer
# to the actual implementations for consumption. Usage outside of development scenarios
# is not advised and could lead to unpredictable results.
import threading
import traceback
from openpilot.common.realtime import Ratekeeper, config_realtime_process
from openpilot.sunnypilot.mapd.live_map_data import get_debug
from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData
def excepthook(args):
get_debug(f'MapD: Threading exception:\n{args}')
traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback)
def live_map_data_sp_thread():
config_realtime_process([0, 1, 2, 3], 5)
live_map_sp = OsmMapData()
rk = Ratekeeper(1, print_delay_threshold=None)
while True:
live_map_sp.tick()
rk.keep_time()
def main():
threading.excepthook = excepthook
live_map_data_sp_thread()
if __name__ == "__main__":
main()
+152
View File
@@ -0,0 +1,152 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
#!/usr/bin/env python3
import logging
import os
import stat
import time
import traceback
import requests
from pathlib import Path
from urllib.request import urlopen
from cereal import messaging
from openpilot.common.params import Params
from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR
from openpilot.system.hardware.hw import Paths
from openpilot.system.ui.spinner import Spinner
from openpilot.system.version import is_prebuilt
import openpilot.system.sentry as sentry
VERSION = 'v1.9.0'
URL = f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{VERSION}/mapd"
class MapdInstallManager:
def __init__(self, spinner_ref: Spinner):
self._spinner = spinner_ref
def download(self) -> None:
self.ensure_directories_exist()
self._download_file()
self.update_installed_version(VERSION)
def check_and_download(self) -> None:
if self.download_needed():
self.download()
@staticmethod
def download_needed() -> bool:
return not os.path.exists(MAPD_PATH) or MapdInstallManager.get_installed_version() != VERSION
@staticmethod
def ensure_directories_exist() -> None:
if not os.path.exists(Paths.mapd_root()):
os.makedirs(Paths.mapd_root())
if not os.path.exists(MAPD_BIN_DIR):
os.makedirs(MAPD_BIN_DIR)
@staticmethod
def _safe_write_and_set_executable(file_path: Path, content: bytes) -> None:
with open(file_path, 'wb') as output:
output.write(content)
output.flush()
os.fsync(output.fileno())
current_permissions = stat.S_IMODE(os.lstat(file_path).st_mode)
os.chmod(file_path, current_permissions | stat.S_IEXEC)
def _download_file(self, num_retries=5) -> None:
temp_file = Path(MAPD_PATH + ".tmp")
download_timeout = 60
for cnt in range(num_retries):
try:
response = requests.get(URL, stream=True, timeout=download_timeout)
response.raise_for_status()
self._safe_write_and_set_executable(temp_file, response.content)
# No exceptions encountered. Safe to replace original file.
temp_file.replace(MAPD_PATH)
return
except requests.exceptions.ReadTimeout:
self._spinner.update(f"ReadTimeout caught. Timeout is [{download_timeout}]. Retrying download... [{cnt}]")
time.sleep(0.5)
except requests.exceptions.RequestException as e:
self._spinner.update(f"RequestException caught: {e}. Retrying download... [{cnt}]")
time.sleep(0.5)
# Delete temp file if the process was not successful.
if temp_file.exists():
temp_file.unlink()
logging.error("Failed to download file after all retries")
@staticmethod
def update_installed_version(version: str) -> None:
Params().put("MapdVersion", version)
@staticmethod
def get_installed_version() -> str:
return Params().get("MapdVersion", encoding="utf-8") or ""
def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool:
max_retries = 10
for retries in range(max_retries + 1):
self._spinner.update(f"Waiting for internet connection... [{retries}/{max_retries}]")
time.sleep(2)
try:
_ = urlopen('https://sentry.io', timeout=10)
return True
except Exception as e:
print(f'Wait for internet failed: {e}')
if return_on_failure and retries == max_retries:
return False
return False
def non_prebuilt_install(self) -> None:
sm = messaging.SubMaster(['deviceState'])
metered = sm['deviceState'].networkMetered
if metered:
self._spinner.update("Can't proceed with mapd install since network is metered!")
time.sleep(5)
return
try:
self.ensure_directories_exist()
if not self.download_needed():
self._spinner.update("Mapd is good!")
time.sleep(0.1)
return
if self.wait_for_internet_connection(return_on_failure=True):
self._spinner.update(f"Downloading pfeiferj's mapd [{install_manager.get_installed_version()}] => [{VERSION}].")
time.sleep(0.1)
self.check_and_download()
self._spinner.close()
except Exception:
for i in range(6):
self._spinner.update("Failed to download OSM maps won't work until properly downloaded!" +
"Try again manually rebooting. " +
f"Boot will continue in {5 - i}s...")
time.sleep(1)
sentry.init(sentry.SentryProject.SELFDRIVE)
traceback.print_exc()
sentry.capture_exception()
if __name__ == "__main__":
spinner = Spinner()
install_manager = MapdInstallManager(spinner)
install_manager.ensure_directories_exist()
if is_prebuilt():
debug_msg = f"[DEBUG] This is prebuilt, no mapd install required. VERSION: [{VERSION}], Param [{install_manager.get_installed_version()}]"
spinner.update(debug_msg)
install_manager.update_installed_version(VERSION)
else:
spinner.update(f"Checking if mapd is installed and valid. Prebuilt [{is_prebuilt()}]")
install_manager.non_prebuilt_install()
+146
View File
@@ -0,0 +1,146 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
import time
import platform
import os
import glob
import shutil
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper, config_realtime_process
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData
from openpilot.system.hardware.hw import Paths
# PFEIFER - MAPD {{
params = Params()
mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else params
# }} PFEIFER - MAPD
MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj')
MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd')
def get_files_for_cleanup() -> list[str]:
paths = [
f"{Paths.mapd_root()}/db",
f"{Paths.mapd_root()}/v*"
]
files_to_remove = []
for path in paths:
if os.path.exists(path):
files = glob.glob(path + '/**', recursive=True)
files_to_remove.extend(files)
# check for version and mapd files
if not os.path.isfile(MAPD_PATH):
files_to_remove.append(MAPD_PATH)
return files_to_remove
def cleanup_old_osm_data(files_to_remove: list[str]) -> None:
for file in files_to_remove:
# Remove trailing slash if path is file
if file.endswith('/') and os.path.isfile(file[:-1]):
file = file[:-1]
# Try to remove as file or symbolic link first
if os.path.islink(file) or os.path.isfile(file):
os.remove(file)
elif os.path.isdir(file): # If it's a directory
shutil.rmtree(file, ignore_errors=False)
def request_refresh_osm_location_data(nations: list[str], states: list[str] = None) -> None:
params.put("OsmDownloadedDate", str(time.time()))
params.put_bool("OsmDbUpdatesCheck", False)
osm_download_locations = json.dumps({
"nations": nations,
"states": states or []
})
print(f"Downloading maps for {osm_download_locations}")
mem_params.put("OSMDownloadLocations", osm_download_locations)
def filter_nations_and_states(nations: list[str], states: list[str] = None) -> tuple[list[str], list[str]]:
"""Filters and prepares nation and state data for OSM map download.
If the nation is 'US' and a specific state is provided, the nation 'US' is removed from the list.
If the nation is 'US' and the state is 'All', the 'All' is removed from the list.
The idea behind these filters is that if a specific state in the US is provided,
there's no need to download map data for the entire US. Conversely,
if the state is unspecified (i.e., 'All'), we intend to download map data for the whole US,
and 'All' isn't a valid state name, so it's removed.
Parameters:
nations (list): A list of nations for which the map data is to be downloaded.
states (list, optional): A list of states for which the map data is to be downloaded. Defaults to None.
Returns:
tuple: Two lists. The first list is filtered nations and the second list is filtered states.
"""
if "US" in nations and states and not any(x.lower() == "all" for x in states):
# If a specific state in the US is provided, remove 'US' from nations
nations.remove("US")
elif "US" in nations and states and any(x.lower() == "all" for x in states):
# If 'All' is provided as a state (case invariant), remove those instances from states
states = [x for x in states if x.lower() != "all"]
elif "US" not in nations and states and any(x.lower() == "all" for x in states):
states.remove("All")
return nations, states or []
def update_osm_db() -> None:
# last_downloaded_date = float(params.get('OsmDownloadedDate', encoding='utf-8') or 0.0)
# if params.get_bool("OsmDbUpdatesCheck") or time.time() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60
if params.get_bool("OsmDbUpdatesCheck"):
cleanup_old_osm_data(get_files_for_cleanup())
country = params.get('OsmLocationName', encoding='utf-8')
state = params.get('OsmStateName', encoding='utf-8') or "All"
filtered_nations, filtered_states = filter_nations_and_states([country], [state])
request_refresh_osm_location_data(filtered_nations, filtered_states)
if not mem_params.get("OSMDownloadBounds"):
mem_params.put("OSMDownloadBounds", "")
if not mem_params.get("LastGPSPosition"):
mem_params.put("LastGPSPosition", "{}")
def main_thread():
config_realtime_process([0, 1, 2, 3], 5)
rk = Ratekeeper(1, print_delay_threshold=None)
live_map_sp = OsmMapData()
# Create folder needed for OSM
try:
os.mkdir(Paths.mapd_root())
except FileExistsError:
pass
except PermissionError:
cloudlog.exception(f"mapd: failed to make {Paths.mapd_root()}")
while True:
show_alert = get_files_for_cleanup() and params.get_bool("OsmLocal")
set_offroad_alert("Offroad_OSMUpdateRequired", show_alert, "This alert will be cleared when new maps are downloaded.")
update_osm_db()
live_map_sp.tick()
rk.keep_time()
def main():
main_thread()
if __name__ == "__main__":
main()
+189
View File
@@ -0,0 +1,189 @@
from __future__ import annotations
import json
import math
import numpy as np
from typing import Any, cast
from openpilot.common.conversions import Conversions
from openpilot.common.params import Params
DIRECTIONS = ('left', 'right', 'straight')
MODIFIABLE_DIRECTIONS = ('left', 'right')
EARTH_MEAN_RADIUS = 6371007.2
SPEED_CONVERSIONS = {
'km/h': Conversions.KPH_TO_MS,
'mph': Conversions.MPH_TO_MS,
}
class Coordinate:
def __init__(self, latitude: float, longitude: float) -> None:
self.latitude = latitude
self.longitude = longitude
self.annotations: dict[str, float] = {}
@classmethod
def from_mapbox_tuple(cls, t: tuple[float, float]) -> Coordinate:
return cls(t[1], t[0])
def as_dict(self) -> dict[str, float]:
return {'latitude': self.latitude, 'longitude': self.longitude}
def __str__(self) -> str:
return f'Coordinate({self.latitude}, {self.longitude})'
def __repr__(self) -> str:
return self.__str__()
def __eq__(self, other) -> bool:
if not isinstance(other, Coordinate):
return False
return (self.latitude == other.latitude) and (self.longitude == other.longitude)
def __sub__(self, other: Coordinate) -> Coordinate:
return Coordinate(self.latitude - other.latitude, self.longitude - other.longitude)
def __add__(self, other: Coordinate) -> Coordinate:
return Coordinate(self.latitude + other.latitude, self.longitude + other.longitude)
def __mul__(self, c: float) -> Coordinate:
return Coordinate(self.latitude * c, self.longitude * c)
def dot(self, other: Coordinate) -> float:
return self.latitude * other.latitude + self.longitude * other.longitude
def distance_to(self, other: Coordinate) -> float:
# Haversine formula
dlat = math.radians(other.latitude - self.latitude)
dlon = math.radians(other.longitude - self.longitude)
haversine_dlat = math.sin(dlat / 2.0)
haversine_dlat *= haversine_dlat
haversine_dlon = math.sin(dlon / 2.0)
haversine_dlon *= haversine_dlon
y = haversine_dlat \
+ math.cos(math.radians(self.latitude)) \
* math.cos(math.radians(other.latitude)) \
* haversine_dlon
x = 2 * math.asin(math.sqrt(y))
return x * EARTH_MEAN_RADIUS
def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate):
if a.distance_to(b) < 0.01:
return a.distance_to(p)
ap = p - a
ab = b - a
t = np.clip(ap.dot(ab) / ab.dot(ab), 0.0, 1.0)
projection = a + ab * t
return projection.distance_to(p)
def distance_along_geometry(geometry: list[Coordinate], pos: Coordinate) -> float:
if len(geometry) <= 2:
return geometry[0].distance_to(pos)
# 1. Find segment that is closest to current position
# 2. Total distance is sum of distance to start of closest segment
# + all previous segments
total_distance = 0.0
total_distance_closest = 0.0
closest_distance = 1e9
for i in range(len(geometry) - 1):
d = minimum_distance(geometry[i], geometry[i + 1], pos)
if d < closest_distance:
closest_distance = d
total_distance_closest = total_distance + geometry[i].distance_to(pos)
total_distance += geometry[i].distance_to(geometry[i + 1])
return total_distance_closest
def coordinate_from_param(param: str, params: Params = None) -> Coordinate | None:
if params is None:
params = Params()
json_str = params.get(param)
if json_str is None:
return None
pos = json.loads(json_str)
if 'latitude' not in pos or 'longitude' not in pos:
return None
return Coordinate(pos['latitude'], pos['longitude'])
def string_to_direction(direction: str) -> str:
for d in DIRECTIONS:
if d in direction:
if 'slight' in direction and d in MODIFIABLE_DIRECTIONS:
return 'slight' + d.capitalize()
return d
return 'none'
def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float:
unit = cast(str, maxspeed['unit'])
speed = cast(float, maxspeed['speed'])
return SPEED_CONVERSIONS[unit] * speed
def field_valid(dat: dict, field: str) -> bool:
return field in dat and dat[field] is not None
def parse_banner_instructions(banners: Any, distance_to_maneuver: float = 0.0) -> dict[str, Any] | None:
if not len(banners):
return None
instruction = {}
# A segment can contain multiple banners, find one that we need to show now
current_banner = banners[0]
for banner in banners:
if distance_to_maneuver < banner['distanceAlongGeometry']:
current_banner = banner
# Only show banner when close enough to maneuver
instruction['showFull'] = distance_to_maneuver < current_banner['distanceAlongGeometry']
# Primary
p = current_banner['primary']
if field_valid(p, 'text'):
instruction['maneuverPrimaryText'] = p['text']
if field_valid(p, 'type'):
instruction['maneuverType'] = p['type']
if field_valid(p, 'modifier'):
instruction['maneuverModifier'] = p['modifier']
# Secondary
if field_valid(current_banner, 'secondary'):
instruction['maneuverSecondaryText'] = current_banner['secondary']['text']
# Lane lines
if field_valid(current_banner, 'sub'):
lanes = []
for component in current_banner['sub']['components']:
if component['type'] != 'lane':
continue
lane = {
'active': component['active'],
'directions': [string_to_direction(d) for d in component['directions']],
}
if field_valid(component, 'active_direction'):
lane['activeDirection'] = string_to_direction(component['active_direction'])
lanes.append(lane)
instruction['lanes'] = lanes
return instruction
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d44f71f7603b106986fff7835f35c4c18c4c619a29e6292f8626ae3eedd85e57
size 7811
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57a92adcf88c7223b07697f8c2b315f4f4a34b32a866284610d5250144863c6f
size 28235
-1
View File
@@ -323,7 +323,6 @@ def hardware_thread(end_event, hw_queue) -> None:
offroad_mode = params.get_bool("OffroadMode")
startup_conditions["not_always_offroad"] = not offroad_mode
onroad_conditions["not_always_offroad"] = not offroad_mode
set_offroad_alert("OffroadMode_Status", offroad_mode)
# if the temperature enters the danger zone, go offroad to cool down
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger
+8
View File
@@ -77,3 +77,11 @@ class Paths:
return str(Path(Paths.comma_home()) / "community" / "crashes")
else:
return "/data/community/crashes"
@staticmethod
def mapd_root() -> str:
if PC:
return str(Path(Paths.comma_home()) / "media" / "0" / "osm")
else:
return "/data/media/0/osm"
+8
View File
@@ -18,6 +18,8 @@ from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.version import get_build_metadata, terms_version, training_version
from openpilot.system.hardware.hw import Paths
from openpilot.sunnypilot.mapd.mapd_installer import VERSION
def manager_init() -> None:
save_bootlog()
@@ -47,6 +49,7 @@ def manager_init() -> None:
("BlindSpot", "0"),
("BlinkerMinLateralControlSpeed", "20"), # MPH or km/h
("BlinkerPauseLateralControl", "0"),
("DeviceBootMode", "0"),
("DynamicExperimentalControl", "0"),
("HyundaiLongitudinalTuning", "0"),
("LagdToggle", "1"),
@@ -54,6 +57,7 @@ def manager_init() -> None:
("MadsMainCruiseAllowed", "1"),
("MadsSteeringMode", "0"),
("MadsUnifiedEngagementMode", "1"),
("MapdVersion", f"{VERSION}"),
("MaxTimeOffroad", "1800"),
("Brightness", "0"),
("ModelManager_LastSyncTime", "0"),
@@ -67,6 +71,10 @@ def manager_init() -> None:
("HkgAngleLiveTuning", "0"),
]
# device boot mode
if params.get("DeviceBootMode") == b"1": # start in always offroad mode
params.put_bool("OffroadMode", True)
if params.get_bool("RecordFrontLock"):
params.put_bool("RecordFront", True)
+7
View File
@@ -6,6 +6,9 @@ from cereal import car, custom
from openpilot.common.params import Params
from openpilot.system.hardware import PC, TICI
from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess
from openpilot.system.hardware.hw import Paths
from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH
from sunnypilot.models.helpers import get_active_model_runner
from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader
@@ -157,6 +160,10 @@ procs += [
# Backup
PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
# mapd
NativeProcess("mapd", Paths.mapd_root(), [MAPD_PATH], always_run),
PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run),
]
if os.path.exists("./github_runner.sh"):
+2
View File
@@ -0,0 +1,2 @@
# MAPD implementation by pfeiferj
https://github.com/pfeiferj/openpilot-mapd/releases/
BIN
View File
Binary file not shown.