IQ.Pilot Release Commit @ e46d557
This commit is contained in:
@@ -1,171 +0,0 @@
|
||||
#include "tools/cabana/utils/api.h"
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "common/params.h"
|
||||
#include "common/util.h"
|
||||
#include "system/hardware/hw.h"
|
||||
#include "tools/cabana/utils/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 {
|
||||
|
||||
EVP_PKEY *get_private_key() {
|
||||
static std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> pkey(nullptr, EVP_PKEY_free);
|
||||
if (!pkey) {
|
||||
FILE *fp = fopen(Path::rsa_file().c_str(), "rb");
|
||||
if (!fp) {
|
||||
qDebug() << "No private key found, please run manager.py or registration.py";
|
||||
return nullptr;
|
||||
}
|
||||
pkey.reset(PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr));
|
||||
fclose(fp);
|
||||
}
|
||||
return pkey.get();
|
||||
}
|
||||
|
||||
QByteArray rsa_sign(const QByteArray &data) {
|
||||
EVP_PKEY *pkey = get_private_key();
|
||||
if (!pkey) return {};
|
||||
|
||||
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
|
||||
if (!mdctx) return {};
|
||||
|
||||
QByteArray sig(EVP_PKEY_size(pkey), Qt::Uninitialized);
|
||||
size_t sig_len = sig.size();
|
||||
|
||||
int ret = EVP_DigestSignInit(mdctx, nullptr, EVP_sha256(), nullptr, pkey);
|
||||
ret &= EVP_DigestSignUpdate(mdctx, data.data(), data.size());
|
||||
ret &= EVP_DigestSignFinal(mdctx, (unsigned char*)sig.data(), &sig_len);
|
||||
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
|
||||
if (ret != 1) return {};
|
||||
sig.resize(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;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#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-iqlabs.konn3kt.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();
|
||||
};
|
||||
@@ -1,41 +1,41 @@
|
||||
#include "tools/cabana/utils/export.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
namespace utils {
|
||||
|
||||
void exportToCSV(const QString &file_name, std::optional<MessageId> msg_id) {
|
||||
QFile file(file_name);
|
||||
if (file.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
|
||||
QTextStream stream(&file);
|
||||
void exportToCSV(const std::string &file_name, std::optional<MessageId> msg_id) {
|
||||
std::ofstream stream(file_name, std::ios::trunc);
|
||||
if (stream) {
|
||||
stream << "time,addr,bus,data\n";
|
||||
for (auto e : msg_id ? can->events(*msg_id) : can->allEvents()) {
|
||||
stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << ","
|
||||
<< "0x" << QString::number(e->address, 16) << "," << e->src << ","
|
||||
<< "0x" << QByteArray::fromRawData((const char *)e->dat, e->size).toHex().toUpper() << "\n";
|
||||
stream << std::fixed << std::setprecision(3) << can->toSeconds(e->mono_time) << ","
|
||||
<< "0x" << std::hex << e->address << std::dec << "," << static_cast<int>(e->src) << ",0x"
|
||||
<< std::uppercase << std::hex << std::setfill('0');
|
||||
for (int i = 0; i < e->size; ++i) stream << std::setw(2) << static_cast<int>(e->dat[i]);
|
||||
stream << std::nouppercase << std::dec << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id) {
|
||||
QFile file(file_name);
|
||||
if (auto msg = dbc()->msg(msg_id); msg && msg->sigs.size() && file.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
|
||||
QTextStream stream(&file);
|
||||
void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id) {
|
||||
std::ofstream stream(file_name, std::ios::trunc);
|
||||
if (auto msg = dbc()->msg(msg_id); msg && !msg->sigs.empty() && stream) {
|
||||
stream << "time,addr,bus";
|
||||
for (auto s : msg->sigs)
|
||||
stream << "," << s->name;
|
||||
stream << "," << s->name.c_str();
|
||||
stream << "\n";
|
||||
|
||||
for (auto e : can->events(msg_id)) {
|
||||
stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << ","
|
||||
<< "0x" << QString::number(e->address, 16) << "," << e->src;
|
||||
stream << std::fixed << std::setprecision(3) << can->toSeconds(e->mono_time) << ","
|
||||
<< "0x" << std::hex << e->address << std::dec << "," << static_cast<int>(e->src);
|
||||
for (auto s : msg->sigs) {
|
||||
double value = 0;
|
||||
s->getValue(e->dat, e->size, &value);
|
||||
stream << "," << QString::number(value, 'f', s->precision);
|
||||
stream << "," << std::fixed << std::setprecision(s->precision) << value;
|
||||
}
|
||||
stream << "\n";
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
|
||||
namespace utils {
|
||||
void exportToCSV(const QString &file_name, std::optional<MessageId> msg_id = std::nullopt);
|
||||
void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id);
|
||||
void exportToCSV(const std::string &file_name, std::optional<MessageId> msg_id = std::nullopt);
|
||||
void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id);
|
||||
} // namespace utils
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFontDatabase>
|
||||
#include <QLocale>
|
||||
#include <QPixmapCache>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QFileInfo>
|
||||
#include <QPainterPath>
|
||||
#include <QTextStream>
|
||||
#include <QtXml/QDomDocument>
|
||||
#include <unordered_map>
|
||||
#include "common/util.h"
|
||||
|
||||
// SegmentTree
|
||||
@@ -101,7 +103,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
|
||||
|
||||
// Paint hex column
|
||||
const auto &bytes = *static_cast<std::vector<uint8_t> *>(data.value<void *>());
|
||||
const auto &colors = *static_cast<std::vector<QColor> *>(index.data(ColorsRole).value<void *>());
|
||||
const auto &colors = *static_cast<std::vector<CabanaColor> *>(index.data(ColorsRole).value<void *>());
|
||||
|
||||
painter->setFont(fixed_font);
|
||||
const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color);
|
||||
@@ -116,7 +118,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
|
||||
painter->setPen(option.palette.color(QPalette::Text));
|
||||
painter->fillRect(r, option.palette.color(QPalette::Window));
|
||||
}
|
||||
painter->fillRect(r, colors[i]);
|
||||
painter->fillRect(r, toQColor(colors[i]));
|
||||
} else {
|
||||
painter->setPen(text_pen);
|
||||
}
|
||||
@@ -149,54 +151,211 @@ void TabBar::closeTabClicked() {
|
||||
|
||||
// UnixSignalHandler
|
||||
|
||||
UnixSignalHandler::UnixSignalHandler(QObject *parent) : QObject(nullptr) {
|
||||
UnixSignalHandler::UnixSignalHandler() {
|
||||
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd)) {
|
||||
qFatal("Couldn't create TERM socketpair");
|
||||
}
|
||||
|
||||
sn = new QSocketNotifier(sig_fd[1], QSocketNotifier::Read, this);
|
||||
connect(sn, &QSocketNotifier::activated, this, &UnixSignalHandler::handleSigTerm);
|
||||
waiter = std::thread([this]() {
|
||||
int tmp = 0;
|
||||
while (::read(sig_fd[1], &tmp, sizeof(tmp)) < 0) {
|
||||
if (errno != EINTR) return;
|
||||
}
|
||||
if (shutting_down.load()) return;
|
||||
|
||||
// Marshal exit onto the GUI thread (qApp methods are not thread-safe).
|
||||
QMetaObject::invokeMethod(qApp, []() {
|
||||
printf("\nexiting...\n");
|
||||
qApp->closeAllWindows();
|
||||
qApp->exit();
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
|
||||
std::signal(SIGINT, signalHandler);
|
||||
std::signal(SIGTERM, UnixSignalHandler::signalHandler);
|
||||
}
|
||||
|
||||
UnixSignalHandler::~UnixSignalHandler() {
|
||||
shutting_down.store(true);
|
||||
int dummy = 0;
|
||||
(void)!::write(sig_fd[0], &dummy, sizeof(dummy));
|
||||
if (waiter.joinable()) waiter.join();
|
||||
::close(sig_fd[0]);
|
||||
::close(sig_fd[1]);
|
||||
}
|
||||
|
||||
void UnixSignalHandler::signalHandler(int s) {
|
||||
::write(sig_fd[0], &s, sizeof(s));
|
||||
}
|
||||
|
||||
void UnixSignalHandler::handleSigTerm() {
|
||||
sn->setEnabled(false);
|
||||
int tmp;
|
||||
::read(sig_fd[1], &tmp, sizeof(tmp));
|
||||
|
||||
printf("\nexiting...\n");
|
||||
qApp->closeAllWindows();
|
||||
qApp->exit();
|
||||
(void)!::write(sig_fd[0], &s, sizeof(s));
|
||||
}
|
||||
|
||||
// NameValidator
|
||||
|
||||
NameValidator::NameValidator(QObject *parent) : QRegExpValidator(QRegExp("^(\\w+)"), parent) {}
|
||||
NameValidator::NameValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State NameValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos);
|
||||
input.replace(' ', '_');
|
||||
return QRegExpValidator::validate(input, pos);
|
||||
if (input.isEmpty()) return QValidator::Intermediate;
|
||||
for (const QChar &c : input) {
|
||||
if (!c.isLetterOrNumber() && c != '_') return QValidator::Invalid;
|
||||
}
|
||||
return QValidator::Acceptable;
|
||||
}
|
||||
|
||||
DoubleValidator::DoubleValidator(QObject *parent) : QDoubleValidator(parent) {
|
||||
// Match locale of QString::toDouble() instead of system
|
||||
QLocale locale(QLocale::C);
|
||||
locale.setNumberOptions(QLocale::RejectGroupSeparator);
|
||||
setLocale(locale);
|
||||
// NodeValidator
|
||||
|
||||
NodeValidator::NodeValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State NodeValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos);
|
||||
if (input.isEmpty()) return QValidator::Intermediate;
|
||||
// Match ^\w+(,\w+)*$ ; a trailing comma is Intermediate (user still typing).
|
||||
bool need_word = true;
|
||||
for (const QChar &c : input) {
|
||||
if (c.isLetterOrNumber() || c == '_') {
|
||||
need_word = false;
|
||||
} else if (c == ',' && !need_word) {
|
||||
need_word = true;
|
||||
} else {
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
}
|
||||
return need_word ? QValidator::Intermediate : QValidator::Acceptable;
|
||||
}
|
||||
|
||||
// NonWhitespaceValidator
|
||||
|
||||
NonWhitespaceValidator::NonWhitespaceValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos);
|
||||
if (input.isEmpty()) return QValidator::Intermediate;
|
||||
for (const QChar &c : input) {
|
||||
if (c.isSpace()) return QValidator::Invalid;
|
||||
}
|
||||
return QValidator::Acceptable;
|
||||
}
|
||||
|
||||
// IpAddressValidator
|
||||
|
||||
IpAddressValidator::IpAddressValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State IpAddressValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos);
|
||||
if (input.isEmpty()) return QValidator::Intermediate;
|
||||
|
||||
int dots = 0;
|
||||
int value = 0;
|
||||
bool has_digit = false;
|
||||
for (const QChar &c : input) {
|
||||
if (c.isDigit()) {
|
||||
value = has_digit ? value * 10 + c.digitValue() : c.digitValue();
|
||||
if (value > 255) return QValidator::Invalid;
|
||||
has_digit = true;
|
||||
} else if (c == '.') {
|
||||
if (!has_digit || dots >= 3) return QValidator::Invalid;
|
||||
++dots;
|
||||
has_digit = false;
|
||||
value = 0;
|
||||
} else {
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
}
|
||||
return (dots == 3 && has_digit) ? QValidator::Acceptable : QValidator::Intermediate;
|
||||
}
|
||||
|
||||
DoubleValidator::DoubleValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State DoubleValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos);
|
||||
if (input.isEmpty()) return QValidator::Intermediate;
|
||||
|
||||
// Match QString::toDouble(): C locale, no hex floats / inf / nan.
|
||||
const std::string bytes = input.toLatin1().toStdString();
|
||||
// strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not.
|
||||
if (bytes.find_first_of("xXpP") != std::string::npos) {
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
|
||||
const char *start = bytes.c_str();
|
||||
char *end = nullptr;
|
||||
const double value = std::strtod(start, &end);
|
||||
if (end == start) {
|
||||
// Still typing a sign, decimal point, or exponent prefix.
|
||||
if (input == "-" || input == "+" || input == "." || input == "-." || input == "+.") {
|
||||
return QValidator::Intermediate;
|
||||
}
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
if (*end == '\0') {
|
||||
// Reject inf/nan (strtod accepts them; QDoubleValidator / toDouble path should not).
|
||||
return std::isfinite(value) ? QValidator::Acceptable : QValidator::Invalid;
|
||||
}
|
||||
|
||||
// Partial exponent / trailing sign while typing (e.g. "1e", "1e-", "1.").
|
||||
for (const char *p = end; *p; ++p) {
|
||||
const char c = *p;
|
||||
if (!(c == 'e' || c == 'E' || c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9'))) {
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
}
|
||||
return QValidator::Intermediate;
|
||||
}
|
||||
|
||||
namespace utils {
|
||||
|
||||
std::string homePath() {
|
||||
const char *home = ::getenv("HOME");
|
||||
return home ? home : "";
|
||||
}
|
||||
|
||||
std::filesystem::path configPath() {
|
||||
#ifdef __APPLE__
|
||||
return std::filesystem::path(homePath()) / "Library/Preferences";
|
||||
#else
|
||||
const char *xdg = ::getenv("XDG_CONFIG_HOME");
|
||||
return (xdg && xdg[0]) ? std::filesystem::path(xdg) : std::filesystem::path(homePath()) / ".config";
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
static const char *clipboard_read_cmds[] = {"pbpaste"};
|
||||
static const char *clipboard_write_cmds[] = {"pbcopy"};
|
||||
#else
|
||||
static const char *clipboard_read_cmds[] = {"wl-paste --no-newline 2>/dev/null", "xclip -selection clipboard -o 2>/dev/null", "xsel -ob 2>/dev/null"};
|
||||
static const char *clipboard_write_cmds[] = {"wl-copy 2>/dev/null", "xclip -selection clipboard 2>/dev/null", "xsel -ib 2>/dev/null"};
|
||||
#endif
|
||||
|
||||
bool getClipboardText(std::string *text) {
|
||||
text->clear();
|
||||
bool has_tool = false;
|
||||
for (const char *cmd : clipboard_read_cmds) {
|
||||
FILE *f = ::popen(cmd, "r");
|
||||
if (!f) continue;
|
||||
std::string out;
|
||||
char buf[4096];
|
||||
for (size_t n; (n = ::fread(buf, 1, sizeof(buf), f)) > 0;) out.append(buf, n);
|
||||
int status = ::pclose(f);
|
||||
if (status == 0) {
|
||||
*text = std::move(out);
|
||||
return true;
|
||||
}
|
||||
has_tool |= WIFEXITED(status) && WEXITSTATUS(status) != 127; // 127: command not found
|
||||
}
|
||||
return has_tool; // tool present but clipboard empty
|
||||
}
|
||||
|
||||
bool setClipboardText(const std::string &text) {
|
||||
std::signal(SIGPIPE, SIG_IGN);
|
||||
for (const char *cmd : clipboard_write_cmds) {
|
||||
FILE *f = ::popen(cmd, "w");
|
||||
if (!f) continue;
|
||||
size_t written = ::fwrite(text.data(), 1, text.size(), f);
|
||||
if (::pclose(f) == 0 && written == text.size()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDarkTheme() {
|
||||
QColor windowColor = QApplication::palette().color(QPalette::Window);
|
||||
return windowColor.lightness() < 128;
|
||||
@@ -209,7 +368,10 @@ QPixmap icon(const QString &id) {
|
||||
QString key = "bootstrap_" % id % (dark_theme ? "1" : "0");
|
||||
if (!QPixmapCache::find(key, &pm)) {
|
||||
pm = bootstrapPixmap(id);
|
||||
if (dark_theme) {
|
||||
// IQ.Pilot patch: ToolButton("") (chartswidget.cc) asks for an empty id, so pm can
|
||||
// be null. Painting a null QPixmap is a no-op that logs two QPainter warnings on
|
||||
// every dark-theme start. Upstream candidate.
|
||||
if (dark_theme && !pm.isNull()) {
|
||||
QPainter p(&pm);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.fillRect(pm.rect(), QColor("#bbbbbb"));
|
||||
@@ -258,10 +420,34 @@ void setTheme(int theme) {
|
||||
}
|
||||
|
||||
QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) {
|
||||
QString format = absolute_time ? "yyyy-MM-dd hh:mm:ss"
|
||||
: (sec > 60 * 60 ? "hh:mm:ss" : "mm:ss");
|
||||
if (include_milliseconds) format += ".zzz";
|
||||
return QDateTime::fromMSecsSinceEpoch(sec * 1000).toString(format);
|
||||
if (absolute_time) {
|
||||
const auto ms_total = static_cast<int64_t>(std::llround(sec * 1000.0));
|
||||
const std::time_t secs = static_cast<std::time_t>(ms_total / 1000);
|
||||
int millis = static_cast<int>(ms_total % 1000);
|
||||
if (millis < 0) millis = -millis;
|
||||
std::tm tm{};
|
||||
localtime_r(&secs, &tm);
|
||||
char buf[64];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
|
||||
if (include_milliseconds) {
|
||||
return QString::asprintf("%s.%03d", buf, millis);
|
||||
}
|
||||
return QString::fromUtf8(buf);
|
||||
}
|
||||
|
||||
// Relative duration (not wall-clock).
|
||||
const bool show_hours = sec > 60 * 60;
|
||||
int total_ms = static_cast<int>(std::llround(std::max(0.0, sec) * 1000.0));
|
||||
const int hours = total_ms / (3600 * 1000);
|
||||
const int minutes = (total_ms / (60 * 1000)) % 60;
|
||||
const int seconds = (total_ms / 1000) % 60;
|
||||
const int millis = total_ms % 1000;
|
||||
if (show_hours) {
|
||||
return include_milliseconds ? QString::asprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis)
|
||||
: QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds);
|
||||
}
|
||||
return include_milliseconds ? QString::asprintf("%02d:%02d.%03d", minutes, seconds, millis)
|
||||
: QString::asprintf("%02d:%02d", minutes, seconds);
|
||||
}
|
||||
|
||||
} // namespace utils
|
||||
@@ -278,24 +464,10 @@ QString signalToolTip(const cabana::Signal *sig) {
|
||||
Start Bit: %2 Size: %3<br />
|
||||
MSB: %4 LSB: %5<br />
|
||||
Little Endian: %6 Signed: %7</span>
|
||||
)").arg(sig->name).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb)
|
||||
)").arg(QString::fromStdString(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();
|
||||
@@ -306,55 +478,68 @@ void initApp(int argc, char *argv[], bool disable_hidpi) {
|
||||
std::signal(SIGINT, sigTermHandler);
|
||||
std::signal(SIGTERM, sigTermHandler);
|
||||
|
||||
QString app_dir;
|
||||
std::filesystem::path app_dir;
|
||||
#ifdef __APPLE__
|
||||
// Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering
|
||||
QApplication tmp(argc, argv);
|
||||
app_dir = QCoreApplication::applicationDirPath();
|
||||
app_dir = QCoreApplication::applicationDirPath().toStdString();
|
||||
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();
|
||||
app_dir = std::filesystem::path(util::readlink("/proc/self/exe")).parent_path();
|
||||
#endif
|
||||
|
||||
qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150));
|
||||
qputenv("QT_DBL_CLICK_DIST", "150");
|
||||
// ensure the current dir matches the exectuable's directory
|
||||
QDir::setCurrent(app_dir);
|
||||
|
||||
setSurfaceFormat();
|
||||
std::error_code ec;
|
||||
std::filesystem::current_path(app_dir, ec);
|
||||
}
|
||||
|
||||
static QHash<QString, QByteArray> load_bootstrap_icons() {
|
||||
QHash<QString, QByteArray> icons;
|
||||
// embedded at build time from the bootstrap_icons package (see SConscript)
|
||||
extern const unsigned char bootstrap_icons_svg[];
|
||||
extern const size_t bootstrap_icons_svg_len;
|
||||
|
||||
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();
|
||||
static std::unordered_map<std::string, std::string> load_bootstrap_icons() {
|
||||
std::unordered_map<std::string, std::string> icons;
|
||||
|
||||
const std::string content(reinterpret_cast<const char *>(bootstrap_icons_svg), bootstrap_icons_svg_len);
|
||||
const std::string sym_open = "<symbol ";
|
||||
const std::string sym_close = "</symbol>";
|
||||
const std::string id_attr = "id=\"";
|
||||
|
||||
size_t pos = 0;
|
||||
while ((pos = content.find(sym_open, pos)) != std::string::npos) {
|
||||
size_t end = content.find(sym_close, pos);
|
||||
if (end == std::string::npos) break;
|
||||
end += sym_close.size();
|
||||
|
||||
// extract id
|
||||
size_t id_start = content.find(id_attr, pos);
|
||||
if (id_start != std::string::npos && id_start < end) {
|
||||
id_start += id_attr.size();
|
||||
size_t id_end = content.find('"', id_start);
|
||||
if (id_end != std::string::npos && id_end < end) {
|
||||
std::string id = content.substr(id_start, id_end - id_start);
|
||||
std::string svg_str = content.substr(pos, end - pos);
|
||||
// replace <symbol with <svg, </symbol> with </svg>
|
||||
svg_str.replace(0, 7, "<svg"); // "<symbol" (7) -> "<svg" (4)
|
||||
svg_str.replace(svg_str.size() - 9, 9, "</svg>"); // "</symbol>" (9) -> "</svg>" (6)
|
||||
icons[id] = std::move(svg_str);
|
||||
}
|
||||
n = n.nextSibling();
|
||||
}
|
||||
pos = end;
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
QPixmap bootstrapPixmap(const QString &id) {
|
||||
static QHash<QString, QByteArray> icons = load_bootstrap_icons();
|
||||
static auto icons = load_bootstrap_icons();
|
||||
|
||||
QPixmap pixmap;
|
||||
if (auto it = icons.find(id); it != icons.end()) {
|
||||
pixmap.loadFromData(it.value(), "svg");
|
||||
auto it = icons.find(id.toStdString());
|
||||
if (it != icons.end()) {
|
||||
pixmap.loadFromData((const uchar *)it->second.data(), it->second.size(), "svg");
|
||||
}
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QDoubleValidator>
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QPainter>
|
||||
#include <QRegExpValidator>
|
||||
#include <QSocketNotifier>
|
||||
#include <QStaticText>
|
||||
#include <QStringBuilder>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QToolButton>
|
||||
#include <QValidator>
|
||||
|
||||
#include "tools/cabana/dbc/dbc.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
|
||||
inline QColor toQColor(const CabanaColor &color) {
|
||||
return QColor(color.r, color.g, color.b, color.a);
|
||||
}
|
||||
|
||||
class LogSlider : public QSlider {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -84,22 +90,53 @@ private:
|
||||
int h_margin, v_margin;
|
||||
};
|
||||
|
||||
class NameValidator : public QRegExpValidator {
|
||||
// Accepts a single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_'.
|
||||
class NameValidator : public QValidator {
|
||||
Q_OBJECT
|
||||
public:
|
||||
NameValidator(QObject *parent=nullptr);
|
||||
QValidator::State validate(QString &input, int &pos) const override;
|
||||
};
|
||||
|
||||
class DoubleValidator : public QDoubleValidator {
|
||||
// Accepts comma-separated identifiers: \w+(,\w+)*
|
||||
class NodeValidator : public QValidator {
|
||||
Q_OBJECT
|
||||
public:
|
||||
NodeValidator(QObject *parent=nullptr);
|
||||
QValidator::State validate(QString &input, int &pos) const override;
|
||||
};
|
||||
|
||||
// Accepts one or more non-whitespace characters (\S+).
|
||||
class NonWhitespaceValidator : public QValidator {
|
||||
Q_OBJECT
|
||||
public:
|
||||
NonWhitespaceValidator(QObject *parent=nullptr);
|
||||
QValidator::State validate(QString &input, int &pos) const override;
|
||||
};
|
||||
|
||||
// Accepts a dotted IPv4 address (0-255 per octet).
|
||||
class IpAddressValidator : public QValidator {
|
||||
Q_OBJECT
|
||||
public:
|
||||
IpAddressValidator(QObject *parent=nullptr);
|
||||
QValidator::State validate(QString &input, int &pos) const override;
|
||||
};
|
||||
|
||||
// C-locale floating-point validator (matches QString::toDouble).
|
||||
class DoubleValidator : public QValidator {
|
||||
Q_OBJECT
|
||||
public:
|
||||
DoubleValidator(QObject *parent = nullptr);
|
||||
QValidator::State validate(QString &input, int &pos) const override;
|
||||
};
|
||||
|
||||
namespace utils {
|
||||
|
||||
QPixmap icon(const QString &id);
|
||||
std::string homePath();
|
||||
std::filesystem::path configPath();
|
||||
bool getClipboardText(std::string *text); // false if no clipboard tool is available
|
||||
bool setClipboardText(const std::string &text);
|
||||
bool isDarkTheme();
|
||||
void setTheme(int theme);
|
||||
QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false);
|
||||
@@ -108,7 +145,22 @@ inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text)
|
||||
p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text);
|
||||
}
|
||||
inline QString toHex(const std::vector<uint8_t> &dat, char separator = '\0') {
|
||||
return QByteArray::fromRawData((const char *)dat.data(), dat.size()).toHex(separator).toUpper();
|
||||
static const char digits[] = "0123456789ABCDEF";
|
||||
QString hex;
|
||||
hex.reserve(dat.size() * (separator ? 3 : 2));
|
||||
for (size_t i = 0; i < dat.size(); ++i) {
|
||||
if (separator && i) hex += QLatin1Char(separator);
|
||||
hex += QLatin1Char(digits[dat[i] >> 4]);
|
||||
hex += QLatin1Char(digits[dat[i] & 0xf]);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
// boundary conversions for the remaining Qt byte-array based state APIs
|
||||
template <typename T>
|
||||
std::vector<uint8_t> toBytes(const T &dat) { return {dat.begin(), dat.end()}; }
|
||||
inline auto qbytes(const std::vector<uint8_t> &dat) {
|
||||
return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -147,20 +199,18 @@ private:
|
||||
void closeTabClicked();
|
||||
};
|
||||
|
||||
class UnixSignalHandler : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread
|
||||
// (no Qt notifiers/timers). Exit is marshaled onto the GUI thread.
|
||||
class UnixSignalHandler {
|
||||
public:
|
||||
UnixSignalHandler(QObject *parent = nullptr);
|
||||
UnixSignalHandler();
|
||||
~UnixSignalHandler();
|
||||
static void signalHandler(int s);
|
||||
|
||||
public slots:
|
||||
void handleSigTerm();
|
||||
|
||||
private:
|
||||
inline static int sig_fd[2] = {};
|
||||
QSocketNotifier *sn;
|
||||
std::atomic<bool> shutting_down{false};
|
||||
std::thread waiter;
|
||||
};
|
||||
|
||||
int num_decimals(double num);
|
||||
|
||||
Reference in New Issue
Block a user