diff --git a/common/params.cc b/common/params.cc index 41fd5a3..d12ee2c 100644 --- a/common/params.cc +++ b/common/params.cc @@ -288,6 +288,7 @@ std::unordered_map keys = { {"LaneDetection", PERSISTENT}, {"LaneDetectionWidth", PERSISTENT}, {"LaneLinesWidth", PERSISTENT}, + {"LastMapsUpdate", PERSISTENT}, {"LateralTune", PERSISTENT}, {"LeadDepartingAlert", PERSISTENT}, {"LeadInfo", PERSISTENT}, @@ -299,6 +300,7 @@ std::unordered_map keys = { {"ManualUpdateInitiated", CLEAR_ON_MANAGER_START}, {"MapboxPublicKey", PERSISTENT}, {"MapboxSecretKey", PERSISTENT}, + {"MapsSelected", PERSISTENT}, {"MapTargetLatA", PERSISTENT}, {"MapTargetVelocities", PERSISTENT}, {"Model", PERSISTENT}, @@ -319,8 +321,11 @@ std::unordered_map keys = { {"NumericalTemp", PERSISTENT}, {"OfflineMode", PERSISTENT}, {"OneLaneChange", PERSISTENT}, + {"OSMDownloadLocations", PERSISTENT}, + {"OSMDownloadProgress", CLEAR_ON_MANAGER_START}, {"PathEdgeWidth", PERSISTENT}, {"PathWidth", PERSISTENT}, + {"PreferredSchedule", PERSISTENT}, {"PromptVolume", PERSISTENT}, {"PromptDistractedVolume", PERSISTENT}, {"QOLControls", PERSISTENT}, @@ -331,6 +336,9 @@ std::unordered_map keys = { {"ReverseCruise", PERSISTENT}, {"ReverseCruiseUI", PERSISTENT}, {"RoadEdgesWidth", PERSISTENT}, + {"RoadName", PERSISTENT}, + {"RoadNameUI", PERSISTENT}, + {"SchedulePending", PERSISTENT}, {"SearchInput", PERSISTENT}, {"SilentMode", PERSISTENT}, {"ShowCPU", PERSISTENT}, diff --git a/selfdrive/frogpilot/functions/mapd.py b/selfdrive/frogpilot/functions/mapd.py new file mode 100644 index 0000000..5681555 --- /dev/null +++ b/selfdrive/frogpilot/functions/mapd.py @@ -0,0 +1,56 @@ +# PFEIFER - MAPD +import os +import subprocess +import urllib.request +from openpilot.common.realtime import Ratekeeper +import stat + +VERSION = 'v1.8.1' +URL = f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{VERSION}/mapd" +MAPD_PATH = '/data/media/0/osm/mapd' +VERSION_PATH = '/data/media/0/osm/mapd_version' + +def download(): + mapd_dir = os.path.dirname(MAPD_PATH) + if not os.path.exists(mapd_dir): + os.makedirs(mapd_dir) + with urllib.request.urlopen(URL) as f: + with open(MAPD_PATH, 'wb') as output: + output.write(f.read()) + os.fsync(output) + current_permissions = stat.S_IMODE(os.lstat(MAPD_PATH).st_mode) # <-- preserve permissions + os.chmod(MAPD_PATH, current_permissions | stat.S_IEXEC) # <-- preserve permissions + with open(VERSION_PATH, 'w') as output: + output.write(VERSION) + os.fsync(output) + +def mapd_thread(sm=None, pm=None): + rk = Ratekeeper(0.05, print_delay_threshold=None) + + while True: + try: + if not os.path.exists(MAPD_PATH): + download() + continue + if not os.path.exists(VERSION_PATH): + download() + continue + with open(VERSION_PATH) as f: + content = f.read() + if content != VERSION: + download() + continue + + process = subprocess.Popen(MAPD_PATH) + process.wait() + except Exception as e: + print(e) + + rk.keep_time() + + +def main(sm=None, pm=None): + mapd_thread(sm, pm) + +if __name__ == "__main__": + main() diff --git a/selfdrive/frogpilot/navigation/ui/navigation_functions.h b/selfdrive/frogpilot/navigation/ui/navigation_functions.h new file mode 100644 index 0000000..b4db3bc --- /dev/null +++ b/selfdrive/frogpilot/navigation/ui/navigation_functions.h @@ -0,0 +1,311 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "selfdrive/ui/qt/offroad/settings.h" +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/ui.h" + +QMap northeastMap = { + {"CT", "Connecticut"}, {"DE", "Delaware"}, {"MA", "Massachusetts"}, + {"MD", "Maryland"}, {"ME", "Maine"}, {"NH", "New Hampshire"}, + {"NJ", "New Jersey"}, {"NY", "New York"}, {"PA", "Pennsylvania"}, + {"RI", "Rhode Island"}, {"VT", "Vermont"} +}; + +QMap midwestMap = { + {"IA", "Iowa"}, {"IL", "Illinois"}, {"IN", "Indiana"}, + {"KS", "Kansas"}, {"MI", "Michigan"}, {"MN", "Minnesota"}, + {"MO", "Missouri"}, {"ND", "North Dakota"}, {"NE", "Nebraska"}, + {"OH", "Ohio"}, {"SD", "South Dakota"}, {"WI", "Wisconsin"} +}; + +QMap southMap = { + {"AL", "Alabama"}, {"AR", "Arkansas"}, {"FL", "Florida"}, + {"GA", "Georgia"}, {"KY", "Kentucky"}, {"LA", "Louisiana"}, + {"MS", "Mississippi"}, {"NC", "North Carolina"}, {"OK", "Oklahoma"}, + {"SC", "South Carolina"}, {"TN", "Tennessee"}, {"TX", "Texas"}, + {"VA", "Virginia"}, {"WV", "West Virginia"} +}; + +QMap westMap = { + {"AK", "Alaska"}, {"AZ", "Arizona"}, {"CA", "California"}, + {"CO", "Colorado"}, {"HI", "Hawaii"}, {"ID", "Idaho"}, + {"MT", "Montana"}, {"NM", "New Mexico"}, {"NV", "Nevada"}, + {"OR", "Oregon"}, {"UT", "Utah"}, {"WA", "Washington"}, + {"WY", "Wyoming"} +}; + +QMap territoriesMap = { + {"AS", "American Samoa"}, {"DC", "District of Columbia"}, + {"GM", "Guam"}, {"MP", "North Mariana Islands"}, + {"PR", "Puerto Rico"}, {"VI", "Virgin Islands"} +}; + +QMap africaMap = { + {"DZ", "Algeria"}, {"AO", "Angola"}, {"BJ", "Benin"}, {"BW", "Botswana"}, + {"BF", "Burkina Faso"}, {"BI", "Burundi"}, {"CM", "Cameroon"}, {"CV", "Cape Verde"}, + {"CF", "Central African Republic"}, {"TD", "Chad"}, {"KM", "Comoros"}, {"CG", "Congo (Brazzaville)"}, + {"CD", "Congo (Kinshasa)"}, {"CI", "Ivory Coast"}, {"DJ", "Djibouti"}, {"EG", "Egypt"}, + {"GQ", "Equatorial Guinea"}, {"ER", "Eritrea"}, {"SZ", "Eswatini"}, {"ET", "Ethiopia"}, + {"GA", "Gabon"}, {"GM", "Gambia"}, {"GH", "Ghana"}, {"GN", "Guinea"}, + {"GW", "Guinea-Bissau"}, {"KE", "Kenya"}, {"LS", "Lesotho"}, {"LR", "Liberia"}, + {"LY", "Libya"}, {"MG", "Madagascar"}, {"MW", "Malawi"}, {"ML", "Mali"}, + {"MR", "Mauritania"}, {"MU", "Mauritius"}, {"MA", "Morocco"}, {"MZ", "Mozambique"}, + {"NA", "Namibia"}, {"NE", "Niger"}, {"NG", "Nigeria"}, {"RW", "Rwanda"}, + {"ST", "Sao Tome and Principe"}, {"SN", "Senegal"}, {"SC", "Seychelles"}, {"SL", "Sierra Leone"}, + {"SO", "Somalia"}, {"ZA", "South Africa"}, {"SS", "South Sudan"}, {"SD", "Sudan"}, + {"TZ", "Tanzania"}, {"TG", "Togo"}, {"TN", "Tunisia"}, {"UG", "Uganda"}, + {"EH", "Western Sahara"}, {"ZM", "Zambia"}, {"ZW", "Zimbabwe"} +}; + +QMap antarcticaMap = { + {"AQ", "Antarctica"} +}; + +QMap asiaMap = { + {"AF", "Afghanistan"}, {"AM", "Armenia"}, {"AZ", "Azerbaijan"}, {"BH", "Bahrain"}, + {"BD", "Bangladesh"}, {"BT", "Bhutan"}, {"BN", "Brunei"}, {"KH", "Cambodia"}, + {"CN", "China"}, {"CY", "Cyprus"}, {"GE", "Georgia"}, {"IN", "India"}, + {"ID", "Indonesia"}, {"IR", "Iran"}, {"IQ", "Iraq"}, {"IL", "Israel"}, + {"JP", "Japan"}, {"JO", "Jordan"}, {"KZ", "Kazakhstan"}, {"KP", "North Korea"}, + {"KR", "South Korea"}, {"KW", "Kuwait"}, {"KG", "Kyrgyzstan"}, {"LA", "Laos"}, + {"LB", "Lebanon"}, {"MY", "Malaysia"}, {"MV", "Maldives"}, {"MN", "Mongolia"}, + {"MM", "Myanmar"}, {"NP", "Nepal"}, {"OM", "Oman"}, {"PK", "Pakistan"}, + {"PS", "Palestine"}, {"PH", "Philippines"}, {"QA", "Qatar"}, {"SA", "Saudi Arabia"}, + {"SG", "Singapore"}, {"LK", "Sri Lanka"}, {"SY", "Syria"}, {"TW", "Taiwan"}, + {"TJ", "Tajikistan"}, {"TH", "Thailand"}, {"TL", "Timor-Leste"}, {"TR", "Turkey"}, + {"TM", "Turkmenistan"}, {"AE", "United Arab Emirates"}, {"UZ", "Uzbekistan"}, {"VN", "Vietnam"}, + {"YE", "Yemen"} +}; + +QMap europeMap = { + {"AL", "Albania"}, {"AD", "Andorra"}, {"AT", "Austria"}, {"BY", "Belarus"}, + {"BE", "Belgium"}, {"BA", "Bosnia and Herzegovina"}, {"BG", "Bulgaria"}, {"HR", "Croatia"}, + {"CY", "Cyprus"}, {"CZ", "Czech Republic"}, {"DK", "Denmark"}, {"EE", "Estonia"}, + {"FI", "Finland"}, {"FR", "France"}, {"DE", "Germany"}, {"GR", "Greece"}, + {"HU", "Hungary"}, {"IS", "Iceland"}, {"IE", "Ireland"}, {"IT", "Italy"}, + {"LV", "Latvia"}, {"LI", "Liechtenstein"}, {"LT", "Lithuania"}, {"LU", "Luxembourg"}, + {"MT", "Malta"}, {"MD", "Moldova"}, {"MC", "Monaco"}, {"ME", "Montenegro"}, + {"NL", "Netherlands"}, {"MK", "North Macedonia"}, {"NO", "Norway"}, {"PL", "Poland"}, + {"PT", "Portugal"}, {"RO", "Romania"}, {"RU", "Russia"}, {"SM", "San Marino"}, + {"RS", "Serbia"}, {"SK", "Slovakia"}, {"SI", "Slovenia"}, {"ES", "Spain"}, + {"SE", "Sweden"}, {"CH", "Switzerland"}, {"TR", "Turkey"}, {"UA", "Ukraine"}, + {"GB", "United Kingdom"}, {"VA", "Vatican City"} +}; + +QMap northAmericaMap = { + {"AG", "Antigua and Barbuda"}, {"BS", "Bahamas"}, {"BB", "Barbados"}, {"BZ", "Belize"}, + {"CA", "Canada"}, {"CR", "Costa Rica"}, {"CU", "Cuba"}, {"DM", "Dominica"}, + {"DO", "Dominican Republic"}, {"SV", "El Salvador"}, {"GD", "Grenada"}, {"GT", "Guatemala"}, + {"HT", "Haiti"}, {"HN", "Honduras"}, {"JM", "Jamaica"}, {"MX", "Mexico"}, + {"NI", "Nicaragua"}, {"PA", "Panama"}, {"KN", "Saint Kitts and Nevis"}, {"LC", "Saint Lucia"}, + {"VC", "Saint Vincent and the Grenadines"}, {"TT", "Trinidad and Tobago"}, {"US", "United States"} +}; + +QMap oceaniaMap = { + {"AU", "Australia"}, {"FJ", "Fiji"}, {"KI", "Kiribati"}, {"MH", "Marshall Islands"}, + {"FM", "Micronesia"}, {"NR", "Nauru"}, {"NZ", "New Zealand"}, {"PW", "Palau"}, + {"PG", "Papua New Guinea"}, {"WS", "Samoa"}, {"SB", "Solomon Islands"}, {"TO", "Tonga"}, + {"TV", "Tuvalu"}, {"VU", "Vanuatu"} +}; + +QMap southAmericaMap = { + {"AR", "Argentina"}, {"BO", "Bolivia"}, {"BR", "Brazil"}, {"CL", "Chile"}, + {"CO", "Colombia"}, {"EC", "Ecuador"}, {"GY", "Guyana"}, {"PY", "Paraguay"}, + {"PE", "Peru"}, {"SR", "Suriname"}, {"TT", "Trinidad and Tobago"}, {"UY", "Uruguay"}, + {"VE", "Venezuela"} +}; + +class ButtonSelectionControl : public QWidget { +public: + static QString selectedStates; + static QString selectedCountries; + + explicit ButtonSelectionControl(const QString &id, const QString &title, const QString &description, + const QMap &map, bool isCountry, QWidget *parent = nullptr) + : QWidget(parent), country(isCountry) { + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setAlignment(Qt::AlignTop); + layout->setSpacing(10); + + QHBoxLayout *buttonsLayout = new QHBoxLayout(); + buttonsLayout->setSpacing(10); + layout->addLayout(buttonsLayout); + + int count = 0; + int max = country ? 3 : 4; + + QJsonObject mapsSelected = QJsonDocument::fromJson(QString::fromStdString(Params().get("MapsSelected")).toUtf8()).object(); + + for (const QString &stateCode : map.keys()) { + if (count % max == 0 && count != 0) { + buttonsLayout = new QHBoxLayout(); + buttonsLayout->setSpacing(10); + layout->addLayout(buttonsLayout); + } + + QPushButton *button = createButton(buttonsLayout, map[stateCode], stateCode); + + QString key = country ? "nations" : "states"; + if (mapsSelected.contains(key)) { + QJsonArray selectedItems = mapsSelected.value(key).toArray(); + button->setChecked(selectedItems.contains(stateCode)); + } + + count++; + } + + adjustButtonWidths(buttonsLayout); + } + +private: + bool country; + + const QString buttonStyle = R"( + QPushButton { + border-radius: 50px; font-size: 40px; font-weight: 500; + height: 100px; padding: 0 25 0 25; color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed, QPushButton:checked { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #33Ab4C; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"; + + QPushButton *createButton(QHBoxLayout *layout, const QString &label, const QString &stateCode) { + QPushButton *button = new QPushButton(label, this); + button->setCheckable(true); + button->setStyleSheet(buttonStyle); + connect(button, &QPushButton::clicked, this, [this, button, stateCode] { updateState(stateCode, button); }); + layout->addWidget(button); + return button; + } + + void adjustButtonWidths(QHBoxLayout *layout) { + if (!layout || layout->count() == (country ? 3 : 4)) return; + + for (int i = 0; i < layout->count(); ++i) { + QWidget *widget = layout->itemAt(i)->widget(); + QPushButton *button = qobject_cast(widget); + if (button) { + button->setMinimumWidth(button->sizeHint().width()); + } + } + } + + void updateState(const QString &newState, QPushButton *button) { + QString &selectedList = country ? selectedCountries : selectedStates; + QStringList tempList = selectedList.split(','); + + if (button->isChecked()) { + if (!selectedList.isEmpty()) selectedList += ","; + selectedList += newState; + } else { + tempList.removeAll(newState); + selectedList = tempList.join(','); + } + + Params("/dev/shm/params").remove("OSMDownloadLocations"); + } +}; + +QString ButtonSelectionControl::selectedStates = ""; +QString ButtonSelectionControl::selectedCountries = ""; + +namespace { + template + T extractFromJson(const std::string &jsonData, const std::string &key, T defaultValue = 0) { + std::string::size_type pos = jsonData.find(key); + return pos != std::string::npos ? std::stol(jsonData.substr(pos + key.length())) : defaultValue; + } +} + +QString formatTime(long timeInSeconds) { + long minutes = timeInSeconds / 60; + long seconds = timeInSeconds % 60; + QString formattedTime = (minutes > 0) ? QString::number(minutes) + "m " : ""; + formattedTime += QString::number(seconds) + "s"; + return formattedTime; +} + +QString formatDateTime(const std::chrono::time_point &timePoint) { + return QDateTime::fromTime_t(std::chrono::system_clock::to_time_t(timePoint)).toString("h:mm ap"); +} + +QString calculateElapsedTime(int totalFiles, int downloadedFiles, const std::chrono::steady_clock::time_point &startTime) { + using namespace std::chrono; + if (totalFiles <= 0 || downloadedFiles >= totalFiles) return "Calculating..."; + + long elapsed = duration_cast(steady_clock::now() - startTime).count(); + return formatTime(elapsed); +} + +QString calculateETA(int totalFiles, int downloadedFiles, const std::chrono::steady_clock::time_point &startTime) { + using namespace std::chrono; + if (totalFiles <= 0 || downloadedFiles >= totalFiles) return "Calculating..."; + + long elapsed = duration_cast(steady_clock::now() - startTime).count(); + + if (downloadedFiles == 0 || elapsed <= 0) { + return "Calculating..."; + } + + double averageTimePerFile = static_cast(elapsed) / downloadedFiles; + int remainingFiles = totalFiles - downloadedFiles; + long estimatedTimeRemaining = static_cast(averageTimePerFile * remainingFiles); + + std::chrono::time_point estimatedCompletionTime = system_clock::now() + seconds(estimatedTimeRemaining); + QString estimatedTimeStr = formatDateTime(estimatedCompletionTime); + + return formatTime(estimatedTimeRemaining) + " (" + estimatedTimeStr + ")"; + +} + +QString formatDownloadStatus(int totalFiles, int downloadedFiles) { + if (totalFiles <= 0) return "Calculating..."; + if (downloadedFiles >= totalFiles) return "Downloaded"; + + int percentage = static_cast(100 * downloadedFiles / totalFiles); + return QString::asprintf("Downloading: %d/%d (%d%%)", downloadedFiles, totalFiles, percentage); +} + +quint64 calculateDirectorySize(const QString &path) { + quint64 totalSize = 0; + QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + QFileInfo fileInfo(it.filePath()); + if (fileInfo.isFile()) { + totalSize += fileInfo.size(); + } + } + return totalSize; +} + +QString formatSize(qint64 size) { + const qint64 kb = 1024; + const qint64 mb = 1024 * kb; + const qint64 gb = 1024 * mb; + + if (size < gb) { + double sizeMB = size / static_cast(mb); + return QString::number(sizeMB, 'f', 2) + " MB"; + } else { + double sizeGB = size / static_cast(gb); + return QString::number(sizeGB, 'f', 2) + " GB"; + } +} diff --git a/selfdrive/frogpilot/navigation/ui/navigation_settings.cc b/selfdrive/frogpilot/navigation/ui/navigation_settings.cc index 9bf0c2e..df04ead 100644 --- a/selfdrive/frogpilot/navigation/ui/navigation_settings.cc +++ b/selfdrive/frogpilot/navigation/ui/navigation_settings.cc @@ -1,5 +1,6 @@ #include +#include "selfdrive/frogpilot/navigation/ui/navigation_functions.h" #include "selfdrive/frogpilot/navigation/ui/navigation_settings.h" FrogPilotNavigationPanel::FrogPilotNavigationPanel(QWidget *parent) : QFrame(parent), scene(uiState()->scene) { @@ -19,6 +20,423 @@ FrogPilotNavigationPanel::FrogPilotNavigationPanel(QWidget *parent) : QFrame(par QObject::connect(primelessPanel, &Primeless::backPress, [=]() { mainLayout->setCurrentWidget(navigationWidget); }); list->addItem(manageNOOButton); manageNOOButton->setVisible(!uiState()->hasPrime()); + + std::vector scheduleOptions{tr("Manually"), tr("Weekly"), tr("Monthly")}; + ButtonParamControl *preferredSchedule = new ButtonParamControl("PreferredSchedule", tr("Maps Scheduler"), + tr("Choose the frequency for updating maps with the latest OpenStreetMap (OSM) changes. " + "Weekly updates begin at midnight every Sunday, while monthly updates start at midnight on the 1st of each month. " + "If your device is off or not connected to WiFi during a scheduled update, the download will be conducted the next " + "time you're offroad with a WiFi connection."), + "", + scheduleOptions); + schedule = params.getInt("PreferredSchedule"); + schedulePending = params.getBool("SchedulePending"); + list->addItem(preferredSchedule); + + list->addItem(offlineMapsSize = new LabelControl(tr("Offline Maps Size"), formatSize(calculateDirectorySize(offlineFolderPath)))); + offlineMapsSize->setVisible(true); + list->addItem(lastMapsDownload = new LabelControl(tr("Last Download"), "")); + lastMapsDownload->setVisible(!params.get("LastMapsUpdate").empty()); + lastMapsDownload->setText(QString::fromStdString(params.get("LastMapsUpdate"))); + list->addItem(offlineMapsStatus = new LabelControl(tr("Offline Maps Status"), "")); + offlineMapsStatus->setVisible(false); + list->addItem(offlineMapsETA = new LabelControl(tr("Offline Maps ETA"), "")); + offlineMapsETA->setVisible(false); + list->addItem(offlineMapsElapsed = new LabelControl(tr("Time Elapsed"), "")); + offlineMapsElapsed->setVisible(false); + + cancelDownloadButton = new ButtonControl(tr("Cancel Download"), tr("CANCEL"), tr("Cancel your current download.")); + QObject::connect(cancelDownloadButton, &ButtonControl::clicked, [this] { cancelDownload(this); }); + list->addItem(cancelDownloadButton); + cancelDownloadButton->setVisible(false); + + downloadOfflineMapsButton = new ButtonControl(tr("Download Offline Maps"), tr("DOWNLOAD"), tr("Download your selected offline maps to use with openpilot.")); + QObject::connect(downloadOfflineMapsButton, &ButtonControl::clicked, [this] { downloadMaps(); }); + list->addItem(downloadOfflineMapsButton); + downloadOfflineMapsButton->setVisible(!params.get("MapsSelected").empty()); + + SelectMaps *mapsPanel = new SelectMaps(this); + mainLayout->addWidget(mapsPanel); + + QObject::connect(mapsPanel, &SelectMaps::setMaps, [=]() { setMaps(); }); + + ButtonControl *selectMapsButton = new ButtonControl(tr("Select Offline Maps"), tr("SELECT"), tr("Select your maps to use with OSM.")); + QObject::connect(selectMapsButton, &ButtonControl::clicked, [=]() { mainLayout->setCurrentWidget(mapsPanel); }); + QObject::connect(mapsPanel, &SelectMaps::backPress, [=]() { mainLayout->setCurrentWidget(navigationWidget); }); + list->addItem(selectMapsButton); + + removeOfflineMapsButton = new ButtonControl(tr("Remove Offline Maps"), tr("REMOVE"), tr("Remove your downloaded offline maps to clear up storage space.")); + QObject::connect(removeOfflineMapsButton, &ButtonControl::clicked, [this] { removeMaps(this); }); + list->addItem(removeOfflineMapsButton); + removeOfflineMapsButton->setVisible(QDir(offlineFolderPath).exists()); + + navigationLayout->addWidget(new ScrollView(list, navigationWidget)); + navigationWidget->setLayout(navigationLayout); + mainLayout->addWidget(navigationWidget); + mainLayout->setCurrentWidget(navigationWidget); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotNavigationPanel::updateState); + + checkIfUpdateMissed(); +} + +void FrogPilotNavigationPanel::hideEvent(QHideEvent *event) { + QWidget::hideEvent(event); + mainLayout->setCurrentWidget(navigationWidget); +} + +void FrogPilotNavigationPanel::updateState() { + if (!isVisible() && downloadActive) updateVisibility(downloadActive); + if (downloadActive) updateStatuses(); + if (schedule) downloadSchedule(); +} + +void FrogPilotNavigationPanel::updateStatuses() { + static std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now(); + osmDownloadProgress = params.get("OSMDownloadProgress"); + + const int totalFiles = extractFromJson(osmDownloadProgress, "\"total_files\":"); + const int downloadedFiles = extractFromJson(osmDownloadProgress, "\"downloaded_files\":"); + + if (paramsMemory.get("OSMDownloadLocations").empty()) { + downloadActive = false; + updateDownloadedLabel(); + qint64 fileSize = calculateDirectorySize(offlineFolderPath); + offlineMapsSize->setText(formatSize(fileSize)); + previousOSMDownloadProgress = osmDownloadProgress; + } + + if (osmDownloadProgress != previousOSMDownloadProgress && isVisible()) { + qint64 fileSize = calculateDirectorySize(offlineFolderPath); + offlineMapsSize->setText(formatSize(fileSize)); + previousOSMDownloadProgress = osmDownloadProgress; + } + + elapsedTime = calculateElapsedTime(totalFiles, downloadedFiles, startTime); + + offlineMapsElapsed->setText(elapsedTime); + offlineMapsETA->setText(calculateETA(totalFiles, downloadedFiles, startTime)); + offlineMapsStatus->setText(formatDownloadStatus(totalFiles, downloadedFiles)); + + if (downloadActive != previousDownloadActive) { + startTime = !downloadActive ? std::chrono::steady_clock::now() : startTime; + updateVisibility(downloadActive); + previousDownloadActive = downloadActive; + } +} + +void FrogPilotNavigationPanel::updateVisibility(bool visibility) { + cancelDownloadButton->setVisible(visibility); + offlineMapsElapsed->setVisible(visibility); + offlineMapsETA->setVisible(visibility); + offlineMapsStatus->setVisible(visibility); + downloadOfflineMapsButton->setVisible(!visibility); + lastMapsDownload->setVisible(QDir(offlineFolderPath).exists() && !downloadActive); + removeOfflineMapsButton->setVisible(QDir(offlineFolderPath).exists() && !downloadActive); + update(); +} + +void FrogPilotNavigationPanel::checkIfUpdateMissed() { + std::string lastMapsUpdate = params.get("LastMapsUpdate"); + + if (lastMapsUpdate.empty() || schedule == 0) { + return; + } + + std::time_t currentTime = std::time(nullptr); + std::tm *now = std::localtime(¤tTime); + + std::tm lastUpdate = {}; + sscanf(lastMapsUpdate.c_str(), "%d-%d-%d", &lastUpdate.tm_year, &lastUpdate.tm_mon, &lastUpdate.tm_mday); + lastUpdate.tm_year -= 1900; + lastUpdate.tm_mon -= 1; + + std::time_t lastUpdateTime = std::mktime(&lastUpdate); + std::tm *lastUpdateDay = std::localtime(&lastUpdateTime); + + if (schedule == 1) { + schedulePending = (now->tm_wday == 0 && lastUpdateDay->tm_wday != 0) || (now->tm_wday > lastUpdateDay->tm_wday); + } else if (schedule == 2) { + schedulePending = (now->tm_mday == 1 && lastUpdate.tm_mday != 1) || (now->tm_mon != lastUpdate.tm_mon); + } +} + +void FrogPilotNavigationPanel::updateDownloadedLabel() { + std::time_t t = std::time(nullptr); + std::tm now = *std::localtime(&t); + char dateBuffer[11]; + std::strftime(dateBuffer, sizeof(dateBuffer), "%Y-%m-%d", &now); + QDate date = QDate::fromString(dateBuffer, "yyyy-MM-dd"); + + int day = date.day(); + std::string suffix = (day == 1 || day == 21 || day == 31) ? "st" : + (day == 2 || day == 22) ? "nd" : + (day == 3 || day == 23) ? "rd" : "th"; + std::string lastMapsUpdate = date.toString("MMMM d").toStdString() + suffix + date.toString(", yyyy").toStdString(); + lastMapsDownload->setText(QString::fromStdString(lastMapsUpdate)); + params.put("LastMapsUpdate", lastMapsUpdate); +} + +void FrogPilotNavigationPanel::downloadSchedule() { + const bool wifi = (*uiState()->sm)["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; + + const std::time_t t = std::time(nullptr); + const std::tm *now = std::localtime(&t); + + const bool isScheduleTime = (schedule == 1 && now->tm_wday == 0) || (schedule == 2 && now->tm_mday == 1); + + if ((isScheduleTime || schedulePending) && !(scene.started || scheduleCompleted) && wifi) { + downloadMaps(); + scheduleCompleted = true; + + if (schedulePending) { + schedulePending = false; + params.putBool("SchedulePending", false); + } + } else if (!isScheduleTime) { + scheduleCompleted = false; + } else { + if (!schedulePending) { + params.putBool("SchedulePending", true); + } + schedulePending = true; + } +} + +void FrogPilotNavigationPanel::cancelDownload(QWidget *parent) { + if (FrogPilotConfirmationDialog::yesorno("Are you sure you want to cancel the download?", parent)) { + std::thread([&] { + std::system("pkill mapd"); + }).detach(); + if (FrogPilotConfirmationDialog::toggle("Reboot required to re-enable map downloads", "Reboot Now", parent)) { + Hardware::soft_reboot(); + } + downloadActive = false; + updateVisibility(downloadActive); + downloadOfflineMapsButton->setVisible(downloadActive); + } +} + +void FrogPilotNavigationPanel::downloadMaps() { + params.remove("OSMDownloadProgress"); + paramsMemory.put("OSMDownloadLocations", params.get("MapsSelected")); + removeOfflineMapsButton->setVisible(true); + downloadActive = true; +} + +void FrogPilotNavigationPanel::removeMaps(QWidget *parent) { + if (FrogPilotConfirmationDialog::yesorno("Are you sure you want to delete all of your downloaded maps?", parent)) { + std::thread([&] { + lastMapsDownload->setVisible(false); + removeOfflineMapsButton->setVisible(false); + offlineMapsSize->setText(formatSize(0)); + params.remove("LastMapsUpdate"); + std::system("rm -rf /data/media/0/osm/offline"); + }).detach(); + } +} + +void FrogPilotNavigationPanel::setMaps() { + std::thread([&] { + QStringList states = ButtonSelectionControl::selectedStates.split(',', QString::SkipEmptyParts); + QStringList countries = ButtonSelectionControl::selectedCountries.split(',', QString::SkipEmptyParts); + + QJsonObject json; + json.insert("states", QJsonArray::fromStringList(states)); + json.insert("nations", QJsonArray::fromStringList(countries)); + + params.put("MapsSelected", QJsonDocument(json).toJson(QJsonDocument::Compact).toStdString()); + + if (!states.isEmpty() || !countries.isEmpty()) { + downloadOfflineMapsButton->setVisible(true); + update(); + } else { + params.remove("MapsSelected"); + } + }).detach(); +} + +SelectMaps::SelectMaps(QWidget *parent) : QWidget(parent) { + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + QHBoxLayout *buttonsLayout = new QHBoxLayout(); + buttonsLayout->setContentsMargins(20, 40, 20, 0); + + backButton = new QPushButton(tr("Back"), this); + statesButton = new QPushButton(tr("States"), this); + countriesButton = new QPushButton(tr("Countries"), this); + + backButton->setFixedSize(400, 100); + statesButton->setFixedSize(400, 100); + countriesButton->setFixedSize(400, 100); + + buttonsLayout->addWidget(backButton); + buttonsLayout->addStretch(); + buttonsLayout->addWidget(statesButton); + buttonsLayout->addStretch(); + buttonsLayout->addWidget(countriesButton); + mainLayout->addLayout(buttonsLayout); + + mainLayout->addWidget(horizontalLine()); + mainLayout->setSpacing(20); + + mapsLayout = new QStackedLayout(); + mapsLayout->setMargin(40); + mapsLayout->setSpacing(20); + mainLayout->addLayout(mapsLayout); + + QObject::connect(backButton, &QPushButton::clicked, this, [this]() { emit backPress(), emit setMaps(); }); + + FrogPilotListWidget *statesList = new FrogPilotListWidget(); + + LabelControl *northeastLabel = new LabelControl(tr("United States - Northeast"), ""); + statesList->addItem(northeastLabel); + + ButtonSelectionControl *northeastControl = new ButtonSelectionControl("", tr(""), tr(""), northeastMap, false); + statesList->addItem(northeastControl); + + LabelControl *midwestLabel = new LabelControl(tr("United States - Midwest"), ""); + statesList->addItem(midwestLabel); + + ButtonSelectionControl *midwestControl = new ButtonSelectionControl("", tr(""), tr(""), midwestMap, false); + statesList->addItem(midwestControl); + + LabelControl *southLabel = new LabelControl(tr("United States - South"), ""); + statesList->addItem(southLabel); + + ButtonSelectionControl *southControl = new ButtonSelectionControl("", tr(""), tr(""), southMap, false); + statesList->addItem(southControl); + + LabelControl *westLabel = new LabelControl(tr("United States - West"), ""); + statesList->addItem(westLabel); + + ButtonSelectionControl *westControl = new ButtonSelectionControl("", tr(""), tr(""), westMap, false); + statesList->addItem(westControl); + + LabelControl *territoriesLabel = new LabelControl(tr("United States - Territories"), ""); + statesList->addItem(territoriesLabel); + + ButtonSelectionControl *territoriesControl = new ButtonSelectionControl("", tr(""), tr(""), territoriesMap, false); + statesList->addItem(territoriesControl); + + statesScrollView = new ScrollView(statesList); + mapsLayout->addWidget(statesScrollView); + + QObject::connect(statesButton, &QPushButton::clicked, this, [this]() { + mapsLayout->setCurrentWidget(statesScrollView); + statesButton->setStyleSheet(activeButtonStyle); + countriesButton->setStyleSheet(normalButtonStyle); + }); + + FrogPilotListWidget *countriesList = new FrogPilotListWidget(); + + LabelControl *africaLabel = new LabelControl(tr("Africa"), ""); + countriesList->addItem(africaLabel); + + ButtonSelectionControl *africaControl = new ButtonSelectionControl("", tr(""), tr(""), africaMap, true); + countriesList->addItem(africaControl); + + LabelControl *antarcticaLabel = new LabelControl(tr("Antarctica"), ""); + countriesList->addItem(antarcticaLabel); + + ButtonSelectionControl *antarcticaControl = new ButtonSelectionControl("", tr(""), tr(""), antarcticaMap, true); + countriesList->addItem(antarcticaControl); + + LabelControl *asiaLabel = new LabelControl(tr("Asia"), ""); + countriesList->addItem(asiaLabel); + + ButtonSelectionControl *asiaControl = new ButtonSelectionControl("", tr(""), tr(""), asiaMap, true); + countriesList->addItem(asiaControl); + + LabelControl *europeLabel = new LabelControl(tr("Europe"), ""); + countriesList->addItem(europeLabel); + + ButtonSelectionControl *europeControl = new ButtonSelectionControl("", tr(""), tr(""), europeMap, true); + countriesList->addItem(europeControl); + + LabelControl *northAmericaLabel = new LabelControl(tr("North America"), ""); + countriesList->addItem(northAmericaLabel); + + ButtonSelectionControl *northAmericaControl = new ButtonSelectionControl("", tr(""), tr(""), northAmericaMap, true); + countriesList->addItem(northAmericaControl); + + LabelControl *oceaniaLabel = new LabelControl(tr("Oceania"), ""); + countriesList->addItem(oceaniaLabel); + + ButtonSelectionControl *oceaniaControl = new ButtonSelectionControl("", tr(""), tr(""), oceaniaMap, true); + countriesList->addItem(oceaniaControl); + + LabelControl *southAmericaLabel = new LabelControl(tr("South America"), ""); + countriesList->addItem(southAmericaLabel); + + ButtonSelectionControl *southAmericaControl = new ButtonSelectionControl("", tr(""), tr(""), southAmericaMap, true); + countriesList->addItem(southAmericaControl); + + countriesScrollView = new ScrollView(countriesList); + mapsLayout->addWidget(countriesScrollView); + + QObject::connect(countriesButton, &QPushButton::clicked, this, [this]() { + mapsLayout->setCurrentWidget(countriesScrollView); + statesButton->setStyleSheet(normalButtonStyle); + countriesButton->setStyleSheet(activeButtonStyle); + }); + + mapsLayout->setCurrentWidget(statesScrollView); + statesButton->setStyleSheet(activeButtonStyle); + + setStyleSheet(R"( + QPushButton { + font-size: 50px; + margin: 0px; + padding: 15px; + border-width: 0; + border-radius: 30px; + color: #dddddd; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + )"); +} + +QString SelectMaps::activeButtonStyle = R"( + font-size: 50px; + margin: 0px; + padding: 15px; + border-width: 0; + border-radius: 30px; + color: #dddddd; + background-color: #33Ab4C; +)"; + +QString SelectMaps::normalButtonStyle = R"( + font-size: 50px; + margin: 0px; + padding: 15px; + border-width: 0; + border-radius: 30px; + color: #dddddd; + background-color: #393939; +)"; + +QFrame *SelectMaps::horizontalLine(QWidget *parent) const { + QFrame *line = new QFrame(parent); + + line->setFrameShape(QFrame::StyledPanel); + line->setStyleSheet(R"( + border-width: 2px; + border-bottom-style: solid; + border-color: gray; + )"); + line->setFixedHeight(2); + + return line; +} + +void SelectMaps::hideEvent(QHideEvent *event) { + QWidget::hideEvent(event); + emit setMaps(); } Primeless::Primeless(QWidget *parent) : QWidget(parent) { diff --git a/selfdrive/frogpilot/navigation/ui/navigation_settings.h b/selfdrive/frogpilot/navigation/ui/navigation_settings.h index 2bf9ede..a27c292 100644 --- a/selfdrive/frogpilot/navigation/ui/navigation_settings.h +++ b/selfdrive/frogpilot/navigation/ui/navigation_settings.h @@ -1,5 +1,6 @@ #pragma once +#include "selfdrive/frogpilot/ui/frogpilot_ui_functions.h" #include "selfdrive/ui/qt/network/wifi_manager.h" #include "selfdrive/ui/qt/offroad/settings.h" #include "selfdrive/ui/qt/widgets/scrollview.h" @@ -44,6 +45,33 @@ signals: void backPress(); }; +class SelectMaps : public QWidget { + Q_OBJECT + +public: + explicit SelectMaps(QWidget *parent = nullptr); + + QFrame *horizontalLine(QWidget *parent = nullptr) const; + +private: + void hideEvent(QHideEvent *event); + + ScrollView *countriesScrollView; + ScrollView *statesScrollView; + QStackedLayout *mapsLayout; + + QPushButton *backButton; + QPushButton *statesButton; + QPushButton *countriesButton; + + static QString activeButtonStyle; + static QString normalButtonStyle; + +signals: + void backPress(); + void setMaps(); +}; + class FrogPilotNavigationPanel : public QFrame { Q_OBJECT @@ -51,9 +79,42 @@ public: explicit FrogPilotNavigationPanel(QWidget *parent = 0); private: + void cancelDownload(QWidget *parent); + void checkIfUpdateMissed(); + void downloadMaps(); + void downloadSchedule(); + void hideEvent(QHideEvent *event); + void removeMaps(QWidget *parent); + void setMaps(); + void updateDownloadedLabel(); + void updateState(); + void updateStatuses(); + void updateVisibility(bool visibility); + QStackedLayout *mainLayout; QWidget *navigationWidget; + ButtonControl *cancelDownloadButton; + ButtonControl *downloadOfflineMapsButton; + ButtonControl *redownloadOfflineMapsButton; + ButtonControl *removeOfflineMapsButton; + + LabelControl *lastMapsDownload; + LabelControl *offlineMapsSize; + LabelControl *offlineMapsStatus; + LabelControl *offlineMapsETA; + LabelControl *offlineMapsElapsed; + + bool downloadActive; + bool previousDownloadActive; + bool scheduleCompleted; + bool schedulePending; + int schedule; + QString elapsedTime; + QString offlineFolderPath = "/data/media/0/osm/offline"; + std::string osmDownloadProgress; + std::string previousOSMDownloadProgress; + Params params; Params paramsMemory{"/dev/shm/params"}; UIScene &scene; diff --git a/selfdrive/frogpilot/ui/visual_settings.cc b/selfdrive/frogpilot/ui/visual_settings.cc index 27193fd..6c460eb 100644 --- a/selfdrive/frogpilot/ui/visual_settings.cc +++ b/selfdrive/frogpilot/ui/visual_settings.cc @@ -32,6 +32,7 @@ FrogPilotVisualsPanel::FrogPilotVisualsPanel(SettingsWindow *parent) : FrogPilot {"BlindSpotPath", "Blind Spot Path", "Visualize your blind spots with a red path when another vehicle is detected nearby.", ""}, {"FPSCounter", "FPS Counter", "Display the Frames Per Second (FPS) of your onroad UI for monitoring system performance.", ""}, {"LeadInfo", "Lead Info and Logics", "Get detailed information about the vehicle ahead, including speed and distance, and the logic behind your following distance.", ""}, + {"RoadNameUI", "Road Name", "See the name of the road you're on at the bottom of your screen. Sourced from OpenStreetMap.", ""}, {"DriverCamera", "Driver Camera On Reverse", "Show the driver's camera feed when you shift to reverse.", "../assets/img_driver_face_static.png"}, diff --git a/selfdrive/frogpilot/ui/visual_settings.h b/selfdrive/frogpilot/ui/visual_settings.h index 9cac028..8b4c92c 100644 --- a/selfdrive/frogpilot/ui/visual_settings.h +++ b/selfdrive/frogpilot/ui/visual_settings.h @@ -30,7 +30,7 @@ private: std::set alertVolumeControlKeys = {"EngageVolume", "DisengageVolume", "RefuseVolume", "PromptVolume", "PromptDistractedVolume", "WarningSoftVolume", "WarningImmediateVolume"}; std::set customAlertsKeys = {"GreenLightAlert", "LeadDepartingAlert", "LoudBlindspotAlert"}; - std::set customOnroadUIKeys = {"AccelerationPath", "AdjacentPath", "BlindSpotPath", "FPSCounter", "LeadInfo"}; + std::set customOnroadUIKeys = {"AccelerationPath", "AdjacentPath", "BlindSpotPath", "FPSCounter", "LeadInfo", "RoadNameUI"}; std::set customThemeKeys = {"HolidayThemes", "CustomColors", "CustomIcons", "CustomSignals", "CustomSounds"}; std::set modelUIKeys = {"DynamicPathWidth", "LaneLinesWidth", "PathEdgeWidth", "PathWidth", "RoadEdgesWidth", "UnlimitedLength"}; std::set qolKeys = {"DriveStats", "FullMap", "HideSpeed"}; diff --git a/selfdrive/locationd/paramsd.py b/selfdrive/locationd/paramsd.py index d124eb5..711879d 100755 --- a/selfdrive/locationd/paramsd.py +++ b/selfdrive/locationd/paramsd.py @@ -14,6 +14,7 @@ from openpilot.selfdrive.locationd.models.car_kf import CarKalman, ObservationKi from openpilot.selfdrive.locationd.models.constants import GENERATED_DIR from openpilot.common.swaglog import cloudlog +params_memory = Params("/dev/shm/params") MAX_ANGLE_OFFSET_DELTA = 20 * DT_MDL # Max 20 deg/s ROLL_MAX_DELTA = math.radians(20.0) * DT_MDL # 20deg in 1 second is well within curvature limits @@ -191,6 +192,12 @@ def main(): learner.handle_log(t, which, sm[which]) if sm.updated['liveLocationKalman']: + location = sm['liveLocationKalman'] + if (location.status == log.LiveLocationKalman.Status.valid) and location.positionGeodetic.valid and location.gpsOK: + bearing = math.degrees(location.calibratedOrientationNED.value[2]) + lat = location.positionGeodetic.value[0] + lon = location.positionGeodetic.value[1] + params_memory.put("LastGPSPosition", json.dumps({ "latitude": lat, "longitude": lon, "bearing": bearing })) x = learner.kf.x P = np.sqrt(learner.kf.P.diagonal()) if not all(map(math.isfinite, x)): diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index 41edf9a..6a3e442 100644 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -347,7 +347,7 @@ def manager_cleanup() -> None: def update_frogpilot_params(params, params_memory): - keys = ["DisableOnroadUploads", "FireTheBabysitter", "NoLogging", "NoUploads"] + keys = ["DisableOnroadUploads", "FireTheBabysitter", "NoLogging", "NoUploads", "RoadNameUI"] for key in keys: params_memory.put_bool(key, params.get_bool(key)) diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 9922329..76aee39 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -51,6 +51,9 @@ def allow_logging(started, params, params_memory, CP: car.CarParams) -> bool: allow_logging = not (params_memory.get_bool("FireTheBabysitter") and params_memory.get_bool("NoLogging")) return allow_logging and logging(started, params, params_memory, CP) +def osm(started, params, params_memory, CP: car.CarParams) -> bool: + return params_memory.get_bool("RoadNameUI") + procs = [ DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"), @@ -100,6 +103,7 @@ procs = [ # FrogPilot processes PythonProcess("fleet_manager", "selfdrive.frogpilot.fleetmanager.fleet_manager", always_run), PythonProcess("frogpilot_process", "selfdrive.frogpilot.functions.frogpilot_process", always_run), + PythonProcess("mapd", "selfdrive.frogpilot.functions.mapd", osm), ] managed_processes = {p.name: p for p in procs} diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index df9eb76..207f7a9 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -283,7 +283,7 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) { int margin = 40; int radius = 30; - int offset = scene.always_on_lateral || scene.conditional_experimental ? 25 : 0; + int offset = scene.always_on_lateral || scene.conditional_experimental || scene.road_name_ui ? 25 : 0; if (alert.size == cereal::ControlsState::AlertSize::FULL) { margin = 0; radius = 0; @@ -827,7 +827,7 @@ void AnnotatedCameraWidget::drawDriverState(QPainter &painter, const UIState *s) // base icon int offset = UI_BORDER_SIZE + btn_size / 2; - offset += alwaysOnLateral || conditionalExperimental ? 25 : 0; + offset += alwaysOnLateral || conditionalExperimental || roadNameUI ? 25 : 0; int x = rightHandDM ? width() - offset : offset; int y = height() - offset; float opacity = dmActive ? 0.65 : 0.2; @@ -1137,6 +1137,8 @@ void AnnotatedCameraWidget::updateFrogPilotWidgets(QPainter &p) { mapOpen = scene.map_open; fullMapOpen = mapOpen && scene.full_map; + roadNameUI = scene.road_name_ui; + showDriverCamera = scene.show_driver_camera; turnSignalLeft = scene.turn_signal_left; @@ -1147,7 +1149,7 @@ void AnnotatedCameraWidget::updateFrogPilotWidgets(QPainter &p) { drawLeadInfo(p); } - if (alwaysOnLateral || conditionalExperimental) { + if (alwaysOnLateral || conditionalExperimental || roadNameUI) { drawStatusBar(p); } @@ -1484,6 +1486,8 @@ void AnnotatedCameraWidget::drawStatusBar(QPainter &p) { {15, "Experimental Mode activated for stop" + (mapOpen ? "" : QString(" sign / stop light"))}, }; + QString roadName = roadNameUI ? QString::fromStdString(paramsMemory.get("RoadName")) : QString(); + // Update status text if (alwaysOnLateralActive) { newStatus = QString("Always On Lateral active") + (mapOpen ? "" : ". Press the \"Cruise Control\" button to disable"); @@ -1506,8 +1510,8 @@ void AnnotatedCameraWidget::drawStatusBar(QPainter &p) { } } - // Check if status has changed - if (newStatus != lastShownStatus) { + // Check if status has changed or if the road name is empty + if (newStatus != lastShownStatus || roadName.isEmpty()) { displayStatusText = true; lastShownStatus = newStatus; timer.restart(); @@ -1521,10 +1525,15 @@ void AnnotatedCameraWidget::drawStatusBar(QPainter &p) { p.setRenderHint(QPainter::TextAntialiasing); // Calculate text opacity + static qreal roadNameOpacity; static qreal statusTextOpacity; int elapsed = timer.elapsed(); if (displayStatusText) { statusTextOpacity = qBound(0.0, 1.0 - (elapsed - textDuration) / fadeDuration, 1.0); + roadNameOpacity = 1.0 - statusTextOpacity; + } else { + roadNameOpacity = qBound(0.0, elapsed / fadeDuration, 1.0); + statusTextOpacity = 0.0; } // Draw the status text @@ -1533,6 +1542,14 @@ void AnnotatedCameraWidget::drawStatusBar(QPainter &p) { textRect.moveBottom(statusBarRect.bottom() - 50); p.drawText(textRect, Qt::AlignCenter | Qt::TextWordWrap, newStatus); + // Draw the road name with the calculated opacity + if (!roadName.isEmpty()) { + p.setOpacity(roadNameOpacity); + textRect = p.fontMetrics().boundingRect(statusBarRect, Qt::AlignCenter | Qt::TextWordWrap, roadName); + textRect.moveBottom(statusBarRect.bottom() - 50); + p.drawText(textRect, Qt::AlignCenter | Qt::TextWordWrap, roadName); + } + p.restore(); } diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index 1d7555d..bd734e2 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -156,6 +156,7 @@ private: bool fullMapOpen; bool leadInfo; bool mapOpen; + bool roadNameUI; bool showDriverCamera; bool turnSignalLeft; bool turnSignalRight; diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 50017d8..f20af63 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -308,6 +308,7 @@ void ui_update_frogpilot_params(UIState *s) { scene.fps_counter = custom_onroad_ui && params.getBool("FPSCounter"); scene.lead_info = custom_onroad_ui && params.getBool("LeadInfo"); scene.use_si = scene.lead_info && params.getBool("UseSI"); + scene.road_name_ui = custom_onroad_ui && params.getBool("RoadNameUI"); bool custom_theme = params.getBool("CustomTheme"); scene.custom_colors = custom_theme ? params.getInt("CustomColors") : 0; diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 1bc16ec..d7e080b 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -197,6 +197,7 @@ typedef struct UIScene { bool numerical_temp; bool reverse_cruise; bool reverse_cruise_ui; + bool road_name_ui; bool show_driver_camera; bool turn_signal_left; bool turn_signal_right;