clearpilot: initial commit of full source
This commit is contained in:
Executable
+17
@@ -0,0 +1,17 @@
|
||||
moc_*
|
||||
*.moc
|
||||
|
||||
translations/main_test_en.*
|
||||
|
||||
_text
|
||||
_spinner
|
||||
|
||||
ui
|
||||
mui
|
||||
watch3
|
||||
installer/installers/*
|
||||
qt/setup/setup
|
||||
qt/setup/reset
|
||||
qt/setup/wifi
|
||||
qt/setup/updater
|
||||
translations/alerts_generated.h
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
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 arch == "Darwin":
|
||||
del base_libs[base_libs.index('OpenCL')]
|
||||
qt_env['FRAMEWORKS'] += ['OpenCL']
|
||||
|
||||
# FIXME: remove this once we're on 5.15 (24.04)
|
||||
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
|
||||
|
||||
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/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/qt/widgets/frogpilot_controls.cc",
|
||||
"../frogpilot/ui/qt/offroad/control_settings.cc", "../frogpilot/ui/qt/offroad/vehicle_settings.cc",
|
||||
"../frogpilot/ui/qt/offroad/visual_settings.cc"]
|
||||
|
||||
qt_env['CPPDEFINES'] = []
|
||||
|
||||
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
|
||||
Export('widgets')
|
||||
qt_libs = [widgets, qt_util] + base_libs
|
||||
|
||||
# CLEARPILOT added qt/ready.cc
|
||||
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",
|
||||
"qt/ready.cc"]
|
||||
|
||||
# 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('<!DOCTYPE RCC><RCC version="1.0">\n<qresource>\n')
|
||||
f.write('\n'.join([f'<file alias="{l}">../ui/translations/{l}.qm</file>' for l in languages.values()]))
|
||||
f.write('\n</qresource>\n</RCC>')
|
||||
|
||||
# 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("_text", ["qt/text.cc"], LIBS=qt_libs)
|
||||
qt_env.Program("_spinner", ["qt/spinner.cc"], LIBS=qt_libs)
|
||||
|
||||
|
||||
# Clearpilot
|
||||
# Add qtwebengine to build paths
|
||||
qt_env['CXXFLAGS'] += ["-I/usr/include/aarch64-linux-gnu/qt5/QtWebEngine"]
|
||||
qt_env['CXXFLAGS'] += ["-I/usr/include/aarch64-linux-gnu/qt5/QtWebEngineCore"]
|
||||
qt_env['CXXFLAGS'] += ["-I/usr/include/aarch64-linux-gnu/qt5/QtWebEngineWidgets"]
|
||||
qt_env['CXXFLAGS'] += ["-I/usr/include/aarch64-linux-gnu/qt5/QtWebChannel"]
|
||||
qt_webengine_libs = qt_libs + ['Qt5WebEngineWidgets']
|
||||
|
||||
# Create clearpilot tools
|
||||
qt_env.Program("/data/openpilot/system/clearpilot/tools/qt_shell", ["/data/openpilot/system/clearpilot/tools/qt_shell.cc"], LIBS=qt_libs)
|
||||
# qt_env.Program("/data/openpilot/system/clearpilot/tools/qt_webview", ["/data/openpilot/system/clearpilot/tools/qt_webview.cc"], LIBS=qt_webengine_libs)
|
||||
|
||||
# build main UI
|
||||
qt_env.Program("ui", qt_src + [asset_obj], LIBS=qt_webengine_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 mui
|
||||
qt_env.Program("mui", ["mui.cc"], LIBS=qt_libs)
|
||||
|
||||
# build installers
|
||||
senv = qt_env.Clone()
|
||||
senv['LINKFLAGS'].append('-Wl,-strip-debug')
|
||||
|
||||
release = "release3"
|
||||
installers = [
|
||||
("openpilot", release),
|
||||
("openpilot_test", f"{release}-staging"),
|
||||
("openpilot_nightly", "nightly"),
|
||||
("openpilot_internal", "master"),
|
||||
]
|
||||
|
||||
cont = senv.Command(f"installer/continue_openpilot.o", f"installer/continue_openpilot.sh",
|
||||
"ld -r -b binary -o $TARGET $SOURCE")
|
||||
for name, branch in installers:
|
||||
d = {'BRANCH': f"'\"{branch}\"'"}
|
||||
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], 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'])
|
||||
Executable
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
cd /data/openpilot
|
||||
exec ./launch_openpilot.sh
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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_openpilot_sh_start");
|
||||
extern const uint8_t str_continue_end[] asm("_binary_selfdrive_ui_installer_continue_openpilot_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<int, QProcess::ExitStatus>::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<QPair<QString, int>> 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<std::string, std::string> 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();
|
||||
}
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QProcess>
|
||||
#include <QProgressBar>
|
||||
#include <QWidget>
|
||||
|
||||
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);
|
||||
};
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#include <sys/resource.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QTranslator>
|
||||
|
||||
#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();
|
||||
}
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#include <QApplication>
|
||||
#include <QtWidgets>
|
||||
#include <QTimer>
|
||||
|
||||
#include "cereal/messaging/messaging.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/qt_window.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QApplication a(argc, argv);
|
||||
QWidget w;
|
||||
setMainWindow(&w);
|
||||
|
||||
w.setStyleSheet("background-color: black;");
|
||||
|
||||
// our beautiful UI
|
||||
QVBoxLayout *layout = new QVBoxLayout(&w);
|
||||
QLabel *label = new QLabel("〇");
|
||||
layout->addWidget(label, 0, Qt::AlignCenter);
|
||||
|
||||
QTimer timer;
|
||||
QObject::connect(&timer, &QTimer::timeout, [=]() {
|
||||
static SubMaster sm({"deviceState", "controlsState"});
|
||||
|
||||
bool onroad_prev = sm.allAliveAndValid({"deviceState"}) &&
|
||||
sm["deviceState"].getDeviceState().getStarted();
|
||||
sm.update(0);
|
||||
|
||||
bool onroad = sm.allAliveAndValid({"deviceState"}) &&
|
||||
sm["deviceState"].getDeviceState().getStarted();
|
||||
|
||||
if (onroad) {
|
||||
label->setText("〇");
|
||||
auto cs = sm["controlsState"].getControlsState();
|
||||
UIStatus status = cs.getEnabled() ? STATUS_ENGAGED : STATUS_DISENGAGED;
|
||||
label->setStyleSheet(QString("color: %1; font-size: 250px;").arg(bg_colors[status].name()));
|
||||
} else {
|
||||
label->setText("offroad");
|
||||
label->setStyleSheet("color: grey; font-size: 40px;");
|
||||
}
|
||||
|
||||
if ((onroad != onroad_prev) || sm.frame < 2) {
|
||||
Hardware::set_brightness(50);
|
||||
Hardware::set_display_power(onroad);
|
||||
}
|
||||
});
|
||||
timer.start(50);
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#include "selfdrive/ui/qt/api.h"
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include <string>
|
||||
|
||||
#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;
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#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();
|
||||
};
|
||||
Executable
+161
@@ -0,0 +1,161 @@
|
||||
#include "selfdrive/ui/qt/body.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStackedLayout>
|
||||
|
||||
#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();
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMovie>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
||||
#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);
|
||||
};
|
||||
Executable
+250
@@ -0,0 +1,250 @@
|
||||
#include "selfdrive/ui/qt/home.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QMouseEvent>
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/widgets/drive_stats.h"
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
#include <QWebEngineView>
|
||||
|
||||
// HomeWindow: the container for the offroad and onroad UIs
|
||||
|
||||
HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
|
||||
// CLEARPILOT Sidebar set to invisible in drive view.
|
||||
params.putBool("Sidebar", false);
|
||||
|
||||
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);
|
||||
QObject::connect(sidebar, &Sidebar::openOnroad, this, &HomeWindow::showOnroad);
|
||||
|
||||
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);
|
||||
slayout->addWidget(onroad);
|
||||
|
||||
// CLEARPILOT
|
||||
ready = new ReadyWindow(this);
|
||||
slayout->addWidget(ready);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Debug function to activate onroad UI
|
||||
void HomeWindow::showOnroad() {
|
||||
sidebar->setVisible(false);
|
||||
slayout->setCurrentWidget(onroad);
|
||||
// sidebar->setVisible(params.getBool("Sidebar"));
|
||||
}
|
||||
|
||||
void HomeWindow::showSidebar(bool show) {
|
||||
sidebar->setVisible(show);
|
||||
}
|
||||
|
||||
void HomeWindow::updateState(const UIState &s) {
|
||||
// const SubMaster &sm = *(s.sm);
|
||||
if (s.scene.started) {
|
||||
showDriverView(s.scene.driver_camera_timer >= 10, true);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::offroadTransition(bool offroad) {
|
||||
if (offroad) {
|
||||
sidebar->setVisible(false);
|
||||
slayout->setCurrentWidget(ready);
|
||||
} else {
|
||||
sidebar->setVisible(false);
|
||||
slayout->setCurrentWidget(onroad);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::showDriverView(bool show, bool started) {
|
||||
if (show) {
|
||||
emit closeSettings();
|
||||
slayout->setCurrentWidget(driver_view);
|
||||
sidebar->setVisible(show == false);
|
||||
} else {
|
||||
if (started) {
|
||||
slayout->setCurrentWidget(onroad);
|
||||
sidebar->setVisible(params.getBool("Sidebar"));
|
||||
} else {
|
||||
slayout->setCurrentWidget(home);
|
||||
sidebar->setVisible(show == false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::mousePressEvent(QMouseEvent* e) {
|
||||
// CLEARPILOT todo - tap on main goes straight to settings
|
||||
// Unless we click a debug widget.
|
||||
|
||||
// CLEARPILOT - click ready shows home
|
||||
if (!onroad->isVisible() && ready->isVisible()) {
|
||||
sidebar->setVisible(true);
|
||||
slayout->setCurrentWidget(home);
|
||||
}
|
||||
|
||||
// Todo: widgets
|
||||
if (onroad->isVisible()) {
|
||||
emit openSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
|
||||
HomeWindow::mousePressEvent(e);
|
||||
// const SubMaster &sm = *(uiState()->sm);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// // // Create a QWebEngineView
|
||||
// QWebEngineView *web_view = new QWebEngineView();
|
||||
// web_view->load(QUrl("http://fark.com"));
|
||||
|
||||
// // Add the QWebEngineView to the layout
|
||||
// home_layout->addWidget(web_view);
|
||||
}
|
||||
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;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
/* Refresh data on screen every 5 seconds. */
|
||||
void OffroadHome::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
timer->start(5 * 1000);
|
||||
}
|
||||
|
||||
void OffroadHome::hideEvent(QHideEvent *event) {
|
||||
timer->stop();
|
||||
}
|
||||
|
||||
void OffroadHome::refresh() {
|
||||
QString model = QString::fromStdString(params.get("ModelName"));
|
||||
|
||||
date->setText(QLocale(uiState()->language.mid(5)).toString(QDateTime::currentDateTime(), "dddd, MMMM d"));
|
||||
version->setText(getBrand() + " v" + getVersion().left(14).trimmed() + " - " + model);
|
||||
|
||||
// bool updateAvailable = update_widget->refresh();
|
||||
|
||||
int alerts = alerts_widget->refresh();
|
||||
|
||||
if (alerts > 0 && !alerts_widget->isVisible()) {
|
||||
alerts_widget->setVisible(true);
|
||||
} else if (alerts == 0 && alerts_widget->isVisible()) {
|
||||
alerts_widget->setVisible(false);
|
||||
}
|
||||
|
||||
// pop-up new notification
|
||||
// CLEARPILOT temp disabled update notifications
|
||||
// int idx = center_layout->currentIndex();
|
||||
// if (!updateAvailable && !alerts && false) {
|
||||
// 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);
|
||||
|
||||
// CLEARPILOT temp disabled update notifications
|
||||
// update_notif->setVisible(updateAvailable);
|
||||
// alert_notif->setVisible(alerts);
|
||||
alert_notif->setVisible(false);
|
||||
if (alerts) {
|
||||
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT")));
|
||||
}
|
||||
}
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedLayout>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/offroad/driverview.h"
|
||||
#include "selfdrive/ui/qt/ready.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* version;
|
||||
QStackedLayout* center_layout;
|
||||
UpdateAlert *update_widget;
|
||||
OffroadAlert* alerts_widget;
|
||||
QPushButton* alert_notif;
|
||||
QPushButton* update_notif;
|
||||
|
||||
// FrogPilot variables
|
||||
ElidedLabel* date;
|
||||
};
|
||||
|
||||
class HomeWindow : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HomeWindow(QWidget* parent = 0);
|
||||
QWidget* _parent = 0;
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
void closeSettings();
|
||||
|
||||
public slots:
|
||||
void offroadTransition(bool offroad);
|
||||
void showDriverView(bool show, bool started=false);
|
||||
void showOnroad();
|
||||
void showSidebar(bool show);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent* e) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent* e) override;
|
||||
|
||||
private:
|
||||
Sidebar *sidebar;
|
||||
OffroadHome *home;
|
||||
OnroadWindow *onroad;
|
||||
DriverViewWindow *driver_view;
|
||||
QStackedLayout *slayout;
|
||||
|
||||
// FrogPilot variables
|
||||
Params params;
|
||||
|
||||
// CLEARPILOT
|
||||
// bool show_ready;
|
||||
ReadyWindow *ready;
|
||||
|
||||
private slots:
|
||||
void updateState(const UIState &s);
|
||||
};
|
||||
Executable
+282
@@ -0,0 +1,282 @@
|
||||
#include "selfdrive/ui/qt/home.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QMouseEvent>
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/widgets/drive_stats.h"
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
#ifdef ENABLE_MAPS
|
||||
#include "selfdrive/ui/qt/maps/map_settings.h"
|
||||
#endif
|
||||
|
||||
#include <pybind11/embed.h> // pybind11 header to control Python embedding
|
||||
namespace py = pybind11;
|
||||
|
||||
// HomeWindow: the container for the offroad and onroad UIs
|
||||
|
||||
HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
|
||||
// CLEARPILOT Sidebar set to invisible in drive view.
|
||||
params.putBool("Sidebar", false);
|
||||
|
||||
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);
|
||||
|
||||
// CLEARPILOT
|
||||
ready = new ReadyWindow(this);
|
||||
slayout->addWidget(ready);
|
||||
|
||||
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);
|
||||
|
||||
// CLEARPILOT
|
||||
// switch to the generic robot UI
|
||||
// if (onroad->isVisible() && !body->isEnabled() && sm["carParams"].getCarParams().getNotCar()) {
|
||||
// body->setEnabled(true);
|
||||
// slayout->setCurrentWidget(body);
|
||||
// }
|
||||
|
||||
if (s.scene.started) {
|
||||
showDriverView(s.scene.driver_camera_timer >= 10, true);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::offroadTransition(bool offroad) {
|
||||
body->setEnabled(false);
|
||||
if (offroad) {
|
||||
sidebar->setVisible(false);
|
||||
slayout->setCurrentWidget(ready);
|
||||
// this->showDriverView(true, true); // Temp
|
||||
} else {
|
||||
sidebar->setVisible(false);
|
||||
slayout->setCurrentWidget(onroad);
|
||||
uiState()->scene.map_open = onroad->isMapVisible();
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::showDriverView(bool show, bool started) {
|
||||
if (show) {
|
||||
emit closeSettings();
|
||||
slayout->setCurrentWidget(driver_view);
|
||||
sidebar->setVisible(show == false);
|
||||
} else {
|
||||
if (started) {
|
||||
slayout->setCurrentWidget(onroad);
|
||||
sidebar->setVisible(params.getBool("Sidebar"));
|
||||
} else {
|
||||
slayout->setCurrentWidget(home);
|
||||
sidebar->setVisible(show == false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::mousePressEvent(QMouseEvent* e) {
|
||||
// CLEARPILOT todo - tap on main goes straight to settings
|
||||
// Unless we click a debug widget.
|
||||
|
||||
// Handle sidebar collapsing
|
||||
// if ((onroad->isVisible() || body->isVisible()) && (!sidebar->isVisible() || e->x() > sidebar->width())) {
|
||||
// sidebar->setVisible(!sidebar->isVisible() && !onroad->isMapVisible());
|
||||
// uiState()->scene.map_open = onroad->isMapVisible();
|
||||
// params.putBool("Sidebar", sidebar->isVisible());
|
||||
// }
|
||||
|
||||
// CLEARPILOT - click ready shows home
|
||||
if (!onroad->isVisible() && ready->isVisible()) {
|
||||
sidebar->setVisible(true);
|
||||
slayout->setCurrentWidget(home);
|
||||
}
|
||||
|
||||
// Todo: widgets
|
||||
if (onroad->isVisible()) {
|
||||
emit openSettings();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Clearpilot: removed home ui widgets
|
||||
py::scoped_interpreter guard{}; // Start the Python interpreter
|
||||
auto python_module = py::module_::import("/data/openpilot/webviewtest.py");
|
||||
auto WebViewWidget = python_module.attr("WebViewWidget");
|
||||
|
||||
auto python_widget_instance = WebViewWidget();
|
||||
auto* python_widget_embedded = python_widget_instance.cast<QWidget*>();
|
||||
home_layout->addWidget(python_widget_embedded);
|
||||
}
|
||||
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;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
/* Refresh data on screen every 5 seconds. */
|
||||
void OffroadHome::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
timer->start(5 * 1000);
|
||||
}
|
||||
|
||||
void OffroadHome::hideEvent(QHideEvent *event) {
|
||||
timer->stop();
|
||||
}
|
||||
|
||||
void OffroadHome::refresh() {
|
||||
QString model = QString::fromStdString(params.get("ModelName"));
|
||||
|
||||
date->setText(QLocale(uiState()->language.mid(5)).toString(QDateTime::currentDateTime(), "dddd, MMMM d"));
|
||||
version->setText(getBrand() + " v" + getVersion().left(14).trimmed() + " - " + model);
|
||||
|
||||
bool updateAvailable = update_widget->refresh();
|
||||
int alerts = alerts_widget->refresh();
|
||||
if (alerts > 0 && !alerts_widget->isVisible()) {
|
||||
alerts_widget->setVisible(true);
|
||||
} else if (alerts == 0 && alerts_widget->isVisible()) {
|
||||
alerts_widget->setVisible(false);
|
||||
}
|
||||
|
||||
// pop-up new notification
|
||||
// CLEARPILOT temp disabled update notifications
|
||||
int idx = center_layout->currentIndex();
|
||||
if (!updateAvailable && !alerts && false) {
|
||||
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);
|
||||
|
||||
// CLEARPILOT temp disabled update notifications
|
||||
// update_notif->setVisible(updateAvailable);
|
||||
// alert_notif->setVisible(alerts);
|
||||
update_notif->setVisible(false);
|
||||
alert_notif->setVisible(false);
|
||||
if (alerts) {
|
||||
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT")));
|
||||
}
|
||||
}
|
||||
Executable
+374
@@ -0,0 +1,374 @@
|
||||
#include "selfdrive/ui/qt/network/networking.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QScrollBar>
|
||||
#include <QStyle>
|
||||
|
||||
#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, false);
|
||||
} 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, false, 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, false, 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->refresh();
|
||||
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, true, pass);
|
||||
} else {
|
||||
wifi->connect(hidden_network, true);
|
||||
}
|
||||
emit requestWifiScreen();
|
||||
}
|
||||
});
|
||||
list->addItem(hiddenNetworkButton);
|
||||
|
||||
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<Network> 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);
|
||||
}
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#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<QPixmap> strengths;
|
||||
ListWidget *wifi_list_widget = nullptr;
|
||||
std::vector<WifiItem*> 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);
|
||||
};
|
||||
Executable
+47
@@ -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;
|
||||
Executable
+491
@@ -0,0 +1,491 @@
|
||||
#include "selfdrive/ui/qt/network/wifi_manager.h"
|
||||
|
||||
#include "selfdrive/ui/ui.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 <typename T = QDBusMessage, typename... Args>
|
||||
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<T, QDBusMessage>) {
|
||||
return response;
|
||||
} else if (response.arguments().count() >= 1) {
|
||||
QVariant vFirst = response.arguments().at(0).value<QDBusVariant>().variant();
|
||||
if (vFirst.canConvert<T>()) {
|
||||
return vFirst.value<T>();
|
||||
}
|
||||
QDebug critical = qCritical();
|
||||
critical << "Variant unpacking failure :" << method << ',';
|
||||
(critical << ... << args);
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
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<Connection>();
|
||||
qDBusRegisterMetaType<IpConfig>();
|
||||
|
||||
// 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<uint>(adapter, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_DEVICE, "State");
|
||||
activeAp = call<QDBusObjectPath>(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<QList<QDBusObjectPath>> wather_reply = *watcher;
|
||||
for (const QDBusObjectPath &path : wather_reply.value()) {
|
||||
QDBusReply<QVariantMap> 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<QString>(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Type");
|
||||
if (type == "802-11-wireless") {
|
||||
auto ip4config = call<QDBusObjectPath>(p.path(), NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACTIVE_CONNECTION, "Ip4Config");
|
||||
const auto &arr = call<QDBusArgument>(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<QString>();
|
||||
}
|
||||
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 bool is_hidden, 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"]["hidden"] = is_hidden;
|
||||
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<QDBusObjectPath>(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<QDBusObjectPath> WifiManager::getActiveConnections() {
|
||||
QVector<QDBusObjectPath> conns;
|
||||
QDBusObjectPath path;
|
||||
const QDBusArgument &arr = call<QDBusArgument>(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<uint>(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<QByteArray>(network_path, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE_ACCESS_POINT, property);
|
||||
}
|
||||
|
||||
QString WifiManager::getAdapter(const uint adapter_type) {
|
||||
QDBusReply<QList<QDBusObjectPath>> 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<QDBusObjectPath>().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<Connection>(call(path.path(), NM_DBUS_INTERFACE_SETTINGS_CONNECTION, "GetSettings")).value();
|
||||
}
|
||||
|
||||
void WifiManager::initConnections() {
|
||||
const QDBusReply<QList<QDBusObjectPath>> 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<QDBusPendingCall> 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<QDBusObjectPath>(NM_DBUS_PATH, NM_DBUS_INTERFACE_PROPERTIES, "Get", NM_DBUS_INTERFACE, "PrimaryConnection");
|
||||
auto primary_type = call<QString>(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<QString>(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) {
|
||||
// Todo: this is false if not prime. Override?
|
||||
if (false) {
|
||||
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<QMap<QString, QVariantMap>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <QtDBus>
|
||||
#include <QTimer>
|
||||
|
||||
#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<QString, QVariantMap> Connection;
|
||||
typedef QVector<QVariantMap> 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<QString, Network> seenNetworks;
|
||||
QMap<QDBusObjectPath, QString> 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<QDBusPendingCall> activateWifiConnection(const QString &ssid);
|
||||
NetworkType currentNetworkType();
|
||||
void updateGsmSettings(bool roaming, QString apn, bool metered);
|
||||
void connect(const Network &ssid, const bool is_hidden = false, 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<QDBusObjectPath> 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);
|
||||
};
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#include "selfdrive/ui/qt/offroad/driverview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <QPainter>
|
||||
|
||||
#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);
|
||||
}
|
||||
Executable
+21
@@ -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;
|
||||
};
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QStyle>
|
||||
|
||||
#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"));
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
||||
#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;
|
||||
};
|
||||
Executable
+234
@@ -0,0 +1,234 @@
|
||||
#include "selfdrive/ui/qt/offroad/onboarding.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickWidget>
|
||||
#include <QTransform>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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) {
|
||||
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;
|
||||
}
|
||||
)");
|
||||
updateActiveScreen();
|
||||
}
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QImage>
|
||||
#include <QMouseEvent>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QWidget>
|
||||
|
||||
#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<QRect> 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();
|
||||
};
|
||||
Executable
+772
@@ -0,0 +1,772 @@
|
||||
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QScrollBar>
|
||||
|
||||
#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/ui/qt/offroad/control_settings.h"
|
||||
#include "selfdrive/frogpilot/ui/qt/offroad/vehicle_settings.h"
|
||||
#include "selfdrive/frogpilot/ui/qt/offroad/visual_settings.h"
|
||||
|
||||
TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
// param, title, desc, icon
|
||||
std::vector<std::tuple<QString, QString, QString, QString>> 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("<b>%1</b><br><br>%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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
std::vector<QString> 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. On supported cars, you can cycle through these personalities with "
|
||||
"your steering wheel distance button."),
|
||||
"../assets/offroad/icon_speed_limit.png",
|
||||
longi_button_texts);
|
||||
|
||||
// set up uiState update for personality setting
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState);
|
||||
|
||||
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") {
|
||||
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::updateState(const UIState &s) {
|
||||
const SubMaster &sm = *(s.sm);
|
||||
|
||||
if (sm.updated("controlsState")) {
|
||||
auto personality = sm["controlsState"].getControlsState().getPersonality();
|
||||
if (personality != s.scene.personality && s.scene.started && isVisible()) {
|
||||
long_personality_setting->setCheckedButton(static_cast<int>(personality));
|
||||
}
|
||||
uiState()->scene.personality = personality;
|
||||
}
|
||||
}
|
||||
|
||||
void TogglesPanel::expandToggleDescription(const QString ¶m) {
|
||||
toggles[param.toStdString()]->showDescription();
|
||||
}
|
||||
|
||||
void TogglesPanel::showEvent(QShowEvent *event) {
|
||||
updateToggles();
|
||||
}
|
||||
|
||||
void TogglesPanel::updateToggles() {
|
||||
auto disengage_on_accelerator_toggle = toggles["DisengageOnAccelerator"];
|
||||
disengage_on_accelerator_toggle->setVisible(!params.getBool("AlwaysOnLateral"));
|
||||
auto driver_camera_toggle = toggles["RecordFront"];
|
||||
driver_camera_toggle->setVisible(!(params.getBool("DeviceManagement") && params.getBool("NoLogging") && params.getBool("NoUploads")));
|
||||
|
||||
auto experimental_mode_toggle = toggles["ExperimentalMode"];
|
||||
auto op_long_toggle = toggles["ExperimentalLongitudinalEnabled"];
|
||||
const QString e2e_description = QString("%1<br>"
|
||||
"<h4>%2</h4><br>"
|
||||
"%3<br>"
|
||||
"<h4>%4</h4><br>"
|
||||
"%5<br>"
|
||||
"<h4>%6</h4><br>"
|
||||
"%7")
|
||||
.arg(tr("openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> 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("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<cereal::CarParams>();
|
||||
|
||||
if (!CP.getExperimentalLongitudinalAvailable() || is_release) {
|
||||
params.remove("ExperimentalLongitudinalEnabled");
|
||||
}
|
||||
op_long_toggle->setVisible(CP.getExperimentalLongitudinalAvailable() && !is_release);
|
||||
if (hasLongitudinalControl(CP)) {
|
||||
// normal description and toggle
|
||||
bool conditional_experimental = params.getBool("ConditionalExperimental");
|
||||
if (conditional_experimental) {
|
||||
params.putBool("ExperimentalMode", true);
|
||||
experimental_mode_toggle->refresh();
|
||||
}
|
||||
experimental_mode_toggle->setEnabled(!conditional_experimental);
|
||||
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("<b>" + long_desc + "</b><br><br>" + 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);
|
||||
|
||||
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<QString, QString> 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 deleteDrivingDataBtn = 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(deleteDrivingDataBtn, &ButtonControl::clicked, [=]() {
|
||||
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([&] {
|
||||
deleteDrivingDataBtn->setValue(tr("Deleting footage..."));
|
||||
std::system("rm -rf /data/media/0/realdata");
|
||||
deleteDrivingDataBtn->setValue(tr("Deleted!"));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
deleteDrivingDataBtn->setValue("");
|
||||
}).detach();
|
||||
});
|
||||
addItem(deleteDrivingDataBtn);
|
||||
|
||||
// Panda flashing button
|
||||
auto flashPandaBtn = new ButtonControl(tr("Flash Panda"), tr("FLASH"), tr("Use this button to troubleshoot and update the Panda device's firmware."));
|
||||
connect(flashPandaBtn, &ButtonControl::clicked, [=]() {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to flash the Panda?"), tr("Flash"), this)) {
|
||||
std::thread([=]() {
|
||||
flashPandaBtn->setValue(tr("Flashing..."));
|
||||
|
||||
QProcess process;
|
||||
|
||||
process.setWorkingDirectory("/data/openpilot/panda/board");
|
||||
process.start("/bin/sh", QStringList{"-c", "./recover.py"});
|
||||
process.waitForFinished();
|
||||
process.start("/bin/sh", QStringList{"-c", "./flash.py"});
|
||||
process.waitForFinished();
|
||||
|
||||
process.setWorkingDirectory("/data/openpilot/panda/tests");
|
||||
process.start("/bin/sh", QStringList{"-c", "python reflash_internal_panda.py"});
|
||||
process.waitForFinished();
|
||||
|
||||
Hardware::soft_reboot();
|
||||
}).detach();
|
||||
}
|
||||
});
|
||||
addItem(flashPandaBtn);
|
||||
|
||||
// Reset toggle button
|
||||
auto resetTogglesBtn = new ButtonControl(tr("Reset Toggle Settings"), tr("RESET"), tr("Reset your toggle settings back to default."));
|
||||
connect(resetTogglesBtn, &ButtonControl::clicked, [=]() {
|
||||
if (!ConfirmationDialog::confirm(tr("Are you sure you want to completely reset your toggle settings? This is irreversible!"), tr("Reset"), this)) return;
|
||||
std::thread([&] {
|
||||
resetTogglesBtn->setValue(tr("Resetting toggles..."));
|
||||
|
||||
std::system("find /data/params -type f ! -name 'FrogPilotDrives' ! -name 'FrogPilotMinutes' ! -name 'FrogPilotKilometers' -exec rm {} +");
|
||||
std::system("find /persist/params -type f ! -name 'FrogPilotDrives' ! -name 'FrogPilotMinutes' ! -name 'FrogPilotKilometers' -exec rm {} +");
|
||||
|
||||
Hardware::soft_reboot();
|
||||
}).detach();
|
||||
});
|
||||
addItem(resetTogglesBtn);
|
||||
|
||||
// Backup FrogPilot
|
||||
std::vector<QString> frogpilotBackupOptions{tr("Backup"), tr("Delete"), tr("Restore")};
|
||||
FrogPilotButtonsControl *frogpilotBackup = new FrogPilotButtonsControl(tr("FrogPilot Backups"), tr("Backup, delete, or restore your FrogPilot backups."), "", frogpilotBackupOptions);
|
||||
|
||||
connect(frogpilotBackup, &FrogPilotButtonsControl::buttonClicked, [=](int id) {
|
||||
// CLEARPILOT - changed backups to /data/cp_backups
|
||||
QDir backupDir("/data/cp_backups");
|
||||
|
||||
if (id == 0) {
|
||||
QString nameSelection = InputDialog::getText(tr("Name your backup"), this, "", false, 1);
|
||||
if (!nameSelection.isEmpty()) {
|
||||
std::thread([=]() {
|
||||
frogpilotBackup->setValue(tr("Backing up..."));
|
||||
|
||||
std::string fullBackupPath = backupDir.absolutePath().toStdString() + "/" + nameSelection.toStdString();
|
||||
|
||||
std::ostringstream commandStream;
|
||||
commandStream << "mkdir -p " << std::quoted(fullBackupPath)
|
||||
<< " && rsync -av /data/openpilot/ " << std::quoted(fullBackupPath + "/");
|
||||
std::string command = commandStream.str();
|
||||
|
||||
int result = std::system(command.c_str());
|
||||
if (result == 0) {
|
||||
std::cout << "Backup successful to " << fullBackupPath << std::endl;
|
||||
frogpilotBackup->setValue(tr("Success!"));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
frogpilotBackup->setValue("");
|
||||
} else {
|
||||
std::cerr << "Backup failed with error code: " << result << std::endl;
|
||||
frogpilotBackup->setValue(tr("Failed..."));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
frogpilotBackup->setValue("");
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
} else if (id == 1) {
|
||||
QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
QString selection = MultiOptionDialog::getSelection(tr("Select a backup to delete"), backupNames, "", this);
|
||||
if (!selection.isEmpty()) {
|
||||
if (!ConfirmationDialog::confirm(tr("Are you sure you want to delete this backup?"), tr("Delete"), this)) return;
|
||||
std::thread([=]() {
|
||||
frogpilotBackup->setValue(tr("Deleting..."));
|
||||
QDir dirToDelete(backupDir.absoluteFilePath(selection));
|
||||
if (dirToDelete.removeRecursively()) {
|
||||
frogpilotBackup->setValue(tr("Deleted!"));
|
||||
} else {
|
||||
frogpilotBackup->setValue(tr("Failed..."));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
frogpilotBackup->setValue("");
|
||||
}).detach();
|
||||
}
|
||||
} else {
|
||||
QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
QString selection = MultiOptionDialog::getSelection(tr("Select a restore point"), backupNames, "", this);
|
||||
if (!selection.isEmpty()) {
|
||||
if (!ConfirmationDialog::confirm(tr("Are you sure you want to restore this version of FrogPilot?"), tr("Restore"), this)) return;
|
||||
std::thread([=]() {
|
||||
frogpilotBackup->setValue(tr("Restoring..."));
|
||||
|
||||
std::string sourcePath = backupDir.absolutePath().toStdString() + "/" + selection.toStdString();
|
||||
std::string targetPath = "/data/safe_staging/finalized";
|
||||
std::string consistentFilePath = targetPath + "/.overlay_consistent";
|
||||
|
||||
std::ostringstream commandStream;
|
||||
commandStream << "rsync -av --delete --exclude='.overlay_consistent' " << std::quoted(sourcePath + "/") << " " << std::quoted(targetPath + "/");
|
||||
std::string command = commandStream.str();
|
||||
int result = std::system(command.c_str());
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "Restore successful from " << sourcePath << " to " << targetPath << std::endl;
|
||||
std::ofstream consistentFile(consistentFilePath);
|
||||
if (consistentFile) {
|
||||
consistentFile.close();
|
||||
std::cout << ".overlay_consistent file created successfully." << std::endl;
|
||||
} else {
|
||||
std::cerr << "Failed to create .overlay_consistent file." << std::endl;
|
||||
frogpilotBackup->setValue(tr("Failed..."));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
frogpilotBackup->setValue("");
|
||||
}
|
||||
Hardware::soft_reboot();
|
||||
} else {
|
||||
std::cerr << "Restore failed with error code: " << result << std::endl;
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
addItem(frogpilotBackup);
|
||||
|
||||
// Backup toggles
|
||||
std::vector<QString> toggleBackupOptions{tr("Backup"), tr("Delete"), tr("Restore")};
|
||||
FrogPilotButtonsControl *toggleBackup = new FrogPilotButtonsControl(tr("Toggle Backups"), tr("Backup, delete, or restore your toggle backups."), "", toggleBackupOptions);
|
||||
|
||||
connect(toggleBackup, &FrogPilotButtonsControl::buttonClicked, [=](int id) {
|
||||
// CLEARPILOT changed to cp_toggle_backups
|
||||
QDir backupDir("/data/cp_toggle_backups");
|
||||
|
||||
if (id == 0) {
|
||||
QString nameSelection = InputDialog::getText(tr("Name your backup"), this, "", false, 1);
|
||||
if (!nameSelection.isEmpty()) {
|
||||
std::thread([=]() {
|
||||
toggleBackup->setValue(tr("Backing up..."));
|
||||
|
||||
std::string fullBackupPath = backupDir.absolutePath().toStdString() + "/" + nameSelection.toStdString() + "/";
|
||||
|
||||
std::ostringstream commandStream;
|
||||
commandStream << "mkdir -p " << std::quoted(fullBackupPath)
|
||||
<< " && rsync -av /data/params/d/ " << std::quoted(fullBackupPath);
|
||||
std::string command = commandStream.str();
|
||||
|
||||
int result = std::system(command.c_str());
|
||||
if (result == 0) {
|
||||
std::cout << "Backup successful to " << fullBackupPath << std::endl;
|
||||
toggleBackup->setValue(tr("Success!"));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
toggleBackup->setValue("");
|
||||
} else {
|
||||
std::cerr << "Backup failed with error code: " << result << std::endl;
|
||||
toggleBackup->setValue(tr("Failed..."));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
toggleBackup->setValue("");
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
} else if (id == 1) {
|
||||
QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
QString selection = MultiOptionDialog::getSelection(tr("Select a backup to delete"), backupNames, "", this);
|
||||
if (!selection.isEmpty()) {
|
||||
if (!ConfirmationDialog::confirm(tr("Are you sure you want to delete this backup?"), tr("Delete"), this)) return;
|
||||
std::thread([=]() {
|
||||
toggleBackup->setValue(tr("Deleting..."));
|
||||
QDir dirToDelete(backupDir.absoluteFilePath(selection));
|
||||
if (dirToDelete.removeRecursively()) {
|
||||
toggleBackup->setValue(tr("Deleted!"));
|
||||
} else {
|
||||
toggleBackup->setValue(tr("Failed..."));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
toggleBackup->setValue("");
|
||||
}).detach();
|
||||
}
|
||||
} else {
|
||||
QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
|
||||
QString selection = MultiOptionDialog::getSelection(tr("Select a restore point"), backupNames, "", this);
|
||||
if (!selection.isEmpty()) {
|
||||
if (!ConfirmationDialog::confirm(tr("Are you sure you want to restore this toggle backup?"), tr("Restore"), this)) return;
|
||||
std::thread([=]() {
|
||||
toggleBackup->setValue(tr("Restoring..."));
|
||||
|
||||
std::string sourcePath = backupDir.absolutePath().toStdString() + "/" + selection.toStdString() + "/";
|
||||
std::string targetPath = "/data/params/d/";
|
||||
|
||||
std::ostringstream commandStream;
|
||||
commandStream << "rsync -av --delete " << std::quoted(sourcePath) << " " << std::quoted(targetPath);
|
||||
std::string command = commandStream.str();
|
||||
|
||||
int result = std::system(command.c_str());
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "Restore successful from " << sourcePath << " to " << targetPath << std::endl;
|
||||
toggleBackup->setValue(tr("Success!"));
|
||||
paramsMemory.putBool("FrogPilotTogglesUpdated", true);
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
toggleBackup->setValue("");
|
||||
paramsMemory.putBool("FrogPilotTogglesUpdated", false);
|
||||
} else {
|
||||
std::cerr << "Restore failed with error code: " << result << std::endl;
|
||||
toggleBackup->setValue(tr("Failed..."));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
toggleBackup->setValue("");
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
addItem(toggleBackup);
|
||||
|
||||
auto lockDoorsButton = new ButtonControl(tr("Lock Doors"), tr("LOCK"), tr("Use this button to lock the doors on Toyota/Lexus vehicles."));
|
||||
connect(lockDoorsButton, &ButtonControl::clicked, [this]() {
|
||||
QProcess *process = new QProcess(this);
|
||||
QString scriptPath = "/data/openpilot/frogpilot/controls/lib/lock_doors.py";
|
||||
QStringList arguments{"--lock"};
|
||||
process->start("python3", QStringList() << scriptPath << arguments);
|
||||
});
|
||||
addItem(lockDoorsButton);
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
|
||||
for (auto btn : findChildren<ButtonControl *>()) {
|
||||
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 *softreboot_btn = new QPushButton(tr("Soft Reboot"));
|
||||
softreboot_btn->setObjectName("softreboot_btn");
|
||||
power_layout->addWidget(softreboot_btn);
|
||||
QObject::connect(softreboot_btn, &QPushButton::clicked, this, &DevicePanel::softreboot);
|
||||
|
||||
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"(
|
||||
#softreboot_btn { height: 120px; border-radius: 15px; background-color: #e2e22c; }
|
||||
#softreboot_btn:pressed { background-color: #ffe224; }
|
||||
#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<cereal::Event>().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<ButtonControl *>(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::softreboot() {
|
||||
if (!uiState()->engaged()) {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to soft reboot?"), tr("Soft Reboot"), this)) {
|
||||
if (!uiState()->engaged()) {
|
||||
params.putBool("DoSoftReboot", true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ConfirmationDialog::alert(tr("Disengage to Soft 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::hideEvent(QHideEvent *event) {
|
||||
closeParentToggle();
|
||||
|
||||
parentToggleOpen = false;
|
||||
subParentToggleOpen = false;
|
||||
|
||||
previousScrollPosition = 0;
|
||||
}
|
||||
|
||||
void SettingsWindow::showEvent(QShowEvent *event) {
|
||||
setCurrentPanel(0);
|
||||
}
|
||||
|
||||
void SettingsWindow::setCurrentPanel(int index, const QString ¶m) {
|
||||
panel_widget->setCurrentIndex(index);
|
||||
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);
|
||||
panel_widget = new QStackedWidget();
|
||||
|
||||
// close button
|
||||
QPushButton *close_btn = new QPushButton(tr("← Back"));
|
||||
close_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
color: white;
|
||||
border-radius: 25px;
|
||||
background: #292929;
|
||||
font-size: 50px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
color: #ADADAD;
|
||||
}
|
||||
)");
|
||||
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 (subParentToggleOpen) {
|
||||
closeSubParentToggle();
|
||||
subParentToggleOpen = false;
|
||||
} else if (parentToggleOpen) {
|
||||
closeParentToggle();
|
||||
parentToggleOpen = false;
|
||||
} else {
|
||||
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::openSubParentToggle, this, [this]() {subParentToggleOpen = true;});
|
||||
QObject::connect(frogpilotControls, &FrogPilotControlsPanel::openParentToggle, this, [this]() {parentToggleOpen = true;});
|
||||
|
||||
FrogPilotVisualsPanel *frogpilotVisuals = new FrogPilotVisualsPanel(this);
|
||||
QObject::connect(frogpilotVisuals, &FrogPilotVisualsPanel::openParentToggle, this, [this]() {parentToggleOpen = true;});
|
||||
|
||||
QList<QPair<QString, QWidget *>> panels = {
|
||||
{tr("Device"), device},
|
||||
{tr("Network"), new Networking(this)},
|
||||
{tr("Toggles"), toggles},
|
||||
{tr("Software"), new SoftwarePanel(this)},
|
||||
{tr("Controls"), frogpilotControls},
|
||||
{tr("Vehicles"), new FrogPilotVehiclesPanel(this)},
|
||||
{tr("Visuals"), frogpilotVisuals},
|
||||
};
|
||||
|
||||
for (auto &[name, panel] : panels) {
|
||||
QPushButton *btn = new QPushButton(name);
|
||||
btn->setCheckable(true);
|
||||
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);
|
||||
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")) {
|
||||
QScrollBar *scrollbar = panel_frame->verticalScrollBar();
|
||||
|
||||
QObject::connect(scrollbar, &QScrollBar::valueChanged, this, [this](int value) {
|
||||
if (!parentToggleOpen) {
|
||||
previousScrollPosition = value;
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(scrollbar, &QScrollBar::rangeChanged, this, [this, panel_frame]() {
|
||||
if (!parentToggleOpen) {
|
||||
panel_frame->restorePosition(previousScrollPosition);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() {
|
||||
closeParentToggle();
|
||||
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;
|
||||
}
|
||||
)");
|
||||
}
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QWidget>
|
||||
|
||||
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#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;
|
||||
|
||||
// FrogPilot widgets
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
signals:
|
||||
void closeSettings();
|
||||
void reviewTrainingGuide();
|
||||
void showDriverView();
|
||||
void expandToggleDescription(const QString ¶m);
|
||||
|
||||
// FrogPilot signals
|
||||
void closeParentToggle();
|
||||
void closeSubParentToggle();
|
||||
void updateMetric();
|
||||
|
||||
private:
|
||||
QPushButton *sidebar_alert_widget;
|
||||
QWidget *sidebar_widget;
|
||||
QStackedWidget *panel_widget;
|
||||
|
||||
// FrogPilot variables
|
||||
bool parentToggleOpen;
|
||||
bool subParentToggleOpen;
|
||||
|
||||
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 softreboot();
|
||||
void updateCalibDescription();
|
||||
|
||||
private:
|
||||
Params params;
|
||||
|
||||
// FrogPilot variables
|
||||
Params paramsMemory{"/dev/shm/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 slots:
|
||||
void updateState(const UIState &s);
|
||||
|
||||
private:
|
||||
Params params;
|
||||
std::map<std::string, ParamControl*> 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 *installBtn;
|
||||
ButtonControl *downloadBtn;
|
||||
ButtonControl *targetBranchBtn;
|
||||
|
||||
Params params;
|
||||
ParamWatcher *fs_watch;
|
||||
|
||||
// FrogPilot variables
|
||||
Params paramsMemory{"/dev/shm/params"};
|
||||
UIScene &scene;
|
||||
};
|
||||
Executable
+186
@@ -0,0 +1,186 @@
|
||||
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QLabel>
|
||||
#include <QProcess>
|
||||
|
||||
#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"
|
||||
|
||||
#include "selfdrive/frogpilot/ui/qt/widgets/frogpilot_controls.h"
|
||||
|
||||
void SoftwarePanel::checkForUpdates() {
|
||||
std::system("pkill -SIGUSR1 -f selfdrive.updated.updated");
|
||||
}
|
||||
|
||||
SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent), scene(uiState()->scene) {
|
||||
onroadLbl = new QLabel(tr("Updates are only downloaded while the car is off or in park."));
|
||||
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);
|
||||
|
||||
// automatic updates toggle
|
||||
ParamControl *automaticUpdatesToggle = new ParamControl("AutomaticUpdates", tr(" Automatically Update ClearPilot"),
|
||||
tr("ClearPilot will automatically update itself and it's assets when you're offroad and connected to Wi-Fi."), "");
|
||||
connect(automaticUpdatesToggle, &ToggleControl::toggleFlipped, [this]() {
|
||||
std::thread([this]() {
|
||||
paramsMemory.putBool("FrogPilotTogglesUpdated", true);
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
paramsMemory.putBool("FrogPilotTogglesUpdated", false);
|
||||
}).detach();
|
||||
});
|
||||
addItem(automaticUpdatesToggle);
|
||||
|
||||
// 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.updated");
|
||||
}
|
||||
paramsMemory.putBool("ManualUpdateInitiated", true);
|
||||
});
|
||||
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(",");
|
||||
if (!Params("/persist/params").getBool("FrogsGoMoo")) {
|
||||
branches.removeAll("FrogPilot-Development");
|
||||
branches.removeAll("FrogPilot-New");
|
||||
branches.removeAll("MAKE-PRS-HERE");
|
||||
}
|
||||
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
|
||||
auto errorLogBtn = new ButtonControl(tr("Error Log"), tr("VIEW"), tr("View the error log for 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) {
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
|
||||
is_onroad = !offroad;
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// updater only runs offroad or when parked
|
||||
bool parked = scene.parked;
|
||||
|
||||
onroadLbl->setVisible(is_onroad && !parked);
|
||||
downloadBtn->setVisible(!is_onroad || parked);
|
||||
|
||||
// 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 || parked) && params.getBool("UpdateAvailable"));
|
||||
installBtn->setValue(QString::fromStdString(params.get("UpdaterNewDescription")));
|
||||
installBtn->setDescription(QString::fromStdString(params.get("UpdaterNewReleaseNotes")));
|
||||
|
||||
update();
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
signal scroll()
|
||||
|
||||
Flickable {
|
||||
id: flickArea
|
||||
objectName: "flickArea"
|
||||
anchors.fill: parent
|
||||
contentHeight: helpText.height
|
||||
contentWidth: width - (leftMargin + rightMargin)
|
||||
bottomMargin: 50
|
||||
topMargin: 50
|
||||
rightMargin: 50
|
||||
leftMargin: 50
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
flickDeceleration: 7500.0
|
||||
maximumFlickVelocity: 10000.0
|
||||
pixelAligned: true
|
||||
|
||||
onAtYEndChanged: root.scroll()
|
||||
|
||||
Text {
|
||||
id: helpText
|
||||
width: flickArea.contentWidth
|
||||
font.family: "Inter"
|
||||
font.weight: "Light"
|
||||
font.pixelSize: 50
|
||||
textFormat: Text.RichText
|
||||
color: "#C9C9C9"
|
||||
wrapMode: Text.Wrap
|
||||
text: text_view
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: scrollbar
|
||||
anchors.right: flickArea.right
|
||||
anchors.rightMargin: 20
|
||||
y: flickArea.topMargin + flickArea.visibleArea.yPosition * (flickArea.height - flickArea.bottomMargin - flickArea.topMargin)
|
||||
width: 12
|
||||
radius: 6
|
||||
height: flickArea.visibleArea.heightRatio * (flickArea.height - flickArea.bottomMargin - flickArea.topMargin)
|
||||
color: "#808080"
|
||||
}
|
||||
}
|
||||
Executable
+1290
File diff suppressed because it is too large
Load Diff
Executable
+190
@@ -0,0 +1,190 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QMovie>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#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;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
// 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);
|
||||
void updateLaneEdgeColor(QColor &bgColor);
|
||||
|
||||
private:
|
||||
void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255);
|
||||
void drawSpeedWidget(QPainter &p, int x, int y, const QString &title, const QString &speedLimitStr, QColor colorSpeed, int width = 176);
|
||||
|
||||
QVBoxLayout *main_layout;
|
||||
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<PubMaster> pm;
|
||||
QColor edgeColor = bg_colors[STATUS_DISENGAGED];
|
||||
|
||||
int skip_frame_count = 0;
|
||||
bool wide_cam_requested = false;
|
||||
|
||||
// FrogPilot widgets
|
||||
void initializeFrogPilotWidgets();
|
||||
void paintFrogPilotWidgets(QPainter &p);
|
||||
void updateFrogPilotWidgets();
|
||||
|
||||
void drawLeadInfo(QPainter &p);
|
||||
void drawSLCConfirmation(QPainter &p);
|
||||
void drawStatusBar(QPainter &p);
|
||||
void drawTurnSignals(QPainter &p);
|
||||
|
||||
// FrogPilot variables
|
||||
Params paramsMemory{"/dev/shm/params"};
|
||||
UIScene &scene;
|
||||
|
||||
ScreenRecorder *recorder_btn;
|
||||
|
||||
QHBoxLayout *bottom_layout;
|
||||
|
||||
bool alwaysOnLateralActive;
|
||||
bool blindSpotLeft;
|
||||
bool blindSpotRight;
|
||||
bool experimentalMode;
|
||||
bool leadInfo;
|
||||
bool roadNameUI;
|
||||
bool showAlwaysOnLateralStatusBar;
|
||||
bool showConditionalExperimentalStatusBar;
|
||||
bool showSLCOffset;
|
||||
bool slcOverridden;
|
||||
bool speedLimitController;
|
||||
bool trafficModeActive;
|
||||
bool turnSignalLeft;
|
||||
bool turnSignalRight;
|
||||
bool useViennaSLCSign;
|
||||
bool vtscControllingCurve;
|
||||
|
||||
float cruiseAdjustment;
|
||||
float distanceConversion;
|
||||
float laneDetectionWidth;
|
||||
float laneWidthLeft;
|
||||
float laneWidthRight;
|
||||
float slcSpeedLimitOffset;
|
||||
float speedConversion;
|
||||
|
||||
int alertSize;
|
||||
int cameraView;
|
||||
int conditionalStatus;
|
||||
int currentHolidayTheme;
|
||||
int customColors;
|
||||
int customSignals;
|
||||
int obstacleDistance;
|
||||
int obstacleDistanceStock;
|
||||
int totalFrames = 8;
|
||||
|
||||
QString leadDistanceUnit;
|
||||
QString leadSpeedUnit;
|
||||
|
||||
size_t animationFrameIndex;
|
||||
|
||||
std::unordered_map<int, std::tuple<QString, QColor, std::map<double, QBrush>>> themeConfiguration;
|
||||
std::unordered_map<int, std::tuple<QString, QColor, std::map<double, QBrush>>> holidayThemeConfiguration;
|
||||
std::vector<QPixmap> signalImgVector;
|
||||
|
||||
QTimer *animationTimer;
|
||||
|
||||
inline QColor blueColor(int alpha = 255) { return QColor(0, 150, 255, alpha); }
|
||||
inline QColor greenColor(int alpha = 242) { return QColor(23, 134, 68, alpha); }
|
||||
|
||||
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::ModelDataV2::LeadDataV3::Reader &lead_data, const QPointF &vd, const float v_ego);
|
||||
void drawHud(QPainter &p);
|
||||
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);
|
||||
|
||||
// signals:
|
||||
|
||||
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;
|
||||
Params params;
|
||||
Params paramsMemory{"/dev/shm/params"};
|
||||
|
||||
QPoint timeoutPoint = QPoint(420, 69);
|
||||
QTimer clickTimer;
|
||||
|
||||
private slots:
|
||||
void offroadTransition(bool offroad);
|
||||
void updateState(const UIState &s);
|
||||
};
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
from cffi import FFI
|
||||
|
||||
import sip
|
||||
|
||||
from openpilot.common.ffi_wrapper import suffix
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
|
||||
def get_ffi():
|
||||
lib = os.path.join(BASEDIR, "selfdrive", "ui", "qt", "libpython_helpers" + suffix())
|
||||
|
||||
ffi = FFI()
|
||||
ffi.cdef("void set_main_window(void *w);")
|
||||
return ffi, ffi.dlopen(lib)
|
||||
|
||||
|
||||
def set_main_window(widget):
|
||||
ffi, lib = get_ffi()
|
||||
lib.set_main_window(ffi.cast('void*', sip.unwrapinstance(widget)))
|
||||
Executable
+34
@@ -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<wl_surface*>(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);
|
||||
}
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
#include <QWidget>
|
||||
|
||||
#ifdef QCOM2
|
||||
#include <qpa/qplatformnativeinterface.h>
|
||||
#include <wayland-client-protocol.h>
|
||||
#include <QPlatformSurfaceEvent>
|
||||
#endif
|
||||
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
const QString ASSET_PATH = ":/";
|
||||
const QSize DEVICE_SCREEN_SIZE = {2160, 1080};
|
||||
|
||||
void setMainWindow(QWidget *w);
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#include "selfdrive/ui/qt/ready.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStackedLayout>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QGridLayout>
|
||||
#include <QString>
|
||||
#include <QTransform>
|
||||
#include <QPixmap>
|
||||
|
||||
#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"
|
||||
|
||||
|
||||
ReadyWindow::ReadyWindow(QWidget *parent) : QWidget(parent) {
|
||||
QGridLayout *layout = new QGridLayout(this);
|
||||
layout->setSpacing(0);
|
||||
layout->setMargin(0);
|
||||
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
setStyleSheet("ReadyWindow { background-color: black; }");
|
||||
|
||||
timer = new QTimer(this);
|
||||
timer->callOnTimeout(this, &ReadyWindow::refresh);
|
||||
}
|
||||
|
||||
void ReadyWindow::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
timer->start(5 * 1000);
|
||||
}
|
||||
|
||||
void ReadyWindow::hideEvent(QHideEvent *event) {
|
||||
timer->stop();
|
||||
}
|
||||
|
||||
void ReadyWindow::paintEvent(QPaintEvent *event) {
|
||||
QPainter painter(this);
|
||||
QPixmap *img_shown = nullptr;
|
||||
|
||||
if (is_hot) {
|
||||
if (img_hot.isNull()) {
|
||||
img_hot.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/hot.png");
|
||||
}
|
||||
img_shown = &img_hot;
|
||||
} else {
|
||||
if (img_ready.isNull()) {
|
||||
img_ready.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready.png");
|
||||
}
|
||||
img_shown = &img_ready;
|
||||
}
|
||||
|
||||
int x = (width() - img_shown->width()) / 2;
|
||||
int y = (height() - img_shown->height()) / 2;
|
||||
painter.drawPixmap(x, y, *img_shown);
|
||||
}
|
||||
|
||||
void ReadyWindow::refresh() {
|
||||
std::string bytes = params.get("Offroad_TemperatureTooHigh");
|
||||
if (!bytes.empty()) {
|
||||
auto doc = QJsonDocument::fromJson(bytes.data());
|
||||
is_hot = true;
|
||||
cur_temp = doc["extra"].toString();
|
||||
update();
|
||||
} else if (is_hot) {
|
||||
is_hot = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMovie>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QPixmap>
|
||||
#include <QProgressBar>
|
||||
#include <QSocketNotifier>
|
||||
#include <QVariantAnimation>
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
class ReadyWindow : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ReadyWindow(QWidget* parent = nullptr);
|
||||
private:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
void refresh();
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
Params params;
|
||||
QTimer* timer;
|
||||
bool is_hot = false;
|
||||
QString cur_temp;
|
||||
QPixmap img_ready;
|
||||
QPixmap img_hot;
|
||||
};
|
||||
Executable
+27
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+15
@@ -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;
|
||||
};
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#include <QApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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("System reset triggered. 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."));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
QApplication a(argc, argv);
|
||||
Reset reset(mode);
|
||||
setMainWindow(&reset);
|
||||
return a.exec();
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
enum ResetMode {
|
||||
USER_RESET, // user initiated a factory reset from openpilot
|
||||
RECOVER, // userdata is corrupt for some reason, give a chance to recover
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
Executable
+487
@@ -0,0 +1,487 @@
|
||||
#include "selfdrive/ui/qt/setup/setup.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#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 OPENPILOT_URL = "https://openpilot.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, this, &Setup::nextPage);
|
||||
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(OPENPILOT_URL);
|
||||
QTimer *timer = new QTimer(this);
|
||||
QObject::connect(timer, &QTimer::timeout, [=]() {
|
||||
if (!request->active() && cont->isVisible()) {
|
||||
request->sendRequest(OPENPILOT_URL);
|
||||
}
|
||||
});
|
||||
timer->start(1000);
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
QWidget * radio_button(QString title, QButtonGroup *group) {
|
||||
QPushButton *btn = new QPushButton(title);
|
||||
btn->setCheckable(true);
|
||||
group->addButton(btn);
|
||||
btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
height: 230;
|
||||
padding-left: 100px;
|
||||
padding-right: 100px;
|
||||
text-align: left;
|
||||
font-size: 80px;
|
||||
font-weight: 400;
|
||||
border-radius: 10px;
|
||||
background-color: #4F4F4F;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #465BEA;
|
||||
}
|
||||
)");
|
||||
|
||||
// checkmark icon
|
||||
QPixmap pix(":/img_circled_check.svg");
|
||||
btn->setIcon(pix);
|
||||
btn->setIconSize(QSize(0, 0));
|
||||
btn->setLayoutDirection(Qt::RightToLeft);
|
||||
QObject::connect(btn, &QPushButton::toggled, [=](bool checked) {
|
||||
btn->setIconSize(checked ? QSize(104, 104) : QSize(0, 0));
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
|
||||
QWidget * Setup::software_selection() {
|
||||
QWidget *widget = new QWidget();
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(widget);
|
||||
main_layout->setContentsMargins(55, 50, 55, 50);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
// title
|
||||
QLabel *title = new QLabel(tr("Choose Software to Install"));
|
||||
title->setStyleSheet("font-size: 90px; font-weight: 500;");
|
||||
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop);
|
||||
|
||||
main_layout->addSpacing(50);
|
||||
|
||||
// openpilot + custom radio buttons
|
||||
QButtonGroup *group = new QButtonGroup(widget);
|
||||
group->setExclusive(true);
|
||||
|
||||
QWidget *openpilot = radio_button(tr("openpilot"), group);
|
||||
main_layout->addWidget(openpilot);
|
||||
|
||||
main_layout->addSpacing(30);
|
||||
|
||||
QWidget *custom = radio_button(tr("Custom Software"), group);
|
||||
main_layout->addWidget(custom);
|
||||
|
||||
main_layout->addStretch();
|
||||
|
||||
// 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(tr("Continue"));
|
||||
cont->setObjectName("navBtn");
|
||||
cont->setEnabled(false);
|
||||
cont->setProperty("primary", true);
|
||||
blayout->addWidget(cont);
|
||||
|
||||
QObject::connect(cont, &QPushButton::clicked, [=]() {
|
||||
auto w = currentWidget();
|
||||
QTimer::singleShot(0, [=]() {
|
||||
setCurrentWidget(downloading_widget);
|
||||
});
|
||||
QString url = OPENPILOT_URL;
|
||||
if (group->checkedButton() != openpilot) {
|
||||
url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software"));
|
||||
}
|
||||
if (!url.isEmpty()) {
|
||||
QTimer::singleShot(1000, this, [=]() {
|
||||
download(url);
|
||||
});
|
||||
} else {
|
||||
setCurrentWidget(w);
|
||||
}
|
||||
});
|
||||
|
||||
connect(group, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked), [=](QAbstractButton *btn) {
|
||||
btn->setChecked(true);
|
||||
cont->setEnabled(true);
|
||||
});
|
||||
|
||||
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());
|
||||
addWidget(software_selection());
|
||||
|
||||
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<QString, QString> 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();
|
||||
}
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QStackedWidget>
|
||||
#include <QString>
|
||||
#include <QTranslator>
|
||||
#include <QWidget>
|
||||
|
||||
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 *software_selection();
|
||||
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);
|
||||
};
|
||||
Executable
+186
@@ -0,0 +1,186 @@
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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(install, &QPushButton::clicked, this, &Updater::installUpdate);
|
||||
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);
|
||||
|
||||
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::installUpdate() {
|
||||
setCurrentWidget(progress);
|
||||
QObject::connect(&proc, &QProcess::readyReadStandardOutput, this, &Updater::readProgress);
|
||||
QObject::connect(&proc, QOverload<int, QProcess::ExitStatus>::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();
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
#include <QProgressBar>
|
||||
#include <QStackedWidget>
|
||||
#include <QWidget>
|
||||
|
||||
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;
|
||||
};
|
||||
Executable
+289
@@ -0,0 +1,289 @@
|
||||
#include "selfdrive/ui/qt/sidebar.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
void Sidebar::drawMetric(QPainter &p, const QPair<QString, QString> &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);
|
||||
currentColor = QColor(255, 255, 255);
|
||||
|
||||
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<PubMaster, const std::initializer_list<const char *>>({"userFlag"});
|
||||
|
||||
// FrogPilot variables
|
||||
isCPU = params.getBool("ShowCPU");
|
||||
isGPU = params.getBool("ShowGPU");
|
||||
|
||||
isIP = params.getBool("ShowIP");
|
||||
|
||||
isMemoryUsage = params.getBool("ShowMemoryUsage");
|
||||
isStorageLeft = params.getBool("ShowStorageLeft");
|
||||
isStorageUsed = params.getBool("ShowStorageUsed");
|
||||
|
||||
}
|
||||
|
||||
void Sidebar::mousePressEvent(QMouseEvent *event) {
|
||||
// Declare the click boxes
|
||||
QRect cpuRect = {30, 496, 240, 126};
|
||||
QRect memoryRect = {30, 654, 240, 126};
|
||||
QRect networkRect = {30, 196, 240, 126};
|
||||
QRect tempRect = {30, 338, 240, 126};
|
||||
|
||||
static int showChip = 0;
|
||||
static int showMemory = 0;
|
||||
static int showNetwork = 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 (networkRect.contains(event->pos())) {
|
||||
showNetwork = (showNetwork + 1) % 2;
|
||||
isIP = (showNetwork == 1);
|
||||
params.putBoolNonBlocking("ShowIP", isIP);
|
||||
|
||||
update();
|
||||
} else if (tempRect.contains(event->pos())) {
|
||||
showTemp = (showTemp + 1) % 3;
|
||||
|
||||
scene.fahrenheit = showTemp == 2;
|
||||
scene.numerical_temp = showTemp != 0;
|
||||
|
||||
params.putBoolNonBlocking("Fahrenheit", showTemp == 2);
|
||||
params.putBoolNonBlocking("NumericalTemp", showTemp != 0);
|
||||
|
||||
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())) {
|
||||
|
||||
// ?? remove?
|
||||
MessageBuilder msg;
|
||||
msg.initEvent().initUserFlag();
|
||||
pm->send("userFlag", msg);
|
||||
|
||||
emit openOnroad();
|
||||
|
||||
} 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);
|
||||
|
||||
auto frogpilotDeviceState = sm["frogpilotDeviceState"].getFrogpilotDeviceState();
|
||||
|
||||
bool isNumericalTemp = scene.numerical_temp;
|
||||
|
||||
int maxTempC = deviceState.getMaxTempC();
|
||||
QString max_temp = scene.fahrenheit ? QString::number(maxTempC * 9 / 5 + 32) + "°F" : QString::number(maxTempC) + "°C";
|
||||
|
||||
// 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 = {{isGPU ? tr("GPU") : tr("CPU"), metric}, currentColor};
|
||||
if (usage >= 85) {
|
||||
cpuStatus = {{isGPU ? tr("GPU") : tr("CPU"), metric}, danger_color};
|
||||
} else if (usage >= 70) {
|
||||
cpuStatus = {{isGPU ? tr("GPU") : tr("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) + tr(" GB");
|
||||
|
||||
if (isMemoryUsage) {
|
||||
ItemStatus memoryStatus = {{tr("MEMORY"), memory}, currentColor};
|
||||
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 = {{isStorageLeft ? tr("LEFT") : tr("USED"), storage}, currentColor};
|
||||
if (25 > storage_left && storage_left >= 10) {
|
||||
storageStatus = {{isStorageLeft ? tr("LEFT") : tr("USED"), storage}, warning_color};
|
||||
} else if (10 > storage_left) {
|
||||
storageStatus = {{isStorageLeft ? tr("LEFT") : tr("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")}, currentColor}
|
||||
: 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")}, currentColor};
|
||||
} 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")}, currentColor};
|
||||
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);
|
||||
p.setFont(InterFont(35));
|
||||
|
||||
if (isIP) {
|
||||
p.setPen(QColor(0xff, 0xff, 0xff));
|
||||
p.save();
|
||||
p.setFont(InterFont(30));
|
||||
QRect ipBox = QRect(50, 196, 225, 27);
|
||||
p.drawText(ipBox, Qt::AlignLeft | Qt::AlignVCenter, uiState()->wifi->getIp4Address());
|
||||
p.restore();
|
||||
} else {
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
p.setBrush(i < net_strength ? Qt::white : gray);
|
||||
p.drawEllipse(x, 196, 27, 27);
|
||||
x += 37;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QFrame>
|
||||
#include <QMap>
|
||||
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
typedef QPair<QPair<QString, QString>, 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 openOnroad(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<QString, QString> &label, QColor c, int y);
|
||||
|
||||
QPixmap home_img, flag_img, settings_img;
|
||||
bool onroad, flag_pressed, settings_pressed;
|
||||
const QMap<cereal::DeviceState::NetworkType, QString> 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<PubMaster> pm;
|
||||
|
||||
// FrogPilot variables
|
||||
Params params;
|
||||
UIScene &scene;
|
||||
|
||||
ItemStatus cpu_status, memory_status, storage_status;
|
||||
|
||||
bool isCPU;
|
||||
bool isGPU;
|
||||
bool isIP;
|
||||
bool isMemoryUsage;
|
||||
bool isStorageLeft;
|
||||
bool isStorageUsed;
|
||||
|
||||
std::unordered_map<int, std::pair<QString, std::vector<QColor>>> themeConfiguration;
|
||||
std::unordered_map<int, QPixmap> flag_imgs;
|
||||
std::unordered_map<int, QPixmap> home_imgs;
|
||||
std::unordered_map<int, QPixmap> settings_imgs;
|
||||
|
||||
std::unordered_map<int, std::pair<QString, std::vector<QColor>>> holidayThemeConfiguration;
|
||||
std::unordered_map<int, QPixmap> holiday_flag_imgs;
|
||||
std::unordered_map<int, QPixmap> holiday_home_imgs;
|
||||
std::unordered_map<int, QPixmap> holiday_settings_imgs;
|
||||
|
||||
QColor currentColor;
|
||||
};
|
||||
Executable
BIN
Binary file not shown.
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#include "selfdrive/ui/qt/spinner.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QGridLayout>
|
||||
#include <QPainter>
|
||||
#include <QString>
|
||||
#include <QTransform>
|
||||
|
||||
#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(23, 134, 68, 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();
|
||||
}
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#include <array>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPixmap>
|
||||
#include <QProgressBar>
|
||||
#include <QSocketNotifier>
|
||||
#include <QVariantAnimation>
|
||||
#include <QWidget>
|
||||
|
||||
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<QPixmap, spinner_fps> 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);
|
||||
};
|
||||
Executable
BIN
Binary file not shown.
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#include <QApplication>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QScrollBar>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#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"
|
||||
|
||||
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 *btn = new QPushButton();
|
||||
#ifdef __aarch64__
|
||||
btn->setText(QObject::tr("Reboot"));
|
||||
QObject::connect(btn, &QPushButton::clicked, [=]() {
|
||||
Hardware::reboot();
|
||||
});
|
||||
#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();
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
Executable
+276
@@ -0,0 +1,276 @@
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLayoutItem>
|
||||
#include <QStyleOption>
|
||||
#include <QPainterPath>
|
||||
#include <QTextStream>
|
||||
#include <QtXml/QDomDocument>
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
QString getVersion() {
|
||||
static QString version = QString::fromStdString(Params().get("Version"));
|
||||
return version;
|
||||
}
|
||||
|
||||
QString getBrand() {
|
||||
return QObject::tr("ClearPilot");
|
||||
}
|
||||
|
||||
QString getUserAgent() {
|
||||
return "openpilot-" + getVersion();
|
||||
}
|
||||
|
||||
std::optional<QString> getDongleId() {
|
||||
std::string id = Params().get("DongleId");
|
||||
|
||||
if (!id.empty() && (id != "UnregisteredDevice")) {
|
||||
return QString::fromStdString(id);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QMap<QString, QString> getSupportedLanguages() {
|
||||
QFile f(":/languages.json");
|
||||
f.open(QIODevice::ReadOnly | QIODevice::Text);
|
||||
QString val = f.readAll();
|
||||
|
||||
QJsonObject obj = QJsonDocument::fromJson(val.toUtf8()).object();
|
||||
QMap<QString, QString> 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));
|
||||
|
||||
// ensure the current dir matches the exectuable's directory
|
||||
QApplication tmp(argc, argv);
|
||||
QString appDir = QCoreApplication::applicationDirPath();
|
||||
QDir::setCurrent(appDir);
|
||||
|
||||
setQtSurfaceFormat();
|
||||
}
|
||||
|
||||
void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
|
||||
static std::map<QtMsgType, int> 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<float> xp, std::vector<QColor> 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<QString, QByteArray> load_bootstrap_icons() {
|
||||
QHash<QString, QByteArray> 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("<symbol", "<svg");
|
||||
svg_str.replace("</symbol>", "</svg>");
|
||||
icons[e.attribute("id")] = svg_str.toUtf8();
|
||||
}
|
||||
n = n.nextSibling();
|
||||
}
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
QPixmap bootstrapPixmap(const QString &id) {
|
||||
static QHash<QString, QByteArray> 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())));
|
||||
}
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QWidget>
|
||||
|
||||
#include "cereal/gen/cpp/car.capnp.h"
|
||||
#include "common/params.h"
|
||||
|
||||
QString getVersion();
|
||||
QString getBrand();
|
||||
QString getUserAgent();
|
||||
std::optional<QString> getDongleId();
|
||||
QMap<QString, QString> 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<float> xp, std::vector<QColor> 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<QString, QString> params_hash;
|
||||
Params params;
|
||||
};
|
||||
Executable
+437
@@ -0,0 +1,437 @@
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <OpenGL/gl3.h>
|
||||
#else
|
||||
#include <GLES3/gl3.h>
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOffscreenSurface>
|
||||
|
||||
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"
|
||||
// gamma to improve worst case visibility when dark
|
||||
" colorOut.rgb = pow(colorOut.rgb, vec3(1.0/1.28));\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<std::set<VisionStreamType>>("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<QOpenGLShaderProgram>(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<VisionStreamType> 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);
|
||||
}
|
||||
} 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<VisionIpcClient> 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();
|
||||
}
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QThread>
|
||||
|
||||
#ifdef QCOM2
|
||||
#define EGL_EGLEXT_PROTOTYPES
|
||||
#define EGL_NO_X11
|
||||
#define GL_TEXTURE_EXTERNAL_OES 0x8D65
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <drm/drm_fourcc.h>
|
||||
#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<VisionStreamType>);
|
||||
|
||||
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<QOpenGLShaderProgram> program;
|
||||
QColor bg = QColor("#000000");
|
||||
|
||||
#ifdef QCOM2
|
||||
std::map<int, EGLImageKHR> egl_images;
|
||||
#endif
|
||||
|
||||
std::string stream_name;
|
||||
int stream_width = 0;
|
||||
int stream_height = 0;
|
||||
int stream_stride = 0;
|
||||
std::atomic<VisionStreamType> active_stream_type;
|
||||
std::atomic<VisionStreamType> requested_stream_type;
|
||||
std::set<VisionStreamType> 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<std::pair<uint32_t, VisionBuf*>> 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<VisionStreamType> streams);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(std::set<VisionStreamType>);
|
||||
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStyleOption>
|
||||
|
||||
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("<body><h2 style=\"text-align: center;\">" + title_label->text() + "</h2><br>"
|
||||
"<p style=\"text-align: center; margin: 0 128px; font-size: 50px;\">" + getDescription() + "</p></body>");
|
||||
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();
|
||||
}
|
||||
}
|
||||
Executable
+300
@@ -0,0 +1,300 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
|
||||
#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 setEnabled(bool enabled) {
|
||||
toggle.setEnabled(enabled);
|
||||
toggle.update();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
toggle.togglePosition();
|
||||
}
|
||||
|
||||
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<QString> &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: #33Ab4C;
|
||||
}
|
||||
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<int>::of(&QButtonGroup::buttonClicked), [=](int id) {
|
||||
params.put(key, std::to_string(id));
|
||||
});
|
||||
}
|
||||
|
||||
void setEnabled(bool enable) {
|
||||
for (auto btn : button_group->buttons()) {
|
||||
btn->setEnabled(enable);
|
||||
}
|
||||
}
|
||||
|
||||
void setCheckedButton(int id) {
|
||||
button_group->button(id)->setChecked(true);
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
int value = atoi(params.get(key).c_str());
|
||||
button_group->button(value)->setChecked(true);
|
||||
}
|
||||
|
||||
void showEvent(QShowEvent *event) override {
|
||||
refresh();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#include "selfdrive/ui/qt/widgets/drive_stats.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QGridLayout>
|
||||
#include <QJsonObject>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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, 25, 50, 20);
|
||||
|
||||
auto add_stats_layouts = [=](const QString &title, StatsLabels& labels, bool FrogPilot=false) {
|
||||
QGridLayout* grid_layout = new QGridLayout;
|
||||
grid_layout->setVerticalSpacing(10);
|
||||
grid_layout->setContentsMargins(0, 10, 0, 10);
|
||||
|
||||
int row = 0;
|
||||
grid_layout->addWidget(newLabel(title, FrogPilot ? "frogpilot_title" : "title"), row++, 0, 1, 3);
|
||||
grid_layout->addItem(new QSpacerItem(0, 10), 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);
|
||||
main_layout->addStretch(1);
|
||||
};
|
||||
|
||||
add_stats_layouts(tr("ALL TIME"), all_);
|
||||
add_stats_layouts(tr("PAST WEEK"), week_);
|
||||
add_stats_layouts(tr("FROGPILOT"), frogPilot_, true);
|
||||
|
||||
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: 50px; font-weight: 500; }
|
||||
QLabel[type="frogpilot_title"] { font-size: 50px; font-weight: 500; color: #178643; }
|
||||
QLabel[type="number"] { font-size: 65px; font-weight: 400; }
|
||||
QLabel[type="unit"] { font-size: 50px; font-weight: 300; color: #A0A0A0; }
|
||||
)");
|
||||
}
|
||||
|
||||
void DriveStats::updateStats() {
|
||||
QJsonObject json = stats_.object();
|
||||
|
||||
auto updateFrogPilot = [this](const QJsonObject& obj, StatsLabels& labels) {
|
||||
labels.routes->setText(QString::number(paramsStorage.getInt("FrogPilotDrives")));
|
||||
labels.distance->setText(QString::number(int(paramsStorage.getFloat("FrogPilotKilometers") * (metric_ ? 1 : KM_TO_MILE))));
|
||||
labels.distance_unit->setText(getDistanceUnit());
|
||||
labels.hours->setText(QString::number(int(paramsStorage.getFloat("FrogPilotMinutes") / 60)));
|
||||
};
|
||||
|
||||
updateFrogPilot(json["frogpilot"].toObject(), frogPilot_);
|
||||
|
||||
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)));
|
||||
};
|
||||
|
||||
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) {
|
||||
metric_ = params.getBool("IsMetric");
|
||||
updateStats();
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QLabel>
|
||||
|
||||
#include "common/params.h"
|
||||
|
||||
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_;
|
||||
Params params;
|
||||
Params paramsStorage{"/persist/params"};
|
||||
QJsonDocument stats_;
|
||||
struct StatsLabels {
|
||||
QLabel *routes, *distance, *distance_unit, *hours;
|
||||
} all_, week_, frogPilot_;
|
||||
|
||||
private slots:
|
||||
void parseResponse(const QString &response, bool success);
|
||||
};
|
||||
Executable
+336
@@ -0,0 +1,336 @@
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QButtonGroup>
|
||||
|
||||
#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 "";
|
||||
}
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#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;
|
||||
};
|
||||
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#include "selfdrive/ui/qt/widgets/keyboard.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMap>
|
||||
#include <QTouchEvent>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
const QString BACKSPACE_KEY = "⌫";
|
||||
const QString ENTER_KEY = "→";
|
||||
|
||||
const QMap<QString, int> 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<QTouchEvent *>(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<QVector<QString>>& 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<QVector<QString>> 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<QVector<QString>> 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<QVector<QString>> 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<QVector<QString>> 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);
|
||||
}
|
||||
}
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QFrame>
|
||||
#include <QPushButton>
|
||||
#include <QStackedLayout>
|
||||
|
||||
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<QVector<QString>>& 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();
|
||||
};
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#include "selfdrive/ui/qt/widgets/offroad_alerts.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#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, [=]() {
|
||||
params.putBool("SnoozeUpdate", true);
|
||||
params.putBool("DeviceManagement", true);
|
||||
params.putBool("OfflineMode", true);
|
||||
});
|
||||
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<std::pair<std::string, int>> 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;
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#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<std::string, QLabel*> alerts;
|
||||
};
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
|
||||
#include <QScrollBar>
|
||||
#include <QScroller>
|
||||
|
||||
// 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::OvershootPolicy>(QScrollerProperties::OvershootAlwaysOff));
|
||||
sp.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue<QScrollerProperties::OvershootPolicy>(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);
|
||||
}
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <QScrollArea>
|
||||
|
||||
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;
|
||||
};
|
||||
Executable
+64
@@ -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");
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
#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);
|
||||
};
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#include "selfdrive/ui/qt/widgets/toggle.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
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(0x33ab4c);
|
||||
} else {
|
||||
circleColor.setRgb(0x888888);
|
||||
green.setRgb(0x227722);
|
||||
}
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QMouseEvent>
|
||||
#include <QPropertyAnimation>
|
||||
|
||||
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);
|
||||
};
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#include "selfdrive/ui/qt/widgets/wifi.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
|
||||
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);
|
||||
|
||||
QWidget *notUploading = new QWidget;
|
||||
QVBoxLayout *not_uploading_layout = new QVBoxLayout(notUploading);
|
||||
not_uploading_layout->setContentsMargins(64, 56, 64, 56);
|
||||
not_uploading_layout->setSpacing(36);
|
||||
{
|
||||
QHBoxLayout *title_layout = new QHBoxLayout;
|
||||
{
|
||||
QLabel *title = new QLabel(tr("Uploading disabled"));
|
||||
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("../frogpilot/assets/other_images/icon_wifi_uploading_disabled.svg");
|
||||
icon->setPixmap(pixmap.scaledToWidth(120, Qt::SmoothTransformation));
|
||||
title_layout->addWidget(icon);
|
||||
}
|
||||
not_uploading_layout->addLayout(title_layout);
|
||||
|
||||
QLabel *desc = new QLabel(tr("Toggle off the 'Disable Uploading' toggle to enable uploads."));
|
||||
desc->setStyleSheet("font-size: 48px; font-weight: 400;");
|
||||
desc->setWordWrap(true);
|
||||
not_uploading_layout->addWidget(desc);
|
||||
}
|
||||
stack->addWidget(notUploading);
|
||||
|
||||
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;
|
||||
bool uploading_disabled = params.getBool("DeviceManagement") && params.getBool("NoUploads");
|
||||
stack->setCurrentIndex(uploading_disabled ? 2 : uploading ? 1 : 0);
|
||||
}
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QStackedLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#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);
|
||||
|
||||
private:
|
||||
// FrogPilot variables
|
||||
Params params;
|
||||
|
||||
protected:
|
||||
QStackedLayout *stack;
|
||||
};
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#include "selfdrive/ui/qt/window.h"
|
||||
|
||||
#include <QFontDatabase>
|
||||
|
||||
#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);
|
||||
|
||||
settingsWindow = new SettingsWindow(this);
|
||||
main_layout->addWidget(settingsWindow);
|
||||
QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings);
|
||||
QObject::connect(settingsWindow, &SettingsWindow::reviewTrainingGuide, [=]() {
|
||||
onboardingWindow->showTrainingGuide();
|
||||
main_layout->setCurrentWidget(onboardingWindow);
|
||||
});
|
||||
QObject::connect(settingsWindow, &SettingsWindow::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() == settingsWindow) {
|
||||
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(settingsWindow);
|
||||
settingsWindow->setCurrentPanel(index, param);
|
||||
}
|
||||
|
||||
void MainWindow::closeSettings() {
|
||||
main_layout->setCurrentWidget(homeWindow);
|
||||
|
||||
if (uiState()->scene.started) {
|
||||
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();
|
||||
device()->resetInteractiveTimeout(uiState()->scene.screen_timeout, uiState()->scene.screen_timeout_onroad);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ignore;
|
||||
}
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <QStackedLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/qt/home.h"
|
||||
#include "selfdrive/ui/qt/offroad/onboarding.h"
|
||||
#include "selfdrive/ui/qt/offroad/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;
|
||||
SettingsWindow *settingsWindow;
|
||||
OnboardingWindow *onboardingWindow;
|
||||
|
||||
// FrogPilot variables
|
||||
Params params;
|
||||
};
|
||||
Executable
+275
@@ -0,0 +1,275 @@
|
||||
import math
|
||||
import numpy as np
|
||||
import os
|
||||
import time
|
||||
import wave
|
||||
|
||||
|
||||
from cereal import car, messaging
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.common.retry import retry
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
from openpilot.system import micd
|
||||
|
||||
SAMPLE_RATE = 48000
|
||||
SAMPLE_BUFFER = 4096 # (approx 100ms)
|
||||
MAX_VOLUME = 1.0
|
||||
MIN_VOLUME = 0.1
|
||||
CONTROLS_TIMEOUT = 5 # 5 seconds
|
||||
FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES)
|
||||
|
||||
AMBIENT_DB = 30 # DB where MIN_VOLUME is applied
|
||||
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
|
||||
sound_list: dict[int, tuple[str, int | None, float]] = {
|
||||
# AudibleAlert, file name, play count (none for infinite)
|
||||
AudibleAlert.engage: ("engage.wav", 1, MAX_VOLUME),
|
||||
AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME),
|
||||
AudibleAlert.refuse: ("refuse.wav", 1, MAX_VOLUME),
|
||||
|
||||
AudibleAlert.prompt: ("prompt.wav", 1, MAX_VOLUME),
|
||||
AudibleAlert.promptRepeat: ("prompt.wav", None, MAX_VOLUME),
|
||||
AudibleAlert.promptDistracted: ("prompt_distracted.wav", None, MAX_VOLUME),
|
||||
|
||||
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.doc: ("doc.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),
|
||||
}
|
||||
|
||||
def check_controls_timeout_alert(sm):
|
||||
controls_missing = time.monotonic() - sm.recv_time['controlsState']
|
||||
|
||||
if controls_missing > CONTROLS_TIMEOUT:
|
||||
if sm['controlsState'].enabled and (controls_missing - CONTROLS_TIMEOUT) < 10:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class Soundd:
|
||||
def __init__(self):
|
||||
# FrogPilot variables
|
||||
self.params = Params()
|
||||
self.params_memory = Params("/dev/shm/params")
|
||||
|
||||
self.previous_sound_directory = None
|
||||
self.random_events_directory = BASEDIR + "/selfdrive/frogpilot/assets/random_events/sounds/"
|
||||
|
||||
self.random_events_map = {
|
||||
AudibleAlert.angry: MAX_VOLUME,
|
||||
AudibleAlert.doc: 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.current_alert = AudibleAlert.none
|
||||
self.current_volume = MIN_VOLUME
|
||||
self.current_sound_frame = 0
|
||||
|
||||
self.controls_timeout_alert = False
|
||||
|
||||
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
|
||||
|
||||
def load_sounds(self):
|
||||
self.loaded_sounds: dict[int, np.ndarray] = {}
|
||||
|
||||
# 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')
|
||||
|
||||
assert wavefile.getnchannels() == 1
|
||||
assert wavefile.getsampwidth() == 2
|
||||
assert wavefile.getframerate() == SAMPLE_RATE
|
||||
|
||||
length = wavefile.getnframes()
|
||||
self.loaded_sounds[sound] = np.frombuffer(wavefile.readframes(length), dtype=np.int16).astype(np.float32) / (2**16/2)
|
||||
|
||||
def get_sound_data(self, frames): # get "frames" worth of data from the current alert sound, looping when required
|
||||
|
||||
ret = np.zeros(frames, dtype=np.float32)
|
||||
|
||||
if self.current_alert != AudibleAlert.none:
|
||||
num_loops = sound_list[self.current_alert][1]
|
||||
sound_data = self.loaded_sounds[self.current_alert]
|
||||
written_frames = 0
|
||||
|
||||
current_sound_frame = self.current_sound_frame % len(sound_data)
|
||||
loops = self.current_sound_frame // len(sound_data)
|
||||
|
||||
while written_frames < frames and (num_loops is None or loops < num_loops):
|
||||
available_frames = sound_data.shape[0] - current_sound_frame
|
||||
frames_to_write = min(available_frames, frames - written_frames)
|
||||
ret[written_frames:written_frames+frames_to_write] = sound_data[current_sound_frame:current_sound_frame+frames_to_write]
|
||||
written_frames += frames_to_write
|
||||
self.current_sound_frame += frames_to_write
|
||||
|
||||
return ret * self.current_volume
|
||||
|
||||
def callback(self, data_out: np.ndarray, frames: int, time, status) -> None:
|
||||
if status:
|
||||
cloudlog.warning(f"soundd stream over/underflow: {status}")
|
||||
data_out[:frames, 0] = self.get_sound_data(frames)
|
||||
|
||||
def update_alert(self, new_alert):
|
||||
current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame > len(self.loaded_sounds[self.current_alert])
|
||||
if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once):
|
||||
self.current_alert = new_alert
|
||||
self.current_sound_frame = 0
|
||||
|
||||
def get_audible_alert(self, sm):
|
||||
if sm.updated['controlsState']:
|
||||
new_alert = sm['controlsState'].alertSound.raw
|
||||
self.update_alert(new_alert)
|
||||
elif check_controls_timeout_alert(sm):
|
||||
self.update_alert(AudibleAlert.warningImmediate)
|
||||
self.controls_timeout_alert = True
|
||||
elif self.controls_timeout_alert:
|
||||
self.update_alert(AudibleAlert.none)
|
||||
self.controls_timeout_alert = False
|
||||
|
||||
def calculate_volume(self, weighted_db):
|
||||
volume = ((weighted_db - AMBIENT_DB) / DB_SCALE) * (MAX_VOLUME - MIN_VOLUME) + MIN_VOLUME
|
||||
return math.pow(10, (np.clip(volume, MIN_VOLUME, MAX_VOLUME) - 1))
|
||||
|
||||
@retry(attempts=7, delay=3)
|
||||
def get_stream(self, sd):
|
||||
# reload sounddevice to reinitialize portaudio
|
||||
sd._terminate()
|
||||
sd._initialize()
|
||||
return sd.OutputStream(channels=1, samplerate=SAMPLE_RATE, callback=self.callback, blocksize=SAMPLE_BUFFER)
|
||||
|
||||
def soundd_thread(self):
|
||||
# sounddevice must be imported after forking processes
|
||||
import sounddevice as sd
|
||||
|
||||
sm = messaging.SubMaster(['controlsState', 'microphone'])
|
||||
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
elif self.alert_volume_control and self.current_alert in self.volume_map:
|
||||
self.current_volume = self.volume_map[self.current_alert] / 100.0
|
||||
|
||||
elif self.current_alert in self.random_events_map:
|
||||
self.current_volume = self.random_events_map[self.current_alert]
|
||||
|
||||
self.get_audible_alert(sm)
|
||||
|
||||
rk.keep_time()
|
||||
|
||||
assert stream.active
|
||||
|
||||
# Update FrogPilot parameters
|
||||
if self.params_memory.get_bool("FrogPilotTogglesUpdated"):
|
||||
self.update_frogpilot_params()
|
||||
|
||||
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"),
|
||||
}
|
||||
|
||||
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 = {
|
||||
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",
|
||||
}
|
||||
|
||||
# Clearpilot: Impl theme switcher
|
||||
# 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/clearpilot/theme/" + theme_name + "/sounds/" if custom_sounds != 0 else "/selfdrive/assets/sounds/")
|
||||
|
||||
self.sound_directory = BASEDIR + ("/selfdrive/clearpilot/theme/clearpilot/sounds/")
|
||||
|
||||
if self.sound_directory != self.previous_sound_directory:
|
||||
self.load_sounds()
|
||||
|
||||
self.previous_sound_directory = self.sound_directory
|
||||
|
||||
def main():
|
||||
s = Soundd()
|
||||
s.soundd_thread()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -f /TICI ] && [ ! -f qt/spinner ]; then
|
||||
cp qt/spinner_larch64 qt/spinner
|
||||
fi
|
||||
|
||||
exec ./qt/spinner "$1"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
test
|
||||
playsound
|
||||
test_sound
|
||||
test_translations
|
||||
ui_snapshot
|
||||
test_ui/report
|
||||
Executable
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
import cereal.messaging as messaging
|
||||
|
||||
if __name__ == "__main__":
|
||||
while True:
|
||||
pm = messaging.PubMaster(['carParams', 'carState'])
|
||||
batt = 1.
|
||||
while True:
|
||||
msg = messaging.new_message('carParams')
|
||||
msg.carParams.carName = "COMMA BODY"
|
||||
msg.carParams.notCar = True
|
||||
pm.send('carParams', msg)
|
||||
|
||||
for b in range(100, 0, -1):
|
||||
msg = messaging.new_message('carState')
|
||||
msg.carState.charging = True
|
||||
msg.carState.fuelGauge = b / 100.
|
||||
pm.send('carState', msg)
|
||||
time.sleep(0.1)
|
||||
|
||||
time.sleep(1)
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
UI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"/..
|
||||
TEST_TEXT="(WRAPPED_SOURCE_TEXT)"
|
||||
TEST_TS_FILE=$UI_DIR/translations/main_test_en.ts
|
||||
TEST_QM_FILE=$UI_DIR/translations/main_test_en.qm
|
||||
|
||||
# translation strings
|
||||
UNFINISHED="<translation type=\"unfinished\"><\/translation>"
|
||||
TRANSLATED="<translation>$TEST_TEXT<\/translation>"
|
||||
|
||||
mkdir -p $UI_DIR/translations
|
||||
rm -f $TEST_TS_FILE $TEST_QM_FILE
|
||||
lupdate -recursive "$UI_DIR" -ts $TEST_TS_FILE
|
||||
sed -i "s/$UNFINISHED/$TRANSLATED/" $TEST_TS_FILE
|
||||
lrelease $TEST_TS_FILE
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.controls.lib.alertmanager import set_offroad_alert
|
||||
|
||||
if __name__ == "__main__":
|
||||
params = Params()
|
||||
|
||||
with open(os.path.join(BASEDIR, "selfdrive/controls/lib/alerts_offroad.json")) as f:
|
||||
offroad_alerts = json.load(f)
|
||||
|
||||
t = 10 if len(sys.argv) < 2 else int(sys.argv[1])
|
||||
while True:
|
||||
print("setting alert update")
|
||||
params.put_bool("UpdateAvailable", True)
|
||||
r = open(os.path.join(BASEDIR, "RELEASES.md")).read()
|
||||
r = r[:r.find('\n\n')] # Slice latest release notes
|
||||
params.put("UpdaterNewReleaseNotes", r + "\n")
|
||||
|
||||
time.sleep(t)
|
||||
params.put_bool("UpdateAvailable", False)
|
||||
|
||||
# cycle through normal alerts
|
||||
for a in offroad_alerts:
|
||||
print("setting alert:", a)
|
||||
set_offroad_alert(a, True)
|
||||
time.sleep(t)
|
||||
set_offroad_alert(a, False)
|
||||
|
||||
print("no alert")
|
||||
time.sleep(t)
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#include <QApplication>
|
||||
#include <QSoundEffect>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
QApplication a(argc, argv);
|
||||
|
||||
QTimer::singleShot(0, [=]{
|
||||
QSoundEffect s;
|
||||
const char *vol = getenv("VOLUME");
|
||||
s.setVolume(vol ? atof(vol) : 1.0);
|
||||
for (int i = 1; i < argc; i++) {
|
||||
QString fn = argv[i];
|
||||
qDebug() << "playing" << fn;
|
||||
|
||||
QEventLoop loop;
|
||||
s.setSource(QUrl::fromLocalFile(fn));
|
||||
QEventLoop::connect(&s, &QSoundEffect::loadedChanged, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
s.play();
|
||||
QEventLoop::connect(&s, &QSoundEffect::playingChanged, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
QCoreApplication::exit();
|
||||
});
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#define CATCH_CONFIG_RUNNER
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QTranslator>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
// unit tests for Qt
|
||||
QApplication app(argc, argv);
|
||||
|
||||
QString language_file = "main_test_en";
|
||||
qDebug() << "Loading language:" << language_file;
|
||||
|
||||
QTranslator translator;
|
||||
QString translationsPath = QDir::cleanPath(qApp->applicationDirPath() + "/../translations");
|
||||
if (!translator.load(language_file, translationsPath)) {
|
||||
qDebug() << "Failed to load translation file!";
|
||||
}
|
||||
app.installTranslator(&translator);
|
||||
|
||||
const int res = Catch::Session().run(argc, argv);
|
||||
return (res < 0xff ? res : 0xff);
|
||||
}
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
import unittest
|
||||
|
||||
from cereal import car
|
||||
from cereal import messaging
|
||||
from cereal.messaging import SubMaster, PubMaster
|
||||
from openpilot.selfdrive.ui.soundd import CONTROLS_TIMEOUT, check_controls_timeout_alert
|
||||
|
||||
import time
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
|
||||
class TestSoundd(unittest.TestCase):
|
||||
def test_check_controls_timeout_alert(self):
|
||||
sm = SubMaster(['controlsState'])
|
||||
pm = PubMaster(['controlsState'])
|
||||
|
||||
for _ in range(100):
|
||||
cs = messaging.new_message('controlsState')
|
||||
cs.controlsState.enabled = True
|
||||
|
||||
pm.send("controlsState", cs)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
sm.update(0)
|
||||
|
||||
self.assertFalse(check_controls_timeout_alert(sm))
|
||||
|
||||
for _ in range(CONTROLS_TIMEOUT * 110):
|
||||
sm.update(0)
|
||||
time.sleep(0.01)
|
||||
|
||||
self.assertTrue(check_controls_timeout_alert(sm))
|
||||
|
||||
# TODO: add test with micd for checking that soundd actually outputs sounds
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/window.h"
|
||||
|
||||
const QString TEST_TEXT = "(WRAPPED_SOURCE_TEXT)"; // what each string should be translated to
|
||||
QRegExp RE_NUM("\\d*");
|
||||
|
||||
QStringList getParentWidgets(QWidget* widget){
|
||||
QStringList parentWidgets;
|
||||
while (widget->parentWidget() != Q_NULLPTR) {
|
||||
widget = widget->parentWidget();
|
||||
parentWidgets.append(widget->metaObject()->className());
|
||||
}
|
||||
return parentWidgets;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void checkWidgetTrWrap(MainWindow &w) {
|
||||
for (auto widget : w.findChildren<T>()) {
|
||||
const QString text = widget->text();
|
||||
bool isNumber = RE_NUM.exactMatch(text);
|
||||
bool wrapped = text.contains(TEST_TEXT);
|
||||
QString parentWidgets = getParentWidgets(widget).join("->");
|
||||
|
||||
if (!text.isEmpty() && !isNumber && !wrapped) {
|
||||
FAIL(("\"" + text + "\" must be wrapped. Parent widgets: " + parentWidgets).toStdString());
|
||||
}
|
||||
|
||||
// warn if source string wrapped, but UI adds text
|
||||
// TODO: add way to ignore this
|
||||
if (wrapped && text != TEST_TEXT) {
|
||||
WARN(("\"" + text + "\" is dynamic and needs a custom retranslate function. Parent widgets: " + parentWidgets).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests all strings in the UI are wrapped with tr()
|
||||
TEST_CASE("UI: test all strings wrapped") {
|
||||
Params().remove("LanguageSetting");
|
||||
Params().remove("HardwareSerial");
|
||||
Params().remove("DongleId");
|
||||
qputenv("TICI", "1");
|
||||
|
||||
MainWindow w;
|
||||
checkWidgetTrWrap<QPushButton*>(w);
|
||||
checkWidgetTrWrap<QLabel*>(w);
|
||||
}
|
||||
Executable
+134
@@ -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) as f:
|
||||
translation_files = json.load(f)
|
||||
|
||||
UNFINISHED_TRANSLATION_TAG = "<translation type=\"unfinished\"" # non-empty translations can be marked unfinished
|
||||
LOCATION_TAG = "<location "
|
||||
FORMAT_ARG = re.compile("%[0-9]+")
|
||||
|
||||
|
||||
@parameterized_class(("name", "file"), translation_files.items())
|
||||
class TestTranslations(unittest.TestCase):
|
||||
name: str
|
||||
file: str
|
||||
|
||||
@staticmethod
|
||||
def _read_translation_file(path, file):
|
||||
tr_file = os.path.join(path, f"{file}.ts")
|
||||
with open(tr_file) as f:
|
||||
return f.read()
|
||||
|
||||
def test_missing_translation_files(self):
|
||||
self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")),
|
||||
f"{self.name} has no XML translation file, run selfdrive/ui/update_translations.py")
|
||||
|
||||
def test_translations_updated(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
shutil.copytree(TRANSLATIONS_DIR, tmpdir, dirs_exist_ok=True)
|
||||
update_translations(translation_files=[self.file], translations_dir=tmpdir)
|
||||
|
||||
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
|
||||
new_translations = self._read_translation_file(tmpdir, self.file)
|
||||
self.assertEqual(cur_translations, new_translations,
|
||||
f"{self.file} ({self.name}) XML translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files")
|
||||
|
||||
@unittest.skip("Only test unfinished translations before going to release")
|
||||
def test_unfinished_translations(self):
|
||||
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
|
||||
self.assertTrue(UNFINISHED_TRANSLATION_TAG not in cur_translations,
|
||||
f"{self.file} ({self.name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist")
|
||||
|
||||
def test_vanished_translations(self):
|
||||
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
|
||||
self.assertTrue("<translation type=\"vanished\">" 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), f"Plural translations must use %n, not %1, %2, etc.: {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()
|
||||
Executable
+191
@@ -0,0 +1,191 @@
|
||||
from collections import namedtuple
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
import jinja2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import os
|
||||
import pywinctl
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
from cereal import messaging, car, log
|
||||
from cereal.visionipc import VisionIpcServer, VisionStreamType
|
||||
|
||||
from cereal.messaging import SubMaster, PubMaster
|
||||
from openpilot.common.mock import mock_messages
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.selfdrive.test.helpers import with_processes
|
||||
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state
|
||||
from openpilot.tools.webcam.camera import Camera
|
||||
|
||||
UI_DELAY = 0.5 # may be slower on CI?
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
NetworkStrength = log.DeviceState.NetworkStrength
|
||||
|
||||
EventName = car.CarEvent.EventName
|
||||
EVENTS_BY_NAME = {v: k for k, v in EventName.schema.enumerants.items()}
|
||||
|
||||
|
||||
def setup_common(click, pm: PubMaster):
|
||||
Params().put("DongleId", "123456789012345")
|
||||
dat = messaging.new_message('deviceState')
|
||||
dat.deviceState.started = True
|
||||
dat.deviceState.networkType = NetworkType.cell4G
|
||||
dat.deviceState.networkStrength = NetworkStrength.moderate
|
||||
dat.deviceState.freeSpacePercent = 80
|
||||
dat.deviceState.memoryUsagePercent = 2
|
||||
dat.deviceState.cpuTempC = [2,]*3
|
||||
dat.deviceState.gpuTempC = [2,]*3
|
||||
dat.deviceState.cpuUsagePercent = [2,]*8
|
||||
|
||||
pm.send("deviceState", dat)
|
||||
|
||||
def setup_homescreen(click, pm: PubMaster):
|
||||
setup_common(click, pm)
|
||||
|
||||
def setup_settings_device(click, pm: PubMaster):
|
||||
setup_common(click, pm)
|
||||
|
||||
click(100, 100)
|
||||
|
||||
def setup_settings_network(click, pm: PubMaster):
|
||||
setup_common(click, pm)
|
||||
|
||||
setup_settings_device(click, pm)
|
||||
click(300, 600)
|
||||
|
||||
def setup_onroad(click, pm: PubMaster):
|
||||
setup_common(click, pm)
|
||||
|
||||
dat = messaging.new_message('pandaStates', 1)
|
||||
dat.pandaStates[0].ignitionLine = True
|
||||
dat.pandaStates[0].pandaType = log.PandaState.PandaType.uno
|
||||
|
||||
pm.send("pandaStates", dat)
|
||||
|
||||
d = DEVICE_CAMERAS[("tici", "ar0231")]
|
||||
server = VisionIpcServer("camerad")
|
||||
server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, False, d.fcam.width, d.fcam.height)
|
||||
server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, False, d.dcam.width, d.dcam.height)
|
||||
server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, False, d.fcam.width, d.fcam.height)
|
||||
server.start_listener()
|
||||
|
||||
time.sleep(0.5) # give time for vipc server to start
|
||||
|
||||
IMG = Camera.bgr2nv12(np.random.randint(0, 255, (d.fcam.width, d.fcam.height, 3), dtype=np.uint8))
|
||||
IMG_BYTES = IMG.flatten().tobytes()
|
||||
|
||||
cams = ('roadCameraState', 'wideRoadCameraState')
|
||||
|
||||
frame_id = 0
|
||||
for cam in cams:
|
||||
msg = messaging.new_message(cam)
|
||||
cs = getattr(msg, cam)
|
||||
cs.frameId = frame_id
|
||||
cs.timestampSof = int((frame_id * DT_MDL) * 1e9)
|
||||
cs.timestampEof = int((frame_id * DT_MDL) * 1e9)
|
||||
cam_meta = meta_from_camera_state(cam)
|
||||
|
||||
pm.send(msg.which(), msg)
|
||||
server.send(cam_meta.stream, IMG_BYTES, cs.frameId, cs.timestampSof, cs.timestampEof)
|
||||
|
||||
@mock_messages(['liveLocationKalman'])
|
||||
|
||||
|
||||
def setup_onroad_sidebar(click, pm: PubMaster):
|
||||
click(500, 500)
|
||||
|
||||
CASES = {
|
||||
"homescreen": setup_homescreen,
|
||||
"settings_device": setup_settings_device,
|
||||
"settings_network": setup_settings_network,
|
||||
"onroad": setup_onroad,
|
||||
"onroad_sidebar": setup_onroad_sidebar
|
||||
}
|
||||
|
||||
TEST_DIR = pathlib.Path(__file__).parent
|
||||
|
||||
TEST_OUTPUT_DIR = TEST_DIR / "report"
|
||||
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots"
|
||||
|
||||
|
||||
class TestUI(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
os.environ["SCALE"] = "1"
|
||||
sys.modules["mouseinfo"] = False
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
del sys.modules["mouseinfo"]
|
||||
|
||||
def setup(self):
|
||||
self.sm = SubMaster(["uiDebug"])
|
||||
self.pm = PubMaster(["deviceState", "pandaStates", "controlsState", 'roadCameraState', 'wideRoadCameraState', 'liveLocationKalman'])
|
||||
while not self.sm.valid["uiDebug"]:
|
||||
self.sm.update(1)
|
||||
time.sleep(UI_DELAY) # wait a bit more for the UI to start rendering
|
||||
try:
|
||||
self.ui = pywinctl.getWindowsWithTitle("ui")[0]
|
||||
except Exception as e:
|
||||
print(f"failed to find ui window, assuming that it's in the top left (for Xvfb) {e}")
|
||||
self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0,0,2160,1080)
|
||||
|
||||
def screenshot(self):
|
||||
import pyautogui
|
||||
im = pyautogui.screenshot(region=(self.ui.left, self.ui.top, self.ui.width, self.ui.height))
|
||||
self.assertEqual(im.width, 2160)
|
||||
self.assertEqual(im.height, 1080)
|
||||
img = np.array(im)
|
||||
im.close()
|
||||
return img
|
||||
|
||||
def click(self, x, y, *args, **kwargs):
|
||||
import pyautogui
|
||||
pyautogui.click(self.ui.left + x, self.ui.top + y, *args, **kwargs)
|
||||
time.sleep(UI_DELAY) # give enough time for the UI to react
|
||||
|
||||
@parameterized.expand(CASES.items())
|
||||
@with_processes(["ui"])
|
||||
def test_ui(self, name, setup_case):
|
||||
self.setup()
|
||||
|
||||
setup_case(self.click, self.pm)
|
||||
|
||||
time.sleep(UI_DELAY) # wait a bit more for the UI to finish rendering
|
||||
|
||||
im = self.screenshot()
|
||||
plt.imsave(SCREENSHOTS_DIR / f"{name}.png", im)
|
||||
|
||||
|
||||
def create_html_report():
|
||||
OUTPUT_FILE = TEST_OUTPUT_DIR / "index.html"
|
||||
|
||||
with open(TEST_DIR / "template.html") as f:
|
||||
template = jinja2.Template(f.read())
|
||||
|
||||
cases = {f.stem: (str(f.relative_to(TEST_OUTPUT_DIR)), "reference.png") for f in SCREENSHOTS_DIR.glob("*.png")}
|
||||
cases = dict(sorted(cases.items()))
|
||||
|
||||
with open(OUTPUT_FILE, "w") as f:
|
||||
f.write(template.render(cases=cases))
|
||||
|
||||
def create_screenshots():
|
||||
if TEST_OUTPUT_DIR.exists():
|
||||
shutil.rmtree(TEST_OUTPUT_DIR)
|
||||
|
||||
SCREENSHOTS_DIR.mkdir(parents=True)
|
||||
unittest.main(exit=False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("creating test screenshots")
|
||||
create_screenshots()
|
||||
|
||||
print("creating html report")
|
||||
create_html_report()
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
<html>
|
||||
|
||||
<style>
|
||||
.column {
|
||||
float: left;
|
||||
width: 50%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.row::after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{% for name, (image, ref_image) in cases.items() %}
|
||||
|
||||
<h1>{{name}}</h1>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<img class="image" src="{{ image }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
{% endfor %}
|
||||
</html>
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#include "selfdrive/ui/tests/ui_snapshot.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QDir>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
|
||||
#include "selfdrive/ui/qt/home.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/window.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
void saveWidgetAsImage(QWidget *widget, const QString &fileName) {
|
||||
QImage image(widget->size(), QImage::Format_ARGB32);
|
||||
QPainter painter(&image);
|
||||
widget->render(&painter);
|
||||
image.save(fileName);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
initApp(argc, argv);
|
||||
|
||||
QApplication app(argc, argv);
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription("Take a snapshot of the UI.");
|
||||
parser.addHelpOption();
|
||||
parser.addOption(QCommandLineOption(QStringList() << "o"
|
||||
<< "output",
|
||||
"Output image file path. The file's suffix is used to "
|
||||
"determine the format. Supports PNG and JPEG formats. "
|
||||
"Defaults to \"snapshot.png\".",
|
||||
"file", "snapshot.png"));
|
||||
parser.process(app);
|
||||
|
||||
const QString output = parser.value("output");
|
||||
if (output.isEmpty()) {
|
||||
qCritical() << "No output file specified";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto current = QDir::current();
|
||||
|
||||
// change working directory to find assets
|
||||
if (!QDir::setCurrent(QCoreApplication::applicationDirPath() + QDir::separator() + "..")) {
|
||||
qCritical() << "Failed to set current directory";
|
||||
return 1;
|
||||
}
|
||||
|
||||
MainWindow w;
|
||||
w.setFixedSize(2160, 1080);
|
||||
w.show();
|
||||
app.installEventFilter(&w);
|
||||
|
||||
// restore working directory
|
||||
QDir::setCurrent(current.absolutePath());
|
||||
|
||||
// wait for the UI to update
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, [&](const UIState &s) {
|
||||
saveWidgetAsImage(&w, output);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
void saveWidgetAsImage(QWidget *widget, const QString &fileName);
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -f /TICI ] && [ ! -f _text ]; then
|
||||
cp qt/text_larch64 _text
|
||||
fi
|
||||
|
||||
exec ./_text "$1"
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
# Multilanguage
|
||||
|
||||
[](#)
|
||||
|
||||
## Contributing
|
||||
|
||||
Before getting started, make sure you have set up the openpilot Ubuntu development environment by reading the [tools README.md](/tools/README.md).
|
||||
|
||||
### Policy
|
||||
|
||||
Most of the languages supported by openpilot come from and are maintained by the community via pull requests. A pull request likely to be merged is one that [fixes a translation or adds missing translations.](https://github.com/commaai/openpilot/blob/master/selfdrive/ui/translations/README.md#improving-an-existing-language)
|
||||
|
||||
We also generally merge pull requests adding support for a new language if there are community members willing to maintain it. Maintaining a language is ensuring quality and completion of translations before each openpilot release.
|
||||
|
||||
comma may remove or hide language support from releases depending on translation quality and completeness.
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
openpilot provides a few tools to help contributors manage their translations and to ensure quality. To get started:
|
||||
|
||||
1. Add your new language to [languages.json](/selfdrive/ui/translations/languages.json) with the appropriate [language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and the localized language name (Traditional Chinese is `中文(繁體)`).
|
||||
2. Generate the XML translation file (`*.ts`):
|
||||
```shell
|
||||
selfdrive/ui/update_translations.py
|
||||
```
|
||||
3. Edit the translation file, marking each translation as completed:
|
||||
```shell
|
||||
linguist selfdrive/ui/translations/your_language_file.ts
|
||||
```
|
||||
4. View your finished translations by compiling and starting the UI, then find it in the language selector:
|
||||
```shell
|
||||
scons -j$(nproc) selfdrive/ui && selfdrive/ui/ui
|
||||
```
|
||||
5. Read [Checking the UI](#checking-the-ui) to double-check your translations fit in the UI.
|
||||
|
||||
### Improving an Existing Language
|
||||
|
||||
Follow step 3. above, you can review existing translations and add missing ones. Once you're done, just open a pull request to openpilot.
|
||||
|
||||
### Checking the UI
|
||||
Different languages use varying space to convey the same message, so it's a good idea to double-check that your translations do not overlap and fit into each widget. Start the UI (step 4. above) and view each page, making adjustments to translations as needed.
|
||||
|
||||
#### To view offroad alerts:
|
||||
|
||||
With the UI started, you can view the offroad alerts with:
|
||||
```shell
|
||||
selfdrive/ui/tests/cycle_offroad_alerts.py
|
||||
```
|
||||
|
||||
### Updating the UI
|
||||
|
||||
Any time you edit source code in the UI, you need to update the translations to ensure the line numbers and contexts are up to date (first step above).
|
||||
|
||||
### Testing
|
||||
|
||||
openpilot has a few unit tests to make sure all translations are up-to-date and that all strings are wrapped in a translation marker. They are run in CI, but you can also run them locally.
|
||||
|
||||
Tests translation files up to date:
|
||||
|
||||
```shell
|
||||
selfdrive/ui/tests/test_translations.py
|
||||
```
|
||||
|
||||
Tests all static source strings are wrapped:
|
||||
|
||||
```shell
|
||||
selfdrive/ui/tests/create_test_translations.sh && selfdrive/ui/tests/test_translations
|
||||
```
|
||||
|
||||
---
|
||||

|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import cast
|
||||
|
||||
import requests
|
||||
|
||||
TRANSLATIONS_DIR = pathlib.Path(__file__).resolve().parent
|
||||
TRANSLATIONS_LANGUAGES = TRANSLATIONS_DIR / "languages.json"
|
||||
|
||||
OPENAI_MODEL = "gpt-4"
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
||||
OPENAI_PROMPT = "You are a professional translator from English to {language} (ISO 639 language code). " + \
|
||||
"The following sentence or word is in the GUI of a software called openpilot, translate it accordingly."
|
||||
|
||||
|
||||
def get_language_files(languages: list[str] = None) -> dict[str, pathlib.Path]:
|
||||
files = {}
|
||||
|
||||
with open(TRANSLATIONS_LANGUAGES) as fp:
|
||||
language_dict = json.load(fp)
|
||||
|
||||
for filename in language_dict.values():
|
||||
path = TRANSLATIONS_DIR / f"{filename}.ts"
|
||||
language = path.stem.split("main_")[1]
|
||||
|
||||
if languages is None or language in languages:
|
||||
files[language] = path
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def translate_phrase(text: str, language: str) -> str:
|
||||
response = requests.post(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
json={
|
||||
"model": OPENAI_MODEL,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": OPENAI_PROMPT.format(language=language),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": text,
|
||||
},
|
||||
],
|
||||
"temperature": 0.8,
|
||||
"max_tokens": 1024,
|
||||
"top_p": 1,
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {OPENAI_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
if 400 <= response.status_code < 600:
|
||||
raise requests.HTTPError(f'Error {response.status_code}: {response.json()}', response=response)
|
||||
|
||||
data = response.json()
|
||||
|
||||
return cast(str, data["choices"][0]["message"]["content"])
|
||||
|
||||
|
||||
def translate_file(path: pathlib.Path, language: str, all_: bool) -> None:
|
||||
tree = ET.parse(path)
|
||||
|
||||
root = tree.getroot()
|
||||
|
||||
for context in root.findall("./context"):
|
||||
name = context.find("name")
|
||||
if name is None:
|
||||
raise ValueError("name not found")
|
||||
|
||||
print(f"Context: {name.text}")
|
||||
|
||||
for message in context.findall("./message"):
|
||||
source = message.find("source")
|
||||
translation = message.find("translation")
|
||||
|
||||
if source is None or translation is None:
|
||||
raise ValueError("source or translation not found")
|
||||
|
||||
if not all_ and translation.attrib.get("type") != "unfinished":
|
||||
continue
|
||||
|
||||
llm_translation = translate_phrase(cast(str, source.text), language)
|
||||
|
||||
print(f"Source: {source.text}\n" +
|
||||
f"Current translation: {translation.text}\n" +
|
||||
f"LLM translation: {llm_translation}")
|
||||
|
||||
translation.text = llm_translation
|
||||
|
||||
with path.open("w", encoding="utf-8") as fp:
|
||||
fp.write('<?xml version="1.0" encoding="utf-8"?>\n' +
|
||||
'<!DOCTYPE TS>\n' +
|
||||
ET.tostring(root, encoding="utf-8").decode())
|
||||
|
||||
|
||||
def main():
|
||||
arg_parser = argparse.ArgumentParser("Auto translate")
|
||||
|
||||
group = arg_parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("-a", "--all-files", action="store_true", help="Translate all files")
|
||||
group.add_argument("-f", "--file", nargs="+", help="Translate the selected files. (Example: -f fr de)")
|
||||
|
||||
arg_parser.add_argument("-t", "--all-translations", action="store_true", default=False, help="Translate all sections. (Default: only unfinished)")
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
if OPENAI_API_KEY is None:
|
||||
print("OpenAI API key is missing. (Hint: use `export OPENAI_API_KEY=YOUR-KEY` before you run the script).\n" +
|
||||
"If you don't have one go to: https://beta.openai.com/account/api-keys.")
|
||||
exit(1)
|
||||
|
||||
files = get_language_files(None if args.all_files else args.file)
|
||||
|
||||
if args.file:
|
||||
missing_files = set(args.file) - set(files)
|
||||
if len(missing_files):
|
||||
print(f"No language files found: {missing_files}")
|
||||
exit(1)
|
||||
|
||||
print(f"Translation mode: {'all' if args.all_translations else 'only unfinished'}. Files: {list(files)}")
|
||||
|
||||
for lang, path in files.items():
|
||||
print(f"Translate {lang} ({path})")
|
||||
translate_file(path, lang, args.all_translations)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.selfdrive.ui.tests.test_translations import UNFINISHED_TRANSLATION_TAG
|
||||
from openpilot.selfdrive.ui.update_translations import LANGUAGES_FILE, TRANSLATIONS_DIR
|
||||
|
||||
TRANSLATION_TAG = "<translation"
|
||||
BADGE_HEIGHT = 20 + 8
|
||||
SHIELDS_URL = "https://img.shields.io/badge"
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open(LANGUAGES_FILE) as f:
|
||||
translation_files = json.load(f)
|
||||
|
||||
badge_svg = []
|
||||
max_badge_width = 0 # keep track of max width to set parent element
|
||||
for idx, (name, file) in enumerate(translation_files.items()):
|
||||
with open(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")) as tr_f:
|
||||
tr_file = tr_f.read()
|
||||
|
||||
total_translations = 0
|
||||
unfinished_translations = 0
|
||||
for line in tr_file.splitlines():
|
||||
if TRANSLATION_TAG in line:
|
||||
total_translations += 1
|
||||
if UNFINISHED_TRANSLATION_TAG in line:
|
||||
unfinished_translations += 1
|
||||
|
||||
percent_finished = int(100 - (unfinished_translations / total_translations * 100.))
|
||||
color = "green" if percent_finished == 100 else "orange" if percent_finished > 90 else "red"
|
||||
|
||||
# Download badge
|
||||
badge_label = f"LANGUAGE {name}"
|
||||
badge_message = f"{percent_finished}% complete"
|
||||
if unfinished_translations != 0:
|
||||
badge_message += f" ({unfinished_translations} unfinished)"
|
||||
|
||||
r = requests.get(f"{SHIELDS_URL}/{badge_label}-{badge_message}-{color}", timeout=10)
|
||||
assert r.status_code == 200, "Error downloading badge"
|
||||
content_svg = r.content.decode("utf-8")
|
||||
|
||||
xml = ET.fromstring(content_svg)
|
||||
assert "width" in xml.attrib
|
||||
max_badge_width = max(max_badge_width, int(xml.attrib["width"]))
|
||||
|
||||
# Make tag ids in each badge unique to combine them into one svg
|
||||
for tag in ("r", "s"):
|
||||
content_svg = content_svg.replace(f'id="{tag}"', f'id="{tag}{idx}"')
|
||||
content_svg = content_svg.replace(f'"url(#{tag})"', f'"url(#{tag}{idx})"')
|
||||
|
||||
badge_svg.extend([f'<g transform="translate(0, {idx * BADGE_HEIGHT})">', content_svg, "</g>"])
|
||||
|
||||
badge_svg.insert(0, '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
|
||||
f'height="{len(translation_files) * BADGE_HEIGHT}" width="{max_badge_width}">')
|
||||
badge_svg.append("</svg>")
|
||||
|
||||
with open(os.path.join(BASEDIR, "translation_badge.svg"), "w") as badge_f:
|
||||
badge_f.write("\n".join(badge_svg))
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"English": "main_en",
|
||||
"Deutsch": "main_de",
|
||||
"Français": "main_fr",
|
||||
"Português": "main_pt-BR",
|
||||
"Türkçe": "main_tr",
|
||||
"العربية": "main_ar",
|
||||
"ไทย": "main_th",
|
||||
"中文(繁體)": "main_zh-CHT",
|
||||
"中文(简体)": "main_zh-CHS",
|
||||
"한국어": "main_ko",
|
||||
"日本語": "main_ja"
|
||||
}
|
||||
Executable
+1661
File diff suppressed because it is too large
Load Diff
Executable
+1585
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user