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 keypull/25957/head
parent
bc7be114d8
commit
1b8324af87
23 changed files with 1218 additions and 1 deletions
@ -0,0 +1,4 @@ |
|||||||
|
moc_* |
||||||
|
*.moc |
||||||
|
|
||||||
|
_cabana |
@ -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) |
@ -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) |
@ -0,0 +1,4 @@ |
|||||||
|
#!/bin/sh |
||||||
|
cd "$(dirname "$0")" |
||||||
|
export LD_LIBRARY_PATH="../../opendbc/can:$LD_LIBRARY_PATH" |
||||||
|
exec ./_cabana "$1" |
@ -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(); |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
}; |
@ -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(); |
||||||
|
}); |
||||||
|
} |
@ -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); |
||||||
|
}; |
@ -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); |
||||||
|
} |
@ -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; |
||||||
|
}; |
@ -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++; |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
}; |
@ -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); |
||||||
|
} |
@ -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; |
@ -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(); |
||||||
|
} |
@ -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; |
||||||
|
}; |
Loading…
Reference in new issue