c++ cabana: Initial version (#25946)
* draft * continue * fix QChart unresponsive with large points * build with --extras * add filter * save DBC button * more buttons * add flag to use qcamera * stop replay in dctor * README * use getMsg * video control * edit signal * add colors * correct ts * add/edit signals * use bus:address as key old-commit-hash: 1b8324af876e66630b5f4e50623e3136a39f6ecb
This commit is contained in:
@@ -433,6 +433,10 @@ SConscript(['selfdrive/navd/SConscript'])
|
||||
|
||||
SConscript(['tools/replay/SConscript'])
|
||||
|
||||
opendbc = abspath([File('opendbc/can/libdbc.so')])
|
||||
Export('opendbc')
|
||||
SConscript(['tools/cabana/SConscript'])
|
||||
|
||||
if GetOption('test'):
|
||||
SConscript('panda/tests/safety/SConscript')
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ if maps:
|
||||
qt_env['CPPDEFINES'] += ["ENABLE_MAPS"]
|
||||
|
||||
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)
|
||||
Export('widgets')
|
||||
qt_libs = [widgets, qt_util] + base_libs
|
||||
|
||||
# build assets
|
||||
|
||||
4
tools/cabana/.gitignore
vendored
Normal file
4
tools/cabana/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
moc_*
|
||||
*.moc
|
||||
|
||||
_cabana
|
||||
9
tools/cabana/README
Normal file
9
tools/cabana/README
Normal file
@@ -0,0 +1,9 @@
|
||||
# Cabana
|
||||
|
||||
<img src="https://cabana.comma.ai/img/cabana.jpg" width="640" height="267" />
|
||||
|
||||
Cabana is a tool developed to view raw CAN data. One use for this is creating and editing [CAN Dictionaries](http://socialledge.com/sjsu/index.php/DBC_Format) (DBC files), and the tool provides direct integration with [commaai/opendbc](https://github.com/commaai/opendbc) (a collection of DBC files), allowing you to load the DBC files direct from source, and save to your fork. In addition, you can load routes from [comma connect](https://connect.comma.ai).
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
See [openpilot wiki](https://github.com/commaai/openpilot/wiki/Cabana)
|
||||
20
tools/cabana/SConscript
Normal file
20
tools/cabana/SConscript
Normal file
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc',
|
||||
'cereal', 'transformations', 'widgets', 'replay_lib', 'opendbc')
|
||||
|
||||
base_frameworks = qt_env['FRAMEWORKS']
|
||||
base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq',
|
||||
'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
|
||||
|
||||
if arch == "Darwin":
|
||||
base_frameworks.append('OpenCL')
|
||||
else:
|
||||
base_libs.append('OpenCL')
|
||||
|
||||
qt_libs = ['qt_util', 'Qt5Charts'] + base_libs
|
||||
if arch in ['x86_64', 'Darwin'] and GetOption('extras'):
|
||||
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
|
||||
|
||||
# qt_env["LD_LIBRARY_PATH"] = [Dir(f"#opendbc/can").abspath]
|
||||
cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, opendbc,'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_libs
|
||||
qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'videowidget.cc', 'parser.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
|
||||
4
tools/cabana/cabana
Executable file
4
tools/cabana/cabana
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
export LD_LIBRARY_PATH="../../opendbc/can:$LD_LIBRARY_PATH"
|
||||
exec ./_cabana "$1"
|
||||
34
tools/cabana/cabana.cc
Normal file
34
tools/cabana/cabana.cc
Normal file
@@ -0,0 +1,34 @@
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "tools/cabana/mainwin.h"
|
||||
|
||||
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
|
||||
Parser *parser = nullptr;
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
initApp(argc, argv);
|
||||
QApplication app(argc, argv);
|
||||
|
||||
QCommandLineParser cmd_parser;
|
||||
cmd_parser.addHelpOption();
|
||||
cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai");
|
||||
cmd_parser.addOption({"demo", "use a demo route instead of providing your own"});
|
||||
cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"});
|
||||
cmd_parser.addOption({"qcam", "use qcamera"});
|
||||
cmd_parser.process(app);
|
||||
const QStringList args = cmd_parser.positionalArguments();
|
||||
if (args.empty() && !cmd_parser.isSet("demo")) {
|
||||
cmd_parser.showHelp();
|
||||
}
|
||||
|
||||
const QString route = args.empty() ? DEMO_ROUTE : args.first();
|
||||
parser = new Parser(&app);
|
||||
if (!parser->loadRoute(route, cmd_parser.value("data_dir"), cmd_parser.isSet("qcam"))) {
|
||||
return 0;
|
||||
}
|
||||
MainWindow w;
|
||||
w.showMaximized();
|
||||
return app.exec();
|
||||
}
|
||||
102
tools/cabana/chartswidget.cc
Normal file
102
tools/cabana/chartswidget.cc
Normal file
@@ -0,0 +1,102 @@
|
||||
#include "tools/cabana/chartswidget.h"
|
||||
|
||||
#include <QtCharts/QLineSeries>
|
||||
|
||||
using namespace QtCharts;
|
||||
|
||||
int64_t get_raw_value(const QByteArray &msg, const Signal &sig) {
|
||||
int64_t ret = 0;
|
||||
|
||||
int i = sig.msb / 8;
|
||||
int bits = sig.size;
|
||||
while (i >= 0 && i < msg.size() && bits > 0) {
|
||||
int lsb = (int)(sig.lsb / 8) == i ? sig.lsb : i * 8;
|
||||
int msb = (int)(sig.msb / 8) == i ? sig.msb : (i + 1) * 8 - 1;
|
||||
int size = msb - lsb + 1;
|
||||
|
||||
uint64_t d = (msg[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1);
|
||||
ret |= d << (bits - size);
|
||||
|
||||
bits -= size;
|
||||
i = sig.is_little_endian ? i - 1 : i + 1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
|
||||
main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
||||
connect(parser, &Parser::updated, this, &ChartsWidget::updateState);
|
||||
connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart);
|
||||
connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart);
|
||||
}
|
||||
|
||||
void ChartsWidget::addChart(const QString &id, const QString &sig_name) {
|
||||
const QString char_name = id + sig_name;
|
||||
if (charts.find(char_name) == charts.end()) {
|
||||
QLineSeries *series = new QLineSeries();
|
||||
series->setUseOpenGL(true);
|
||||
auto chart = new QChart();
|
||||
chart->setTitle(id + ": " + sig_name);
|
||||
chart->addSeries(series);
|
||||
chart->createDefaultAxes();
|
||||
chart->legend()->hide();
|
||||
auto chart_view = new QChartView(chart);
|
||||
chart_view->setMinimumSize({width(), 300});
|
||||
chart_view->setMaximumSize({width(), 300});
|
||||
chart_view->setRenderHint(QPainter::Antialiasing);
|
||||
main_layout->addWidget(chart_view);
|
||||
charts[char_name] = {.id = id, .sig_name = sig_name, .chart_view = chart_view};
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::removeChart(const QString &id, const QString &sig_name) {
|
||||
auto it = charts.find(id + sig_name);
|
||||
if (it == charts.end()) return;
|
||||
|
||||
delete it->second.chart_view;
|
||||
charts.erase(it);
|
||||
}
|
||||
|
||||
void ChartsWidget::updateState() {
|
||||
static double last_update = millis_since_boot();
|
||||
double current_ts = millis_since_boot();
|
||||
bool update = (current_ts - last_update) > 500;
|
||||
if (update) {
|
||||
last_update = current_ts;
|
||||
}
|
||||
|
||||
auto getSig = [=](const QString &id, const QString &name) -> const Signal * {
|
||||
for (auto &sig : parser->getMsg(id)->sigs) {
|
||||
if (name == sig.name.c_str()) return &sig;
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
for (auto &[_, c] : charts) {
|
||||
if (auto sig = getSig(c.id, c.sig_name)) {
|
||||
const auto &can_data = parser->can_msgs[c.id].back();
|
||||
int64_t val = get_raw_value(can_data.dat, *sig);
|
||||
if (sig->is_signed) {
|
||||
val -= ((val >> (sig->size - 1)) & 0x1) ? (1ULL << sig->size) : 0;
|
||||
}
|
||||
double value = val * sig->factor + sig->offset;
|
||||
|
||||
if (value > c.max_y) c.max_y = value;
|
||||
if (value < c.min_y) c.min_y = value;
|
||||
|
||||
while (c.data.size() > DATA_LIST_SIZE) {
|
||||
c.data.pop_front();
|
||||
}
|
||||
c.data.push_back({can_data.ts / 1000., value});
|
||||
|
||||
if (update) {
|
||||
QChart *chart = c.chart_view->chart();
|
||||
QLineSeries *series = (QLineSeries *)chart->series()[0];
|
||||
series->replace(c.data);
|
||||
chart->axisX()->setRange(c.data.front().x(), c.data.back().x());
|
||||
chart->axisY()->setRange(c.min_y, c.max_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
tools/cabana/chartswidget.h
Normal file
34
tools/cabana/chartswidget.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <QtCharts/QChartView>
|
||||
#include <QtCharts/QLineSeries>
|
||||
#include <map>
|
||||
|
||||
#include "tools/cabana/parser.h"
|
||||
|
||||
class ChartsWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ChartsWidget(QWidget *parent = nullptr);
|
||||
inline bool hasChart(const QString &id, const QString &sig_name) {
|
||||
return charts.find(id+sig_name) != charts.end();
|
||||
}
|
||||
void addChart(const QString &id, const QString &sig_name);
|
||||
void removeChart(const QString &id, const QString &sig_name);
|
||||
void updateState();
|
||||
|
||||
protected:
|
||||
QVBoxLayout *main_layout;
|
||||
struct SignalChart {
|
||||
QString id;
|
||||
QString sig_name;
|
||||
int max_y = 0;
|
||||
int min_y = 0;
|
||||
QList<QPointF> data;
|
||||
QtCharts::QChartView *chart_view = nullptr;
|
||||
};
|
||||
std::map<QString, SignalChart> charts;
|
||||
};
|
||||
458
tools/cabana/detailwidget.cc
Normal file
458
tools/cabana/detailwidget.cc
Normal file
@@ -0,0 +1,458 @@
|
||||
#include "tools/cabana/detailwidget.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QHeaderView>
|
||||
#include <QMessageBox>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <bitset>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
|
||||
const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"};
|
||||
|
||||
static QVector<int> BIG_ENDIAN_START_BITS = []() {
|
||||
QVector<int> ret;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
for (int j = 7; j >= 0; j--) {
|
||||
ret.push_back(j + i * 8);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}();
|
||||
|
||||
static int bigEndianBitIndex(int index) {
|
||||
// TODO: Add a helper function in dbc.h
|
||||
return BIG_ENDIAN_START_BITS.indexOf(index);
|
||||
}
|
||||
|
||||
DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
QLabel *title = new QLabel(tr("SELECTED MESSAGE:"), this);
|
||||
main_layout->addWidget(title);
|
||||
|
||||
QHBoxLayout *name_layout = new QHBoxLayout();
|
||||
name_label = new QLabel(this);
|
||||
name_label->setStyleSheet("font-weight:bold;");
|
||||
name_layout->addWidget(name_label);
|
||||
name_layout->addStretch();
|
||||
edit_btn = new QPushButton(tr("Edit"), this);
|
||||
edit_btn->setVisible(false);
|
||||
QObject::connect(edit_btn, &QPushButton::clicked, [=]() {
|
||||
EditMessageDialog dlg(msg_id, this);
|
||||
int ret = dlg.exec();
|
||||
if (ret) {
|
||||
setMsg(msg_id);
|
||||
}
|
||||
});
|
||||
name_layout->addWidget(edit_btn);
|
||||
main_layout->addLayout(name_layout);
|
||||
|
||||
binary_view = new BinaryView(this);
|
||||
main_layout->addWidget(binary_view);
|
||||
|
||||
QHBoxLayout *signals_layout = new QHBoxLayout();
|
||||
signals_layout->addWidget(new QLabel(tr("Signals")));
|
||||
signals_layout->addStretch();
|
||||
add_sig_btn = new QPushButton(tr("Add signal"), this);
|
||||
add_sig_btn->setVisible(false);
|
||||
QObject::connect(add_sig_btn, &QPushButton::clicked, [=]() {
|
||||
AddSignalDialog dlg(msg_id, this);
|
||||
int ret = dlg.exec();
|
||||
if (ret) {
|
||||
setMsg(msg_id);
|
||||
}
|
||||
});
|
||||
signals_layout->addWidget(add_sig_btn);
|
||||
main_layout->addLayout(signals_layout);
|
||||
|
||||
QWidget *container = new QWidget(this);
|
||||
QVBoxLayout *container_layout = new QVBoxLayout(container);
|
||||
signal_edit_layout = new QVBoxLayout();
|
||||
signal_edit_layout->setSpacing(2);
|
||||
container_layout->addLayout(signal_edit_layout);
|
||||
|
||||
messages_view = new MessagesView(this);
|
||||
container_layout->addWidget(messages_view);
|
||||
|
||||
QScrollArea *scroll = new QScrollArea(this);
|
||||
scroll->setWidget(container);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
||||
|
||||
main_layout->addWidget(scroll);
|
||||
setFixedWidth(600);
|
||||
|
||||
connect(parser, &Parser::updated, this, &DetailWidget::updateState);
|
||||
}
|
||||
|
||||
void DetailWidget::updateState() {
|
||||
if (msg_id.isEmpty()) return;
|
||||
|
||||
auto &list = parser->can_msgs[msg_id];
|
||||
if (!list.empty()) {
|
||||
binary_view->setData(list.back().dat);
|
||||
messages_view->setMessages(list);
|
||||
}
|
||||
}
|
||||
|
||||
SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *v_layout = new QVBoxLayout(this);
|
||||
|
||||
QHBoxLayout *h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Name")));
|
||||
name = new QLineEdit(sig.name.c_str());
|
||||
h->addWidget(name);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Size")));
|
||||
size = new QSpinBox();
|
||||
size->setValue(sig.size);
|
||||
h->addWidget(size);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Most significant bit")));
|
||||
msb = new QSpinBox();
|
||||
msb->setValue(sig.msb);
|
||||
h->addWidget(msb);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Endianness")));
|
||||
endianness = new QComboBox();
|
||||
endianness->addItems({"Little", "Big"});
|
||||
endianness->setCurrentIndex(sig.is_little_endian ? 0 : 1);
|
||||
h->addWidget(endianness);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("sign")));
|
||||
sign = new QComboBox();
|
||||
sign->addItems({"Signed", "Unsigned"});
|
||||
sign->setCurrentIndex(sig.is_signed ? 0 : 1);
|
||||
h->addWidget(sign);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Factor")));
|
||||
factor = new QSpinBox();
|
||||
factor->setValue(sig.factor);
|
||||
h->addWidget(factor);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Offset")));
|
||||
offset = new QSpinBox();
|
||||
offset->setValue(sig.offset);
|
||||
h->addWidget(offset);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
// TODO: parse the following parameters in opendbc
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Unit")));
|
||||
unit = new QLineEdit();
|
||||
h->addWidget(unit);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Comment")));
|
||||
comment = new QLineEdit();
|
||||
h->addWidget(comment);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Minimum value")));
|
||||
min_val = new QSpinBox();
|
||||
h->addWidget(min_val);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Maximum value")));
|
||||
max_val = new QSpinBox();
|
||||
h->addWidget(max_val);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
h = new QHBoxLayout();
|
||||
h->addWidget(new QLabel(tr("Value descriptions")));
|
||||
val_desc = new QLineEdit();
|
||||
h->addWidget(val_desc);
|
||||
v_layout->addLayout(h);
|
||||
}
|
||||
|
||||
std::optional<Signal> SignalForm::getSignal() {
|
||||
Signal sig = {};
|
||||
sig.name = name->text().toStdString();
|
||||
sig.size = size->text().toInt();
|
||||
sig.offset = offset->text().toDouble();
|
||||
sig.factor = factor->text().toDouble();
|
||||
sig.msb = msb->text().toInt();
|
||||
sig.is_signed = sign->currentIndex() == 0;
|
||||
sig.is_little_endian = endianness->currentIndex() == 0;
|
||||
if (sig.is_little_endian) {
|
||||
sig.lsb = sig.start_bit;
|
||||
sig.msb = sig.start_bit + sig.size - 1;
|
||||
} else {
|
||||
sig.lsb = BIG_ENDIAN_START_BITS[bigEndianBitIndex(sig.start_bit) + sig.size - 1];
|
||||
sig.msb = sig.start_bit;
|
||||
}
|
||||
return (sig.name.empty() || sig.size <= 0) ? std::nullopt : std::optional(sig);
|
||||
}
|
||||
|
||||
void DetailWidget::setMsg(const QString &id) {
|
||||
msg_id = id;
|
||||
QString name = tr("untitled");
|
||||
|
||||
for (auto edit : signal_edit) {
|
||||
delete edit;
|
||||
}
|
||||
signal_edit.clear();
|
||||
int i = 0;
|
||||
auto msg = parser->getMsg(id);
|
||||
if (msg) {
|
||||
for (auto &s : msg->sigs) {
|
||||
SignalEdit *edit = new SignalEdit(id, s, i++, this);
|
||||
connect(edit, &SignalEdit::removed, [=]() {
|
||||
QTimer::singleShot(0, [=]() { setMsg(id); });
|
||||
});
|
||||
signal_edit_layout->addWidget(edit);
|
||||
signal_edit.push_back(edit);
|
||||
}
|
||||
name = msg->name.c_str();
|
||||
}
|
||||
name_label->setText(name);
|
||||
binary_view->setMsg(msg_id);
|
||||
|
||||
edit_btn->setVisible(true);
|
||||
add_sig_btn->setVisible(msg != nullptr);
|
||||
}
|
||||
|
||||
SignalEdit::SignalEdit(const QString &id, const Signal &sig, int idx, QWidget *parent) : id(id), name_(sig.name.c_str()), QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// title
|
||||
QHBoxLayout *title_layout = new QHBoxLayout();
|
||||
QLabel *icon = new QLabel(">");
|
||||
icon->setStyleSheet("font-weight:bold");
|
||||
title_layout->addWidget(icon);
|
||||
title = new ElidedLabel(this);
|
||||
title->setText(sig.name.c_str());
|
||||
title->setStyleSheet(QString("font-weight:bold; color:%1").arg(SIGNAL_COLORS[idx % std::size(SIGNAL_COLORS)]));
|
||||
connect(title, &ElidedLabel::clicked, [=]() {
|
||||
edit_container->isVisible() ? edit_container->hide() : edit_container->show();
|
||||
icon->setText(edit_container->isVisible() ? "▼" : ">");
|
||||
});
|
||||
title_layout->addWidget(title);
|
||||
title_layout->addStretch();
|
||||
QPushButton *show_plot = new QPushButton(tr("Show Plot"));
|
||||
QObject::connect(show_plot, &QPushButton::clicked, [=]() {
|
||||
if (show_plot->text() == tr("Show Plot")) {
|
||||
emit parser->showPlot(id, name_);
|
||||
show_plot->setText(tr("Hide Plot"));
|
||||
} else {
|
||||
emit parser->hidePlot(id, name_);
|
||||
show_plot->setText(tr("Show Plot"));
|
||||
}
|
||||
});
|
||||
title_layout->addWidget(show_plot);
|
||||
main_layout->addLayout(title_layout);
|
||||
|
||||
edit_container = new QWidget(this);
|
||||
QVBoxLayout *v_layout = new QVBoxLayout(edit_container);
|
||||
form = new SignalForm(sig, this);
|
||||
v_layout->addWidget(form);
|
||||
|
||||
QHBoxLayout *h = new QHBoxLayout();
|
||||
remove_btn = new QPushButton(tr("Remove Signal"));
|
||||
QObject::connect(remove_btn, &QPushButton::clicked, this, &SignalEdit::remove);
|
||||
h->addWidget(remove_btn);
|
||||
h->addStretch();
|
||||
QPushButton *save_btn = new QPushButton(tr("Save"));
|
||||
QObject::connect(save_btn, &QPushButton::clicked, this, &SignalEdit::save);
|
||||
h->addWidget(save_btn);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
edit_container->setVisible(false);
|
||||
main_layout->addWidget(edit_container);
|
||||
}
|
||||
|
||||
void SignalEdit::save() {
|
||||
Msg *msg = const_cast<Msg *>(parser->getMsg(id));
|
||||
if (!msg) return;
|
||||
|
||||
for (auto &sig : msg->sigs) {
|
||||
if (name_ == sig.name.c_str()) {
|
||||
if (auto s = form->getSignal()) {
|
||||
sig = *s;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalEdit::remove() {
|
||||
QMessageBox msgbox;
|
||||
msgbox.setText(tr("Remove signal"));
|
||||
msgbox.setInformativeText(tr("Are you sure you want to remove signal '%1'").arg(name_));
|
||||
msgbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
|
||||
msgbox.setDefaultButton(QMessageBox::Cancel);
|
||||
if (msgbox.exec()) {
|
||||
parser->removeSignal(id, name_);
|
||||
emit removed();
|
||||
}
|
||||
}
|
||||
|
||||
BinaryView::BinaryView(QWidget *parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
table = new QTableWidget(this);
|
||||
table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
table->horizontalHeader()->hide();
|
||||
table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
main_layout->addWidget(table);
|
||||
table->setColumnCount(9);
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||
}
|
||||
|
||||
void BinaryView::setMsg(const QString &id) {
|
||||
auto msg = parser->getMsg(Parser::addressFromId(id));
|
||||
int row_count = msg ? msg->size : parser->can_msgs[id].back().dat.size();
|
||||
|
||||
table->setRowCount(row_count);
|
||||
table->setColumnCount(9);
|
||||
for (int i = 0; i < table->rowCount(); ++i) {
|
||||
for (int j = 0; j < table->columnCount(); ++j) {
|
||||
auto item = new QTableWidgetItem();
|
||||
item->setTextAlignment(Qt::AlignCenter);
|
||||
if (j == 8) {
|
||||
QFont font;
|
||||
font.setBold(true);
|
||||
item->setFont(font);
|
||||
}
|
||||
table->setItem(i, j, item);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg) {
|
||||
for (int i = 0; i < msg->sigs.size(); ++i) {
|
||||
const auto &sig = msg->sigs[i];
|
||||
int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit);
|
||||
for (int j = start; j <= start + sig.size - 1; ++j) {
|
||||
table->item(j / 8, j % 8)->setBackground(QColor(SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFixedHeight(table->rowHeight(0) * table->rowCount() + 25);
|
||||
if (!parser->can_msgs.empty()) {
|
||||
setData(parser->can_msgs[id].back().dat);
|
||||
}
|
||||
}
|
||||
|
||||
void BinaryView::setData(const QByteArray &binary) {
|
||||
std::string s;
|
||||
for (int j = 0; j < binary.size(); ++j) {
|
||||
s += std::bitset<8>(binary[j]).to_string();
|
||||
}
|
||||
|
||||
char hex[3] = {'\0'};
|
||||
for (int i = 0; i < binary.size(); ++i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
table->item(i, j)->setText(QChar(s[i * 8 + j]));
|
||||
}
|
||||
sprintf(&hex[0], "%02X", (unsigned char)binary[i]);
|
||||
table->item(i, 8)->setText(hex);
|
||||
}
|
||||
}
|
||||
|
||||
MessagesView::MessagesView(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
QLabel *title = new QLabel("MESSAGE TIME BYTES");
|
||||
main_layout->addWidget(title);
|
||||
|
||||
message_layout = new QVBoxLayout();
|
||||
main_layout->addLayout(message_layout);
|
||||
main_layout->addStretch();
|
||||
}
|
||||
|
||||
void MessagesView::setMessages(const std::list<CanData> &list) {
|
||||
auto begin = list.begin();
|
||||
std::advance(begin, std::max(0, (int)(list.size() - 100)));
|
||||
int j = 0;
|
||||
for (auto it = begin; it != list.end(); ++it) {
|
||||
QLabel *label;
|
||||
if (j >= messages.size()) {
|
||||
label = new QLabel();
|
||||
message_layout->addWidget(label);
|
||||
messages.push_back(label);
|
||||
} else {
|
||||
label = messages[j];
|
||||
}
|
||||
label->setText(it->hex_dat);
|
||||
++j;
|
||||
}
|
||||
}
|
||||
|
||||
EditMessageDialog::EditMessageDialog(const QString &id, QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle(tr("Edit message"));
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->addWidget(new QLabel(tr("ID: (%1)").arg(id)));
|
||||
|
||||
auto msg = const_cast<Msg *>(parser->getMsg(Parser::addressFromId(id)));
|
||||
QHBoxLayout *h_layout = new QHBoxLayout();
|
||||
h_layout->addWidget(new QLabel(tr("Name")));
|
||||
h_layout->addStretch();
|
||||
QLineEdit *name_edit = new QLineEdit(this);
|
||||
name_edit->setText(msg ? msg->name.c_str() : "untitled");
|
||||
h_layout->addWidget(name_edit);
|
||||
main_layout->addLayout(h_layout);
|
||||
|
||||
h_layout = new QHBoxLayout();
|
||||
h_layout->addWidget(new QLabel(tr("Size")));
|
||||
h_layout->addStretch();
|
||||
QSpinBox *size_spin = new QSpinBox(this);
|
||||
size_spin->setValue(msg ? msg->size : parser->can_msgs[id].back().dat.size());
|
||||
h_layout->addWidget(size_spin);
|
||||
main_layout->addLayout(h_layout);
|
||||
|
||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
main_layout->addWidget(buttonBox);
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, [=]() {
|
||||
if (size_spin->value() <= 0 || name_edit->text().isEmpty()) return;
|
||||
|
||||
if (msg) {
|
||||
msg->name = name_edit->text().toStdString();
|
||||
msg->size = size_spin->value();
|
||||
} else {
|
||||
Msg m = {};
|
||||
m.address = Parser::addressFromId(id);
|
||||
m.name = name_edit->text().toStdString();
|
||||
m.size = size_spin->value();
|
||||
parser->addNewMsg(m);
|
||||
}
|
||||
QDialog::accept();
|
||||
});
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
AddSignalDialog::AddSignalDialog(const QString &id, QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle(tr("Add signal to %1").arg(parser->getMsg(id)->name.c_str()));
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
Signal sig = {.name = "untitled"};
|
||||
auto form = new SignalForm(sig, this);
|
||||
main_layout->addWidget(form);
|
||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
main_layout->addWidget(buttonBox);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, [=]() {
|
||||
if (auto msg = const_cast<Msg *>(parser->getMsg(id))) {
|
||||
if (auto signal = form->getSignal()) {
|
||||
msg->sigs.push_back(*signal);
|
||||
}
|
||||
}
|
||||
QDialog::accept();
|
||||
});
|
||||
}
|
||||
102
tools/cabana/detailwidget.h
Normal file
102
tools/cabana/detailwidget.h
Normal file
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <optional>
|
||||
|
||||
#include "opendbc/can/common.h"
|
||||
#include "opendbc/can/common_dbc.h"
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "tools/cabana/parser.h"
|
||||
|
||||
class SignalForm : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SignalForm(const Signal &sig, QWidget *parent);
|
||||
std::optional<Signal> getSignal();
|
||||
QLineEdit *name, *unit, *comment, *val_desc;
|
||||
QSpinBox *size, *msb, *lsb, *factor, *offset, *min_val, *max_val;
|
||||
QComboBox *sign, *endianness;
|
||||
};
|
||||
|
||||
class MessagesView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MessagesView(QWidget *parent);
|
||||
void setMessages(const std::list<CanData> &data);
|
||||
std::vector<QLabel *> messages;
|
||||
QVBoxLayout *message_layout;
|
||||
};
|
||||
|
||||
class BinaryView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BinaryView(QWidget *parent);
|
||||
void setMsg(const QString &id);
|
||||
void setData(const QByteArray &binary);
|
||||
|
||||
QTableWidget *table;
|
||||
};
|
||||
|
||||
class SignalEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SignalEdit(const QString &id, const Signal &sig, int idx, QWidget *parent);
|
||||
void save();
|
||||
|
||||
signals:
|
||||
void removed();
|
||||
protected:
|
||||
void remove();
|
||||
QString id;
|
||||
QString name_;
|
||||
ElidedLabel *title;
|
||||
SignalForm *form;
|
||||
QWidget *edit_container;
|
||||
QPushButton *remove_btn;
|
||||
};
|
||||
|
||||
class DetailWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
DetailWidget(QWidget *parent);
|
||||
void setMsg(const QString &id);
|
||||
|
||||
public slots:
|
||||
void updateState();
|
||||
|
||||
protected:
|
||||
QLabel *name_label = nullptr;
|
||||
QPushButton *edit_btn, *add_sig_btn;
|
||||
QVBoxLayout *signal_edit_layout;
|
||||
Signal *sig = nullptr;
|
||||
MessagesView *messages_view;
|
||||
QString msg_id;
|
||||
BinaryView *binary_view;
|
||||
std::vector<SignalEdit *> signal_edit;
|
||||
};
|
||||
|
||||
class EditMessageDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
EditMessageDialog(const QString &id, QWidget *parent);
|
||||
};
|
||||
|
||||
class AddSignalDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AddSignalDialog(const QString &id, QWidget *parent);
|
||||
};
|
||||
38
tools/cabana/mainwin.cc
Normal file
38
tools/cabana/mainwin.cc
Normal file
@@ -0,0 +1,38 @@
|
||||
#include "tools/cabana/mainwin.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
MainWindow::MainWindow() : QWidget() {
|
||||
assert(parser != nullptr);
|
||||
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
QHBoxLayout *h_layout = new QHBoxLayout();
|
||||
main_layout->addLayout(h_layout);
|
||||
|
||||
messages_widget = new MessagesWidget(this);
|
||||
QObject::connect(messages_widget, &MessagesWidget::msgChanged, [=](const QString &id) {
|
||||
detail_widget->setMsg(id);
|
||||
});
|
||||
h_layout->addWidget(messages_widget);
|
||||
|
||||
detail_widget = new DetailWidget(this);
|
||||
h_layout->addWidget(detail_widget);
|
||||
|
||||
// right widget
|
||||
QWidget *right_container = new QWidget(this);
|
||||
right_container->setFixedWidth(640);
|
||||
QVBoxLayout *r_layout = new QVBoxLayout(right_container);
|
||||
video_widget = new VideoWidget(this);
|
||||
r_layout->addWidget(video_widget);
|
||||
|
||||
charts_widget = new ChartsWidget(this);
|
||||
QScrollArea *scroll = new QScrollArea(this);
|
||||
scroll->setWidget(charts_widget);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
||||
r_layout->addWidget(scroll);
|
||||
|
||||
h_layout->addWidget(right_container);
|
||||
}
|
||||
22
tools/cabana/mainwin.h
Normal file
22
tools/cabana/mainwin.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "tools/cabana/chartswidget.h"
|
||||
#include "tools/cabana/detailwidget.h"
|
||||
#include "tools/cabana/messageswidget.h"
|
||||
#include "tools/cabana/parser.h"
|
||||
#include "tools/cabana/videowidget.h"
|
||||
|
||||
class MainWindow : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow();
|
||||
|
||||
protected:
|
||||
VideoWidget *video_widget;
|
||||
MessagesWidget *messages_widget;
|
||||
DetailWidget *detail_widget;
|
||||
ChartsWidget *charts_widget;
|
||||
};
|
||||
94
tools/cabana/messageswidget.cc
Normal file
94
tools/cabana/messageswidget.cc
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "tools/cabana/messageswidget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDebug>
|
||||
#include <QHeaderView>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <bitset>
|
||||
|
||||
MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
|
||||
QHBoxLayout *dbc_file_layout = new QHBoxLayout();
|
||||
QComboBox *combo = new QComboBox(this);
|
||||
auto dbc_names = get_dbc_names();
|
||||
for (const auto &name : dbc_names) {
|
||||
combo->addItem(QString::fromStdString(name));
|
||||
}
|
||||
connect(combo, &QComboBox::currentTextChanged, [=](const QString &dbc) {
|
||||
parser->openDBC(dbc);
|
||||
});
|
||||
// For test purpose
|
||||
combo->setCurrentText("toyota_nodsu_pt_generated");
|
||||
dbc_file_layout->addWidget(combo);
|
||||
|
||||
dbc_file_layout->addStretch();
|
||||
QPushButton *save_btn = new QPushButton(tr("Save DBC"), this);
|
||||
QObject::connect(save_btn, &QPushButton::clicked, [=]() {
|
||||
// TODO: save DBC to file
|
||||
});
|
||||
dbc_file_layout->addWidget(save_btn);
|
||||
|
||||
main_layout->addLayout(dbc_file_layout);
|
||||
|
||||
filter = new QLineEdit(this);
|
||||
filter->setPlaceholderText(tr("filter messages"));
|
||||
main_layout->addWidget(filter);
|
||||
|
||||
table_widget = new QTableWidget(this);
|
||||
table_widget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table_widget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
||||
table_widget->setColumnCount(4);
|
||||
table_widget->setColumnWidth(0, 250);
|
||||
table_widget->setColumnWidth(1, 80);
|
||||
table_widget->setColumnWidth(2, 80);
|
||||
table_widget->setHorizontalHeaderLabels({tr("Name"), tr("ID"), tr("Count"), tr("Bytes")});
|
||||
table_widget->horizontalHeader()->setStretchLastSection(true);
|
||||
QObject::connect(table_widget, &QTableWidget::itemSelectionChanged, [=]() {
|
||||
auto id = table_widget->selectedItems()[0]->data(Qt::UserRole);
|
||||
emit msgChanged(id.toString());
|
||||
});
|
||||
main_layout->addWidget(table_widget);
|
||||
|
||||
connect(parser, &Parser::updated, this, &MessagesWidget::updateState);
|
||||
}
|
||||
|
||||
void MessagesWidget::updateState() {
|
||||
auto getTableItem = [=](int row, int col) -> QTableWidgetItem * {
|
||||
auto item = table_widget->item(row, col);
|
||||
if (!item) {
|
||||
item = new QTableWidgetItem();
|
||||
table_widget->setItem(row, col, item);
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
table_widget->setRowCount(parser->can_msgs.size());
|
||||
int i = 0;
|
||||
const QString filter_str = filter->text().toLower();
|
||||
for (const auto &[id, list] : parser->can_msgs) {
|
||||
assert(!list.empty());
|
||||
|
||||
QString name;
|
||||
if (auto msg = parser->getMsg(list.back().address)) {
|
||||
name = msg->name.c_str();
|
||||
} else {
|
||||
name = tr("untitled");
|
||||
}
|
||||
if (!filter_str.isEmpty() && !name.toLower().contains(filter_str)) {
|
||||
table_widget->hideRow(i++);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto item = getTableItem(i, 0);
|
||||
item->setText(name);
|
||||
item->setData(Qt::UserRole, id);
|
||||
getTableItem(i, 1)->setText(id);
|
||||
getTableItem(i, 2)->setText(QString("%1").arg(parser->counters[id]));
|
||||
getTableItem(i, 3)->setText(list.back().hex_dat);
|
||||
table_widget->showRow(i);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
24
tools/cabana/messageswidget.h
Normal file
24
tools/cabana/messageswidget.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QTableWidget>
|
||||
#include <QWidget>
|
||||
|
||||
#include "tools/cabana/parser.h"
|
||||
|
||||
class MessagesWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MessagesWidget(QWidget *parent);
|
||||
|
||||
public slots:
|
||||
void updateState();
|
||||
|
||||
signals:
|
||||
void msgChanged(const QString &id);
|
||||
|
||||
protected:
|
||||
QLineEdit *filter;
|
||||
QTableWidget *table_widget;
|
||||
};
|
||||
98
tools/cabana/parser.cc
Normal file
98
tools/cabana/parser.cc
Normal file
@@ -0,0 +1,98 @@
|
||||
#include "tools/cabana/parser.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include "cereal/messaging/messaging.h"
|
||||
|
||||
Parser::Parser(QObject *parent) : QObject(parent) {
|
||||
qRegisterMetaType<std::vector<CanData>>();
|
||||
QObject::connect(this, &Parser::received, this, &Parser::process, Qt::QueuedConnection);
|
||||
|
||||
thread = new QThread();
|
||||
connect(thread, &QThread::started, [=]() { recvThread(); });
|
||||
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
||||
thread->start();
|
||||
}
|
||||
|
||||
Parser::~Parser() {
|
||||
replay->stop();
|
||||
exit = true;
|
||||
thread->quit();
|
||||
thread->wait();
|
||||
}
|
||||
|
||||
bool Parser::loadRoute(const QString &route, const QString &data_dir, bool use_qcam) {
|
||||
replay = new Replay(route, {"can", "roadEncodeIdx"}, {}, nullptr, use_qcam ? REPLAY_FLAG_QCAMERA : 0, data_dir, this);
|
||||
if (!replay->load()) {
|
||||
return false;
|
||||
}
|
||||
replay->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Parser::openDBC(const QString &name) {
|
||||
dbc_name = name;
|
||||
dbc = const_cast<DBC *>(dbc_lookup(name.toStdString()));
|
||||
msg_map.clear();
|
||||
for (auto &msg : dbc->msgs) {
|
||||
msg_map[msg.address] = &msg;
|
||||
}
|
||||
}
|
||||
|
||||
void Parser::process(std::vector<CanData> can) {
|
||||
for (auto &data : can) {
|
||||
++counters[data.id];
|
||||
auto &list = can_msgs[data.id];
|
||||
while (list.size() > DATA_LIST_SIZE) {
|
||||
list.pop_front();
|
||||
}
|
||||
list.push_back(data);
|
||||
}
|
||||
emit updated();
|
||||
}
|
||||
|
||||
void Parser::recvThread() {
|
||||
AlignedBuffer aligned_buf;
|
||||
std::unique_ptr<Context> context(Context::create());
|
||||
std::unique_ptr<SubSocket> subscriber(SubSocket::create(context.get(), "can"));
|
||||
subscriber->setTimeout(100);
|
||||
while (!exit) {
|
||||
std::unique_ptr<Message> msg(subscriber->receive());
|
||||
if (!msg) continue;
|
||||
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(msg.get()));
|
||||
cereal::Event::Reader event = cmsg.getRoot<cereal::Event>();
|
||||
std::vector<CanData> can;
|
||||
can.reserve(event.getCan().size());
|
||||
for (const auto &c : event.getCan()) {
|
||||
CanData &data = can.emplace_back();
|
||||
data.address = c.getAddress();
|
||||
data.bus_time = c.getBusTime();
|
||||
data.source = c.getSrc();
|
||||
data.dat.append((char *)c.getDat().begin(), c.getDat().size());
|
||||
data.hex_dat = data.dat.toHex(' ').toUpper();
|
||||
data.id = QString("%1:%2").arg(data.source).arg(data.address, 1, 16);
|
||||
data.ts = (event.getLogMonoTime() - replay->routeStartTime()) / (double)1e6;
|
||||
}
|
||||
emit received(can);
|
||||
}
|
||||
}
|
||||
|
||||
void Parser::addNewMsg(const Msg &msg) {
|
||||
dbc->msgs.push_back(msg);
|
||||
msg_map[dbc->msgs.back().address] = &dbc->msgs.back();
|
||||
}
|
||||
|
||||
void Parser::removeSignal(const QString &id, const QString &sig_name) {
|
||||
Msg *msg = const_cast<Msg *>(getMsg(id));
|
||||
if (!msg) return;
|
||||
|
||||
auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); });
|
||||
if (it != msg->sigs.end()) {
|
||||
msg->sigs.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Parser::addressFromId(const QString &id) {
|
||||
return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16);
|
||||
}
|
||||
66
tools/cabana/parser.h
Normal file
66
tools/cabana/parser.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <QApplication>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
|
||||
#include "opendbc/can/common.h"
|
||||
#include "opendbc/can/common_dbc.h"
|
||||
#include "tools/replay/replay.h"
|
||||
|
||||
const int DATA_LIST_SIZE = 500;
|
||||
// const int FPS = 20;
|
||||
|
||||
struct CanData {
|
||||
QString id;
|
||||
double ts;
|
||||
uint32_t address;
|
||||
uint16_t bus_time;
|
||||
uint8_t source;
|
||||
QByteArray dat;
|
||||
QString hex_dat;
|
||||
};
|
||||
|
||||
class Parser : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Parser(QObject *parent);
|
||||
~Parser();
|
||||
static uint32_t addressFromId(const QString &id);
|
||||
bool loadRoute(const QString &route, const QString &data_dir, bool use_qcam);
|
||||
void openDBC(const QString &name);
|
||||
void saveDBC(const QString &name) {}
|
||||
void addNewMsg(const Msg &msg);
|
||||
void removeSignal(const QString &id, const QString &sig_name);
|
||||
const Msg *getMsg(const QString &id) {
|
||||
return getMsg(addressFromId(id));
|
||||
}
|
||||
const Msg *getMsg(uint32_t address) {
|
||||
auto it = msg_map.find(address);
|
||||
return it != msg_map.end() ? it->second : nullptr;
|
||||
}
|
||||
signals:
|
||||
void showPlot(const QString &id, const QString &name);
|
||||
void hidePlot(const QString &id, const QString &name);
|
||||
void received(std::vector<CanData> can);
|
||||
void updated();
|
||||
|
||||
public:
|
||||
void recvThread();
|
||||
void process(std::vector<CanData> can);
|
||||
QThread *thread;
|
||||
QString dbc_name;
|
||||
std::atomic<bool> exit = false;
|
||||
std::map<QString, std::list<CanData>> can_msgs;
|
||||
std::map<QString, uint64_t> counters;
|
||||
Replay *replay = nullptr;
|
||||
DBC *dbc = nullptr;
|
||||
std::map<uint32_t, const Msg *> msg_map;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(std::vector<CanData>);
|
||||
|
||||
extern Parser *parser;
|
||||
80
tools/cabana/videowidget.cc
Normal file
80
tools/cabana/videowidget.cc
Normal file
@@ -0,0 +1,80 @@
|
||||
#include "tools/cabana/videowidget.h"
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QDateTime>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "tools/cabana/parser.h"
|
||||
|
||||
inline QString formatTime(int seconds) {
|
||||
return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh::mm::ss" : "mm::ss");
|
||||
}
|
||||
|
||||
VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
|
||||
cam_widget = new CameraViewWidget("camerad", VISION_STREAM_ROAD, true, this);
|
||||
cam_widget->setFixedSize(640, 480);
|
||||
main_layout->addWidget(cam_widget);
|
||||
|
||||
// slider controls
|
||||
QHBoxLayout *slider_layout = new QHBoxLayout();
|
||||
QLabel *time_label = new QLabel("00:00");
|
||||
slider_layout->addWidget(time_label);
|
||||
|
||||
slider = new QSlider(Qt::Horizontal, this);
|
||||
// slider->setFixedWidth(640);
|
||||
slider->setSingleStep(1);
|
||||
slider->setMaximum(parser->replay->totalSeconds());
|
||||
QObject::connect(slider, &QSlider::sliderReleased, [=]() {
|
||||
time_label->setText(formatTime(slider->value()));
|
||||
parser->replay->seekTo(slider->value(), false);
|
||||
});
|
||||
slider_layout->addWidget(slider);
|
||||
|
||||
QLabel *total_time_label = new QLabel(formatTime(parser->replay->totalSeconds()));
|
||||
slider_layout->addWidget(total_time_label);
|
||||
|
||||
main_layout->addLayout(slider_layout);
|
||||
|
||||
// btn controls
|
||||
QHBoxLayout *control_layout = new QHBoxLayout();
|
||||
QPushButton *play = new QPushButton("⏸");
|
||||
play->setStyleSheet("font-weight:bold");
|
||||
QObject::connect(play, &QPushButton::clicked, [=]() {
|
||||
bool is_paused = parser->replay->isPaused();
|
||||
play->setText(is_paused ? "⏸" : "▶");
|
||||
parser->replay->pause(!is_paused);
|
||||
});
|
||||
control_layout->addWidget(play);
|
||||
|
||||
QButtonGroup *group = new QButtonGroup(this);
|
||||
group->setExclusive(true);
|
||||
for (float speed : {0.1, 0.5, 1., 2.}) {
|
||||
QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this);
|
||||
btn->setCheckable(true);
|
||||
QObject::connect(btn, &QPushButton::clicked, [=]() {
|
||||
parser->replay->setSpeed(speed);
|
||||
});
|
||||
control_layout->addWidget(btn);
|
||||
group->addButton(btn);
|
||||
if (speed == 1.0) btn->setChecked(true);
|
||||
}
|
||||
|
||||
main_layout->addLayout(control_layout);
|
||||
|
||||
QTimer *timer = new QTimer(this);
|
||||
timer->setInterval(1000);
|
||||
timer->callOnTimeout([=]() {
|
||||
int current_seconds = parser->replay->currentSeconds();
|
||||
time_label->setText(formatTime(current_seconds));
|
||||
if (!slider->isSliderDown()) {
|
||||
slider->setValue(current_seconds);
|
||||
}
|
||||
});
|
||||
timer->start();
|
||||
}
|
||||
17
tools/cabana/videowidget.h
Normal file
17
tools/cabana/videowidget.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSlider>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h"
|
||||
|
||||
class VideoWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
VideoWidget(QWidget *parnet = nullptr);
|
||||
|
||||
protected:
|
||||
CameraViewWidget *cam_widget;
|
||||
QSlider *slider;
|
||||
};
|
||||
@@ -18,6 +18,7 @@ if arch in ['x86_64', 'Darwin'] or GetOption('extras'):
|
||||
replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc"]
|
||||
|
||||
replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=qt_libs, FRAMEWORKS=base_frameworks)
|
||||
Export('replay_lib')
|
||||
replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'ncurses'] + qt_libs
|
||||
qt_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks)
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ void Replay::stream() {
|
||||
|
||||
if (cur_which < sockets_.size() && sockets_[cur_which] != nullptr) {
|
||||
// keep time
|
||||
long etime = cur_mono_time_ - evt_start_ts;
|
||||
long etime = (cur_mono_time_ - evt_start_ts) / speed_;
|
||||
long rtime = nanos_since_boot() - loop_start_ts;
|
||||
long behind_ns = etime - rtime;
|
||||
// if behind_ns is greater than 1 second, it means that an invalid segemnt is skipped by seeking/replaying
|
||||
|
||||
@@ -52,8 +52,11 @@ public:
|
||||
inline void removeFlag(REPLAY_FLAGS flag) { flags_ &= ~flag; }
|
||||
inline const Route* route() const { return route_.get(); }
|
||||
inline int currentSeconds() const { return (cur_mono_time_ - route_start_ts_) / 1e9; }
|
||||
inline uint64_t routeStartTime() const { return route_start_ts_; }
|
||||
inline int toSeconds(uint64_t mono_time) const { return (mono_time - route_start_ts_) / 1e9; }
|
||||
inline int totalSeconds() const { return segments_.size() * 60; }
|
||||
inline void setSpeed(float speed) { speed_ = speed; }
|
||||
inline float getSpeed() const { return speed_; }
|
||||
inline const std::string &carFingerprint() const { return car_fingerprint_; }
|
||||
inline const std::vector<std::tuple<int, int, TimelineType>> getTimeline() {
|
||||
std::lock_guard lk(timeline_lock);
|
||||
@@ -112,4 +115,5 @@ protected:
|
||||
QFuture<void> timeline_future;
|
||||
std::vector<std::tuple<int, int, TimelineType>> timeline;
|
||||
std::string car_fingerprint_;
|
||||
float speed_ = 1.0;
|
||||
};
|
||||
|
||||
@@ -63,6 +63,7 @@ function install_ubuntu_common_requirements() {
|
||||
qttools5-dev-tools \
|
||||
libqt5sql5-sqlite \
|
||||
libqt5svg5-dev \
|
||||
libqt5charts5-dev \
|
||||
libqt5x11extras5-dev \
|
||||
libreadline-dev \
|
||||
libdw1 \
|
||||
|
||||
Reference in New Issue
Block a user