diff --git a/selfdrive/monitoring/driver_monitor.py b/selfdrive/monitoring/driver_monitor.py index 2279002..74e0067 100644 --- a/selfdrive/monitoring/driver_monitor.py +++ b/selfdrive/monitoring/driver_monitor.py @@ -19,12 +19,19 @@ class DRIVER_MONITOR_SETTINGS(): def __init__(self): self._DT_DMON = DT_DMON # ref (page15-16): https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:42018X1947&rid=2 - self._AWARENESS_TIME = 30. # passive wheeltouch total timeout - self._AWARENESS_PRE_TIME_TILL_TERMINAL = 15. - self._AWARENESS_PROMPT_TIME_TILL_TERMINAL = 6. - self._DISTRACTED_TIME = 11. # active monitoring total timeout - self._DISTRACTED_PRE_TIME_TILL_TERMINAL = 8. - self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6. + # self._AWARENESS_TIME = 30. # passive wheeltouch total timeout + # self._AWARENESS_PRE_TIME_TILL_TERMINAL = 15. + # self._AWARENESS_PROMPT_TIME_TILL_TERMINAL = 6. + # self._DISTRACTED_TIME = 11. # active monitoring total timeout + # self._DISTRACTED_PRE_TIME_TILL_TERMINAL = 8. + # self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6. + + self._AWARENESS_TIME = 60. # passive wheeltouch total timeout + self._AWARENESS_PRE_TIME_TILL_TERMINAL = 60. + self._AWARENESS_PROMPT_TIME_TILL_TERMINAL = 30. + self._DISTRACTED_TIME = 30. # active monitoring total timeout + self._DISTRACTED_PRE_TIME_TILL_TERMINAL = 60. + self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 30. self._FACE_THRESHOLD = 0.7 self._EYE_THRESHOLD = 0.65 diff --git a/selfdrive/ui/.gitignore b/selfdrive/ui/.gitignore old mode 100644 new mode 100755 index f972481..99f9097 --- a/selfdrive/ui/.gitignore +++ b/selfdrive/ui/.gitignore @@ -3,13 +3,11 @@ moc_* translations/main_test_en.* -_text -_spinner - ui -mui watch3 installer/installers/* +qt/text +qt/spinner qt/setup/setup qt/setup/reset qt/setup/wifi diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript new file mode 100755 index 0000000..8a90134 --- /dev/null +++ b/selfdrive/ui/SConscript @@ -0,0 +1,154 @@ +import os +import json +Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', + 'cereal', 'transformations') + +base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', + 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread', 'OmxCore', 'avformat', 'avcodec', 'avutil', 'yuv'] + qt_env["LIBS"] + +if arch == 'larch64': + base_libs.append('EGL') + +maps = arch in ['larch64', 'aarch64', 'x86_64'] + +if maps and arch != 'larch64': + rpath = [Dir(f"#third_party/mapbox-gl-native-qt/{arch}").srcnode().abspath] + qt_env["RPATH"] += rpath + +if arch == "Darwin": + del base_libs[base_libs.index('OpenCL')] + qt_env['FRAMEWORKS'] += ['OpenCL'] + +qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"], LIBS=base_libs) +widgets_src = ["ui.cc", "qt/widgets/input.cc", "qt/widgets/drive_stats.cc", "qt/widgets/wifi.cc", + "qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc", + "qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc", + "qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc", + "qt/request_repeater.cc", "qt/qt_window.cc", "qt/network/networking.cc", "qt/network/wifi_manager.cc", + "../frogpilot/ui/frogpilot_functions.cc", "../frogpilot/navigation/ui/navigation_settings.cc", + "../frogpilot/ui/control_settings.cc", "../frogpilot/ui/vehicle_settings.cc", + "../frogpilot/ui/visual_settings.cc", + "../oscarpilot/settings/settings.cc", "../oscarpilot/settings/basic.cc", + ] + +qt_env['CPPDEFINES'] = [] +if maps: + base_libs += ['qmapboxgl'] + widgets_src += ["qt/maps/map_helpers.cc", "qt/maps/map_settings.cc", "qt/maps/map.cc", "qt/maps/map_panel.cc", + "qt/maps/map_eta.cc", "qt/maps/map_instructions.cc"] + qt_env['CPPDEFINES'] += ["ENABLE_MAPS"] + +widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs) +Export('widgets') +qt_libs = [widgets, qt_util] + base_libs + +qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc", + "qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", + "qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc", + "qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", + "../frogpilot/screenrecorder/omx_encoder.cc", "../frogpilot/screenrecorder/screenrecorder.cc"] + +def is_running_on_wsl2(): + try: + with open('/proc/version', 'r') as f: + return 'WSL2' in f.read() + except FileNotFoundError: + return False + +if is_running_on_wsl2(): + qt_env.Append(CXXFLAGS=['-DWSL2']) + base_libs.remove('OmxCore') + qt_libs.remove('OmxCore') + qt_src.remove("../frogpilot/screenrecorder/screenrecorder.cc") + qt_src.remove("../frogpilot/screenrecorder/omx_encoder.cc") + print("Building for WSL2. Removing Screen Recorder") + +# build translation files +with open(File("translations/languages.json").abspath) as f: + languages = json.loads(f.read()) +translation_sources = [f"#selfdrive/ui/translations/{l}.ts" for l in languages.values()] +translation_targets = [src.replace(".ts", ".qm") for src in translation_sources] +lrelease_bin = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease' + +lupdate = qt_env.Command(translation_sources, qt_src + widgets_src, "selfdrive/ui/update_translations.py") +lrelease = qt_env.Command(translation_targets, translation_sources, f"{lrelease_bin} $SOURCES") +qt_env.Depends(lrelease, lupdate) +qt_env.NoClean(translation_sources) +qt_env.Precious(translation_sources) +qt_env.NoCache(lupdate) + +# create qrc file for compiled translations to include with assets +translations_assets_src = "#selfdrive/assets/translations_assets.qrc" +with open(File(translations_assets_src).abspath, 'w') as f: + f.write('\n\n') + f.write('\n'.join([f'../ui/translations/{l}.qm' for l in languages.values()])) + f.write('\n\n') + +# build assets +assets = "#selfdrive/assets/assets.cc" +assets_src = "#selfdrive/assets/assets.qrc" +qt_env.Command(assets, [assets_src, translations_assets_src], f"rcc $SOURCES -o $TARGET") +qt_env.Depends(assets, Glob('#selfdrive/assets/*', exclude=[assets, assets_src, translations_assets_src, "#selfdrive/assets/assets.o"]) + [lrelease]) +asset_obj = qt_env.Object("assets", assets) + +qt_env.SharedLibrary("qt/python_helpers", ["qt/qt_window.cc"], LIBS=qt_libs) + +# spinner and text window +qt_env.Program("qt/text", ["qt/text.cc"], LIBS=qt_libs) +qt_env.Program("qt/spinner", ["qt/spinner.cc"], LIBS=qt_libs) + +# build main UI +qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_libs) +if GetOption('extras'): + qt_src.remove("main.cc") # replaced by test_runner + qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) + qt_env.Program('tests/ui_snapshot', [asset_obj, "tests/ui_snapshot.cc"] + qt_src, LIBS=qt_libs) + +qt_env['CPPPATH'] += ["../frogpilot/screenrecorder/openmax/include/"] + +if GetOption('extras') and arch != "Darwin": + # setup and factory resetter + qt_env.Program("qt/setup/reset", ["qt/setup/reset.cc"], LIBS=qt_libs) + qt_env.Program("qt/setup/setup", ["qt/setup/setup.cc", asset_obj], + LIBS=qt_libs + ['curl', 'common', 'json11']) + + # build updater UI + qt_env.Program("qt/setup/updater", ["qt/setup/updater.cc", asset_obj], LIBS=qt_libs) + + # build installers + senv = qt_env.Clone() + senv['LINKFLAGS'].append('-Wl,-strip-debug') + + release = "release3" + dashcam = "dashcam3" + installers = [ + ("openpilot", release), + ("openpilot_test", f"{release}-staging"), + ("openpilot_nightly", "nightly"), + ("openpilot_internal", "master"), + ("dashcam", dashcam), + ("dashcam_test", f"{dashcam}-staging"), + ] + + cont = {} + for brand in ("openpilot", "dashcam"): + cont[brand] = senv.Command(f"installer/continue_{brand}.o", f"installer/continue_{brand}.sh", + "ld -r -b binary -o $TARGET $SOURCE") + for name, branch in installers: + brand = "dashcam" if "dashcam" in branch else "openpilot" + d = {'BRANCH': f"'\"{branch}\"'", 'BRAND': f"'\"{brand}\"'"} + if "internal" in name: + d['INTERNAL'] = "1" + + import requests + r = requests.get("https://github.com/commaci2.keys") + r.raise_for_status() + d['SSH_KEYS'] = f'\\"{r.text.strip()}\\"' + obj = senv.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d) + f = senv.Program(f"installer/installers/installer_{name}", [obj, cont[brand]], LIBS=qt_libs) + # keep installers small + assert f[0].get_size() < 350*1e3 + +# build watch3 +if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'): + qt_env.Program("watch3", ["watch3.cc"], LIBS=qt_libs + ['common', 'json11', 'zmq', 'visionipc', 'messaging']) diff --git a/selfdrive/ui/_spinner b/selfdrive/ui/_spinner deleted file mode 100755 index 0e10c27..0000000 Binary files a/selfdrive/ui/_spinner and /dev/null differ diff --git a/selfdrive/ui/_text b/selfdrive/ui/_text deleted file mode 100755 index cc7647e..0000000 Binary files a/selfdrive/ui/_text and /dev/null differ diff --git a/selfdrive/ui/installer/installer.cc b/selfdrive/ui/installer/installer.cc new file mode 100755 index 0000000..179ce60 --- /dev/null +++ b/selfdrive/ui/installer/installer.cc @@ -0,0 +1,221 @@ +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "selfdrive/ui/installer/installer.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/qt_window.h" + +std::string get_str(std::string const s) { + std::string::size_type pos = s.find('?'); + assert(pos != std::string::npos); + return s.substr(0, pos); +} + +// Leave some extra space for the fork installer +const std::string GIT_URL = get_str("https://github.com/commaai/openpilot.git" "? "); +const std::string BRANCH_STR = get_str(BRANCH "? "); + +#define GIT_SSH_URL "git@github.com:commaai/openpilot.git" +#define CONTINUE_PATH "/data/continue.sh" + +const QString CACHE_PATH = "/data/openpilot.cache"; + +#define INSTALL_PATH "/data/openpilot" +#define TMP_INSTALL_PATH "/data/tmppilot" + +extern const uint8_t str_continue[] asm("_binary_selfdrive_ui_installer_continue_" BRAND "_sh_start"); +extern const uint8_t str_continue_end[] asm("_binary_selfdrive_ui_installer_continue_" BRAND "_sh_end"); + +bool time_valid() { + time_t rawtime; + time(&rawtime); + + struct tm * sys_time = gmtime(&rawtime); + return (1900 + sys_time->tm_year) >= 2020; +} + +void run(const char* cmd) { + int err = std::system(cmd); + assert(err == 0); +} + +Installer::Installer(QWidget *parent) : QWidget(parent) { + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setContentsMargins(150, 290, 150, 150); + layout->setSpacing(0); + + QLabel *title = new QLabel(tr("Installing...")); + title->setStyleSheet("font-size: 90px; font-weight: 600;"); + layout->addWidget(title, 0, Qt::AlignTop); + + layout->addSpacing(170); + + bar = new QProgressBar(); + bar->setRange(0, 100); + bar->setTextVisible(false); + bar->setFixedHeight(72); + layout->addWidget(bar, 0, Qt::AlignTop); + + layout->addSpacing(30); + + val = new QLabel("0%"); + val->setStyleSheet("font-size: 70px; font-weight: 300;"); + layout->addWidget(val, 0, Qt::AlignTop); + + layout->addStretch(); + + QObject::connect(&proc, QOverload::of(&QProcess::finished), this, &Installer::cloneFinished); + QObject::connect(&proc, &QProcess::readyReadStandardError, this, &Installer::readProgress); + + QTimer::singleShot(100, this, &Installer::doInstall); + + setStyleSheet(R"( + * { + font-family: Inter; + color: white; + background-color: black; + } + QProgressBar { + border: none; + background-color: #292929; + } + QProgressBar::chunk { + background-color: #364DEF; + } + )"); +} + +void Installer::updateProgress(int percent) { + bar->setValue(percent); + val->setText(QString("%1%").arg(percent)); + update(); +} + +void Installer::doInstall() { + // wait for valid time + while (!time_valid()) { + usleep(500 * 1000); + qDebug() << "Waiting for valid time"; + } + + // cleanup previous install attempts + run("rm -rf " TMP_INSTALL_PATH " " INSTALL_PATH); + + // do the install + if (QDir(CACHE_PATH).exists()) { + cachedFetch(CACHE_PATH); + } else { + freshClone(); + } +} + +void Installer::freshClone() { + qDebug() << "Doing fresh clone"; + proc.start("git", {"clone", "--progress", GIT_URL.c_str(), "-b", BRANCH_STR.c_str(), + "--depth=1", "--recurse-submodules", TMP_INSTALL_PATH}); +} + +void Installer::cachedFetch(const QString &cache) { + qDebug() << "Fetching with cache: " << cache; + + run(QString("cp -rp %1 %2").arg(cache, TMP_INSTALL_PATH).toStdString().c_str()); + int err = chdir(TMP_INSTALL_PATH); + assert(err == 0); + run(("git remote set-branches --add origin " + BRANCH_STR).c_str()); + + updateProgress(10); + + proc.setWorkingDirectory(TMP_INSTALL_PATH); + proc.start("git", {"fetch", "--progress", "origin", BRANCH_STR.c_str()}); +} + +void Installer::readProgress() { + const QVector> stages = { + // prefix, weight in percentage + {"Receiving objects: ", 91}, + {"Resolving deltas: ", 2}, + {"Updating files: ", 7}, + }; + + auto line = QString(proc.readAllStandardError()); + + int base = 0; + for (const QPair kv : stages) { + if (line.startsWith(kv.first)) { + auto perc = line.split(kv.first)[1].split("%")[0]; + int p = base + int(perc.toFloat() / 100. * kv.second); + updateProgress(p); + break; + } + base += kv.second; + } +} + +void Installer::cloneFinished(int exitCode, QProcess::ExitStatus exitStatus) { + qDebug() << "git finished with " << exitCode; + assert(exitCode == 0); + + updateProgress(100); + + // ensure correct branch is checked out + int err = chdir(TMP_INSTALL_PATH); + assert(err == 0); + run(("git checkout " + BRANCH_STR).c_str()); + run(("git reset --hard origin/" + BRANCH_STR).c_str()); + run("git submodule update --init"); + + // move into place + run("mv " TMP_INSTALL_PATH " " INSTALL_PATH); + +#ifdef INTERNAL + run("mkdir -p /data/params/d/"); + + std::map params = { + {"SshEnabled", "1"}, + {"RecordFrontLock", "1"}, + {"GithubSshKeys", SSH_KEYS}, + }; + for (const auto& [key, value] : params) { + std::ofstream param; + param.open("/data/params/d/" + key); + param << value; + param.close(); + } + run("cd " INSTALL_PATH " && " + "git remote set-url origin --push " GIT_SSH_URL " && " + "git config --replace-all remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\""); +#endif + + // write continue.sh + FILE *of = fopen("/data/continue.sh.new", "wb"); + assert(of != NULL); + + size_t num = str_continue_end - str_continue; + size_t num_written = fwrite(str_continue, 1, num, of); + assert(num == num_written); + fclose(of); + + run("chmod +x /data/continue.sh.new"); + run("mv /data/continue.sh.new " CONTINUE_PATH); + + // wait for the installed software's UI to take over + QTimer::singleShot(60 * 1000, &QCoreApplication::quit); +} + +int main(int argc, char *argv[]) { + initApp(argc, argv); + QApplication a(argc, argv); + Installer installer; + setMainWindow(&installer); + return a.exec(); +} diff --git a/selfdrive/ui/installer/installer.h b/selfdrive/ui/installer/installer.h new file mode 100755 index 0000000..de3af0f --- /dev/null +++ b/selfdrive/ui/installer/installer.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +class Installer : public QWidget { + Q_OBJECT + +public: + explicit Installer(QWidget *parent = 0); + +private slots: + void updateProgress(int percent); + + void readProgress(); + void cloneFinished(int exitCode, QProcess::ExitStatus exitStatus); + +private: + QLabel *val; + QProgressBar *bar; + QProcess proc; + + void doInstall(); + void freshClone(); + void cachedFetch(const QString &cache); +}; diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc new file mode 100755 index 0000000..4903a3d --- /dev/null +++ b/selfdrive/ui/main.cc @@ -0,0 +1,30 @@ +#include + +#include +#include + +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/window.h" + +int main(int argc, char *argv[]) { + setpriority(PRIO_PROCESS, 0, -20); + + qInstallMessageHandler(swagLogMessageHandler); + initApp(argc, argv); + + QTranslator translator; + QString translation_file = QString::fromStdString(Params().get("LanguageSetting")); + if (!translator.load(QString(":/%1").arg(translation_file)) && translation_file.length()) { + qCritical() << "Failed to load translation file:" << translation_file; + } + + QApplication a(argc, argv); + a.installTranslator(&translator); + + MainWindow w; + setMainWindow(&w); + a.installEventFilter(&w); + return a.exec(); +} diff --git a/selfdrive/ui/qt/api.cc b/selfdrive/ui/qt/api.cc new file mode 100755 index 0000000..0e321d4 --- /dev/null +++ b/selfdrive/ui/qt/api.cc @@ -0,0 +1,142 @@ +#include "selfdrive/ui/qt/api.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "common/params.h" +#include "common/util.h" +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/util.h" + +namespace CommaApi { + +QByteArray rsa_sign(const QByteArray &data) { + static std::string key = util::read_file(Path::rsa_file()); + if (key.empty()) { + qDebug() << "No RSA private key found, please run manager.py or registration.py"; + return {}; + } + + BIO* mem = BIO_new_mem_buf(key.data(), key.size()); + assert(mem); + RSA* rsa_private = PEM_read_bio_RSAPrivateKey(mem, NULL, NULL, NULL); + assert(rsa_private); + auto sig = QByteArray(); + sig.resize(RSA_size(rsa_private)); + unsigned int sig_len; + int ret = RSA_sign(NID_sha256, (unsigned char*)data.data(), data.size(), (unsigned char*)sig.data(), &sig_len, rsa_private); + assert(ret == 1); + assert(sig_len == sig.size()); + BIO_free(mem); + RSA_free(rsa_private); + return sig; +} + +QString create_jwt(const QJsonObject &payloads, int expiry) { + QJsonObject header = {{"alg", "RS256"}}; + + auto t = QDateTime::currentSecsSinceEpoch(); + QJsonObject payload = {{"identity", getDongleId().value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}}; + for (auto it = payloads.begin(); it != payloads.end(); ++it) { + payload.insert(it.key(), it.value()); + } + + auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals; + QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' + + QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts); + + auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256); + auto sig = rsa_sign(hash); + jwt += '.' + sig.toBase64(b64_opts); + return jwt; +} + +} // namespace CommaApi + +HttpRequest::HttpRequest(QObject *parent, bool create_jwt, int timeout) : create_jwt(create_jwt), QObject(parent) { + networkTimer = new QTimer(this); + networkTimer->setSingleShot(true); + networkTimer->setInterval(timeout); + connect(networkTimer, &QTimer::timeout, this, &HttpRequest::requestTimeout); +} + +bool HttpRequest::active() const { + return reply != nullptr; +} + +bool HttpRequest::timeout() const { + return reply && reply->error() == QNetworkReply::OperationCanceledError; +} + +void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) { + if (active()) { + qDebug() << "HttpRequest is active"; + return; + } + QString token; + if (create_jwt) { + token = CommaApi::create_jwt(); + } else { + QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json")); + QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8()); + token = json_d["access_token"].toString(); + } + + QNetworkRequest request; + request.setUrl(QUrl(requestURL)); + request.setRawHeader("User-Agent", getUserAgent().toUtf8()); + + if (!token.isEmpty()) { + request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8()); + } + + if (method == HttpRequest::Method::GET) { + reply = nam()->get(request); + } else if (method == HttpRequest::Method::DELETE) { + reply = nam()->deleteResource(request); + } + + networkTimer->start(); + connect(reply, &QNetworkReply::finished, this, &HttpRequest::requestFinished); +} + +void HttpRequest::requestTimeout() { + reply->abort(); +} + +void HttpRequest::requestFinished() { + networkTimer->stop(); + + if (reply->error() == QNetworkReply::NoError) { + emit requestDone(reply->readAll(), true, reply->error()); + } else { + QString error; + if (reply->error() == QNetworkReply::OperationCanceledError) { + nam()->clearAccessCache(); + nam()->clearConnectionCache(); + error = "Request timed out"; + } else { + error = reply->errorString(); + } + emit requestDone(error, false, reply->error()); + } + + reply->deleteLater(); + reply = nullptr; +} + +QNetworkAccessManager *HttpRequest::nam() { + static QNetworkAccessManager *networkAccessManager = new QNetworkAccessManager(qApp); + return networkAccessManager; +} diff --git a/selfdrive/ui/qt/api.h b/selfdrive/ui/qt/api.h new file mode 100755 index 0000000..ad64d7e --- /dev/null +++ b/selfdrive/ui/qt/api.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include "common/util.h" + +namespace CommaApi { + +const QString BASE_URL = util::getenv("API_HOST", "https://api.commadotai.com").c_str(); +QByteArray rsa_sign(const QByteArray &data); +QString create_jwt(const QJsonObject &payloads = {}, int expiry = 3600); + +} // namespace CommaApi + +/** + * Makes a request to the request endpoint. + */ + +class HttpRequest : public QObject { + Q_OBJECT + +public: + enum class Method {GET, DELETE}; + + explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000); + void sendRequest(const QString &requestURL, const Method method = Method::GET); + bool active() const; + bool timeout() const; + +signals: + void requestDone(const QString &response, bool success, QNetworkReply::NetworkError error); + +protected: + QNetworkReply *reply = nullptr; + +private: + static QNetworkAccessManager *nam(); + QTimer *networkTimer = nullptr; + bool create_jwt; + +private slots: + void requestTimeout(); + void requestFinished(); +}; diff --git a/selfdrive/ui/qt/body.cc b/selfdrive/ui/qt/body.cc new file mode 100755 index 0000000..563acc0 --- /dev/null +++ b/selfdrive/ui/qt/body.cc @@ -0,0 +1,56 @@ +#include "selfdrive/ui/qt/body.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "common/params.h" +#include "common/timing.h" + +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/util.h" + +BodyWindow::BodyWindow(QWidget *parent) : QWidget(parent) { + QGridLayout *layout = new QGridLayout(this); + layout->setSpacing(0); + layout->setMargin(200); + + setAttribute(Qt::WA_OpaquePaintEvent); + + setStyleSheet(R"( + BodyWindow { + background-color: blue; + } + )"); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &BodyWindow::updateState); +} + +void BodyWindow::paintEvent(QPaintEvent *event) { + QPainter painter(this); + + QPixmap comma_img = loadPixmap("../assets/oscarpilot_ready.png"); + + // Calculate the top-left position to center the image in the window. + int x = (this->width() - comma_img.width()) / 2; + int y = (this->height() - comma_img.height()) / 2; + + // Draw the pixmap at the calculated position. + painter.drawPixmap(x, y, comma_img); +} + + +void BodyWindow::updateState(const UIState &s) { +} + +void BodyWindow::offroadTransition(bool offroad) { +} diff --git a/selfdrive/ui/qt/body.good b/selfdrive/ui/qt/body.good new file mode 100644 index 0000000..5ac284e --- /dev/null +++ b/selfdrive/ui/qt/body.good @@ -0,0 +1,60 @@ +#include "selfdrive/ui/qt/body.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "common/params.h" +#include "common/timing.h" + +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/util.h" + +void LogoWidget::paintEvent(QPaintEvent *event) { + QPainter painter(this); +} + +BodyWindow::BodyWindow(QWidget *parent) : QWidget(parent) { + QGridLayout *layout = new QGridLayout(this); + layout->setSpacing(0); + layout->setMargin(200); + + setAttribute(Qt::WA_OpaquePaintEvent); + + setStyleSheet(R"( + BodyWindow { + background-color: blue; + } + )"); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &BodyWindow::updateState); +} + +void BodyWindow::paintEvent(QPaintEvent *event) { + QPainter painter(this); + + QPixmap comma_img = loadPixmap("../assets/oscarpilot_ready.png"); + + // Calculate the top-left position to center the image in the window. + int x = (this->width() - comma_img.width()) / 2; + int y = (this->height() - comma_img.height()) / 2; + + // Draw the pixmap at the calculated position. + painter.drawPixmap(x, y, comma_img); +} + + +void BodyWindow::updateState(const UIState &s) { +} + +void BodyWindow::offroadTransition(bool offroad) { +} diff --git a/selfdrive/ui/qt/body.h b/selfdrive/ui/qt/body.h new file mode 100755 index 0000000..922a50e --- /dev/null +++ b/selfdrive/ui/qt/body.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/util.h" +#include "selfdrive/ui/ui.h" + +class BodyWindow : public QWidget { + Q_OBJECT +public: + BodyWindow(QWidget* parent = 0); +private: + void paintEvent(QPaintEvent*) override; +private slots: + void updateState(const UIState &s); + void offroadTransition(bool onroad); +}; diff --git a/selfdrive/ui/qt/body.h.org b/selfdrive/ui/qt/body.h.org new file mode 100644 index 0000000..20df3c1 --- /dev/null +++ b/selfdrive/ui/qt/body.h.org @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include "common/util.h" +#include "selfdrive/ui/ui.h" + +class RecordButton : public QPushButton { + Q_OBJECT + +public: + RecordButton(QWidget* parent = 0); + +private: + void paintEvent(QPaintEvent*) override; +}; + +class BodyWindow : public QWidget { + Q_OBJECT + +public: + BodyWindow(QWidget* parent = 0); + +private: + bool charging = false; + uint64_t last_button = 0; + FirstOrderFilter fuel_filter; + QLabel *face; + QMovie *awake, *sleep; + RecordButton *btn; + void paintEvent(QPaintEvent*) override; + +private slots: + void updateState(const UIState &s); + void offroadTransition(bool onroad); +}; \ No newline at end of file diff --git a/selfdrive/ui/qt/body.org b/selfdrive/ui/qt/body.org new file mode 100644 index 0000000..304ef6e --- /dev/null +++ b/selfdrive/ui/qt/body.org @@ -0,0 +1,161 @@ +#include "selfdrive/ui/qt/body.h" + +#include +#include + +#include +#include + +#include "common/params.h" +#include "common/timing.h" + +RecordButton::RecordButton(QWidget *parent) : QPushButton(parent) { + setCheckable(true); + setChecked(false); + setFixedSize(148, 148); + + QObject::connect(this, &QPushButton::toggled, [=]() { + setEnabled(false); + }); +} + +void RecordButton::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + QPoint center(width() / 2, height() / 2); + + QColor bg(isChecked() ? "#FFFFFF" : "#737373"); + QColor accent(isChecked() ? "#FF0000" : "#FFFFFF"); + if (!isEnabled()) { + bg = QColor("#404040"); + accent = QColor("#FFFFFF"); + } + + if (isDown()) { + accent.setAlphaF(0.7); + } + + p.setPen(Qt::NoPen); + p.setBrush(bg); + p.drawEllipse(center, 74, 74); + + p.setPen(QPen(accent, 6)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(center, 42, 42); + + p.setPen(Qt::NoPen); + p.setBrush(accent); + p.drawEllipse(center, 22, 22); +} + + +BodyWindow::BodyWindow(QWidget *parent) : fuel_filter(1.0, 5., 1. / UI_FREQ), QWidget(parent) { + QStackedLayout *layout = new QStackedLayout(this); + layout->setStackingMode(QStackedLayout::StackAll); + + QWidget *w = new QWidget; + QVBoxLayout *vlayout = new QVBoxLayout(w); + vlayout->setMargin(45); + layout->addWidget(w); + + // face + face = new QLabel(); + face->setAlignment(Qt::AlignCenter); + layout->addWidget(face); + awake = new QMovie("../assets/body/awake.gif", {}, this); + awake->setCacheMode(QMovie::CacheAll); + sleep = new QMovie("../assets/body/sleep.gif", {}, this); + sleep->setCacheMode(QMovie::CacheAll); + + // record button + btn = new RecordButton(this); + vlayout->addWidget(btn, 0, Qt::AlignBottom | Qt::AlignRight); + QObject::connect(btn, &QPushButton::clicked, [=](bool checked) { + btn->setEnabled(false); + Params().putBool("DisableLogging", !checked); + last_button = nanos_since_boot(); + }); + w->raise(); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &BodyWindow::updateState); +} + +void BodyWindow::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + p.fillRect(rect(), QColor(0, 0, 0)); + + // battery outline + detail + p.translate(width() - 136, 16); + const QColor gray = QColor("#737373"); + p.setBrush(Qt::NoBrush); + p.setPen(QPen(gray, 4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); + p.drawRoundedRect(2, 2, 78, 36, 8, 8); + + p.setPen(Qt::NoPen); + p.setBrush(gray); + p.drawRoundedRect(84, 12, 6, 16, 4, 4); + p.drawRect(84, 12, 3, 16); + + // battery level + double fuel = std::clamp(fuel_filter.x(), 0.2f, 1.0f); + const int m = 5; // manual margin since we can't do an inner border + p.setPen(Qt::NoPen); + p.setBrush(fuel > 0.25 ? QColor("#32D74B") : QColor("#FF453A")); + p.drawRoundedRect(2 + m, 2 + m, (78 - 2*m)*fuel, 36 - 2*m, 4, 4); + + // charging status + if (charging) { + p.setPen(Qt::NoPen); + p.setBrush(Qt::white); + const QPolygonF charger({ + QPointF(12.31, 0), + QPointF(12.31, 16.92), + QPointF(18.46, 16.92), + QPointF(6.15, 40), + QPointF(6.15, 23.08), + QPointF(0, 23.08), + }); + p.drawPolygon(charger.translated(98, 0)); + } +} + +void BodyWindow::offroadTransition(bool offroad) { + btn->setChecked(true); + btn->setEnabled(true); + fuel_filter.reset(1.0); +} + +void BodyWindow::updateState(const UIState &s) { + if (!isVisible()) { + return; + } + + const SubMaster &sm = *(s.sm); + auto cs = sm["carState"].getCarState(); + + charging = cs.getCharging(); + fuel_filter.update(cs.getFuelGauge()); + + // TODO: use carState.standstill when that's fixed + const bool standstill = std::abs(cs.getVEgo()) < 0.01; + QMovie *m = standstill ? sleep : awake; + if (m != face->movie()) { + face->setMovie(m); + face->movie()->start(); + } + + // update record button state + if (sm.updated("managerState") && (sm.rcv_time("managerState") - last_button)*1e-9 > 0.5) { + for (auto proc : sm["managerState"].getManagerState().getProcesses()) { + if (proc.getName() == "loggerd") { + btn->setEnabled(true); + btn->setChecked(proc.getRunning()); + } + } + } + + update(); +} diff --git a/selfdrive/ui/qt/body.webbrowser.test b/selfdrive/ui/qt/body.webbrowser.test new file mode 100644 index 0000000..88a8c5d --- /dev/null +++ b/selfdrive/ui/qt/body.webbrowser.test @@ -0,0 +1,52 @@ +#include "selfdrive/ui/qt/body.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include // Include the QWebEngineView header + +#include "common/params.h" +#include "common/timing.h" +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/util.h" + +void LogoWidget::paintEvent(QPaintEvent *event) { + QPainter painter(this); +} + +BodyWindow::BodyWindow(QWidget *parent) : QWidget(parent) { + // Create a QWebEngineView + QWebEngineView *view = new QWebEngineView(this); + view->setUrl(QUrl("http://www.fark.com/")); // Set the URL to fark.com + + // Filler + + QGridLayout *layout = new QGridLayout(this); + layout->setSpacing(0); + layout->setMargin(0); // Set margin to 0 to fill the entire window + layout->addWidget(view, 0, 0); // Add the view to the layout + + setAttribute(Qt::WA_OpaquePaintEvent); + + setStyleSheet(R"( + BodyWindow { + background-color: blue; + } + )"); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &BodyWindow::updateState); +} + +void BodyWindow::updateState(const UIState &s) { +} + +void BodyWindow::offroadTransition(bool offroad) { +} diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc new file mode 100755 index 0000000..574f055 --- /dev/null +++ b/selfdrive/ui/qt/home.cc @@ -0,0 +1,270 @@ +#include "selfdrive/ui/qt/home.h" + +#include +#include +#include +#include + +#include "selfdrive/ui/qt/offroad/experimental_mode.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/drive_stats.h" +#include "selfdrive/ui/qt/widgets/prime.h" + +#ifdef ENABLE_MAPS +#include "selfdrive/ui/qt/maps/map_settings.h" +#endif + +// HomeWindow: the container for the offroad and onroad UIs + +HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { + QHBoxLayout *main_layout = new QHBoxLayout(this); + main_layout->setMargin(0); + main_layout->setSpacing(0); + + sidebar = new Sidebar(this); + main_layout->addWidget(sidebar); + QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings); + + slayout = new QStackedLayout(); + main_layout->addLayout(slayout); + + home = new OffroadHome(this); + QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings); + slayout->addWidget(home); + + onroad = new OnroadWindow(this); + QObject::connect(onroad, &OnroadWindow::mapPanelRequested, this, [=] { sidebar->hide(); }); + slayout->addWidget(onroad); + + body = new BodyWindow(this); + slayout->addWidget(body); + + driver_view = new DriverViewWindow(this); + connect(driver_view, &DriverViewWindow::done, [=] { + showDriverView(false); + }); + slayout->addWidget(driver_view); + setAttribute(Qt::WA_NoSystemBackground); + // QObject::connect(uiState(), &UIState::uiUpdate, this, &HomeWindow::updateState); + QObject::connect(uiState(), &UIState::offroadTransition, this, &HomeWindow::offroadTransition); + QObject::connect(uiState(), &UIState::offroadTransition, sidebar, &Sidebar::offroadTransition); +} + +void HomeWindow::showSidebar(bool show) { + sidebar->setVisible(show); +} + +void HomeWindow::showMapPanel(bool show) { + onroad->showMapPanel(show); +} + +void HomeWindow::updateState(const UIState &s) { + const SubMaster &sm = *(s.sm); + + // switch to the generic robot UI + if (onroad->isVisible() && !body->isEnabled() && sm["carParams"].getCarParams().getNotCar()) { + body->setEnabled(true); + slayout->setCurrentWidget(body); + } +} + +void HomeWindow::offroadTransition(bool offroad) { + body->setEnabled(false); + sidebar->setVisible(false); + if (offroad) { + slayout->setCurrentWidget(body); + } else { + slayout->setCurrentWidget(onroad); + } +} + +void HomeWindow::showDriverView(bool show) { + if (show) { + emit closeSettings(); + slayout->setCurrentWidget(driver_view); + } else { + slayout->setCurrentWidget(body); + } + sidebar->setVisible(false); +} + +void HomeWindow::mousePressEvent(QMouseEvent* e) { + if (body->isVisible()) { + showSidebar(true); + slayout->setCurrentWidget(home); + } else { + // Handle sidebar collapsing + if ((onroad->isVisible() || body->isVisible()) && (!sidebar->isVisible() || e->x() > sidebar->width())) { + sidebar->setVisible(!sidebar->isVisible() && !onroad->isMapVisible()); + } + } +} + +void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { + HomeWindow::mousePressEvent(e); + const SubMaster &sm = *(uiState()->sm); + if (sm["carParams"].getCarParams().getNotCar()) { + if (onroad->isVisible()) { + slayout->setCurrentWidget(body); + } else if (body->isVisible()) { + slayout->setCurrentWidget(onroad); + } + showSidebar(false); + } +} + +// OffroadHome: the offroad home page + +OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(40, 40, 40, 40); + + // top header + QHBoxLayout* header_layout = new QHBoxLayout(); + header_layout->setContentsMargins(0, 0, 0, 0); + header_layout->setSpacing(16); + + update_notif = new QPushButton(tr("UPDATE")); + update_notif->setVisible(false); + update_notif->setStyleSheet("background-color: #364DEF;"); + QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); }); + header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); + + alert_notif = new QPushButton(); + alert_notif->setVisible(false); + alert_notif->setStyleSheet("background-color: #E22C2C;"); + QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); }); + header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); + + date = new ElidedLabel(); + header_layout->addWidget(date, 0, Qt::AlignHCenter | Qt::AlignLeft); + + version = new ElidedLabel(); + header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight); + + main_layout->addLayout(header_layout); + + // main content + main_layout->addSpacing(25); + center_layout = new QStackedLayout(); + + QWidget *home_widget = new QWidget(this); + { + QHBoxLayout *home_layout = new QHBoxLayout(home_widget); + home_layout->setContentsMargins(0, 0, 0, 0); + home_layout->setSpacing(30); + + // left: MapSettings/PrimeAdWidget + QStackedWidget *left_widget = new QStackedWidget(this); +#ifdef ENABLE_MAPS + left_widget->addWidget(new MapSettings); +#else + left_widget->addWidget(new QWidget); +#endif + left_widget->addWidget(new PrimeAdWidget); + left_widget->addWidget(new DriveStats); + left_widget->setStyleSheet("border-radius: 10px;"); + + left_widget->setCurrentIndex(params.getBool("DriveStats") ? 2 : uiState()->hasPrime() ? 0 : 1); + connect(uiState(), &UIState::primeChanged, [=](bool prime) { + left_widget->setCurrentIndex(prime ? 0 : 1); + }); + + home_layout->addWidget(left_widget, 1); + + // right: ExperimentalModeButton, SetupWidget + QWidget* right_widget = new QWidget(this); + QVBoxLayout* right_column = new QVBoxLayout(right_widget); + right_column->setContentsMargins(0, 0, 0, 0); + right_widget->setFixedWidth(750); + right_column->setSpacing(30); + + ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this); + QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings); + right_column->addWidget(experimental_mode, 1); + + SetupWidget *setup_widget = new SetupWidget; + QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings); + right_column->addWidget(setup_widget, 1); + + home_layout->addWidget(right_widget, 1); + } + center_layout->addWidget(home_widget); + + // add update & alerts widgets + update_widget = new UpdateAlert(); + QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); + center_layout->addWidget(update_widget); + alerts_widget = new OffroadAlert(); + QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); }); + center_layout->addWidget(alerts_widget); + + main_layout->addLayout(center_layout, 1); + + // set up refresh timer + timer = new QTimer(this); + timer->callOnTimeout(this, &OffroadHome::refresh); + + setStyleSheet(R"( + * { + color: white; + } + OffroadHome { + background-color: black; + } + OffroadHome > QPushButton { + padding: 15px 30px; + border-radius: 5px; + font-size: 40px; + font-weight: 500; + } + OffroadHome > QLabel { + font-size: 55px; + } + )"); + + // Set the model name + std::map MODEL_NAME { + {0, "New Delhi"}, + {1, "Blue Diamond V1"}, + {2, "Blue Diamond V2"}, + {3, "Farmville"}, + {4, "New Lemon Pie"}, + }; + + modelName = MODEL_NAME[params.getInt("Model")]; +} + +void OffroadHome::showEvent(QShowEvent *event) { + refresh(); + timer->start(10 * 1000); +} + +void OffroadHome::hideEvent(QHideEvent *event) { + timer->stop(); +} + +void OffroadHome::refresh() { + date->setText(QLocale(uiState()->language.mid(5)).toString(QDateTime::currentDateTime(), "dddd, MMMM d")); + version->setText(getBrand() + " v" + getVersion().left(14).trimmed() + " - " + modelName); + + bool updateAvailable = update_widget->refresh(); + int alerts = alerts_widget->refresh(); + + // pop-up new notification + int idx = center_layout->currentIndex(); + if (!updateAvailable && !alerts) { + idx = 0; + } else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) { + idx = 1; + } else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) { + idx = 2; + } + center_layout->setCurrentIndex(idx); + + update_notif->setVisible(updateAvailable); + alert_notif->setVisible(alerts); + if (alerts) { + alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT"))); + } +} diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h new file mode 100755 index 0000000..780b8c2 --- /dev/null +++ b/selfdrive/ui/qt/home.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/offroad/driverview.h" +#include "selfdrive/ui/qt/body.h" +#include "selfdrive/ui/qt/onroad.h" +#include "selfdrive/ui/qt/sidebar.h" +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/offroad_alerts.h" +#include "selfdrive/ui/ui.h" + +class OffroadHome : public QFrame { + Q_OBJECT + +public: + explicit OffroadHome(QWidget* parent = 0); + +signals: + void openSettings(int index = 0, const QString ¶m = ""); + +private: + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + void refresh(); + + Params params; + + QTimer* timer; + ElidedLabel* date; + ElidedLabel* version; + QStackedLayout* center_layout; + UpdateAlert *update_widget; + OffroadAlert* alerts_widget; + QPushButton* alert_notif; + QPushButton* update_notif; + + // FrogPilot variables + QString modelName; +}; + +class HomeWindow : public QWidget { + Q_OBJECT + +public: + explicit HomeWindow(QWidget* parent = 0); + +signals: + void openSettings(int index = 0, const QString ¶m = ""); + void closeSettings(); + +public slots: + void offroadTransition(bool offroad); + void showDriverView(bool show); + void showSidebar(bool show); + void showMapPanel(bool show); + +protected: + void mousePressEvent(QMouseEvent* e) override; + void mouseDoubleClickEvent(QMouseEvent* e) override; + +private: + Sidebar *sidebar; + OffroadHome *home; + OnroadWindow *onroad; + BodyWindow *body; + DriverViewWindow *driver_view; + QStackedLayout *slayout; + +private slots: + void updateState(const UIState &s); +}; diff --git a/selfdrive/ui/qt/libpython_helpers.so b/selfdrive/ui/qt/libpython_helpers.so deleted file mode 100755 index b8d2bef..0000000 Binary files a/selfdrive/ui/qt/libpython_helpers.so and /dev/null differ diff --git a/selfdrive/ui/qt/maps/map.cc b/selfdrive/ui/qt/maps/map.cc new file mode 100755 index 0000000..aff2e4b --- /dev/null +++ b/selfdrive/ui/qt/maps/map.cc @@ -0,0 +1,438 @@ +#include "selfdrive/ui/qt/maps/map.h" + +#include +#include + +#include + +#include "selfdrive/ui/qt/maps/map_helpers.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/ui.h" + + +const int INTERACTION_TIMEOUT = 100; + +const float MAX_ZOOM = 17; +const float MIN_ZOOM = 14; +const float MAX_PITCH = 50; +const float MIN_PITCH = 0; +const float MAP_SCALE = 2; + +MapWindow::MapWindow(const QMapboxGLSettings &settings) : m_settings(settings), velocity_filter(0, 10, 0.05, false) { + QObject::connect(uiState(), &UIState::uiUpdate, this, &MapWindow::updateState); + + map_overlay = new QWidget (this); + map_overlay->setAttribute(Qt::WA_TranslucentBackground, true); + QVBoxLayout *overlay_layout = new QVBoxLayout(map_overlay); + overlay_layout->setContentsMargins(0, 0, 0, 0); + + // Instructions + map_instructions = new MapInstructions(this); + map_instructions->setVisible(false); + + map_eta = new MapETA(this); + map_eta->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + map_eta->setFixedHeight(120); + + error = new QLabel(this); + error->setStyleSheet(R"(color:white;padding:50px 11px;font-size: 90px; background-color:rgba(0, 0, 0, 150);)"); + error->setAlignment(Qt::AlignCenter); + + overlay_layout->addWidget(error); + overlay_layout->addWidget(map_instructions); + overlay_layout->addStretch(1); + overlay_layout->addWidget(map_eta); + + last_position = coordinate_from_param("LastGPSPosition"); + grabGesture(Qt::GestureType::PinchGesture); + qDebug() << "MapWindow initialized"; +} + +MapWindow::~MapWindow() { + makeCurrent(); +} + +void MapWindow::initLayers() { + // This doesn't work from initializeGL + if (!m_map->layerExists("modelPathLayer")) { + qDebug() << "Initializing modelPathLayer"; + QVariantMap modelPath; + modelPath["id"] = "modelPathLayer"; + modelPath["type"] = "line"; + modelPath["source"] = "modelPathSource"; + m_map->addLayer(modelPath); + m_map->setPaintProperty("modelPathLayer", "line-color", QColor("red")); + m_map->setPaintProperty("modelPathLayer", "line-width", 5.0); + m_map->setLayoutProperty("modelPathLayer", "line-cap", "round"); + } + if (!m_map->layerExists("navLayer")) { + qDebug() << "Initializing navLayer"; + QVariantMap nav; + nav["id"] = "navLayer"; + nav["type"] = "line"; + nav["source"] = "navSource"; + m_map->addLayer(nav, "road-intersection"); + + QVariantMap transition; + transition["duration"] = 400; // ms + m_map->setPaintProperty("navLayer", "line-color", getNavPathColor(uiState()->scene.navigate_on_openpilot)); + m_map->setPaintProperty("navLayer", "line-color-transition", transition); + m_map->setPaintProperty("navLayer", "line-width", 7.5); + m_map->setLayoutProperty("navLayer", "line-cap", "round"); + } + if (!m_map->layerExists("pinLayer")) { + qDebug() << "Initializing pinLayer"; + m_map->addImage("default_marker", QImage("../assets/navigation/default_marker.svg")); + QVariantMap pin; + pin["id"] = "pinLayer"; + pin["type"] = "symbol"; + pin["source"] = "pinSource"; + m_map->addLayer(pin); + m_map->setLayoutProperty("pinLayer", "icon-pitch-alignment", "viewport"); + m_map->setLayoutProperty("pinLayer", "icon-image", "default_marker"); + m_map->setLayoutProperty("pinLayer", "icon-ignore-placement", true); + m_map->setLayoutProperty("pinLayer", "icon-allow-overlap", true); + m_map->setLayoutProperty("pinLayer", "symbol-sort-key", 0); + m_map->setLayoutProperty("pinLayer", "icon-anchor", "bottom"); + } + if (!m_map->layerExists("carPosLayer")) { + qDebug() << "Initializing carPosLayer"; + m_map->addImage("label-arrow", QImage("../assets/images/triangle.svg")); + + QVariantMap carPos; + carPos["id"] = "carPosLayer"; + carPos["type"] = "symbol"; + carPos["source"] = "carPosSource"; + m_map->addLayer(carPos); + m_map->setLayoutProperty("carPosLayer", "icon-pitch-alignment", "map"); + m_map->setLayoutProperty("carPosLayer", "icon-image", "label-arrow"); + m_map->setLayoutProperty("carPosLayer", "icon-size", 0.5); + m_map->setLayoutProperty("carPosLayer", "icon-ignore-placement", true); + m_map->setLayoutProperty("carPosLayer", "icon-allow-overlap", true); + // TODO: remove, symbol-sort-key does not seem to matter outside of each layer + m_map->setLayoutProperty("carPosLayer", "symbol-sort-key", 0); + } + + if (!m_map->layerExists("buildingsLayer")) { + qDebug() << "Initializing buildingsLayer"; + QVariantMap buildings; + buildings["id"] = "buildingsLayer"; + buildings["source"] = "composite"; + buildings["source-layer"] = "building"; + buildings["type"] = "fill-extrusion"; + buildings["minzoom"] = 15; + m_map->addLayer(buildings); + m_map->setFilter("buildingsLayer", QVariantList({"==", "extrude", "true"})); + + QVariantList fillExtrusionHight = { + "interpolate", + QVariantList{"linear"}, + QVariantList{"zoom"}, + 15, 0, + 15.05, QVariantList{"get", "height"} + }; + + QVariantList fillExtrusionBase = { + "interpolate", + QVariantList{"linear"}, + QVariantList{"zoom"}, + 15, 0, + 15.05, QVariantList{"get", "min_height"} + }; + + QVariantList fillExtrusionOpacity = { + "interpolate", + QVariantList{"linear"}, + QVariantList{"zoom"}, + 15, 0, + 15.5, .6, + 17, .6, + 20, 0 + }; + + m_map->setPaintProperty("buildingsLayer", "fill-extrusion-color", QColor("grey")); + m_map->setPaintProperty("buildingsLayer", "fill-extrusion-opacity", fillExtrusionOpacity); + m_map->setPaintProperty("buildingsLayer", "fill-extrusion-height", fillExtrusionHight); + m_map->setPaintProperty("buildingsLayer", "fill-extrusion-base", fillExtrusionBase); + m_map->setLayoutProperty("buildingsLayer", "visibility", "visible"); + } +} + +void MapWindow::updateState(const UIState &s) { + if (!uiState()->scene.started) { + return; + } + const SubMaster &sm = *(s.sm); + update(); + + if (sm.updated("modelV2")) { + // set path color on change, and show map on rising edge of navigate on openpilot + bool nav_enabled = sm["modelV2"].getModelV2().getNavEnabled() && + (sm["controlsState"].getControlsState().getEnabled() || sm["frogpilotCarControl"].getFrogpilotCarControl().getAlwaysOnLateral()) && + (!params.get("NavDestination").empty() || params.getInt("PrimeType") != 0); + if (nav_enabled != uiState()->scene.navigate_on_openpilot) { + if (loaded_once) { + m_map->setPaintProperty("navLayer", "line-color", getNavPathColor(nav_enabled)); + } + if (nav_enabled) { + emit requestVisible(true); + } + } + uiState()->scene.navigate_on_openpilot = nav_enabled; + } + + if (sm.updated("liveLocationKalman")) { + auto locationd_location = sm["liveLocationKalman"].getLiveLocationKalman(); + auto locationd_pos = locationd_location.getPositionGeodetic(); + auto locationd_orientation = locationd_location.getCalibratedOrientationNED(); + auto locationd_velocity = locationd_location.getVelocityCalibrated(); + + // Check std norm + auto pos_ecef_std = locationd_location.getPositionECEF().getStd(); + bool pos_accurate_enough = sqrt(pow(pos_ecef_std[0], 2) + pow(pos_ecef_std[1], 2) + pow(pos_ecef_std[2], 2)) < 100; + + locationd_valid = (locationd_pos.getValid() && locationd_orientation.getValid() && locationd_velocity.getValid() && pos_accurate_enough); + + if (locationd_valid) { + last_position = QMapbox::Coordinate(locationd_pos.getValue()[0], locationd_pos.getValue()[1]); + last_bearing = RAD2DEG(locationd_orientation.getValue()[2]); + velocity_filter.update(std::max(10.0, locationd_velocity.getValue()[0])); + } + } + + if (sm.updated("navRoute") && sm["navRoute"].getNavRoute().getCoordinates().size()) { + auto nav_dest = coordinate_from_param("NavDestination"); + bool allow_open = std::exchange(last_valid_nav_dest, nav_dest) != nav_dest && + nav_dest && !isVisible(); + qWarning() << "Got new navRoute from navd. Opening map:" << allow_open; + + // Show map on destination set/change + if (allow_open) { + emit requestSettings(false); + emit requestVisible(true); + } + } + + loaded_once = loaded_once || (m_map && m_map->isFullyLoaded()); + if (!loaded_once) { + setError(tr("Map Loading")); + return; + } + initLayers(); + + if (!locationd_valid) { + setError(tr("Waiting for GPS")); + } else if (routing_problem) { + setError(tr("Waiting for route")); + } else { + setError(""); + } + + if (locationd_valid) { + // Update current location marker + auto point = coordinate_to_collection(*last_position); + QMapbox::Feature feature1(QMapbox::Feature::PointType, point, {}, {}); + QVariantMap carPosSource; + carPosSource["type"] = "geojson"; + carPosSource["data"] = QVariant::fromValue(feature1); + m_map->updateSource("carPosSource", carPosSource); + + // Map bearing isn't updated when interacting, keep location marker up to date + if (last_bearing) { + m_map->setLayoutProperty("carPosLayer", "icon-rotate", *last_bearing - m_map->bearing()); + } + } + + if (interaction_counter == 0) { + if (last_position) m_map->setCoordinate(*last_position); + if (last_bearing) m_map->setBearing(*last_bearing); + m_map->setZoom(util::map_val(velocity_filter.x(), 0, 30, MAX_ZOOM, MIN_ZOOM)); + } else { + interaction_counter--; + } + + if (sm.updated("navInstruction")) { + // an invalid navInstruction packet with a nav destination is only possible if: + // - API exception/no internet + // - route response is empty + // - any time navd is waiting for recompute_countdown + routing_problem = !sm.valid("navInstruction") && coordinate_from_param("NavDestination").has_value(); + + if (sm.valid("navInstruction")) { + auto i = sm["navInstruction"].getNavInstruction(); + map_eta->updateETA(i.getTimeRemaining(), i.getTimeRemainingTypical(), i.getDistanceRemaining()); + + if (locationd_valid) { + m_map->setPitch(MAX_PITCH); // TODO: smooth pitching based on maneuver distance + map_instructions->updateInstructions(i); + } + } else { + clearRoute(); + } + } + + if (sm.rcv_frame("navRoute") != route_rcv_frame) { + qWarning() << "Updating navLayer with new route"; + auto route = sm["navRoute"].getNavRoute(); + auto route_points = capnp_coordinate_list_to_collection(route.getCoordinates()); + QMapbox::Feature feature(QMapbox::Feature::LineStringType, route_points, {}, {}); + QVariantMap navSource; + navSource["type"] = "geojson"; + navSource["data"] = QVariant::fromValue(feature); + m_map->updateSource("navSource", navSource); + m_map->setLayoutProperty("navLayer", "visibility", "visible"); + + route_rcv_frame = sm.rcv_frame("navRoute"); + updateDestinationMarker(); + } +} + +void MapWindow::setError(const QString &err_str) { + if (err_str != error->text()) { + error->setText(err_str); + error->setVisible(!err_str.isEmpty()); + if (!err_str.isEmpty()) map_instructions->setVisible(false); + } +} + +void MapWindow::resizeGL(int w, int h) { + m_map->resize(size() / MAP_SCALE); + map_overlay->setFixedSize(width(), height()); +} + +void MapWindow::initializeGL() { + m_map.reset(new QMapboxGL(this, m_settings, size(), 1)); + + if (last_position) { + m_map->setCoordinateZoom(*last_position, MAX_ZOOM); + } else { + m_map->setCoordinateZoom(QMapbox::Coordinate(64.31990695292795, -149.79038934046247), MIN_ZOOM); + } + + m_map->setMargins({0, 350, 0, 50}); + m_map->setPitch(MIN_PITCH); + m_map->setStyleUrl("mapbox://styles/commaai/clkqztk0f00ou01qyhsa5bzpj"); + + QObject::connect(m_map.data(), &QMapboxGL::mapChanged, [=](QMapboxGL::MapChange change) { + // set global animation duration to 0 ms so visibility changes are instant + if (change == QMapboxGL::MapChange::MapChangeDidFinishLoadingStyle) { + m_map->setTransitionOptions(0, 0); + } + if (change == QMapboxGL::MapChange::MapChangeDidFinishLoadingMap) { + loaded_once = true; + } + }); +} + +void MapWindow::paintGL() { + if (!isVisible() || m_map.isNull()) return; + m_map->render(); +} + +void MapWindow::clearRoute() { + if (!m_map.isNull()) { + m_map->setLayoutProperty("navLayer", "visibility", "none"); + m_map->setPitch(MIN_PITCH); + updateDestinationMarker(); + } + + map_instructions->setVisible(false); + map_eta->setVisible(false); + last_valid_nav_dest = std::nullopt; +} + +void MapWindow::mousePressEvent(QMouseEvent *ev) { + m_lastPos = ev->localPos(); + ev->accept(); +} + +void MapWindow::mouseDoubleClickEvent(QMouseEvent *ev) { + if (last_position) m_map->setCoordinate(*last_position); + if (last_bearing) m_map->setBearing(*last_bearing); + m_map->setZoom(util::map_val(velocity_filter.x(), 0, 30, MAX_ZOOM, MIN_ZOOM)); + update(); + + interaction_counter = 0; +} + +void MapWindow::mouseMoveEvent(QMouseEvent *ev) { + QPointF delta = ev->localPos() - m_lastPos; + + if (!delta.isNull()) { + interaction_counter = INTERACTION_TIMEOUT; + m_map->moveBy(delta / MAP_SCALE); + update(); + } + + m_lastPos = ev->localPos(); + ev->accept(); +} + +void MapWindow::wheelEvent(QWheelEvent *ev) { + if (ev->orientation() == Qt::Horizontal) { + return; + } + + float factor = ev->delta() / 1200.; + if (ev->delta() < 0) { + factor = factor > -1 ? factor : 1 / factor; + } + + m_map->scaleBy(1 + factor, ev->pos() / MAP_SCALE); + update(); + + interaction_counter = INTERACTION_TIMEOUT; + ev->accept(); +} + +bool MapWindow::event(QEvent *event) { + if (event->type() == QEvent::Gesture) { + return gestureEvent(static_cast(event)); + } + + return QWidget::event(event); +} + +bool MapWindow::gestureEvent(QGestureEvent *event) { + if (QGesture *pinch = event->gesture(Qt::PinchGesture)) { + pinchTriggered(static_cast(pinch)); + } + return true; +} + +void MapWindow::pinchTriggered(QPinchGesture *gesture) { + QPinchGesture::ChangeFlags changeFlags = gesture->changeFlags(); + if (changeFlags & QPinchGesture::ScaleFactorChanged) { + // TODO: figure out why gesture centerPoint doesn't work + m_map->scaleBy(gesture->scaleFactor(), {width() / 2.0 / MAP_SCALE, height() / 2.0 / MAP_SCALE}); + update(); + interaction_counter = INTERACTION_TIMEOUT; + } +} + +void MapWindow::offroadTransition(bool offroad) { + if (offroad) { + clearRoute(); + uiState()->scene.navigate_on_openpilot = false; + routing_problem = false; + } else { + auto dest = coordinate_from_param("NavDestination"); + emit requestVisible(dest.has_value()); + } + last_bearing = {}; +} + +void MapWindow::updateDestinationMarker() { + auto nav_dest = coordinate_from_param("NavDestination"); + if (nav_dest.has_value()) { + auto point = coordinate_to_collection(*nav_dest); + QMapbox::Feature feature(QMapbox::Feature::PointType, point, {}, {}); + QVariantMap pinSource; + pinSource["type"] = "geojson"; + pinSource["data"] = QVariant::fromValue(feature); + m_map->updateSource("pinSource", pinSource); + m_map->setPaintProperty("pinLayer", "visibility", "visible"); + } else { + m_map->setPaintProperty("pinLayer", "visibility", "none"); + } +} diff --git a/selfdrive/ui/qt/maps/map.h b/selfdrive/ui/qt/maps/map.h new file mode 100755 index 0000000..243ab96 --- /dev/null +++ b/selfdrive/ui/qt/maps/map.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cereal/messaging/messaging.h" +#include "common/params.h" +#include "common/util.h" +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/maps/map_eta.h" +#include "selfdrive/ui/qt/maps/map_instructions.h" + +class MapWindow : public QOpenGLWidget { + Q_OBJECT + +public: + MapWindow(const QMapboxGLSettings &); + ~MapWindow(); + +private: + void initializeGL() final; + void paintGL() final; + void resizeGL(int w, int h) override; + + QMapboxGLSettings m_settings; + QScopedPointer m_map; + + void initLayers(); + + void mousePressEvent(QMouseEvent *ev) final; + void mouseDoubleClickEvent(QMouseEvent *ev) final; + void mouseMoveEvent(QMouseEvent *ev) final; + void wheelEvent(QWheelEvent *ev) final; + bool event(QEvent *event) final; + bool gestureEvent(QGestureEvent *event); + void pinchTriggered(QPinchGesture *gesture); + void setError(const QString &err_str); + + bool loaded_once = false; + + // Panning + QPointF m_lastPos; + int interaction_counter = 0; + + // Position + std::optional last_valid_nav_dest; + std::optional last_position; + std::optional last_bearing; + FirstOrderFilter velocity_filter; + bool locationd_valid = false; + bool routing_problem = false; + + QWidget *map_overlay; + QLabel *error; + MapInstructions* map_instructions; + MapETA* map_eta; + + // Blue with normal nav, green when nav is input into the model + QColor getNavPathColor(bool nav_enabled) { + return nav_enabled ? QColor("#31ee73") : QColor("#31a1ee"); + } + + void clearRoute(); + void updateDestinationMarker(); + uint64_t route_rcv_frame = 0; + + // FrogPilot variables + Params params; + +private slots: + void updateState(const UIState &s); + +public slots: + void offroadTransition(bool offroad); + +signals: + void requestVisible(bool visible); + void requestSettings(bool settings); +}; diff --git a/selfdrive/ui/qt/maps/map_eta.cc b/selfdrive/ui/qt/maps/map_eta.cc new file mode 100755 index 0000000..161844c --- /dev/null +++ b/selfdrive/ui/qt/maps/map_eta.cc @@ -0,0 +1,56 @@ +#include "selfdrive/ui/qt/maps/map_eta.h" + +#include +#include + +#include "selfdrive/ui/qt/maps/map_helpers.h" +#include "selfdrive/ui/ui.h" + +const float MANEUVER_TRANSITION_THRESHOLD = 10; + +MapETA::MapETA(QWidget *parent) : QWidget(parent) { + setVisible(false); + setAttribute(Qt::WA_TranslucentBackground); + eta_doc.setUndoRedoEnabled(false); + eta_doc.setDefaultStyleSheet("body {font-family:Inter;font-size:70px;color:white;} b{font-weight:600;} td{padding:0 3px;}"); +} + +void MapETA::paintEvent(QPaintEvent *event) { + if (!eta_doc.isEmpty()) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 0, 0, 255)); + QSizeF txt_size = eta_doc.size(); + p.drawRoundedRect((width() - txt_size.width()) / 2 - UI_BORDER_SIZE, 0, txt_size.width() + UI_BORDER_SIZE * 2, height() + 25, 25, 25); + p.translate((width() - txt_size.width()) / 2, (height() - txt_size.height()) / 2); + eta_doc.drawContents(&p); + } +} + +void MapETA::updateETA(float s, float s_typical, float d) { + // ETA + auto eta_t = QDateTime::currentDateTime().addSecs(s).time(); + auto eta = format_24h ? std::pair{eta_t.toString("HH:mm"), tr("eta")} + : std::pair{eta_t.toString("h:mm a").split(' ')[0], eta_t.toString("a")}; + + // Remaining time + auto remaining = s < 3600 ? std::pair{QString::number(int(s / 60)), tr("min")} + : std::pair{QString("%1:%2").arg((int)s / 3600).arg(((int)s % 3600) / 60, 2, 10, QLatin1Char('0')), tr("hr")}; + QString color = "#25DA6E"; + if (s / s_typical > 1.5) + color = "#DA3025"; + else if (s / s_typical > 1.2) + color = "#DAA725"; + + // Distance + auto distance = map_format_distance(d, uiState()->scene.is_metric); + + eta_doc.setHtml(QString(R"( + + )") + .arg(eta.first, eta.second, color, remaining.first, remaining.second, distance.first, distance.second)); + + setVisible(d >= MANEUVER_TRANSITION_THRESHOLD); + update(); +} diff --git a/selfdrive/ui/qt/maps/map_eta.h b/selfdrive/ui/qt/maps/map_eta.h new file mode 100755 index 0000000..6e59837 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_eta.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +#include "common/params.h" + +class MapETA : public QWidget { + Q_OBJECT + +public: + MapETA(QWidget * parent=nullptr); + void updateETA(float seconds, float seconds_typical, float distance); + +private: + void paintEvent(QPaintEvent *event) override; + void showEvent(QShowEvent *event) override { format_24h = param.getBool("NavSettingTime24h"); } + + bool format_24h = false; + QTextDocument eta_doc; + Params param; +}; diff --git a/selfdrive/ui/qt/maps/map_helpers.cc b/selfdrive/ui/qt/maps/map_helpers.cc new file mode 100755 index 0000000..022355e --- /dev/null +++ b/selfdrive/ui/qt/maps/map_helpers.cc @@ -0,0 +1,152 @@ +#include "selfdrive/ui/qt/maps/map_helpers.h" + +#include +#include +#include + +#include +#include + +#include "common/params.h" +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/api.h" + +QString get_mapbox_token() { + // Valid for 4 weeks since we can't swap tokens on the fly + return MAPBOX_TOKEN.isEmpty() ? CommaApi::create_jwt({}, 4 * 7 * 24 * 3600) : MAPBOX_TOKEN; +} + +QMapboxGLSettings get_mapbox_settings() { + QMapboxGLSettings settings; + + if (!Hardware::PC()) { + settings.setCacheDatabasePath(MAPS_CACHE_PATH); + settings.setCacheDatabaseMaximumSize(100 * 1024 * 1024); + } + settings.setApiBaseUrl(MAPS_HOST); + settings.setAccessToken(get_mapbox_token()); + + return settings; +} + +QGeoCoordinate to_QGeoCoordinate(const QMapbox::Coordinate &in) { + return QGeoCoordinate(in.first, in.second); +} + +QMapbox::CoordinatesCollections model_to_collection( + const cereal::LiveLocationKalman::Measurement::Reader &calibratedOrientationECEF, + const cereal::LiveLocationKalman::Measurement::Reader &positionECEF, + const cereal::XYZTData::Reader &line){ + + Eigen::Vector3d ecef(positionECEF.getValue()[0], positionECEF.getValue()[1], positionECEF.getValue()[2]); + Eigen::Vector3d orient(calibratedOrientationECEF.getValue()[0], calibratedOrientationECEF.getValue()[1], calibratedOrientationECEF.getValue()[2]); + Eigen::Matrix3d ecef_from_local = euler2rot(orient); + + QMapbox::Coordinates coordinates; + auto x = line.getX(); + auto y = line.getY(); + auto z = line.getZ(); + for (int i = 0; i < x.size(); i++) { + Eigen::Vector3d point_ecef = ecef_from_local * Eigen::Vector3d(x[i], y[i], z[i]) + ecef; + Geodetic point_geodetic = ecef2geodetic((ECEF){.x = point_ecef[0], .y = point_ecef[1], .z = point_ecef[2]}); + coordinates.push_back({point_geodetic.lat, point_geodetic.lon}); + } + + return {QMapbox::CoordinatesCollection{coordinates}}; +} + +QMapbox::CoordinatesCollections coordinate_to_collection(const QMapbox::Coordinate &c) { + QMapbox::Coordinates coordinates{c}; + return {QMapbox::CoordinatesCollection{coordinates}}; +} + +QMapbox::CoordinatesCollections capnp_coordinate_list_to_collection(const capnp::List::Reader& coordinate_list) { + QMapbox::Coordinates coordinates; + for (auto const &c : coordinate_list) { + coordinates.push_back({c.getLatitude(), c.getLongitude()}); + } + return {QMapbox::CoordinatesCollection{coordinates}}; +} + +QMapbox::CoordinatesCollections coordinate_list_to_collection(const QList &coordinate_list) { + QMapbox::Coordinates coordinates; + for (auto &c : coordinate_list) { + coordinates.push_back({c.latitude(), c.longitude()}); + } + return {QMapbox::CoordinatesCollection{coordinates}}; +} + +QList polyline_to_coordinate_list(const QString &polylineString) { + QList path; + if (polylineString.isEmpty()) + return path; + + QByteArray data = polylineString.toLatin1(); + + bool parsingLatitude = true; + + int shift = 0; + int value = 0; + + QGeoCoordinate coord(0, 0); + + for (int i = 0; i < data.length(); ++i) { + unsigned char c = data.at(i) - 63; + + value |= (c & 0x1f) << shift; + shift += 5; + + // another chunk + if (c & 0x20) + continue; + + int diff = (value & 1) ? ~(value >> 1) : (value >> 1); + + if (parsingLatitude) { + coord.setLatitude(coord.latitude() + (double)diff/1e6); + } else { + coord.setLongitude(coord.longitude() + (double)diff/1e6); + path.append(coord); + } + + parsingLatitude = !parsingLatitude; + + value = 0; + shift = 0; + } + + return path; +} + +std::optional coordinate_from_param(const std::string ¶m) { + QString json_str = QString::fromStdString(Params().get(param)); + if (json_str.isEmpty()) return {}; + + QJsonDocument doc = QJsonDocument::fromJson(json_str.toUtf8()); + if (doc.isNull()) return {}; + + QJsonObject json = doc.object(); + if (json["latitude"].isDouble() && json["longitude"].isDouble()) { + QMapbox::Coordinate coord(json["latitude"].toDouble(), json["longitude"].toDouble()); + return coord; + } else { + return {}; + } +} + +// return {distance, unit} +std::pair map_format_distance(float d, bool is_metric) { + auto round_distance = [](float d) -> float { + return (d > 10) ? std::nearbyint(d) : std::nearbyint(d * 10) / 10.0; + }; + + d = std::max(d, 0.0f); + if (is_metric) { + return (d > 500) ? std::pair{QString::number(round_distance(d / 1000)), QObject::tr("km")} + : std::pair{QString::number(50 * std::nearbyint(d / 50)), QObject::tr("m")}; + } else { + float feet = d * METER_TO_FOOT; + return (feet > 500) ? std::pair{QString::number(round_distance(d * METER_TO_MILE)), QObject::tr("mi")} + : std::pair{QString::number(50 * std::nearbyint(d / 50)), QObject::tr("ft")}; + } +} diff --git a/selfdrive/ui/qt/maps/map_helpers.h b/selfdrive/ui/qt/maps/map_helpers.h new file mode 100755 index 0000000..04e7686 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_helpers.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/params.h" +#include "common/util.h" +#include "common/transformations/coordinates.hpp" +#include "common/transformations/orientation.hpp" +#include "cereal/messaging/messaging.h" + +const QString MAPBOX_TOKEN = QString::fromStdString(Params().get("MapboxPublicKey")); +const QString MAPS_HOST = util::getenv("MAPS_HOST", MAPBOX_TOKEN.isEmpty() ? "https://maps.comma.ai" : "https://api.mapbox.com").c_str(); +const QString MAPS_CACHE_PATH = "/data/mbgl-cache-navd.db"; + +QString get_mapbox_token(); +QMapboxGLSettings get_mapbox_settings(); +QGeoCoordinate to_QGeoCoordinate(const QMapbox::Coordinate &in); +QMapbox::CoordinatesCollections model_to_collection( + const cereal::LiveLocationKalman::Measurement::Reader &calibratedOrientationECEF, + const cereal::LiveLocationKalman::Measurement::Reader &positionECEF, + const cereal::XYZTData::Reader &line); +QMapbox::CoordinatesCollections coordinate_to_collection(const QMapbox::Coordinate &c); +QMapbox::CoordinatesCollections capnp_coordinate_list_to_collection(const capnp::List::Reader &coordinate_list); +QMapbox::CoordinatesCollections coordinate_list_to_collection(const QList &coordinate_list); +QList polyline_to_coordinate_list(const QString &polylineString); +std::optional coordinate_from_param(const std::string ¶m); +std::pair map_format_distance(float d, bool is_metric); diff --git a/selfdrive/ui/qt/maps/map_instructions.cc b/selfdrive/ui/qt/maps/map_instructions.cc new file mode 100755 index 0000000..ba8cb35 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_instructions.cc @@ -0,0 +1,144 @@ +#include "selfdrive/ui/qt/maps/map_instructions.h" + +#include +#include + +#include "selfdrive/ui/qt/maps/map_helpers.h" +#include "selfdrive/ui/ui.h" + +const QString ICON_SUFFIX = ".png"; + +MapInstructions::MapInstructions(QWidget *parent) : QWidget(parent) { + is_rhd = Params().getBool("IsRhdDetected"); + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(11, UI_BORDER_SIZE, 11, 20); + + QHBoxLayout *top_layout = new QHBoxLayout; + top_layout->addWidget(icon_01 = new QLabel, 0, Qt::AlignTop); + + QVBoxLayout *right_layout = new QVBoxLayout; + right_layout->setContentsMargins(9, 9, 9, 0); + right_layout->addWidget(distance = new QLabel); + distance->setStyleSheet(R"(font-size: 90px;)"); + + right_layout->addWidget(primary = new QLabel); + primary->setStyleSheet(R"(font-size: 60px;)"); + primary->setWordWrap(true); + + right_layout->addWidget(secondary = new QLabel); + secondary->setStyleSheet(R"(font-size: 50px;)"); + secondary->setWordWrap(true); + + top_layout->addLayout(right_layout); + + main_layout->addLayout(top_layout); + main_layout->addLayout(lane_layout = new QHBoxLayout); + lane_layout->setAlignment(Qt::AlignHCenter); + lane_layout->setSpacing(10); + + setStyleSheet("color:white"); + QPalette pal = palette(); + pal.setColor(QPalette::Background, QColor(0, 0, 0, 150)); + setAutoFillBackground(true); + setPalette(pal); + + buildPixmapCache(); +} + +void MapInstructions::buildPixmapCache() { + QDir dir("../assets/navigation"); + for (QString fn : dir.entryList({"*" + ICON_SUFFIX}, QDir::Files)) { + QPixmap pm(dir.filePath(fn)); + QString key = fn.left(fn.size() - ICON_SUFFIX.length()); + pm = pm.scaledToWidth(200, Qt::SmoothTransformation); + + // Maneuver icons + pixmap_cache[key] = pm; + // lane direction icons + if (key.contains("turn_")) { + pixmap_cache["lane_" + key] = pm.scaled({125, 125}, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + + // for rhd, reflect direction and then flip + if (key.contains("_left")) { + pixmap_cache["rhd_" + key.replace("_left", "_right")] = pm.transformed(QTransform().scale(-1, 1)); + } else if (key.contains("_right")) { + pixmap_cache["rhd_" + key.replace("_right", "_left")] = pm.transformed(QTransform().scale(-1, 1)); + } + } +} + +void MapInstructions::updateInstructions(cereal::NavInstruction::Reader instruction) { + setUpdatesEnabled(false); + + // Show instruction text + QString primary_str = QString::fromStdString(instruction.getManeuverPrimaryText()); + QString secondary_str = QString::fromStdString(instruction.getManeuverSecondaryText()); + + primary->setText(primary_str); + secondary->setVisible(secondary_str.length() > 0); + secondary->setText(secondary_str); + + auto distance_str_pair = map_format_distance(instruction.getManeuverDistance(), uiState()->scene.is_metric); + distance->setText(QString("%1 %2").arg(distance_str_pair.first, distance_str_pair.second)); + + // Show arrow with direction + QString type = QString::fromStdString(instruction.getManeuverType()); + QString modifier = QString::fromStdString(instruction.getManeuverModifier()); + if (!type.isEmpty()) { + QString fn = "direction_" + type; + if (!modifier.isEmpty()) { + fn += "_" + modifier; + } + fn = fn.replace(' ', '_'); + bool rhd = is_rhd && (fn.contains("_left") || fn.contains("_right")); + icon_01->setPixmap(pixmap_cache[!rhd ? fn : "rhd_" + fn]); + icon_01->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + icon_01->setVisible(true); + } else { + icon_01->setVisible(false); + } + + // Hide distance after arrival + distance->setVisible(type != "arrive" || instruction.getManeuverDistance() > 0); + + // Show lanes + auto lanes = instruction.getLanes(); + for (int i = 0; i < lanes.size(); ++i) { + bool active = lanes[i].getActive(); + const auto active_direction = lanes[i].getActiveDirection(); + + // TODO: Make more images based on active direction and combined directions + QString fn = "lane_direction_"; + + // active direction has precedence + if (active && active_direction != cereal::NavInstruction::Direction::NONE) { + fn += "turn_" + DIRECTIONS[active_direction]; + } else { + for (auto const &direction : lanes[i].getDirections()) { + if (direction != cereal::NavInstruction::Direction::NONE) { + fn += "turn_" + DIRECTIONS[direction]; + break; + } + } + } + + if (!active) { + fn += "_inactive"; + } + + QLabel *label = (i < lane_labels.size()) ? lane_labels[i] : lane_labels.emplace_back(new QLabel); + if (!label->parentWidget()) { + lane_layout->addWidget(label); + } + label->setPixmap(pixmap_cache[fn]); + label->setVisible(true); + } + + for (int i = lanes.size(); i < lane_labels.size(); ++i) { + lane_labels[i]->setVisible(false); + } + + setUpdatesEnabled(true); + setVisible(true); +} diff --git a/selfdrive/ui/qt/maps/map_instructions.h b/selfdrive/ui/qt/maps/map_instructions.h new file mode 100755 index 0000000..06a943d --- /dev/null +++ b/selfdrive/ui/qt/maps/map_instructions.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "cereal/gen/cpp/log.capnp.h" + +static std::map DIRECTIONS = { + {cereal::NavInstruction::Direction::NONE, "none"}, + {cereal::NavInstruction::Direction::LEFT, "left"}, + {cereal::NavInstruction::Direction::RIGHT, "right"}, + {cereal::NavInstruction::Direction::STRAIGHT, "straight"}, + {cereal::NavInstruction::Direction::SLIGHT_LEFT, "slight_left"}, + {cereal::NavInstruction::Direction::SLIGHT_RIGHT, "slight_right"}, +}; + +class MapInstructions : public QWidget { + Q_OBJECT + +private: + QLabel *distance; + QLabel *primary; + QLabel *secondary; + QLabel *icon_01; + QHBoxLayout *lane_layout; + bool is_rhd = false; + std::vector lane_labels; + QHash pixmap_cache; + +public: + MapInstructions(QWidget * parent=nullptr); + void buildPixmapCache(); + void updateInstructions(cereal::NavInstruction::Reader instruction); +}; diff --git a/selfdrive/ui/qt/maps/map_panel.cc b/selfdrive/ui/qt/maps/map_panel.cc new file mode 100755 index 0000000..7913314 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_panel.cc @@ -0,0 +1,48 @@ +#include "selfdrive/ui/qt/maps/map_panel.h" + +#include +#include + +#include "selfdrive/ui/qt/maps/map.h" +#include "selfdrive/ui/qt/maps/map_settings.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/ui.h" + +MapPanel::MapPanel(const QMapboxGLSettings &mapboxSettings, QWidget *parent) : QFrame(parent) { + content_stack = new QStackedLayout(this); + content_stack->setContentsMargins(0, 0, 0, 0); + + auto map = new MapWindow(mapboxSettings); + QObject::connect(uiState(), &UIState::offroadTransition, map, &MapWindow::offroadTransition); + QObject::connect(device(), &Device::interactiveTimeout, this, [=]() { + content_stack->setCurrentIndex(0); + }); + QObject::connect(map, &MapWindow::requestVisible, this, [=](bool visible) { + // when we show the map for a new route, signal HomeWindow to hide the sidebar + if (visible) { emit mapPanelRequested(); } + setVisible(visible); + }); + QObject::connect(map, &MapWindow::requestSettings, this, [=](bool settings) { + content_stack->setCurrentIndex(settings ? 1 : 0); + }); + content_stack->addWidget(map); + + auto settings = new MapSettings(true, parent); + QObject::connect(settings, &MapSettings::closeSettings, this, [=]() { + content_stack->setCurrentIndex(0); + }); + content_stack->addWidget(settings); +} + +void MapPanel::toggleMapSettings() { + // show settings if not visible, then toggle between map and settings + int new_index = isVisible() ? (1 - content_stack->currentIndex()) : 1; + content_stack->setCurrentIndex(new_index); + emit mapPanelRequested(); + show(); +} + +void MapPanel::setVisible(bool visible) { + QFrame::setVisible(visible); + uiState()->scene.map_open = visible; +} diff --git a/selfdrive/ui/qt/maps/map_panel.h b/selfdrive/ui/qt/maps/map_panel.h new file mode 100755 index 0000000..1b0ba42 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_panel.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +class MapPanel : public QFrame { + Q_OBJECT + +public: + explicit MapPanel(const QMapboxGLSettings &settings, QWidget *parent = nullptr); + void setVisible(bool visible); + +signals: + void mapPanelRequested(); + +public slots: + void toggleMapSettings(); + +private: + QStackedLayout *content_stack; +}; diff --git a/selfdrive/ui/qt/maps/map_settings.cc b/selfdrive/ui/qt/maps/map_settings.cc new file mode 100755 index 0000000..69a1406 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_settings.cc @@ -0,0 +1,397 @@ +#include "selfdrive/ui/qt/maps/map_settings.h" + +#include + +#include +#include + +#include "common/util.h" +#include "selfdrive/ui/qt/request_repeater.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" + +static void swap(QJsonValueRef v1, QJsonValueRef v2) { std::swap(v1, v2); } + +static bool locationEqual(const QJsonValue &v1, const QJsonValue &v2) { + return v1["latitude"] == v2["latitude"] && v1["longitude"] == v2["longitude"]; +} + +static qint64 convertTimestampToEpoch(const QString ×tamp) { + QDateTime dt = QDateTime::fromString(timestamp, Qt::ISODate); + return dt.isValid() ? dt.toSecsSinceEpoch() : 0; +} + +MapSettings::MapSettings(bool closeable, QWidget *parent) : QFrame(parent) { + setContentsMargins(0, 0, 0, 0); + setAttribute(Qt::WA_NoMousePropagation); + + auto *frame = new QVBoxLayout(this); + frame->setContentsMargins(40, 40, 40, 0); + frame->setSpacing(0); + + auto *heading_frame = new QHBoxLayout; + heading_frame->setContentsMargins(0, 0, 0, 0); + heading_frame->setSpacing(32); + { + if (closeable) { + auto *close_btn = new QPushButton("←"); + close_btn->setStyleSheet(R"( + QPushButton { + color: #FFFFFF; + font-size: 100px; + padding-bottom: 8px; + border 1px grey solid; + border-radius: 70px; + background-color: #292929; + font-weight: 500; + } + QPushButton:pressed { + background-color: #3B3B3B; + } + )"); + close_btn->setFixedSize(140, 140); + QObject::connect(close_btn, &QPushButton::clicked, [=]() { emit closeSettings(); }); + // TODO: read map_on_left from ui state + heading_frame->addWidget(close_btn); + } + + auto *heading = new QVBoxLayout; + heading->setContentsMargins(0, 0, 0, 0); + heading->setSpacing(16); + { + auto *title = new QLabel(tr("NAVIGATION"), this); + title->setStyleSheet("color: #FFFFFF; font-size: 54px; font-weight: 600;"); + heading->addWidget(title); + + // NOO without Prime IP extraction + if (notPrime) { + ipAddress = QString("%1:8082").arg(wifi->getIp4Address()); + subtitle = new QLabel(tr("Manage at %1").arg(ipAddress), this); + } else { + subtitle = new QLabel(tr("Manage at connect.comma.ai"), this); + } + subtitle->setStyleSheet("color: #A0A0A0; font-size: 40px; font-weight: 300;"); + heading->addWidget(subtitle); + } + heading_frame->addLayout(heading, 1); + } + frame->addLayout(heading_frame); + frame->addSpacing(32); + + current_widget = new DestinationWidget(this); + QObject::connect(current_widget, &DestinationWidget::actionClicked, + []() { NavManager::instance()->setCurrentDestination({}); }); + frame->addWidget(current_widget); + frame->addSpacing(32); + + QWidget *destinations_container = new QWidget(this); + destinations_layout = new QVBoxLayout(destinations_container); + destinations_layout->setContentsMargins(0, 32, 0, 32); + destinations_layout->setSpacing(20); + destinations_layout->addWidget(home_widget = new DestinationWidget(this)); + destinations_layout->addWidget(work_widget = new DestinationWidget(this)); + QObject::connect(home_widget, &DestinationWidget::navigateTo, this, &MapSettings::navigateTo); + QObject::connect(work_widget, &DestinationWidget::navigateTo, this, &MapSettings::navigateTo); + destinations_layout->addStretch(); + + ScrollView *destinations_scroller = new ScrollView(destinations_container, this); + destinations_scroller->setFrameShape(QFrame::NoFrame); + frame->addWidget(destinations_scroller); + + setStyleSheet("MapSettings { background-color: #333333; }"); + QObject::connect(NavManager::instance(), &NavManager::updated, this, &MapSettings::refresh); +} + +void MapSettings::showEvent(QShowEvent *event) { + refresh(); +} + +void MapSettings::refresh() { + if (!isVisible()) return; + + setUpdatesEnabled(false); + + auto get_w = [this](int i) { + auto w = i < widgets.size() ? widgets[i] : widgets.emplace_back(new DestinationWidget); + if (!w->parentWidget()) { + destinations_layout->insertWidget(destinations_layout->count() - 1, w); + QObject::connect(w, &DestinationWidget::navigateTo, this, &MapSettings::navigateTo); + } + return w; + }; + + const auto current_dest = NavManager::instance()->currentDestination(); + if (!current_dest.isEmpty()) { + current_widget->set(current_dest, true); + } else { + current_widget->unset("", true); + } + home_widget->unset(NAV_FAVORITE_LABEL_HOME); + work_widget->unset(NAV_FAVORITE_LABEL_WORK); + + int n = 0; + for (auto location : NavManager::instance()->currentLocations()) { + DestinationWidget *w = nullptr; + auto dest = location.toObject(); + if (dest["save_type"].toString() == NAV_TYPE_FAVORITE) { + auto label = dest["label"].toString(); + if (label == NAV_FAVORITE_LABEL_HOME) w = home_widget; + if (label == NAV_FAVORITE_LABEL_WORK) w = work_widget; + } + w = w ? w : get_w(n++); + w->set(dest, false); + w->setVisible(!locationEqual(dest, current_dest)); + } + for (; n < widgets.size(); ++n) widgets[n]->setVisible(false); + + setUpdatesEnabled(true); + + // NOO without Prime IP update + if (notPrime) { + ipAddress = QString("%1:8082").arg(wifi->getIp4Address()); + subtitle->setText(tr("Manage at %1").arg(ipAddress)); + } +} + +void MapSettings::navigateTo(const QJsonObject &place) { + NavManager::instance()->setCurrentDestination(place); + emit closeSettings(); +} + +DestinationWidget::DestinationWidget(QWidget *parent) : QPushButton(parent) { + setContentsMargins(0, 0, 0, 0); + + auto *frame = new QHBoxLayout(this); + frame->setContentsMargins(32, 24, 32, 24); + frame->setSpacing(32); + + icon = new QLabel(this); + icon->setAlignment(Qt::AlignCenter); + icon->setFixedSize(96, 96); + icon->setObjectName("icon"); + frame->addWidget(icon); + + auto *inner_frame = new QVBoxLayout; + inner_frame->setContentsMargins(0, 0, 0, 0); + inner_frame->setSpacing(0); + { + title = new ElidedLabel(this); + title->setAttribute(Qt::WA_TransparentForMouseEvents); + inner_frame->addWidget(title); + + subtitle = new ElidedLabel(this); + subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); + subtitle->setObjectName("subtitle"); + inner_frame->addWidget(subtitle); + } + frame->addLayout(inner_frame, 1); + + action = new QPushButton(this); + action->setFixedSize(96, 96); + action->setObjectName("action"); + action->setStyleSheet("font-size: 65px; font-weight: 600;"); + QObject::connect(action, &QPushButton::clicked, this, &QPushButton::clicked); + QObject::connect(action, &QPushButton::clicked, this, &DestinationWidget::actionClicked); + frame->addWidget(action); + + setFixedHeight(164); + setStyleSheet(R"( + DestinationWidget { background-color: #202123; border-radius: 10px; } + QLabel { color: #FFFFFF; font-size: 48px; font-weight: 400; } + #icon { background-color: #3B4356; border-radius: 48px; } + #subtitle { color: #9BA0A5; } + #action { border: none; border-radius: 48px; color: #FFFFFF; padding-bottom: 4px; } + + /* current destination */ + [current="true"] { background-color: #E8E8E8; } + [current="true"] QLabel { color: #000000; } + [current="true"] #icon { background-color: #42906B; } + [current="true"] #subtitle { color: #333333; } + [current="true"] #action { color: #202123; } + + /* no saved destination */ + [set="false"] QLabel { color: #9BA0A5; } + [current="true"][set="false"] QLabel { color: #A0000000; } + + /* pressed */ + [current="false"]:pressed { background-color: #18191B; } + [current="true"] #action:pressed { background-color: #D6D6D6; } + )"); + QObject::connect(this, &QPushButton::clicked, [this]() { if (!dest.isEmpty()) emit navigateTo(dest); }); +} + +void DestinationWidget::set(const QJsonObject &destination, bool current) { + if (dest == destination) return; + + dest = destination; + setProperty("current", current); + setProperty("set", true); + + auto icon_pixmap = current ? icons().directions : icons().recent; + auto title_text = destination["place_name"].toString(); + auto subtitle_text = destination["place_details"].toString(); + + if (destination["save_type"] == NAV_TYPE_FAVORITE) { + if (destination["label"] == NAV_FAVORITE_LABEL_HOME) { + icon_pixmap = icons().home; + subtitle_text = title_text + ", " + subtitle_text; + title_text = tr("Home"); + } else if (destination["label"] == NAV_FAVORITE_LABEL_WORK) { + icon_pixmap = icons().work; + subtitle_text = title_text + ", " + subtitle_text; + title_text = tr("Work"); + } else { + icon_pixmap = icons().favorite; + } + } + + icon->setPixmap(icon_pixmap); + + title->setText(title_text); + subtitle->setText(subtitle_text); + subtitle->setVisible(true); + + // TODO: use pixmap + action->setAttribute(Qt::WA_TransparentForMouseEvents, !current); + action->setText(current ? "×" : "→"); + action->setVisible(true); + + setStyleSheet(styleSheet()); +} + +void DestinationWidget::unset(const QString &label, bool current) { + dest = {}; + setProperty("current", current); + setProperty("set", false); + + if (label.isEmpty()) { + icon->setPixmap(icons().directions); + title->setText(tr("No destination set")); + } else { + QString title_text = label == NAV_FAVORITE_LABEL_HOME ? tr("home") : tr("work"); + icon->setPixmap(label == NAV_FAVORITE_LABEL_HOME ? icons().home : icons().work); + title->setText(tr("No %1 location set").arg(title_text)); + } + + subtitle->setVisible(false); + action->setVisible(false); + + setStyleSheet(styleSheet()); + setVisible(true); +} + +// singleton NavManager + +NavManager *NavManager::instance() { + static NavManager *request = new NavManager(qApp); + return request; +} + +NavManager::NavManager(QObject *parent) : QObject(parent) { + locations = QJsonDocument::fromJson(params.get("NavPastDestinations").c_str()).array(); + current_dest = QJsonDocument::fromJson(params.get("NavDestination").c_str()).object(); + if (auto dongle_id = getDongleId()) { + { + // Fetch favorite and recent locations + QString url = CommaApi::BASE_URL + "/v1/navigation/" + *dongle_id + "/locations"; + RequestRepeater *repeater = new RequestRepeater(this, url, "ApiCache_NavDestinations", 30, true); + QObject::connect(repeater, &RequestRepeater::requestDone, this, &NavManager::parseLocationsResponse); + } + { + auto param_watcher = new ParamWatcher(this); + QObject::connect(param_watcher, &ParamWatcher::paramChanged, this, &NavManager::updated); + + // Destination set while offline + QString url = CommaApi::BASE_URL + "/v1/navigation/" + *dongle_id + "/next"; + HttpRequest *deleter = new HttpRequest(this); + RequestRepeater *repeater = new RequestRepeater(this, url, "", 10, true); + QObject::connect(repeater, &RequestRepeater::requestDone, [=](const QString &resp, bool success) { + if (success && resp != "null") { + if (params.get("NavDestination").empty()) { + qWarning() << "Setting NavDestination from /next" << resp; + params.put("NavDestination", resp.toStdString()); + } else { + qWarning() << "Got location from /next, but NavDestination already set"; + } + // Send DELETE to clear destination server side + deleter->sendRequest(url, HttpRequest::Method::DELETE); + } + + // athena can set destination at any time + param_watcher->addParam("NavDestination"); + current_dest = QJsonDocument::fromJson(params.get("NavDestination").c_str()).object(); + emit updated(); + }); + } + } +} + +void NavManager::parseLocationsResponse(const QString &response, bool success) { + if (!success || response == prev_response) return; + + prev_response = response; + QJsonDocument doc = QJsonDocument::fromJson(response.trimmed().toUtf8()); + if (doc.isNull()) { + qWarning() << "JSON Parse failed on navigation locations" << response; + return; + } + + // set last activity time. + auto remote_locations = doc.array(); + for (QJsonValueRef loc : remote_locations) { + auto obj = loc.toObject(); + auto serverTime = convertTimestampToEpoch(obj["modified"].toString()); + obj.insert("time", qMax(serverTime, getLastActivity(obj))); + loc = obj; + } + + locations = remote_locations; + sortLocations(); + emit updated(); +} + +void NavManager::sortLocations() { + // Sort: alphabetical FAVORITES, and then most recent. + // We don't need to care about the ordering of HOME and WORK. DestinationWidget always displays them at the top. + std::stable_sort(locations.begin(), locations.end(), [](const QJsonValue &a, const QJsonValue &b) { + if (a["save_type"] == NAV_TYPE_FAVORITE || b["save_type"] == NAV_TYPE_FAVORITE) { + return (std::tuple(a["save_type"].toString(), a["place_name"].toString()) < + std::tuple(b["save_type"].toString(), b["place_name"].toString())); + } else { + return a["time"].toVariant().toLongLong() > b["time"].toVariant().toLongLong(); + } + }); + + write_param_future = std::async(std::launch::async, [destinations = QJsonArray(locations)]() { + Params().put("NavPastDestinations", QJsonDocument(destinations).toJson().toStdString()); + }); +} + +qint64 NavManager::getLastActivity(const QJsonObject &loc) const { + qint64 last_activity = 0; + auto it = std::find_if(locations.begin(), locations.end(), + [&loc](const QJsonValue &l) { return locationEqual(loc, l); }); + if (it != locations.end()) { + auto tm = it->toObject().value("time"); + if (!tm.isUndefined() && !tm.isNull()) { + last_activity = tm.toVariant().toLongLong(); + } + } + return last_activity; +} + +void NavManager::setCurrentDestination(const QJsonObject &loc) { + current_dest = loc; + if (!current_dest.isEmpty()) { + current_dest["time"] = QDateTime::currentSecsSinceEpoch(); + auto it = std::find_if(locations.begin(), locations.end(), + [&loc](const QJsonValue &l) { return locationEqual(loc, l); }); + if (it != locations.end()) { + *it = current_dest; + sortLocations(); + } + params.put("NavDestination", QJsonDocument(current_dest).toJson().toStdString()); + } else { + params.remove("NavDestination"); + } + emit updated(); +} diff --git a/selfdrive/ui/qt/maps/map_settings.h b/selfdrive/ui/qt/maps/map_settings.h new file mode 100755 index 0000000..6a87a4d --- /dev/null +++ b/selfdrive/ui/qt/maps/map_settings.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/network/wifi_manager.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/controls.h" + +const QString NAV_TYPE_FAVORITE = "favorite"; +const QString NAV_TYPE_RECENT = "recent"; + +const QString NAV_FAVORITE_LABEL_HOME = "home"; +const QString NAV_FAVORITE_LABEL_WORK = "work"; + +class DestinationWidget; + +class NavManager : public QObject { + Q_OBJECT + +public: + static NavManager *instance(); + QJsonArray currentLocations() const { return locations; } + QJsonObject currentDestination() const { return current_dest; } + void setCurrentDestination(const QJsonObject &loc); + qint64 getLastActivity(const QJsonObject &loc) const; + +signals: + void updated(); + +private: + NavManager(QObject *parent); + void parseLocationsResponse(const QString &response, bool success); + void sortLocations(); + + Params params; + QString prev_response; + QJsonArray locations; + QJsonObject current_dest; + std::future write_param_future; +}; + +class MapSettings : public QFrame { + Q_OBJECT +public: + explicit MapSettings(bool closeable = false, QWidget *parent = nullptr); + void navigateTo(const QJsonObject &place); + +private: + void showEvent(QShowEvent *event) override; + void refresh(); + + QVBoxLayout *destinations_layout; + DestinationWidget *current_widget; + DestinationWidget *home_widget; + DestinationWidget *work_widget; + std::vector widgets; + + // FrogPilot variables + bool notPrime = Params().getInt("PrimeType") == 0; + QLabel *subtitle; + QString ipAddress; + WifiManager *wifi = new WifiManager(this); + +signals: + void closeSettings(); +}; + +class DestinationWidget : public QPushButton { + Q_OBJECT +public: + explicit DestinationWidget(QWidget *parent = nullptr); + void set(const QJsonObject &location, bool current = false); + void unset(const QString &label, bool current = false); + +signals: + void actionClicked(); + void navigateTo(const QJsonObject &destination); + +private: + struct NavIcons { + QPixmap home, work, favorite, recent, directions; + }; + + static NavIcons icons() { + static NavIcons nav_icons { + loadPixmap("../assets/navigation/icon_home.svg", {48, 48}), + loadPixmap("../assets/navigation/icon_work.svg", {48, 48}), + loadPixmap("../assets/navigation/icon_favorite.svg", {48, 48}), + loadPixmap("../assets/navigation/icon_recent.svg", {48, 48}), + loadPixmap("../assets/navigation/icon_directions.svg", {48, 48}), + }; + return nav_icons; + } + +private: + QLabel *icon, *title, *subtitle; + QPushButton *action; + QJsonObject dest; +}; diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc new file mode 100755 index 0000000..a14e74b --- /dev/null +++ b/selfdrive/ui/qt/network/networking.cc @@ -0,0 +1,384 @@ +#include "selfdrive/ui/qt/network/networking.h" + +#include + +#include +#include +#include + +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" + +static const int ICON_WIDTH = 49; + +// Networking functions + +Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { + main_layout = new QStackedLayout(this); + + wifi = new WifiManager(this); + connect(wifi, &WifiManager::refreshSignal, this, &Networking::refresh); + connect(wifi, &WifiManager::wrongPassword, this, &Networking::wrongPassword); + + wifiScreen = new QWidget(this); + QVBoxLayout* vlayout = new QVBoxLayout(wifiScreen); + vlayout->setContentsMargins(20, 20, 20, 20); + if (show_advanced) { + QPushButton* advancedSettings = new QPushButton(tr("Advanced")); + advancedSettings->setObjectName("advanced_btn"); + advancedSettings->setStyleSheet("margin-right: 30px;"); + advancedSettings->setFixedSize(400, 100); + connect(advancedSettings, &QPushButton::clicked, [=]() { main_layout->setCurrentWidget(an); }); + vlayout->addSpacing(10); + vlayout->addWidget(advancedSettings, 0, Qt::AlignRight); + vlayout->addSpacing(10); + } + + wifiWidget = new WifiUI(this, wifi); + wifiWidget->setObjectName("wifiWidget"); + connect(wifiWidget, &WifiUI::connectToNetwork, this, &Networking::connectToNetwork); + + ScrollView *wifiScroller = new ScrollView(wifiWidget, this); + wifiScroller->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + vlayout->addWidget(wifiScroller, 1); + main_layout->addWidget(wifiScreen); + + an = new AdvancedNetworking(this, wifi); + connect(an, &AdvancedNetworking::backPress, [=]() { main_layout->setCurrentWidget(wifiScreen); }); + connect(an, &AdvancedNetworking::requestWifiScreen, [=]() { main_layout->setCurrentWidget(wifiScreen); }); + main_layout->addWidget(an); + + QPalette pal = palette(); + pal.setColor(QPalette::Window, QColor(0x29, 0x29, 0x29)); + setAutoFillBackground(true); + setPalette(pal); + + setStyleSheet(R"( + #wifiWidget > QPushButton, #back_btn, #advanced_btn { + font-size: 50px; + margin: 0px; + padding: 15px; + border-width: 0; + border-radius: 30px; + color: #dddddd; + background-color: #393939; + } + #back_btn:pressed, #advanced_btn:pressed { + background-color: #4a4a4a; + } + )"); + main_layout->setCurrentWidget(wifiScreen); +} + +void Networking::refresh() { + wifiWidget->refresh(); + an->refresh(); +} + +void Networking::connectToNetwork(const Network n) { + if (wifi->isKnownConnection(n.ssid)) { + wifi->activateWifiConnection(n.ssid); + } else if (n.security_type == SecurityType::OPEN) { + wifi->connect(n); + } else if (n.security_type == SecurityType::WPA) { + QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8); + if (!pass.isEmpty()) { + wifi->connect(n, pass); + } + } +} + +void Networking::wrongPassword(const QString &ssid) { + if (wifi->seenNetworks.contains(ssid)) { + const Network &n = wifi->seenNetworks.value(ssid); + QString pass = InputDialog::getText(tr("Wrong password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8); + if (!pass.isEmpty()) { + wifi->connect(n, pass); + } + } +} + +void Networking::showEvent(QShowEvent *event) { + wifi->start(); +} + +void Networking::hideEvent(QHideEvent *event) { + main_layout->setCurrentWidget(wifiScreen); + wifi->stop(); +} + +// AdvancedNetworking functions + +AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWidget(parent), wifi(wifi) { + + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setMargin(40); + main_layout->setSpacing(20); + + // Back button + QPushButton* back = new QPushButton(tr("Back")); + back->setObjectName("back_btn"); + back->setFixedSize(400, 100); + connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); + main_layout->addWidget(back, 0, Qt::AlignLeft); + + ListWidget *list = new ListWidget(this); + // Enable tethering layout + tetheringToggle = new ToggleControl(tr("Enable Tethering"), "", "", wifi->isTetheringEnabled()); + list->addItem(tetheringToggle); + QObject::connect(tetheringToggle, &ToggleControl::toggleFlipped, this, &AdvancedNetworking::toggleTethering); + if (params.getBool("TetheringEnabled")) { + tetheringToggle->setVisualOn(); + uiState()->scene.tethering_enabled = true; + } + + // Change tethering password + ButtonControl *editPasswordButton = new ButtonControl(tr("Tethering Password"), tr("EDIT")); + connect(editPasswordButton, &ButtonControl::clicked, [=]() { + QString pass = InputDialog::getText(tr("Enter new tethering password"), this, "", true, 8, wifi->getTetheringPassword()); + if (!pass.isEmpty()) { + wifi->changeTetheringPassword(pass); + } + }); + list->addItem(editPasswordButton); + + // IP address + ipLabel = new LabelControl(tr("IP Address"), wifi->ipv4_address); + list->addItem(ipLabel); + + // SSH keys + list->addItem(new SshToggle()); + list->addItem(new SshControl()); + + // Roaming toggle + const bool roamingEnabled = params.getBool("GsmRoaming"); + roamingToggle = new ToggleControl(tr("Enable Roaming"), "", "", roamingEnabled); + QObject::connect(roamingToggle, &ToggleControl::toggleFlipped, [=](bool state) { + params.putBool("GsmRoaming", state); + wifi->updateGsmSettings(state, QString::fromStdString(params.get("GsmApn")), params.getBool("GsmMetered")); + }); + list->addItem(roamingToggle); + + // APN settings + editApnButton = new ButtonControl(tr("APN Setting"), tr("EDIT")); + connect(editApnButton, &ButtonControl::clicked, [=]() { + const QString cur_apn = QString::fromStdString(params.get("GsmApn")); + QString apn = InputDialog::getText(tr("Enter APN"), this, tr("leave blank for automatic configuration"), false, -1, cur_apn).trimmed(); + + if (apn.isEmpty()) { + params.remove("GsmApn"); + } else { + params.put("GsmApn", apn.toStdString()); + } + wifi->updateGsmSettings(params.getBool("GsmRoaming"), apn, params.getBool("GsmMetered")); + }); + list->addItem(editApnButton); + + // Metered toggle + const bool metered = params.getBool("GsmMetered"); + meteredToggle = new ToggleControl(tr("Cellular Metered"), tr("Prevent large data uploads when on a metered connection"), "", metered); + QObject::connect(meteredToggle, &SshToggle::toggleFlipped, [=](bool state) { + params.putBool("GsmMetered", state); + wifi->updateGsmSettings(params.getBool("GsmRoaming"), QString::fromStdString(params.get("GsmApn")), state); + }); + list->addItem(meteredToggle); + + // Hidden Network + hiddenNetworkButton = new ButtonControl(tr("Hidden Network"), tr("CONNECT")); + connect(hiddenNetworkButton, &ButtonControl::clicked, [=]() { + QString ssid = InputDialog::getText(tr("Enter SSID"), this, "", false, 1); + if (!ssid.isEmpty()) { + QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(ssid), true, -1); + Network hidden_network; + hidden_network.ssid = ssid.toUtf8(); + if (!pass.isEmpty()) { + hidden_network.security_type = SecurityType::WPA; + wifi->connect(hidden_network, pass); + } else { + wifi->connect(hidden_network); + } + emit requestWifiScreen(); + } + }); + list->addItem(hiddenNetworkButton); + + // Set initial config + wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn")), metered); + + connect(uiState(), &UIState::primeTypeChanged, this, [=](PrimeType prime_type) { + bool gsmVisible = prime_type == PrimeType::NONE || prime_type == PrimeType::LITE; + roamingToggle->setVisible(gsmVisible); + editApnButton->setVisible(gsmVisible); + meteredToggle->setVisible(gsmVisible); + }); + + main_layout->addWidget(new ScrollView(list, this)); + main_layout->addStretch(1); +} + +void AdvancedNetworking::refresh() { + ipLabel->setText(wifi->ipv4_address); + tetheringToggle->setEnabled(true); + update(); +} + +void AdvancedNetworking::toggleTethering(bool enabled) { + wifi->setTetheringEnabled(enabled); + tetheringToggle->setEnabled(false); + params.putBool("TetheringEnabled", enabled); + uiState()->scene.tethering_enabled = enabled; +} + +// WifiUI functions + +WifiUI::WifiUI(QWidget *parent, WifiManager* wifi) : QWidget(parent), wifi(wifi) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + main_layout->setSpacing(0); + + // load imgs + for (const auto &s : {"low", "medium", "high", "full"}) { + QPixmap pix(ASSET_PATH + "/offroad/icon_wifi_strength_" + s + ".svg"); + strengths.push_back(pix.scaledToHeight(68, Qt::SmoothTransformation)); + } + lock = QPixmap(ASSET_PATH + "offroad/icon_lock_closed.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); + checkmark = QPixmap(ASSET_PATH + "offroad/icon_checkmark.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); + circled_slash = QPixmap(ASSET_PATH + "img_circled_slash.svg").scaledToWidth(ICON_WIDTH, Qt::SmoothTransformation); + + scanningLabel = new QLabel(tr("Scanning for networks...")); + scanningLabel->setStyleSheet("font-size: 65px;"); + main_layout->addWidget(scanningLabel, 0, Qt::AlignCenter); + + wifi_list_widget = new ListWidget(this); + wifi_list_widget->setVisible(false); + main_layout->addWidget(wifi_list_widget); + + setStyleSheet(R"( + QScrollBar::handle:vertical { + min-height: 0px; + border-radius: 4px; + background-color: #8A8A8A; + } + #forgetBtn { + font-size: 32px; + font-weight: 600; + color: #292929; + background-color: #BDBDBD; + border-width: 1px solid #828282; + border-radius: 5px; + padding: 40px; + padding-bottom: 16px; + padding-top: 16px; + } + #forgetBtn:pressed { + background-color: #828282; + } + #connecting { + font-size: 32px; + font-weight: 600; + color: white; + border-radius: 0; + padding: 27px; + padding-left: 43px; + padding-right: 43px; + background-color: black; + } + #ssidLabel { + text-align: left; + border: none; + padding-top: 50px; + padding-bottom: 50px; + } + #ssidLabel:disabled { + color: #696969; + } + )"); +} + +void WifiUI::refresh() { + bool is_empty = wifi->seenNetworks.isEmpty(); + scanningLabel->setVisible(is_empty); + wifi_list_widget->setVisible(!is_empty); + if (is_empty) return; + + setUpdatesEnabled(false); + + const bool is_tethering_enabled = wifi->isTetheringEnabled(); + QList sortedNetworks = wifi->seenNetworks.values(); + std::sort(sortedNetworks.begin(), sortedNetworks.end(), compare_by_strength); + + int n = 0; + for (Network &network : sortedNetworks) { + QPixmap status_icon; + if (network.connected == ConnectedType::CONNECTED) { + status_icon = checkmark; + } else if (network.security_type == SecurityType::UNSUPPORTED) { + status_icon = circled_slash; + } else if (network.security_type == SecurityType::WPA) { + status_icon = lock; + } + bool show_forget_btn = wifi->isKnownConnection(network.ssid) && !is_tethering_enabled; + QPixmap strength = strengths[strengthLevel(network.strength)]; + + auto item = getItem(n++); + item->setItem(network, status_icon, show_forget_btn, strength); + item->setVisible(true); + } + for (; n < wifi_items.size(); ++n) wifi_items[n]->setVisible(false); + + setUpdatesEnabled(true); +} + +WifiItem *WifiUI::getItem(int n) { + auto item = n < wifi_items.size() ? wifi_items[n] : wifi_items.emplace_back(new WifiItem(tr("CONNECTING..."), tr("FORGET"))); + if (!item->parentWidget()) { + QObject::connect(item, &WifiItem::connectToNetwork, this, &WifiUI::connectToNetwork); + QObject::connect(item, &WifiItem::forgotNetwork, [this](const Network n) { + if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"%1\"?").arg(QString::fromUtf8(n.ssid)), tr("Forget"), this)) + wifi->forgetConnection(n.ssid); + }); + wifi_list_widget->addItem(item); + } + return item; +} + +// WifiItem + +WifiItem::WifiItem(const QString &connecting_text, const QString &forget_text, QWidget *parent) : QWidget(parent) { + QHBoxLayout *hlayout = new QHBoxLayout(this); + hlayout->setContentsMargins(44, 0, 73, 0); + hlayout->setSpacing(50); + + hlayout->addWidget(ssidLabel = new ElidedLabel()); + ssidLabel->setObjectName("ssidLabel"); + ssidLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + hlayout->addWidget(connecting = new QPushButton(connecting_text), 0, Qt::AlignRight); + connecting->setObjectName("connecting"); + hlayout->addWidget(forgetBtn = new QPushButton(forget_text), 0, Qt::AlignRight); + forgetBtn->setObjectName("forgetBtn"); + hlayout->addWidget(iconLabel = new QLabel(), 0, Qt::AlignRight); + hlayout->addWidget(strengthLabel = new QLabel(), 0, Qt::AlignRight); + + iconLabel->setFixedWidth(ICON_WIDTH); + QObject::connect(forgetBtn, &QPushButton::clicked, [this]() { emit forgotNetwork(network); }); + QObject::connect(ssidLabel, &ElidedLabel::clicked, [this]() { + if (network.connected == ConnectedType::DISCONNECTED) emit connectToNetwork(network); + }); +} + +void WifiItem::setItem(const Network &n, const QPixmap &status_icon, bool show_forget_btn, const QPixmap &strength_icon) { + network = n; + + ssidLabel->setText(n.ssid); + ssidLabel->setEnabled(n.security_type != SecurityType::UNSUPPORTED); + ssidLabel->setFont(InterFont(55, network.connected == ConnectedType::DISCONNECTED ? QFont::Normal : QFont::Bold)); + + connecting->setVisible(n.connected == ConnectedType::CONNECTING); + forgetBtn->setVisible(show_forget_btn); + + iconLabel->setPixmap(status_icon); + strengthLabel->setPixmap(strength_icon); +} diff --git a/selfdrive/ui/qt/network/networking.h b/selfdrive/ui/qt/network/networking.h new file mode 100755 index 0000000..9b6af00 --- /dev/null +++ b/selfdrive/ui/qt/network/networking.h @@ -0,0 +1,101 @@ +#pragma once + +#include + +#include "selfdrive/ui/qt/network/wifi_manager.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "selfdrive/ui/qt/widgets/ssh_keys.h" +#include "selfdrive/ui/qt/widgets/toggle.h" + +class WifiItem : public QWidget { + Q_OBJECT +public: + explicit WifiItem(const QString &connecting_text, const QString &forget_text, QWidget* parent = nullptr); + void setItem(const Network& n, const QPixmap &icon, bool show_forget_btn, const QPixmap &strength); + +signals: + // Cannot pass Network by reference. it may change after the signal is sent. + void connectToNetwork(const Network n); + void forgotNetwork(const Network n); + +protected: + ElidedLabel* ssidLabel; + QPushButton* connecting; + QPushButton* forgetBtn; + QLabel* iconLabel; + QLabel* strengthLabel; + Network network; +}; + +class WifiUI : public QWidget { + Q_OBJECT + +public: + explicit WifiUI(QWidget *parent = 0, WifiManager* wifi = 0); + +private: + WifiItem *getItem(int n); + + WifiManager *wifi = nullptr; + QLabel *scanningLabel = nullptr; + QPixmap lock; + QPixmap checkmark; + QPixmap circled_slash; + QVector strengths; + ListWidget *wifi_list_widget = nullptr; + std::vector wifi_items; + +signals: + void connectToNetwork(const Network n); + +public slots: + void refresh(); +}; + +class AdvancedNetworking : public QWidget { + Q_OBJECT +public: + explicit AdvancedNetworking(QWidget* parent = 0, WifiManager* wifi = 0); + +private: + LabelControl* ipLabel; + ToggleControl* tetheringToggle; + ToggleControl* roamingToggle; + ButtonControl* editApnButton; + ButtonControl* hiddenNetworkButton; + ToggleControl* meteredToggle; + WifiManager* wifi = nullptr; + Params params; + +signals: + void backPress(); + void requestWifiScreen(); + +public slots: + void toggleTethering(bool enabled); + void refresh(); +}; + +class Networking : public QFrame { + Q_OBJECT + +public: + explicit Networking(QWidget* parent = 0, bool show_advanced = true); + WifiManager* wifi = nullptr; + +private: + QStackedLayout* main_layout = nullptr; + QWidget* wifiScreen = nullptr; + AdvancedNetworking* an = nullptr; + WifiUI* wifiWidget; + + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + +public slots: + void refresh(); + +private slots: + void connectToNetwork(const Network n); + void wrongPassword(const QString &ssid); +}; diff --git a/selfdrive/ui/qt/network/networkmanager.h b/selfdrive/ui/qt/network/networkmanager.h new file mode 100755 index 0000000..2896b0f --- /dev/null +++ b/selfdrive/ui/qt/network/networkmanager.h @@ -0,0 +1,47 @@ +#pragma once + +/** + * We are using a NetworkManager DBUS API : https://developer.gnome.org/NetworkManager/1.26/spec.html + * */ + +// https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApFlags +const int NM_802_11_AP_FLAGS_NONE = 0x00000000; +const int NM_802_11_AP_FLAGS_PRIVACY = 0x00000001; +const int NM_802_11_AP_FLAGS_WPS = 0x00000002; + +// https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApSecurityFlags +const int NM_802_11_AP_SEC_PAIR_WEP40 = 0x00000001; +const int NM_802_11_AP_SEC_PAIR_WEP104 = 0x00000002; +const int NM_802_11_AP_SEC_GROUP_WEP40 = 0x00000010; +const int NM_802_11_AP_SEC_GROUP_WEP104 = 0x00000020; +const int NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x00000100; +const int NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x00000200; + +const QString NM_DBUS_PATH = "/org/freedesktop/NetworkManager"; +const QString NM_DBUS_PATH_SETTINGS = "/org/freedesktop/NetworkManager/Settings"; + +const QString NM_DBUS_INTERFACE = "org.freedesktop.NetworkManager"; +const QString NM_DBUS_INTERFACE_PROPERTIES = "org.freedesktop.DBus.Properties"; +const QString NM_DBUS_INTERFACE_SETTINGS = "org.freedesktop.NetworkManager.Settings"; +const QString NM_DBUS_INTERFACE_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection"; +const QString NM_DBUS_INTERFACE_DEVICE = "org.freedesktop.NetworkManager.Device"; +const QString NM_DBUS_INTERFACE_DEVICE_WIRELESS = "org.freedesktop.NetworkManager.Device.Wireless"; +const QString NM_DBUS_INTERFACE_ACCESS_POINT = "org.freedesktop.NetworkManager.AccessPoint"; +const QString NM_DBUS_INTERFACE_ACTIVE_CONNECTION = "org.freedesktop.NetworkManager.Connection.Active"; +const QString NM_DBUS_INTERFACE_IP4_CONFIG = "org.freedesktop.NetworkManager.IP4Config"; + +const QString NM_DBUS_SERVICE = "org.freedesktop.NetworkManager"; + +const int NM_DEVICE_STATE_ACTIVATED = 100; +const int NM_DEVICE_STATE_NEED_AUTH = 60; +const int NM_DEVICE_TYPE_WIFI = 2; +const int NM_DEVICE_TYPE_MODEM = 8; +const int NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8; +const int DBUS_TIMEOUT = 100; + +// https://developer-old.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMMetered +const int NM_METERED_UNKNOWN = 0; +const int NM_METERED_YES = 1; +const int NM_METERED_NO = 2; +const int NM_METERED_GUESS_YES = 3; +const int NM_METERED_GUESS_NO = 4; diff --git a/selfdrive/ui/qt/network/wifi_manager.cc b/selfdrive/ui/qt/network/wifi_manager.cc new file mode 100755 index 0000000..ebb5cb8 --- /dev/null +++ b/selfdrive/ui/qt/network/wifi_manager.cc @@ -0,0 +1,493 @@ +#include "selfdrive/ui/qt/network/wifi_manager.h" + +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/widgets/prime.h" + +#include "common/params.h" +#include "common/swaglog.h" +#include "selfdrive/ui/qt/util.h" + +bool compare_by_strength(const Network &a, const Network &b) { + return std::tuple(a.connected, strengthLevel(a.strength), b.ssid) > + std::tuple(b.connected, strengthLevel(b.strength), a.ssid); +} + +template +T call(const QString &path, const QString &interface, const QString &method, Args &&...args) { + QDBusInterface nm = QDBusInterface(NM_DBUS_SERVICE, path, interface, QDBusConnection::systemBus()); + nm.setTimeout(DBUS_TIMEOUT); + QDBusMessage response = nm.call(method, args...); + if constexpr (std::is_same_v) { + return response; + } else if (response.arguments().count() >= 1) { + QVariant vFirst = response.arguments().at(0).value().variant(); + if (vFirst.canConvert()) { + return vFirst.value(); + } + QDebug critical = qCritical(); + critical << "Variant unpacking failure :" << method << ','; + (critical << ... << args); + } + return T(); +} + +template +QDBusPendingCall asyncCall(const QString &path, const QString &interface, const QString &method, Args &&...args) { + QDBusInterface nm = QDBusInterface(NM_DBUS_SERVICE, path, interface, QDBusConnection::systemBus()); + return nm.asyncCall(method, args...); +} + +bool emptyPath(const QString &path) { + return path == "" || path == "/"; +} + +WifiManager::WifiManager(QObject *parent) : QObject(parent) { + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + // Set tethering ssid as "weedle" + first 4 characters of a dongle id + tethering_ssid = "weedle"; + if (auto dongle_id = getDongleId()) { + tethering_ssid += "-" + dongle_id->left(4); + } + + adapter = getAdapter(); + if (!adapter.isEmpty()) { + setup(); + } else { + QDBusConnection::systemBus().connect(NM_DBUS_SERVICE, NM_DBUS_PATH, NM_DBUS_INTERFACE, "DeviceAdded", this, SLOT(deviceAdded(QDBusObjectPath))); + } + + timer.callOnTimeout(this, &WifiManager::requestScan); + + initConnections(); +} + +void WifiManager::setup() { + auto bus = QDBusConnection::systemBus(); + bus.connect(NM_DBUS_SERVICE, adapter, NM_DBUS_INTERFACE_DEVICE, "StateChanged", this, SLOT(stateChange(unsigned int, unsigned int, unsigned int))); + bus.connect(NM_DBUS_SERVICE, adapter, NM_DBUS_INTERFACE_PROPERTIES, "PropertiesChanged", this, SLOT(propertyChange(QString, QVariantMap, QStringList))); + + bus.connect(NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "ConnectionRemoved", this, SLOT(connectionRemoved(QDBusObjectPath))); + bus.connect(NM_DBUS_SERVICE, NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "NewConnection", this, SLOT(newConnection(QDBusObjectPath))); + + raw_adapter_state = call(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "State"); + activeAp = call(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE_WIRELESS, "ActiveAccessPoint").path(); + + requestScan(); +} + +void WifiManager::start() { + timer.start(5000); + refreshNetworks(); +} + +void WifiManager::stop() { + timer.stop(); +} + +void WifiManager::refreshNetworks() { + if (adapter.isEmpty() || !timer.isActive()) return; + + QDBusPendingCall pending_call = asyncCall(adapter, NM_DBUS_INTERFACE_DEVICE_WIRELESS, "GetAllAccessPoints"); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending_call); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &WifiManager::refreshFinished); +} + +void WifiManager::refreshFinished(QDBusPendingCallWatcher *watcher) { + ipv4_address = getIp4Address(); + seenNetworks.clear(); + + const QDBusReply> wather_reply = *watcher; + for (const QDBusObjectPath &path : wather_reply.value()) { + QDBusReply replay = call(path.path(), NM_DBUS_INTERFACE_PROPERTIES, "GetAll", NM_DBUS_INTERFACE_ACCESS_POINT); + auto properties = replay.value(); + + const QByteArray ssid = properties["Ssid"].toByteArray(); + if (ssid.isEmpty()) continue; + + // May be multiple access points for each SSID. + // Use first for ssid and security type, then update connected status and strength using all + if (!seenNetworks.contains(ssid)) { + seenNetworks[ssid] = {ssid, 0U, ConnectedType::DISCONNECTED, getSecurityType(properties)}; + } + + if (path.path() == activeAp) { + seenNetworks[ssid].connected = (ssid == connecting_to_network) ? ConnectedType::CONNECTING : ConnectedType::CONNECTED; + } + + uint32_t strength = properties["Strength"].toUInt(); + if (seenNetworks[ssid].strength < strength) { + seenNetworks[ssid].strength = strength; + } + } + + emit refreshSignal(); + watcher->deleteLater(); +} + +QString WifiManager::getIp4Address() { + if (raw_adapter_state != NM_DEVICE_STATE_ACTIVATED) return ""; + + for (const auto &p : getActiveConnections()) { + QString type = call(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); + if (type == "802-11-wireless") { + auto ip4config = call(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Ip4Config"); + const auto &arr = call(ip4config.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_IP4_CONFIG, "AddressData"); + QVariantMap path; + arr.beginArray(); + while (!arr.atEnd()) { + arr >> path; + arr.endArray(); + return path.value("address").value(); + } + arr.endArray(); + } + } + return ""; +} + +SecurityType WifiManager::getSecurityType(const QVariantMap &properties) { + int sflag = properties["Flags"].toUInt(); + int wpaflag = properties["WpaFlags"].toUInt(); + int rsnflag = properties["RsnFlags"].toUInt(); + int wpa_props = wpaflag | rsnflag; + + // obtained by looking at flags of networks in the office as reported by an Android phone + const int supports_wpa = NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104 | NM_802_11_AP_SEC_GROUP_WEP40 | NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_KEY_MGMT_PSK; + + if ((sflag == NM_802_11_AP_FLAGS_NONE) || ((sflag & NM_802_11_AP_FLAGS_WPS) && !(wpa_props & supports_wpa))) { + return SecurityType::OPEN; + } else if ((sflag & NM_802_11_AP_FLAGS_PRIVACY) && (wpa_props & supports_wpa) && !(wpa_props & NM_802_11_AP_SEC_KEY_MGMT_802_1X)) { + return SecurityType::WPA; + } else { + LOGW("Unsupported network! sflag: %d, wpaflag: %d, rsnflag: %d", sflag, wpaflag, rsnflag); + return SecurityType::UNSUPPORTED; + } +} + +void WifiManager::connect(const Network &n, const QString &password, const QString &username) { + setCurrentConnecting(n.ssid); + forgetConnection(n.ssid); // Clear all connections that may already exist to the network we are connecting + Connection connection; + connection["connection"]["type"] = "802-11-wireless"; + connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); + connection["connection"]["id"] = "openpilot connection " + QString::fromStdString(n.ssid.toStdString()); + connection["connection"]["autoconnect-retries"] = 0; + + connection["802-11-wireless"]["ssid"] = n.ssid; + connection["802-11-wireless"]["mode"] = "infrastructure"; + + if (n.security_type == SecurityType::WPA) { + connection["802-11-wireless-security"]["key-mgmt"] = "wpa-psk"; + connection["802-11-wireless-security"]["auth-alg"] = "open"; + connection["802-11-wireless-security"]["psk"] = password; + } + + connection["ipv4"]["method"] = "auto"; + connection["ipv4"]["dns-priority"] = 600; + connection["ipv6"]["method"] = "ignore"; + + call(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "AddConnection", QVariant::fromValue(connection)); +} + +void WifiManager::deactivateConnectionBySsid(const QString &ssid) { + for (QDBusObjectPath active_connection : getActiveConnections()) { + auto pth = call(active_connection.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "SpecificObject"); + if (!emptyPath(pth.path())) { + QString Ssid = get_property(pth.path(), "Ssid"); + if (Ssid == ssid) { + deactivateConnection(active_connection); + return; + } + } + } +} + +void WifiManager::deactivateConnection(const QDBusObjectPath &path) { + asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "DeactivateConnection", QVariant::fromValue(path)); +} + +QVector WifiManager::getActiveConnections() { + QVector conns; + QDBusObjectPath path; + const QDBusArgument &arr = call(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "ActiveConnections"); + arr.beginArray(); + while (!arr.atEnd()) { + arr >> path; + conns.push_back(path); + } + arr.endArray(); + return conns; +} + +bool WifiManager::isKnownConnection(const QString &ssid) { + return !getConnectionPath(ssid).path().isEmpty(); +} + +void WifiManager::forgetConnection(const QString &ssid) { + const QDBusObjectPath &path = getConnectionPath(ssid); + if (!path.path().isEmpty()) { + call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Delete"); + } +} + +void WifiManager::setCurrentConnecting(const QString &ssid) { + connecting_to_network = ssid; + for (auto &network : seenNetworks) { + network.connected = (network.ssid == ssid) ? ConnectedType::CONNECTING : ConnectedType::DISCONNECTED; + } + emit refreshSignal(); +} + +uint WifiManager::getAdapterType(const QDBusObjectPath &path) { + return call(path.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "DeviceType"); +} + +void WifiManager::requestScan() { + if (!adapter.isEmpty()) { + asyncCall(adapter, NM_DBUS_INTERFACE_DEVICE_WIRELESS, "RequestScan", QVariantMap()); + } +} + +QByteArray WifiManager::get_property(const QString &network_path , const QString &property) { + return call(network_path, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACCESS_POINT, property); +} + +QString WifiManager::getAdapter(const uint adapter_type) { + QDBusReply> response = call(NM_DBUS_PATH, NM_DBUS_INTERFACE, "GetDevices"); + for (const QDBusObjectPath &path : response.value()) { + if (getAdapterType(path) == adapter_type) { + return path.path(); + } + } + return ""; +} + +void WifiManager::stateChange(unsigned int new_state, unsigned int previous_state, unsigned int change_reason) { + raw_adapter_state = new_state; + if (new_state == NM_DEVICE_STATE_NEED_AUTH && change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT && !connecting_to_network.isEmpty()) { + forgetConnection(connecting_to_network); + emit wrongPassword(connecting_to_network); + } else if (new_state == NM_DEVICE_STATE_ACTIVATED) { + connecting_to_network = ""; + refreshNetworks(); + } +} + +// https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Device.Wireless.html +void WifiManager::propertyChange(const QString &interface, const QVariantMap &props, const QStringList &invalidated_props) { + if (interface == NM_DBUS_INTERFACE_DEVICE_WIRELESS && props.contains("LastScan")) { + refreshNetworks(); + } else if (interface == NM_DBUS_INTERFACE_DEVICE_WIRELESS && props.contains("ActiveAccessPoint")) { + activeAp = props.value("ActiveAccessPoint").value().path(); + } +} + +void WifiManager::deviceAdded(const QDBusObjectPath &path) { + if (getAdapterType(path) == NM_DEVICE_TYPE_WIFI && emptyPath(adapter)) { + adapter = path.path(); + setup(); + } +} + +void WifiManager::connectionRemoved(const QDBusObjectPath &path) { + knownConnections.remove(path); +} + +void WifiManager::newConnection(const QDBusObjectPath &path) { + Connection settings = getConnectionSettings(path); + if (settings.value("connection").value("type") == "802-11-wireless") { + knownConnections[path] = settings.value("802-11-wireless").value("ssid").toString(); + if (knownConnections[path] != tethering_ssid) { + activateWifiConnection(knownConnections[path]); + } + } +} + +QDBusObjectPath WifiManager::getConnectionPath(const QString &ssid) { + return knownConnections.key(ssid); +} + +Connection WifiManager::getConnectionSettings(const QDBusObjectPath &path) { + return QDBusReply(call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSettings")).value(); +} + +void WifiManager::initConnections() { + const QDBusReply> response = call(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "ListConnections"); + for (const QDBusObjectPath &path : response.value()) { + const Connection settings = getConnectionSettings(path); + if (settings.value("connection").value("type") == "802-11-wireless") { + knownConnections[path] = settings.value("802-11-wireless").value("ssid").toString(); + } else if (settings.value("connection").value("id") == "lte") { + lteConnectionPath = path; + } + } +} + +std::optional WifiManager::activateWifiConnection(const QString &ssid) { + const QDBusObjectPath &path = getConnectionPath(ssid); + if (!path.path().isEmpty()) { + setCurrentConnecting(ssid); + return asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "ActivateConnection", QVariant::fromValue(path), QVariant::fromValue(QDBusObjectPath(adapter)), QVariant::fromValue(QDBusObjectPath("/"))); + } + return std::nullopt; +} + +void WifiManager::activateModemConnection(const QDBusObjectPath &path) { + QString modem = getAdapter(NM_DEVICE_TYPE_MODEM); + if (!path.path().isEmpty() && !modem.isEmpty()) { + asyncCall(NM_DBUS_PATH, NM_DBUS_INTERFACE, "ActivateConnection", QVariant::fromValue(path), QVariant::fromValue(QDBusObjectPath(modem)), QVariant::fromValue(QDBusObjectPath("/"))); + } +} + +// function matches tici/hardware.py +NetworkType WifiManager::currentNetworkType() { + auto primary_conn = call(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "PrimaryConnection"); + auto primary_type = call(primary_conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); + + if (primary_type == "802-3-ethernet") { + return NetworkType::ETHERNET; + } else if (primary_type == "802-11-wireless" && !isTetheringEnabled()) { + return NetworkType::WIFI; + } else { + for (const QDBusObjectPath &conn : getActiveConnections()) { + auto type = call(conn.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type"); + if (type == "gsm") { + return NetworkType::CELL; + } + } + } + return NetworkType::NONE; +} + +void WifiManager::updateGsmSettings(bool roaming, QString apn, bool metered) { + if (!lteConnectionPath.path().isEmpty()) { + bool changes = false; + bool auto_config = apn.isEmpty(); + Connection settings = getConnectionSettings(lteConnectionPath); + if (settings.value("gsm").value("auto-config").toBool() != auto_config) { + qWarning() << "Changing gsm.auto-config to" << auto_config; + settings["gsm"]["auto-config"] = auto_config; + changes = true; + } + + if (settings.value("gsm").value("apn").toString() != apn) { + qWarning() << "Changing gsm.apn to" << apn; + settings["gsm"]["apn"] = apn; + changes = true; + } + + if (settings.value("gsm").value("home-only").toBool() == roaming) { + qWarning() << "Changing gsm.home-only to" << !roaming; + settings["gsm"]["home-only"] = !roaming; + changes = true; + } + + int meteredInt = metered ? NM_METERED_UNKNOWN : NM_METERED_NO; + if (settings.value("connection").value("metered").toInt() != meteredInt) { + qWarning() << "Changing connection.metered to" << meteredInt; + settings["connection"]["metered"] = meteredInt; + changes = true; + } + + if (changes) { + call(lteConnectionPath.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "UpdateUnsaved", QVariant::fromValue(settings)); // update is temporary + deactivateConnection(lteConnectionPath); + activateModemConnection(lteConnectionPath); + } + } +} + +// Functions for tethering +void WifiManager::addTetheringConnection() { + Connection connection; + connection["connection"]["id"] = "Hotspot"; + connection["connection"]["uuid"] = QUuid::createUuid().toString().remove('{').remove('}'); + connection["connection"]["type"] = "802-11-wireless"; + connection["connection"]["interface-name"] = "wlan0"; + connection["connection"]["autoconnect"] = false; + + connection["802-11-wireless"]["band"] = "bg"; + connection["802-11-wireless"]["mode"] = "ap"; + connection["802-11-wireless"]["ssid"] = tethering_ssid.toUtf8(); + + connection["802-11-wireless-security"]["group"] = QStringList("ccmp"); + connection["802-11-wireless-security"]["key-mgmt"] = "wpa-psk"; + connection["802-11-wireless-security"]["pairwise"] = QStringList("ccmp"); + connection["802-11-wireless-security"]["proto"] = QStringList("rsn"); + connection["802-11-wireless-security"]["psk"] = defaultTetheringPassword; + + connection["ipv4"]["method"] = "shared"; + QVariantMap address; + address["address"] = "192.168.43.1"; + address["prefix"] = 24u; + connection["ipv4"]["address-data"] = QVariant::fromValue(IpConfig() << address); + connection["ipv4"]["gateway"] = "192.168.43.1"; + connection["ipv4"]["route-metric"] = 1100; + connection["ipv6"]["method"] = "ignore"; + + call(NM_DBUS_PATH_SETTINGS, NM_DBUS_INTERFACE_SETTINGS, "AddConnection", QVariant::fromValue(connection)); +} + +void WifiManager::tetheringActivated(QDBusPendingCallWatcher *call) { + int prime_type = uiState()->primeType(); + int ipv4_forward = (prime_type == PrimeType::NONE || prime_type == PrimeType::LITE); + + if (!ipv4_forward) { + QTimer::singleShot(5000, this, [=] { + qWarning() << "net.ipv4.ip_forward = 0"; + std::system("sudo sysctl net.ipv4.ip_forward=0"); + }); + } + call->deleteLater(); +} + +void WifiManager::setTetheringEnabled(bool enabled) { + if (enabled) { + if (!isKnownConnection(tethering_ssid)) { + addTetheringConnection(); + } + + auto pending_call = activateWifiConnection(tethering_ssid); + + if (pending_call) { + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(*pending_call); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &WifiManager::tetheringActivated); + } + + } else { + deactivateConnectionBySsid(tethering_ssid); + } +} + +bool WifiManager::isTetheringEnabled() { + if (!emptyPath(activeAp)) { + return get_property(activeAp, "Ssid") == tethering_ssid; + } + return false; +} + +QString WifiManager::getTetheringPassword() { + if (!isKnownConnection(tethering_ssid)) { + addTetheringConnection(); + } + const QDBusObjectPath &path = getConnectionPath(tethering_ssid); + if (!path.path().isEmpty()) { + QDBusReply> response = call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSecrets", "802-11-wireless-security"); + return response.value().value("802-11-wireless-security").value("psk").toString(); + } + return ""; +} + +void WifiManager::changeTetheringPassword(const QString &newPassword) { + const QDBusObjectPath &path = getConnectionPath(tethering_ssid); + if (!path.path().isEmpty()) { + Connection settings = getConnectionSettings(path); + settings["802-11-wireless-security"]["psk"] = newPassword; + call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "Update", QVariant::fromValue(settings)); + if (isTetheringEnabled()) { + activateWifiConnection(tethering_ssid); + } + } +} diff --git a/selfdrive/ui/qt/network/wifi_manager.h b/selfdrive/ui/qt/network/wifi_manager.h new file mode 100755 index 0000000..51d1175 --- /dev/null +++ b/selfdrive/ui/qt/network/wifi_manager.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include + +#include "selfdrive/ui/qt/network/networkmanager.h" + +enum class SecurityType { + OPEN, + WPA, + UNSUPPORTED +}; +enum class ConnectedType { + DISCONNECTED, + CONNECTING, + CONNECTED +}; +enum class NetworkType { + NONE, + WIFI, + CELL, + ETHERNET +}; + +typedef QMap Connection; +typedef QVector IpConfig; + +struct Network { + QByteArray ssid; + unsigned int strength; + ConnectedType connected; + SecurityType security_type; +}; +bool compare_by_strength(const Network &a, const Network &b); +inline int strengthLevel(unsigned int strength) { return std::clamp((int)round(strength / 33.), 0, 3); } + +class WifiManager : public QObject { + Q_OBJECT + +public: + QMap seenNetworks; + QMap knownConnections; + QString ipv4_address; + + explicit WifiManager(QObject* parent); + void start(); + void stop(); + void requestScan(); + void forgetConnection(const QString &ssid); + bool isKnownConnection(const QString &ssid); + std::optional activateWifiConnection(const QString &ssid); + NetworkType currentNetworkType(); + void updateGsmSettings(bool roaming, QString apn, bool metered); + void connect(const Network &ssid, const QString &password = {}, const QString &username = {}); + + // Tethering functions + void setTetheringEnabled(bool enabled); + bool isTetheringEnabled(); + void changeTetheringPassword(const QString &newPassword); + QString getIp4Address(); + QString getTetheringPassword(); + +private: + QString adapter; // Path to network manager wifi-device + QTimer timer; + unsigned int raw_adapter_state; // Connection status https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NMDeviceState + QString connecting_to_network; + QString tethering_ssid; + const QString defaultTetheringPassword = "swagswagcomma"; + QString activeAp; + QDBusObjectPath lteConnectionPath; + + QString getAdapter(const uint = NM_DEVICE_TYPE_WIFI); + uint getAdapterType(const QDBusObjectPath &path); + void deactivateConnectionBySsid(const QString &ssid); + void deactivateConnection(const QDBusObjectPath &path); + QVector getActiveConnections(); + QByteArray get_property(const QString &network_path, const QString &property); + SecurityType getSecurityType(const QVariantMap &properties); + QDBusObjectPath getConnectionPath(const QString &ssid); + Connection getConnectionSettings(const QDBusObjectPath &path); + void initConnections(); + void setup(); + void refreshNetworks(); + void activateModemConnection(const QDBusObjectPath &path); + void addTetheringConnection(); + void setCurrentConnecting(const QString &ssid); + +signals: + void wrongPassword(const QString &ssid); + void refreshSignal(); + +private slots: + void stateChange(unsigned int new_state, unsigned int previous_state, unsigned int change_reason); + void propertyChange(const QString &interface, const QVariantMap &props, const QStringList &invalidated_props); + void deviceAdded(const QDBusObjectPath &path); + void connectionRemoved(const QDBusObjectPath &path); + void newConnection(const QDBusObjectPath &path); + void refreshFinished(QDBusPendingCallWatcher *call); + void tetheringActivated(QDBusPendingCallWatcher *call); +}; diff --git a/selfdrive/ui/qt/offroad/driverview.cc b/selfdrive/ui/qt/offroad/driverview.cc new file mode 100755 index 0000000..df9bb24 --- /dev/null +++ b/selfdrive/ui/qt/offroad/driverview.cc @@ -0,0 +1,77 @@ +#include "selfdrive/ui/qt/offroad/driverview.h" + +#include +#include + +#include "selfdrive/ui/qt/util.h" + +const int FACE_IMG_SIZE = 130; + +DriverViewWindow::DriverViewWindow(QWidget* parent) : CameraWidget("camerad", VISION_STREAM_DRIVER, true, parent) { + face_img = loadPixmap("../assets/img_driver_face_static.png", {FACE_IMG_SIZE, FACE_IMG_SIZE}); + QObject::connect(this, &CameraWidget::clicked, this, &DriverViewWindow::done); + QObject::connect(device(), &Device::interactiveTimeout, this, [this]() { + if (isVisible()) { + emit done(); + } + }); +} + +void DriverViewWindow::showEvent(QShowEvent* event) { + params.putBool("IsDriverViewEnabled", true); + device()->resetInteractiveTimeout(60); + CameraWidget::showEvent(event); +} + +void DriverViewWindow::hideEvent(QHideEvent* event) { + params.putBool("IsDriverViewEnabled", false); + stopVipcThread(); + CameraWidget::hideEvent(event); +} + +void DriverViewWindow::paintGL() { + CameraWidget::paintGL(); + + std::lock_guard lk(frame_lock); + QPainter p(this); + // startup msg + if (frames.empty()) { + p.setPen(Qt::white); + p.setRenderHint(QPainter::TextAntialiasing); + p.setFont(InterFont(100, QFont::Bold)); + p.drawText(geometry(), Qt::AlignCenter, tr("camera starting")); + return; + } + + const auto &sm = *(uiState()->sm); + cereal::DriverStateV2::Reader driver_state = sm["driverStateV2"].getDriverStateV2(); + bool is_rhd = driver_state.getWheelOnRightProb() > 0.5; + auto driver_data = is_rhd ? driver_state.getRightDriverData() : driver_state.getLeftDriverData(); + + bool face_detected = driver_data.getFaceProb() > 0.7; + if (face_detected) { + auto fxy_list = driver_data.getFacePosition(); + auto std_list = driver_data.getFaceOrientationStd(); + float face_x = fxy_list[0]; + float face_y = fxy_list[1]; + float face_std = std::max(std_list[0], std_list[1]); + + float alpha = 0.7; + if (face_std > 0.15) { + alpha = std::max(0.7 - (face_std-0.15)*3.5, 0.0); + } + const int box_size = 220; + // use approx instead of distort_points + int fbox_x = 1080.0 - 1714.0 * face_x; + int fbox_y = -135.0 + (504.0 + std::abs(face_x)*112.0) + (1205.0 - std::abs(face_x)*724.0) * face_y; + p.setPen(QPen(QColor(255, 255, 255, alpha * 255), 10)); + p.drawRoundedRect(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size, 35.0, 35.0); + } + + // icon + const int img_offset = 60; + const int img_x = is_rhd ? rect().right() - FACE_IMG_SIZE - img_offset : rect().left() + img_offset; + const int img_y = rect().bottom() - FACE_IMG_SIZE - img_offset; + p.setOpacity(face_detected ? 1.0 : 0.2); + p.drawPixmap(img_x, img_y, face_img); +} diff --git a/selfdrive/ui/qt/offroad/driverview.h b/selfdrive/ui/qt/offroad/driverview.h new file mode 100755 index 0000000..155e4ed --- /dev/null +++ b/selfdrive/ui/qt/offroad/driverview.h @@ -0,0 +1,21 @@ +#pragma once + +#include "selfdrive/ui/qt/widgets/cameraview.h" + +class DriverViewWindow : public CameraWidget { + Q_OBJECT + +public: + explicit DriverViewWindow(QWidget *parent); + +signals: + void done(); + +protected: + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + void paintGL() override; + + Params params; + QPixmap face_img; +}; diff --git a/selfdrive/ui/qt/offroad/experimental_mode.cc b/selfdrive/ui/qt/offroad/experimental_mode.cc new file mode 100755 index 0000000..b99220c --- /dev/null +++ b/selfdrive/ui/qt/offroad/experimental_mode.cc @@ -0,0 +1,76 @@ +#include "selfdrive/ui/qt/offroad/experimental_mode.h" + +#include +#include +#include +#include +#include + +#include "selfdrive/ui/ui.h" + +ExperimentalModeButton::ExperimentalModeButton(QWidget *parent) : QPushButton(parent) { + chill_pixmap = QPixmap("../assets/img_couch.svg").scaledToWidth(img_width, Qt::SmoothTransformation); + experimental_pixmap = QPixmap("../assets/img_experimental_grey.svg").scaledToWidth(img_width, Qt::SmoothTransformation); + + // go to toggles and expand experimental mode description + connect(this, &QPushButton::clicked, [=]() { emit openSettings(2, "ExperimentalMode"); }); + + setFixedHeight(125); + QHBoxLayout *main_layout = new QHBoxLayout; + main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0); + + mode_label = new QLabel; + mode_icon = new QLabel; + mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + + 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 ExperimentalModeButton::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.setPen(Qt::NoPen); + p.setRenderHint(QPainter::Antialiasing); + + QPainterPath path; + path.addRoundedRect(rect(), 10, 10); + + // gradient + bool pressed = isDown(); + QLinearGradient gradient(rect().left(), 0, rect().right(), 0); + if (experimental_mode) { + gradient.setColorAt(0, QColor(255, 155, 63, pressed ? 0xcc : 0xff)); + gradient.setColorAt(1, QColor(219, 56, 34, pressed ? 0xcc : 0xff)); + } else { + gradient.setColorAt(0, QColor(20, 255, 171, pressed ? 0xcc : 0xff)); + gradient.setColorAt(1, QColor(35, 149, 255, pressed ? 0xcc : 0xff)); + } + p.fillPath(path, gradient); + + // 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()); +} + +void ExperimentalModeButton::showEvent(QShowEvent *event) { + experimental_mode = params.getBool("ExperimentalMode"); + mode_icon->setPixmap(experimental_mode ? experimental_pixmap : chill_pixmap); + mode_label->setText(experimental_mode ? tr("EXPERIMENTAL MODE ON") : tr("CHILL MODE ON")); +} diff --git a/selfdrive/ui/qt/offroad/experimental_mode.h b/selfdrive/ui/qt/offroad/experimental_mode.h new file mode 100755 index 0000000..bfb7638 --- /dev/null +++ b/selfdrive/ui/qt/offroad/experimental_mode.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "common/params.h" + +class ExperimentalModeButton : public QPushButton { + Q_OBJECT + +public: + explicit ExperimentalModeButton(QWidget* parent = 0); + +signals: + void openSettings(int index = 0, const QString &toggle = ""); + +private: + void showEvent(QShowEvent *event) override; + + Params params; + bool experimental_mode; + int img_width = 100; + int horizontal_padding = 30; + QPixmap experimental_pixmap; + QPixmap chill_pixmap; + QLabel *mode_label; + QLabel *mode_icon; + +protected: + void paintEvent(QPaintEvent *event) override; +}; diff --git a/selfdrive/ui/qt/offroad/onboarding.cc b/selfdrive/ui/qt/offroad/onboarding.cc new file mode 100755 index 0000000..b52a6a3 --- /dev/null +++ b/selfdrive/ui/qt/offroad/onboarding.cc @@ -0,0 +1,240 @@ +#include "selfdrive/ui/qt/offroad/onboarding.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "common/util.h" +#include "common/params.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/input.h" + +TrainingGuide::TrainingGuide(QWidget *parent) : QFrame(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); +} + +void TrainingGuide::mouseReleaseEvent(QMouseEvent *e) { + if (click_timer.elapsed() < 250) { + return; + } + click_timer.restart(); + + auto contains = [this](QRect r, const QPoint &pt) { + if (image.size() != image_raw_size) { + QTransform transform; + transform.translate((width()- image.width()) / 2.0, (height()- image.height()) / 2.0); + transform.scale(image.width() / (float)image_raw_size.width(), image.height() / (float)image_raw_size.height()); + r= transform.mapRect(r); + } + return r.contains(pt); + }; + + if (contains(boundingRect[currentIndex], e->pos())) { + if (currentIndex == 9) { + const QRect yes = QRect(707, 804, 531, 164); + Params().putBool("RecordFront", contains(yes, e->pos())); + } + currentIndex += 1; + } else if (currentIndex == (boundingRect.size() - 2) && contains(boundingRect.last(), e->pos())) { + currentIndex = 0; + } + + if (currentIndex >= (boundingRect.size() - 1)) { + emit completedTraining(); + } else { + update(); + } +} + +void TrainingGuide::showEvent(QShowEvent *event) { + currentIndex = 0; + click_timer.start(); +} + +QImage TrainingGuide::loadImage(int id) { + QImage img(img_path + QString("step%1.png").arg(id)); + image_raw_size = img.size(); + if (image_raw_size != rect().size()) { + img = img.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + return img; +} + +void TrainingGuide::paintEvent(QPaintEvent *event) { + QPainter painter(this); + + QRect bg(0, 0, painter.device()->width(), painter.device()->height()); + painter.fillRect(bg, QColor("#000000")); + + image = loadImage(currentIndex); + QRect rect(image.rect()); + rect.moveCenter(bg.center()); + painter.drawImage(rect.topLeft(), image); + + // progress bar + if (currentIndex > 0 && currentIndex < (boundingRect.size() - 2)) { + const int h = 20; + const int w = (currentIndex / (float)(boundingRect.size() - 2)) * width(); + painter.fillRect(QRect(0, height() - h, w, h), QColor("#465BEA")); + } +} + +void TermsPage::showEvent(QShowEvent *event) { + // late init, building QML widget takes 200ms + if (layout()) { + return; + } + + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(45, 35, 45, 45); + main_layout->setSpacing(0); + + QLabel *title = new QLabel(tr("Terms & Conditions")); + title->setStyleSheet("font-size: 90px; font-weight: 600;"); + main_layout->addWidget(title); + + main_layout->addSpacing(30); + + QQuickWidget *text = new QQuickWidget(this); + text->setResizeMode(QQuickWidget::SizeRootObjectToView); + text->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + text->setAttribute(Qt::WA_AlwaysStackOnTop); + text->setClearColor(QColor("#1B1B1B")); + + QString text_view = util::read_file("../assets/offroad/tc.html").c_str(); + text->rootContext()->setContextProperty("text_view", text_view); + + text->setSource(QUrl::fromLocalFile("qt/offroad/text_view.qml")); + + main_layout->addWidget(text, 1); + main_layout->addSpacing(50); + + QObject *obj = (QObject*)text->rootObject(); + QObject::connect(obj, SIGNAL(scroll()), SLOT(enableAccept())); + + QHBoxLayout* buttons = new QHBoxLayout; + buttons->setMargin(0); + buttons->setSpacing(45); + main_layout->addLayout(buttons); + + QPushButton *decline_btn = new QPushButton(tr("Decline")); + buttons->addWidget(decline_btn); + QObject::connect(decline_btn, &QPushButton::clicked, this, &TermsPage::declinedTerms); + + accept_btn = new QPushButton(tr("Scroll to accept")); + accept_btn->setEnabled(false); + accept_btn->setStyleSheet(R"( + QPushButton { + background-color: #465BEA; + } + QPushButton:pressed { + background-color: #3049F4; + } + QPushButton:disabled { + background-color: #4F4F4F; + } + )"); + buttons->addWidget(accept_btn); + QObject::connect(accept_btn, &QPushButton::clicked, this, &TermsPage::acceptedTerms); +} + +void TermsPage::enableAccept() { + accept_btn->setText(tr("Agree")); + accept_btn->setEnabled(true); +} + +void DeclinePage::showEvent(QShowEvent *event) { + if (layout()) { + return; + } + + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setMargin(45); + main_layout->setSpacing(40); + + QLabel *text = new QLabel(this); + text->setText(tr("You must accept the Terms and Conditions in order to use openpilot.")); + text->setStyleSheet(R"(font-size: 80px; font-weight: 300; margin: 200px;)"); + text->setWordWrap(true); + main_layout->addWidget(text, 0, Qt::AlignCenter); + + QHBoxLayout* buttons = new QHBoxLayout; + buttons->setSpacing(45); + main_layout->addLayout(buttons); + + QPushButton *back_btn = new QPushButton(tr("Back")); + buttons->addWidget(back_btn); + + QObject::connect(back_btn, &QPushButton::clicked, this, &DeclinePage::getBack); + + QPushButton *uninstall_btn = new QPushButton(tr("Decline, uninstall %1").arg(getBrand())); + uninstall_btn->setStyleSheet("background-color: #B73D3D"); + buttons->addWidget(uninstall_btn); + QObject::connect(uninstall_btn, &QPushButton::clicked, [=]() { + Params().putBool("DoUninstall", true); + }); +} + +void OnboardingWindow::updateActiveScreen() { + if (!accepted_terms) { + setCurrentIndex(0); + // } else if (!training_done && !params.getBool("Passive")) { + // setCurrentIndex(1); + } else { + emit onboardingDone(); + } +} + +OnboardingWindow::OnboardingWindow(QWidget *parent) : QStackedWidget(parent) { + std::string current_terms_version = params.get("TermsVersion"); + std::string current_training_version = params.get("TrainingVersion"); + accepted_terms = params.get("HasAcceptedTerms") == current_terms_version; + training_done = params.get("CompletedTrainingVersion") == current_training_version; + + TermsPage* terms = new TermsPage(this); + addWidget(terms); + connect(terms, &TermsPage::acceptedTerms, [=]() { + Params().put("HasAcceptedTerms", current_terms_version); + accepted_terms = true; + updateActiveScreen(); + }); + connect(terms, &TermsPage::declinedTerms, [=]() { setCurrentIndex(2); }); + + TrainingGuide* tr = new TrainingGuide(this); + addWidget(tr); + connect(tr, &TrainingGuide::completedTraining, [=]() { + training_done = true; + Params().put("CompletedTrainingVersion", current_training_version); + updateActiveScreen(); + }); + + DeclinePage* declinePage = new DeclinePage(this); + addWidget(declinePage); + connect(declinePage, &DeclinePage::getBack, [=]() { updateActiveScreen(); }); + + setStyleSheet(R"( + * { + color: white; + background-color: black; + } + QPushButton { + height: 160px; + font-size: 55px; + font-weight: 400; + border-radius: 10px; + background-color: #4F4F4F; + } + )"); + + // # Oscar sez + Params().put("HasAcceptedTerms", current_terms_version); + Params().put("CompletedTrainingVersion", current_training_version); + accepted_terms = true; + emit onboardingDone(); + updateActiveScreen(); +} diff --git a/selfdrive/ui/qt/offroad/onboarding.h b/selfdrive/ui/qt/offroad/onboarding.h new file mode 100755 index 0000000..a1b6895 --- /dev/null +++ b/selfdrive/ui/qt/offroad/onboarding.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/qt_window.h" + +class TrainingGuide : public QFrame { + Q_OBJECT + +public: + explicit TrainingGuide(QWidget *parent = 0); + +private: + void showEvent(QShowEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void mouseReleaseEvent(QMouseEvent* e) override; + QImage loadImage(int id); + + QImage image; + QSize image_raw_size; + int currentIndex = 0; + + // Bounding boxes for each training guide step + const QRect continueBtn = {1840, 0, 320, 1080}; + QVector boundingRect { + QRect(112, 804, 618, 164), + continueBtn, + continueBtn, + QRect(1641, 558, 210, 313), + QRect(1662, 528, 184, 108), + continueBtn, + QRect(1814, 621, 211, 170), + QRect(1350, 0, 497, 755), + QRect(1540, 386, 468, 238), + QRect(112, 804, 1126, 164), + QRect(1598, 199, 316, 333), + continueBtn, + QRect(1364, 90, 796, 990), + continueBtn, + QRect(1593, 114, 318, 853), + QRect(1379, 511, 391, 243), + continueBtn, + continueBtn, + QRect(630, 804, 626, 164), + QRect(108, 804, 426, 164), + }; + + const QString img_path = "../assets/training/"; + QElapsedTimer click_timer; + +signals: + void completedTraining(); +}; + + +class TermsPage : public QFrame { + Q_OBJECT + +public: + explicit TermsPage(QWidget *parent = 0) : QFrame(parent) {} + +public slots: + void enableAccept(); + +private: + void showEvent(QShowEvent *event) override; + + QPushButton *accept_btn; + +signals: + void acceptedTerms(); + void declinedTerms(); +}; + +class DeclinePage : public QFrame { + Q_OBJECT + +public: + explicit DeclinePage(QWidget *parent = 0) : QFrame(parent) {} + +private: + void showEvent(QShowEvent *event) override; + +signals: + void getBack(); +}; + +class OnboardingWindow : public QStackedWidget { + Q_OBJECT + +public: + explicit OnboardingWindow(QWidget *parent = 0); + inline void showTrainingGuide() { setCurrentIndex(1); } + inline bool completed() const { return accepted_terms && training_done; } + +private: + void updateActiveScreen(); + + Params params; + bool accepted_terms = false, training_done = false; + +signals: + void onboardingDone(); +}; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc new file mode 100755 index 0000000..b5b6927 --- /dev/null +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -0,0 +1,529 @@ +#include "selfdrive/ui/qt/offroad/settings.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "selfdrive/ui/qt/network/networking.h" + +#include "common/params.h" +#include "common/watchdog.h" +#include "common/util.h" +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" +#include "selfdrive/ui/qt/widgets/ssh_keys.h" +#include "selfdrive/ui/qt/widgets/toggle.h" +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/qt_window.h" + +#include "selfdrive/frogpilot/navigation/ui/navigation_settings.h" +#include "selfdrive/frogpilot/ui/control_settings.h" +#include "selfdrive/frogpilot/ui/vehicle_settings.h" +#include "selfdrive/frogpilot/ui/visual_settings.h" + +TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { + // param, title, desc, icon + std::vector> toggle_defs{ + { + "OpenpilotEnabledToggle", + tr("Enable openpilot"), + tr("Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off."), + "../assets/offroad/icon_openpilot.png", + }, + { + "ExperimentalLongitudinalEnabled", + tr("openpilot Longitudinal Control (Alpha)"), + QString("%1

%2") + .arg(tr("WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).")) + .arg(tr("On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " + "Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.")), + "../assets/offroad/icon_speed_limit.png", + }, + { + "ExperimentalMode", + tr("Experimental Mode"), + "", + "../assets/img_experimental_white.svg", + }, + { + "DisengageOnAccelerator", + tr("Disengage on Accelerator Pedal"), + tr("When enabled, pressing the accelerator pedal will disengage openpilot."), + "../assets/offroad/icon_disengage_on_accelerator.svg", + }, + { + "IsLdwEnabled", + tr("Enable Lane Departure Warnings"), + tr("Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."), + "../assets/offroad/icon_warning.png", + }, + { + "RecordFront", + tr("Record and Upload Driver Camera"), + tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), + "../assets/offroad/icon_monitoring.png", + }, + { + "IsMetric", + tr("Use Metric System"), + tr("Display speed in km/h instead of mph."), + "../assets/offroad/icon_metric.png", + }, +#ifdef ENABLE_MAPS + { + "NavSettingTime24h", + tr("Show ETA in 24h Format"), + tr("Use 24h format instead of am/pm"), + "../assets/offroad/icon_metric.png", + }, + { + "NavSettingLeftSide", + tr("Show Map on Left Side of UI"), + tr("Show map on left side when in split screen view."), + "../assets/offroad/icon_road.png", + }, +#endif + }; + + + std::vector longi_button_texts{tr("Aggressive"), tr("Standard"), tr("Relaxed")}; + long_personality_setting = new ButtonParamControl("LongitudinalPersonality", tr("Driving Personality"), + tr("Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " + "In relaxed mode openpilot will stay further away from lead cars."), + "../assets/offroad/icon_speed_limit.png", + longi_button_texts); + for (auto &[param, title, desc, icon] : toggle_defs) { + auto toggle = new ParamControl(param, title, desc, icon, this); + + bool locked = params.getBool((param + "Lock").toStdString()); + toggle->setEnabled(!locked); + + addItem(toggle); + toggles[param.toStdString()] = toggle; + + // insert longitudinal personality after NDOG toggle + if (param == "DisengageOnAccelerator" && !params.getInt("AdjustablePersonalities")) { + addItem(long_personality_setting); + } + } + + // Toggles with confirmation dialogs + toggles["ExperimentalMode"]->setActiveIcon("../assets/img_experimental.svg"); + toggles["ExperimentalMode"]->setConfirmation(true, true); + toggles["ExperimentalLongitudinalEnabled"]->setConfirmation(true, false); + + connect(toggles["ExperimentalLongitudinalEnabled"], &ToggleControl::toggleFlipped, [=]() { + updateToggles(); + }); + + connect(toggles["IsMetric"], &ToggleControl::toggleFlipped, [=]() { + updateMetric(); + }); +} + +void TogglesPanel::expandToggleDescription(const QString ¶m) { + toggles[param.toStdString()]->showDescription(); +} + +void TogglesPanel::showEvent(QShowEvent *event) { + updateToggles(); +} + +void TogglesPanel::updateToggles() { + auto experimental_mode_toggle = toggles["ExperimentalMode"]; + auto op_long_toggle = toggles["ExperimentalLongitudinalEnabled"]; + const QString e2e_description = QString("%1
" + "

%2


" + "%3
" + "

%4


" + "%5
" + "

%6


" + "%7") + .arg(tr("openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. Experimental features are listed below:")) + .arg(tr("End-to-End Longitudinal Control")) + .arg(tr("Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " + "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " + "mistakes should be expected.")) + .arg(tr("Navigate on openpilot")) + .arg(tr("When navigation has a destination, openpilot will input the map information into the model. This provides useful context for the model and allows openpilot to keep left or right " + "appropriately at forks/exits. Lane change behavior is unchanged and still activated by the driver. This is an alpha quality feature; mistakes should be expected, particularly around " + "exits and forks. These mistakes can include unintended laneline crossings, late exit taking, driving towards dividing barriers in the gore areas, etc.")) + .arg(tr("New Driving Visualization")) + .arg(tr("The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. " + "When a navigation destination is set and the driving model is using it as input, the driving path on the map will turn green.")); + + const bool is_release = params.getBool("IsReleaseBranch"); + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + if (!CP.getExperimentalLongitudinalAvailable() || is_release) { + params.remove("ExperimentalLongitudinalEnabled"); + } + op_long_toggle->setVisible(CP.getExperimentalLongitudinalAvailable() && !is_release); + if (hasLongitudinalControl(CP)) { + // normal description and toggle + experimental_mode_toggle->setEnabled(!params.getBool("ConditionalExperimental")); + experimental_mode_toggle->setDescription(e2e_description); + long_personality_setting->setEnabled(true); + } else { + // no long for now + experimental_mode_toggle->setEnabled(false); + long_personality_setting->setEnabled(false); + params.remove("ExperimentalMode"); + + const QString unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control."); + + QString long_desc = unavailable + " " + \ + tr("openpilot longitudinal control may come in a future update."); + if (CP.getExperimentalLongitudinalAvailable()) { + if (is_release) { + long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches."); + } else { + long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode."); + } + } + experimental_mode_toggle->setDescription("" + long_desc + "

" + e2e_description); + } + + experimental_mode_toggle->refresh(); + } else { + experimental_mode_toggle->setDescription(e2e_description); + op_long_toggle->setVisible(false); + } +} + +DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { + setSpacing(50); + addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A")))); + addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str())); + + // offroad-only buttons + + auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"), + tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)")); + connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); + addItem(dcamBtn); + + auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); + connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); + connect(resetCalibBtn, &ButtonControl::clicked, [&]() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { + params.remove("CalibrationParams"); + params.remove("LiveTorqueParameters"); + } + }); + addItem(resetCalibBtn); + + if (!params.getBool("Passive")) { + auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of openpilot")); + connect(retrainingBtn, &ButtonControl::clicked, [=]() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { + emit reviewTrainingGuide(); + } + }); + addItem(retrainingBtn); + } + + if (Hardware::TICI()) { + auto regulatoryBtn = new ButtonControl(tr("Regulatory"), tr("VIEW"), ""); + connect(regulatoryBtn, &ButtonControl::clicked, [=]() { + const std::string txt = util::read_file("../assets/offroad/fcc.html"); + ConfirmationDialog::rich(QString::fromStdString(txt), this); + }); + addItem(regulatoryBtn); + } + + auto translateBtn = new ButtonControl(tr("Change Language"), tr("CHANGE"), ""); + connect(translateBtn, &ButtonControl::clicked, [=]() { + QMap langs = getSupportedLanguages(); + QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), langs.key(uiState()->language), this); + if (!selection.isEmpty()) { + // put language setting, exit Qt UI, and trigger fast restart + params.put("LanguageSetting", langs[selection].toStdString()); + qApp->exit(18); + watchdog_kick(0); + } + }); + addItem(translateBtn); + + // Delete driving footage button + auto deleteFootageBtn = new ButtonControl(tr("Delete Driving Data"), tr("DELETE"), tr("This button provides a swift and secure way to permanently delete all " + "stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space.") + ); + connect(deleteFootageBtn, &ButtonControl::clicked, [this]() { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to permanently delete all of your driving footage and data?"), tr("Delete"), this)) return; + std::thread([&] { + std::system("rm -rf /data/media/0/realdata"); + }).detach(); + }); + addItem(deleteFootageBtn); + + // Panda flashing button + auto flashPandaBtn = new ButtonControl(tr("Flash Panda"), tr("FLASH"), "Use this button to troubleshoot and update the Panda device's firmware."); + connect(flashPandaBtn, &ButtonControl::clicked, [this]() { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to flash the Panda?"), tr("Flash"), this)) return; + QProcess process; + // Get Panda type + SubMaster &sm = *(uiState()->sm); + auto pandaStates = sm["pandaStates"].getPandaStates(); + // Choose recovery script based on Panda type + if (pandaStates.size() != 0) { + auto pandaType = pandaStates[0].getPandaType(); + bool isRedPanda = (pandaType == cereal::PandaState::PandaType::RED_PANDA || + pandaType == cereal::PandaState::PandaType::RED_PANDA_V2); + QString recoveryScript = isRedPanda ? "./recover.sh" : "./recover.py"; + // Run recovery script and flash Panda + process.setWorkingDirectory("/data/openpilot/panda/board"); + process.start("/bin/sh", QStringList{"-c", recoveryScript}); + process.waitForFinished(); + } + // Run the killall script as a redundancy + process.setWorkingDirectory("/data/openpilot/panda"); + process.start("/bin/sh", QStringList{"-c", "pkill -f boardd; PYTHONPATH=.. python -c \"from panda import Panda; Panda().flash()\""}); + process.waitForFinished(); + Hardware::reboot(); + }); + addItem(flashPandaBtn); + + QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + for (auto btn : findChildren()) { + btn->setEnabled(offroad); + } + }); + + // power buttons + QHBoxLayout *power_layout = new QHBoxLayout(); + power_layout->setSpacing(30); + + QPushButton *reboot_btn = new QPushButton(tr("Reboot")); + reboot_btn->setObjectName("reboot_btn"); + power_layout->addWidget(reboot_btn); + QObject::connect(reboot_btn, &QPushButton::clicked, this, &DevicePanel::reboot); + + QPushButton *poweroff_btn = new QPushButton(tr("Power Off")); + poweroff_btn->setObjectName("poweroff_btn"); + power_layout->addWidget(poweroff_btn); + QObject::connect(poweroff_btn, &QPushButton::clicked, this, &DevicePanel::poweroff); + + if (!Hardware::PC()) { + connect(uiState(), &UIState::offroadTransition, poweroff_btn, &QPushButton::setVisible); + } + + setStyleSheet(R"( + #reboot_btn { height: 120px; border-radius: 15px; background-color: #393939; } + #reboot_btn:pressed { background-color: #4a4a4a; } + #poweroff_btn { height: 120px; border-radius: 15px; background-color: #E22C2C; } + #poweroff_btn:pressed { background-color: #FF2424; } + )"); + addItem(power_layout); +} + +void DevicePanel::updateCalibDescription() { + QString desc = + tr("openpilot requires the device to be mounted within 4° left or right and " + "within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required."); + std::string calib_bytes = params.get("CalibrationParams"); + if (!calib_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size())); + auto calib = cmsg.getRoot().getLiveCalibration(); + if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) { + double pitch = calib.getRpyCalib()[1] * (180 / M_PI); + double yaw = calib.getRpyCalib()[2] * (180 / M_PI); + desc += tr(" Your device is pointed %1° %2 and %3° %4.") + .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"), + QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right")); + } + } catch (kj::Exception) { + qInfo() << "invalid CalibrationParams"; + } + } + qobject_cast(sender())->setDescription(desc); +} + +void DevicePanel::reboot() { + if (!uiState()->engaged()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), tr("Reboot"), this)) { + // Check engaged again in case it changed while the dialog was open + if (!uiState()->engaged()) { + params.putBool("DoReboot", true); + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Reboot"), this); + } +} + +void DevicePanel::poweroff() { + if (!uiState()->engaged()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), tr("Power Off"), this)) { + // Check engaged again in case it changed while the dialog was open + if (!uiState()->engaged()) { + params.putBool("DoShutdown", true); + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Power Off"), this); + } +} + +void SettingsWindow::showEvent(QShowEvent *event) { + setCurrentPanel(0); +} + +void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { + panel_widget->setCurrentIndex(index); + nav_btns->buttons()[index]->setChecked(true); + if (!param.isEmpty()) { + emit expandToggleDescription(param); + } +} + +SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { + + // setup two main layouts + sidebar_widget = new QWidget; + QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget); + sidebar_layout->setMargin(0); + panel_widget = new QStackedWidget(); + + // close button + QPushButton *close_btn = new QPushButton(tr("← Back")); + close_btn->setStyleSheet(R"( + QPushButton { + font-size: 50px; + padding-bottom: 0px; + border 1px grey solid; + border-radius: 25px; + background-color: #292929; + font-weight: 500; + } + QPushButton:pressed { + background-color: #3B3B3B; + } + )"); + close_btn->setFixedSize(300, 125); + sidebar_layout->addSpacing(10); + sidebar_layout->addWidget(close_btn, 0, Qt::AlignRight); + QObject::connect(close_btn, &QPushButton::clicked, [this]() { + if (frogPilotTogglesOpen) { + frogPilotTogglesOpen = false; + this->closeParentToggle(); + } else { + this->closeSettings(); + } + }); + + // setup panels + DevicePanel *device = new DevicePanel(this); + QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); + QObject::connect(device, &DevicePanel::showDriverView, this, &SettingsWindow::showDriverView); + + TogglesPanel *toggles = new TogglesPanel(this); + QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + QObject::connect(toggles, &TogglesPanel::updateMetric, this, &SettingsWindow::updateMetric); + + // FrogPilotControlsPanel *frogpilotControls = new FrogPilotControlsPanel(this); + // QObject::connect(frogpilotControls, &FrogPilotControlsPanel::closeParentToggle, this, [this]() {frogPilotTogglesOpen = false;}); + // QObject::connect(frogpilotControls, &FrogPilotControlsPanel::openParentToggle, this, [this]() {frogPilotTogglesOpen = true;}); + + FrogPilotVisualsPanel *frogpilotVisuals = new FrogPilotVisualsPanel(this); + QObject::connect(frogpilotVisuals, &FrogPilotVisualsPanel::closeParentToggle, this, [this]() {frogPilotTogglesOpen = false;}); + QObject::connect(frogpilotVisuals, &FrogPilotVisualsPanel::openParentToggle, this, [this]() {frogPilotTogglesOpen = true;}); + + QList> panels = { + {tr("Device"), device}, + {tr("Network"), new Networking(this)}, + {tr("Toggles"), toggles}, + {tr("Software"), new SoftwarePanel(this)}, + // {tr("Controls"), frogpilotControls}, + {tr("Navigation"), new FrogPilotNavigationPanel(this)}, + {tr("Vehicles"), new FrogPilotVehiclesPanel(this)}, + {tr("Visuals"), frogpilotVisuals}, + }; + + nav_btns = new QButtonGroup(this); + for (auto &[name, panel] : panels) { + QPushButton *btn = new QPushButton(name); + btn->setCheckable(true); + btn->setChecked(nav_btns->buttons().size() == 0); + btn->setStyleSheet(R"( + QPushButton { + color: grey; + border: none; + background: none; + font-size: 65px; + font-weight: 500; + } + QPushButton:checked { + color: white; + } + QPushButton:pressed { + color: #ADADAD; + } + )"); + btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + nav_btns->addButton(btn); + sidebar_layout->addWidget(btn, 0, Qt::AlignRight); + + const int lr_margin = name != tr("Network") ? 50 : 0; // Network panel handles its own margins + panel->setContentsMargins(lr_margin, 25, lr_margin, 25); + + ScrollView *panel_frame = new ScrollView(panel, this); + panel_widget->addWidget(panel_frame); + + if (name == tr("Controls") || name == tr("Visuals")) { + QScrollBar *scrollbar = panel_frame->verticalScrollBar(); + + QObject::connect(scrollbar, &QScrollBar::valueChanged, this, [this](int value) { + if (!frogPilotTogglesOpen) { + previousScrollPosition = value; + } + }); + + QObject::connect(scrollbar, &QScrollBar::rangeChanged, this, [this, panel_frame]() { + panel_frame->restorePosition(previousScrollPosition); + }); + } + + QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { + previousScrollPosition = 0; + btn->setChecked(true); + panel_widget->setCurrentWidget(w); + }); + } + sidebar_layout->setContentsMargins(50, 50, 100, 50); + + // main settings layout, sidebar + main panel + QHBoxLayout *main_layout = new QHBoxLayout(this); + + sidebar_widget->setFixedWidth(500); + main_layout->addWidget(sidebar_widget); + main_layout->addWidget(panel_widget); + + setStyleSheet(R"( + * { + color: white; + font-size: 50px; + } + SettingsWindow { + background-color: black; + } + QStackedWidget, ScrollView { + background-color: #292929; + border-radius: 30px; + } + )"); +} diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h new file mode 100755 index 0000000..0a028a2 --- /dev/null +++ b/selfdrive/ui/qt/offroad/settings.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/controls.h" + +// ********** settings window + top-level panels ********** +class SettingsWindow : public QFrame { + Q_OBJECT + +public: + explicit SettingsWindow(QWidget *parent = 0); + void setCurrentPanel(int index, const QString ¶m = ""); + +protected: + void showEvent(QShowEvent *event) override; + +signals: + void closeSettings(); + void reviewTrainingGuide(); + void showDriverView(); + void expandToggleDescription(const QString ¶m); + + // FrogPilot signals + void closeParentToggle(); + void updateMetric(); +private: + QPushButton *sidebar_alert_widget; + QWidget *sidebar_widget; + QButtonGroup *nav_btns; + QStackedWidget *panel_widget; + + // FrogPilot variables + bool frogPilotTogglesOpen; + int previousScrollPosition; +}; + +class DevicePanel : public ListWidget { + Q_OBJECT +public: + explicit DevicePanel(SettingsWindow *parent); +signals: + void reviewTrainingGuide(); + void showDriverView(); + +private slots: + void poweroff(); + void reboot(); + void updateCalibDescription(); + +private: + Params params; +}; + +class TogglesPanel : public ListWidget { + Q_OBJECT +public: + explicit TogglesPanel(SettingsWindow *parent); + void showEvent(QShowEvent *event) override; + +signals: + // FrogPilot signals + void updateMetric(); + +public slots: + void expandToggleDescription(const QString ¶m); + +private: + Params params; + std::map toggles; + ButtonParamControl *long_personality_setting; + + void updateToggles(); +}; + +class SoftwarePanel : public ListWidget { + Q_OBJECT +public: + explicit SoftwarePanel(QWidget* parent = nullptr); + +private: + void showEvent(QShowEvent *event) override; + void updateLabels(); + void checkForUpdates(); + + bool is_onroad = false; + + QLabel *onroadLbl; + LabelControl *versionLbl; + ButtonControl *errorLogBtn; + ButtonControl *installBtn; + ButtonControl *downloadBtn; + ButtonControl *targetBranchBtn; + + Params params; + ParamWatcher *fs_watch; + + // FrogPilot variables + void automaticUpdate(); + + ButtonControl *updateTime; + + int deviceShutdown; + int schedule; + int time; +}; diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc new file mode 100755 index 0000000..bfe1803 --- /dev/null +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -0,0 +1,282 @@ +#include "selfdrive/ui/qt/offroad/settings.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "common/params.h" +#include "common/util.h" +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/controls.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "system/hardware/hw.h" + + +void SoftwarePanel::checkForUpdates() { + std::system("pkill -SIGUSR1 -f selfdrive.updated"); +} + +SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { + onroadLbl = new QLabel(tr("Updates are only downloaded while the car is off.")); + onroadLbl->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; padding-top: 30px; padding-bottom: 30px;"); + addItem(onroadLbl); + + // current version + versionLbl = new LabelControl(tr("Current Version"), ""); + addItem(versionLbl); + + // update scheduler + std::vector scheduleOptions{tr("Manually"), tr("Daily"), tr("Weekly")}; + ButtonParamControl *preferredSchedule = new ButtonParamControl("UpdateSchedule", tr("Update Scheduler"), + tr("Choose the update frequency for FrogPilot's automatic updates.\n\n" + "This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' experience.\n\n" + "Weekly updates start at midnight every Sunday."), + "", + scheduleOptions); + schedule = params.getInt("UpdateSchedule"); + addItem(preferredSchedule); + + updateTime = new ButtonControl(tr("Update Time"), tr("SELECT")); + QStringList hours; + for (int h = 0; h < 24; h++) { + int displayHour = (h % 12 == 0) ? 12 : h % 12; + QString meridiem = (h < 12) ? "AM" : "PM"; + hours << QString("%1:00 %2").arg(displayHour).arg(meridiem) + << QString("%1:30 %2").arg(displayHour).arg(meridiem); + } + QObject::connect(updateTime, &ButtonControl::clicked, [=]() { + int currentHourIndex = params.getInt("UpdateTime"); + QString currentHourLabel = hours[currentHourIndex]; + + QString selection = MultiOptionDialog::getSelection(tr("Select a time to automatically update"), hours, currentHourLabel, this); + if (!selection.isEmpty()) { + int selectedHourIndex = hours.indexOf(selection); + params.putInt("UpdateTime", selectedHourIndex); + updateTime->setValue(selection); + } + }); + time = params.getInt("UpdateTime"); + deviceShutdown = params.getInt("DeviceShutdown") * 3600; + updateTime->setValue(hours[time]); + addItem(updateTime); + + // download update btn + downloadBtn = new ButtonControl(tr("Download"), tr("CHECK")); + connect(downloadBtn, &ButtonControl::clicked, [=]() { + downloadBtn->setEnabled(false); + if (downloadBtn->text() == tr("CHECK")) { + checkForUpdates(); + } else { + std::system("pkill -SIGHUP -f selfdrive.updated"); + } + }); + addItem(downloadBtn); + + // install update btn + installBtn = new ButtonControl(tr("Install Update"), tr("INSTALL")); + connect(installBtn, &ButtonControl::clicked, [=]() { + installBtn->setEnabled(false); + params.putBool("DoReboot", true); + }); + addItem(installBtn); + + // branch selecting + targetBranchBtn = new ButtonControl(tr("Target Branch"), tr("SELECT")); + connect(targetBranchBtn, &ButtonControl::clicked, [=]() { + auto current = params.get("GitBranch"); + QStringList branches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(","); + for (QString b : {current.c_str(), "devel-staging", "devel", "nightly", "master-ci", "master"}) { + auto i = branches.indexOf(b); + if (i >= 0) { + branches.removeAt(i); + branches.insert(0, b); + } + } + + QString cur = QString::fromStdString(params.get("UpdaterTargetBranch")); + QString selection = MultiOptionDialog::getSelection(tr("Select a branch"), branches, cur, this); + if (!selection.isEmpty()) { + params.put("UpdaterTargetBranch", selection.toStdString()); + targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); + checkForUpdates(); + } + }); + if (!params.getBool("IsTestedBranch")) { + addItem(targetBranchBtn); + } + + // uninstall button + auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); + connect(uninstallBtn, &ButtonControl::clicked, [&]() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), tr("Uninstall"), this)) { + params.putBool("DoUninstall", true); + } + }); + addItem(uninstallBtn); + + // error log button + errorLogBtn = new ButtonControl(tr("Error Log"), tr("VIEW"), "View the error log for debugging purposes when openpilot crashes."); + connect(errorLogBtn, &ButtonControl::clicked, [=]() { + std::string txt = util::read_file("/data/community/crashes/error.txt"); + ConfirmationDialog::rich(QString::fromStdString(txt), this); + }); + addItem(errorLogBtn); + + fs_watch = new ParamWatcher(this); + QObject::connect(fs_watch, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + schedule = params.getInt("UpdateSchedule"); + time = params.getInt("UpdateTime"); + updateLabels(); + }); + + connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + is_onroad = !offroad; + updateLabels(); + }); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &SoftwarePanel::automaticUpdate); + + updateLabels(); +} + +void SoftwarePanel::showEvent(QShowEvent *event) { + // nice for testing on PC + installBtn->setEnabled(true); + + updateLabels(); +} + +void SoftwarePanel::updateLabels() { + // add these back in case the files got removed + fs_watch->addParam("LastUpdateTime"); + fs_watch->addParam("UpdateFailedCount"); + fs_watch->addParam("UpdaterState"); + fs_watch->addParam("UpdateAvailable"); + + fs_watch->addParam("UpdateSchedule"); + fs_watch->addParam("UpdateTime"); + + if (!isVisible()) { + return; + } + + // updater only runs offroad + onroadLbl->setVisible(is_onroad); + downloadBtn->setVisible(!is_onroad); + + // download update + QString updater_state = QString::fromStdString(params.get("UpdaterState")); + bool failed = std::atoi(params.get("UpdateFailedCount").c_str()) > 0; + if (updater_state != "idle") { + downloadBtn->setEnabled(false); + downloadBtn->setValue(updater_state); + } else { + if (failed) { + downloadBtn->setText(tr("CHECK")); + downloadBtn->setValue(tr("failed to check for update")); + } else if (params.getBool("UpdaterFetchAvailable")) { + downloadBtn->setText(tr("DOWNLOAD")); + downloadBtn->setValue(tr("update available")); + } else { + QString lastUpdate = tr("never"); + auto tm = params.get("LastUpdateTime"); + if (!tm.empty()) { + lastUpdate = timeAgo(QDateTime::fromString(QString::fromStdString(tm + "Z"), Qt::ISODate)); + } + downloadBtn->setText(tr("CHECK")); + downloadBtn->setValue(tr("up to date, last checked %1").arg(lastUpdate)); + } + downloadBtn->setEnabled(true); + } + targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); + + // current + new versions + versionLbl->setText(QString::fromStdString(params.get("UpdaterCurrentDescription"))); + versionLbl->setDescription(QString::fromStdString(params.get("UpdaterCurrentReleaseNotes"))); + + installBtn->setVisible(!is_onroad && params.getBool("UpdateAvailable")); + installBtn->setValue(QString::fromStdString(params.get("UpdaterNewDescription"))); + installBtn->setDescription(QString::fromStdString(params.get("UpdaterNewReleaseNotes"))); + + updateTime->setVisible(params.getInt("UpdateSchedule")); + + update(); +} + +void SoftwarePanel::automaticUpdate() { + static int timer = 0; + static std::chrono::system_clock::time_point lastOffroadTime; + + if (!is_onroad) { + timer = (timer == 0) ? 0 : std::chrono::duration_cast(std::chrono::system_clock::now() - lastOffroadTime).count(); + lastOffroadTime = std::chrono::system_clock::now(); + } else { + timer = 0; + } + + bool isWifiConnected = (*uiState()->sm)["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; + if (schedule == 0 || is_onroad || !isWifiConnected || isVisible()) return; + + static bool isDownloadCompleted = false; + if (isDownloadCompleted && params.getBool("UpdateAvailable")) { + params.putBool(timer > deviceShutdown ? "DoShutdown" : "DoReboot", true); + return; + } + + int updateHour = time / 2; + int updateMinute = (time % 2) * 30; + + if (updateHour >= 1 && updateHour <= 11 && time >= 24) { + updateHour += 12; + } else if (updateHour == 12 && time < 24) { + updateHour = 0; + } + + std::time_t currentTimeT = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm now = *std::localtime(¤tTimeT); + + static bool updateCheckedToday = false; + static int lastCheckedDay = now.tm_yday; + if (lastCheckedDay != now.tm_yday) { + updateCheckedToday = false; + lastCheckedDay = now.tm_yday; + } + + if (now.tm_hour != updateHour || now.tm_min < updateMinute) return; + + std::string lastUpdateStr = params.get("Updated"); + std::tm lastUpdate = {}; + std::istringstream iss(lastUpdateStr); + + if (iss >> std::get_time(&lastUpdate, "%Y-%m-%d %H:%M:%S")) { + lastUpdate.tm_year -= 1900; + lastUpdate.tm_mon -= 1; + } + std::time_t lastUpdateTimeT = std::mktime(&lastUpdate); + + if (lastUpdate.tm_yday == now.tm_yday) { + return; + } + + if (!isDownloadCompleted) { + std::chrono::hours durationSinceLastUpdate = std::chrono::duration_cast(std::chrono::system_clock::now() - std::chrono::system_clock::from_time_t(lastUpdateTimeT)); + int daysSinceLastUpdate = durationSinceLastUpdate.count() / 24; + + if ((schedule == 1 && daysSinceLastUpdate >= 1) || (schedule == 2 && (now.tm_yday / 7) != (std::localtime(&lastUpdateTimeT)->tm_yday / 7))) { + if (downloadBtn->text() == tr("CHECK") && !updateCheckedToday) { + checkForUpdates(); + updateCheckedToday = true; + } else { + std::system("pkill -SIGHUP -f selfdrive.updated"); + isDownloadCompleted = true; + } + } + } +} diff --git a/selfdrive/ui/qt/offroad/text_view.qml b/selfdrive/ui/qt/offroad/text_view.qml old mode 100644 new mode 100755 diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc new file mode 100755 index 0000000..d53218d --- /dev/null +++ b/selfdrive/ui/qt/onroad.cc @@ -0,0 +1,1704 @@ +#include "selfdrive/ui/qt/onroad.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "common/swaglog.h" +#include "common/timing.h" +#include "selfdrive/ui/qt/util.h" +#ifdef ENABLE_MAPS +#include "selfdrive/ui/qt/maps/map_helpers.h" +#include "selfdrive/ui/qt/maps/map_panel.h" +#endif + +static void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity) { + p.setRenderHint(QPainter::Antialiasing); + p.setOpacity(1.0); // bg dictates opacity of ellipse + p.setPen(Qt::NoPen); + p.setBrush(bg); + p.drawEllipse(center, btn_size / 2, btn_size / 2); + p.setOpacity(opacity); + p.drawPixmap(center - QPoint(img.width() / 2, img.height() / 2), img); + p.setOpacity(1.0); +} + +// static void drawIconRotate(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrush &bg, float opacity, const int angle) { +// p.setRenderHint(QPainter::Antialiasing); +// p.setOpacity(1.0); // bg dictates opacity of ellipse +// p.setPen(Qt::NoPen); +// p.setBrush(bg); +// p.drawEllipse(center, btn_size / 2, btn_size / 2); +// p.save(); +// p.translate(center); +// p.rotate(-angle); +// p.setOpacity(opacity); +// p.drawPixmap(-QPoint(img.width() / 2, img.height() / 2), img); +// p.setOpacity(1.0); +// p.restore(); +// } + +OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent), scene(uiState()->scene) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setMargin(UI_BORDER_SIZE); + QStackedLayout *stacked_layout = new QStackedLayout; + stacked_layout->setStackingMode(QStackedLayout::StackAll); + main_layout->addLayout(stacked_layout); + + nvg = new AnnotatedCameraWidget(VISION_STREAM_ROAD, this); + + QWidget * split_wrapper = new QWidget; + split = new QHBoxLayout(split_wrapper); + split->setContentsMargins(0, 0, 0, 0); + split->setSpacing(0); + split->addWidget(nvg); + + if (getenv("DUAL_CAMERA_VIEW")) { + CameraWidget *arCam = new CameraWidget("camerad", VISION_STREAM_ROAD, true, this); + split->insertWidget(0, arCam); + } + + if (getenv("MAP_RENDER_VIEW")) { + CameraWidget *map_render = new CameraWidget("navd", VISION_STREAM_MAP, false, this); + split->insertWidget(0, map_render); + } + + stacked_layout->addWidget(split_wrapper); + + alerts = new OnroadAlerts(this); + alerts->setAttribute(Qt::WA_TransparentForMouseEvents, true); + stacked_layout->addWidget(alerts); + + // setup stacking order + alerts->raise(); + + setAttribute(Qt::WA_OpaquePaintEvent); + QObject::connect(uiState(), &UIState::uiUpdate, this, &OnroadWindow::updateState); + QObject::connect(uiState(), &UIState::offroadTransition, this, &OnroadWindow::offroadTransition); + QObject::connect(uiState(), &UIState::primeChanged, this, &OnroadWindow::primeChanged); + + QObject::connect(&clickTimer, &QTimer::timeout, this, [this]() { + clickTimer.stop(); + QMouseEvent *event = new QMouseEvent(QEvent::MouseButtonPress, timeoutPoint, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QApplication::postEvent(this, event); + }); +} + +void OnroadWindow::updateState(const UIState &s) { + if (!s.scene.started) { + return; + } + + QColor bgColor = bg_colors[s.status]; + Alert alert = Alert::get(*(s.sm), s.scene.started_frame); + alerts->updateAlert(alert); + + if (s.scene.map_on_left) { + split->setDirection(QBoxLayout::LeftToRight); + } else { + split->setDirection(QBoxLayout::RightToLeft); + } + + nvg->updateState(s); + + if (bg != bgColor) { + // repaint border + bg = bgColor; + update(); + } +} + +void OnroadWindow::mousePressEvent(QMouseEvent* e) { + Params params = Params(); + Params paramsMemory = Params("/dev/shm/params"); + + // FrogPilot clickable widgets + bool widgetClicked = false; + + // Change cruise control increments button + QRect maxSpeedRect(7, 25, 225, 225); + bool isMaxSpeedClicked = maxSpeedRect.contains(e->pos()); + + // Hide speed button + QRect speedRect(rect().center().x() - 175, 50, 350, 350); + bool isSpeedClicked = speedRect.contains(e->pos()); + + // Speed limit offset button + const QRect speedLimitRect(7, 250, 225, 225); + const bool isSpeedLimitClicked = speedLimitRect.contains(e->pos()); + + if (isMaxSpeedClicked || isSpeedClicked || isSpeedLimitClicked) { + // Check if the click was within the max speed area + if (isMaxSpeedClicked) { + bool currentReverseCruise = !params.getBool("ReverseCruise"); + params.putBoolNonBlocking("ReverseCruise", currentReverseCruise); + if (!params.getBool("QOLControls")) { + params.putBoolNonBlocking("QOLControls", true); + } + // Check if the click was within the speed text area + } else if (isSpeedClicked) { + bool currentHideSpeed = !params.getBool("HideSpeed"); + params.putBoolNonBlocking("HideSpeed", currentHideSpeed); + if (!params.getBool("QOLVisuals")) { + params.putBoolNonBlocking("QOLVisuals", true); + } + } else { + bool currentShowSLCOffset = !params.getBool("ShowSLCOffset"); + params.putBoolNonBlocking("ShowSLCOffset", currentShowSLCOffset); + if (!params.getBool("QOLVisuals")) { + params.putBoolNonBlocking("QOLVisuals", true); + } + } + widgetClicked = true; + paramsMemory.putBoolNonBlocking("FrogPilotTogglesUpdated", true); + // If the click wasn't for anything specific, change the value of "ExperimentalMode" + } else if (scene.experimental_mode_via_screen && e->pos() != timeoutPoint) { + if (clickTimer.isActive()) { + clickTimer.stop(); + if (scene.conditional_experimental) { + int override_value = (scene.conditional_status >= 1 && scene.conditional_status <= 4) ? 0 : scene.conditional_status >= 5 ? 3 : 4; + paramsMemory.putIntNonBlocking("CEStatus", override_value); + } else { + bool experimentalMode = params.getBool("ExperimentalMode"); + params.putBoolNonBlocking("ExperimentalMode", !experimentalMode); + } + } else { + clickTimer.start(500); + } + widgetClicked = true; + } + +#ifdef ENABLE_MAPS + if (map != nullptr && !widgetClicked) { + // Switch between map and sidebar when using navigate on openpilot + bool sidebarVisible = geometry().x() > 0; + bool show_map = uiState()->scene.navigate_on_openpilot ? sidebarVisible : !sidebarVisible; + if (!scene.experimental_mode_via_screen || map->isVisible()) { + map->setVisible(show_map && !map->isVisible()); + } + } +#endif + // propagation event to parent(HomeWindow) + if (!widgetClicked) { + QWidget::mousePressEvent(e); + } +} + +void OnroadWindow::offroadTransition(bool offroad) { +#ifdef ENABLE_MAPS + if (!offroad) { + if (map == nullptr && (uiState()->hasPrime() || !MAPBOX_TOKEN.isEmpty())) { + auto m = new MapPanel(get_mapbox_settings()); + map = m; + + QObject::connect(m, &MapPanel::mapPanelRequested, this, &OnroadWindow::mapPanelRequested); + QObject::connect(nvg->map_settings_btn, &MapSettingsButton::clicked, m, &MapPanel::toggleMapSettings); + QObject::connect(nvg->map_settings_btn_bottom, &MapSettingsButton::clicked, m, &MapPanel::toggleMapSettings); + nvg->map_settings_btn->setEnabled(true); + + if (scene.full_map) { + m->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + } else { + m->setFixedWidth(topWidget(this)->width() / 2 - UI_BORDER_SIZE); + } + split->insertWidget(0, m); + + // hidden by default, made visible when navRoute is published + m->setVisible(false); + } + } +#endif + + alerts->updateAlert({}); +} + +void OnroadWindow::primeChanged(bool prime) { +#ifdef ENABLE_MAPS + if (map && (!prime && MAPBOX_TOKEN.isEmpty())) { + nvg->map_settings_btn->setEnabled(false); + nvg->map_settings_btn->setVisible(false); + map->deleteLater(); + map = nullptr; + } +#endif +} + +void OnroadWindow::paintEvent(QPaintEvent *event) { + Params paramsMemory = Params("/dev/shm/params"); + + QPainter p(this); + p.fillRect(rect(), QColor(bg.red(), bg.green(), bg.blue(), 255)); + + // Draw FPS on screen + if (scene.show_fps) { + constexpr double minAllowedFPS = 0.1; + constexpr double maxAllowedFPS = 99.9; + constexpr qint64 oneMinuteInMilliseconds = 60000; + static double avgFPS; + static double maxFPS; + static double minFPS; + static double totalFPS; + static qint64 frameCount; + + // Store the last reset time + static qint64 lastResetTime = QDateTime::currentMSecsSinceEpoch(); + const qint64 currentMillis = QDateTime::currentMSecsSinceEpoch(); + + // Reset the counter if it's been 60 seconds + if (currentMillis - lastResetTime >= oneMinuteInMilliseconds) { + avgFPS = 0; + frameCount = 0; + minFPS = maxAllowedFPS; + maxFPS = minAllowedFPS; + totalFPS = 0; + lastResetTime = currentMillis; + } + + // Update the FPS variables + fps = qBound(minAllowedFPS, fps, maxAllowedFPS); + minFPS = qMin(minFPS, fps); + maxFPS = qMax(maxFPS, fps); + frameCount++; + totalFPS += fps; + avgFPS = totalFPS / frameCount; + update(); + + // Text declarations + p.setFont(InterFont(30, QFont::DemiBold)); + p.setRenderHint(QPainter::TextAntialiasing); + p.setPen(Qt::white); + + // Construct the FPS display string + QString fpsDisplayString = QString("FPS: %1 (%2) | Min: %3 | Max: %4 | Avg: %5") + .arg(fps, 0, 'f', 2) + .arg(paramsMemory.getInt("CameraFPS")) + .arg(minFPS, 0, 'f', 2) + .arg(maxFPS, 0, 'f', 2) + .arg(avgFPS, 0, 'f', 2); + + // Calculate text positioning + const QRect currentRect = rect(); + const int textWidth = p.fontMetrics().horizontalAdvance(fpsDisplayString); + const int xPos = (currentRect.width() - textWidth) / 2; + const int yPos = currentRect.bottom() - 5; + p.drawText(xPos, yPos, fpsDisplayString); + } +} + +// ***** onroad widgets ***** + +// OnroadAlerts +void OnroadAlerts::updateAlert(const Alert &a) { + if (!alert.equal(a)) { + alert = a; + update(); + } +} + +void OnroadAlerts::paintEvent(QPaintEvent *event) { + if (alert.size == cereal::ControlsState::AlertSize::NONE || scene.show_driver_camera) { + return; + } + static std::map alert_heights = { + {cereal::ControlsState::AlertSize::SMALL, 271}, + {cereal::ControlsState::AlertSize::MID, 420}, + {cereal::ControlsState::AlertSize::FULL, height()}, + }; + int h = alert_heights[alert.size]; + + int margin = 40; + int radius = 30; + 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; + offset = 0; + } + QRect r = QRect(0 + margin, height() - h + margin - offset, width() - margin*2, h - margin*2); + + QPainter p(this); + + // draw background + gradient + p.setPen(Qt::NoPen); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + p.setBrush(QBrush(alert_colors[alert.status])); + p.drawRoundedRect(r, radius, radius); + + QLinearGradient g(0, r.y(), 0, r.bottom()); + g.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.05)); + g.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0.35)); + + p.setCompositionMode(QPainter::CompositionMode_DestinationOver); + p.setBrush(QBrush(g)); + p.drawRoundedRect(r, radius, radius); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + + // text + const QPoint c = r.center(); + p.setPen(QColor(0xff, 0xff, 0xff)); + p.setRenderHint(QPainter::TextAntialiasing); + if (alert.size == cereal::ControlsState::AlertSize::SMALL) { + p.setFont(InterFont(74, QFont::DemiBold)); + p.drawText(r, Qt::AlignCenter, alert.text1); + } else if (alert.size == cereal::ControlsState::AlertSize::MID) { + p.setFont(InterFont(88, QFont::Bold)); + p.drawText(QRect(0, c.y() - 125, width(), 150), Qt::AlignHCenter | Qt::AlignTop, alert.text1); + p.setFont(InterFont(66)); + p.drawText(QRect(0, c.y() + 21, width(), 90), Qt::AlignHCenter, alert.text2); + } else if (alert.size == cereal::ControlsState::AlertSize::FULL) { + bool l = alert.text1.length() > 15; + p.setFont(InterFont(l ? 132 : 177, QFont::Bold)); + p.drawText(QRect(0, r.y() + (l ? 240 : 270), width(), 600), Qt::AlignHCenter | Qt::TextWordWrap, alert.text1); + p.setFont(InterFont(88)); + p.drawText(QRect(0, r.height() - (l ? 361 : 420), width(), 300), Qt::AlignHCenter | Qt::TextWordWrap, alert.text2); + } +} + +// ExperimentalButton +ExperimentalButton::ExperimentalButton(QWidget *parent) : experimental_mode(false), engageable(false), QPushButton(parent), scene(uiState()->scene) { + setFixedSize(btn_size, btn_size + 10); + + engage_img = loadPixmap("../assets/img_chffr_wheel.png", {img_size, img_size}); + experimental_img = loadPixmap("../assets/img_experimental.svg", {img_size, img_size}); + QObject::connect(this, &QPushButton::clicked, this, &ExperimentalButton::changeMode); + + // Custom steering wheel images + wheelImages = { + {0, loadPixmap("../assets/img_chffr_wheel.png", {img_size, img_size})}, + {1, loadPixmap("../frogpilot/assets/wheel_images/lexus.png", {img_size, img_size})}, + {2, loadPixmap("../frogpilot/assets/wheel_images/toyota.png", {img_size, img_size})}, + {3, loadPixmap("../frogpilot/assets/wheel_images/frog.png", {img_size, img_size})}, + {4, loadPixmap("../frogpilot/assets/wheel_images/rocket.png", {img_size, img_size})}, + {5, loadPixmap("../frogpilot/assets/wheel_images/hyundai.png", {img_size, img_size})}, + {6, loadPixmap("../frogpilot/assets/wheel_images/stalin.png", {img_size, img_size})}, + {7, loadPixmap("../frogpilot/assets/wheel_images/firefox.png", {img_size, img_size})} + }; +} + +void ExperimentalButton::changeMode() { + Params paramsMemory = Params("/dev/shm/params"); + + const auto cp = (*uiState()->sm)["carParams"].getCarParams(); + bool can_change = hasLongitudinalControl(cp) && (params.getBool("ExperimentalModeConfirmed") || scene.experimental_mode_via_screen); + if (can_change) { + if (scene.conditional_experimental) { + int override_value = (scene.conditional_status >= 1 && scene.conditional_status <= 4) ? 0 : scene.conditional_status >= 5 ? 3 : 4; + paramsMemory.putIntNonBlocking("ConditionalStatus", override_value); + } else { + params.putBoolNonBlocking("ExperimentalMode", !experimental_mode); + } + } +} + +void ExperimentalButton::updateState(const UIState &s, bool leadInfo) { + const auto cs = (*s.sm)["controlsState"].getControlsState(); + bool eng = cs.getEngageable() || cs.getEnabled(); + if ((cs.getExperimentalMode() != experimental_mode) || (eng != engageable)) { + engageable = eng; + experimental_mode = cs.getExperimentalMode(); + update(); + } + + // FrogPilot variables + firefoxRandomEventTriggered = scene.current_random_event == 1; + rotatingWheel = scene.rotating_wheel; + wheelIcon = scene.wheel_icon; + + y_offset = leadInfo ? 10 : 0; + + if (firefoxRandomEventTriggered) { + static int rotationDegree = 0; + rotationDegree = (rotationDegree + 36) % 360; + steeringAngleDeg = rotationDegree; + wheelIcon = 7; + update(); + // Update the icon so the steering wheel rotates in real time + } else if (rotatingWheel && steeringAngleDeg != scene.steering_angle_deg) { + steeringAngleDeg = scene.steering_angle_deg; + update(); + } +} + +void ExperimentalButton::paintEvent(QPaintEvent *event) { + QPainter p(this); + // Steering wheel icon disabled + + // // Custom steering wheel icon + // engage_img = wheelImages[wheelIcon]; + // QPixmap img = wheelIcon ? engage_img : (experimental_mode ? experimental_img : engage_img); + + // QColor background_color = wheelIcon && !isDown() && engageable ? + // (scene.always_on_lateral_active ? QColor(10, 186, 181, 255) : + // (scene.conditional_status == 1 ? QColor(255, 246, 0, 255) : + // (experimental_mode ? QColor(218, 111, 37, 241) : + // (scene.navigate_on_openpilot ? QColor(49, 161, 238, 255) : QColor(0, 0, 0, 166))))) : + // QColor(0, 0, 0, 166); + + // if (!scene.show_driver_camera) { + // if (rotatingWheel || firefoxRandomEventTriggered) { + // drawIconRotate(p, QPoint(btn_size / 2, btn_size / 2 + y_offset), img, background_color, (isDown() || (!engageable && !scene.always_on_lateral_active)) ? 0.6 : 1.0, steeringAngleDeg); + // } else { + // drawIcon(p, QPoint(btn_size / 2, btn_size / 2 + y_offset), img, background_color, (isDown() || (!engageable && !scene.always_on_lateral_active)) ? 0.6 : 1.0); + // } + // } +} + + +// MapSettingsButton +MapSettingsButton::MapSettingsButton(QWidget *parent) : QPushButton(parent) { + setFixedSize(btn_size, btn_size + 20); + settings_img = loadPixmap("../assets/navigation/icon_directions_outlined.svg", {img_size, img_size}); + + // hidden by default, made visible if map is created (has prime or mapbox token) + setVisible(false); + setEnabled(false); +} + +void MapSettingsButton::paintEvent(QPaintEvent *event) { + QPainter p(this); + drawIcon(p, QPoint(btn_size / 2, btn_size / 2), settings_img, QColor(0, 0, 0, 166), isDown() ? 0.6 : 1.0); +} + + +// Window that shows camera view and variety of info drawn on top +AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* parent) : fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraWidget("camerad", type, true, parent), scene(uiState()->scene) { + pm = std::make_unique>({"uiDebug"}); + + main_layout = new QVBoxLayout(this); + main_layout->setMargin(UI_BORDER_SIZE); + main_layout->setSpacing(0); + + // Neokii screen recorder + QHBoxLayout *top_right_layout = new QHBoxLayout(); + top_right_layout->setSpacing(0); + + // recorder_btn = new ScreenRecorder(this); + // top_right_layout->addWidget(recorder_btn); + + // experimental_btn = new ExperimentalButton(this); + // top_right_layout->addWidget(experimental_btn); + + main_layout->addLayout(top_right_layout, 0); + main_layout->setAlignment(top_right_layout, Qt::AlignTop | Qt::AlignRight); + + map_settings_btn = new MapSettingsButton(this); + main_layout->addWidget(map_settings_btn, 0, Qt::AlignBottom | Qt::AlignRight); + + dm_img = loadPixmap("../assets/img_driver_face.png", {img_size + 5, img_size + 5}); + + // Initialize FrogPilot widgets + initializeFrogPilotWidgets(); +} + +void AnnotatedCameraWidget::updateState(const UIState &s) { + const int SET_SPEED_NA = 255; + const SubMaster &sm = *(s.sm); + + const bool cs_alive = sm.alive("controlsState"); + const bool nav_alive = sm.alive("navInstruction") && sm["navInstruction"].getValid(); + const auto cs = sm["controlsState"].getControlsState(); + const auto car_state = sm["carState"].getCarState(); + const auto nav_instruction = sm["navInstruction"].getNavInstruction(); + + // Handle older routes where vCruiseCluster is not set + float v_cruise = cs.getVCruiseCluster() == 0.0 ? cs.getVCruise() : cs.getVCruiseCluster(); + setSpeed = cs_alive ? v_cruise : SET_SPEED_NA; + is_cruise_set = setSpeed > 0 && (int)setSpeed != SET_SPEED_NA; + if (is_cruise_set && !s.scene.is_metric) { + setSpeed *= KM_TO_MILE; + } + + // Handle older routes where vEgoCluster is not set + v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0; + float v_ego = v_ego_cluster_seen ? car_state.getVEgoCluster() : car_state.getVEgo(); + speed = cs_alive ? std::max(0.0, v_ego) : 0.0; + speed *= s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH; + + auto speed_limit_sign = nav_instruction.getSpeedLimitSign(); + speedLimit = slcOverridden ? slcOverriddenSpeed : slcSpeedLimit ? slcSpeedLimit : nav_alive ? nav_instruction.getSpeedLimit() : 0.0; + speedLimit *= (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH); + if (slcSpeedLimit && !slcOverridden) { + speedLimit = speedLimit - (showSLCOffset ? slcSpeedLimitOffset : 0); + } + + has_us_speed_limit = (nav_alive && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::MUTCD) || (slcSpeedLimit && !useViennaSLCSign); + has_eu_speed_limit = (nav_alive && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA) || (slcSpeedLimit && useViennaSLCSign); + is_metric = s.scene.is_metric; + speedUnit = s.scene.is_metric ? tr("km/h") : tr("mph"); + hideBottomIcons = (cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE || customSignals && (turnSignalLeft || turnSignalRight)) || showDriverCamera; + status = s.status; + + // update engageability/experimental mode button + experimental_btn->updateState(s, leadInfo); + + // update DM icon + auto dm_state = sm["driverMonitoringState"].getDriverMonitoringState(); + dmActive = dm_state.getIsActiveMode(); + rightHandDM = dm_state.getIsRHD(); + // DM icon transition + dm_fade_state = std::clamp(dm_fade_state+0.2*(0.5-dmActive), 0.0, 1.0); + + // hide map settings button for alerts and flip for right hand DM + if (map_settings_btn->isEnabled()) { + map_settings_btn->setVisible(!hideBottomIcons && compass); + main_layout->setAlignment(map_settings_btn, (rightHandDM ? Qt::AlignLeft : Qt::AlignRight) | (compass ? Qt::AlignTop : Qt::AlignBottom)); + } +} + +void AnnotatedCameraWidget::drawHud(QPainter &p) { + p.save(); + + // Header gradient + QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT); + bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45)); + bg.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0)); + p.fillRect(0, 0, width(), UI_HEADER_HEIGHT, bg); + + QString speedLimitStr = (speedLimit > 1) ? QString::number(std::nearbyint(speedLimit)) : "–"; + QString speedLimitOffsetStr = (showSLCOffset) ? "+" + QString::number(std::nearbyint(slcSpeedLimitOffset)) : "–"; + QString speedStr = QString::number(std::nearbyint(speed)); + QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(setSpeed - cruiseAdjustment)) : "–"; + + // Draw outer box + border to contain set speed and speed limit + const int sign_margin = 12; + const int us_sign_height = 186; + const int eu_sign_size = 176; + + const QSize default_size = {172, 204}; + QSize set_speed_size = default_size; + if (is_metric || has_eu_speed_limit) set_speed_size.rwidth() = 200; + if (has_us_speed_limit && speedLimitStr.size() >= 3) set_speed_size.rwidth() = 223; + + if (has_us_speed_limit) set_speed_size.rheight() += us_sign_height + sign_margin; + else if (has_eu_speed_limit) set_speed_size.rheight() += eu_sign_size + sign_margin; + + int top_radius = 32; + int bottom_radius = has_eu_speed_limit ? 100 : 32; + + QRect set_speed_rect(QPoint(60 + (default_size.width() - set_speed_size.width()) / 2, 45), set_speed_size); + if (is_cruise_set && cruiseAdjustment) { + float transition = qBound(0.0f, 4.0f * (cruiseAdjustment / setSpeed), 1.0f); + QColor min = whiteColor(75); + QColor max = redColor(75); + + p.setPen(QPen(QColor::fromRgbF( + min.redF() + transition * (max.redF() - min.redF()), + min.greenF() + transition * (max.greenF() - min.greenF()), + min.blueF() + transition * (max.blueF() - min.blueF()) + ), 6)); + } else if (reverseCruise) { + p.setPen(QPen(QColor(0, 150, 255), 6)); + } else { + p.setPen(QPen(whiteColor(75), 6)); + } + p.setBrush(blackColor(166)); + drawRoundedRect(p, set_speed_rect, top_radius, top_radius, bottom_radius, bottom_radius); + + // Draw MAX + QColor max_color = QColor(0x80, 0xd8, 0xa6, 0xff); + QColor set_speed_color = whiteColor(); + if (is_cruise_set) { + if (status == STATUS_DISENGAGED) { + max_color = whiteColor(); + } else if (status == STATUS_OVERRIDE) { + max_color = QColor(0x91, 0x9b, 0x95, 0xff); + } else if (speedLimit > 0) { + auto interp_color = [=](QColor c1, QColor c2, QColor c3) { + return speedLimit > 0 ? interpColor(setSpeed, {speedLimit + 5, speedLimit + 15, speedLimit + 25}, {c1, c2, c3}) : c1; + }; + max_color = interp_color(max_color, QColor(0xff, 0xe4, 0xbf), QColor(0xff, 0xbf, 0xbf)); + set_speed_color = interp_color(set_speed_color, QColor(0xff, 0x95, 0x00), QColor(0xff, 0x00, 0x00)); + } + } else { + max_color = QColor(0xa6, 0xa6, 0xa6, 0xff); + set_speed_color = QColor(0x72, 0x72, 0x72, 0xff); + } + p.setFont(InterFont(40, QFont::DemiBold)); + p.setPen(max_color); + p.drawText(set_speed_rect.adjusted(0, 27, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("MAX")); + p.setFont(InterFont(90, QFont::Bold)); + p.setPen(set_speed_color); + p.drawText(set_speed_rect.adjusted(0, 77, 0, 0), Qt::AlignTop | Qt::AlignHCenter, setSpeedStr); + + const QRect sign_rect = set_speed_rect.adjusted(sign_margin, default_size.height(), -sign_margin, -sign_margin); + // US/Canada (MUTCD style) sign + if (has_us_speed_limit) { + p.setPen(Qt::NoPen); + p.setBrush(whiteColor()); + p.drawRoundedRect(sign_rect, 24, 24); + p.setPen(QPen(blackColor(), 6)); + p.drawRoundedRect(sign_rect.adjusted(9, 9, -9, -9), 16, 16); + + p.save(); + p.setOpacity(slcOverridden ? 0.25 : 1.0); + if (showSLCOffset) { + p.setFont(InterFont(28, QFont::DemiBold)); + p.drawText(sign_rect.adjusted(0, 22, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("LIMIT")); + p.setFont(InterFont(70, QFont::Bold)); + p.drawText(sign_rect.adjusted(0, 51, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedLimitStr); + p.setFont(InterFont(50, QFont::DemiBold)); + p.drawText(sign_rect.adjusted(0, 120, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedLimitOffsetStr); + } else { + p.setFont(InterFont(28, QFont::DemiBold)); + p.drawText(sign_rect.adjusted(0, 22, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("SPEED")); + p.drawText(sign_rect.adjusted(0, 51, 0, 0), Qt::AlignTop | Qt::AlignHCenter, tr("LIMIT")); + p.setFont(InterFont(70, QFont::Bold)); + p.drawText(sign_rect.adjusted(0, 85, 0, 0), Qt::AlignTop | Qt::AlignHCenter, speedLimitStr); + } + p.restore(); + } + + // EU (Vienna style) sign + if (has_eu_speed_limit) { + p.setPen(Qt::NoPen); + p.setBrush(whiteColor()); + p.drawEllipse(sign_rect); + p.setPen(QPen(Qt::red, 20)); + p.drawEllipse(sign_rect.adjusted(16, 16, -16, -16)); + + p.setFont(InterFont((speedLimitStr.size() >= 3) ? 60 : 70, QFont::Bold)); + p.setPen(blackColor()); + p.drawText(sign_rect, Qt::AlignCenter, speedLimitStr); + } + + // current speed + if (!hideSpeed) { + p.setFont(InterFont(176, QFont::Bold)); + drawText(p, rect().center().x(), 210, speedStr); + p.setFont(InterFont(66)); + drawText(p, rect().center().x(), 290, speedUnit, 200); + } + + p.restore(); +} + +void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &text, int alpha) { + QRect real_rect = p.fontMetrics().boundingRect(text); + real_rect.moveCenter({x, y - real_rect.height() / 2}); + + p.setPen(QColor(0xff, 0xff, 0xff, alpha)); + p.drawText(real_rect.x(), real_rect.bottom(), text); +} + +void AnnotatedCameraWidget::initializeGL() { + CameraWidget::initializeGL(); + qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION)); + qInfo() << "OpenGL vendor:" << QString((const char*)glGetString(GL_VENDOR)); + qInfo() << "OpenGL renderer:" << QString((const char*)glGetString(GL_RENDERER)); + qInfo() << "OpenGL language version:" << QString((const char*)glGetString(GL_SHADING_LANGUAGE_VERSION)); + + prev_draw_t = millis_since_boot(); + setBackgroundColor(bg_colors[STATUS_DISENGAGED]); +} + +void AnnotatedCameraWidget::updateFrameMat() { + CameraWidget::updateFrameMat(); + UIState *s = uiState(); + int w = width(), h = height(); + + s->fb_w = w; + s->fb_h = h; + + // Apply transformation such that video pixel coordinates match video + // 1) Put (0, 0) in the middle of the video + // 2) Apply same scaling as video + // 3) Put (0, 0) in top left corner of video + s->car_space_transform.reset(); + s->car_space_transform.translate(w / 2 - x_offset, h / 2 - y_offset) + .scale(zoom, zoom) + .translate(-intrinsic_matrix.v[2], -intrinsic_matrix.v[5]); +} + +void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) { + painter.save(); + + SubMaster &sm = *(s->sm); + + // lanelines + for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) { + if (customColors != 0) { + painter.setBrush(themeConfiguration[customColors].second.first); + } else { + painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp(scene.lane_line_probs[i], 0.0, 0.7))); + } + painter.drawPolygon(scene.lane_line_vertices[i]); + } + + // road edges + for (int i = 0; i < std::size(scene.road_edge_vertices); ++i) { + if (customColors != 0) { + painter.setBrush(themeConfiguration[customColors].second.first); + } else { + painter.setBrush(QColor::fromRgbF(1.0, 0, 0, std::clamp(1.0 - scene.road_edge_stds[i], 0.0, 1.0))); + } + painter.drawPolygon(scene.road_edge_vertices[i]); + } + + // paint path + QLinearGradient bg(0, height(), 0, 0); + if (sm["controlsState"].getControlsState().getExperimentalMode() || accelerationPath) { + // The first half of track_vertices are the points for the right side of the path + // and the indices match the positions of accel from uiPlan + const auto &acceleration_const = sm["uiPlan"].getUiPlan().getAccel(); + const int max_len = std::min(scene.track_vertices.length() / 2, acceleration_const.size()); + + // Copy of the acceleration vector + std::vector acceleration; + for (int i = 0; i < acceleration_const.size(); i++) { + acceleration.push_back(acceleration_const[i]); + } + + for (int i = 0; i < max_len; ++i) { + // Some points are out of frame + if (scene.track_vertices[i].y() < 0 || scene.track_vertices[i].y() > height()) continue; + + // Flip so 0 is bottom of frame + float lin_grad_point = (height() - scene.track_vertices[i].y()) / height(); + + // If acceleration is between -0.2 and 0.2, resort to the theme color + if (std::abs(acceleration[i]) < 0.2 && (customColors != 0)) { + const auto &colorMap = themeConfiguration[customColors].second.second; + for (const auto &[position, brush] : colorMap) { + bg.setColorAt(position, brush.color()); + } + } else { + // speed up: 120, slow down: 0 + float path_hue = fmax(fmin(60 + acceleration[i] * 35, 120), 0); + // FIXME: painter.drawPolygon can be slow if hue is not rounded + path_hue = int(path_hue * 100 + 0.5) / 100; + + float saturation = fmin(fabs(acceleration[i] * 1.5), 1); + float lightness = util::map_val(saturation, 0.0f, 1.0f, 0.95f, 0.62f); // lighter when grey + float alpha = util::map_val(lin_grad_point, 0.75f / 2.f, 0.75f, 0.4f, 0.0f); // matches previous alpha fade + bg.setColorAt(lin_grad_point, QColor::fromHslF(path_hue / 360., saturation, lightness, alpha)); + + // Skip a point, unless next is last + i += (i + 2) < max_len ? 1 : 0; + } + } + + } else if (customColors != 0) { + const auto &colorMap = themeConfiguration[customColors].second.second; + for (const auto &[position, brush] : colorMap) { + bg.setColorAt(position, brush.color()); + } + } else { + bg.setColorAt(0.0, QColor::fromHslF(148 / 360., 0.94, 0.51, 0.4)); + bg.setColorAt(0.5, QColor::fromHslF(112 / 360., 1.0, 0.68, 0.35)); + bg.setColorAt(1.0, QColor::fromHslF(112 / 360., 1.0, 0.68, 0.0)); + } + + painter.setBrush(bg); + painter.drawPolygon(scene.track_vertices); + + // create new path with track vertices and track edge vertices + QPainterPath path; + path.addPolygon(scene.track_vertices); + path.addPolygon(scene.track_edge_vertices); + + // paint path edges + QLinearGradient pe(0, height(), 0, 0); + if (alwaysOnLateral) { + pe.setColorAt(0.0, QColor::fromHslF(178 / 360., 0.90, 0.38, 1.0)); + pe.setColorAt(0.5, QColor::fromHslF(178 / 360., 0.90, 0.38, 0.5)); + pe.setColorAt(1.0, QColor::fromHslF(178 / 360., 0.90, 0.38, 0.1)); + } else if (conditionalStatus == 1 || conditionalStatus == 3) { + pe.setColorAt(0.0, QColor::fromHslF(58 / 360., 1.00, 0.50, 1.0)); + pe.setColorAt(0.5, QColor::fromHslF(58 / 360., 1.00, 0.50, 0.5)); + pe.setColorAt(1.0, QColor::fromHslF(58 / 360., 1.00, 0.50, 0.1)); + } else if (experimentalMode) { + pe.setColorAt(0.0, QColor::fromHslF(25 / 360., 0.71, 0.50, 1.0)); + pe.setColorAt(0.5, QColor::fromHslF(25 / 360., 0.71, 0.50, 0.5)); + pe.setColorAt(1.0, QColor::fromHslF(25 / 360., 0.71, 0.50, 0.1)); + } else if (scene.navigate_on_openpilot) { + pe.setColorAt(0.0, QColor::fromHslF(205 / 360., 0.85, 0.56, 1.0)); + pe.setColorAt(0.5, QColor::fromHslF(205 / 360., 0.85, 0.56, 0.5)); + pe.setColorAt(1.0, QColor::fromHslF(205 / 360., 0.85, 0.56, 0.1)); + } else if (customColors != 0) { + const auto &colorMap = themeConfiguration[customColors].second.second; + for (const auto &[position, brush] : colorMap) { + QColor darkerColor = brush.color().darker(120); + pe.setColorAt(position, darkerColor); + } + } else { + pe.setColorAt(0.0, QColor::fromHslF(148 / 360., 0.94, 0.51, 1.0)); + pe.setColorAt(0.5, QColor::fromHslF(112 / 360., 1.00, 0.68, 0.5)); + pe.setColorAt(1.0, QColor::fromHslF(112 / 360., 1.00, 0.68, 0.1)); + } + + painter.setBrush(pe); + painter.drawPath(path); + + // paint blindspot path + QLinearGradient bs(0, height(), 0, 0); + if (blindSpotLeft || blindSpotRight) { + bs.setColorAt(0.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.6)); + bs.setColorAt(0.5, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.4)); + bs.setColorAt(1.0, QColor::fromHslF(0 / 360., 0.75, 0.50, 0.2)); + } + + painter.setBrush(bs); + if (blindSpotLeft) { + painter.drawPolygon(scene.track_adjacent_vertices[4]); + } + if (blindSpotRight) { + painter.drawPolygon(scene.track_adjacent_vertices[5]); + } + + // paint adjacent lane paths + if (adjacentPath && (laneWidthLeft != 0 || laneWidthRight != 0)) { + // Set up the units + double conversionFactor = is_metric ? 1.0 : 3.28084; + QString unit_d = is_metric ? " meters" : " feet"; + + // Declare the lane width thresholds + constexpr float minLaneWidth = 2.5f; + constexpr float maxLaneWidth = 3.5f; + + // Set gradient colors based on laneWidth and blindspot + auto setGradientColors = [](QLinearGradient &gradient, float laneWidth, bool blindspot) { + // Make the path red for smaller paths or if there's a car in the blindspot and green for larger paths + double hue = (laneWidth < minLaneWidth || blindspot) ? 0.0 : + (laneWidth >= maxLaneWidth) ? 120.0 : + 120.0 * (laneWidth - minLaneWidth) / (maxLaneWidth - minLaneWidth); + auto hue_ratio = hue / 360.0; + gradient.setColorAt(0.0, QColor::fromHslF(hue_ratio, 0.75, 0.50, 0.6)); + gradient.setColorAt(0.5, QColor::fromHslF(hue_ratio, 0.75, 0.50, 0.4)); + gradient.setColorAt(1.0, QColor::fromHslF(hue_ratio, 0.75, 0.50, 0.2)); + }; + + // Paint the lanes + auto paintLane = [&](QPainter &painter, const QPolygonF &lane, const float laneWidth, const bool blindspot) { + QLinearGradient gradient(0, height(), 0, 0); + setGradientColors(gradient, laneWidth, blindspot); + painter.setFont(InterFont(30, QFont::DemiBold)); + painter.setBrush(gradient); + painter.setPen(Qt::transparent); + painter.drawPolygon(lane); + painter.setPen(Qt::white); + + QRectF boundingRect = lane.boundingRect(); + painter.drawText(boundingRect.center(), blindspot ? "Vehicle in blind spot" : + QString("%1%2").arg(laneWidth * conversionFactor, 0, 'f', 2).arg(unit_d)); + painter.setPen(Qt::NoPen); + }; + + // Paint lanes + paintLane(painter, scene.track_adjacent_vertices[4], laneWidthLeft, blindSpotLeft); + paintLane(painter, scene.track_adjacent_vertices[5], laneWidthRight, blindSpotRight); + } + + painter.restore(); +} + +void AnnotatedCameraWidget::drawDriverState(QPainter &painter, const UIState *s) { + painter.save(); + + // base icon + int offset = UI_BORDER_SIZE + btn_size / 2; + offset += alwaysOnLateral || conditionalExperimental || roadNameUI ? 25 : 0; + int x = rightHandDM ? width() - offset : offset; + x += onroadAdjustableProfiles ? 250 : 0; + int y = height() - offset; + float opacity = dmActive ? 0.65 : 0.2; + drawIcon(painter, QPoint(x, y), dm_img, blackColor(70), opacity); + + // face + QPointF face_kpts_draw[std::size(default_face_kpts_3d)]; + float kp; + for (int i = 0; i < std::size(default_face_kpts_3d); ++i) { + kp = (scene.face_kpts_draw[i].v[2] - 8) / 120 + 1.0; + face_kpts_draw[i] = QPointF(scene.face_kpts_draw[i].v[0] * kp + x, scene.face_kpts_draw[i].v[1] * kp + y); + } + + painter.setPen(QPen(QColor::fromRgbF(1.0, 1.0, 1.0, opacity), 5.2, Qt::SolidLine, Qt::RoundCap)); + painter.drawPolyline(face_kpts_draw, std::size(default_face_kpts_3d)); + + // tracking arcs + const int arc_l = 133; + const float arc_t_default = 6.7; + const float arc_t_extend = 12.0; + QColor arc_color = QColor::fromRgbF(0.545 - 0.445 * s->engaged(), + 0.545 + 0.4 * s->engaged(), + 0.545 - 0.285 * s->engaged(), + 0.4 * (1.0 - dm_fade_state)); + float delta_x = -scene.driver_pose_sins[1] * arc_l / 2; + float delta_y = -scene.driver_pose_sins[0] * arc_l / 2; + painter.setPen(QPen(arc_color, arc_t_default+arc_t_extend*fmin(1.0, scene.driver_pose_diff[1] * 5.0), Qt::SolidLine, Qt::RoundCap)); + painter.drawArc(QRectF(std::fmin(x + delta_x, x), y - arc_l / 2, fabs(delta_x), arc_l), (scene.driver_pose_sins[1]>0 ? 90 : -90) * 16, 180 * 16); + painter.setPen(QPen(arc_color, arc_t_default+arc_t_extend*fmin(1.0, scene.driver_pose_diff[0] * 5.0), Qt::SolidLine, Qt::RoundCap)); + painter.drawArc(QRectF(x - arc_l / 2, std::fmin(y + delta_y, y), arc_l, fabs(delta_y)), (scene.driver_pose_sins[0]>0 ? 0 : 180) * 16, 180 * 16); + + painter.restore(); +} + +void AnnotatedCameraWidget::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd) { + painter.save(); + + const float speedBuff = customColors ? 25. : 10.; // Make the center of the chevron appear sooner if a custom theme is active + const float leadBuff = customColors ? 100. : 40.; // Make the center of the chevron appear sooner if a custom theme is active + const float d_rel = lead_data.getDRel(); + const float v_rel = lead_data.getVRel(); + + float fillAlpha = 0; + if (d_rel < leadBuff) { + fillAlpha = 255 * (1.0 - (d_rel / leadBuff)); + if (v_rel < 0) { + fillAlpha += 255 * (-1 * (v_rel / speedBuff)); + } + fillAlpha = (int)(fmin(fillAlpha, 255)); + } + + float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35; + float x = std::clamp((float)vd.x(), 0.f, width() - sz / 2); + float y = std::fmin(height() - sz * .6, (float)vd.y()); + + float g_xo = sz / 5; + float g_yo = sz / 10; + + QPointF glow[] = {{x + (sz * 1.35) + g_xo, y + sz + g_yo}, {x, y - g_yo}, {x - (sz * 1.35) - g_xo, y + sz + g_yo}}; + painter.setBrush(QColor(218, 202, 37, 255)); + painter.drawPolygon(glow, std::size(glow)); + + // chevron + QPointF chevron[] = {{x + (sz * 1.25), y + sz}, {x, y}, {x - (sz * 1.25), y + sz}}; + if (customColors != 0) { + painter.setBrush(themeConfiguration[customColors].second.first); + } else { + painter.setBrush(redColor(fillAlpha)); + } + painter.drawPolygon(chevron, std::size(chevron)); + + // Add lead info + if (leadInfo) { + // Declare the variables + QString unit_d = "meters"; + QString unit_s = "km/h"; + float distance = d_rel; + float lead_speed = std::max(lead_data.getVLead(), 0.0f); // Ensure speed doesn't go under 0 m/s cause that's dumb + + // Conversion factors and units + constexpr float toFeet = 3.28084f; + constexpr float toMph = 2.23694f; + constexpr float toKmph = 3.6f; + + // Metric speed conversion + if (is_metric || useSI) { + lead_speed *= toKmph; + } else { + // US imperial conversion + distance *= toFeet; + lead_speed *= toMph; + unit_d = "feet"; + unit_s = "mph"; + } + + // Form the text and center it below the chevron + painter.setPen(Qt::white); + painter.setFont(InterFont(35, QFont::Bold)); + + QString text = QString("%1 %2 | %3 %4") + .arg(distance, 0, 'f', 2, '0') + .arg(unit_d) + .arg(lead_speed, 0, 'f', 2, '0') + .arg(unit_s); + + // Calculate the text starting position + QFontMetrics metrics(painter.font()); + int middle_x = (chevron[2].x() + chevron[0].x()) / 2; + int textWidth = metrics.horizontalAdvance(text); + painter.drawText(middle_x - textWidth / 2, chevron[0].y() + metrics.height() + 5, text); + } + + painter.restore(); +} + +void AnnotatedCameraWidget::paintGL() { +} + +void AnnotatedCameraWidget::paintEvent(QPaintEvent *event) { + UIState *s = uiState(); + SubMaster &sm = *(s->sm); + QPainter painter(this); + const double start_draw_t = millis_since_boot(); + const cereal::ModelDataV2::Reader &model = sm["modelV2"].getModelV2(); + + // draw camera frame + { + std::lock_guard lk(frame_lock); + + if (frames.empty()) { + if (skip_frame_count > 0) { + skip_frame_count--; + qDebug() << "skipping frame, not ready"; + return; + } + } else { + // skip drawing up to this many frames if we're + // missing camera frames. this smooths out the + // transitions from the narrow and wide cameras + skip_frame_count = 5; + } + + // Wide or narrow cam dependent on speed + bool has_wide_cam = available_streams.count(VISION_STREAM_WIDE_ROAD); + if (has_wide_cam) { + float v_ego = sm["carState"].getCarState().getVEgo(); + if ((v_ego < 10) || available_streams.size() == 1) { + wide_cam_requested = true; + } else if (v_ego > 15) { + wide_cam_requested = false; + } + wide_cam_requested = wide_cam_requested && sm["controlsState"].getControlsState().getExperimentalMode() || cameraView == 2; + // for replay of old routes, never go to widecam + wide_cam_requested = wide_cam_requested && s->scene.calibration_wide_valid; + } + paramsMemory.putBoolNonBlocking("WideCamera", wide_cam_requested); + CameraWidget::setStreamType(showDriverCamera || cameraView == 3 ? VISION_STREAM_DRIVER : + wide_cam_requested && cameraView != 1 ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD); + + s->scene.wide_cam = CameraWidget::getStreamType() == VISION_STREAM_WIDE_ROAD; + if (s->scene.calibration_valid) { + auto calib = s->scene.wide_cam ? s->scene.view_from_wide_calib : s->scene.view_from_calib; + CameraWidget::updateCalibration(calib); + } else { + CameraWidget::updateCalibration(DEFAULT_CALIBRATION); + } + painter.beginNativePainting(); + CameraWidget::setFrameId(model.getFrameId()); + CameraWidget::paintGL(); + painter.endNativePainting(); + } + + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + + if (s->scene.world_objects_visible && !showDriverCamera) { + update_model(s, model, sm["uiPlan"].getUiPlan()); + drawLaneLines(painter, s); + + if (s->scene.longitudinal_control && sm.rcv_frame("radarState") > s->scene.started_frame) { + auto radar_state = sm["radarState"].getRadarState(); + update_leads(s, radar_state, model.getPosition()); + auto lead_one = radar_state.getLeadOne(); + auto lead_two = radar_state.getLeadTwo(); + if (lead_one.getStatus()) { + drawLead(painter, lead_one, s->scene.lead_vertices[0]); + } + if (lead_two.getStatus() && (std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0)) { + drawLead(painter, lead_two, s->scene.lead_vertices[1]); + } + } + } + + // DMoji + if (!hideBottomIcons && (sm.rcv_frame("driverStateV2") > s->scene.started_frame) && !muteDM) { + update_dmonitoring(s, sm["driverStateV2"].getDriverStateV2(), dm_fade_state, rightHandDM); + drawDriverState(painter, s); + } + + drawHud(painter); + + double cur_draw_t = millis_since_boot(); + double dt = cur_draw_t - prev_draw_t; + fps = fps_filter.update(1. / dt * 1000); + if (fps < 15) { + LOGW("slow frame rate: %.2f fps", fps); + } + prev_draw_t = cur_draw_t; + + // publish debug msg + MessageBuilder msg; + auto m = msg.initEvent().initUiDebug(); + m.setDrawTimeMillis(cur_draw_t - start_draw_t); + pm->send("uiDebug", msg); + + // Update FrogPilot widgets + updateFrogPilotWidgets(painter); +} + +void AnnotatedCameraWidget::showEvent(QShowEvent *event) { + CameraWidget::showEvent(event); + + std::thread updateFrogPilotParams(ui_update_params, uiState()); + updateFrogPilotParams.detach(); + prev_draw_t = millis_since_boot(); +} + +// FrogPilot widgets +void AnnotatedCameraWidget::initializeFrogPilotWidgets() { + Params params = Params(); + + bottom_layout = new QHBoxLayout(); + + personality_btn = new PersonalityButton(this); + bottom_layout->addWidget(personality_btn); + + QSpacerItem *spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); + bottom_layout->addItem(spacer); + + compass_img = new Compass(this); + bottom_layout->addWidget(compass_img); + + map_settings_btn_bottom = new MapSettingsButton(this); + bottom_layout->addWidget(map_settings_btn_bottom); + + main_layout->addLayout(bottom_layout); + + // Custom themes configuration + themeConfiguration = { + {1, {QString("frog_theme"), {QColor(23, 134, 68, 242), {{0.0, QBrush(QColor::fromHslF(144 / 360., 0.71, 0.31, 0.9))}, + {0.5, QBrush(QColor::fromHslF(144 / 360., 0.71, 0.31, 0.5))}, + {1.0, QBrush(QColor::fromHslF(144 / 360., 0.71, 0.31, 0.1))}}}}}, + {2, {QString("tesla_theme"), {QColor(0, 72, 255, 255), {{0.0, QBrush(QColor::fromHslF(223 / 360., 1.0, 0.5, 0.9))}, + {0.5, QBrush(QColor::fromHslF(223 / 360., 1.0, 0.5, 0.5))}, + {1.0, QBrush(QColor::fromHslF(223 / 360., 1.0, 0.5, 0.1))}}}}}, + {3, {QString("stalin_theme"), {QColor(255, 0, 0, 255), {{0.0, QBrush(QColor::fromHslF(0 / 360., 1.0, 0.5, 0.9))}, + {0.5, QBrush(QColor::fromHslF(0 / 360., 1.0, 0.5, 0.5))}, + {1.0, QBrush(QColor::fromHslF(0 / 360., 1.0, 0.5, 0.1))}}}}} + }; + + // Initialize the timer for the turn signal animation + animationTimer = new QTimer(this); + connect(animationTimer, &QTimer::timeout, this, [this] { + animationFrameIndex = (animationFrameIndex + 1) % totalFrames; + }); + + // Initialize the timer for the screen recorder + QTimer *record_timer = new QTimer(this); + connect(record_timer, &QTimer::timeout, this, [this]() { + if (this->recorder_btn) { + this->recorder_btn->update_screen(); + } + }); + record_timer->start(1000 / UI_FREQ); +} + +void AnnotatedCameraWidget::updateFrogPilotWidgets(QPainter &p) { + accelerationPath = scene.acceleration_path; + adjacentPath = scene.adjacent_path; + alwaysOnLateral = scene.always_on_lateral_active; + blindSpotLeft = scene.blind_spot_left; + blindSpotRight = scene.blind_spot_right; + cameraView = scene.camera_view; + compass = scene.compass; + conditionalExperimental = scene.conditional_experimental; + conditionalSpeed = scene.conditional_speed; + conditionalSpeedLead = scene.conditional_speed_lead; + conditionalStatus = scene.conditional_status; + cruiseAdjustment = fmax((0.1 * fmax(setSpeed - scene.adjusted_cruise, 0) + 0.9 * cruiseAdjustment) - 1, 0); + customColors = scene.custom_colors; + desiredFollow = scene.desired_follow; + experimentalMode = scene.experimental_mode; + hideSpeed = scene.hide_speed; + laneWidthLeft = scene.lane_width_left; + laneWidthRight = scene.lane_width_right; + leadInfo = scene.lead_info; + mapOpen = scene.map_open; + muteDM = scene.mute_dm; + obstacleDistance = scene.obstacle_distance; + obstacleDistanceStock = scene.obstacle_distance_stock; + onroadAdjustableProfiles = scene.personalities_via_screen; + reverseCruise = scene.reverse_cruise; + roadNameUI = scene.road_name_ui; + showDriverCamera = scene.show_driver_camera; + showSLCOffset = scene.show_slc_offset; + slcOverridden = scene.speed_limit_overridden; + slcOverriddenSpeed = scene.speed_limit_overridden_speed; + slcSpeedLimit = scene.speed_limit; + slcSpeedLimitOffset = scene.speed_limit_offset * (is_metric ? MS_TO_KPH : MS_TO_MPH); + stoppedEquivalence = scene.stopped_equivalence; + turnSignalLeft = scene.turn_signal_left; + turnSignalRight = scene.turn_signal_right; + useSI = scene.use_si; + useViennaSLCSign = scene.use_vienna_slc_sign; + + if (!showDriverCamera) { + if (leadInfo) { + drawLeadInfo(p); + } + + if (alwaysOnLateral || conditionalExperimental || roadNameUI) { + drawStatusBar(p); + } + + if (customSignals && (turnSignalLeft || turnSignalRight)) { + if (!animationTimer->isActive()) { + animationTimer->start(totalFrames * 11); // 440 milliseconds per loop; syncs up perfectly with my 2019 Lexus ES 350 turn signal clicks + } + drawTurnSignals(p); + } else if (animationTimer->isActive()) { + animationTimer->stop(); + } + } + + const bool enableCompass = compass && !hideBottomIcons; + compass_img->setVisible(enableCompass); + if (enableCompass) { + if (bearingDeg != scene.bearing_deg) { + bearingDeg = scene.bearing_deg; + compass_img->updateState(bearingDeg); + } + bottom_layout->setAlignment(compass_img, (rightHandDM ? Qt::AlignLeft : Qt::AlignRight)); + } + + const bool enablePersonalityButton = onroadAdjustableProfiles && !hideBottomIcons; + personality_btn->setVisible(enablePersonalityButton); + if (enablePersonalityButton) { + if (paramsMemory.getBool("PersonalityChangedViaWheel")) { + personality_btn->checkUpdate(); + } + bottom_layout->setAlignment(personality_btn, (rightHandDM ? Qt::AlignRight : Qt::AlignLeft)); + } + + map_settings_btn_bottom->setEnabled(map_settings_btn->isEnabled()); + if (map_settings_btn_bottom->isEnabled()) { + map_settings_btn_bottom->setVisible(!hideBottomIcons && !compass); + bottom_layout->setAlignment(map_settings_btn_bottom, rightHandDM ? Qt::AlignLeft : Qt::AlignRight); + } + + // Update the turn signal animation images upon toggle change + if (customSignals != scene.custom_signals) { + customSignals = scene.custom_signals; + + const QString theme_path = QString("../frogpilot/assets/custom_themes/%1/images").arg(themeConfiguration.find(customSignals) != themeConfiguration.end() ? + themeConfiguration[customSignals].first : "stock_theme"); + const QStringList imagePaths = { + theme_path + "/turn_signal_1.png", + theme_path + "/turn_signal_2.png", + theme_path + "/turn_signal_3.png", + theme_path + "/turn_signal_4.png" + }; + + signalImgVector.clear(); + signalImgVector.reserve(4 * imagePaths.size() + 2); // Reserve space for both regular and flipped images + for (int i = 0; i < 2; ++i) { + for (const QString &imagePath : imagePaths) { + QPixmap pixmap(imagePath); + signalImgVector.push_back(pixmap); // Regular image + signalImgVector.push_back(pixmap.transformed(QTransform().scale(-1, 1))); // Flipped image + } + } + signalImgVector.push_back(QPixmap(theme_path + "/turn_signal_1_red.png")); // Regular blindspot image + signalImgVector.push_back(QPixmap(theme_path + "/turn_signal_1_red.png").transformed(QTransform().scale(-1, 1))); // Flipped blindspot image + } +} + +Compass::Compass(QWidget *parent) : QWidget(parent) { + setFixedSize(btn_size * 1.5, btn_size * 1.5); + + compassSize = btn_size; + circleOffset = compassSize / 2; + degreeLabelOffset = circleOffset + 25; + innerCompass = compassSize / 2; + x = (btn_size * 1.5) / 2 + 20; + y = (btn_size * 1.5) / 2; + + compassInnerImg = loadPixmap("../frogpilot/assets/other_images/compass_inner.png", QSize(compassSize / 1.75, compassSize / 1.75)); + + initializeStaticElements(); +} + +void Compass::updateState(int bearing_deg) { + bearingDeg = bearing_deg; + update(); +} + +void Compass::initializeStaticElements() { + staticElements = QPixmap(size()); + staticElements.fill(Qt::transparent); + QPainter p(&staticElements); + + p.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); + + // Configure the circles + QPen whitePen(Qt::white, 2); + p.setPen(whitePen); + + // Draw the circle background and white inner circle + p.setOpacity(1.0); + p.setBrush(QColor(0, 0, 0, 100)); + p.drawEllipse(x - circleOffset, y - circleOffset, circleOffset * 2, circleOffset * 2); + + // Draw the white circles + p.setBrush(Qt::NoBrush); + p.drawEllipse(x - (innerCompass + 5), y - (innerCompass + 5), (innerCompass + 5) * 2, (innerCompass + 5) * 2); + p.drawEllipse(x - degreeLabelOffset, y - degreeLabelOffset, degreeLabelOffset * 2, degreeLabelOffset * 2); + + // Draw the black background for the bearing degrees + QPainterPath outerCircle, innerCircle; + outerCircle.addEllipse(x - degreeLabelOffset, y - degreeLabelOffset, degreeLabelOffset * 2, degreeLabelOffset * 2); + innerCircle.addEllipse(x - circleOffset, y - circleOffset, compassSize, compassSize); + p.fillPath(outerCircle.subtracted(innerCircle), Qt::black); + + // Draw the static degree lines + for (int i = 0; i < 360; i += 15) { + bool isCardinalDirection = i % 90 == 0; + int lineLength = isCardinalDirection ? 15 : 10; + p.setPen(QPen(Qt::white, isCardinalDirection ? 3 : 1)); + p.save(); + p.translate(x, y); + p.rotate(i); + p.drawLine(0, -(compassSize / 2 - lineLength), 0, -(compassSize / 2)); + p.restore(); + } +} + +void Compass::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); + + // Draw static elements + p.drawPixmap(0, 0, staticElements); + + // Rotate and draw the compassInnerImg image + p.translate(x, y); + p.rotate(bearingDeg); + p.drawPixmap(-compassInnerImg.width() / 2, -compassInnerImg.height() / 2, compassInnerImg); + p.rotate(-bearingDeg); + p.translate(-x, -y); + + // Draw the dynamic bearing degree numbers and lines + QFont font = InterFont(10, QFont::Normal); + for (int i = 0; i < 360; i += 15) { + bool isBold = abs(i - static_cast(bearingDeg)) <= 7; + font.setWeight(isBold ? QFont::Bold : QFont::Normal); + p.setFont(font); + p.setPen(QPen(Qt::white, i % 90 == 0 ? 2 : 1)); + + p.save(); + p.translate(x, y); + p.rotate(i); + p.drawLine(0, -(compassSize / 2 - (i % 90 == 0 ? 12 : 8)), 0, -(compassSize / 2)); + p.translate(0, -(compassSize / 2 + 12)); + p.rotate(-i); + p.drawText(QRect(-20, -10, 40, 20), Qt::AlignCenter, QString::number(i)); + p.restore(); + } + + // Draw cardinal directions + p.setFont(InterFont(20, QFont::Bold)); + QString directions[] = {"N", "E", "S", "W", "N"}; + int fromAngles[] = {337, 68, 158, 248, 337}; + int toAngles[] = {22, 112, 202, 292, 360}; + int alignmentFlags[] = {Qt::AlignTop | Qt::AlignHCenter, Qt::AlignRight | Qt::AlignVCenter, Qt::AlignBottom | Qt::AlignHCenter, Qt::AlignLeft | Qt::AlignVCenter, Qt::AlignTop | Qt::AlignHCenter}; + int directionOffset = 20; + + for (int i = 0; i < 5; ++i) { + int offset = (directions[i] == "E") ? -5 : (directions[i] == "W" ? 5 : 0); + p.setOpacity((bearingDeg >= fromAngles[i] && bearingDeg < toAngles[i]) ? 1.0 : 0.2); + QRect textRect(x - innerCompass + offset + directionOffset, y - innerCompass + directionOffset, innerCompass * 2 - 2 * directionOffset, innerCompass * 2 - 2 * directionOffset); + p.drawText(textRect, alignmentFlags[i], directions[i]); + } +} + +void AnnotatedCameraWidget::drawLeadInfo(QPainter &p) { + SubMaster &sm = *uiState()->sm; + + // Declare the variables + static QElapsedTimer timer; + static bool isFiveSecondsPassed = false; + constexpr int maxAccelDuration = 5000; + + // Constants for units and conversions + QString unit_a = " m/s²"; + QString unit_d = mapOpen ? "m" : "meters"; + QString unit_s = "kmh"; + float distanceConversion = 1.0f; + float speedConversion = 1.0f; + + // Conversion factors and units + constexpr float toFeet = 3.28084f; + constexpr float toMph = 2.23694f; + + // Metric speed conversion + if (!(is_metric || useSI)) { + // US imperial conversion + unit_a = " ft/s²"; + unit_d = mapOpen ? "ft" : "feet"; + unit_s = "mph"; + distanceConversion = toFeet; + speedConversion = toMph; + } + + // Update acceleration + double currentAcceleration = std::round(sm["carState"].getCarState().getAEgo() * 100) / 100; + static double maxAcceleration = 0.0; + + if (currentAcceleration > maxAcceleration && status == STATUS_ENGAGED) { + maxAcceleration = currentAcceleration; + isFiveSecondsPassed = false; + timer.start(); + } else { + isFiveSecondsPassed = timer.hasExpired(maxAccelDuration); + } + + // Construct text segments + auto createText = [&](const QString &title, const double data) { + return title + QString::number(std::round(data * distanceConversion)) + " " + unit_d; + }; + + // Create segments for insights + QString accelText = QString("Accel: %1%2") + .arg(currentAcceleration * speedConversion, 0, 'f', 2) + .arg(unit_a); + + QString maxAccSuffix = QString(mapOpen ? "" : " - Max: %1%2") + .arg(maxAcceleration * speedConversion, 0, 'f', 2) + .arg(unit_a); + + QString obstacleText = createText(mapOpen ? " | Obstacle: " : " | Obstacle Factor: ", obstacleDistance); + QString stopText = createText(mapOpen ? " - Stop: " : " - Stop Factor: ", stoppedEquivalence); + QString followText = " = " + createText(mapOpen ? "Follow: " : "Follow Distance: ", desiredFollow); + + // Check if the longitudinal toggles have an impact on the driving logics + auto createDiffText = [&](const double data, const double stockData) { + double difference = std::round((data - stockData) * distanceConversion); + return difference != 0 ? QString(" (%1%2)").arg(difference > 0 ? "+" : "").arg(difference) : QString(); + }; + + // Prepare rectangle for insights + p.save(); + QRect insightsRect(rect().left() - 1, rect().top() - 60, rect().width() + 2, 100); + p.setBrush(QColor(0, 0, 0, 150)); + p.drawRoundedRect(insightsRect, 30, 30); + p.setFont(InterFont(30, QFont::DemiBold)); + p.setRenderHint(QPainter::TextAntialiasing); + + // Calculate positioning for text drawing + QRect adjustedRect = insightsRect.adjusted(0, 27, 0, 27); + int textBaseLine = adjustedRect.y() + (adjustedRect.height() + p.fontMetrics().height()) / 2 - p.fontMetrics().descent(); + + // Calculate the entire text width to ensure perfect centering + int totalTextWidth = p.fontMetrics().horizontalAdvance(accelText) + + p.fontMetrics().horizontalAdvance(maxAccSuffix) + + p.fontMetrics().horizontalAdvance(obstacleText) + + p.fontMetrics().horizontalAdvance(createDiffText(obstacleDistance, obstacleDistanceStock)) + + p.fontMetrics().horizontalAdvance(stopText) + + p.fontMetrics().horizontalAdvance(followText); + + int textStartPos = adjustedRect.x() + (adjustedRect.width() - totalTextWidth) / 2; + + // Draw the text + auto drawText = [&](const QString &text, const QColor color) { + p.setPen(color); + p.drawText(textStartPos, textBaseLine, text); + textStartPos += p.fontMetrics().horizontalAdvance(text); + }; + + drawText(accelText, Qt::white); + drawText(maxAccSuffix, isFiveSecondsPassed ? Qt::white : Qt::red); + drawText(obstacleText, Qt::white); + drawText(createDiffText(obstacleDistance, obstacleDistanceStock), (obstacleDistance - obstacleDistanceStock) > 0 ? Qt::green : Qt::red); + drawText(stopText, Qt::white); + drawText(followText, Qt::white); + + p.restore(); +} + +PersonalityButton::PersonalityButton(QWidget *parent) : QPushButton(parent), scene(uiState()->scene) { + setFixedSize(btn_size * 1.5, btn_size * 1.5); + + // Configure the profile vector + profile_data = { + {QPixmap("../frogpilot/assets/other_images/aggressive.png"), "Aggressive"}, + {QPixmap("../frogpilot/assets/other_images/standard.png"), "Standard"}, + {QPixmap("../frogpilot/assets/other_images/relaxed.png"), "Relaxed"} + }; + + // Start the timer as soon as the button is created + transitionTimer.start(); + + // Initialize the click event + connect(this, &QPushButton::clicked, this, &PersonalityButton::handleClick); + + personalityProfile = params.getInt("LongitudinalPersonality"); + setVisible(scene.personalities_via_screen); +} + +void PersonalityButton::checkUpdate() { + // Sync with the steering wheel button + personalityProfile = params.getInt("LongitudinalPersonality"); + updateState(); + paramsMemory.putBool("PersonalityChangedViaWheel", false); +} + +void PersonalityButton::handleClick() { + int mapping[] = {2, 0, 1}; + personalityProfile = mapping[personalityProfile]; + + params.putInt("LongitudinalPersonality", personalityProfile); + paramsMemory.putBool("PersonalityChangedViaUI", true); + + updateState(); +} + +void PersonalityButton::updateState() { + // Start the transition + transitionTimer.restart(); +} + +void PersonalityButton::paintEvent(QPaintEvent *) { + // Declare the constants + constexpr qreal fadeDuration = 1000.0; // 1 second + constexpr qreal textDuration = 3000.0; // 3 seconds + + QPainter p(this); + int elapsed = transitionTimer.elapsed(); + qreal textOpacity = qBound(0.0, 1.0 - ((elapsed - textDuration) / fadeDuration), 1.0); + qreal imageOpacity = qBound(0.0, (elapsed - textDuration) / fadeDuration, 1.0); + + // Enable Antialiasing + p.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); + + // Configure the button + const auto &[profile_image, profile_text] = profile_data[personalityProfile]; + QRect rect(0, 0, width(), height() + 95); + + // Draw the profile text with the calculated opacity + if (textOpacity > 0.0) { + p.setOpacity(textOpacity); + p.setFont(InterFont(40, QFont::Bold)); + p.setPen(Qt::white); + p.drawText(rect, Qt::AlignCenter, profile_text); + } + + // Draw the profile image with the calculated opacity + if (imageOpacity > 0.0) { + drawIcon(p, QPoint((btn_size / 2) * 1.25, btn_size / 2 + 95), profile_image, Qt::transparent, imageOpacity); + } +} + +void AnnotatedCameraWidget::drawStatusBar(QPainter &p) { + p.save(); + + // Variable declarations + QElapsedTimer timer; + static QString lastShownStatus; + static QString newStatus; + + static bool displayStatusText = false; + + constexpr qreal fadeDuration = 1500.0; + constexpr qreal textDuration = 5000.0; + + // Draw status bar + QRect currentRect = rect(); + QRect statusBarRect(currentRect.left() - 1, currentRect.bottom() - 50, currentRect.width() + 2, 100); + p.setBrush(QColor(0, 0, 0, 150)); + p.setOpacity(1.0); + p.drawRoundedRect(statusBarRect, 30, 30); + +QString roadName = roadNameUI ? QString::fromStdString(paramsMemory.get("RoadName")) : QString(); +// // QString roadName = QString::fromStdString(paramsMemory.get("oscar_debug")); + +// QMap conditionalStatusMap = { +// {0, "Conditional Experimental Mode ready"}, +// {1, "Conditional Experimental overridden"}, +// {2, "Experimental Mode manually activated"}, +// {3, "Conditional Experimental overridden"}, +// {4, "Experimental Mode manually activated"}, +// {5, "Experimental Mode activated for navigation" + (mapOpen ? "" : QString(" instructions input"))}, +// {6, "Experimental Mode activated due to" + (mapOpen ? "SLC" : QString(" no speed limit set"))}, +// {7, "Experimental Mode activated due to" + (mapOpen ? " speed" : " speed being less than " + QString::number(conditionalSpeedLead) + (is_metric ? " kph" : " mph"))}, +// {8, "Experimental Mode activated due to" + (mapOpen ? " speed" : " speed being less than " + QString::number(conditionalSpeed) + (is_metric ? " kph" : " mph"))}, +// {9, "Experimental Mode activated for slower lead"}, +// {10, "Experimental Mode activated for turn" + (mapOpen ? "" : QString(" / lane change"))}, +// {11, "Experimental Mode activated for curve"}, +// {12, "Experimental Mode activated for stop" + (mapOpen ? "" : QString(" sign / stop light"))}, +// }; + + QString screenSuffix = ". Double tap the screen to revert"; + QString wheelSuffix = ". Double press the \"LKAS\" button to revert"; + +// if (alwaysOnLateral) { +// newStatus = QString("Always On Lateral active") + (mapOpen ? "" : ". Press the \"Cruise Control\" button to disable"); +// } else if (conditionalExperimental) { +// newStatus = conditionalStatusMap.contains(conditionalStatus) && status != STATUS_DISENGAGED ? conditionalStatusMap[conditionalStatus] : conditionalStatusMap[0]; +// } + + newStatus = QString::fromStdString(paramsMemory.get("oscar_debug")); + + // Check if status has changed or if the road name is empty + if (newStatus != lastShownStatus || roadName.isEmpty()) { + displayStatusText = true; + lastShownStatus = newStatus; + timer.restart(); + } else if (displayStatusText && timer.hasExpired(textDuration + fadeDuration)) { + displayStatusText = false; + } + if (!alwaysOnLateral && !mapOpen && status != STATUS_DISENGAGED && !newStatus.isEmpty()) { + newStatus += (conditionalStatus == 3 || conditionalStatus == 4) ? screenSuffix : (conditionalStatus == 1 || conditionalStatus == 2) ? wheelSuffix : ""; + } + + // Configure the text + p.setFont(InterFont(40, QFont::Bold)); + p.setPen(Qt::white); + 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 = 1.0 - roadNameOpacity; + } + + // Draw the status text + p.setOpacity(statusTextOpacity); + QRect textRect = p.fontMetrics().boundingRect(statusBarRect, Qt::AlignCenter | Qt::TextWordWrap, newStatus); + 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(); +} + +void AnnotatedCameraWidget::drawTurnSignals(QPainter &p) { + // Declare the turn signal size + constexpr int signalHeight = 480; + constexpr int signalWidth = 360; + + // Calculate the vertical position for the turn signals + int baseYPosition = (height() - signalHeight) / 2 + (alwaysOnLateral || conditionalExperimental || roadNameUI ? 225 : 300); + // Calculate the x-coordinates for the turn signals + int leftSignalXPosition = 75 + width() - signalWidth - 300 * (blindSpotLeft ? 0 : animationFrameIndex); + int rightSignalXPosition = -75 + 300 * (blindSpotRight ? 0 : animationFrameIndex); + + // Enable Antialiasing + p.setRenderHint(QPainter::Antialiasing); + + // Draw the turn signals + if (animationFrameIndex < signalImgVector.size()) { + auto drawSignal = [&](bool signalActivated, int xPosition, bool flip, bool blindspot) { + if (signalActivated) { + // Get the appropriate image from the signalImgVector + int uniqueImages = signalImgVector.size() / 4; // Each image has a regular, flipped, and two blindspot versions + int index = (blindspot ? 2 * uniqueImages : 2 * animationFrameIndex % totalFrames) + (flip ? 1 : 0); + QPixmap &signal = signalImgVector[index]; + p.drawPixmap(xPosition, baseYPosition, signalWidth, signalHeight, signal); + } + }; + + // Display the animation based on which signal is activated + drawSignal(turnSignalLeft, leftSignalXPosition, false, blindSpotLeft); + drawSignal(turnSignalRight, rightSignalXPosition, true, blindSpotRight); + } +} diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h new file mode 100755 index 0000000..07c1c3f --- /dev/null +++ b/selfdrive/ui/qt/onroad.h @@ -0,0 +1,281 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "common/util.h" +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/widgets/cameraview.h" + +#include "selfdrive/frogpilot/screenrecorder/screenrecorder.h" + +const int btn_size = 192; +const int img_size = (btn_size / 4) * 3; + +// FrogPilot global variables +static double fps; + +// ***** onroad widgets ***** +class OnroadAlerts : public QWidget { + Q_OBJECT + +public: + OnroadAlerts(QWidget *parent = 0) : QWidget(parent), scene(uiState()->scene) {} + void updateAlert(const Alert &a); + +protected: + void paintEvent(QPaintEvent*) override; + +private: + QColor bg; + Alert alert = {}; + + // FrogPilot variables + UIScene &scene; +}; + +class Compass : public QWidget { +public: + explicit Compass(QWidget *parent = nullptr); + + void initializeStaticElements(); + void updateState(int bearing_deg); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + int bearingDeg; + int circleOffset; + int compassSize; + int degreeLabelOffset; + int innerCompass; + int x; + int y; + QPixmap compassInnerImg; + QPixmap staticElements; +}; + +class ExperimentalButton : public QPushButton { + Q_OBJECT + +public: + explicit ExperimentalButton(QWidget *parent = 0); + void updateState(const UIState &s, bool leadInfo); + +private: + void paintEvent(QPaintEvent *event) override; + void changeMode(); + + Params params; + QPixmap engage_img; + QPixmap experimental_img; + bool experimental_mode; + bool engageable; + + // FrogPilot variables + UIScene &scene; + + std::map wheelImages; + + bool firefoxRandomEventTriggered; + bool rotatingWheel; + int steeringAngleDeg; + int wheelIcon; + int y_offset; +}; + + +class MapSettingsButton : public QPushButton { + Q_OBJECT + +public: + explicit MapSettingsButton(QWidget *parent = 0); + +private: + void paintEvent(QPaintEvent *event) override; + + QPixmap settings_img; +}; + +// FrogPilot buttons +class PersonalityButton : public QPushButton { +public: + explicit PersonalityButton(QWidget *parent = 0); + + void checkUpdate(); + void handleClick(); + void updateState(); + +private: + void paintEvent(QPaintEvent *event) override; + + Params params; + Params paramsMemory{"/dev/shm/params"}; + + UIScene &scene; + + int personalityProfile = 0; + + QElapsedTimer transitionTimer; + + QVector> profile_data; +}; + +// container window for the NVG UI +class AnnotatedCameraWidget : public CameraWidget { + Q_OBJECT + +public: + explicit AnnotatedCameraWidget(VisionStreamType type, QWidget* parent = 0); + void updateState(const UIState &s); + + MapSettingsButton *map_settings_btn; + MapSettingsButton *map_settings_btn_bottom; + +private: + void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255); + + QVBoxLayout *main_layout; + ExperimentalButton *experimental_btn; + QPixmap dm_img; + float speed; + QString speedUnit; + float setSpeed; + float speedLimit; + bool is_cruise_set = false; + bool is_metric = false; + bool dmActive = false; + bool hideBottomIcons = false; + bool rightHandDM = false; + float dm_fade_state = 1.0; + bool has_us_speed_limit = false; + bool has_eu_speed_limit = false; + bool v_ego_cluster_seen = false; + int status = STATUS_DISENGAGED; + std::unique_ptr pm; + + int skip_frame_count = 0; + bool wide_cam_requested = false; + + // FrogPilot widgets + void drawLeadInfo(QPainter &p); + void drawStatusBar(QPainter &p); + void drawTurnSignals(QPainter &p); + void initializeFrogPilotWidgets(); + void updateFrogPilotWidgets(QPainter &p); + + // FrogPilot variables + Params paramsMemory{"/dev/shm/params"}; + + UIScene &scene; + + Compass *compass_img; + PersonalityButton *personality_btn; + ScreenRecorder *recorder_btn; + + QHBoxLayout *bottom_layout; + + bool accelerationPath; + bool adjacentPath; + bool alwaysOnLateral; + bool blindSpotLeft; + bool blindSpotRight; + bool compass; + bool conditionalExperimental; + bool experimentalMode; + bool hideSpeed; + bool leadInfo; + bool mapOpen; + bool muteDM; + bool onroadAdjustableProfiles; + bool reverseCruise; + bool roadNameUI; + bool showDriverCamera; + bool showSLCOffset; + bool slcOverridden; + bool turnSignalLeft; + bool turnSignalRight; + bool useSI; + bool useViennaSLCSign; + double maxAcceleration; + float cruiseAdjustment; + float laneWidthLeft; + float laneWidthRight; + float slcOverriddenSpeed; + float slcSpeedLimit; + float slcSpeedLimitOffset; + int bearingDeg; + int cameraView; + int conditionalSpeed; + int conditionalSpeedLead; + int conditionalStatus; + int customColors; + int customSignals; + int desiredFollow; + int obstacleDistance; + int obstacleDistanceStock; + int stoppedEquivalence; + int totalFrames = 8; + QTimer *animationTimer; + size_t animationFrameIndex; + + inline QColor greenColor(int alpha = 242) { return QColor(23, 134, 68, alpha); } + + std::unordered_map>>> themeConfiguration; + std::vector signalImgVector; + +protected: + void paintGL() override; + void initializeGL() override; + void showEvent(QShowEvent *event) override; + void updateFrameMat() override; + void drawLaneLines(QPainter &painter, const UIState *s); + void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd); + void drawHud(QPainter &p); + void drawDriverState(QPainter &painter, const UIState *s); + void paintEvent(QPaintEvent *event) override; + inline QColor redColor(int alpha = 255) { return QColor(201, 34, 49, alpha); } + inline QColor whiteColor(int alpha = 255) { return QColor(255, 255, 255, alpha); } + inline QColor blackColor(int alpha = 255) { return QColor(0, 0, 0, alpha); } + + double prev_draw_t = 0; + FirstOrderFilter fps_filter; +}; + +// container for all onroad widgets +class OnroadWindow : public QWidget { + Q_OBJECT + +public: + OnroadWindow(QWidget* parent = 0); + bool isMapVisible() const { return map && map->isVisible(); } + void showMapPanel(bool show) { if (map) map->setVisible(show); } + +signals: + void mapPanelRequested(); + +private: + void paintEvent(QPaintEvent *event); + void mousePressEvent(QMouseEvent* e) override; + OnroadAlerts *alerts; + AnnotatedCameraWidget *nvg; + QColor bg = bg_colors[STATUS_DISENGAGED]; + QWidget *map = nullptr; + QHBoxLayout* split; + + // FrogPilot variables + UIScene &scene; + + QPoint timeoutPoint = QPoint(420, 69); + QTimer clickTimer; + +private slots: + void offroadTransition(bool offroad); + void primeChanged(bool prime); + void updateState(const UIState &s); +}; diff --git a/selfdrive/ui/qt/qt_window.cc b/selfdrive/ui/qt/qt_window.cc new file mode 100755 index 0000000..f71cea0 --- /dev/null +++ b/selfdrive/ui/qt/qt_window.cc @@ -0,0 +1,34 @@ +#include "selfdrive/ui/qt/qt_window.h" + +void setMainWindow(QWidget *w) { + const float scale = util::getenv("SCALE", 1.0f); + const QSize sz = QGuiApplication::primaryScreen()->size(); + + if (Hardware::PC() && scale == 1.0 && !(sz - DEVICE_SCREEN_SIZE).isValid()) { + w->setMinimumSize(QSize(640, 480)); // allow resize smaller than fullscreen + w->setMaximumSize(DEVICE_SCREEN_SIZE); + w->resize(sz); + } else { + w->setFixedSize(DEVICE_SCREEN_SIZE * scale); + } + w->show(); + +#ifdef QCOM2 + QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); + wl_surface *s = reinterpret_cast(native->nativeResourceForWindow("surface", w->windowHandle())); + wl_surface_set_buffer_transform(s, WL_OUTPUT_TRANSFORM_270); + wl_surface_commit(s); + w->showFullScreen(); + + // ensure we have a valid eglDisplay, otherwise the ui will silently fail + void *egl = native->nativeResourceForWindow("egldisplay", w->windowHandle()); + assert(egl != nullptr); +#endif +} + + +extern "C" { + void set_main_window(void *w) { + setMainWindow((QWidget*)w); + } +} diff --git a/selfdrive/ui/qt/qt_window.h b/selfdrive/ui/qt/qt_window.h new file mode 100755 index 0000000..6f16e00 --- /dev/null +++ b/selfdrive/ui/qt/qt_window.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include +#include +#include + +#ifdef QCOM2 +#include +#include +#include +#endif + +#include "system/hardware/hw.h" + +const QString ASSET_PATH = ":/"; +const QSize DEVICE_SCREEN_SIZE = {2160, 1080}; + +void setMainWindow(QWidget *w); diff --git a/selfdrive/ui/qt/request_repeater.cc b/selfdrive/ui/qt/request_repeater.cc new file mode 100755 index 0000000..7aa7318 --- /dev/null +++ b/selfdrive/ui/qt/request_repeater.cc @@ -0,0 +1,27 @@ +#include "selfdrive/ui/qt/request_repeater.h" + +RequestRepeater::RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey, + int period, bool while_onroad) : HttpRequest(parent) { + timer = new QTimer(this); + timer->setTimerType(Qt::VeryCoarseTimer); + QObject::connect(timer, &QTimer::timeout, [=]() { + if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) { + sendRequest(requestURL); + } + }); + + timer->start(period * 1000); + + if (!cacheKey.isEmpty()) { + prevResp = QString::fromStdString(params.get(cacheKey.toStdString())); + if (!prevResp.isEmpty()) { + QTimer::singleShot(500, [=]() { emit requestDone(prevResp, true, QNetworkReply::NoError); }); + } + QObject::connect(this, &HttpRequest::requestDone, [=](const QString &resp, bool success) { + if (success && resp != prevResp) { + params.put(cacheKey.toStdString(), resp.toStdString()); + prevResp = resp; + } + }); + } +} diff --git a/selfdrive/ui/qt/request_repeater.h b/selfdrive/ui/qt/request_repeater.h new file mode 100755 index 0000000..c0e2758 --- /dev/null +++ b/selfdrive/ui/qt/request_repeater.h @@ -0,0 +1,15 @@ +#pragma once + +#include "common/util.h" +#include "selfdrive/ui/qt/api.h" +#include "selfdrive/ui/ui.h" + +class RequestRepeater : public HttpRequest { +public: + RequestRepeater(QObject *parent, const QString &requestURL, const QString &cacheKey = "", int period = 0, bool while_onroad=false); + +private: + Params params; + QTimer *timer; + QString prevResp; +}; diff --git a/selfdrive/ui/qt/setup/reset.cc b/selfdrive/ui/qt/setup/reset.cc new file mode 100755 index 0000000..7999dd6 --- /dev/null +++ b/selfdrive/ui/qt/setup/reset.cc @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include +#include + +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/setup/reset.h" + +#define NVME "/dev/nvme0n1" +#define USERDATA "/dev/disk/by-partlabel/userdata" + +void Reset::doErase() { + // best effort to wipe nvme + std::system("sudo umount " NVME); + std::system("yes | sudo mkfs.ext4 " NVME); + + int rm = std::system("sudo rm -rf /data/*"); + std::system("sudo umount " USERDATA); + int fmt = std::system("yes | sudo mkfs.ext4 " USERDATA); + + if (rm == 0 || fmt == 0) { + std::system("sudo reboot"); + } + body->setText(tr("Reset failed. Reboot to try again.")); + rebootBtn->show(); +} + +void Reset::startReset() { + body->setText(tr("Resetting device...\nThis may take up to a minute.")); + rejectBtn->hide(); + rebootBtn->hide(); + confirmBtn->hide(); +#ifdef __aarch64__ + QTimer::singleShot(100, this, &Reset::doErase); +#endif +} + +void Reset::confirm() { + const QString confirm_txt = tr("Are you sure you want to reset your device?"); + if (body->text() != confirm_txt) { + body->setText(confirm_txt); + } else { + startReset(); + } +} + +Reset::Reset(ResetMode mode, QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(45, 220, 45, 45); + main_layout->setSpacing(0); + + QLabel *title = new QLabel(tr("System Reset")); + title->setStyleSheet("font-size: 90px; font-weight: 600;"); + main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); + + main_layout->addSpacing(60); + + body = new QLabel(tr("Press confirm to erase all content and settings. Press cancel to resume boot.")); + body->setWordWrap(true); + body->setStyleSheet("font-size: 80px; font-weight: light;"); + main_layout->addWidget(body, 1, Qt::AlignTop | Qt::AlignLeft); + + QHBoxLayout *blayout = new QHBoxLayout(); + main_layout->addLayout(blayout); + blayout->setSpacing(50); + + rejectBtn = new QPushButton(tr("Cancel")); + blayout->addWidget(rejectBtn); + QObject::connect(rejectBtn, &QPushButton::clicked, QCoreApplication::instance(), &QCoreApplication::quit); + + rebootBtn = new QPushButton(tr("Reboot")); + blayout->addWidget(rebootBtn); +#ifdef __aarch64__ + QObject::connect(rebootBtn, &QPushButton::clicked, [=]{ + std::system("sudo reboot"); + }); +#endif + + confirmBtn = new QPushButton(tr("Confirm")); + confirmBtn->setStyleSheet(R"( + QPushButton { + background-color: #465BEA; + } + QPushButton:pressed { + background-color: #3049F4; + } + )"); + blayout->addWidget(confirmBtn); + QObject::connect(confirmBtn, &QPushButton::clicked, this, &Reset::confirm); + + bool recover = mode == ResetMode::RECOVER; + rejectBtn->setVisible(!recover); + rebootBtn->setVisible(recover); + if (recover) { + body->setText(tr("Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device.")); + } + + // automatically start if we're just finishing up an ABL reset + if (mode == ResetMode::FORMAT) { + startReset(); + } + + setStyleSheet(R"( + * { + font-family: Inter; + color: white; + background-color: black; + } + QLabel { + margin-left: 140; + } + QPushButton { + height: 160; + font-size: 55px; + font-weight: 400; + border-radius: 10px; + background-color: #333333; + } + QPushButton:pressed { + background-color: #444444; + } + )"); +} + +int main(int argc, char *argv[]) { + ResetMode mode = ResetMode::USER_RESET; + if (argc > 1) { + if (strcmp(argv[1], "--recover") == 0) { + mode = ResetMode::RECOVER; + } else if (strcmp(argv[1], "--format") == 0) { + mode = ResetMode::FORMAT; + } + } + + QApplication a(argc, argv); + Reset reset(mode); + setMainWindow(&reset); + return a.exec(); +} diff --git a/selfdrive/ui/qt/setup/reset.h b/selfdrive/ui/qt/setup/reset.h new file mode 100755 index 0000000..04a191d --- /dev/null +++ b/selfdrive/ui/qt/setup/reset.h @@ -0,0 +1,27 @@ +#include +#include +#include + +enum ResetMode { + USER_RESET, // user initiated a factory reset from openpilot + RECOVER, // userdata is corrupt for some reason, give a chance to recover + FORMAT, // finish up an ABL factory reset +}; + +class Reset : public QWidget { + Q_OBJECT + +public: + explicit Reset(ResetMode mode, QWidget *parent = 0); + +private: + QLabel *body; + QPushButton *rejectBtn; + QPushButton *rebootBtn; + QPushButton *confirmBtn; + void doErase(); + void startReset(); + +private slots: + void confirm(); +}; diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc new file mode 100755 index 0000000..67f1c13 --- /dev/null +++ b/selfdrive/ui/qt/setup/setup.cc @@ -0,0 +1,399 @@ +#include "selfdrive/ui/qt/setup/setup.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "common/util.h" +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/api.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/network/networking.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/input.h" + +const std::string USER_AGENT = "AGNOSSetup-"; +const QString DASHCAM_URL = "https://dashcam.comma.ai"; + +bool is_elf(char *fname) { + FILE *fp = fopen(fname, "rb"); + if (fp == NULL) { + return false; + } + char buf[4]; + size_t n = fread(buf, 1, 4, fp); + fclose(fp); + return n == 4 && buf[0] == 0x7f && buf[1] == 'E' && buf[2] == 'L' && buf[3] == 'F'; +} + +void Setup::download(QString url) { + // autocomplete incomplete urls + if (QRegularExpression("^([^/.]+)/([^/]+)$").match(url).hasMatch()) { + url.prepend("https://installer.comma.ai/"); + } + + CURL *curl = curl_easy_init(); + if (!curl) { + emit finished(url, tr("Something went wrong. Reboot the device.")); + return; + } + + auto version = util::read_file("/VERSION"); + + struct curl_slist *list = NULL; + list = curl_slist_append(list, ("X-openpilot-serial: " + Hardware::get_serial()).c_str()); + + char tmpfile[] = "/tmp/installer_XXXXXX"; + FILE *fp = fdopen(mkstemp(tmpfile), "w"); + + curl_easy_setopt(curl, CURLOPT_URL, url.toStdString().c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, (USER_AGENT + version).c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); + + int ret = curl_easy_perform(curl); + long res_status = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &res_status); + + if (ret != CURLE_OK || res_status != 200) { + emit finished(url, tr("Ensure the entered URL is valid, and the device’s internet connection is good.")); + } else if (!is_elf(tmpfile)) { + emit finished(url, tr("No custom software found at this URL.")); + } else { + rename(tmpfile, "/tmp/installer"); + + FILE *fp_url = fopen("/tmp/installer_url", "w"); + fprintf(fp_url, "%s", url.toStdString().c_str()); + fclose(fp_url); + + emit finished(url); + } + + curl_slist_free_all(list); + curl_easy_cleanup(curl); + fclose(fp); +} + +QWidget * Setup::low_voltage() { + QWidget *widget = new QWidget(); + QVBoxLayout *main_layout = new QVBoxLayout(widget); + main_layout->setContentsMargins(55, 0, 55, 55); + main_layout->setSpacing(0); + + // inner text layout: warning icon, title, and body + QVBoxLayout *inner_layout = new QVBoxLayout(); + inner_layout->setContentsMargins(110, 144, 365, 0); + main_layout->addLayout(inner_layout); + + QLabel *triangle = new QLabel(); + triangle->setPixmap(QPixmap(ASSET_PATH + "offroad/icon_warning.png")); + inner_layout->addWidget(triangle, 0, Qt::AlignTop | Qt::AlignLeft); + inner_layout->addSpacing(80); + + QLabel *title = new QLabel(tr("WARNING: Low Voltage")); + title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;"); + inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); + + inner_layout->addSpacing(25); + + QLabel *body = new QLabel(tr("Power your device in a car with a harness or proceed at your own risk.")); + body->setWordWrap(true); + body->setAlignment(Qt::AlignTop | Qt::AlignLeft); + body->setStyleSheet("font-size: 80px; font-weight: 300;"); + inner_layout->addWidget(body); + + inner_layout->addStretch(); + + // power off + continue buttons + QHBoxLayout *blayout = new QHBoxLayout(); + blayout->setSpacing(50); + main_layout->addLayout(blayout, 0); + + QPushButton *poweroff = new QPushButton(tr("Power off")); + poweroff->setObjectName("navBtn"); + blayout->addWidget(poweroff); + QObject::connect(poweroff, &QPushButton::clicked, this, [=]() { + Hardware::poweroff(); + }); + + QPushButton *cont = new QPushButton(tr("Continue")); + cont->setObjectName("navBtn"); + blayout->addWidget(cont); + QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); + + return widget; +} + +QWidget * Setup::getting_started() { + QWidget *widget = new QWidget(); + + QHBoxLayout *main_layout = new QHBoxLayout(widget); + main_layout->setMargin(0); + + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setContentsMargins(165, 280, 100, 0); + main_layout->addLayout(vlayout); + + QLabel *title = new QLabel(tr("Getting Started")); + title->setStyleSheet("font-size: 90px; font-weight: 500;"); + vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); + + vlayout->addSpacing(90); + QLabel *desc = new QLabel(tr("Before we get on the road, let’s finish installation and cover some details.")); + desc->setWordWrap(true); + desc->setStyleSheet("font-size: 80px; font-weight: 300;"); + vlayout->addWidget(desc, 0, Qt::AlignTop | Qt::AlignLeft); + + vlayout->addStretch(); + + QPushButton *btn = new QPushButton(); + btn->setIcon(QIcon(":/img_continue_triangle.svg")); + btn->setIconSize(QSize(54, 106)); + btn->setFixedSize(310, 1080); + btn->setProperty("primary", true); + btn->setStyleSheet("border: none;"); + main_layout->addWidget(btn, 0, Qt::AlignRight); + QObject::connect(btn, &QPushButton::clicked, this, &Setup::nextPage); + + return widget; +} + +QWidget * Setup::network_setup() { + QWidget *widget = new QWidget(); + QVBoxLayout *main_layout = new QVBoxLayout(widget); + main_layout->setContentsMargins(55, 50, 55, 50); + + // title + QLabel *title = new QLabel(tr("Connect to Wi-Fi")); + title->setStyleSheet("font-size: 90px; font-weight: 500;"); + main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); + + main_layout->addSpacing(25); + + // wifi widget + Networking *networking = new Networking(this, false); + networking->setStyleSheet("Networking {background-color: #292929; border-radius: 13px;}"); + main_layout->addWidget(networking, 1); + + main_layout->addSpacing(35); + + // back + continue buttons + QHBoxLayout *blayout = new QHBoxLayout; + main_layout->addLayout(blayout); + blayout->setSpacing(50); + + QPushButton *back = new QPushButton(tr("Back")); + back->setObjectName("navBtn"); + QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); + blayout->addWidget(back); + + QPushButton *cont = new QPushButton(); + cont->setObjectName("navBtn"); + cont->setProperty("primary", true); + QObject::connect(cont, &QPushButton::clicked, [=]() { + auto w = currentWidget(); + QTimer::singleShot(0, [=]() { + setCurrentWidget(downloading_widget); + }); + QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software")); + if (!url.isEmpty()) { + QTimer::singleShot(1000, this, [=]() { + download(url); + }); + } else { + setCurrentWidget(w); + } + }); + blayout->addWidget(cont); + + // setup timer for testing internet connection + HttpRequest *request = new HttpRequest(this, false, 2500); + QObject::connect(request, &HttpRequest::requestDone, [=](const QString &, bool success) { + cont->setEnabled(success); + if (success) { + const bool wifi = networking->wifi->currentNetworkType() == NetworkType::WIFI; + cont->setText(wifi ? tr("Continue") : tr("Continue without Wi-Fi")); + } else { + cont->setText(tr("Waiting for internet")); + } + repaint(); + }); + request->sendRequest(DASHCAM_URL); + QTimer *timer = new QTimer(this); + QObject::connect(timer, &QTimer::timeout, [=]() { + if (!request->active() && cont->isVisible()) { + request->sendRequest(DASHCAM_URL); + } + }); + timer->start(1000); + + return widget; +} + +QWidget * Setup::downloading() { + QWidget *widget = new QWidget(); + QVBoxLayout *main_layout = new QVBoxLayout(widget); + QLabel *txt = new QLabel(tr("Downloading...")); + txt->setStyleSheet("font-size: 90px; font-weight: 500;"); + main_layout->addWidget(txt, 0, Qt::AlignCenter); + return widget; +} + +QWidget * Setup::download_failed(QLabel *url, QLabel *body) { + QWidget *widget = new QWidget(); + QVBoxLayout *main_layout = new QVBoxLayout(widget); + main_layout->setContentsMargins(55, 185, 55, 55); + main_layout->setSpacing(0); + + QLabel *title = new QLabel(tr("Download Failed")); + title->setStyleSheet("font-size: 90px; font-weight: 500;"); + main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); + + main_layout->addSpacing(67); + + url->setWordWrap(true); + url->setAlignment(Qt::AlignTop | Qt::AlignLeft); + url->setStyleSheet("font-family: \"JetBrains Mono\"; font-size: 64px; font-weight: 400; margin-right: 100px;"); + main_layout->addWidget(url); + + main_layout->addSpacing(48); + + body->setWordWrap(true); + body->setAlignment(Qt::AlignTop | Qt::AlignLeft); + body->setStyleSheet("font-size: 80px; font-weight: 300; margin-right: 100px;"); + main_layout->addWidget(body); + + main_layout->addStretch(); + + // reboot + start over buttons + QHBoxLayout *blayout = new QHBoxLayout(); + blayout->setSpacing(50); + main_layout->addLayout(blayout, 0); + + QPushButton *reboot = new QPushButton(tr("Reboot device")); + reboot->setObjectName("navBtn"); + blayout->addWidget(reboot); + QObject::connect(reboot, &QPushButton::clicked, this, [=]() { + Hardware::reboot(); + }); + + QPushButton *restart = new QPushButton(tr("Start over")); + restart->setObjectName("navBtn"); + restart->setProperty("primary", true); + blayout->addWidget(restart); + QObject::connect(restart, &QPushButton::clicked, this, [=]() { + setCurrentIndex(1); + }); + + widget->setStyleSheet(R"( + QLabel { + margin-left: 117; + } + )"); + return widget; +} + +void Setup::prevPage() { + setCurrentIndex(currentIndex() - 1); +} + +void Setup::nextPage() { + setCurrentIndex(currentIndex() + 1); +} + +Setup::Setup(QWidget *parent) : QStackedWidget(parent) { + if (std::getenv("MULTILANG")) { + selectLanguage(); + } + + std::stringstream buffer; + buffer << std::ifstream("/sys/class/hwmon/hwmon1/in1_input").rdbuf(); + float voltage = (float)std::atoi(buffer.str().c_str()) / 1000.; + if (voltage < 7) { + addWidget(low_voltage()); + } + + addWidget(getting_started()); + addWidget(network_setup()); + + downloading_widget = downloading(); + addWidget(downloading_widget); + + QLabel *url_label = new QLabel(); + QLabel *body_label = new QLabel(); + failed_widget = download_failed(url_label, body_label); + addWidget(failed_widget); + + QObject::connect(this, &Setup::finished, [=](const QString &url, const QString &error) { + qDebug() << "finished" << url << error; + if (error.isEmpty()) { + // hide setup on success + QTimer::singleShot(3000, this, &QWidget::hide); + } else { + url_label->setText(url); + body_label->setText(error); + setCurrentWidget(failed_widget); + } + }); + + // TODO: revisit pressed bg color + setStyleSheet(R"( + * { + color: white; + font-family: Inter; + } + Setup { + background-color: black; + } + QPushButton#navBtn { + height: 160; + font-size: 55px; + font-weight: 400; + border-radius: 10px; + background-color: #333333; + } + QPushButton#navBtn:disabled, QPushButton[primary='true']:disabled { + color: #808080; + background-color: #333333; + } + QPushButton#navBtn:pressed { + background-color: #444444; + } + QPushButton[primary='true'], #navBtn[primary='true'] { + background-color: #465BEA; + } + QPushButton[primary='true']:pressed, #navBtn:pressed[primary='true'] { + background-color: #3049F4; + } + )"); +} + +void Setup::selectLanguage() { + QMap langs = getSupportedLanguages(); + QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), "", this); + if (!selection.isEmpty()) { + QString selectedLang = langs[selection]; + Params().put("LanguageSetting", selectedLang.toStdString()); + if (translator.load(":/" + selectedLang)) { + qApp->installTranslator(&translator); + } + } +} + +int main(int argc, char *argv[]) { + QApplication a(argc, argv); + Setup setup; + setMainWindow(&setup); + return a.exec(); +} diff --git a/selfdrive/ui/qt/setup/setup.h b/selfdrive/ui/qt/setup/setup.h new file mode 100755 index 0000000..8c33acc --- /dev/null +++ b/selfdrive/ui/qt/setup/setup.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Setup : public QStackedWidget { + Q_OBJECT + +public: + explicit Setup(QWidget *parent = 0); + +private: + void selectLanguage(); + QWidget *low_voltage(); + QWidget *getting_started(); + QWidget *network_setup(); + QWidget *downloading(); + QWidget *download_failed(QLabel *url, QLabel *body); + + QWidget *failed_widget; + QWidget *downloading_widget; + QTranslator translator; + +signals: + void finished(const QString &url, const QString &error = ""); + +public slots: + void nextPage(); + void prevPage(); + void download(QString url); +}; diff --git a/selfdrive/ui/qt/setup/updater.cc b/selfdrive/ui/qt/setup/updater.cc new file mode 100755 index 0000000..15c5332 --- /dev/null +++ b/selfdrive/ui/qt/setup/updater.cc @@ -0,0 +1,215 @@ +#include +#include +#include + +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/setup/updater.h" +#include "selfdrive/ui/qt/network/networking.h" + +Updater::Updater(const QString &updater_path, const QString &manifest_path, QWidget *parent) + : updater(updater_path), manifest(manifest_path), QStackedWidget(parent) { + + assert(updater.size()); + assert(manifest.size()); + + // initial prompt screen + prompt = new QWidget; + { + QVBoxLayout *layout = new QVBoxLayout(prompt); + layout->setContentsMargins(100, 250, 100, 100); + + QLabel *title = new QLabel(tr("Update Required")); + title->setStyleSheet("font-size: 80px; font-weight: bold;"); + layout->addWidget(title); + + layout->addSpacing(75); + + QLabel *desc = new QLabel(tr("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB.")); + desc->setWordWrap(true); + desc->setStyleSheet("font-size: 65px;"); + layout->addWidget(desc); + + layout->addStretch(); + + QHBoxLayout *hlayout = new QHBoxLayout; + hlayout->setSpacing(30); + layout->addLayout(hlayout); + + QPushButton *connect = new QPushButton(tr("Connect to Wi-Fi")); + connect->setObjectName("navBtn"); + QObject::connect(connect, &QPushButton::clicked, [=]() { + setCurrentWidget(wifi); + }); + hlayout->addWidget(connect); + + QPushButton *install = new QPushButton(tr("Install")); + install->setObjectName("navBtn"); + install->setStyleSheet(R"( + QPushButton { + background-color: #465BEA; + } + QPushButton:pressed { + background-color: #3049F4; + } + )"); + + QObject::connect(connect, &QPushButton::clicked, [=]() { + countdownTimer->stop(); + setCurrentWidget(wifi); + }); + + hlayout->addWidget(install); + } + + // wifi connection screen + wifi = new QWidget; + { + QVBoxLayout *layout = new QVBoxLayout(wifi); + layout->setContentsMargins(100, 100, 100, 100); + + Networking *networking = new Networking(this, false); + networking->setStyleSheet("Networking { background-color: #292929; border-radius: 13px; }"); + layout->addWidget(networking, 1); + + QPushButton *back = new QPushButton(tr("Back")); + back->setObjectName("navBtn"); + back->setStyleSheet("padding-left: 60px; padding-right: 60px;"); + QObject::connect(back, &QPushButton::clicked, [=]() { + setCurrentWidget(prompt); + }); + layout->addWidget(back, 0, Qt::AlignLeft); + } + + // progress screen + progress = new QWidget; + { + QVBoxLayout *layout = new QVBoxLayout(progress); + layout->setContentsMargins(150, 330, 150, 150); + layout->setSpacing(0); + + text = new QLabel(tr("Loading...")); + text->setStyleSheet("font-size: 90px; font-weight: 600;"); + layout->addWidget(text, 0, Qt::AlignTop); + + layout->addSpacing(100); + + bar = new QProgressBar(); + bar->setRange(0, 100); + bar->setTextVisible(false); + bar->setFixedHeight(72); + layout->addWidget(bar, 0, Qt::AlignTop); + + layout->addStretch(); + + reboot = new QPushButton(tr("Reboot")); + reboot->setObjectName("navBtn"); + reboot->setStyleSheet("padding-left: 60px; padding-right: 60px;"); + QObject::connect(reboot, &QPushButton::clicked, [=]() { + Hardware::reboot(); + }); + layout->addWidget(reboot, 0, Qt::AlignLeft); + reboot->hide(); + + layout->addStretch(); + } + + addWidget(prompt); + addWidget(wifi); + addWidget(progress); + + // Initialize the countdown timer and value + countdownValue = 5; // 5 seconds countdown + countdownTimer = new QTimer(this); + countdownTimer->setInterval(1000); // 1 second interval + + // Connect the timer's timeout signal to update the countdown and button text + QObject::connect(countdownTimer, &QTimer::timeout, this, &Updater::updateCountdown); + + // Start the countdown + countdownTimer->start(); + + // Set initial button text + install->setText(tr("Install (5)")); + + setStyleSheet(R"( + * { + color: white; + outline: none; + font-family: Inter; + } + Updater { + color: white; + background-color: black; + } + QPushButton#navBtn { + height: 160; + font-size: 55px; + font-weight: 400; + border-radius: 10px; + background-color: #333333; + } + QPushButton#navBtn:pressed { + background-color: #444444; + } + QProgressBar { + border: none; + background-color: #292929; + } + QProgressBar::chunk { + background-color: #364DEF; + } + )"); +} + +void Updater::updateCountdown() { + countdownValue--; + if (countdownValue > 0) { + install->setText(tr("Install (%1)").arg(countdownValue)); + } else { + countdownTimer->stop(); + installUpdate(); // Assuming this is the method that starts the update + } +} + +void Updater::installUpdate() { + setCurrentWidget(progress); + QObject::connect(&proc, &QProcess::readyReadStandardOutput, this, &Updater::readProgress); + QObject::connect(&proc, QOverload::of(&QProcess::finished), this, &Updater::updateFinished); + proc.setProcessChannelMode(QProcess::ForwardedErrorChannel); + proc.start(updater, {"--swap", manifest}); +} + +void Updater::readProgress() { + auto lines = QString(proc.readAllStandardOutput()); + for (const QString &line : lines.trimmed().split("\n")) { + auto parts = line.split(":"); + if (parts.size() == 2) { + text->setText(parts[0]); + bar->setValue((int)parts[1].toDouble()); + } else { + qDebug() << line; + } + } + update(); +} + +void Updater::updateFinished(int exitCode, QProcess::ExitStatus exitStatus) { + qDebug() << "finished with " << exitCode; + if (exitCode == 0) { + Hardware::reboot(); + } else { + text->setText(tr("Update failed")); + reboot->show(); + } +} + +int main(int argc, char *argv[]) { + initApp(argc, argv); + QApplication a(argc, argv); + Updater updater(argv[1], argv[2]); + setMainWindow(&updater); + a.installEventFilter(&updater); + return a.exec(); +} diff --git a/selfdrive/ui/qt/setup/updater.h b/selfdrive/ui/qt/setup/updater.h new file mode 100755 index 0000000..ba9737f --- /dev/null +++ b/selfdrive/ui/qt/setup/updater.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class Updater : public QStackedWidget { + Q_OBJECT + +public: + explicit Updater(const QString &updater_path, const QString &manifest_path, QWidget *parent = 0); + +private slots: + void installUpdate(); + void readProgress(); + void updateFinished(int exitCode, QProcess::ExitStatus exitStatus); + +private: + QProcess proc; + QString updater, manifest; + + QLabel *text; + QProgressBar *bar; + QPushButton *reboot; + QWidget *prompt, *wifi, *progress; + QTimer *countdownTimer; + int countdownValue; + +}; diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc new file mode 100755 index 0000000..55e9597 --- /dev/null +++ b/selfdrive/ui/qt/sidebar.cc @@ -0,0 +1,283 @@ +#include "selfdrive/ui/qt/sidebar.h" + +#include + +#include "selfdrive/ui/qt/util.h" + +void Sidebar::drawMetric(QPainter &p, const QPair &label, QColor c, int y) { + const QRect rect = {30, y, 240, 126}; + + p.setPen(Qt::NoPen); + p.setBrush(QBrush(c)); + p.setClipRect(rect.x() + 4, rect.y(), 18, rect.height(), Qt::ClipOperation::ReplaceClip); + p.drawRoundedRect(QRect(rect.x() + 4, rect.y() + 4, 100, 118), 18, 18); + p.setClipping(false); + + QPen pen = QPen(QColor(0xff, 0xff, 0xff, 0x55)); + pen.setWidth(2); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(rect, 20, 20); + + p.setPen(QColor(0xff, 0xff, 0xff)); + p.setFont(InterFont(35, QFont::DemiBold)); + p.drawText(rect.adjusted(22, 0, 0, 0), Qt::AlignCenter, label.first + "\n" + label.second); +} + +Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false), scene(uiState()->scene) { + home_img = loadPixmap("../assets/images/button_home.png", home_btn.size()); + flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size()); + settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio); + + connect(this, &Sidebar::valueChanged, [=] { update(); }); + + setAttribute(Qt::WA_OpaquePaintEvent); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); + setFixedWidth(300); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); + + pm = std::make_unique>({"userFlag"}); + + // FrogPilot variables + isCPU = params.getBool("ShowCPU"); + isGPU = params.getBool("ShowGPU"); + + isMemoryUsage = params.getBool("ShowMemoryUsage"); + isStorageLeft = params.getBool("ShowStorageLeft"); + isStorageUsed = params.getBool("ShowStorageUsed"); + + isNumericalTemp = params.getBool("NumericalTemp"); + isFahrenheit = params.getBool("Fahrenheit"); + + themeConfiguration = { + {0, {"stock", {QColor(255, 255, 255)}}}, + {1, {"frog_theme", {QColor(0, 72, 255)}}}, + {2, {"tesla_theme", {QColor(0, 72, 255)}}}, + {3, {"stalin_theme", {QColor(255, 0, 0)}}} + }; + + for (auto &[key, themeData] : themeConfiguration) { + QString &themeName = themeData.first; + QString base = themeName == "stock" ? "../assets/images" : QString("../frogpilot/assets/custom_themes/%1/images").arg(themeName); + std::vector paths = {base + "/button_home.png", base + "/button_flag.png", base + "/button_settings.png"}; + + home_imgs[key] = loadPixmap(paths[0], home_btn.size()); + flag_imgs[key] = loadPixmap(paths[1], home_btn.size()); + settings_imgs[key] = loadPixmap(paths[2], settings_btn.size(), Qt::IgnoreAspectRatio); + } + + home_img = home_imgs[scene.custom_icons]; + flag_img = flag_imgs[scene.custom_icons]; + settings_img = settings_imgs[scene.custom_icons]; + + currentColors = themeConfiguration[scene.custom_colors].second; +} + +void Sidebar::mousePressEvent(QMouseEvent *event) { + // Declare the click boxes + QRect cpuRect = {30, 496, 240, 126}; + QRect memoryRect = {30, 654, 240, 126}; + QRect tempRect = {30, 338, 240, 126}; + + static int showChip = 0; + static int showMemory = 0; + static int showTemp = 0; + + // Swap between the respective metrics upon tap + if (cpuRect.contains(event->pos())) { + showChip = (showChip + 1) % 3; + isCPU = (showChip == 1); + isGPU = (showChip == 2); + params.putBoolNonBlocking("ShowCPU", isCPU); + params.putBoolNonBlocking("ShowGPU", isGPU); + update(); + } else if (memoryRect.contains(event->pos())) { + showMemory = (showMemory + 1) % 4; + isMemoryUsage = (showMemory == 1); + isStorageLeft = (showMemory == 2); + isStorageUsed = (showMemory == 3); + params.putBoolNonBlocking("ShowMemoryUsage", isMemoryUsage); + params.putBoolNonBlocking("ShowStorageLeft", isStorageLeft); + params.putBoolNonBlocking("ShowStorageUsed", isStorageUsed); + update(); + } else if (tempRect.contains(event->pos())) { + showTemp = (showTemp + 1) % 3; + isNumericalTemp = (showTemp != 0); + isFahrenheit = (showTemp == 2); + params.putBoolNonBlocking("Fahrenheit", isFahrenheit); + params.putBoolNonBlocking("NumericalTemp", isNumericalTemp); + update(); + } else if (onroad && home_btn.contains(event->pos())) { + flag_pressed = true; + update(); + } else if (settings_btn.contains(event->pos())) { + settings_pressed = true; + update(); + } +} + +void Sidebar::mouseReleaseEvent(QMouseEvent *event) { + if (flag_pressed || settings_pressed) { + flag_pressed = settings_pressed = false; + update(); + } + if (home_btn.contains(event->pos())) { + MessageBuilder msg; + msg.initEvent().initUserFlag(); + pm->send("userFlag", msg); + } else if (settings_btn.contains(event->pos())) { + emit openSettings(); + } +} + +void Sidebar::offroadTransition(bool offroad) { + onroad = !offroad; + update(); +} + +void Sidebar::updateState(const UIState &s) { + if (!isVisible()) return; + + auto &sm = *(s.sm); + + auto deviceState = sm["deviceState"].getDeviceState(); + setProperty("netType", network_type[deviceState.getNetworkType()]); + int strength = (int)deviceState.getNetworkStrength(); + setProperty("netStrength", strength > 0 ? strength + 1 : 0); + + // FrogPilot properties + home_img = home_imgs[scene.custom_icons]; + flag_img = flag_imgs[scene.custom_icons]; + settings_img = settings_imgs[scene.custom_icons]; + + currentColors = themeConfiguration[scene.custom_colors].second; + + auto frogpilotDeviceState = sm["frogpilotDeviceState"].getFrogpilotDeviceState(); + + int maxTempC = deviceState.getMaxTempC(); + QString max_temp = isFahrenheit ? QString::number(maxTempC * 9 / 5 + 32) + "°F" : QString::number(maxTempC) + "°C"; + QColor theme_color = currentColors[0]; + + // FrogPilot metrics + if (isCPU || isGPU) { + auto cpu_loads = deviceState.getCpuUsagePercent(); + int cpu_usage = std::accumulate(cpu_loads.begin(), cpu_loads.end(), 0) / cpu_loads.size(); + int gpu_usage = deviceState.getGpuUsagePercent(); + + QString cpu = QString::number(cpu_usage) + "%"; + QString gpu = QString::number(gpu_usage) + "%"; + + QString metric = isGPU ? gpu : cpu; + int usage = isGPU ? gpu_usage : cpu_usage; + + ItemStatus cpuStatus = {{tr(isGPU ? "GPU" : "CPU"), metric}, theme_color}; + if (usage >= 85) { + cpuStatus = {{tr(isGPU ? "GPU" : "CPU"), metric}, danger_color}; + } else if (usage >= 70) { + cpuStatus = {{tr(isGPU ? "GPU" : "CPU"), metric}, warning_color}; + } + setProperty("cpuStatus", QVariant::fromValue(cpuStatus)); + } + + if (isMemoryUsage || isStorageLeft || isStorageUsed) { + int memory_usage = deviceState.getMemoryUsagePercent(); + int storage_left = frogpilotDeviceState.getFreeSpace(); + int storage_used = frogpilotDeviceState.getUsedSpace(); + + QString memory = QString::number(memory_usage) + "%"; + QString storage = QString::number(isStorageLeft ? storage_left : storage_used) + " GB"; + + if (isMemoryUsage) { + ItemStatus memoryStatus = {{tr("MEMORY"), memory}, theme_color}; + if (memory_usage >= 85) { + memoryStatus = {{tr("MEMORY"), memory}, danger_color}; + } else if (memory_usage >= 70) { + memoryStatus = {{tr("MEMORY"), memory}, warning_color}; + } + setProperty("memoryStatus", QVariant::fromValue(memoryStatus)); + } else { + ItemStatus storageStatus = {{tr(isStorageLeft ? "LEFT" : "USED"), storage}, theme_color}; + if (10 <= storage_left && storage_left < 25) { + storageStatus = {{tr(isStorageLeft ? "LEFT" : "USED"), storage}, warning_color}; + } else if (storage_left < 10) { + storageStatus = {{tr(isStorageLeft ? "LEFT" : "USED"), storage}, danger_color}; + } + setProperty("storageStatus", QVariant::fromValue(storageStatus)); + } + } + + ItemStatus connectStatus; + auto last_ping = deviceState.getLastAthenaPingTime(); + if (last_ping == 0) { + connectStatus = ItemStatus{{tr("CONNECT"), tr("OFFLINE")}, warning_color}; + } else { + connectStatus = nanos_since_boot() - last_ping < 80e9 + ? ItemStatus{{tr("CONNECT"), tr("ONLINE")}, theme_color} + : ItemStatus{{tr("CONNECT"), tr("ERROR")}, danger_color}; + } + setProperty("connectStatus", QVariant::fromValue(connectStatus)); + + ItemStatus tempStatus = {{tr("TEMP"), isNumericalTemp ? max_temp : tr("HIGH")}, danger_color}; + auto ts = deviceState.getThermalStatus(); + if (ts == cereal::DeviceState::ThermalStatus::GREEN) { + tempStatus = {{tr("TEMP"), isNumericalTemp ? max_temp : tr("GOOD")}, theme_color}; + } else if (ts == cereal::DeviceState::ThermalStatus::YELLOW) { + tempStatus = {{tr("TEMP"), isNumericalTemp ? max_temp : tr("OK")}, warning_color}; + } + setProperty("tempStatus", QVariant::fromValue(tempStatus)); + + ItemStatus pandaStatus = {{tr("VEHICLE"), tr("ONLINE")}, theme_color}; + if (s.scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) { + pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color}; + } else if (s.scene.started && !sm["liveLocationKalman"].getLiveLocationKalman().getGpsOK()) { + pandaStatus = {{tr("GPS"), tr("SEARCH")}, warning_color}; + } + setProperty("pandaStatus", QVariant::fromValue(pandaStatus)); +} + +void Sidebar::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.setPen(Qt::NoPen); + p.setRenderHint(QPainter::Antialiasing); + + p.fillRect(rect(), QColor(57, 57, 57)); + + // buttons + p.setOpacity(settings_pressed ? 0.65 : 1.0); + p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_img); + p.setOpacity(onroad && flag_pressed ? 0.65 : 1.0); + p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img); + p.setOpacity(1.0); + + // network + int x = 58; + const QColor gray(0x54, 0x54, 0x54); + for (int i = 0; i < 5; ++i) { + p.setBrush(i < net_strength ? Qt::white : gray); + p.drawEllipse(x, 196, 27, 27); + x += 37; + } + + p.setFont(InterFont(35)); + p.setPen(QColor(0xff, 0xff, 0xff)); + const QRect r = QRect(50, 247, 100, 50); + p.drawText(r, Qt::AlignCenter, net_type); + + // metrics + drawMetric(p, temp_status.first, temp_status.second, 338); + + if (isCPU || isGPU) { + drawMetric(p, cpu_status.first, cpu_status.second, 496); + } else { + drawMetric(p, panda_status.first, panda_status.second, 496); + } + + if (isMemoryUsage) { + drawMetric(p, memory_status.first, memory_status.second, 654); + } else if (isStorageLeft || isStorageUsed) { + drawMetric(p, storage_status.first, storage_status.second, 654); + } else { + drawMetric(p, connect_status.first, connect_status.second, 654); + } +} diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h new file mode 100755 index 0000000..e3a4d55 --- /dev/null +++ b/selfdrive/ui/qt/sidebar.h @@ -0,0 +1,87 @@ +#pragma once + +#include + +#include +#include + +#include "selfdrive/ui/ui.h" + +typedef QPair, QColor> ItemStatus; +Q_DECLARE_METATYPE(ItemStatus); + +class Sidebar : public QFrame { + Q_OBJECT + Q_PROPERTY(ItemStatus connectStatus MEMBER connect_status NOTIFY valueChanged); + Q_PROPERTY(ItemStatus pandaStatus MEMBER panda_status NOTIFY valueChanged); + Q_PROPERTY(ItemStatus tempStatus MEMBER temp_status NOTIFY valueChanged); + Q_PROPERTY(QString netType MEMBER net_type NOTIFY valueChanged); + Q_PROPERTY(int netStrength MEMBER net_strength NOTIFY valueChanged); + + // FrogPilot properties + Q_PROPERTY(ItemStatus cpuStatus MEMBER cpu_status NOTIFY valueChanged) + Q_PROPERTY(ItemStatus memoryStatus MEMBER memory_status NOTIFY valueChanged) + Q_PROPERTY(ItemStatus storageStatus MEMBER storage_status NOTIFY valueChanged) + +public: + explicit Sidebar(QWidget* parent = 0); + +signals: + void openSettings(int index = 0, const QString ¶m = ""); + void valueChanged(); + +public slots: + void offroadTransition(bool offroad); + void updateState(const UIState &s); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void drawMetric(QPainter &p, const QPair &label, QColor c, int y); + + QPixmap home_img, flag_img, settings_img; + bool onroad, flag_pressed, settings_pressed; + const QMap network_type = { + {cereal::DeviceState::NetworkType::NONE, tr("--")}, + {cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")}, + {cereal::DeviceState::NetworkType::ETHERNET, tr("ETH")}, + {cereal::DeviceState::NetworkType::CELL2_G, tr("2G")}, + {cereal::DeviceState::NetworkType::CELL3_G, tr("3G")}, + {cereal::DeviceState::NetworkType::CELL4_G, tr("LTE")}, + {cereal::DeviceState::NetworkType::CELL5_G, tr("5G")} + }; + + const QRect home_btn = QRect(60, 860, 180, 180); + const QRect settings_btn = QRect(50, 35, 200, 117); + const QColor good_color = QColor(255, 255, 255); + const QColor warning_color = QColor(218, 202, 37); + const QColor danger_color = QColor(201, 34, 49); + + ItemStatus connect_status, panda_status, temp_status; + QString net_type; + int net_strength = 0; + +private: + std::unique_ptr pm; + + // FrogPilot variables + Params params; + UIScene &scene; + + ItemStatus cpu_status, memory_status, storage_status; + + std::unordered_map>> themeConfiguration; + std::unordered_map flag_imgs; + std::unordered_map home_imgs; + std::unordered_map settings_imgs; + std::vector currentColors; + + bool isCPU; + bool isFahrenheit; + bool isGPU; + bool isMemoryUsage; + bool isNumericalTemp; + bool isStorageLeft; + bool isStorageUsed; +}; diff --git a/selfdrive/ui/qt/spinner.cc b/selfdrive/ui/qt/spinner.cc new file mode 100755 index 0000000..efb44f5 --- /dev/null +++ b/selfdrive/ui/qt/spinner.cc @@ -0,0 +1,120 @@ +#include "selfdrive/ui/qt/spinner.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/util.h" + +TrackWidget::TrackWidget(QWidget *parent) : QWidget(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); + setFixedSize(spinner_size); + + // pre-compute all the track imgs. make this a gif instead? + QPixmap comma_img = loadPixmap("../assets/img_spinner_comma.png", spinner_size); + QPixmap track_img = loadPixmap("../assets/img_spinner_track.png", spinner_size); + + QTransform transform(1, 0, 0, 1, width() / 2, height() / 2); + QPixmap pm(spinner_size); + QPainter p(&pm); + p.setRenderHint(QPainter::SmoothPixmapTransform); + for (int i = 0; i < track_imgs.size(); ++i) { + p.resetTransform(); + p.fillRect(0, 0, spinner_size.width(), spinner_size.height(), Qt::black); + p.drawPixmap(0, 0, comma_img); + p.setTransform(transform.rotate(360 / spinner_fps)); + p.drawPixmap(-width() / 2, -height() / 2, track_img); + track_imgs[i] = pm.copy(); + } + + m_anim.setDuration(1000); + m_anim.setStartValue(0); + m_anim.setEndValue(int(track_imgs.size() -1)); + m_anim.setLoopCount(-1); + m_anim.start(); + connect(&m_anim, SIGNAL(valueChanged(QVariant)), SLOT(update())); +} + +void TrackWidget::paintEvent(QPaintEvent *event) { + QPainter painter(this); + painter.drawPixmap(0, 0, track_imgs[m_anim.currentValue().toInt()]); +} + +// Spinner + +Spinner::Spinner(QWidget *parent) : QWidget(parent) { + QGridLayout *main_layout = new QGridLayout(this); + main_layout->setSpacing(0); + main_layout->setMargin(200); + + main_layout->addWidget(new TrackWidget(this), 0, 0, Qt::AlignHCenter | Qt::AlignVCenter); + + text = new QLabel(); + text->setWordWrap(true); + text->setVisible(false); + text->setAlignment(Qt::AlignCenter); + main_layout->addWidget(text, 1, 0, Qt::AlignHCenter); + + progress_bar = new QProgressBar(); + progress_bar->setRange(5, 100); + progress_bar->setTextVisible(false); + progress_bar->setVisible(false); + progress_bar->setFixedHeight(20); + main_layout->addWidget(progress_bar, 1, 0, Qt::AlignHCenter); + + setStyleSheet(R"( + Spinner { + background-color: black; + } + QLabel { + color: white; + font-size: 80px; + background-color: transparent; + } + QProgressBar { + background-color: #373737; + width: 1000px; + border solid white; + border-radius: 10px; + } + QProgressBar::chunk { + border-radius: 10px; + background-color: rgba(179, 0, 0, 255); + } + )"); + + notifier = new QSocketNotifier(fileno(stdin), QSocketNotifier::Read); + QObject::connect(notifier, &QSocketNotifier::activated, this, &Spinner::update); +} + +void Spinner::update(int n) { + std::string line; + std::getline(std::cin, line); + + if (line.length()) { + bool number = std::all_of(line.begin(), line.end(), ::isdigit); + text->setVisible(!number); + progress_bar->setVisible(number); + text->setText(QString::fromStdString(line)); + if (number) { + progress_bar->setValue(std::stoi(line)); + } + } +} + +int main(int argc, char *argv[]) { + initApp(argc, argv); + QApplication a(argc, argv); + Spinner spinner; + setMainWindow(&spinner); + return a.exec(); +} diff --git a/selfdrive/ui/qt/spinner.h b/selfdrive/ui/qt/spinner.h new file mode 100755 index 0000000..43d90a7 --- /dev/null +++ b/selfdrive/ui/qt/spinner.h @@ -0,0 +1,37 @@ +#include + +#include +#include +#include +#include +#include +#include + +constexpr int spinner_fps = 30; +constexpr QSize spinner_size = QSize(360, 360); + +class TrackWidget : public QWidget { + Q_OBJECT +public: + TrackWidget(QWidget *parent = nullptr); + +private: + void paintEvent(QPaintEvent *event) override; + std::array track_imgs; + QVariantAnimation m_anim; +}; + +class Spinner : public QWidget { + Q_OBJECT + +public: + explicit Spinner(QWidget *parent = 0); + +private: + QLabel *text; + QProgressBar *progress_bar; + QSocketNotifier *notifier; + +public slots: + void update(int n); +}; diff --git a/selfdrive/ui/qt/text.cc b/selfdrive/ui/qt/text.cc new file mode 100755 index 0000000..4143fee --- /dev/null +++ b/selfdrive/ui/qt/text.cc @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" +#include "selfdrive/ui/qt/network/wifi_manager.h" + +// wifi connection screen +// wifi = new QWidget; +// { +// QVBoxLayout *layout = new QVBoxLayout(wifi); +// layout->setContentsMargins(100, 100, 100, 100); + +// Networking *networking = new Networking(this, false); +// networking->setStyleSheet("Networking { background-color: #292929; border-radius: 13px; }"); +// layout->addWidget(networking, 1); + +// QPushButton *back = new QPushButton(tr("Back")); +// back->setObjectName("navBtn"); +// back->setStyleSheet("padding-left: 60px; padding-right: 60px;"); +// QObject::connect(back, &QPushButton::clicked, [=]() { +// setCurrentWidget(prompt); +// }); +// layout->addWidget(back, 0, Qt::AlignLeft); +// } + +int main(int argc, char *argv[]) { + initApp(argc, argv); + QApplication a(argc, argv); + QWidget window; + setMainWindow(&window); + + QGridLayout *main_layout = new QGridLayout(&window); + main_layout->setMargin(50); + + QLabel *label = new QLabel(argv[1]); + label->setWordWrap(true); + label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); + ScrollView *scroll = new ScrollView(label); + scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + main_layout->addWidget(scroll, 0, 0, Qt::AlignTop); + + // Scroll to the bottom + QObject::connect(scroll->verticalScrollBar(), &QAbstractSlider::rangeChanged, [=]() { + scroll->verticalScrollBar()->setValue(scroll->verticalScrollBar()->maximum()); + }); + + + QPushButton *btnupdate = new QPushButton(); +#ifdef __aarch64__ + btnupdate->setText(QObject::tr("Update")); + QObject::connect(btnupdate, &QPushButton::clicked, [=]() { + QProcess process; + label->setText("Attempting to connect to wifi"); + // TODO: make this work, then copy the compiled binary into git + // wifi = new WifiManager(null); + // connect(wifi, &WifiManager::refreshSignal, [=]() { + // label->setText("Performing update"); + // process.setWorkingDirectory("/data/openpilot/"); + // process.start("/bin/bash", QStringList{"-c", "update.sh"}); + // process.waitForFinished(); + // label->setText("Rebooting"); + // Hardware::reboot(); + // }); + // wifi->start(); + }); +#else + btnupdate->setText(QObject::tr("Exit")); + QObject::connect(btnupdate, &QPushButton::clicked, &a, &QApplication::quit); +#endif + main_layout->addWidget(btnupdate, 0, 0, Qt::AlignLeft | Qt::AlignBottom); + + QPushButton *btn = new QPushButton(); +#ifdef __aarch64__ + btn->setText(QObject::tr("Reboot")); + QObject::connect(btn, &QPushButton::clicked, [=]() { + Hardware::reboot(); // bbot this is the dreaded crash reboot button + }); +#else + btn->setText(QObject::tr("Exit")); + QObject::connect(btn, &QPushButton::clicked, &a, &QApplication::quit); +#endif + main_layout->addWidget(btn, 0, 0, Qt::AlignRight | Qt::AlignBottom); + + window.setStyleSheet(R"( + * { + outline: none; + color: white; + background-color: black; + font-size: 60px; + } + QPushButton { + padding: 50px; + padding-right: 100px; + padding-left: 100px; + border: 2px solid white; + border-radius: 20px; + margin-right: 40px; + } + )"); + + return a.exec(); +} diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc new file mode 100755 index 0000000..e18f92a --- /dev/null +++ b/selfdrive/ui/qt/util.cc @@ -0,0 +1,270 @@ +#include "selfdrive/ui/qt/util.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/swaglog.h" +#include "system/hardware/hw.h" + +QString getVersion() { + static QString version = QString::fromStdString(Params().get("Version")); + return version; +} + +QString getBrand() { + return Params().getBool("Passive") ? QObject::tr("dashcam") : QObject::tr("OscarPilot"); +} + +QString getUserAgent() { + return "openpilot-" + getVersion(); +} + +std::optional getDongleId() { + std::string id = Params().get("DongleId"); + + if (!id.empty() && (id != "UnregisteredDevice")) { + return QString::fromStdString(id); + } else { + return {}; + } +} + +QMap getSupportedLanguages() { + QFile f(":/languages.json"); + f.open(QIODevice::ReadOnly | QIODevice::Text); + QString val = f.readAll(); + + QJsonObject obj = QJsonDocument::fromJson(val.toUtf8()).object(); + QMap map; + for (auto key : obj.keys()) { + map[key] = obj[key].toString(); + } + return map; +} + +QString timeAgo(const QDateTime &date) { + int diff = date.secsTo(QDateTime::currentDateTimeUtc()); + + QString s; + if (diff < 60) { + s = "now"; + } else if (diff < 60 * 60) { + int minutes = diff / 60; + s = QObject::tr("%n minute(s) ago", "", minutes); + } else if (diff < 60 * 60 * 24) { + int hours = diff / (60 * 60); + s = QObject::tr("%n hour(s) ago", "", hours); + } else if (diff < 3600 * 24 * 7) { + int days = diff / (60 * 60 * 24); + s = QObject::tr("%n day(s) ago", "", days); + } else { + s = date.date().toString(); + } + + return s; +} + +void setQtSurfaceFormat() { + QSurfaceFormat fmt; +#ifdef __APPLE__ + fmt.setVersion(3, 2); + fmt.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); + fmt.setRenderableType(QSurfaceFormat::OpenGL); +#else + fmt.setRenderableType(QSurfaceFormat::OpenGLES); +#endif + fmt.setSamples(16); + fmt.setStencilBufferSize(1); + QSurfaceFormat::setDefaultFormat(fmt); +} + +void sigTermHandler(int s) { + std::signal(s, SIG_DFL); + qApp->quit(); +} + +void initApp(int argc, char *argv[], bool disable_hidpi) { + Hardware::set_display_power(true); + Hardware::set_brightness(65); + + // setup signal handlers to exit gracefully + std::signal(SIGINT, sigTermHandler); + std::signal(SIGTERM, sigTermHandler); + + if (disable_hidpi) { +#ifdef __APPLE__ + // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering + QApplication tmp(argc, argv); + qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio() ).toLocal8Bit()); +#endif + } + + qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150)); + + setQtSurfaceFormat(); +} + +void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { + static std::map levels = { + {QtMsgType::QtDebugMsg, CLOUDLOG_DEBUG}, + {QtMsgType::QtInfoMsg, CLOUDLOG_INFO}, + {QtMsgType::QtWarningMsg, CLOUDLOG_WARNING}, + {QtMsgType::QtCriticalMsg, CLOUDLOG_ERROR}, + {QtMsgType::QtSystemMsg, CLOUDLOG_ERROR}, + {QtMsgType::QtFatalMsg, CLOUDLOG_CRITICAL}, + }; + + std::string file, function; + if (context.file != nullptr) file = context.file; + if (context.function != nullptr) function = context.function; + + auto bts = msg.toUtf8(); + cloudlog_e(levels[type], file.c_str(), context.line, function.c_str(), "%s", bts.constData()); +} + + +QWidget* topWidget(QWidget* widget) { + while (widget->parentWidget() != nullptr) widget=widget->parentWidget(); + return widget; +} + +QPixmap loadPixmap(const QString &fileName, const QSize &size, Qt::AspectRatioMode aspectRatioMode) { + if (size.isEmpty()) { + return QPixmap(fileName); + } else { + return QPixmap(fileName).scaled(size, aspectRatioMode, Qt::SmoothTransformation); + } +} + +void drawRoundedRect(QPainter &painter, const QRectF &rect, qreal xRadiusTop, qreal yRadiusTop, qreal xRadiusBottom, qreal yRadiusBottom){ + qreal w_2 = rect.width() / 2; + qreal h_2 = rect.height() / 2; + + xRadiusTop = 100 * qMin(xRadiusTop, w_2) / w_2; + yRadiusTop = 100 * qMin(yRadiusTop, h_2) / h_2; + + xRadiusBottom = 100 * qMin(xRadiusBottom, w_2) / w_2; + yRadiusBottom = 100 * qMin(yRadiusBottom, h_2) / h_2; + + qreal x = rect.x(); + qreal y = rect.y(); + qreal w = rect.width(); + qreal h = rect.height(); + + qreal rxx2Top = w*xRadiusTop/100; + qreal ryy2Top = h*yRadiusTop/100; + + qreal rxx2Bottom = w*xRadiusBottom/100; + qreal ryy2Bottom = h*yRadiusBottom/100; + + QPainterPath path; + path.arcMoveTo(x, y, rxx2Top, ryy2Top, 180); + path.arcTo(x, y, rxx2Top, ryy2Top, 180, -90); + path.arcTo(x+w-rxx2Top, y, rxx2Top, ryy2Top, 90, -90); + path.arcTo(x+w-rxx2Bottom, y+h-ryy2Bottom, rxx2Bottom, ryy2Bottom, 0, -90); + path.arcTo(x, y+h-ryy2Bottom, rxx2Bottom, ryy2Bottom, 270, -90); + path.closeSubpath(); + + painter.drawPath(path); +} + +QColor interpColor(float xv, std::vector xp, std::vector fp) { + assert(xp.size() == fp.size()); + + int N = xp.size(); + int hi = 0; + + while (hi < N and xv > xp[hi]) hi++; + int low = hi - 1; + + if (hi == N && xv > xp[low]) { + return fp[fp.size() - 1]; + } else if (hi == 0){ + return fp[0]; + } else { + return QColor( + (xv - xp[low]) * (fp[hi].red() - fp[low].red()) / (xp[hi] - xp[low]) + fp[low].red(), + (xv - xp[low]) * (fp[hi].green() - fp[low].green()) / (xp[hi] - xp[low]) + fp[low].green(), + (xv - xp[low]) * (fp[hi].blue() - fp[low].blue()) / (xp[hi] - xp[low]) + fp[low].blue(), + (xv - xp[low]) * (fp[hi].alpha() - fp[low].alpha()) / (xp[hi] - xp[low]) + fp[low].alpha()); + } +} + +static QHash load_bootstrap_icons() { + QHash icons; + + QFile f(":/bootstrap-icons.svg"); + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + QDomDocument xml; + xml.setContent(&f); + QDomNode n = xml.documentElement().firstChild(); + while (!n.isNull()) { + QDomElement e = n.toElement(); + if (!e.isNull() && e.hasAttribute("id")) { + QString svg_str; + QTextStream stream(&svg_str); + n.save(stream, 0); + svg_str.replace("", ""); + icons[e.attribute("id")] = svg_str.toUtf8(); + } + n = n.nextSibling(); + } + } + return icons; +} + +QPixmap bootstrapPixmap(const QString &id) { + static QHash icons = load_bootstrap_icons(); + + QPixmap pixmap; + if (auto it = icons.find(id); it != icons.end()) { + pixmap.loadFromData(it.value(), "svg"); + } + return pixmap; +} + +bool hasLongitudinalControl(const cereal::CarParams::Reader &car_params) { + // Using the experimental longitudinal toggle, returns whether longitudinal control + // will be active without needing a restart of openpilot + return car_params.getExperimentalLongitudinalAvailable() + ? Params().getBool("ExperimentalLongitudinalEnabled") + : car_params.getOpenpilotLongitudinalControl(); +} + +// ParamWatcher + +ParamWatcher::ParamWatcher(QObject *parent) : QObject(parent) { + watcher = new QFileSystemWatcher(this); + QObject::connect(watcher, &QFileSystemWatcher::fileChanged, this, &ParamWatcher::fileChanged); +} + +void ParamWatcher::fileChanged(const QString &path) { + auto param_name = QFileInfo(path).fileName(); + auto param_value = QString::fromStdString(params.get(param_name.toStdString())); + + auto it = params_hash.find(param_name); + bool content_changed = (it == params_hash.end()) || (it.value() != param_value); + params_hash[param_name] = param_value; + // emit signal when the content changes. + if (content_changed) { + emit paramChanged(param_name, param_value); + } +} + +void ParamWatcher::addParam(const QString ¶m_name) { + watcher->addPath(QString::fromStdString(params.getParamPath(param_name.toStdString()))); +} diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h new file mode 100755 index 0000000..2aae97b --- /dev/null +++ b/selfdrive/ui/qt/util.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "cereal/gen/cpp/car.capnp.h" +#include "common/params.h" + +QString getVersion(); +QString getBrand(); +QString getUserAgent(); +std::optional getDongleId(); +QMap getSupportedLanguages(); +void setQtSurfaceFormat(); +void sigTermHandler(int s); +QString timeAgo(const QDateTime &date); +void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); +void initApp(int argc, char *argv[], bool disable_hidpi = true); +QWidget* topWidget(QWidget* widget); +QPixmap loadPixmap(const QString &fileName, const QSize &size = {}, Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio); +QPixmap bootstrapPixmap(const QString &id); + +void drawRoundedRect(QPainter &painter, const QRectF &rect, qreal xRadiusTop, qreal yRadiusTop, qreal xRadiusBottom, qreal yRadiusBottom); +QColor interpColor(float xv, std::vector xp, std::vector fp); +bool hasLongitudinalControl(const cereal::CarParams::Reader &car_params); + +struct InterFont : public QFont { + InterFont(int pixel_size, QFont::Weight weight = QFont::Normal) : QFont("Inter") { + setPixelSize(pixel_size); + setWeight(weight); + } +}; + +class ParamWatcher : public QObject { + Q_OBJECT + +public: + ParamWatcher(QObject *parent); + void addParam(const QString ¶m_name); + +signals: + void paramChanged(const QString ¶m_name, const QString ¶m_value); + +private: + void fileChanged(const QString &path); + + QFileSystemWatcher *watcher; + QHash params_hash; + Params params; +}; diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc new file mode 100755 index 0000000..d7d52f9 --- /dev/null +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -0,0 +1,436 @@ +#include "selfdrive/ui/qt/widgets/cameraview.h" + +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include +#include +#include +#include + +#include +#include + +namespace { + +const char frame_vertex_shader[] = +#ifdef __APPLE__ + "#version 330 core\n" +#else + "#version 300 es\n" +#endif + "layout(location = 0) in vec4 aPosition;\n" + "layout(location = 1) in vec2 aTexCoord;\n" + "uniform mat4 uTransform;\n" + "out vec2 vTexCoord;\n" + "void main() {\n" + " gl_Position = uTransform * aPosition;\n" + " vTexCoord = aTexCoord;\n" + "}\n"; + +const char frame_fragment_shader[] = +#ifdef QCOM2 + "#version 300 es\n" + "#extension GL_OES_EGL_image_external_essl3 : enable\n" + "precision mediump float;\n" + "uniform samplerExternalOES uTexture;\n" + "in vec2 vTexCoord;\n" + "out vec4 colorOut;\n" + "void main() {\n" + " colorOut = texture(uTexture, vTexCoord);\n" + "}\n"; +#else +#ifdef __APPLE__ + "#version 330 core\n" +#else + "#version 300 es\n" + "precision mediump float;\n" +#endif + "uniform sampler2D uTextureY;\n" + "uniform sampler2D uTextureUV;\n" + "in vec2 vTexCoord;\n" + "out vec4 colorOut;\n" + "void main() {\n" + " float y = texture(uTextureY, vTexCoord).r;\n" + " vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n" + " float r = y + 1.402 * uv.y;\n" + " float g = y - 0.344 * uv.x - 0.714 * uv.y;\n" + " float b = y + 1.772 * uv.x;\n" + " colorOut = vec4(r, g, b, 1.0);\n" + "}\n"; +#endif + +mat4 get_driver_view_transform(int screen_width, int screen_height, int stream_width, int stream_height) { + const float driver_view_ratio = 2.0; + const float yscale = stream_height * driver_view_ratio / stream_width; + const float xscale = yscale*screen_height/screen_width*stream_width/stream_height; + mat4 transform = (mat4){{ + xscale, 0.0, 0.0, 0.0, + 0.0, yscale, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + }}; + return transform; +} + +mat4 get_fit_view_transform(float widget_aspect_ratio, float frame_aspect_ratio) { + float zx = 1, zy = 1; + if (frame_aspect_ratio > widget_aspect_ratio) { + zy = widget_aspect_ratio / frame_aspect_ratio; + } else { + zx = frame_aspect_ratio / widget_aspect_ratio; + } + + const mat4 frame_transform = {{ + zx, 0.0, 0.0, 0.0, + 0.0, zy, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + }}; + return frame_transform; +} + +} // namespace + +CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, bool zoom, QWidget* parent) : + stream_name(stream_name), requested_stream_type(type), zoomed_view(zoom), QOpenGLWidget(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); + qRegisterMetaType>("availableStreams"); + QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection); + QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); + QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); +} + +CameraWidget::~CameraWidget() { + makeCurrent(); + stopVipcThread(); + if (isValid()) { + glDeleteVertexArrays(1, &frame_vao); + glDeleteBuffers(1, &frame_vbo); + glDeleteBuffers(1, &frame_ibo); + glDeleteBuffers(2, textures); + } + doneCurrent(); +} + +// Qt uses device-independent pixels, depending on platform this may be +// different to what OpenGL uses +int CameraWidget::glWidth() { + return width() * devicePixelRatio(); +} + +int CameraWidget::glHeight() { + return height() * devicePixelRatio(); +} + +void CameraWidget::initializeGL() { + initializeOpenGLFunctions(); + + program = std::make_unique(context()); + bool ret = program->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader); + assert(ret); + ret = program->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader); + assert(ret); + + program->link(); + GLint frame_pos_loc = program->attributeLocation("aPosition"); + GLint frame_texcoord_loc = program->attributeLocation("aTexCoord"); + + auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f); + const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3}; + const float frame_coords[4][4] = { + {-1.0, -1.0, x2, y1}, // bl + {-1.0, 1.0, x2, y2}, // tl + { 1.0, 1.0, x1, y2}, // tr + { 1.0, -1.0, x1, y1}, // br + }; + + glGenVertexArrays(1, &frame_vao); + glBindVertexArray(frame_vao); + glGenBuffers(1, &frame_vbo); + glBindBuffer(GL_ARRAY_BUFFER, frame_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW); + glEnableVertexAttribArray(frame_pos_loc); + glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE, + sizeof(frame_coords[0]), (const void *)0); + glEnableVertexAttribArray(frame_texcoord_loc); + glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE, + sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2)); + glGenBuffers(1, &frame_ibo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + glUseProgram(program->programId()); + +#ifdef QCOM2 + glUniform1i(program->uniformLocation("uTexture"), 0); +#else + glGenTextures(2, textures); + glUniform1i(program->uniformLocation("uTextureY"), 0); + glUniform1i(program->uniformLocation("uTextureUV"), 1); +#endif +} + +void CameraWidget::showEvent(QShowEvent *event) { + if (!vipc_thread) { + clearFrames(); + vipc_thread = new QThread(); + connect(vipc_thread, &QThread::started, [=]() { vipcThread(); }); + connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater); + vipc_thread->start(); + } +} + +void CameraWidget::stopVipcThread() { + makeCurrent(); + if (vipc_thread) { + vipc_thread->requestInterruption(); + vipc_thread->quit(); + vipc_thread->wait(); + vipc_thread = nullptr; + } + +#ifdef QCOM2 + EGLDisplay egl_display = eglGetCurrentDisplay(); + assert(egl_display != EGL_NO_DISPLAY); + for (auto &pair : egl_images) { + eglDestroyImageKHR(egl_display, pair.second); + assert(eglGetError() == EGL_SUCCESS); + } + egl_images.clear(); +#endif +} + +void CameraWidget::availableStreamsUpdated(std::set streams) { + available_streams = streams; +} + +void CameraWidget::updateFrameMat() { + int w = glWidth(), h = glHeight(); + + if (zoomed_view) { + if (active_stream_type == VISION_STREAM_DRIVER) { + if (stream_width > 0 && stream_height > 0) { + frame_mat = get_driver_view_transform(w, h, stream_width, stream_height); + frame_mat.v[0] *= -1.0; + } + } else { + // Project point at "infinity" to compute x and y offsets + // to ensure this ends up in the middle of the screen + // for narrow come and a little lower for wide cam. + // TODO: use proper perspective transform? + if (active_stream_type == VISION_STREAM_WIDE_ROAD) { + intrinsic_matrix = ECAM_INTRINSIC_MATRIX; + zoom = 2.0; + } else { + intrinsic_matrix = FCAM_INTRINSIC_MATRIX; + zoom = 1.1; + } + const vec3 inf = {{1000., 0., 0.}}; + const vec3 Ep = matvecmul3(calibration, inf); + const vec3 Kep = matvecmul3(intrinsic_matrix, Ep); + + float x_offset_ = (Kep.v[0] / Kep.v[2] - intrinsic_matrix.v[2]) * zoom; + float y_offset_ = (Kep.v[1] / Kep.v[2] - intrinsic_matrix.v[5]) * zoom; + + float max_x_offset = intrinsic_matrix.v[2] * zoom - w / 2 - 5; + float max_y_offset = intrinsic_matrix.v[5] * zoom - h / 2 - 5; + + x_offset = std::clamp(x_offset_, -max_x_offset, max_x_offset); + y_offset = std::clamp(y_offset_, -max_y_offset, max_y_offset); + + float zx = zoom * 2 * intrinsic_matrix.v[2] / w; + float zy = zoom * 2 * intrinsic_matrix.v[5] / h; + const mat4 frame_transform = {{ + zx, 0.0, 0.0, -x_offset / w * 2, + 0.0, zy, 0.0, y_offset / h * 2, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, + }}; + frame_mat = frame_transform; + } + } else if (stream_width > 0 && stream_height > 0) { + // fit frame to widget size + float widget_aspect_ratio = (float)w / h; + float frame_aspect_ratio = (float)stream_width / stream_height; + frame_mat = get_fit_view_transform(widget_aspect_ratio, frame_aspect_ratio); + } +} + +void CameraWidget::updateCalibration(const mat3 &calib) { + calibration = calib; +} + +void CameraWidget::paintGL() { + glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); + glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + + std::lock_guard lk(frame_lock); + if (frames.empty()) return; + + int frame_idx = frames.size() - 1; + + // Always draw latest frame until sync logic is more stable + // for (frame_idx = 0; frame_idx < frames.size() - 1; frame_idx++) { + // if (frames[frame_idx].first == draw_frame_id) break; + // } + + // Log duplicate/dropped frames + if (frames[frame_idx].first == prev_frame_id) { + qDebug() << "Drawing same frame twice" << frames[frame_idx].first; + } else if (frames[frame_idx].first != prev_frame_id + 1) { + qDebug() << "Skipped frame" << frames[frame_idx].first; + } + prev_frame_id = frames[frame_idx].first; + VisionBuf *frame = frames[frame_idx].second; + assert(frame != nullptr); + + updateFrameMat(); + + glViewport(0, 0, glWidth(), glHeight()); + glBindVertexArray(frame_vao); + glUseProgram(program->programId()); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + +#ifdef QCOM2 + // no frame copy + glActiveTexture(GL_TEXTURE0); + glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, egl_images[frame->idx]); + assert(glGetError() == GL_NO_ERROR); +#else + // fallback to copy + glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textures[0]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, frame->y); + assert(glGetError() == GL_NO_ERROR); + + glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2); + glActiveTexture(GL_TEXTURE0 + 1); + glBindTexture(GL_TEXTURE_2D, textures[1]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, frame->uv); + assert(glGetError() == GL_NO_ERROR); +#endif + + glUniformMatrix4fv(program->uniformLocation("uTransform"), 1, GL_TRUE, frame_mat.v); + glEnableVertexAttribArray(0); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (const void *)0); + glDisableVertexAttribArray(0); + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); +} + +void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) { + makeCurrent(); + stream_width = vipc_client->buffers[0].width; + stream_height = vipc_client->buffers[0].height; + stream_stride = vipc_client->buffers[0].stride; + +#ifdef QCOM2 + EGLDisplay egl_display = eglGetCurrentDisplay(); + assert(egl_display != EGL_NO_DISPLAY); + for (auto &pair : egl_images) { + eglDestroyImageKHR(egl_display, pair.second); + } + egl_images.clear(); + + for (int i = 0; i < vipc_client->num_buffers; i++) { // import buffers into OpenGL + int fd = dup(vipc_client->buffers[i].fd); // eglDestroyImageKHR will close, so duplicate + EGLint img_attrs[] = { + EGL_WIDTH, (int)vipc_client->buffers[i].width, + EGL_HEIGHT, (int)vipc_client->buffers[i].height, + EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_NV12, + EGL_DMA_BUF_PLANE0_FD_EXT, fd, + EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)vipc_client->buffers[i].stride, + EGL_DMA_BUF_PLANE1_FD_EXT, fd, + EGL_DMA_BUF_PLANE1_OFFSET_EXT, (int)vipc_client->buffers[i].uv_offset, + EGL_DMA_BUF_PLANE1_PITCH_EXT, (int)vipc_client->buffers[i].stride, + EGL_NONE + }; + egl_images[i] = eglCreateImageKHR(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, img_attrs); + assert(eglGetError() == EGL_SUCCESS); + } +#else + glBindTexture(GL_TEXTURE_2D, textures[0]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + assert(glGetError() == GL_NO_ERROR); + + glBindTexture(GL_TEXTURE_2D, textures[1]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); + assert(glGetError() == GL_NO_ERROR); +#endif +} + +void CameraWidget::vipcFrameReceived() { + update(); +} + +void CameraWidget::vipcThread() { + VisionStreamType cur_stream = requested_stream_type; + std::unique_ptr vipc_client; + VisionIpcBufExtra meta_main = {0}; + + while (!QThread::currentThread()->isInterruptionRequested()) { + if (!vipc_client || cur_stream != requested_stream_type) { + clearFrames(); + qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream; + cur_stream = requested_stream_type; + vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); + } + active_stream_type = cur_stream; + + if (!vipc_client->connected) { + clearFrames(); + auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); + if (streams.empty()) { + QThread::msleep(100); + continue; + } + emit vipcAvailableStreamsUpdated(streams); + + if (!vipc_client->connect(false)) { + QThread::msleep(100); + continue; + } + emit vipcThreadConnected(vipc_client.get()); + } + + if (VisionBuf *buf = vipc_client->recv(&meta_main, 1000)) { + { + std::lock_guard lk(frame_lock); + frames.push_back(std::make_pair(meta_main.frame_id, buf)); + while (frames.size() > FRAME_BUFFER_SIZE) { + frames.pop_front(); + } + } + emit vipcThreadFrameReceived(); + } else { + if (!isVisible()) { + vipc_client->connected = false; + } + } + } +} + +void CameraWidget::clearFrames() { + std::lock_guard lk(frame_lock); + frames.clear(); + available_streams.clear(); +} diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h new file mode 100755 index 0000000..c97038c --- /dev/null +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifdef QCOM2 +#define EGL_EGLEXT_PROTOTYPES +#define EGL_NO_X11 +#define GL_TEXTURE_EXTERNAL_OES 0x8D65 +#include +#include +#include +#endif + +#include "cereal/visionipc/visionipc_client.h" +#include "system/camerad/cameras/camera_common.h" +#include "selfdrive/ui/ui.h" + +const int FRAME_BUFFER_SIZE = 5; +static_assert(FRAME_BUFFER_SIZE <= YUV_BUFFER_COUNT); + +class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions { + Q_OBJECT + +public: + using QOpenGLWidget::QOpenGLWidget; + explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, bool zoom, QWidget* parent = nullptr); + ~CameraWidget(); + void setBackgroundColor(const QColor &color) { bg = color; } + void setFrameId(int frame_id) { draw_frame_id = frame_id; } + void setStreamType(VisionStreamType type) { requested_stream_type = type; } + VisionStreamType getStreamType() { return active_stream_type; } + void stopVipcThread(); + +signals: + void clicked(); + void vipcThreadConnected(VisionIpcClient *); + void vipcThreadFrameReceived(); + void vipcAvailableStreamsUpdated(std::set); + +protected: + void paintGL() override; + void initializeGL() override; + void resizeGL(int w, int h) override { updateFrameMat(); } + void showEvent(QShowEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } + virtual void updateFrameMat(); + void updateCalibration(const mat3 &calib); + void vipcThread(); + void clearFrames(); + + int glWidth(); + int glHeight(); + + bool zoomed_view; + GLuint frame_vao, frame_vbo, frame_ibo; + GLuint textures[2]; + mat4 frame_mat = {}; + std::unique_ptr program; + QColor bg = QColor("#000000"); + +#ifdef QCOM2 + std::map egl_images; +#endif + + std::string stream_name; + int stream_width = 0; + int stream_height = 0; + int stream_stride = 0; + std::atomic active_stream_type; + std::atomic requested_stream_type; + std::set available_streams; + QThread *vipc_thread = nullptr; + + // Calibration + float x_offset = 0; + float y_offset = 0; + float zoom = 1.0; + mat3 calibration = DEFAULT_CALIBRATION; + mat3 intrinsic_matrix = FCAM_INTRINSIC_MATRIX; + + std::recursive_mutex frame_lock; + std::deque> frames; + uint32_t draw_frame_id = 0; + uint32_t prev_frame_id = 0; + +protected slots: + void vipcConnected(VisionIpcClient *vipc_client); + void vipcFrameReceived(); + void availableStreamsUpdated(std::set streams); +}; + +Q_DECLARE_METATYPE(std::set); diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc new file mode 100755 index 0000000..40dda97 --- /dev/null +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -0,0 +1,141 @@ +#include "selfdrive/ui/qt/widgets/controls.h" + +#include +#include + +AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setMargin(0); + + hlayout = new QHBoxLayout; + hlayout->setMargin(0); + hlayout->setSpacing(20); + + // left icon + icon_label = new QLabel(this); + hlayout->addWidget(icon_label); + if (!icon.isEmpty()) { + icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); + icon_label->setPixmap(icon_pixmap); + icon_label->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + } + icon_label->setVisible(!icon.isEmpty()); + + // title + title_label = new QPushButton(title); + title_label->setFixedHeight(120); + title_label->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; border: none;"); + hlayout->addWidget(title_label, 1); + + // value next to control button + value = new ElidedLabel(); + value->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + value->setStyleSheet("color: #aaaaaa"); + hlayout->addWidget(value); + + main_layout->addLayout(hlayout); + + // description + description = new QLabel(desc); + description->setContentsMargins(40, 20, 40, 20); + description->setStyleSheet("font-size: 40px; color: grey"); + description->setWordWrap(true); + description->setVisible(false); + main_layout->addWidget(description); + + connect(title_label, &QPushButton::clicked, [=]() { + if (!description->isVisible()) { + emit showDescriptionEvent(); + } + + if (!description->text().isEmpty()) { + description->setVisible(!description->isVisible()); + } + }); + + main_layout->addStretch(); +} + +void AbstractControl::hideEvent(QHideEvent *e) { + if (description != nullptr) { + description->hide(); + } +} + +// controls + +ButtonControl::ButtonControl(const QString &title, const QString &text, const QString &desc, QWidget *parent) : AbstractControl(title, desc, "", parent) { + btn.setText(text); + btn.setStyleSheet(R"( + QPushButton { + padding: 0; + border-radius: 50px; + font-size: 35px; + font-weight: 500; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"); + btn.setFixedSize(250, 100); + QObject::connect(&btn, &QPushButton::clicked, this, &ButtonControl::clicked); + hlayout->addWidget(&btn); +} + +// ElidedLabel + +ElidedLabel::ElidedLabel(QWidget *parent) : ElidedLabel({}, parent) {} + +ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) { + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + setMinimumWidth(1); +} + +void ElidedLabel::resizeEvent(QResizeEvent* event) { + QLabel::resizeEvent(event); + lastText_ = elidedText_ = ""; +} + +void ElidedLabel::paintEvent(QPaintEvent *event) { + const QString curText = text(); + if (curText != lastText_) { + elidedText_ = fontMetrics().elidedText(curText, Qt::ElideRight, contentsRect().width()); + lastText_ = curText; + } + + QPainter painter(this); + drawFrame(&painter); + QStyleOption opt; + opt.initFrom(this); + style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole()); +} + +// ParamControl + +ParamControl::ParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : ToggleControl(title, desc, icon, false, parent) { + key = param.toStdString(); + QObject::connect(this, &ParamControl::toggleFlipped, this, &ParamControl::toggleClicked); +} + +void ParamControl::toggleClicked(bool state) { + auto do_confirm = [this]() { + QString content("

" + title_label->text() + "


" + "

" + getDescription() + "

"); + return ConfirmationDialog(content, tr("Enable"), tr("Cancel"), true, this).exec(); + }; + + bool confirmed = store_confirm && params.getBool(key + "Confirmed"); + if (!confirm || confirmed || !state || do_confirm()) { + if (store_confirm && state) params.putBool(key + "Confirmed", true); + params.putBool(key, state); + setIcon(state); + } else { + toggle.togglePosition(); + } +} diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h new file mode 100755 index 0000000..2468afa --- /dev/null +++ b/selfdrive/ui/qt/widgets/controls.h @@ -0,0 +1,289 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "selfdrive/ui/qt/widgets/toggle.h" + +class ElidedLabel : public QLabel { + Q_OBJECT + +public: + explicit ElidedLabel(QWidget *parent = 0); + explicit ElidedLabel(const QString &text, QWidget *parent = 0); + +signals: + void clicked(); + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent* event) override; + void mouseReleaseEvent(QMouseEvent *event) override { + if (rect().contains(event->pos())) { + emit clicked(); + } + } + QString lastText_, elidedText_; +}; + + +class AbstractControl : public QFrame { + Q_OBJECT + +public: + void setDescription(const QString &desc) { + if (description) description->setText(desc); + } + + void setTitle(const QString &title) { + title_label->setText(title); + } + + void setValue(const QString &val) { + value->setText(val); + } + + const QString getDescription() { + return description->text(); + } + + QLabel *icon_label; + QPixmap icon_pixmap; + +public slots: + void showDescription() { + description->setVisible(true); + } + +signals: + void showDescriptionEvent(); + +protected: + AbstractControl(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + void hideEvent(QHideEvent *e) override; + + QHBoxLayout *hlayout; + QPushButton *title_label; + +private: + ElidedLabel *value; + QLabel *description = nullptr; +}; + +// widget to display a value +class LabelControl : public AbstractControl { + Q_OBJECT + +public: + LabelControl(const QString &title, const QString &text = "", const QString &desc = "", QWidget *parent = nullptr) : AbstractControl(title, desc, "", parent) { + label.setText(text); + label.setAlignment(Qt::AlignRight | Qt::AlignVCenter); + hlayout->addWidget(&label); + } + void setText(const QString &text) { label.setText(text); } + +private: + ElidedLabel label; +}; + +// widget for a button with a label +class ButtonControl : public AbstractControl { + Q_OBJECT + +public: + ButtonControl(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr); + inline void setText(const QString &text) { btn.setText(text); } + inline QString text() const { return btn.text(); } + +signals: + void clicked(); + +public slots: + void setEnabled(bool enabled) { btn.setEnabled(enabled); } + +private: + QPushButton btn; +}; + +class ToggleControl : public AbstractControl { + Q_OBJECT + +public: + ToggleControl(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControl(title, desc, icon, parent) { + toggle.setFixedSize(150, 100); + if (state) { + toggle.togglePosition(); + } + hlayout->addWidget(&toggle); + QObject::connect(&toggle, &Toggle::stateChanged, this, &ToggleControl::toggleFlipped); + } + + void setVisualOn() { + toggle.togglePosition(); + } + + void setEnabled(bool enabled) { + toggle.setEnabled(enabled); + toggle.update(); + } + +signals: + void toggleFlipped(bool state); + +protected: + Toggle toggle; +}; + +// widget to toggle params +class ParamControl : public ToggleControl { + Q_OBJECT + +public: + ParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); + void setConfirmation(bool _confirm, bool _store_confirm) { + confirm = _confirm; + store_confirm = _store_confirm; + } + + void setActiveIcon(const QString &icon) { + active_icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); + } + + void refresh() { + bool state = params.getBool(key); + if (state != toggle.on) { + toggle.togglePosition(); + setIcon(state); + } + } + + void showEvent(QShowEvent *event) override { + refresh(); + } + +private: + void toggleClicked(bool state); + void setIcon(bool state) { + if (state && !active_icon_pixmap.isNull()) { + icon_label->setPixmap(active_icon_pixmap); + } else if (!icon_pixmap.isNull()) { + icon_label->setPixmap(icon_pixmap); + } + } + + std::string key; + Params params; + QPixmap active_icon_pixmap; + bool confirm = false; + bool store_confirm = false; +}; + +class ButtonParamControl : public AbstractControl { + Q_OBJECT +public: + ButtonParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const std::vector &button_texts, const int minimum_button_width = 225) : AbstractControl(title, desc, icon) { + const QString style = 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 { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #0048FF; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"; + key = param.toStdString(); + int value = atoi(params.get(key).c_str()); + + button_group = new QButtonGroup(this); + button_group->setExclusive(true); + for (int i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setCheckable(true); + button->setChecked(i == value); + button->setStyleSheet(style); + button->setMinimumWidth(minimum_button_width); + hlayout->addWidget(button); + button_group->addButton(button, i); + } + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonToggled), [=](int id, bool checked) { + if (checked) { + params.put(key, std::to_string(id)); + } + }); + } + + void setEnabled(bool enable) { + for (auto btn : button_group->buttons()) { + btn->setEnabled(enable); + } + } + +private: + std::string key; + Params params; + QButtonGroup *button_group; +}; + +class ListWidget : public QWidget { + Q_OBJECT + public: + explicit ListWidget(QWidget *parent = 0) : QWidget(parent), outer_layout(this) { + outer_layout.setMargin(0); + outer_layout.setSpacing(0); + outer_layout.addLayout(&inner_layout); + inner_layout.setMargin(0); + inner_layout.setSpacing(25); // default spacing is 25 + outer_layout.addStretch(); + } + inline void addItem(QWidget *w) { inner_layout.addWidget(w); } + inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); } + inline void setSpacing(int spacing) { inner_layout.setSpacing(spacing); } + +private: + void paintEvent(QPaintEvent *) override { + QPainter p(this); + p.setPen(Qt::gray); + for (int i = 0; i < inner_layout.count() - 1; ++i) { + QWidget *widget = inner_layout.itemAt(i)->widget(); + if (widget == nullptr || widget->isVisible()) { + QRect r = inner_layout.itemAt(i)->geometry(); + int bottom = r.bottom() + inner_layout.spacing() / 2; + p.drawLine(r.left() + 40, bottom, r.right() - 40, bottom); + } + } + } + QVBoxLayout outer_layout; + QVBoxLayout inner_layout; +}; + +// convenience class for wrapping layouts +class LayoutWidget : public QWidget { + Q_OBJECT + +public: + LayoutWidget(QLayout *l, QWidget *parent = nullptr) : QWidget(parent) { + setLayout(l); + } +}; diff --git a/selfdrive/ui/qt/widgets/drive_stats.cc b/selfdrive/ui/qt/widgets/drive_stats.cc new file mode 100755 index 0000000..31009f0 --- /dev/null +++ b/selfdrive/ui/qt/widgets/drive_stats.cc @@ -0,0 +1,97 @@ +#include "selfdrive/ui/qt/widgets/drive_stats.h" + +#include +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/request_repeater.h" +#include "selfdrive/ui/qt/util.h" + +static QLabel* newLabel(const QString& text, const QString &type) { + QLabel* label = new QLabel(text); + label->setProperty("type", type); + return label; +} + +DriveStats::DriveStats(QWidget* parent) : QFrame(parent) { + metric_ = Params().getBool("IsMetric"); + + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 50, 50, 60); + + auto add_stats_layouts = [=](const QString &title, StatsLabels& labels) { + QGridLayout* grid_layout = new QGridLayout; + grid_layout->setVerticalSpacing(10); + grid_layout->setContentsMargins(0, 10, 0, 10); + + 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->addWidget(labels.routes = newLabel("0", "number"), row, 0, Qt::AlignLeft); + grid_layout->addWidget(labels.distance = newLabel("0", "number"), row, 1, Qt::AlignLeft); + grid_layout->addWidget(labels.hours = newLabel("0", "number"), row, 2, Qt::AlignLeft); + + grid_layout->addWidget(newLabel((tr("Drives")), "unit"), row + 1, 0, Qt::AlignLeft); + grid_layout->addWidget(labels.distance_unit = newLabel(getDistanceUnit(), "unit"), row + 1, 1, Qt::AlignLeft); + grid_layout->addWidget(newLabel(tr("Hours"), "unit"), row + 1, 2, Qt::AlignLeft); + + main_layout->addLayout(grid_layout); + }; + + add_stats_layouts(tr("ALL TIME"), all_); + main_layout->addStretch(); + add_stats_layouts(tr("PAST WEEK"), week_); + + if (auto dongleId = getDongleId()) { + QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/stats"; + RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_DriveStats", 30); + QObject::connect(repeater, &RequestRepeater::requestDone, this, &DriveStats::parseResponse); + } + + setStyleSheet(R"( + DriveStats { + background-color: #333333; + border-radius: 10px; + } + + QLabel[type="title"] { font-size: 51px; font-weight: 500; } + QLabel[type="number"] { font-size: 78px; font-weight: 500; } + QLabel[type="unit"] { font-size: 51px; font-weight: 300; color: #A0A0A0; } + )"); +} + +void DriveStats::updateStats() { + auto update = [=](const QJsonObject& obj, StatsLabels& labels) { + labels.routes->setText(QString::number((int)obj["routes"].toDouble())); + labels.distance->setText(QString::number(int(obj["distance"].toDouble() * (metric_ ? MILE_TO_KM : 1)))); + labels.distance_unit->setText(getDistanceUnit()); + labels.hours->setText(QString::number((int)(obj["minutes"].toDouble() / 60))); + }; + + QJsonObject json = stats_.object(); + update(json["all"].toObject(), all_); + update(json["week"].toObject(), week_); +} + +void DriveStats::parseResponse(const QString& response, bool success) { + if (!success) return; + + QJsonDocument doc = QJsonDocument::fromJson(response.trimmed().toUtf8()); + if (doc.isNull()) { + qDebug() << "JSON Parse failed on getting past drives statistics"; + return; + } + stats_ = doc; + updateStats(); +} + +void DriveStats::showEvent(QShowEvent* event) { + bool metric = Params().getBool("IsMetric"); + if (metric_ != metric) { + metric_ = metric; + updateStats(); + } +} diff --git a/selfdrive/ui/qt/widgets/drive_stats.h b/selfdrive/ui/qt/widgets/drive_stats.h new file mode 100755 index 0000000..5e2d96b --- /dev/null +++ b/selfdrive/ui/qt/widgets/drive_stats.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +class DriveStats : public QFrame { + Q_OBJECT + +public: + explicit DriveStats(QWidget* parent = 0); + +private: + void showEvent(QShowEvent *event) override; + void updateStats(); + inline QString getDistanceUnit() const { return metric_ ? tr("KM") : tr("Miles"); } + + bool metric_; + QJsonDocument stats_; + struct StatsLabels { + QLabel *routes, *distance, *distance_unit, *hours; + } all_, week_; + +private slots: + void parseResponse(const QString &response, bool success); +}; diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc new file mode 100755 index 0000000..4ff0de2 --- /dev/null +++ b/selfdrive/ui/qt/widgets/input.cc @@ -0,0 +1,336 @@ +#include "selfdrive/ui/qt/widgets/input.h" + +#include +#include + +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" + + +DialogBase::DialogBase(QWidget *parent) : QDialog(parent) { + Q_ASSERT(parent != nullptr); + parent->installEventFilter(this); + + setStyleSheet(R"( + * { + outline: none; + color: white; + font-family: Inter; + } + DialogBase { + background-color: black; + } + QPushButton { + height: 160; + font-size: 55px; + font-weight: 400; + border-radius: 10px; + color: white; + background-color: #333333; + } + QPushButton:pressed { + background-color: #444444; + } + )"); +} + +bool DialogBase::eventFilter(QObject *o, QEvent *e) { + if (o == parent() && e->type() == QEvent::Hide) { + reject(); + } + return QDialog::eventFilter(o, e); +} + +int DialogBase::exec() { + setMainWindow(this); + return QDialog::exec(); +} + +InputDialog::InputDialog(const QString &title, QWidget *parent, const QString &subtitle, bool secret) : DialogBase(parent) { + main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 55, 50, 50); + main_layout->setSpacing(0); + + // build header + QHBoxLayout *header_layout = new QHBoxLayout(); + + QVBoxLayout *vlayout = new QVBoxLayout; + header_layout->addLayout(vlayout); + label = new QLabel(title, this); + label->setStyleSheet("font-size: 90px; font-weight: bold;"); + vlayout->addWidget(label, 1, Qt::AlignTop | Qt::AlignLeft); + + if (!subtitle.isEmpty()) { + sublabel = new QLabel(subtitle, this); + sublabel->setStyleSheet("font-size: 55px; font-weight: light; color: #BDBDBD;"); + vlayout->addWidget(sublabel, 1, Qt::AlignTop | Qt::AlignLeft); + } + + QPushButton* cancel_btn = new QPushButton(tr("Cancel")); + cancel_btn->setFixedSize(386, 125); + cancel_btn->setStyleSheet(R"( + QPushButton { + font-size: 48px; + border-radius: 10px; + color: #E4E4E4; + background-color: #333333; + } + QPushButton:pressed { + background-color: #444444; + } + )"); + header_layout->addWidget(cancel_btn, 0, Qt::AlignRight); + QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::reject); + QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::cancel); + + main_layout->addLayout(header_layout); + + // text box + main_layout->addStretch(2); + + QWidget *textbox_widget = new QWidget; + textbox_widget->setObjectName("textbox"); + QHBoxLayout *textbox_layout = new QHBoxLayout(textbox_widget); + textbox_layout->setContentsMargins(50, 0, 50, 0); + + textbox_widget->setStyleSheet(R"( + #textbox { + margin-left: 50px; + margin-right: 50px; + border-radius: 0; + border-bottom: 3px solid #BDBDBD; + } + * { + border: none; + font-size: 80px; + font-weight: light; + background-color: transparent; + } + )"); + + line = new QLineEdit(); + line->setStyleSheet("lineedit-password-character: 8226; lineedit-password-mask-delay: 1500;"); + textbox_layout->addWidget(line, 1); + + if (secret) { + eye_btn = new QPushButton(); + eye_btn->setCheckable(true); + eye_btn->setFixedSize(150, 120); + QObject::connect(eye_btn, &QPushButton::toggled, [=](bool checked) { + if (checked) { + eye_btn->setIcon(QIcon(ASSET_PATH + "img_eye_closed.svg")); + eye_btn->setIconSize(QSize(81, 54)); + line->setEchoMode(QLineEdit::Password); + } else { + eye_btn->setIcon(QIcon(ASSET_PATH + "img_eye_open.svg")); + eye_btn->setIconSize(QSize(81, 44)); + line->setEchoMode(QLineEdit::Normal); + } + }); + eye_btn->toggle(); + eye_btn->setChecked(false); + textbox_layout->addWidget(eye_btn); + } + + main_layout->addWidget(textbox_widget, 0, Qt::AlignBottom); + main_layout->addSpacing(25); + + k = new Keyboard(this); + QObject::connect(k, &Keyboard::emitEnter, this, &InputDialog::handleEnter); + QObject::connect(k, &Keyboard::emitBackspace, this, [=]() { + line->backspace(); + }); + QObject::connect(k, &Keyboard::emitKey, this, [=](const QString &key) { + line->insert(key.left(1)); + }); + + main_layout->addWidget(k, 2, Qt::AlignBottom); +} + +QString InputDialog::getText(const QString &prompt, QWidget *parent, const QString &subtitle, + bool secret, int minLength, const QString &defaultText) { + InputDialog d = InputDialog(prompt, parent, subtitle, secret); + d.line->setText(defaultText); + d.setMinLength(minLength); + const int ret = d.exec(); + return ret ? d.text() : QString(); +} + +QString InputDialog::text() { + return line->text(); +} + +void InputDialog::show() { + setMainWindow(this); +} + +void InputDialog::handleEnter() { + if (line->text().length() >= minLength) { + done(QDialog::Accepted); + emitText(line->text()); + } else { + setMessage(tr("Need at least %n character(s)!", "", minLength), false); + } +} + +void InputDialog::setMessage(const QString &message, bool clearInputField) { + label->setText(message); + if (clearInputField) { + line->setText(""); + } +} + +void InputDialog::setMinLength(int length) { + minLength = length; +} + +// ConfirmationDialog + +ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, const QString &cancel_text, + const bool rich, QWidget *parent) : DialogBase(parent) { + QFrame *container = new QFrame(this); + container->setStyleSheet(R"( + QFrame { background-color: #1B1B1B; color: #C9C9C9; } + #confirm_btn { background-color: #465BEA; } + #confirm_btn:pressed { background-color: #3049F4; } + )"); + QVBoxLayout *main_layout = new QVBoxLayout(container); + main_layout->setContentsMargins(32, rich ? 32 : 120, 32, 32); + + QLabel *prompt = new QLabel(prompt_text, this); + prompt->setWordWrap(true); + prompt->setAlignment(rich ? Qt::AlignLeft : Qt::AlignHCenter); + prompt->setStyleSheet((rich ? "font-size: 42px; font-weight: light;" : "font-size: 70px; font-weight: bold;") + QString(" margin: 45px;")); + main_layout->addWidget(rich ? (QWidget*)new ScrollView(prompt, this) : (QWidget*)prompt, 1, Qt::AlignTop); + + // cancel + confirm buttons + QHBoxLayout *btn_layout = new QHBoxLayout(); + btn_layout->setSpacing(30); + main_layout->addLayout(btn_layout); + + if (cancel_text.length()) { + QPushButton* cancel_btn = new QPushButton(cancel_text); + btn_layout->addWidget(cancel_btn); + QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); + } + + if (confirm_text.length()) { + QPushButton* confirm_btn = new QPushButton(confirm_text); + confirm_btn->setObjectName("confirm_btn"); + btn_layout->addWidget(confirm_btn); + QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); + } + + QVBoxLayout *outer_layout = new QVBoxLayout(this); + int margin = rich ? 100 : 200; + outer_layout->setContentsMargins(margin, margin, margin, margin); + outer_layout->addWidget(container); +} + +bool ConfirmationDialog::alert(const QString &prompt_text, QWidget *parent) { + ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), "", false, parent); + return d.exec(); +} + +bool ConfirmationDialog::confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent) { + ConfirmationDialog d = ConfirmationDialog(prompt_text, confirm_text, tr("Cancel"), false, parent); + return d.exec(); +} + +bool ConfirmationDialog::rich(const QString &prompt_text, QWidget *parent) { + ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), "", true, parent); + return d.exec(); +} + +// MultiOptionDialog + +MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) : DialogBase(parent) { + QFrame *container = new QFrame(this); + container->setStyleSheet(R"( + QFrame { background-color: #1B1B1B; } + #confirm_btn[enabled="false"] { background-color: #2B2B2B; } + #confirm_btn:enabled { background-color: #465BEA; } + #confirm_btn:enabled:pressed { background-color: #3049F4; } + )"); + + QVBoxLayout *main_layout = new QVBoxLayout(container); + main_layout->setContentsMargins(55, 50, 55, 50); + + QLabel *title = new QLabel(prompt_text, this); + title->setStyleSheet("font-size: 70px; font-weight: 500;"); + main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); + main_layout->addSpacing(25); + + QWidget *listWidget = new QWidget(this); + QVBoxLayout *listLayout = new QVBoxLayout(listWidget); + listLayout->setSpacing(20); + listWidget->setStyleSheet(R"( + QPushButton { + height: 135; + padding: 0px 50px; + text-align: left; + font-size: 55px; + font-weight: 300; + border-radius: 10px; + background-color: #4F4F4F; + } + QPushButton:checked { background-color: #465BEA; } + )"); + + QButtonGroup *group = new QButtonGroup(listWidget); + group->setExclusive(true); + + QPushButton *confirm_btn = new QPushButton(tr("Select")); + confirm_btn->setObjectName("confirm_btn"); + confirm_btn->setEnabled(false); + + for (const QString &s : l) { + QPushButton *selectionLabel = new QPushButton(s); + selectionLabel->setCheckable(true); + selectionLabel->setChecked(s == current); + QObject::connect(selectionLabel, &QPushButton::toggled, [=](bool checked) { + if (checked) selection = s; + if (selection != current) { + confirm_btn->setEnabled(true); + } else { + confirm_btn->setEnabled(false); + } + }); + + group->addButton(selectionLabel); + listLayout->addWidget(selectionLabel); + } + // add stretch to keep buttons spaced correctly + listLayout->addStretch(1); + + ScrollView *scroll_view = new ScrollView(listWidget, this); + scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + main_layout->addWidget(scroll_view); + main_layout->addSpacing(35); + + // cancel + confirm buttons + QHBoxLayout *blayout = new QHBoxLayout; + main_layout->addLayout(blayout); + blayout->setSpacing(50); + + QPushButton *cancel_btn = new QPushButton(tr("Cancel")); + QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); + QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); + blayout->addWidget(cancel_btn); + blayout->addWidget(confirm_btn); + + QVBoxLayout *outer_layout = new QVBoxLayout(this); + outer_layout->setContentsMargins(50, 50, 50, 50); + outer_layout->addWidget(container); +} + +QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) { + MultiOptionDialog d = MultiOptionDialog(prompt_text, l, current, parent); + if (d.exec()) { + return d.selection; + } + return ""; +} diff --git a/selfdrive/ui/qt/widgets/input.h b/selfdrive/ui/qt/widgets/input.h new file mode 100755 index 0000000..089e54e --- /dev/null +++ b/selfdrive/ui/qt/widgets/input.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "selfdrive/ui/qt/widgets/keyboard.h" + + +class DialogBase : public QDialog { + Q_OBJECT + +protected: + DialogBase(QWidget *parent); + bool eventFilter(QObject *o, QEvent *e) override; + +public slots: + int exec() override; +}; + +class InputDialog : public DialogBase { + Q_OBJECT + +public: + explicit InputDialog(const QString &title, QWidget *parent, const QString &subtitle = "", bool secret = false); + static QString getText(const QString &title, QWidget *parent, const QString &subtitle = "", + bool secret = false, int minLength = -1, const QString &defaultText = ""); + QString text(); + void setMessage(const QString &message, bool clearInputField = true); + void setMinLength(int length); + void show(); + +private: + int minLength; + QLineEdit *line; + Keyboard *k; + QLabel *label; + QLabel *sublabel; + QVBoxLayout *main_layout; + QPushButton *eye_btn; + +private slots: + void handleEnter(); + +signals: + void cancel(); + void emitText(const QString &text); +}; + +class ConfirmationDialog : public DialogBase { + Q_OBJECT + +public: + explicit ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, + const QString &cancel_text, const bool rich, QWidget* parent); + static bool alert(const QString &prompt_text, QWidget *parent); + static bool confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent); + static bool rich(const QString &prompt_text, QWidget *parent); +}; + +class MultiOptionDialog : public DialogBase { + Q_OBJECT + +public: + explicit MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); + static QString getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); + QString selection; +}; diff --git a/selfdrive/ui/qt/widgets/keyboard.cc b/selfdrive/ui/qt/widgets/keyboard.cc new file mode 100755 index 0000000..370e9a5 --- /dev/null +++ b/selfdrive/ui/qt/widgets/keyboard.cc @@ -0,0 +1,167 @@ +#include "selfdrive/ui/qt/widgets/keyboard.h" + +#include + +#include +#include +#include +#include +#include + +const QString BACKSPACE_KEY = "⌫"; +const QString ENTER_KEY = "→"; + +const QMap KEY_STRETCH = {{" ", 5}, {ENTER_KEY, 2}}; + +const QStringList CONTROL_BUTTONS = {"↑", "↓", "ABC", "#+=", "123", BACKSPACE_KEY, ENTER_KEY}; + +const float key_spacing_vertical = 20; +const float key_spacing_horizontal = 15; + +KeyButton::KeyButton(const QString &text, QWidget *parent) : QPushButton(text, parent) { + setAttribute(Qt::WA_AcceptTouchEvents); + setFocusPolicy(Qt::NoFocus); +} + +bool KeyButton::event(QEvent *event) { + if (event->type() == QEvent::TouchBegin || event->type() == QEvent::TouchEnd) { + QTouchEvent *touchEvent = static_cast(event); + if (!touchEvent->touchPoints().empty()) { + const QEvent::Type mouseType = event->type() == QEvent::TouchBegin ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease; + QMouseEvent mouseEvent(mouseType, touchEvent->touchPoints().front().pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QPushButton::event(&mouseEvent); + event->accept(); + parentWidget()->update(); + return true; + } + } + return QPushButton::event(event); +} + +KeyboardLayout::KeyboardLayout(QWidget* parent, const std::vector>& layout) : QWidget(parent) { + QVBoxLayout* main_layout = new QVBoxLayout(this); + main_layout->setMargin(0); + main_layout->setSpacing(0); + + QButtonGroup* btn_group = new QButtonGroup(this); + QObject::connect(btn_group, SIGNAL(buttonClicked(QAbstractButton*)), parent, SLOT(handleButton(QAbstractButton*))); + + for (const auto &s : layout) { + QHBoxLayout *hlayout = new QHBoxLayout; + hlayout->setSpacing(0); + + if (main_layout->count() == 1) { + hlayout->addSpacing(90); + } + + for (const QString &p : s) { + KeyButton* btn = new KeyButton(p); + if (p == BACKSPACE_KEY) { + btn->setAutoRepeat(true); + } else if (p == ENTER_KEY) { + btn->setStyleSheet(R"( + QPushButton { + background-color: #465BEA; + } + QPushButton:pressed { + background-color: #444444; + } + )"); + } + btn->setFixedHeight(135 + key_spacing_vertical); + btn_group->addButton(btn); + hlayout->addWidget(btn, KEY_STRETCH.value(p, 1)); + } + + if (main_layout->count() == 1) { + hlayout->addSpacing(90); + } + + main_layout->addLayout(hlayout); + } + + setStyleSheet(QString(R"( + QPushButton { + font-size: 75px; + margin-left: %1px; + margin-right: %1px; + margin-top: %2px; + margin-bottom: %2px; + padding: 0px; + border-radius: 10px; + color: #dddddd; + background-color: #444444; + } + QPushButton:pressed { + background-color: #333333; + } + )").arg(key_spacing_vertical / 2).arg(key_spacing_horizontal / 2)); +} + +Keyboard::Keyboard(QWidget *parent) : QFrame(parent) { + main_layout = new QStackedLayout(this); + main_layout->setMargin(0); + + // lowercase + std::vector> lowercase = { + {"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}, + {"a", "s", "d", "f", "g", "h", "j", "k", "l"}, + {"↑", "z", "x", "c", "v", "b", "n", "m", BACKSPACE_KEY}, + {"123", " ", ".", ENTER_KEY}, + }; + main_layout->addWidget(new KeyboardLayout(this, lowercase)); + + // uppercase + std::vector> uppercase = { + {"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"}, + {"A", "S", "D", "F", "G", "H", "J", "K", "L"}, + {"↓", "Z", "X", "C", "V", "B", "N", "M", BACKSPACE_KEY}, + {"123", " ", ".", ENTER_KEY}, + }; + main_layout->addWidget(new KeyboardLayout(this, uppercase)); + + // numbers + specials + std::vector> numbers = { + {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}, + {"-", "/", ":", ";", "(", ")", "$", "&&", "@", "\""}, + {"#+=", ".", ",", "?", "!", "`", BACKSPACE_KEY}, + {"ABC", " ", ".", ENTER_KEY}, + }; + main_layout->addWidget(new KeyboardLayout(this, numbers)); + + // extra specials + std::vector> specials = { + {"[", "]", "{", "}", "#", "%", "^", "*", "+", "="}, + {"_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"}, + {"123", ".", ",", "?", "!", "'", BACKSPACE_KEY}, + {"ABC", " ", ".", ENTER_KEY}, + }; + main_layout->addWidget(new KeyboardLayout(this, specials)); + + main_layout->setCurrentIndex(0); +} + +void Keyboard::handleButton(QAbstractButton* btn) { + const QString &key = btn->text(); + if (CONTROL_BUTTONS.contains(key)) { + if (key == "↓" || key == "ABC") { + main_layout->setCurrentIndex(0); + } else if (key == "↑") { + main_layout->setCurrentIndex(1); + } else if (key == "123") { + main_layout->setCurrentIndex(2); + } else if (key == "#+=") { + main_layout->setCurrentIndex(3); + } else if (key == ENTER_KEY) { + main_layout->setCurrentIndex(0); + emit emitEnter(); + } else if (key == BACKSPACE_KEY) { + emit emitBackspace(); + } + } else { + if ("A" <= key && key <= "Z") { + main_layout->setCurrentIndex(0); + } + emit emitKey(key); + } +} diff --git a/selfdrive/ui/qt/widgets/keyboard.h b/selfdrive/ui/qt/widgets/keyboard.h new file mode 100755 index 0000000..efc02d0 --- /dev/null +++ b/selfdrive/ui/qt/widgets/keyboard.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include +#include +#include + +class KeyButton : public QPushButton { + Q_OBJECT + +public: + KeyButton(const QString &text, QWidget *parent = 0); + bool event(QEvent *event) override; +}; + +class KeyboardLayout : public QWidget { + Q_OBJECT + +public: + explicit KeyboardLayout(QWidget* parent, const std::vector>& layout); +}; + +class Keyboard : public QFrame { + Q_OBJECT + +public: + explicit Keyboard(QWidget *parent = 0); + +private: + QStackedLayout* main_layout; + +private slots: + void handleButton(QAbstractButton* m_button); + +signals: + void emitKey(const QString &s); + void emitBackspace(); + void emitEnter(); +}; diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.cc b/selfdrive/ui/qt/widgets/offroad_alerts.cc new file mode 100755 index 0000000..f9db008 --- /dev/null +++ b/selfdrive/ui/qt/widgets/offroad_alerts.cc @@ -0,0 +1,144 @@ +#include "selfdrive/ui/qt/widgets/offroad_alerts.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "common/util.h" +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/widgets/scrollview.h" + +AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setMargin(50); + main_layout->setSpacing(30); + + QWidget *widget = new QWidget; + scrollable_layout = new QVBoxLayout(widget); + widget->setStyleSheet("background-color: transparent;"); + main_layout->addWidget(new ScrollView(widget)); + + // bottom footer, dismiss + reboot buttons + QHBoxLayout *footer_layout = new QHBoxLayout(); + main_layout->addLayout(footer_layout); + + QPushButton *dismiss_btn = new QPushButton(tr("Close")); + dismiss_btn->setFixedSize(400, 125); + footer_layout->addWidget(dismiss_btn, 0, Qt::AlignBottom | Qt::AlignLeft); + QObject::connect(dismiss_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); + + disable_check_btn = new QPushButton(tr("Disable Internet Check")); + disable_check_btn->setVisible(false); + disable_check_btn->setFixedSize(625, 125); + footer_layout->addWidget(disable_check_btn, 1, Qt::AlignBottom | Qt::AlignCenter); + QObject::connect(disable_check_btn, &QPushButton::clicked, [=]() { + if (!params.getBool("FireTheBabysitter")) { + params.putBool("FireTheBabysitter", true); + } + if (!params.getBool("OfflineMode")) { + params.putBool("OfflineMode", true); + } + Hardware::reboot(); + }); + QObject::connect(disable_check_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); + disable_check_btn->setStyleSheet(R"(color: white; background-color: #4F4F4F;)"); + + snooze_btn = new QPushButton(tr("Snooze Update")); + snooze_btn->setVisible(false); + snooze_btn->setFixedSize(550, 125); + footer_layout->addWidget(snooze_btn, 0, Qt::AlignBottom | Qt::AlignRight); + QObject::connect(snooze_btn, &QPushButton::clicked, [=]() { + params.putBool("SnoozeUpdate", true); + }); + QObject::connect(snooze_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); + snooze_btn->setStyleSheet(R"(color: white; background-color: #4F4F4F;)"); + + if (hasRebootBtn) { + QPushButton *rebootBtn = new QPushButton(tr("Reboot and Update")); + rebootBtn->setFixedSize(600, 125); + footer_layout->addWidget(rebootBtn, 0, Qt::AlignBottom | Qt::AlignRight); + QObject::connect(rebootBtn, &QPushButton::clicked, [=]() { Hardware::reboot(); }); + } + + setStyleSheet(R"( + * { + font-size: 48px; + color: white; + } + QFrame { + border-radius: 30px; + background-color: #393939; + } + QPushButton { + color: black; + font-weight: 500; + border-radius: 30px; + background-color: white; + } + )"); +} + +int OffroadAlert::refresh() { + // build widgets for each offroad alert on first refresh + if (alerts.empty()) { + QString json = util::read_file("../controls/lib/alerts_offroad.json").c_str(); + QJsonObject obj = QJsonDocument::fromJson(json.toUtf8()).object(); + + // descending sort labels by severity + std::vector> sorted; + for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { + sorted.push_back({it.key().toStdString(), it.value()["severity"].toInt()}); + } + std::sort(sorted.begin(), sorted.end(), [=](auto &l, auto &r) { return l.second > r.second; }); + + for (auto &[key, severity] : sorted) { + QLabel *l = new QLabel(this); + alerts[key] = l; + l->setMargin(60); + l->setWordWrap(true); + l->setStyleSheet(QString("background-color: %1").arg(severity ? "#E22C2C" : "#292929")); + scrollable_layout->addWidget(l); + } + scrollable_layout->addStretch(1); + } + + int alertCount = 0; + for (const auto &[key, label] : alerts) { + QString text; + std::string bytes = params.get(key); + if (bytes.size()) { + auto doc_par = QJsonDocument::fromJson(bytes.c_str()); + text = tr(doc_par["text"].toString().toUtf8().data()); + auto extra = doc_par["extra"].toString(); + if (!extra.isEmpty()) { + text = text.arg(extra); + } + } + label->setText(text); + label->setVisible(!text.isEmpty()); + alertCount += !text.isEmpty(); + } + disable_check_btn->setVisible(!alerts["Offroad_ConnectivityNeeded"]->text().isEmpty()); + snooze_btn->setVisible(!alerts["Offroad_ConnectivityNeeded"]->text().isEmpty()); + return alertCount; +} + +UpdateAlert::UpdateAlert(QWidget *parent) : AbstractAlert(true, parent) { + releaseNotes = new QLabel(this); + releaseNotes->setWordWrap(true); + releaseNotes->setAlignment(Qt::AlignTop); + scrollable_layout->addWidget(releaseNotes); +} + +bool UpdateAlert::refresh() { + bool updateAvailable = params.getBool("UpdateAvailable"); + if (updateAvailable) { + releaseNotes->setText(params.get("UpdaterNewReleaseNotes").c_str()); + } + return updateAvailable; +} diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.h b/selfdrive/ui/qt/widgets/offroad_alerts.h new file mode 100755 index 0000000..abeafd2 --- /dev/null +++ b/selfdrive/ui/qt/widgets/offroad_alerts.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "common/params.h" + +class AbstractAlert : public QFrame { + Q_OBJECT + +protected: + AbstractAlert(bool hasRebootBtn, QWidget *parent = nullptr); + + QPushButton *disable_check_btn; + QPushButton *snooze_btn; + QVBoxLayout *scrollable_layout; + Params params; + +signals: + void dismiss(); +}; + +class UpdateAlert : public AbstractAlert { + Q_OBJECT + +public: + UpdateAlert(QWidget *parent = 0); + bool refresh(); + +private: + QLabel *releaseNotes = nullptr; +}; + +class OffroadAlert : public AbstractAlert { + Q_OBJECT + +public: + explicit OffroadAlert(QWidget *parent = 0) : AbstractAlert(false, parent) {} + int refresh(); + +private: + std::map alerts; +}; diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc new file mode 100755 index 0000000..324d6cf --- /dev/null +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -0,0 +1,283 @@ +#include "selfdrive/ui/qt/widgets/prime.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "selfdrive/ui/qt/request_repeater.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/widgets/wifi.h" + +using qrcodegen::QrCode; + +PairingQRWidget::PairingQRWidget(QWidget* parent) : QWidget(parent) { + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &PairingQRWidget::refresh); +} + +void PairingQRWidget::showEvent(QShowEvent *event) { + refresh(); + timer->start(5 * 60 * 1000); + device()->setOffroadBrightness(100); +} + +void PairingQRWidget::hideEvent(QHideEvent *event) { + timer->stop(); + device()->setOffroadBrightness(BACKLIGHT_OFFROAD); +} + +void PairingQRWidget::refresh() { + QString pairToken = CommaApi::create_jwt({{"pair", true}}); + QString qrString = "https://connect.comma.ai/?pair=" + pairToken; + this->updateQrCode(qrString); + update(); +} + +void PairingQRWidget::updateQrCode(const QString &text) { + QrCode qr = QrCode::encodeText(text.toUtf8().data(), QrCode::Ecc::LOW); + qint32 sz = qr.getSize(); + QImage im(sz, sz, QImage::Format_RGB32); + + QRgb black = qRgb(0, 0, 0); + QRgb white = qRgb(255, 255, 255); + for (int y = 0; y < sz; y++) { + for (int x = 0; x < sz; x++) { + im.setPixel(x, y, qr.getModule(x, y) ? black : white); + } + } + + // Integer division to prevent anti-aliasing + int final_sz = ((width() / sz) - 1) * sz; + img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly); +} + +void PairingQRWidget::paintEvent(QPaintEvent *e) { + QPainter p(this); + p.fillRect(rect(), Qt::white); + + QSize s = (size() - img.size()) / 2; + p.drawPixmap(s.width(), s.height(), img); +} + + +PairingPopup::PairingPopup(QWidget *parent) : DialogBase(parent) { + QHBoxLayout *hlayout = new QHBoxLayout(this); + hlayout->setContentsMargins(0, 0, 0, 0); + hlayout->setSpacing(0); + + setStyleSheet("PairingPopup { background-color: #E0E0E0; }"); + + // text + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setContentsMargins(85, 70, 50, 70); + vlayout->setSpacing(50); + hlayout->addLayout(vlayout, 1); + { + QPushButton *close = new QPushButton(QIcon(":/icons/close.svg"), "", this); + close->setIconSize(QSize(80, 80)); + close->setStyleSheet("border: none;"); + vlayout->addWidget(close, 0, Qt::AlignLeft); + QObject::connect(close, &QPushButton::clicked, this, &QDialog::reject); + + vlayout->addSpacing(30); + + QLabel *title = new QLabel(tr("Pair your device to your comma account"), this); + title->setStyleSheet("font-size: 75px; color: black;"); + title->setWordWrap(true); + vlayout->addWidget(title); + + QLabel *instructions = new QLabel(QString(R"( +
    +
  1. %1
  2. +
  3. %2
  4. +
  5. %3
  6. +
+ )").arg(tr("Go to https://connect.comma.ai on your phone")) + .arg(tr("Click \"add new device\" and scan the QR code on the right")) + .arg(tr("Bookmark connect.comma.ai to your home screen to use it like an app")), this); + + instructions->setStyleSheet("font-size: 47px; font-weight: bold; color: black;"); + instructions->setWordWrap(true); + vlayout->addWidget(instructions); + + vlayout->addStretch(); + } + + // QR code + PairingQRWidget *qr = new PairingQRWidget(this); + hlayout->addWidget(qr, 1); +} + + +PrimeUserWidget::PrimeUserWidget(QWidget *parent) : QFrame(parent) { + setObjectName("primeWidget"); + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(56, 40, 56, 40); + mainLayout->setSpacing(20); + + QLabel *subscribed = new QLabel(tr("✓ SUBSCRIBED")); + subscribed->setStyleSheet("font-size: 41px; font-weight: bold; color: #86FF4E;"); + mainLayout->addWidget(subscribed); + + QLabel *commaPrime = new QLabel(tr("comma prime")); + commaPrime->setStyleSheet("font-size: 75px; font-weight: bold;"); + mainLayout->addWidget(commaPrime); +} + + +PrimeAdWidget::PrimeAdWidget(QWidget* parent) : QFrame(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(80, 90, 80, 60); + main_layout->setSpacing(0); + + QLabel *upgrade = new QLabel(tr("Upgrade Now")); + upgrade->setStyleSheet("font-size: 75px; font-weight: bold;"); + main_layout->addWidget(upgrade, 0, Qt::AlignTop); + main_layout->addSpacing(50); + + QLabel *description = new QLabel(tr("Become a comma prime member at connect.comma.ai")); + description->setStyleSheet("font-size: 56px; font-weight: light; color: white;"); + description->setWordWrap(true); + main_layout->addWidget(description, 0, Qt::AlignTop); + + main_layout->addStretch(); + + QLabel *features = new QLabel(tr("PRIME FEATURES:")); + features->setStyleSheet("font-size: 41px; font-weight: bold; color: #E5E5E5;"); + main_layout->addWidget(features, 0, Qt::AlignBottom); + main_layout->addSpacing(30); + + QVector bullets = {tr("Remote access"), tr("24/7 LTE connectivity"), tr("1 year of drive storage"), tr("Turn-by-turn navigation")}; + for (auto &b : bullets) { + const QString check = " "; + QLabel *l = new QLabel(check + b); + l->setAlignment(Qt::AlignLeft); + l->setStyleSheet("font-size: 50px; margin-bottom: 15px;"); + main_layout->addWidget(l, 0, Qt::AlignBottom); + } + + setStyleSheet(R"( + PrimeAdWidget { + border-radius: 10px; + background-color: #333333; + } + )"); +} + + +SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) { + mainLayout = new QStackedWidget; + + // Unpaired, registration prompt layout + + QFrame* finishRegistration = new QFrame; + finishRegistration->setObjectName("primeWidget"); + QVBoxLayout* finishRegistationLayout = new QVBoxLayout(finishRegistration); + finishRegistationLayout->setSpacing(38); + finishRegistationLayout->setContentsMargins(64, 48, 64, 48); + + QLabel* registrationTitle = new QLabel(tr("Finish Setup")); + registrationTitle->setStyleSheet("font-size: 75px; font-weight: bold;"); + finishRegistationLayout->addWidget(registrationTitle); + + QLabel* registrationDescription = new QLabel(tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); + registrationDescription->setWordWrap(true); + registrationDescription->setStyleSheet("font-size: 50px; font-weight: light;"); + finishRegistationLayout->addWidget(registrationDescription); + + finishRegistationLayout->addStretch(); + + QPushButton* pair = new QPushButton(tr("Pair device")); + pair->setStyleSheet(R"( + QPushButton { + font-size: 55px; + font-weight: 500; + border-radius: 10px; + background-color: #465BEA; + padding: 64px; + } + QPushButton:pressed { + background-color: #3049F4; + } + )"); + finishRegistationLayout->addWidget(pair); + + popup = new PairingPopup(this); + QObject::connect(pair, &QPushButton::clicked, popup, &PairingPopup::exec); + + mainLayout->addWidget(finishRegistration); + + // build stacked layout + QVBoxLayout *outer_layout = new QVBoxLayout(this); + outer_layout->setContentsMargins(0, 0, 0, 0); + outer_layout->addWidget(mainLayout); + + QWidget *content = new QWidget; + QVBoxLayout *content_layout = new QVBoxLayout(content); + content_layout->setContentsMargins(0, 0, 0, 0); + content_layout->setSpacing(30); + + primeUser = new PrimeUserWidget; + content_layout->addWidget(primeUser); + + WiFiPromptWidget *wifi_prompt = new WiFiPromptWidget; + QObject::connect(wifi_prompt, &WiFiPromptWidget::openSettings, this, &SetupWidget::openSettings); + content_layout->addWidget(wifi_prompt); + content_layout->addStretch(); + + mainLayout->addWidget(content); + + primeUser->setVisible(uiState()->primeType()); + mainLayout->setCurrentIndex(1); + + setStyleSheet(R"( + #primeWidget { + border-radius: 10px; + background-color: #333333; + } + )"); + + // Retain size while hidden + QSizePolicy sp_retain = sizePolicy(); + sp_retain.setRetainSizeWhenHidden(true); + setSizePolicy(sp_retain); + + // set up API requests + if (auto dongleId = getDongleId()) { + QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/"; + RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_Device", 5); + + QObject::connect(repeater, &RequestRepeater::requestDone, this, &SetupWidget::replyFinished); + } +} + +void SetupWidget::replyFinished(const QString &response, bool success) { + if (!success) return; + + QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); + if (doc.isNull()) { + qDebug() << "JSON Parse failed on getting pairing and prime status"; + return; + } + + QJsonObject json = doc.object(); + PrimeType prime_type = static_cast(json["prime_type"].toInt()); + uiState()->setPrimeType(prime_type); + + if (!json["is_paired"].toBool()) { + mainLayout->setCurrentIndex(0); + } else { + popup->reject(); + + primeUser->setVisible(prime_type); + mainLayout->setCurrentIndex(1); + } +} diff --git a/selfdrive/ui/qt/widgets/prime.h b/selfdrive/ui/qt/widgets/prime.h new file mode 100755 index 0000000..63341c4 --- /dev/null +++ b/selfdrive/ui/qt/widgets/prime.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include + +#include "selfdrive/ui/qt/widgets/input.h" + +// pairing QR code +class PairingQRWidget : public QWidget { + Q_OBJECT + +public: + explicit PairingQRWidget(QWidget* parent = 0); + void paintEvent(QPaintEvent*) override; + +private: + QPixmap img; + QTimer *timer; + void updateQrCode(const QString &text); + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + +private slots: + void refresh(); +}; + + +// pairing popup widget +class PairingPopup : public DialogBase { + Q_OBJECT + +public: + explicit PairingPopup(QWidget* parent); +}; + + +// widget for paired users with prime +class PrimeUserWidget : public QFrame { + Q_OBJECT + +public: + explicit PrimeUserWidget(QWidget* parent = 0); +}; + + +// widget for paired users without prime +class PrimeAdWidget : public QFrame { + Q_OBJECT +public: + explicit PrimeAdWidget(QWidget* parent = 0); +}; + + +// container widget +class SetupWidget : public QFrame { + Q_OBJECT + +public: + explicit SetupWidget(QWidget* parent = 0); + +signals: + void openSettings(int index = 0, const QString ¶m = ""); + +private: + PairingPopup *popup; + QStackedWidget *mainLayout; + PrimeUserWidget *primeUser; + +private slots: + void replyFinished(const QString &response, bool success); +}; diff --git a/selfdrive/ui/qt/widgets/scrollview.cc b/selfdrive/ui/qt/widgets/scrollview.cc new file mode 100755 index 0000000..2846007 --- /dev/null +++ b/selfdrive/ui/qt/widgets/scrollview.cc @@ -0,0 +1,53 @@ +#include "selfdrive/ui/qt/widgets/scrollview.h" + +#include +#include + +// TODO: disable horizontal scrolling and resize + +ScrollView::ScrollView(QWidget *w, QWidget *parent) : QScrollArea(parent) { + setWidget(w); + setWidgetResizable(true); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setStyleSheet("background-color: transparent; border:none"); + + QString style = R"( + QScrollBar:vertical { + border: none; + background: transparent; + width: 10px; + margin: 0; + } + QScrollBar::handle:vertical { + min-height: 0px; + border-radius: 5px; + background-color: white; + } + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0px; + } + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; + } + )"; + verticalScrollBar()->setStyleSheet(style); + horizontalScrollBar()->setStyleSheet(style); + + QScroller *scroller = QScroller::scroller(this->viewport()); + QScrollerProperties sp = scroller->scrollerProperties(); + + sp.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOff)); + sp.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOff)); + sp.setScrollMetric(QScrollerProperties::MousePressEventDelay, 0.01); + scroller->grabGesture(this->viewport(), QScroller::LeftMouseButtonGesture); + scroller->setScrollerProperties(sp); +} + +void ScrollView::restorePosition(int previousScrollPosition) { + verticalScrollBar()->setValue(previousScrollPosition); +} + +void ScrollView::hideEvent(QHideEvent *e) { + verticalScrollBar()->setValue(0); +} diff --git a/selfdrive/ui/qt/widgets/scrollview.h b/selfdrive/ui/qt/widgets/scrollview.h new file mode 100755 index 0000000..1e67bbe --- /dev/null +++ b/selfdrive/ui/qt/widgets/scrollview.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class ScrollView : public QScrollArea { + Q_OBJECT + +public: + explicit ScrollView(QWidget *w = nullptr, QWidget *parent = nullptr); + + // FrogPilot functions + void restorePosition(int previousScrollPosition); +protected: + void hideEvent(QHideEvent *e) override; +}; diff --git a/selfdrive/ui/qt/widgets/ssh_keys.cc b/selfdrive/ui/qt/widgets/ssh_keys.cc new file mode 100755 index 0000000..2674395 --- /dev/null +++ b/selfdrive/ui/qt/widgets/ssh_keys.cc @@ -0,0 +1,64 @@ +#include "selfdrive/ui/qt/widgets/ssh_keys.h" + +#include "common/params.h" +#include "selfdrive/ui/qt/api.h" +#include "selfdrive/ui/qt/widgets/input.h" + +SshControl::SshControl() : + ButtonControl(tr("SSH Keys"), "", tr("Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " + "other than your own. A comma employee will NEVER ask you to add their GitHub username.")) { + + QObject::connect(this, &ButtonControl::clicked, [=]() { + if (text() == tr("ADD")) { + QString username = InputDialog::getText(tr("Enter your GitHub username"), this); + if (username.length() > 0) { + setText(tr("LOADING")); + setEnabled(false); + getUserKeys(username); + } + } else { + params.remove("GithubUsername"); + params.remove("GithubSshKeys"); + refresh(); + } + }); + + refresh(); +} + +void SshControl::refresh() { + QString param = QString::fromStdString(params.get("GithubSshKeys")); + if (param.length()) { + setValue(QString::fromStdString(params.get("GithubUsername"))); + setText(tr("REMOVE")); + } else { + setValue(""); + setText(tr("ADD")); + } + setEnabled(true); +} + +void SshControl::getUserKeys(const QString &username) { + HttpRequest *request = new HttpRequest(this, false); + QObject::connect(request, &HttpRequest::requestDone, [=](const QString &resp, bool success) { + if (success) { + if (!resp.isEmpty()) { + params.put("GithubUsername", username.toStdString()); + params.put("GithubSshKeys", resp.toStdString()); + } else { + ConfirmationDialog::alert(tr("Username '%1' has no keys on GitHub").arg(username), this); + } + } else { + if (request->timeout()) { + ConfirmationDialog::alert(tr("Request timed out"), this); + } else { + ConfirmationDialog::alert(tr("Username '%1' doesn't exist on GitHub").arg(username), this); + } + } + + refresh(); + request->deleteLater(); + }); + + request->sendRequest("https://github.com/" + username + ".keys"); +} diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h new file mode 100755 index 0000000..920bd65 --- /dev/null +++ b/selfdrive/ui/qt/widgets/ssh_keys.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "system/hardware/hw.h" +#include "selfdrive/ui/qt/widgets/controls.h" + +// SSH enable toggle +class SshToggle : public ToggleControl { + Q_OBJECT + +public: + SshToggle() : ToggleControl(tr("Enable SSH"), "", "", Hardware::get_ssh_enabled()) { + QObject::connect(this, &SshToggle::toggleFlipped, [=](bool state) { + Hardware::set_ssh_enabled(state); + }); + } +}; + +// SSH key management widget +class SshControl : public ButtonControl { + Q_OBJECT + +public: + SshControl(); + +private: + Params params; + + void refresh(); + void getUserKeys(const QString &username); +}; diff --git a/selfdrive/ui/qt/widgets/toggle.cc b/selfdrive/ui/qt/widgets/toggle.cc new file mode 100755 index 0000000..ffee7e5 --- /dev/null +++ b/selfdrive/ui/qt/widgets/toggle.cc @@ -0,0 +1,83 @@ +#include "selfdrive/ui/qt/widgets/toggle.h" + +#include + +Toggle::Toggle(QWidget *parent) : QAbstractButton(parent), +_height(80), +_height_rect(60), +on(false), +_anim(new QPropertyAnimation(this, "offset_circle", this)) +{ + _radius = _height / 2; + _x_circle = _radius; + _y_circle = _radius; + _y_rect = (_height - _height_rect)/2; + circleColor = QColor(0xffffff); // placeholder + green = QColor(0xffffff); // placeholder + setEnabled(true); +} + +void Toggle::paintEvent(QPaintEvent *e) { + this->setFixedHeight(_height); + QPainter p(this); + p.setPen(Qt::NoPen); + p.setRenderHint(QPainter::Antialiasing, true); + + // Draw toggle background left + p.setBrush(green); + p.drawRoundedRect(QRect(0, _y_rect, _x_circle + _radius, _height_rect), _height_rect/2, _height_rect/2); + + // Draw toggle background right + p.setBrush(QColor(0x393939)); + p.drawRoundedRect(QRect(_x_circle - _radius, _y_rect, width() - (_x_circle - _radius), _height_rect), _height_rect/2, _height_rect/2); + + // Draw toggle circle + p.setBrush(circleColor); + p.drawEllipse(QRectF(_x_circle - _radius, _y_circle - _radius, 2 * _radius, 2 * _radius)); +} + +void Toggle::mouseReleaseEvent(QMouseEvent *e) { + if (!enabled) { + return; + } + const int left = _radius; + const int right = width() - _radius; + if ((_x_circle != left && _x_circle != right) || !this->rect().contains(e->localPos().toPoint())) { + // If mouse release isn't in rect or animation is running, don't parse touch events + return; + } + if (e->button() & Qt::LeftButton) { + togglePosition(); + emit stateChanged(on); + } +} + +void Toggle::togglePosition() { + on = !on; + const int left = _radius; + const int right = width() - _radius; + _anim->setStartValue(on ? left + immediateOffset : right - immediateOffset); + _anim->setEndValue(on ? right : left); + _anim->setDuration(animation_duration); + _anim->start(); + repaint(); +} + +void Toggle::enterEvent(QEvent *e) { + QAbstractButton::enterEvent(e); +} + +bool Toggle::getEnabled() { + return enabled; +} + +void Toggle::setEnabled(bool value) { + enabled = value; + if (value) { + circleColor.setRgb(0xfafafa); + green.setRgb(0x0048FF); + } else { + circleColor.setRgb(0x888888); + green.setRgb(0x227722); + } +} diff --git a/selfdrive/ui/qt/widgets/toggle.h b/selfdrive/ui/qt/widgets/toggle.h new file mode 100755 index 0000000..e7263a0 --- /dev/null +++ b/selfdrive/ui/qt/widgets/toggle.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +class Toggle : public QAbstractButton { + Q_OBJECT + Q_PROPERTY(int offset_circle READ offset_circle WRITE set_offset_circle CONSTANT) + +public: + Toggle(QWidget* parent = nullptr); + void togglePosition(); + bool on; + int animation_duration = 150; + int immediateOffset = 0; + int offset_circle() const { + return _x_circle; + } + + void set_offset_circle(int o) { + _x_circle = o; + update(); + } + bool getEnabled(); + void setEnabled(bool value); + +protected: + void paintEvent(QPaintEvent*) override; + void mouseReleaseEvent(QMouseEvent*) override; + void enterEvent(QEvent*) override; + +private: + QColor circleColor; + QColor green; + bool enabled = true; + int _x_circle, _y_circle; + int _height, _radius; + int _height_rect, _y_rect; + QPropertyAnimation *_anim = nullptr; + +signals: + void stateChanged(bool new_state); +}; diff --git a/selfdrive/ui/qt/widgets/wifi.cc b/selfdrive/ui/qt/widgets/wifi.cc new file mode 100755 index 0000000..9c5289a --- /dev/null +++ b/selfdrive/ui/qt/widgets/wifi.cc @@ -0,0 +1,103 @@ +#include "selfdrive/ui/qt/widgets/wifi.h" + +#include +#include +#include +#include + +WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) { + stack = new QStackedLayout(this); + + // Setup Wi-Fi + QFrame *setup = new QFrame; + QVBoxLayout *setup_layout = new QVBoxLayout(setup); + setup_layout->setContentsMargins(56, 40, 56, 40); + setup_layout->setSpacing(20); + { + QHBoxLayout *title_layout = new QHBoxLayout; + title_layout->setSpacing(32); + { + QLabel *icon = new QLabel; + QPixmap pixmap("../assets/offroad/icon_wifi_strength_full.svg"); + icon->setPixmap(pixmap.scaledToWidth(80, Qt::SmoothTransformation)); + title_layout->addWidget(icon); + + QLabel *title = new QLabel(tr("Setup Wi-Fi")); + title->setStyleSheet("font-size: 64px; font-weight: 600;"); + title_layout->addWidget(title); + title_layout->addStretch(); + } + setup_layout->addLayout(title_layout); + + QLabel *desc = new QLabel(tr("Connect to Wi-Fi to upload driving data and help improve openpilot")); + desc->setStyleSheet("font-size: 40px; font-weight: 400;"); + desc->setWordWrap(true); + setup_layout->addWidget(desc); + + QPushButton *settings_btn = new QPushButton(tr("Open Settings")); + connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(1); }); + settings_btn->setStyleSheet(R"( + QPushButton { + font-size: 48px; + font-weight: 500; + border-radius: 10px; + background-color: #465BEA; + padding: 32px; + } + QPushButton:pressed { + background-color: #3049F4; + } + )"); + setup_layout->addWidget(settings_btn); + } + stack->addWidget(setup); + + // Uploading data + QWidget *uploading = new QWidget; + QVBoxLayout *uploading_layout = new QVBoxLayout(uploading); + uploading_layout->setContentsMargins(64, 56, 64, 56); + uploading_layout->setSpacing(36); + { + QHBoxLayout *title_layout = new QHBoxLayout; + { + QLabel *title = new QLabel(tr("Ready to upload")); + title->setStyleSheet("font-size: 64px; font-weight: 600;"); + title->setWordWrap(true); + title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + title_layout->addWidget(title); + title_layout->addStretch(); + + QLabel *icon = new QLabel; + QPixmap pixmap("../assets/offroad/icon_wifi_uploading.svg"); + icon->setPixmap(pixmap.scaledToWidth(120, Qt::SmoothTransformation)); + title_layout->addWidget(icon); + } + uploading_layout->addLayout(title_layout); + + QLabel *desc = new QLabel(tr("Training data will be pulled periodically while your device is on Wi-Fi")); + desc->setStyleSheet("font-size: 48px; font-weight: 400;"); + desc->setWordWrap(true); + uploading_layout->addWidget(desc); + } + stack->addWidget(uploading); + + setStyleSheet(R"( + WiFiPromptWidget { + background-color: #333333; + border-radius: 10px; + } + )"); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &WiFiPromptWidget::updateState); +} + +void WiFiPromptWidget::updateState(const UIState &s) { + if (!isVisible()) return; + + auto &sm = *(s.sm); + + auto network_type = sm["deviceState"].getDeviceState().getNetworkType(); + auto uploading = network_type == cereal::DeviceState::NetworkType::WIFI || + network_type == cereal::DeviceState::NetworkType::ETHERNET; + stack->setCurrentIndex(uploading ? 1 : 0); +} diff --git a/selfdrive/ui/qt/widgets/wifi.h b/selfdrive/ui/qt/widgets/wifi.h new file mode 100755 index 0000000..60c865f --- /dev/null +++ b/selfdrive/ui/qt/widgets/wifi.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +#include "selfdrive/ui/ui.h" + +class WiFiPromptWidget : public QFrame { + Q_OBJECT + +public: + explicit WiFiPromptWidget(QWidget* parent = 0); + +signals: + void openSettings(int index = 0, const QString ¶m = ""); + +public slots: + void updateState(const UIState &s); + +protected: + QStackedLayout *stack; +}; diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc new file mode 100755 index 0000000..18e51c7 --- /dev/null +++ b/selfdrive/ui/qt/window.cc @@ -0,0 +1,108 @@ +#include "selfdrive/ui/qt/window.h" + +#include + +#include "system/hardware/hw.h" + +MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { + main_layout = new QStackedLayout(this); + main_layout->setMargin(0); + + homeWindow = new HomeWindow(this); + main_layout->addWidget(homeWindow); + QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings); + QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings); + + oscarSettingsWindow = new OscarSettingsWindow(this); + main_layout->addWidget(oscarSettingsWindow); + QObject::connect(oscarSettingsWindow, &OscarSettingsWindow::closeSettings, this, &MainWindow::closeSettings); + // QObject::connect(oscarSettingsWindow, &OscarSettingsWindow::reviewTrainingGuide, [=]() { + // onboardingWindow->showTrainingGuide(); + // main_layout->setCurrentWidget(onboardingWindow); + // }); + QObject::connect(oscarSettingsWindow, &OscarSettingsWindow::showDriverView, [=] { + homeWindow->showDriverView(true); + }); + + onboardingWindow = new OnboardingWindow(this); + main_layout->addWidget(onboardingWindow); + QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() { + main_layout->setCurrentWidget(homeWindow); + }); + if (!onboardingWindow->completed()) { + main_layout->setCurrentWidget(onboardingWindow); + } + + QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + if (!offroad) { + closeSettings(); + } + }); + // QObject::connect(device(), &Device::interactiveTimeout, [=]() { + // if (main_layout->currentWidget() == oscarSettingsWindow) { + // closeSettings(); + // } + // }); + + // load fonts + QFontDatabase::addApplicationFont("../assets/fonts/Inter-Black.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/Inter-Bold.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/Inter-ExtraBold.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/Inter-ExtraLight.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/Inter-Medium.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/Inter-Regular.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/Inter-SemiBold.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/Inter-Thin.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/JetBrainsMono-Medium.ttf"); + + // no outline to prevent the focus rectangle + setStyleSheet(R"( + * { + font-family: Inter; + outline: none; + } + )"); + setAttribute(Qt::WA_NoSystemBackground); +} + +void MainWindow::openSettings(int index, const QString ¶m) { + main_layout->setCurrentWidget(oscarSettingsWindow); + oscarSettingsWindow->setCurrentPanel(index, param); +} + +void MainWindow::closeSettings() { + main_layout->setCurrentWidget(homeWindow); + + if (uiState()->scene.started) { + // Map is always shown when using navigate on openpilot + if (uiState()->scene.navigate_on_openpilot) { + homeWindow->showMapPanel(true); + } else { + homeWindow->showSidebar(params.getBool("Sidebar")); + } + } +} + +bool MainWindow::eventFilter(QObject *obj, QEvent *event) { + bool ignore = false; + switch (event->type()) { + case QEvent::TouchBegin: + case QEvent::TouchUpdate: + case QEvent::TouchEnd: + case QEvent::MouseButtonPress: + case QEvent::MouseMove: { + // ignore events when device is awakened by resetInteractiveTimeout + ignore = !device()->isAwake(); + // if (main_layout->currentWidget() == oscarSettingsWindow) { + // Not working... + // device()->resetInteractiveTimeout(60 * 5); // 5 minute timeout if looking at settings window + // } else { + device()->resetInteractiveTimeout(); // Default 30 seconds otherwise + // } + break; + } + default: + break; + } + return ignore; +} diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h new file mode 100755 index 0000000..ed024ce --- /dev/null +++ b/selfdrive/ui/qt/window.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "selfdrive/ui/qt/home.h" +#include "selfdrive/ui/qt/offroad/onboarding.h" +#include "selfdrive/ui/qt/offroad/settings.h" +#include "selfdrive/oscarpilot/settings/settings.h" + +class MainWindow : public QWidget { + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + +private: + bool eventFilter(QObject *obj, QEvent *event) override; + void openSettings(int index = 0, const QString ¶m = ""); + void closeSettings(); + + QStackedLayout *main_layout; + HomeWindow *homeWindow; + OscarSettingsWindow *oscarSettingsWindow; + OnboardingWindow *onboardingWindow; + + // FrogPilot variables + Params params; +}; diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py old mode 100644 new mode 100755 index 8954c09..8e17de7 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -1,7 +1,7 @@ import math import numpy as np -import os import time +import threading import wave from typing import Dict, Optional, Tuple @@ -42,20 +42,11 @@ sound_list: Dict[int, Tuple[str, Optional[int], float]] = { AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME), AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME), - # Random Events - AudibleAlert.angry: ("angry.wav", 1, MAX_VOLUME), - AudibleAlert.fart: ("fart.wav", 1, MAX_VOLUME), - AudibleAlert.firefox: ("firefox.wav", 1, MAX_VOLUME), - AudibleAlert.nessie: ("nessie.wav", 1, MAX_VOLUME), - AudibleAlert.noice: ("noice.wav", 1, MAX_VOLUME), - AudibleAlert.uwu: ("uwu.wav", 1, MAX_VOLUME), - - # Other - AudibleAlert.goat: ("goat.wav", None, MAX_VOLUME), + AudibleAlert.firefox: ("firefox.wav", None, MAX_VOLUME), } def check_controls_timeout_alert(sm): - controls_missing = time.monotonic() - sm.recv_time['controlsState'] + controls_missing = time.monotonic() - sm.rcv_time['controlsState'] if controls_missing > CONTROLS_TIMEOUT: if sm['controlsState'].enabled and (controls_missing - CONTROLS_TIMEOUT) < 10: @@ -70,19 +61,10 @@ class Soundd: self.params = Params() self.params_memory = Params("/dev/shm/params") - self.random_events_directory = BASEDIR + "/selfdrive/frogpilot/assets/random_events/sounds/" - - self.random_events_map = { - AudibleAlert.angry: MAX_VOLUME, - AudibleAlert.fart: MAX_VOLUME, - AudibleAlert.firefox: MAX_VOLUME, - AudibleAlert.nessie: MAX_VOLUME, - AudibleAlert.noice: MAX_VOLUME, - AudibleAlert.uwu: MAX_VOLUME, - } - self.update_frogpilot_params() + self.load_sounds() + self.current_alert = AudibleAlert.none self.current_volume = MIN_VOLUME self.current_sound_frame = 0 @@ -96,18 +78,9 @@ class Soundd: # Load all sounds for sound in sound_list: - if sound == AudibleAlert.goat and not self.goat_scream: - continue - filename, play_count, volume = sound_list[sound] - if sound in self.random_events_map: - wavefile = wave.open(self.random_events_directory + filename, 'r') - else: - try: - wavefile = wave.open(self.sound_directory + filename, 'r') - except FileNotFoundError: - wavefile = wave.open(BASEDIR + "/selfdrive/assets/sounds/" + filename, 'r') + wavefile = wave.open(self.sound_directory + filename, 'r') assert wavefile.getnchannels() == 1 assert wavefile.getsampwidth() == 2 @@ -176,91 +149,43 @@ class Soundd: sm = messaging.SubMaster(['controlsState', 'microphone']) - try: - with self.get_stream(sd) as stream: - rk = Ratekeeper(20) + with self.get_stream(sd) as stream: + rk = Ratekeeper(20) - cloudlog.info(f"soundd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}") - while True: - sm.update(0) + cloudlog.info(f"soundd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}") + while True: + sm.update(0) - if sm.updated['microphone'] and self.current_alert == AudibleAlert.none and not self.alert_volume_control: # only update volume filter when not playing alert - self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb) - self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) + if sm.updated['microphone'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert + self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb) + self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) if not self.silent_mode else 0 - elif self.alert_volume_control and self.current_alert in self.volume_map: - self.current_volume = self.volume_map[self.current_alert] / 100.0 + self.get_audible_alert(sm) - # Increase the volume for Random Events - elif self.current_alert in self.random_events_map: - self.current_volume = self.random_events_map[self.current_alert] + rk.keep_time() - self.get_audible_alert(sm) + assert stream.active - rk.keep_time() - - if not stream.active: - raise AssertionError("Stream is not active") - - # Update FrogPilot parameters - if self.params_memory.get_bool("FrogPilotTogglesUpdated"): - self.update_frogpilot_params() - - except AssertionError: - pass + # Update FrogPilot parameters + if self.params_memory.get_bool("FrogPilotTogglesUpdated"): + updateFrogPilotParams = threading.Thread(target=self.update_frogpilot_params) + updateFrogPilotParams.start() def update_frogpilot_params(self): - self.alert_volume_control = self.params.get_bool("AlertVolumeControl") - - self.volume_map = { - AudibleAlert.engage: self.params.get_int("EngageVolume"), - AudibleAlert.disengage: self.params.get_int("DisengageVolume"), - AudibleAlert.refuse: self.params.get_int("RefuseVolume"), - - AudibleAlert.prompt: self.params.get_int("PromptVolume"), - AudibleAlert.promptRepeat: self.params.get_int("PromptVolume"), - AudibleAlert.promptDistracted: self.params.get_int("PromptDistractedVolume"), - - AudibleAlert.warningSoft: self.params.get_int("WarningSoftVolume"), - AudibleAlert.warningImmediate: self.params.get_int("WarningImmediateVolume"), - - AudibleAlert.goat: self.params.get_int("PromptVolume"), - } + self.silent_mode = self.params.get_bool("SilentMode") custom_theme = self.params.get_bool("CustomTheme") custom_sounds = self.params.get_int("CustomSounds") if custom_theme else 0 - self.goat_scream = custom_sounds == 1 and self.params.get_bool("GoatScream") theme_configuration = { + 0: "stock", 1: "frog_theme", 2: "tesla_theme", 3: "stalin_theme" } - holiday_themes = custom_theme and self.params.get_bool("HolidayThemes") - current_holiday_theme = self.params_memory.get_int("CurrentHolidayTheme") if holiday_themes else 0 - - holiday_theme_configuration = { - 1: "april_fools", - 2: "christmas", - 3: "cinco_de_mayo", - 4: "easter", - 5: "fourth_of_july", - 6: "halloween", - 7: "new_years_day", - 8: "st_patricks_day", - 9: "thanksgiving", - 10: "valentines_day", - 11: "world_frog_day", - } - - if current_holiday_theme != 0: - theme_name = holiday_theme_configuration.get(current_holiday_theme) - self.sound_directory = BASEDIR + ("/selfdrive/frogpilot/assets/holiday_themes/" + theme_name + "/sounds/") - self.goat_scream = False - else: - theme_name = theme_configuration.get(custom_sounds) - self.sound_directory = BASEDIR + ("/selfdrive/frogpilot/assets/custom_themes/" + theme_name + "/sounds/" if custom_sounds != 0 else "/selfdrive/assets/sounds/") + theme_name = theme_configuration.get(custom_sounds, "stock") + self.sound_directory = (f"{BASEDIR}/selfdrive/frogpilot/assets/custom_themes/{theme_name}/sounds/" if custom_sounds else f"{BASEDIR}/selfdrive/assets/sounds/") self.load_sounds() diff --git a/selfdrive/ui/spinner b/selfdrive/ui/spinner index 965c8f5..35feab3 100755 --- a/selfdrive/ui/spinner +++ b/selfdrive/ui/spinner @@ -1,7 +1,7 @@ #!/bin/sh -if [ -f /TICI ] && [ ! -f _spinner ]; then - cp qt/spinner_larch64 _spinner +if [ -f /TICI ] && [ ! -f qt/spinner ]; then + cp qt/spinner_larch64 qt/spinner fi -exec ./_spinner "$1" +exec ./qt/spinner "$1" diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py new file mode 100755 index 0000000..9ba9054 --- /dev/null +++ b/selfdrive/ui/tests/test_translations.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +import json +import os +import re +import unittest +import shutil +import tempfile +import xml.etree.ElementTree as ET +import string +import requests +from parameterized import parameterized_class + +from openpilot.selfdrive.ui.update_translations import TRANSLATIONS_DIR, LANGUAGES_FILE, update_translations + +with open(LANGUAGES_FILE, "r") as f: + translation_files = json.load(f) + +UNFINISHED_TRANSLATION_TAG = "" not in cur_translations, + f"{self.file} ({self.name}) translation file has obsolete translations. Run selfdrive/ui/update_translations.py --vanish to remove them") + + def test_finished_translations(self): + """ + Tests ran on each translation marked "finished" + Plural: + - that any numerus (plural) translations have all plural forms non-empty + - that the correct format specifier is used (%n) + Non-plural: + - that translation is not empty + - that translation format arguments are consistent + """ + tr_xml = ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")) + + for context in tr_xml.getroot(): + for message in context.iterfind("message"): + translation = message.find("translation") + source_text = message.find("source").text + + # Do not test unfinished translations + if translation.get("type") == "unfinished": + continue + + if message.get("numerus") == "yes": + numerusform = [t.text for t in translation.findall("numerusform")] + + for nf in numerusform: + self.assertIsNotNone(nf, f"Ensure all plural translation forms are completed: {source_text}") + self.assertIn("%n", nf, "Ensure numerus argument (%n) exists in translation.") + self.assertIsNone(FORMAT_ARG.search(nf), "Plural translations must use %n, not %1, %2, etc.: {}".format(numerusform)) + + else: + self.assertIsNotNone(translation.text, f"Ensure translation is completed: {source_text}") + + source_args = FORMAT_ARG.findall(source_text) + translation_args = FORMAT_ARG.findall(translation.text) + self.assertEqual(sorted(source_args), sorted(translation_args), + f"Ensure format arguments are consistent: `{source_text}` vs. `{translation.text}`") + + def test_no_locations(self): + for line in self._read_translation_file(TRANSLATIONS_DIR, self.file).splitlines(): + self.assertFalse(line.strip().startswith(LOCATION_TAG), + f"Line contains location tag: {line.strip()}, remove all line numbers.") + + def test_entities_error(self): + cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file) + matches = re.findall(r'@(\w+);', cur_translations) + self.assertEqual(len(matches), 0, f"The string(s) {matches} were found with '@' instead of '&'") + + def test_bad_language(self): + IGNORED_WORDS = {'pédale'} + + match = re.search(r'_([a-zA-Z]{2,3})', self.file) + assert match, f"{self.name} - could not parse language" + + response = requests.get(f"https://raw.githubusercontent.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/master/{match.group(1)}") + response.raise_for_status() + + banned_words = {line.strip() for line in response.text.splitlines()} + + for context in ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")).getroot(): + for message in context.iterfind("message"): + translation = message.find("translation") + if translation.get("type") == "unfinished": + continue + + translation_text = " ".join([t.text for t in translation.findall("numerusform")]) if message.get("numerus") == "yes" else translation.text + + if not translation_text: + continue + + words = set(translation_text.translate(str.maketrans('', '', string.punctuation + '%n')).lower().split()) + bad_words_found = words & (banned_words - IGNORED_WORDS) + assert not bad_words_found, f"Bad language found in {self.name}: '{translation_text}'. Bad word(s): {', '.join(bad_words_found)}" + + +if __name__ == "__main__": + unittest.main() diff --git a/selfdrive/ui/text b/selfdrive/ui/text index b12235f..b44bec4 100755 --- a/selfdrive/ui/text +++ b/selfdrive/ui/text @@ -1,7 +1,7 @@ #!/bin/sh -if [ -f /TICI ] && [ ! -f _text ]; then - cp qt/text_larch64 _text +if [ -f /TICI ] && [ ! -f qt/text ]; then + cp qt/text_larch64 qt/text fi -exec ./_text "$1" +exec ./qt/text "$1" diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json old mode 100644 new mode 100755 diff --git a/selfdrive/ui/translations/main_ar.qm b/selfdrive/ui/translations/main_ar.qm deleted file mode 100644 index 145ccff..0000000 Binary files a/selfdrive/ui/translations/main_ar.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts old mode 100644 new mode 100755 index eb9a7f2..2032807 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -15,10 +15,6 @@ Reboot and Update إعادة التشغيل والتحديث - - Disable Internet Check - - AdvancedNetworking @@ -297,117 +293,6 @@ Review مراجعة - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -477,10 +362,6 @@ Manage at connect.comma.ai الإدارة في connect.comma.ai - - Manage at %1 - - MapWindow @@ -681,9 +562,13 @@ Exit إغلاق + + dashcam + dashcam + openpilot - openpilot + openpilot %n minute(s) ago @@ -734,10 +619,6 @@ ft قدم - - FrogPilot - - Reset @@ -772,19 +653,19 @@ This may take up to a minute. قد يستغرق الأمر حوالي الدقيقة. - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - غير قادر على تحميل جزء البيانات. قد يكون الجزء تالفاً. اضغط على تأكيد لمسح جهازك وإعادة ضبطه. + Press confirm to erase all content and settings. Press cancel to resume boot. + اضغط على تأكيد لمسح جميع المحتويات والإعدادات. اضغط على إلغاء لمتابعة التشغيل. - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - + Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. + غير قادر على تحميل جزء البيانات. قد يكون الجزء تالفاً. اضغط على تأكيد لمسح جهازك وإعادة ضبطه. SettingsWindow × - × + × Device @@ -802,26 +683,6 @@ This may take up to a minute. Software البرنامج - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -1003,10 +864,6 @@ This may take up to a minute. 5G 5G - - MEMORY - - SoftwarePanel @@ -1028,7 +885,7 @@ This may take up to a minute. Updates are only downloaded while the car is off. - يتم تحميل التحديثات فقط عندما تكون السيارة متوقفة. + يتم تحميل التحديثات فقط عندما تكون السيارة متوقفة. Current Version @@ -1082,50 +939,6 @@ This may take up to a minute. up to date, last checked %1 أحدث نسخة، آخر تحقق %1 - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - عرض - SshControl @@ -1388,14 +1201,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi سيتم سحب بيانات التدريب دورياً عندما يكون جهازك متصل بشبكة واي فاي - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_de.qm b/selfdrive/ui/translations/main_de.qm deleted file mode 100644 index f09c931..0000000 Binary files a/selfdrive/ui/translations/main_de.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts old mode 100644 new mode 100755 index 9946993..782d65b --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -15,10 +15,6 @@ Reboot and Update Aktualisieren und neu starten - - Disable Internet Check - - AdvancedNetworking @@ -297,117 +293,6 @@ Review Überprüfen - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -473,10 +358,6 @@ Manage at connect.comma.ai - - Manage at %1 - - MapWindow @@ -676,9 +557,13 @@ Exit Verlassen + + dashcam + dashcam + openpilot - openpilot + openpilot %n minute(s) ago @@ -717,10 +602,6 @@ ft fuß - - FrogPilot - - Reset @@ -753,12 +634,12 @@ - Resetting device... -This may take up to a minute. + Press confirm to erase all content and settings. Press cancel to resume boot. - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + Resetting device... +This may take up to a minute. @@ -766,7 +647,7 @@ This may take up to a minute. SettingsWindow × - x + x Device @@ -784,26 +665,6 @@ This may take up to a minute. Software Software - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -986,10 +847,6 @@ This may take up to a minute. 5G 5G - - MEMORY - - SoftwarePanel @@ -1012,7 +869,7 @@ This may take up to a minute. Updates are only downloaded while the car is off. - Updates werden nur heruntergeladen, wenn das Auto aus ist. + Updates werden nur heruntergeladen, wenn das Auto aus ist. Current Version @@ -1066,50 +923,6 @@ This may take up to a minute. never - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - ANSEHEN - SshControl @@ -1374,14 +1187,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_en.qm b/selfdrive/ui/translations/main_en.qm deleted file mode 100644 index dab9f0e..0000000 Binary files a/selfdrive/ui/translations/main_en.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_en.ts b/selfdrive/ui/translations/main_en.ts old mode 100644 new mode 100755 diff --git a/selfdrive/ui/translations/main_fr.qm b/selfdrive/ui/translations/main_fr.qm deleted file mode 100644 index 065a72d..0000000 Binary files a/selfdrive/ui/translations/main_fr.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts old mode 100644 new mode 100755 index 531b8a9..219e935 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -15,10 +15,6 @@ Reboot and Update Redémarrer et mettre à jour - - Disable Internet Check - - AdvancedNetworking @@ -72,23 +68,23 @@ Hidden Network - Réseau Caché + CONNECT - CONNECTER + CONNECTER Enter SSID - Entrer le SSID + Entrer le SSID Enter password - Entrer le mot de passe + Entrer le mot de passe for "%1" - pour "%1" + pour "%1" @@ -297,117 +293,6 @@ Disengage to Power Off Désengager pour éteindre - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -473,10 +358,6 @@ Manage at connect.comma.ai Gérer sur connect.comma.ai - - Manage at %1 - - MapWindow @@ -677,9 +558,13 @@ Exit Quitter + + dashcam + dashcam + openpilot - openpilot + openpilot %n minute(s) ago @@ -718,10 +603,6 @@ ft ft - - FrogPilot - - Reset @@ -743,6 +624,10 @@ Cela peut prendre jusqu'à une minute. System Reset Réinitialisation du système + + Press confirm to erase all content and settings. Press cancel to resume boot. + Appuyez sur confirmer pour effacer tout le contenu et les paramètres. Appuyez sur annuler pour reprendre le démarrage. + Cancel Annuler @@ -759,16 +644,12 @@ Cela peut prendre jusqu'à une minute. Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. Impossible de monter la partition data. La partition peut être corrompue. Appuyez sur confirmer pour effacer et réinitialiser votre appareil. - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - - SettingsWindow × - × + × Device @@ -786,26 +667,6 @@ Cela peut prendre jusqu'à une minute. Software Logiciel - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -987,16 +848,12 @@ Cela peut prendre jusqu'à une minute. 5G 5G - - MEMORY - - SoftwarePanel Updates are only downloaded while the car is off. - Les MàJ sont téléchargées uniquement si la voiture est éteinte. + Les MàJ sont téléchargées uniquement si la voiture est éteinte. Current Version @@ -1066,50 +923,6 @@ Cela peut prendre jusqu'à une minute. up to date, last checked %1 à jour, dernière vérification %1 - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - VOIR - SshControl @@ -1372,14 +1185,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi Les données d'entraînement seront envoyées périodiquement lorsque votre appareil est connecté au réseau Wi-Fi - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_ja.qm b/selfdrive/ui/translations/main_ja.qm deleted file mode 100644 index 50c53e8..0000000 Binary files a/selfdrive/ui/translations/main_ja.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts old mode 100644 new mode 100755 index 65fabe9..ba3ca8f --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -15,10 +15,6 @@ Reboot and Update 再起動してアップデート - - Disable Internet Check - - AdvancedNetworking @@ -297,117 +293,6 @@ Review 確認 - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -472,10 +357,6 @@ Manage at connect.comma.ai - - Manage at %1 - - MapWindow @@ -675,9 +556,13 @@ Exit 閉じる + + dashcam + ドライブレコーダー + openpilot - openpilot + openpilot %n minute(s) ago @@ -713,10 +598,6 @@ ft フィート - - FrogPilot - - Reset @@ -749,12 +630,12 @@ - Resetting device... -This may take up to a minute. + Press confirm to erase all content and settings. Press cancel to resume boot. - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + Resetting device... +This may take up to a minute. @@ -762,7 +643,7 @@ This may take up to a minute. SettingsWindow × - × + × Device @@ -780,26 +661,6 @@ This may take up to a minute. Software ソフトウェア - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -981,16 +842,12 @@ This may take up to a minute. 5G 5G - - MEMORY - - SoftwarePanel Updates are only downloaded while the car is off. - 車の電源がオフの間のみ、アップデートのダウンロードが行われます。 + 車の電源がオフの間のみ、アップデートのダウンロードが行われます。 Current Version @@ -1060,50 +917,6 @@ This may take up to a minute. never - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - 見る - SshControl @@ -1366,14 +1179,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_ko.qm b/selfdrive/ui/translations/main_ko.qm deleted file mode 100644 index f51916c..0000000 Binary files a/selfdrive/ui/translations/main_ko.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts old mode 100644 new mode 100755 index a1b178a..8c1a0fb --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -15,10 +15,6 @@ Reboot and Update 업데이트 및 재부팅 - - Disable Internet Check - - AdvancedNetworking @@ -72,23 +68,23 @@ Hidden Network - 숨겨진 네트워크 + CONNECT - 연결됨 + 연결됨 Enter SSID - SSID 입력 + SSID 입력 Enter password - 비밀번호를 입력하세요 + 비밀번호를 입력하세요 for "%1" - "%1"에 접속하려면 비밀번호가 필요합니다 + "%1"에 접속하려면 비밀번호가 필요합니다 @@ -297,117 +293,6 @@ Review 다시보기 - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -472,10 +357,6 @@ Manage at connect.comma.ai connect.comma.ai에서 관리하세요 - - Manage at %1 - - MapWindow @@ -676,9 +557,13 @@ Exit 종료 + + dashcam + 블랙박스 + openpilot - openpilot + openpilot %n minute(s) ago @@ -714,10 +599,6 @@ ft ft - - FrogPilot - - Reset @@ -749,22 +630,22 @@ Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. 데이터 파티션을 마운트할 수 없습니다. 파티션이 손상되었을 수 있습니다. 모든 설정을 삭제하고 장치를 초기화하려면 확인을 누르세요. + + Press confirm to erase all content and settings. Press cancel to resume boot. + 모든 콘텐츠와 설정을 삭제하려면 확인을 누르세요. 계속 부팅하려면 취소를 누르세요. + Resetting device... This may take up to a minute. 장치를 초기화하는 중... 최대 1분이 소요될 수 있습니다. - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - - SettingsWindow × - × + × Device @@ -782,26 +663,6 @@ This may take up to a minute. Software 소프트웨어 - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -983,16 +844,12 @@ This may take up to a minute. 5G 5G - - MEMORY - - SoftwarePanel Updates are only downloaded while the car is off. - 업데이트는 차량 시동이 꺼졌을 때 다운로드됩니다. + 업데이트는 차량 시동이 꺼졌을 때 다운로드됩니다. Current Version @@ -1062,50 +919,6 @@ This may take up to a minute. never 업데이트 안함 - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - 보기 - SshControl @@ -1368,14 +1181,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi 기기가 Wi-Fi에 연결되어 있는 동안 트레이닝 데이터를 주기적으로 전송합니다 - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_nl.ts b/selfdrive/ui/translations/main_nl.ts old mode 100644 new mode 100755 diff --git a/selfdrive/ui/translations/main_pl.ts b/selfdrive/ui/translations/main_pl.ts old mode 100644 new mode 100755 diff --git a/selfdrive/ui/translations/main_pt-BR.qm b/selfdrive/ui/translations/main_pt-BR.qm deleted file mode 100644 index 7dde67a..0000000 Binary files a/selfdrive/ui/translations/main_pt-BR.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts old mode 100644 new mode 100755 index d45133a..f983c8f --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -15,10 +15,6 @@ Reboot and Update Reiniciar e Atualizar - - Disable Internet Check - - AdvancedNetworking @@ -72,23 +68,23 @@ Hidden Network - Rede Oculta + CONNECT - CONECTE + CONEXÃO Enter SSID - Digite o SSID + Insira SSID Enter password - Insira a senha + Insira a senha for "%1" - para "%1" + para "%1" @@ -297,117 +293,6 @@ Review Revisar - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -473,10 +358,6 @@ Manage at connect.comma.ai Gerencie em connect.comma.ai - - Manage at %1 - - MapWindow @@ -677,9 +558,13 @@ Exit Sair + + dashcam + dashcam + openpilot - openpilot + openpilot %n minute(s) ago @@ -718,10 +603,6 @@ ft pés - - FrogPilot - - Reset @@ -753,22 +634,22 @@ Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. Não é possível montar a partição de dados. Partição corrompida. Confirme para apagar e redefinir o dispositivo. + + Press confirm to erase all content and settings. Press cancel to resume boot. + Pressione confirmar para apagar todo o conteúdo e configurações. Pressione cancelar para voltar. + Resetting device... This may take up to a minute. Redefinindo o dispositivo Isso pode levar até um minuto. - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Reinicialização do sistema acionada. Pressione confirmar para apagar todo o conteúdo e configurações. Pressione cancel para retomar a inicialização. - SettingsWindow × - × + × Device @@ -786,26 +667,6 @@ Isso pode levar até um minuto. Software Software - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -987,16 +848,12 @@ Isso pode levar até um minuto. 5G 5G - - MEMORY - - SoftwarePanel Updates are only downloaded while the car is off. - Atualizações baixadas durante o motor desligado. + Atualizações baixadas durante o motor desligado. Current Version @@ -1066,50 +923,6 @@ Isso pode levar até um minuto. never nunca - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - VER - SshControl @@ -1372,14 +1185,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi Os dados de treinamento serão extraídos periodicamente enquanto o dispositivo estiver no Wi-Fi - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_th.qm b/selfdrive/ui/translations/main_th.qm deleted file mode 100644 index 29a4ad2..0000000 Binary files a/selfdrive/ui/translations/main_th.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts old mode 100644 new mode 100755 index 8cd55f3..0d0bda4 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -15,10 +15,6 @@ Reboot and Update รีบูตและอัปเดต - - Disable Internet Check - - AdvancedNetworking @@ -297,117 +293,6 @@ Review ทบทวน - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -472,10 +357,6 @@ Manage at connect.comma.ai จัดการได้ที่ connect.comma.ai - - Manage at %1 - - MapWindow @@ -676,9 +557,13 @@ Exit ปิด + + dashcam + กล้องติดรถยนต์ + openpilot - openpilot + openpilot %n minute(s) ago @@ -714,10 +599,6 @@ ft ฟุต - - FrogPilot - - Reset @@ -752,19 +633,19 @@ This may take up to a minute. อาจใช้เวลาถึงหนึ่งนาที - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - ไม่สามารถเมานต์พาร์ติชั่นข้อมูลได้ พาร์ติชั่นอาจเสียหาย กดยืนยันเพื่อลบและรีเซ็ตอุปกรณ์ของคุณ + Press confirm to erase all content and settings. Press cancel to resume boot. + กดยืนยันเพื่อลบข้อมูลและการตั้งค่าทั้งหมด กดยกเลิกเพื่อบูตต่อ - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - + Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. + ไม่สามารถเมานต์พาร์ติชั่นข้อมูลได้ พาร์ติชั่นอาจเสียหาย กดยืนยันเพื่อลบและรีเซ็ตอุปกรณ์ของคุณ SettingsWindow × - × + × Device @@ -782,26 +663,6 @@ This may take up to a minute. Software ซอฟต์แวร์ - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -983,10 +844,6 @@ This may take up to a minute. 5G 5G - - MEMORY - - SoftwarePanel @@ -1008,7 +865,7 @@ This may take up to a minute. Updates are only downloaded while the car is off. - ตัวอัปเดตจะดำเนินการดาวน์โหลดเมื่อรถดับเครื่องยนต์อยู่เท่านั้น + ตัวอัปเดตจะดำเนินการดาวน์โหลดเมื่อรถดับเครื่องยนต์อยู่เท่านั้น Current Version @@ -1062,50 +919,6 @@ This may take up to a minute. up to date, last checked %1 ล่าสุดแล้ว ตรวจสอบครั้งสุดท้ายเมื่อ %1 - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - ดู - SshControl @@ -1368,14 +1181,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi ข้อมูลการฝึกฝนจะถูกดึงเป็นระยะระหว่างที่อุปกรณ์ของคุณเชื่อมต่อกับ Wi-Fi - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_tr.qm b/selfdrive/ui/translations/main_tr.qm deleted file mode 100644 index b3629ee..0000000 Binary files a/selfdrive/ui/translations/main_tr.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts old mode 100644 new mode 100755 index 2c957e0..d14dd51 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -15,10 +15,6 @@ Reboot and Update Güncelle ve Yeniden başlat - - Disable Internet Check - - AdvancedNetworking @@ -297,117 +293,6 @@ Review - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -472,10 +357,6 @@ Manage at connect.comma.ai - - Manage at %1 - - MapWindow @@ -675,9 +556,13 @@ Exit Çık + + dashcam + araç yol kamerası + openpilot - openpilot + openpilot %n minute(s) ago @@ -713,10 +598,6 @@ ft ft - - FrogPilot - - Reset @@ -750,11 +631,11 @@ This may take up to a minute. - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. + Press confirm to erase all content and settings. Press cancel to resume boot. - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. @@ -762,7 +643,7 @@ This may take up to a minute. SettingsWindow × - x + x Device @@ -780,26 +661,6 @@ This may take up to a minute. Software Yazılım - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -981,10 +842,6 @@ This may take up to a minute. 5G 5G - - MEMORY - - SoftwarePanel @@ -1004,6 +861,10 @@ This may take up to a minute. CHECK KONTROL ET + + Updates are only downloaded while the car is off. + + Current Version @@ -1056,50 +917,6 @@ This may take up to a minute. up to date, last checked %1 - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - BAK - SshControl @@ -1362,14 +1179,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm deleted file mode 100644 index 2355145..0000000 Binary files a/selfdrive/ui/translations/main_zh-CHS.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts old mode 100644 new mode 100755 index 3fd0f7b..ef0d784 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -15,10 +15,6 @@ Reboot and Update 重启并更新 - - Disable Internet Check - - AdvancedNetworking @@ -297,117 +293,6 @@ Review 预览 - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -472,10 +357,6 @@ Manage at connect.comma.ai 请在 connect.comma.ai 上管理 - - Manage at %1 - - MapWindow @@ -676,9 +557,13 @@ Exit 退出 + + dashcam + 行车记录仪 + openpilot - openpilot + openpilot %n minute(s) ago @@ -714,10 +599,6 @@ ft ft - - FrogPilot - - Reset @@ -749,22 +630,22 @@ Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. 无法挂载数据分区。分区可能已经损坏。请确认是否要删除并重新设置。 + + Press confirm to erase all content and settings. Press cancel to resume boot. + 按下确认以删除所有内容及设置。按下取消来继续开机。 + Resetting device... This may take up to a minute. 设备重置中… 这可能需要一分钟的时间。 - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - - SettingsWindow × - × + × Device @@ -782,26 +663,6 @@ This may take up to a minute. Software 软件 - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -983,16 +844,12 @@ This may take up to a minute. 5G 5G - - MEMORY - - SoftwarePanel Updates are only downloaded while the car is off. - 车辆熄火时才能下载升级文件。 + 车辆熄火时才能下载升级文件。 Current Version @@ -1062,50 +919,6 @@ This may take up to a minute. never 从未更新 - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - 查看 - SshControl @@ -1368,14 +1181,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi 训练数据将定期通过 Wi-Fi 上载 - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/translations/main_zh-CHT.qm b/selfdrive/ui/translations/main_zh-CHT.qm deleted file mode 100644 index 6db3b36..0000000 Binary files a/selfdrive/ui/translations/main_zh-CHT.qm and /dev/null differ diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts old mode 100644 new mode 100755 index 56c447b..121bf58 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -15,10 +15,6 @@ Reboot and Update 重啟並更新 - - Disable Internet Check - - AdvancedNetworking @@ -297,117 +293,6 @@ Review 回顧 - - Delete Driving Data - - - - DELETE - - - - This button provides a swift and secure way to permanently delete all stored driving footage and data from your device. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your driving footage and data? - - - - Delete - - - - Delete Toggle Storage Data - - - - This button provides a swift and secure way to permanently delete all long term stored toggle settings. Ideal for maintaining privacy or freeing up space. - - - - Are you sure you want to permanently delete all of your long term toggle settings storage? - - - - Backup - - - - Restore - - - - Name your backup - - - - Select a backup to delete - - - - Are you sure you want to delete this backup? - - - - Select a restore point - - - - Are you sure you want to restore this version of FrogPilot? - - - - Are you sure you want to restore this toggle backup? - - - - Flash Panda - - - - FLASH - - - - Are you sure you want to flash the Panda? - - - - Flash - - - - - DriveStats - - Drives - - - - Hours - - - - ALL TIME - - - - PAST WEEK - - - - FROGPILOT - - - - KM - - - - Miles - - DriverViewWindow @@ -472,10 +357,6 @@ Manage at connect.comma.ai 請在 connect.comma.ai 上管理 - - Manage at %1 - - MapWindow @@ -676,9 +557,13 @@ Exit 離開 + + dashcam + 行車記錄器 + openpilot - openpilot + openpilot %n minute(s) ago @@ -714,10 +599,6 @@ ft ft - - FrogPilot - - Reset @@ -749,22 +630,22 @@ Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. 無法掛載資料分割區。分割區可能已經毀損。請確認是否要刪除並重新設定。 + + Press confirm to erase all content and settings. Press cancel to resume boot. + 按下確認以刪除所有內容及設定。按下取消來繼續開機。 + Resetting device... This may take up to a minute. 設備重設中… 這可能需要一分鐘的時間。 - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - - SettingsWindow × - × + × Device @@ -782,26 +663,6 @@ This may take up to a minute. Software 軟體 - - ← Back - - - - Controls - - - - Navigation - - - - Vehicles - - - - Visuals - - Setup @@ -983,16 +844,12 @@ This may take up to a minute. 5G 5G - - MEMORY - - SoftwarePanel Updates are only downloaded while the car is off. - 系統更新只會在熄火時下載。 + 系統更新只會在熄火時下載。 Current Version @@ -1062,50 +919,6 @@ This may take up to a minute. never 從未更新 - - Updates are only downloaded while the car is off or in park. - - - - Manually - - - - Daily - - - - Weekly - - - - Update Scheduler - - - - Choose the frequency to automatically update FrogPilot. - -This feature will handle the download, installation, and device reboot for a seamless 'Set and Forget' update experience. - -Weekly updates start at midnight every Sunday. - - - - Update Time - - - - Select a time to automatically update - - - - Error Log - - - - VIEW - 觀看 - SshControl @@ -1368,14 +1181,6 @@ Weekly updates start at midnight every Sunday. Training data will be pulled periodically while your device is on Wi-Fi 訓練數據將定期經過 Wi-Fi 上傳 - - Uploading disabled - - - - Training data wont be pulled periodically until you disable the 'Disable Uploading' toggle - - WifiUI diff --git a/selfdrive/ui/ui b/selfdrive/ui/ui deleted file mode 100755 index 514fd83..0000000 Binary files a/selfdrive/ui/ui and /dev/null differ diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc new file mode 100755 index 0000000..ed95e9d --- /dev/null +++ b/selfdrive/ui/ui.cc @@ -0,0 +1,526 @@ +#include "selfdrive/ui/ui.h" + +#include +#include +#include + +#include + +#include "common/transformations/orientation.hpp" +#include "common/params.h" +#include "common/swaglog.h" +#include "common/util.h" +#include "common/watchdog.h" +#include "system/hardware/hw.h" + +#include "selfdrive/frogpilot/ui/frogpilot_functions.h" + +#define BACKLIGHT_DT 0.05 +#define BACKLIGHT_TS 10.00 + +// Projects a point in car to space to the corresponding point in full frame +// image space. +static bool calib_frame_to_full_frame(const UIState *s, float in_x, float in_y, float in_z, QPointF *out) { + const float margin = 500.0f; + const QRectF clip_region{-margin, -margin, s->fb_w + 2 * margin, s->fb_h + 2 * margin}; + + const vec3 pt = (vec3){{in_x, in_y, in_z}}; + const vec3 Ep = matvecmul3(s->scene.wide_cam ? s->scene.view_from_wide_calib : s->scene.view_from_calib, pt); + const vec3 KEp = matvecmul3(s->scene.wide_cam ? ECAM_INTRINSIC_MATRIX : FCAM_INTRINSIC_MATRIX, Ep); + + // Project. + QPointF point = s->car_space_transform.map(QPointF{KEp.v[0] / KEp.v[2], KEp.v[1] / KEp.v[2]}); + if (clip_region.contains(point)) { + *out = point; + return true; + } + return false; +} + +int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) { + const auto line_x = line.getX(); + int max_idx = 0; + for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) { + max_idx = i; + } + return max_idx; +} + +void update_leads(UIState *s, const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line) { + for (int i = 0; i < 2; ++i) { + auto lead_data = (i == 0) ? radar_state.getLeadOne() : radar_state.getLeadTwo(); + if (lead_data.getStatus()) { + float z = line.getZ()[get_path_length_idx(line, lead_data.getDRel())]; + calib_frame_to_full_frame(s, lead_data.getDRel(), -lead_data.getYRel(), z + 1.22, &s->scene.lead_vertices[i]); + } + } +} + +void update_line_data(const UIState *s, const cereal::XYZTData::Reader &line, + float y_off, float z_off, QPolygonF *pvd, int max_idx, bool allow_invert=true) { + const auto line_x = line.getX(), line_y = line.getY(), line_z = line.getZ(); + QPolygonF left_points, right_points; + left_points.reserve(max_idx + 1); + right_points.reserve(max_idx + 1); + + for (int i = 0; i <= max_idx; i++) { + // highly negative x positions are drawn above the frame and cause flickering, clip to zy plane of camera + if (line_x[i] < 0) continue; + QPointF left, right; + bool l = calib_frame_to_full_frame(s, line_x[i], line_y[i] - y_off, line_z[i] + z_off, &left); + bool r = calib_frame_to_full_frame(s, line_x[i], line_y[i] + y_off, line_z[i] + z_off, &right); + if (l && r) { + // For wider lines the drawn polygon will "invert" when going over a hill and cause artifacts + if (!allow_invert && left_points.size() && left.y() > left_points.back().y()) { + continue; + } + left_points.push_back(left); + right_points.push_front(right); + } + } + *pvd = left_points + right_points; +} + +void update_model(UIState *s, + const cereal::ModelDataV2::Reader &model, + const cereal::UiPlan::Reader &plan) { + UIScene &scene = s->scene; + auto plan_position = plan.getPosition(); + if (plan_position.getX().size() < model.getPosition().getX().size()) { + plan_position = model.getPosition(); + } + float max_distance = scene.unlimited_road_ui_length ? *(plan_position.getX().end() - 1) : + std::clamp(*(plan_position.getX().end() - 1), + MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE); + + // update lane lines + const auto lane_lines = model.getLaneLines(); + const auto lane_line_probs = model.getLaneLineProbs(); + int max_idx = get_path_length_idx(lane_lines[0], max_distance); + for (int i = 0; i < std::size(scene.lane_line_vertices); i++) { + scene.lane_line_probs[i] = lane_line_probs[i]; + update_line_data(s, lane_lines[i], scene.model_ui ? scene.lane_line_width * scene.lane_line_probs[i] : 0.025 * scene.lane_line_probs[i], 0, &scene.lane_line_vertices[i], max_idx); + } + + // update road edges + const auto road_edges = model.getRoadEdges(); + const auto road_edge_stds = model.getRoadEdgeStds(); + for (int i = 0; i < std::size(scene.road_edge_vertices); i++) { + scene.road_edge_stds[i] = road_edge_stds[i]; + update_line_data(s, road_edges[i], scene.model_ui ? scene.road_edge_width : 0.025, 0, &scene.road_edge_vertices[i], max_idx); + } + + // update path + auto lead_one = (*s->sm)["radarState"].getRadarState().getLeadOne(); + if (lead_one.getStatus()) { + const float lead_d = lead_one.getDRel() * 2.; + max_distance = std::clamp((float)(lead_d - fmin(lead_d * 0.35, 10.)), 0.0f, max_distance); + } + max_idx = get_path_length_idx(plan_position, max_distance); + update_line_data(s, plan_position, scene.model_ui ? scene.path_width * (1 - scene.path_edge_width / 100) : 0.9, 1.22, &scene.track_vertices, max_idx, false); + + // update path edges + update_line_data(s, plan_position, scene.model_ui ? scene.path_width : 0, 1.22, &scene.track_edge_vertices, max_idx, false); + + // update adjacent paths + for (int i = 4; i <= 5; i++) { + update_line_data(s, lane_lines[i], scene.blind_spot_path ? (i == 4 ? scene.lane_width_left : scene.lane_width_right) / 2 : 0, 0, &scene.track_adjacent_vertices[i], max_idx); + } +} + +void update_dmonitoring(UIState *s, const cereal::DriverStateV2::Reader &driverstate, float dm_fade_state, bool is_rhd) { + UIScene &scene = s->scene; + const auto driver_orient = is_rhd ? driverstate.getRightDriverData().getFaceOrientation() : driverstate.getLeftDriverData().getFaceOrientation(); + for (int i = 0; i < std::size(scene.driver_pose_vals); i++) { + float v_this = (i == 0 ? (driver_orient[i] < 0 ? 0.7 : 0.9) : 0.4) * driver_orient[i]; + scene.driver_pose_diff[i] = fabs(scene.driver_pose_vals[i] - v_this); + scene.driver_pose_vals[i] = 0.8 * v_this + (1 - 0.8) * scene.driver_pose_vals[i]; + scene.driver_pose_sins[i] = sinf(scene.driver_pose_vals[i]*(1.0-dm_fade_state)); + scene.driver_pose_coss[i] = cosf(scene.driver_pose_vals[i]*(1.0-dm_fade_state)); + } + + const mat3 r_xyz = (mat3){{ + scene.driver_pose_coss[1]*scene.driver_pose_coss[2], + scene.driver_pose_coss[1]*scene.driver_pose_sins[2], + -scene.driver_pose_sins[1], + + -scene.driver_pose_sins[0]*scene.driver_pose_sins[1]*scene.driver_pose_coss[2] - scene.driver_pose_coss[0]*scene.driver_pose_sins[2], + -scene.driver_pose_sins[0]*scene.driver_pose_sins[1]*scene.driver_pose_sins[2] + scene.driver_pose_coss[0]*scene.driver_pose_coss[2], + -scene.driver_pose_sins[0]*scene.driver_pose_coss[1], + + scene.driver_pose_coss[0]*scene.driver_pose_sins[1]*scene.driver_pose_coss[2] - scene.driver_pose_sins[0]*scene.driver_pose_sins[2], + scene.driver_pose_coss[0]*scene.driver_pose_sins[1]*scene.driver_pose_sins[2] + scene.driver_pose_sins[0]*scene.driver_pose_coss[2], + scene.driver_pose_coss[0]*scene.driver_pose_coss[1], + }}; + + // transform vertices + for (int kpi = 0; kpi < std::size(default_face_kpts_3d); kpi++) { + vec3 kpt_this = default_face_kpts_3d[kpi]; + kpt_this = matvecmul3(r_xyz, kpt_this); + scene.face_kpts_draw[kpi] = (vec3){{(float)kpt_this.v[0], (float)kpt_this.v[1], (float)(kpt_this.v[2] * (1.0-dm_fade_state) + 8 * dm_fade_state)}}; + } +} + +static void update_sockets(UIState *s) { + s->sm->update(0); +} + +static void update_state(UIState *s) { + SubMaster &sm = *(s->sm); + UIScene &scene = s->scene; + + if (sm.updated("liveCalibration")) { + auto live_calib = sm["liveCalibration"].getLiveCalibration(); + auto rpy_list = live_calib.getRpyCalib(); + auto wfde_list = live_calib.getWideFromDeviceEuler(); + Eigen::Vector3d rpy; + Eigen::Vector3d wfde; + if (rpy_list.size() == 3) rpy << rpy_list[0], rpy_list[1], rpy_list[2]; + if (wfde_list.size() == 3) wfde << wfde_list[0], wfde_list[1], wfde_list[2]; + Eigen::Matrix3d device_from_calib = euler2rot(rpy); + Eigen::Matrix3d wide_from_device = euler2rot(wfde); + Eigen::Matrix3d view_from_device; + view_from_device << 0, 1, 0, + 0, 0, 1, + 1, 0, 0; + Eigen::Matrix3d view_from_calib = view_from_device * device_from_calib; + Eigen::Matrix3d view_from_wide_calib = view_from_device * wide_from_device * device_from_calib; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + scene.view_from_calib.v[i*3 + j] = view_from_calib(i, j); + scene.view_from_wide_calib.v[i*3 + j] = view_from_wide_calib(i, j); + } + } + scene.calibration_valid = live_calib.getCalStatus() == cereal::LiveCalibrationData::Status::CALIBRATED; + scene.calibration_wide_valid = wfde_list.size() == 3; + } + if (sm.updated("pandaStates")) { + auto pandaStates = sm["pandaStates"].getPandaStates(); + if (pandaStates.size() > 0) { + scene.pandaType = pandaStates[0].getPandaType(); + + if (scene.pandaType != cereal::PandaState::PandaType::UNKNOWN) { + scene.ignition = false; + for (const auto& pandaState : pandaStates) { + scene.ignition |= pandaState.getIgnitionLine() || pandaState.getIgnitionCan(); + } + } + } + } else if ((s->sm->frame - s->sm->rcv_frame("pandaStates")) > 5*UI_FREQ) { + scene.pandaType = cereal::PandaState::PandaType::UNKNOWN; + } + if (sm.updated("carParams")) { + scene.longitudinal_control = sm["carParams"].getCarParams().getOpenpilotLongitudinalControl(); + } + if (sm.updated("carState")) { + auto carState = sm["carState"].getCarState(); + if (scene.blind_spot_path || scene.custom_signals) { + scene.blind_spot_left = carState.getLeftBlindspot(); + scene.blind_spot_right = carState.getRightBlindspot(); + } + if (scene.custom_signals) { + scene.turn_signal_left = carState.getLeftBlinker(); + scene.turn_signal_right = carState.getRightBlinker(); + } + if (scene.rotating_wheel) { + scene.steering_angle_deg = carState.getSteeringAngleDeg(); + } + if (scene.driver_camera) { + scene.show_driver_camera = carState.getGearShifter() == cereal::CarState::GearShifter::REVERSE; + } + } + if (sm.updated("controlsState")) { + auto controlsState = sm["controlsState"].getControlsState(); + scene.enabled = controlsState.getEnabled(); + scene.experimental_mode = controlsState.getExperimentalMode(); + } + if (sm.updated("frogpilotCarControl")) { + auto frogpilotCarControl = sm["frogpilotCarControl"].getFrogpilotCarControl(); + if (scene.always_on_lateral) { + scene.always_on_lateral_active = !scene.enabled && frogpilotCarControl.getAlwaysOnLateral(); + } + } + if (sm.updated("frogpilotLateralPlan")) { + auto frogpilotLateralPlan = sm["frogpilotLateralPlan"].getFrogpilotLateralPlan(); + if (scene.blind_spot_path) { + scene.lane_width_left = frogpilotLateralPlan.getLaneWidthLeft(); + scene.lane_width_right = frogpilotLateralPlan.getLaneWidthRight(); + } + } + if (sm.updated("frogpilotLongitudinalPlan")) { + auto frogpilotLongitudinalPlan = sm["frogpilotLongitudinalPlan"].getFrogpilotLongitudinalPlan(); + if (scene.lead_info) { + scene.desired_follow = frogpilotLongitudinalPlan.getDesiredFollowDistance(); + scene.obstacle_distance = frogpilotLongitudinalPlan.getSafeObstacleDistance(); + scene.obstacle_distance_stock = frogpilotLongitudinalPlan.getSafeObstacleDistanceStock(); + scene.stopped_equivalence = frogpilotLongitudinalPlan.getStoppedEquivalenceFactor(); + } + if (scene.speed_limit_controller) { + scene.speed_limit = frogpilotLongitudinalPlan.getSlcSpeedLimit(); + scene.speed_limit_offset = frogpilotLongitudinalPlan.getSlcSpeedLimitOffset(); + scene.speed_limit_overridden = frogpilotLongitudinalPlan.getSlcOverridden(); + scene.speed_limit_overridden_speed = frogpilotLongitudinalPlan.getSlcOverriddenSpeed(); + } + scene.adjusted_cruise = frogpilotLongitudinalPlan.getAdjustedCruise(); + } + if (sm.updated("liveLocationKalman")) { + auto liveLocationKalman = sm["liveLocationKalman"].getLiveLocationKalman(); + if (scene.compass) { + auto orientation = liveLocationKalman.getCalibratedOrientationNED(); + if (orientation.getValid()) { + scene.bearing_deg = RAD2DEG(orientation.getValue()[2]); + } + } + } + if (sm.updated("wideRoadCameraState")) { + auto cam_state = sm["wideRoadCameraState"].getWideRoadCameraState(); + float scale = (cam_state.getSensor() == cereal::FrameData::ImageSensor::AR0231) ? 6.0f : 1.0f; + scene.light_sensor = std::max(100.0f - scale * cam_state.getExposureValPercent(), 0.0f); + } + scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition; + + scene.world_objects_visible = scene.world_objects_visible || + (scene.started && + sm.rcv_frame("liveCalibration") > scene.started_frame && + sm.rcv_frame("modelV2") > scene.started_frame && + sm.rcv_frame("uiPlan") > scene.started_frame); +} + +void ui_update_params(UIState *s) { + auto params = Params(); + s->scene.is_metric = params.getBool("IsMetric"); + s->scene.map_on_left = params.getBool("NavSettingLeftSide"); + + // FrogPilot variables + UIScene &scene = s->scene; + + scene.always_on_lateral = params.getBool("AlwaysOnLateral"); + scene.camera_view = params.getInt("CameraView"); + scene.compass = params.getBool("Compass"); + + scene.conditional_experimental = params.getBool("ConditionalExperimental"); + scene.conditional_speed = params.getInt("CESpeed"); + scene.conditional_speed_lead = params.getInt("CESpeedLead"); + + scene.custom_onroad_ui = params.getBool("CustomUI"); + scene.adjacent_path = params.getBool("AdjacentPath") && scene.custom_onroad_ui; + scene.blind_spot_path = params.getBool("BlindSpotPath") && scene.custom_onroad_ui; + scene.lead_info = params.getBool("LeadInfo") && scene.custom_onroad_ui; + scene.road_name_ui = params.getBool("RoadNameUI") && scene.custom_onroad_ui; + scene.show_fps = params.getBool("ShowFPS") && scene.custom_onroad_ui; + scene.use_si = params.getBool("UseSI") && scene.custom_onroad_ui; + scene.use_vienna_slc_sign = params.getBool("UseVienna") && scene.custom_onroad_ui; + + scene.custom_theme = params.getBool("CustomTheme"); + scene.custom_colors = scene.custom_theme ? params.getInt("CustomColors") : 0; + scene.custom_icons = scene.custom_theme ? params.getInt("CustomIcons") : 0; + scene.custom_signals = scene.custom_theme ? params.getInt("CustomSignals") : 0; + + scene.model_ui = params.getBool("ModelUI"); + scene.acceleration_path = params.getBool("AccelerationPath") && scene.model_ui; + scene.lane_line_width = params.getInt("LaneLinesWidth") * (scene.is_metric ? 1 : INCH_TO_CM) / 200; + scene.path_edge_width = params.getInt("PathEdgeWidth"); + scene.path_width = params.getInt("PathWidth") / 10.0 * (scene.is_metric ? 1 : FOOT_TO_METER) / 2; + scene.road_edge_width = params.getInt("RoadEdgesWidth") * (scene.is_metric ? 1 : INCH_TO_CM) / 200; + scene.unlimited_road_ui_length = params.getBool("UnlimitedLength") && scene.model_ui; + + scene.driver_camera = params.getBool("DriverCamera"); + scene.experimental_mode_via_screen = params.getBool("ExperimentalModeViaScreen") && params.getBool("ExperimentalModeActivation"); + scene.mute_dm = params.getBool("MuteDM") && params.getBool("FireTheBabysitter"); + // scene.personalities_via_screen = (params.getInt("AdjustablePersonalities") == 2 || params.getInt("AdjustablePersonalities") == 3); + scene.personalities_via_screen = false; + + scene.quality_of_life_controls = params.getBool("QOLControls"); + scene.reverse_cruise = params.getBool("ReverseCruise") && scene.quality_of_life_controls; + + scene.quality_of_life_visuals = params.getBool("QOLVisuals"); + scene.full_map = params.getBool("FullMap") && scene.quality_of_life_visuals; + scene.hide_speed = params.getBool("HideSpeed") && scene.quality_of_life_visuals; + scene.show_slc_offset = params.getBool("ShowSLCOffset") && scene.quality_of_life_visuals; + + scene.random_events = params.getBool("RandomEvents"); + scene.rotating_wheel = params.getBool("RotatingWheel"); + scene.screen_brightness = params.getInt("ScreenBrightness"); + scene.speed_limit_controller = params.getBool("SpeedLimitController"); + scene.wheel_icon = params.getInt("WheelIcon"); +} + +void UIState::updateStatus() { + if (scene.started && sm->updated("controlsState")) { + auto controls_state = (*sm)["controlsState"].getControlsState(); + auto state = controls_state.getState(); + if (state == cereal::ControlsState::OpenpilotState::PRE_ENABLED || state == cereal::ControlsState::OpenpilotState::OVERRIDING) { + status = STATUS_OVERRIDE; + } else if (scene.always_on_lateral_active) { + status = STATUS_LATERAL_ACTIVE; + } else { + status = controls_state.getEnabled() ? STATUS_ENGAGED : STATUS_DISENGAGED; + } + } + + // Handle onroad/offroad transition + if (scene.started != started_prev || sm->frame == 1) { + if (scene.started) { + status = STATUS_DISENGAGED; + scene.started_frame = sm->frame; + } + started_prev = scene.started; + scene.world_objects_visible = false; + emit offroadTransition(!scene.started); + wifi->setTetheringEnabled(scene.started && scene.tethering_enabled); + } +} + +UIState::UIState(QObject *parent) : QObject(parent) { + sm = std::make_unique>({ + "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", + "pandaStates", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2", + "wideRoadCameraState", "managerState", "navInstruction", "navRoute", "uiPlan", + "frogpilotCarControl", "frogpilotDeviceState", "frogpilotLateralPlan", "frogpilotLongitudinalPlan", + }); + + Params params; + language = QString::fromStdString(params.get("LanguageSetting")); + auto prime_value = params.get("PrimeType"); + if (!prime_value.empty()) { + prime_type = static_cast(std::atoi(prime_value.c_str())); + } + + // update timer + timer = new QTimer(this); + QObject::connect(timer, &QTimer::timeout, this, &UIState::update); + timer->start(1000 / UI_FREQ); + + wifi = new WifiManager(this); + + ui_update_params(this); + setDefaultParams(); +} + +void UIState::update() { + update_sockets(this); + update_state(this); + updateStatus(); + + if (sm->frame % UI_FREQ == 0) { + watchdog_kick(nanos_since_boot()); + } + emit uiUpdate(*this); + + // Update FrogPilot variables when they are changed + Params paramsMemory{"/dev/shm/params"}; + if (paramsMemory.getBool("FrogPilotTogglesUpdated")) { + std::thread updateFrogPilotParams(ui_update_params, this); + updateFrogPilotParams.detach(); + } + + // FrogPilot live variables that need to be constantly checked + if (scene.conditional_experimental) { + scene.conditional_status = paramsMemory.getInt("CEStatus"); + } + if (scene.random_events) { + scene.current_random_event = paramsMemory.getInt("CurrentRandomEvent"); + } +} + +void UIState::setPrimeType(PrimeType type) { + if (type != prime_type) { + bool prev_prime = hasPrime(); + + prime_type = type; + Params().put("PrimeType", std::to_string(prime_type)); + emit primeTypeChanged(prime_type); + + bool prime = hasPrime(); + if (prev_prime != prime) { + emit primeChanged(prime); + } + } +} + +Device::Device(QObject *parent) : brightness_filter(BACKLIGHT_OFFROAD, BACKLIGHT_TS, BACKLIGHT_DT), QObject(parent) { + setAwake(true); + resetInteractiveTimeout(); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &Device::update); +} + +void Device::update(const UIState &s) { + updateBrightness(s); + updateWakefulness(s); +} + +void Device::setAwake(bool on) { + if (on != awake) { + awake = on; + Hardware::set_display_power(awake); + LOGD("setting display power %d", awake); + emit displayPowerChanged(awake); + } +} + +void Device::resetInteractiveTimeout(int timeout) { + if (timeout == -1) { + timeout = 60; + } + interactive_timeout = timeout * UI_FREQ; +} + +void Device::updateBrightness(const UIState &s) { + float clipped_brightness = offroad_brightness; + if (s.scene.started) { + clipped_brightness = s.scene.light_sensor; + + // CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm + if (clipped_brightness <= 8) { + clipped_brightness = (clipped_brightness / 903.3); + } else { + clipped_brightness = std::pow((clipped_brightness + 16.0) / 116.0, 3.0); + } + + // Scale back to 10% to 100% + clipped_brightness = std::clamp(100.0f * clipped_brightness, 10.0f, 100.0f); + } + + int brightness = brightness_filter.update(clipped_brightness); + if (!awake) { + brightness = 0; + } else if (s.scene.screen_brightness <= 100) { + // Bring the screen brightness up to 5% upon screen tap + brightness = fmax(5, s.scene.screen_brightness); + } + + if (brightness != last_brightness) { + if (!brightness_future.isRunning()) { + brightness_future = QtConcurrent::run(Hardware::set_brightness, brightness); + last_brightness = brightness; + } + } +} + +void Device::updateWakefulness(const UIState &s) { + bool ignition_just_turned_off = !s.scene.ignition && ignition_on; + ignition_on = s.scene.ignition; + + if (ignition_just_turned_off) { + resetInteractiveTimeout(); + } else if (interactive_timeout > 0 && --interactive_timeout == 0) { + emit interactiveTimeout(); + } + + if (s.scene.screen_brightness != 0) { + setAwake(s.scene.ignition || interactive_timeout > 0); + } else { + setAwake(interactive_timeout > 0); + } +} + +UIState *uiState() { + static UIState ui_state; + return &ui_state; +} + +Device *device() { + static Device _device; + return &_device; +} diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h new file mode 100755 index 0000000..91346ea --- /dev/null +++ b/selfdrive/ui/ui.h @@ -0,0 +1,331 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "cereal/messaging/messaging.h" +#include "common/mat.h" +#include "common/params.h" +#include "common/timing.h" +#include "selfdrive/ui/qt/network/wifi_manager.h" +#include "system/hardware/hw.h" + +const int UI_BORDER_SIZE = 30; +const int UI_HEADER_HEIGHT = 420; + +const int UI_FREQ = 20; // Hz +const int BACKLIGHT_OFFROAD = 50; +typedef cereal::CarControl::HUDControl::AudibleAlert AudibleAlert; + +const float MIN_DRAW_DISTANCE = 10.0; +const float MAX_DRAW_DISTANCE = 100.0; +constexpr mat3 DEFAULT_CALIBRATION = {{ 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0 }}; +constexpr mat3 FCAM_INTRINSIC_MATRIX = (mat3){{2648.0, 0.0, 1928.0 / 2, + 0.0, 2648.0, 1208.0 / 2, + 0.0, 0.0, 1.0}}; +// tici ecam focal probably wrong? magnification is not consistent across frame +// Need to retrain model before this can be changed +constexpr mat3 ECAM_INTRINSIC_MATRIX = (mat3){{567.0, 0.0, 1928.0 / 2, + 0.0, 567.0, 1208.0 / 2, + 0.0, 0.0, 1.0}}; + + +constexpr vec3 default_face_kpts_3d[] = { + {-5.98, -51.20, 8.00}, {-17.64, -49.14, 8.00}, {-23.81, -46.40, 8.00}, {-29.98, -40.91, 8.00}, {-32.04, -37.49, 8.00}, + {-34.10, -32.00, 8.00}, {-36.16, -21.03, 8.00}, {-36.16, 6.40, 8.00}, {-35.47, 10.51, 8.00}, {-32.73, 19.43, 8.00}, + {-29.30, 26.29, 8.00}, {-24.50, 33.83, 8.00}, {-19.01, 41.37, 8.00}, {-14.21, 46.17, 8.00}, {-12.16, 47.54, 8.00}, + {-4.61, 49.60, 8.00}, {4.99, 49.60, 8.00}, {12.53, 47.54, 8.00}, {14.59, 46.17, 8.00}, {19.39, 41.37, 8.00}, + {24.87, 33.83, 8.00}, {29.67, 26.29, 8.00}, {33.10, 19.43, 8.00}, {35.84, 10.51, 8.00}, {36.53, 6.40, 8.00}, + {36.53, -21.03, 8.00}, {34.47, -32.00, 8.00}, {32.42, -37.49, 8.00}, {30.36, -40.91, 8.00}, {24.19, -46.40, 8.00}, + {18.02, -49.14, 8.00}, {6.36, -51.20, 8.00}, {-5.98, -51.20, 8.00}, +}; + +struct Alert { + QString text1; + QString text2; + QString type; + cereal::ControlsState::AlertSize size; + cereal::ControlsState::AlertStatus status; + AudibleAlert sound; + + bool equal(const Alert &a2) { + return text1 == a2.text1 && text2 == a2.text2 && type == a2.type && sound == a2.sound; + } + + static Alert get(const SubMaster &sm, uint64_t started_frame) { + const cereal::ControlsState::Reader &cs = sm["controlsState"].getControlsState(); + const uint64_t controls_frame = sm.rcv_frame("controlsState"); + + Alert alert = {}; + if (controls_frame >= started_frame) { // Don't get old alert. + alert = {cs.getAlertText1().cStr(), cs.getAlertText2().cStr(), + cs.getAlertType().cStr(), cs.getAlertSize(), + cs.getAlertStatus(), + cs.getAlertSound()}; + } + + if (!sm.updated("controlsState") && (sm.frame - started_frame) > 5 * UI_FREQ) { + const int CONTROLS_TIMEOUT = 5; + const int controls_missing = (nanos_since_boot() - sm.rcv_time("controlsState")) / 1e9; + + // Handle controls timeout + if (controls_frame < started_frame) { + // car is started, but controlsState hasn't been seen at all + alert = {"openpilot Unavailable", "Waiting for controls to start", + "controlsWaiting", cereal::ControlsState::AlertSize::MID, + cereal::ControlsState::AlertStatus::NORMAL, + AudibleAlert::NONE}; + } else if (controls_missing > CONTROLS_TIMEOUT && !Hardware::PC()) { + // car is started, but controls is lagging or died + if (cs.getEnabled() && (controls_missing - CONTROLS_TIMEOUT) < 10) { + alert = {"TAKE CONTROL IMMEDIATELY", "Controls Unresponsive", + "controlsUnresponsive", cereal::ControlsState::AlertSize::FULL, + cereal::ControlsState::AlertStatus::CRITICAL, + AudibleAlert::WARNING_IMMEDIATE}; + } else { + alert = {"Controls Unresponsive", "Reboot Device", + "controlsUnresponsivePermanent", cereal::ControlsState::AlertSize::MID, + cereal::ControlsState::AlertStatus::NORMAL, + AudibleAlert::NONE}; + } + } + } + return alert; + } +}; + +typedef enum UIStatus { + STATUS_DISENGAGED, + STATUS_OVERRIDE, + STATUS_ENGAGED, + + // FrogPilot statuses + STATUS_LATERAL_ACTIVE, +} UIStatus; + +enum PrimeType { + UNKNOWN = -1, + NONE = 0, + MAGENTA = 1, + LITE = 2, + BLUE = 3, + MAGENTA_NEW = 4, + PURPLE = 5, +}; + +const QColor bg_colors [] = { + [STATUS_DISENGAGED] = QColor(0x17, 0x33, 0x49, 0xc8), + [STATUS_OVERRIDE] = QColor(0x91, 0x9b, 0x95, 0xf1), + [STATUS_ENGAGED] = QColor(0x17, 0x86, 0x44, 0xf1), + + // FrogPilot colors + [STATUS_LATERAL_ACTIVE] = QColor(0x0a, 0xba, 0xb5, 0xf1), +}; + +static std::map alert_colors = { + {cereal::ControlsState::AlertStatus::NORMAL, QColor(0x15, 0x15, 0x15, 0xf1)}, + {cereal::ControlsState::AlertStatus::USER_PROMPT, QColor(0xDA, 0x6F, 0x25, 0xf1)}, + {cereal::ControlsState::AlertStatus::CRITICAL, QColor(0xC9, 0x22, 0x31, 0xf1)}, + {cereal::ControlsState::AlertStatus::FROGPILOT, QColor(0x17, 0x86, 0x44, 0xf1)}, +}; + +typedef struct UIScene { + bool calibration_valid = false; + bool calibration_wide_valid = false; + bool wide_cam = true; + mat3 view_from_calib = DEFAULT_CALIBRATION; + mat3 view_from_wide_calib = DEFAULT_CALIBRATION; + cereal::PandaState::PandaType pandaType; + + // modelV2 + float lane_line_probs[4]; + float road_edge_stds[2]; + QPolygonF track_vertices; + QPolygonF lane_line_vertices[4]; + QPolygonF road_edge_vertices[2]; + + // lead + QPointF lead_vertices[2]; + + // DMoji state + float driver_pose_vals[3]; + float driver_pose_diff[3]; + float driver_pose_sins[3]; + float driver_pose_coss[3]; + vec3 face_kpts_draw[std::size(default_face_kpts_3d)]; + + bool navigate_on_openpilot = false; + + float light_sensor; + bool started, ignition, is_metric, map_on_left, longitudinal_control; + bool world_objects_visible = false; + uint64_t started_frame; + + // FrogPilot variables + bool acceleration_path; + bool adjacent_path; + bool always_on_lateral; + bool always_on_lateral_active; + bool blind_spot_left; + bool blind_spot_path; + bool blind_spot_right; + bool compass; + bool conditional_experimental; + bool custom_onroad_ui; + bool custom_theme; + bool driver_camera; + bool enabled; + bool experimental_mode; + bool experimental_mode_via_screen; + bool full_map; + bool hide_speed; + bool lead_info; + bool map_open; + bool model_ui; + bool mute_dm; + bool personalities_via_screen; + bool quality_of_life_controls; + bool quality_of_life_visuals; + bool random_events; + bool reverse_cruise; + bool road_name_ui; + bool rotating_wheel; + bool show_driver_camera; + bool show_slc_offset; + bool show_fps; + bool speed_limit_controller; + bool speed_limit_overridden; + bool tethering_enabled; + bool turn_signal_left; + bool turn_signal_right; + bool unlimited_road_ui_length; + bool use_si; + bool use_vienna_slc_sign; + float adjusted_cruise; + float lane_line_width; + float lane_width_left; + float lane_width_right; + float path_edge_width; + float path_width; + float road_edge_width; + float speed_limit; + float speed_limit_offset; + float speed_limit_overridden_speed; + int bearing_deg; + int camera_view; + int conditional_speed; + int conditional_speed_lead; + int conditional_status; + int current_random_event; + int custom_colors; + int custom_icons; + int custom_signals; + int desired_follow; + int obstacle_distance; + int obstacle_distance_stock; + int screen_brightness; + int steering_angle_deg; + int stopped_equivalence; + int wheel_icon; + QPolygonF track_adjacent_vertices[6]; + QPolygonF track_edge_vertices; + +} UIScene; + +class UIState : public QObject { + Q_OBJECT + +public: + UIState(QObject* parent = 0); + void updateStatus(); + inline bool engaged() const { + return scene.started && (*sm)["controlsState"].getControlsState().getEnabled(); + } + + void setPrimeType(PrimeType type); + inline PrimeType primeType() const { return prime_type; } + inline bool hasPrime() const { return prime_type != PrimeType::UNKNOWN && prime_type != PrimeType::NONE; } + + int fb_w = 0, fb_h = 0; + + std::unique_ptr sm; + + UIStatus status; + UIScene scene = {}; + + QString language; + + QTransform car_space_transform; + +signals: + void uiUpdate(const UIState &s); + void offroadTransition(bool offroad); + void primeChanged(bool prime); + void primeTypeChanged(PrimeType prime_type); + +private slots: + void update(); + +private: + QTimer *timer; + bool started_prev = false; + PrimeType prime_type = PrimeType::UNKNOWN; + + WifiManager *wifi = nullptr; +}; + +UIState *uiState(); + +// device management class +class Device : public QObject { + Q_OBJECT + +public: + Device(QObject *parent = 0); + bool isAwake() { return awake; } + void setOffroadBrightness(int brightness) { + offroad_brightness = std::clamp(brightness, 0, 100); + } + +private: + bool awake = false; + int interactive_timeout = 0; + bool ignition_on = false; + + int offroad_brightness = BACKLIGHT_OFFROAD; + int last_brightness = 0; + FirstOrderFilter brightness_filter; + QFuture brightness_future; + + void updateBrightness(const UIState &s); + void updateWakefulness(const UIState &s); + void setAwake(bool on); + +signals: + void displayPowerChanged(bool on); + void interactiveTimeout(); + +public slots: + void resetInteractiveTimeout(int timeout = -1); + void update(const UIState &s); +}; + +Device *device(); + +void ui_update_params(UIState *s); +int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height); +void update_model(UIState *s, + const cereal::ModelDataV2::Reader &model, + const cereal::UiPlan::Reader &plan); +void update_dmonitoring(UIState *s, const cereal::DriverStateV2::Reader &driverstate, float dm_fade_state, bool is_rhd); +void update_leads(UIState *s, const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line); +void update_line_data(const UIState *s, const cereal::XYZTData::Reader &line, + float y_off, float z_off, QPolygonF *pvd, int max_idx, bool allow_invert); diff --git a/selfdrive/ui/watch3.cc b/selfdrive/ui/watch3.cc new file mode 100755 index 0000000..ec35c29 --- /dev/null +++ b/selfdrive/ui/watch3.cc @@ -0,0 +1,34 @@ +#include +#include + +#include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/widgets/cameraview.h" + +int main(int argc, char *argv[]) { + initApp(argc, argv); + + QApplication a(argc, argv); + QWidget w; + setMainWindow(&w); + + QVBoxLayout *layout = new QVBoxLayout(&w); + layout->setMargin(0); + layout->setSpacing(0); + + { + QHBoxLayout *hlayout = new QHBoxLayout(); + layout->addLayout(hlayout); + hlayout->addWidget(new CameraWidget("navd", VISION_STREAM_MAP, false)); + hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_ROAD, false)); + } + + { + QHBoxLayout *hlayout = new QHBoxLayout(); + layout->addLayout(hlayout); + hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_DRIVER, false)); + hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_WIDE_ROAD, false)); + } + + return a.exec(); +} diff --git a/selfdrive/ui_old.tgz b/selfdrive/ui_old.tgz new file mode 100644 index 0000000..815791c Binary files /dev/null and b/selfdrive/ui_old.tgz differ diff --git a/system/clearpilot/configure/dependencies.sh b/system/clearpilot/configure/dependencies.sh index 91993e7..96df049 100644 --- a/system/clearpilot/configure/dependencies.sh +++ b/system/clearpilot/configure/dependencies.sh @@ -18,4 +18,6 @@ fi apt-get update apt-get install -y python3-pyqt5 -pip3 install termqt \ No newline at end of file +pip3 install termqt +apt-get install python3-pyqt5.qtwebengine +pip3 install pywayland diff --git a/system/clearpilot/startup_logo/set_logo.sh b/system/clearpilot/startup_logo/set_logo.sh index e0cc12e..589d3c1 100644 --- a/system/clearpilot/startup_logo/set_logo.sh +++ b/system/clearpilot/startup_logo/set_logo.sh @@ -4,8 +4,8 @@ set -x # Test: sudo mount -o remount,rw /; cp /usr/comma/bg.org /usr/comma/bg.jpg; bash shell/set_logo.sh -# Check if md5sum of /usr/comma/bg.jpg is not equal to md5sum of /data/openpilot/shell/bg.jpg -if [ "$(md5sum /usr/comma/bg.jpg | awk '{print $1}')" != "$(md5sum /data/openpilot/shell/bg.jpg | awk '{print $1}')" ]; then +# Check if md5sum of /usr/comma/bg.jpg is not equal to md5sum of /data/openpilot/system/clearpilot/startup_logo/bg.jpg +if [ "$(md5sum /usr/comma/bg.jpg | awk '{print $1}')" != "$(md5sum /data/openpilot/system/clearpilot/startup_logo/bg.jpg | awk '{print $1}')" ]; then # If /usr/comma/bg.org does not exist if [ ! -f /usr/comma/bg.org ]; then @@ -25,22 +25,22 @@ if [ "$(md5sum /usr/comma/bg.jpg | awk '{print $1}')" != "$(md5sum /data/openpil # If /usr/comma/bg.org does exist if [ -f /usr/comma/bg.org ]; then - sudo cp -f /data/openpilot/shell/bg.jpg /usr/comma/bg.jpg + sudo cp -f /data/openpilot/system/clearpilot/startup_logo/bg.jpg /usr/comma/bg.jpg fi # If file /usr/comma/revert_logo.sh does not exist - if [ "$(md5sum /data/openpilot/shell/revert_logo.sh | awk '{print $1}')" != "$(md5sum /usr/comma/revert_logo.sh | awk '{print $1}')" ]; then - sudo cp /data/openpilot/shell/revert_logo.sh /usr/comma/revert_logo.sh + if [ "$(md5sum /data/openpilot/system/clearpilot/startup_logo/revert_logo.sh | awk '{print $1}')" != "$(md5sum /usr/comma/revert_logo.sh | awk '{print $1}')" ]; then + sudo cp /data/openpilot/system/clearpilot/startup_logo/revert_logo.sh /usr/comma/revert_logo.sh fi - if [ "$(md5sum md5sum /usr/comma/comma.sh | awk '{print $1}')" != "$(md5sum /data/openpilot/shell/usr_comma_comma.sh | awk '{print $1}')" ]; then + if [ "$(md5sum md5sum /usr/comma/comma.sh | awk '{print $1}')" != "$(md5sum /data/openpilot/system/clearpilot/startup_logo/usr_comma_comma.sh | awk '{print $1}')" ]; then if [[ "$(md5sum /usr/comma/comma.sh | awk '{print $1}')" == "ddbac0b46dd02efd672a0ef31ca426cf" ]]; then if [ ! -f /usr/comma/comma.org ]; then sudo cp /usr/comma/comma.sh /usr/comma/comma.org fi fi - if [-f /usr/comma/comma.org ]; then - sudo cp /data/openpilot/shell/usr_comma_comma.sh /usr/comma/comma.sh + if [ -f /usr/comma/comma.org ]; then + sudo cp /data/openpilot/system/clearpilot/startup_logo/usr_comma_comma.sh /usr/comma/comma.sh echo updated comma.sh fi fi diff --git a/prebuilt b/system/clearpilot/startup_logo/usr_comma_comma.sh similarity index 100% rename from prebuilt rename to system/clearpilot/startup_logo/usr_comma_comma.sh diff --git a/system/clearpilot/tools/shell_wayland.py b/system/clearpilot/tools/shell.py similarity index 100% rename from system/clearpilot/tools/shell_wayland.py rename to system/clearpilot/tools/shell.py diff --git a/system/clearpilot/tools/shell.sh b/system/clearpilot/tools/shell.sh new file mode 100644 index 0000000..bf50aa4 --- /dev/null +++ b/system/clearpilot/tools/shell.sh @@ -0,0 +1 @@ +sudo su comma -c "python3 shell.py \"echo hello; sleep 5\" diff --git a/system/clearpilot/tools/webview.py b/system/clearpilot/tools/webview.py index 2adc8e0..a3dff52 100644 --- a/system/clearpilot/tools/webview.py +++ b/system/clearpilot/tools/webview.py @@ -2,7 +2,7 @@ import os import sys import signal from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QGraphicsView, QGraphicsScene -from PyQt5.QtCore import Qt, QUrl, QPointF, QTimer +from PyQt5.QtCore import Qt, QUrl, QPointF, QTimer, QObject, QEvent from PyQt5.QtGui import QCursor, QPixmap, QTransform from PyQt5.QtWebEngineWidgets import QWebEngineView @@ -10,23 +10,125 @@ webview = None def on_load_finished(arg): global webview - if arg: # Check if the page load was successful - webview.setZoomFactor(2) + nothing=0 +# if arg: # Check if the page load was successful +# webview.setZoomFactor(2) -class RotatedWindow(QWidget): +class MouseEventFilter(QObject): + def __init__(self, target, width, height): + super().__init__() + + def eventFilter(self, obj, event): + if event.type() in [QEvent.MouseButtonPress, QEvent.MouseButtonRelease, QEvent.MouseMove]: + print("Mouse event blocked:", event.type()) + return True # Block event propagation + return False # Other events are not blocked + +class MouseEventFilter2(QObject): + def __init__(self, target, width, height): + super().__init__() + self.target = target + self.width = width + self.height = height + + def eventFilter(self, obj, event): + if event.type() in [QEvent.MouseButtonPress, QEvent.MouseButtonRelease, QEvent.MouseMove]: + # Calculate new coordinates (90 degrees rotation) + new_x = event.y() + new_y = self.width - event.x() - 1 + print (new_x, new_y) + + # Create a new event with the transformed coordinates + new_event = QEvent(event.type()) + new_event.setButtons(event.buttons()) + new_event.setButton(event.button()) + new_event.setGlobalPos(event.globalPos()) + new_event.setLocalPos(QPointF(new_x, new_y)) + new_event.setWindowPos(QPointF(new_x, new_y)) + new_event.setScreenPos(QPointF(new_x, new_y)) + + # Send event to the target +# QApplication.sendEvent(self.target, new_event) + print (event) + return True +# print (event) + return False + +class MyWebView(QWebEngineView): def __init__(self, parent=None): super().__init__(parent) self.rotation_angle = 90 - self.cursor_rotation_timer = QTimer(self) - self.cursor_rotation_timer.timeout.connect(self.rotateCursor) - self.cursor_rotation_timer.start(1000) # Rotate the cursor every 1 second + self.is_custom_event = False # Flag to indicate a custom event - def rotateCursor(self): - current_cursor = self.cursor() - pixmap = current_cursor.pixmap() - rotated_pixmap = pixmap.transformed(QTransform().rotate(self.rotation_angle)) - rotated_cursor = QCursor(rotated_pixmap) - self.setCursor(rotated_cursor) + def rotateMouseEvent(self, event): + original_pos = event.localPos() + rotated_pos = QPointF(original_pos.y(), self.width() - original_pos.x()) + # Create a new QMouseEvent with the rotated coordinates + rotated_event = QMouseEvent(event.type(), rotated_pos, event.button(), event.buttons(), event.modifiers()) + + # Mark as custom to prevent recursion + self.is_custom_event = True + QApplication.sendEvent(self, rotated_event) + self.is_custom_event = False + + def mousePressEvent(self, event): + print(event) + if not self.is_custom_event: # Check if it's a native event + self.rotateMouseEvent(event) + + def mouseReleaseEvent(self, event): + if not self.is_custom_event: # Check if it's a native event + self.rotateMouseEvent(event) + +class BetterWebView2(QWebEngineView): + def __init__(self): + super().__init__() + self.load(QUrl('http://www.example.com')) + QApplication.processEvents() # Process pending events to ensure children are created + self.eventsReceiverWidget = None + for obj in self.children(): + widget = self.findChild(QWidget, obj.objectName()) + if widget: + self.eventsReceiverWidget = widget + break + if self.eventsReceiverWidget: + self.eventsReceiverWidget.installEventFilter(self) + + def eventFilter(self, source, event): + if source == self.eventsReceiverWidget and event.type() == QEvent.MouseButtonPress: + print("Mouse Button Pressed!") + return super().eventFilter(source, event) + +class TransparentOverlay(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TransparentForMouseEvents) + self.setStyleSheet("background:transparent;") +# self.setStyleSheet("background:black;") + + def mousePressEvent(self, event): + print("Mouse event captured and discarded") + +class BetterWebView(QWebEngineView): + def __init__(self): + super().__init__() + self.load(QUrl('http://www.example.com')) + QApplication.processEvents() # Process pending events to ensure children are created + # self.install_filters(self) # Install filters starting from the web view itself +# self.overlay = TransparentOverlay(self) +# self.overlay.resize(self.size()) + + def install_filters(self, widget): + if isinstance(widget, QWidget): # Check if it's a QWidget to be able to receive events + widget.installEventFilter(self) + for child in widget.children(): # Recursively apply to all children + self.install_filters(child) + + def eventFilter(self, source, event): + if isinstance(source, QWidget) and event.type() == QEvent.MouseButtonPress: + print(f"Mouse Button Pressed in widget: {source}") + return super().eventFilter(source, event) def create_webview_app(): global webview @@ -40,31 +142,41 @@ def create_webview_app(): # Application setup app = QApplication(sys.argv) desktop = QApplication.desktop() - screen_geometry = desktop.availableGeometry(desktop.primaryScreen()) - window = RotatedWindow() + sg = desktop.availableGeometry(desktop.primaryScreen()) + + window = QWidget() window.setWindowTitle("Qt WebView Example") window.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - window.setGeometry(0, 0, screen_geometry.height(), screen_geometry.width()) - window.setStyleSheet("background-color: white;") + window.setGeometry(0, 0, sg.height(), sg.width()) + window.setStyleSheet("background-color: black;") window.showFullScreen() scene = QGraphicsScene() view = QGraphicsView(scene, window) - view.setGeometry(0, 0, window.width(), window.height()) + view.setGeometry(0, 0, sg.width(), sg.height()) view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Create WebView - webview = QWebEngineView() +# webview = MyWebView() + webview = BetterWebView() webview.load(QUrl("https://cdpn.io/yananas/fullpage/rwvZvY")) - webview.setGeometry(0, 0, window.width(), window.height()) + webview.setGeometry(0, 0, sg.width(), sg.height()) webview.loadFinished.connect(on_load_finished) + white_box = QWidget() + white_box.setGeometry(0, 0, sg.width(), sg.height()) + white_box.setStyleSheet("background-color: blue;") + filter = MouseEventFilter(white_box, sg.height(), sg.width()) + white_box.installEventFilter(filter) + white_box.setAttribute(Qt.WA_TransparentForMouseEvents, False) + # Add WebView to the scene proxy_webview = scene.addWidget(webview) + proxy_webview = scene.addWidget(white_box) view.setScene(scene) - view.rotate(window.rotation_angle) # Rotate the view by 90 degrees + view.rotate(90) # Rotate the view by 90 degrees # Layout setup window_layout = QHBoxLayout(window) @@ -81,4 +193,4 @@ def main(): sys.exit(exit_code) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/system/clearpilot/tools/webview.sh b/system/clearpilot/tools/webview.sh new file mode 100644 index 0000000..f6ed25b --- /dev/null +++ b/system/clearpilot/tools/webview.sh @@ -0,0 +1 @@ +sudo su comma -c "nice python3 webview.py" \ No newline at end of file
%1%2%4%5%6%7