cabana: remove dependency on selfdrive/ui (#36434)

remove dependency on selfdrive/ui
pull/36438/head
Dean Lee 2 weeks ago committed by GitHub
parent 4f52f3f3c5
commit 378212e5ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 64
      tools/cabana/SConscript
  2. 1
      tools/cabana/cabana.cc
  3. 1
      tools/cabana/detailwidget.cc
  4. 2
      tools/cabana/detailwidget.h
  5. 3
      tools/cabana/streams/routes.h
  6. 161
      tools/cabana/utils/api.cc
  7. 47
      tools/cabana/utils/api.h
  8. 29
      tools/cabana/utils/elidedlabel.cc
  9. 25
      tools/cabana/utils/elidedlabel.h
  10. 86
      tools/cabana/utils/util.cc
  11. 2
      tools/cabana/utils/util.h

@ -1,7 +1,61 @@
Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal', 'widgets')
import subprocess
import os
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal')
qt_env = env.Clone()
qt_modules = ["Widgets", "Gui", "Core", "Network", "Concurrent", "DBus", "Xml"]
qt_libs = []
if arch == "Darwin":
brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip()
qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5"
qt_dirs = [
os.path.join(qt_env['QTDIR'], "include"),
]
qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules]
qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")]
qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"]
qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin"))
else:
qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip()
qt_install_headers = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip()
qt_env['QTDIR'] = qt_install_prefix
qt_dirs = [
f"{qt_install_headers}",
]
qt_gui_path = os.path.join(qt_install_headers, "QtGui")
qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))]
qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else []
qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules]
qt_libs = [f"Qt5{m}" for m in qt_modules]
if arch == "larch64":
qt_libs += ["GLESv2", "wayland-client"]
qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath)
elif arch != "Darwin":
qt_libs += ["GL"]
qt_env['QT3DIR'] = qt_env['QTDIR']
qt_env.Tool('qt3')
qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"]
qt_flags = [
"-D_REENTRANT",
"-DQT_NO_DEBUG",
"-DQT_WIDGETS_LIB",
"-DQT_GUI_LIB",
"-DQT_CORE_LIB",
"-DQT_MESSAGELOGCONTEXT",
]
qt_env['CXXFLAGS'] += qt_flags
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
qt_env['LIBPATH'] += ['#selfdrive/ui', ]
qt_env['LIBS'] = qt_libs
base_frameworks = qt_env['FRAMEWORKS']
base_libs = [common, messaging, cereal, visionipc, 'qt_util', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
if arch == "Darwin":
base_frameworks.append('OpenCL')
@ -12,11 +66,11 @@ else:
base_libs.append('Qt5Charts')
base_libs.append('Qt5SerialBus')
qt_libs = ['qt_util'] + base_libs
qt_libs = base_libs
cabana_env = qt_env.Clone()
cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs
cabana_libs = [cereal, messaging, visionipc, replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs
opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath)
cabana_env['CXXFLAGS'] += [opendbc_path]
@ -28,7 +82,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "asset
cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcanstream.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
'utils/export.cc', 'utils/util.cc',
'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'utils/api.cc',
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc',
'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)

@ -1,7 +1,6 @@
#include <QApplication>
#include <QCommandLineParser>
#include "selfdrive/ui/qt/util.h"
#include "tools/cabana/mainwin.h"
#include "tools/cabana/streams/devicestream.h"
#include "tools/cabana/streams/pandastream.h"

@ -3,6 +3,7 @@
#include <QFormLayout>
#include <QMenu>
#include <QRadioButton>
#include <QPushButton>
#include <QToolBar>
#include "tools/cabana/commands.h"

@ -6,11 +6,11 @@
#include <QTextEdit>
#include <set>
#include "selfdrive/ui/qt/widgets/controls.h"
#include "tools/cabana/binaryview.h"
#include "tools/cabana/chart/chartswidget.h"
#include "tools/cabana/historylog.h"
#include "tools/cabana/signalview.h"
#include "tools/cabana/utils/elidedlabel.h"
class EditMessageDialog : public QDialog {
public:

@ -2,8 +2,7 @@
#include <QComboBox>
#include <QDialog>
#include "selfdrive/ui/qt/api.h"
#include "tools/cabana/utils/api.h"
class RouteListWidget;
class OneShotHttpRequest;

@ -0,0 +1,161 @@
#include "selfdrive/ui/qt/api.h"
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <QApplication>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDebug>
#include <QJsonDocument>
#include <QNetworkRequest>
#include <memory>
#include <string>
#include "common/util.h"
#include "system/hardware/hw.h"
#include "selfdrive/ui/qt/util.h"
QString getVersion() {
static QString version = QString::fromStdString(Params().get("Version"));
return version;
}
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 {};
}
}
namespace CommaApi {
RSA *get_rsa_private_key() {
static std::unique_ptr<RSA, decltype(&RSA_free)> rsa_private(nullptr, RSA_free);
if (!rsa_private) {
FILE *fp = fopen(Path::rsa_file().c_str(), "rb");
if (!fp) {
qDebug() << "No RSA private key found, please run manager.py or registration.py";
return nullptr;
}
rsa_private.reset(PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL));
fclose(fp);
}
return rsa_private.get();
}
QByteArray rsa_sign(const QByteArray &data) {
RSA *rsa_private = get_rsa_private_key();
if (!rsa_private) return {};
QByteArray sig(RSA_size(rsa_private), Qt::Uninitialized);
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.size() == sig_len);
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);
return jwt + "." + rsa_sign(hash).toBase64(b64_opts);
}
} // 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;
}

@ -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();
};

@ -0,0 +1,29 @@
#include "tools/cabana/utils/elidedlabel.h"
#include <QPainter>
#include <QStyleOption>
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());
}

@ -0,0 +1,25 @@
#pragma once
#include <QLabel>
#include <QMouseEvent>
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_;
};

@ -10,11 +10,16 @@
#include <QColor>
#include <QDateTime>
#include <QDir>
#include <QFontDatabase>
#include <QLocale>
#include <QPixmapCache>
#include "selfdrive/ui/qt/util.h"
#include <QSurfaceFormat>
#include <QFileInfo>
#include <QPainterPath>
#include <QTextStream>
#include <QtXml/QDomDocument>
#include "common/util.h"
// SegmentTree
@ -276,3 +281,80 @@ QString signalToolTip(const cabana::Signal *sig) {
)").arg(sig->name).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb)
.arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N");
}
void setSurfaceFormat() {
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) {
// setup signal handlers to exit gracefully
std::signal(SIGINT, sigTermHandler);
std::signal(SIGTERM, sigTermHandler);
QString app_dir;
#ifdef __APPLE__
// Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering
QApplication tmp(argc, argv);
app_dir = QCoreApplication::applicationDirPath();
if (disable_hidpi) {
qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit());
}
#else
app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path();
#endif
qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150));
// ensure the current dir matches the exectuable's directory
QDir::setCurrent(app_dir);
setSurfaceFormat();
}
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;
}

@ -166,3 +166,5 @@ private:
int num_decimals(double num);
QString signalToolTip(const cabana::Signal *sig);
inline QString toHexString(int value) { return QString("0x%1").arg(QString::number(value, 16).toUpper(), 2, '0'); }
void initApp(int argc, char *argv[], bool disable_hidpi = true);
QPixmap bootstrapPixmap(const QString &id);

Loading…
Cancel
Save