mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-05 19:55:45 +08:00
Merge branch 'master-new' into hkg-angle-steering-2025
# Conflicts: # common/params_keys.h
This commit is contained in:
+7
-1
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ class DeviceSP : public Device {
|
||||
|
||||
public:
|
||||
DeviceSP(QObject *parent = 0);
|
||||
|
||||
private:
|
||||
Params params;
|
||||
void handleDisplayPowerChanged(bool on);
|
||||
};
|
||||
|
||||
DeviceSP *deviceSP();
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+152
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
# MAPD implementation by pfeiferj
|
||||
https://github.com/pfeiferj/openpilot-mapd/releases/
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user