canbana: complete basic functions (#25965)

* add chart header

* get all signal val from logs

* loop in selected range

* clear list before append

* automatically zoom on yaxis

* cleanup

* sync charts

* fix event_begin_sec

* set the color of rubber

* add TODO

* sync slider with charts

* keep video aspect ratio

* sync plot buttons

* reduce flickers

* cleanup

* refactor detail view

* clear counters

* more

use qcamera
old-commit-hash: a6ba073231
taco
Dean Lee 3 years ago committed by GitHub
parent 73f0c74b9b
commit f93f4e9f9b
  1. 3
      tools/cabana/SConscript
  2. 6
      tools/cabana/cabana.cc
  3. 206
      tools/cabana/chartswidget.cc
  4. 52
      tools/cabana/chartswidget.h
  5. 421
      tools/cabana/detailwidget.cc
  6. 86
      tools/cabana/detailwidget.h
  7. 9
      tools/cabana/mainwin.cc
  8. 4
      tools/cabana/mainwin.h
  9. 35
      tools/cabana/messageswidget.cc
  10. 2
      tools/cabana/messageswidget.h
  11. 114
      tools/cabana/parser.cc
  12. 58
      tools/cabana/parser.h
  13. 203
      tools/cabana/signaledit.cc
  14. 50
      tools/cabana/signaledit.h
  15. 49
      tools/cabana/videowidget.cc
  16. 5
      tools/cabana/videowidget.h
  17. 18
      tools/replay/logreader.cc
  18. 7
      tools/replay/logreader.h
  19. 21
      tools/replay/replay.cc
  20. 9
      tools/replay/replay.h
  21. 6
      tools/replay/route.cc
  22. 3
      tools/replay/route.h

@ -16,6 +16,5 @@ if arch in ['x86_64', 'Darwin'] and GetOption('extras'):
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
Import('replay_lib') Import('replay_lib')
# 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 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) qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'videowidget.cc', 'signaledit.cc', 'parser.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)

@ -5,7 +5,6 @@
#include "tools/cabana/mainwin.h" #include "tools/cabana/mainwin.h"
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
Parser *parser = nullptr;
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
initApp(argc, argv); initApp(argc, argv);
@ -16,7 +15,6 @@ int main(int argc, char *argv[]) {
cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai"); 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({"demo", "use a demo route instead of providing your own"});
cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"}); cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"});
cmd_parser.addOption({"qcam", "use qcamera"});
cmd_parser.process(app); cmd_parser.process(app);
const QStringList args = cmd_parser.positionalArguments(); const QStringList args = cmd_parser.positionalArguments();
if (args.empty() && !cmd_parser.isSet("demo")) { if (args.empty() && !cmd_parser.isSet("demo")) {
@ -24,8 +22,8 @@ int main(int argc, char *argv[]) {
} }
const QString route = args.empty() ? DEMO_ROUTE : args.first(); const QString route = args.empty() ? DEMO_ROUTE : args.first();
parser = new Parser(&app); Parser p(&app);
if (!parser->loadRoute(route, cmd_parser.value("data_dir"), cmd_parser.isSet("qcam"))) { if (!p.loadRoute(route, cmd_parser.value("data_dir"), true)) {
return 0; return 0;
} }
MainWindow w; MainWindow w;

@ -1,20 +1,24 @@
#include "tools/cabana/chartswidget.h" #include "tools/cabana/chartswidget.h"
#include <QGraphicsLayout>
#include <QLabel>
#include <QPushButton>
#include <QRubberBand>
#include <QStackedLayout>
#include <QtCharts/QLineSeries> #include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis>
using namespace QtCharts; int64_t get_raw_value(uint8_t *data, size_t data_size, const Signal &sig) {
int64_t get_raw_value(const QByteArray &msg, const Signal &sig) {
int64_t ret = 0; int64_t ret = 0;
int i = sig.msb / 8; int i = sig.msb / 8;
int bits = sig.size; int bits = sig.size;
while (i >= 0 && i < msg.size() && bits > 0) { while (i >= 0 && i < data_size && bits > 0) {
int lsb = (int)(sig.lsb / 8) == i ? sig.lsb : i * 8; 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 msb = (int)(sig.msb / 8) == i ? sig.msb : (i + 1) * 8 - 1;
int size = msb - lsb + 1; int size = msb - lsb + 1;
uint64_t d = (msg[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1); uint64_t d = (data[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1);
ret |= d << (bits - size); ret |= d << (bits - size);
bits -= size; bits -= size;
@ -26,77 +30,163 @@ int64_t get_raw_value(const QByteArray &msg, const Signal &sig) {
ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
main_layout = new QVBoxLayout(this); main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0); main_layout->setContentsMargins(0, 0, 0, 0);
connect(parser, &Parser::updated, this, &ChartsWidget::updateState);
connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart); connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart);
connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart); connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart);
connect(parser, &Parser::signalRemoved, this, &ChartsWidget::removeChart);
} }
void ChartsWidget::addChart(const QString &id, const QString &sig_name) { void ChartsWidget::addChart(const QString &id, const QString &sig_name) {
const QString char_name = id + sig_name; const QString char_name = id + sig_name;
if (charts.find(char_name) == charts.end()) { if (charts.find(char_name) == charts.end()) {
QLineSeries *series = new QLineSeries(); auto chart = new ChartWidget(id, sig_name, this);
series->setUseOpenGL(true); main_layout->addWidget(chart);
auto chart = new QChart(); charts[char_name] = chart;
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) { void ChartsWidget::removeChart(const QString &id, const QString &sig_name) {
auto it = charts.find(id + sig_name); if (auto it = charts.find(id + sig_name); it != charts.end()) {
if (it == charts.end()) return; it->second->deleteLater();
charts.erase(it);
delete it->second.chart_view; }
charts.erase(it);
} }
void ChartsWidget::updateState() { ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *parent) : id(id), sig_name(sig_name), QWidget(parent) {
static double last_update = millis_since_boot(); QStackedLayout *stacked = new QStackedLayout(this);
double current_ts = millis_since_boot(); stacked->setStackingMode(QStackedLayout::StackAll);
bool update = (current_ts - last_update) > 500;
if (update) { QWidget *chart_widget = new QWidget(this);
last_update = current_ts; QVBoxLayout *chart_layout = new QVBoxLayout(chart_widget);
chart_layout->setSpacing(0);
chart_layout->setContentsMargins(0, 0, 0, 0);
QWidget *header = new QWidget(this);
header->setStyleSheet("background-color:white");
QHBoxLayout *header_layout = new QHBoxLayout(header);
header_layout->setContentsMargins(11, 11, 11, 0);
auto title = new QLabel(tr("%1 %2").arg(parser->getMsg(id)->name.c_str()).arg(id));
header_layout->addWidget(title);
header_layout->addStretch();
zoom_label = new QLabel("", this);
header_layout->addWidget(zoom_label);
QPushButton *zoom_in = new QPushButton("", this);
zoom_in->setToolTip(tr("reset zoom"));
QObject::connect(zoom_in, &QPushButton::clicked, []() { parser->resetRange(); });
header_layout->addWidget(zoom_in);
QPushButton *remove_btn = new QPushButton("", this);
QObject::connect(remove_btn, &QPushButton::clicked, [=]() {
emit parser->hidePlot(id, sig_name);
});
header_layout->addWidget(remove_btn);
chart_layout->addWidget(header);
QLineSeries *series = new QLineSeries();
series->setUseOpenGL(true);
auto chart = new QChart();
chart->setTitle(sig_name);
chart->addSeries(series);
chart->createDefaultAxes();
chart->legend()->hide();
QFont font;
font.setBold(true);
chart->setTitleFont(font);
chart->setMargins({0, 0, 0, 0});
chart->layout()->setContentsMargins(0, 0, 0, 0);
QObject::connect(dynamic_cast<QValueAxis *>(chart->axisX()), &QValueAxis::rangeChanged, parser, &Parser::setRange);
chart_view = new QChartView(chart);
chart_view->setFixedHeight(300);
chart_view->setRenderHint(QPainter::Antialiasing);
chart_view->setRubberBand(QChartView::HorizontalRubberBand);
if (auto rubber = chart_view->findChild<QRubberBand *>()) {
QPalette pal;
pal.setBrush(QPalette::Base, QColor(0, 0, 0, 80));
rubber->setPalette(pal);
} }
chart_layout->addWidget(chart_view);
chart_layout->addStretch();
auto getSig = [=](const QString &id, const QString &name) -> const Signal * { stacked->addWidget(chart_widget);
for (auto &sig : parser->getMsg(id)->sigs) { line_marker = new LineMarker(chart, this);
if (name == sig.name.c_str()) return &sig; stacked->addWidget(line_marker);
} line_marker->setAttribute(Qt::WA_TransparentForMouseEvents, true);
return nullptr; line_marker->raise();
};
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; setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
if (value < c.min_y) c.min_y = value; QObject::connect(parser, &Parser::updated, this, &ChartWidget::updateState);
QObject::connect(parser, &Parser::rangeChanged, this, &ChartWidget::rangeChanged);
QObject::connect(parser, &Parser::eventsMerged, this, &ChartWidget::updateSeries);
while (c.data.size() > DATA_LIST_SIZE) { updateSeries();
c.data.pop_front(); }
}
c.data.push_back({can_data.ts / 1000., value}); void ChartWidget::updateState() {
line_marker->update();
if (update) { }
QChart *chart = c.chart_view->chart();
QLineSeries *series = (QLineSeries *)chart->series()[0]; void ChartWidget::updateSeries() {
series->replace(c.data); const Signal *sig = parser->getSig(id, sig_name);
chart->axisX()->setRange(c.data.front().x(), c.data.back().x()); auto events = parser->replay->events();
chart->axisY()->setRange(c.min_y, c.max_y); if (!sig || !events) return;
auto l = id.split(':');
int bus = l[0].toInt();
uint32_t address = l[1].toUInt(nullptr, 16);
vals.clear();
vals.reserve(3 * 60 * 100);
uint64_t route_start_time = parser->replay->routeStartTime();
for (auto &evt : *events) {
if (evt->which == cereal::Event::Which::CAN) {
for (auto c : evt->event.getCan()) {
if (bus == c.getSrc() && address == c.getAddress()) {
auto dat = c.getDat();
int64_t val = get_raw_value((uint8_t *)dat.begin(), dat.size(), *sig);
if (sig->is_signed) {
val -= ((val >> (sig->size - 1)) & 0x1) ? (1ULL << sig->size) : 0;
}
double value = val * sig->factor + sig->offset;
double ts = (evt->mono_time - route_start_time) / (double)1e9; // seconds
vals.push_back({ts, value});
}
} }
} }
} }
QLineSeries *series = (QLineSeries *)chart_view->chart()->series()[0];
series->replace(vals);
auto [begin, end] = parser->range();
chart_view->chart()->axisX()->setRange(begin, end);
}
void ChartWidget::rangeChanged(qreal min, qreal max) {
auto axis_x = dynamic_cast<QValueAxis *>(chart_view->chart()->axisX());
if (axis_x->min() != min || axis_x->max() != max) {
axis_x->setRange(min, max);
}
// auto zoom on yaxis
double min_y = 0, max_y = 0;
for (auto &p : vals) {
if (p.x() > max) break;
if (p.x() >= min) {
if (p.y() < min_y) min_y = p.y();
if (p.y() > max_y) max_y = p.y();
}
}
chart_view->chart()->axisY()->setRange(min_y * 0.95, max_y * 1.05);
}
LineMarker::LineMarker(QChart *chart, QWidget *parent) : chart(chart), QWidget(parent) {}
void LineMarker::paintEvent(QPaintEvent *event) {
auto axis_x = dynamic_cast<QValueAxis *>(chart->axisX());
if (axis_x->max() <= axis_x->min()) return;
double x = chart->plotArea().left() + chart->plotArea().width() * (parser->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min());
QPainter p(this);
QPen pen = QPen(Qt::black);
pen.setWidth(2);
p.setPen(pen);
p.drawLine(QPointF{x, 50.}, QPointF{x, (qreal)height() - 11});
} }

@ -1,17 +1,53 @@
#pragma once #pragma once
#include <map>
#include <QLabel>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <QtCharts/QChartView> #include <QtCharts/QChartView>
#include <QtCharts/QLineSeries> #include <QtCharts/QLineSeries>
#include <map>
#include "tools/cabana/parser.h" #include "tools/cabana/parser.h"
using namespace QtCharts;
class LineMarker : public QWidget {
Q_OBJECT
public:
LineMarker(QChart *chart, QWidget *parent);
void paintEvent(QPaintEvent *event) override;
private:
QChart *chart;
};
class ChartWidget : public QWidget {
Q_OBJECT
public:
ChartWidget(const QString &id, const QString &sig_name, QWidget *parent);
inline QChart *chart() const { return chart_view->chart(); }
protected:
void updateState();
void addData(const CanData &can_data, const Signal &sig);
void updateSeries();
void rangeChanged(qreal min, qreal max);
QString id;
QString sig_name;
QLabel *zoom_label;
QChartView *chart_view = nullptr;
LineMarker *line_marker = nullptr;
QList<QPointF> vals;
};
class ChartsWidget : public QWidget { class ChartsWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
ChartsWidget(QWidget *parent = nullptr); ChartsWidget(QWidget *parent = nullptr);
inline bool hasChart(const QString &id, const QString &sig_name) { inline bool hasChart(const QString &id, const QString &sig_name) {
return charts.find(id+sig_name) != charts.end(); return charts.find(id+sig_name) != charts.end();
@ -20,15 +56,7 @@ class ChartsWidget : public QWidget {
void removeChart(const QString &id, const QString &sig_name); void removeChart(const QString &id, const QString &sig_name);
void updateState(); void updateState();
protected: protected:
QVBoxLayout *main_layout; QVBoxLayout *main_layout;
struct SignalChart { std::map<QString, ChartWidget *> charts;
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;
}; };

@ -1,68 +1,53 @@
#include "tools/cabana/detailwidget.h" #include "tools/cabana/detailwidget.h"
#include <QDebug> #include <QDebug>
#include <QDialogButtonBox>
#include <QHeaderView> #include <QHeaderView>
#include <QMessageBox>
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <bitset> #include <bitset>
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/widgets/scrollview.h"
const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"}; inline const QString &getColor(int i) {
static const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"};
static QVector<int> BIG_ENDIAN_START_BITS = []() { return SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)];
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::DetailWidget(QWidget *parent) : QWidget(parent) { DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); 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 = new QLabel(this);
name_label->setStyleSheet("font-weight:bold;"); name_label->setStyleSheet("font-weight:bold;");
name_layout->addWidget(name_label); name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
name_layout->addStretch(); name_label->setAlignment(Qt::AlignCenter);
main_layout->addWidget(name_label);
// title
QHBoxLayout *title_layout = new QHBoxLayout();
time_label = new QLabel(this);
title_layout->addWidget(time_label);
title_layout->addStretch();
edit_btn = new QPushButton(tr("Edit"), this); edit_btn = new QPushButton(tr("Edit"), this);
edit_btn->setVisible(false); edit_btn->setVisible(false);
QObject::connect(edit_btn, &QPushButton::clicked, [=]() { title_layout->addWidget(edit_btn);
EditMessageDialog dlg(msg_id, this); main_layout->addLayout(title_layout);
int ret = dlg.exec();
if (ret) {
setMsg(msg_id);
}
});
name_layout->addWidget(edit_btn);
main_layout->addLayout(name_layout);
// binary view
binary_view = new BinaryView(this); binary_view = new BinaryView(this);
main_layout->addWidget(binary_view); main_layout->addWidget(binary_view);
// scroll area
QHBoxLayout *signals_layout = new QHBoxLayout(); QHBoxLayout *signals_layout = new QHBoxLayout();
signals_layout->addWidget(new QLabel(tr("Signals"))); signals_layout->addWidget(new QLabel(tr("Signals")));
signals_layout->addStretch(); signals_layout->addStretch();
add_sig_btn = new QPushButton(tr("Add signal"), this); add_sig_btn = new QPushButton(tr("Add signal"), this);
add_sig_btn->setVisible(false); 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); signals_layout->addWidget(add_sig_btn);
main_layout->addLayout(signals_layout); main_layout->addLayout(signals_layout);
@ -72,238 +57,67 @@ DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) {
signal_edit_layout->setSpacing(2); signal_edit_layout->setSpacing(2);
container_layout->addLayout(signal_edit_layout); container_layout->addLayout(signal_edit_layout);
messages_view = new MessagesView(this); history_log = new HistoryLog(this);
container_layout->addWidget(messages_view); container_layout->addWidget(history_log);
QScrollArea *scroll = new QScrollArea(this); QScrollArea *scroll = new QScrollArea(this);
scroll->setWidget(container); scroll->setWidget(container);
scroll->setWidgetResizable(true); scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
main_layout->addWidget(scroll); main_layout->addWidget(scroll);
setFixedWidth(600);
connect(parser, &Parser::updated, this, &DetailWidget::updateState); QObject::connect(add_sig_btn, &QPushButton::clicked, this, &DetailWidget::addSignal);
QObject::connect(edit_btn, &QPushButton::clicked, this, &DetailWidget::editMsg);
QObject::connect(parser, &Parser::updated, this, &DetailWidget::updateState);
} }
void DetailWidget::updateState() { void DetailWidget::setMsg(const CanData *c) {
if (msg_id.isEmpty()) return; can_data = c;
clearLayout(signal_edit_layout);
auto &list = parser->can_msgs[msg_id]; edit_btn->setVisible(true);
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) { if (auto msg = parser->getMsg(can_data->address)) {
delete edit; name_label->setText(msg->name.c_str());
} add_sig_btn->setVisible(true);
signal_edit.clear(); for (int i = 0; i < msg->sigs.size(); ++i) {
int i = 0; signal_edit_layout->addWidget(new SignalEdit(can_data->id, msg->sigs[i], getColor(i)));
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(); } else {
name_label->setText(tr("untitled"));
add_sig_btn->setVisible(false);
} }
name_label->setText(name);
binary_view->setMsg(msg_id);
edit_btn->setVisible(true); binary_view->setMsg(can_data);
add_sig_btn->setVisible(msg != nullptr); history_log->clear();
} }
SignalEdit::SignalEdit(const QString &id, const Signal &sig, int idx, QWidget *parent) : id(id), name_(sig.name.c_str()), QWidget(parent) { void DetailWidget::updateState() {
QVBoxLayout *main_layout = new QVBoxLayout(this); if (!can_data) return;
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); time_label->setText(QString("time: %1").arg(can_data->ts, 0, 'f', 3));
QVBoxLayout *v_layout = new QVBoxLayout(edit_container); binary_view->setData(can_data->dat);
form = new SignalForm(sig, this); history_log->updateState();
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() { void DetailWidget::editMsg() {
Msg *msg = const_cast<Msg *>(parser->getMsg(id)); EditMessageDialog dlg(can_data->id, this);
if (!msg) return; if (dlg.exec()) {
setMsg(can_data);
for (auto &sig : msg->sigs) {
if (name_ == sig.name.c_str()) {
if (auto s = form->getSignal()) {
sig = *s;
}
break;
}
} }
} }
void SignalEdit::remove() { void DetailWidget::addSignal() {
QMessageBox msgbox; AddSignalDialog dlg(can_data->id, this);
msgbox.setText(tr("Remove signal")); if (dlg.exec()) {
msgbox.setInformativeText(tr("Are you sure you want to remove signal '%1'").arg(name_)); setMsg(can_data);
msgbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgbox.setDefaultButton(QMessageBox::Cancel);
if (msgbox.exec()) {
parser->removeSignal(id, name_);
emit removed();
} }
} }
// BinaryView
BinaryView::BinaryView(QWidget *parent) { BinaryView::BinaryView(QWidget *parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
table = new QTableWidget(this); table = new QTableWidget(this);
@ -316,15 +130,16 @@ BinaryView::BinaryView(QWidget *parent) {
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
} }
void BinaryView::setMsg(const QString &id) { void BinaryView::setMsg(const CanData *can_data) {
auto msg = parser->getMsg(Parser::addressFromId(id)); const Msg *msg = parser->getMsg(can_data->address);
int row_count = msg ? msg->size : parser->can_msgs[id].back().dat.size(); int row_count = msg ? msg->size : can_data->dat.size();
table->setRowCount(row_count); table->setRowCount(row_count);
table->setColumnCount(9); table->setColumnCount(9);
for (int i = 0; i < table->rowCount(); ++i) { for (int i = 0; i < table->rowCount(); ++i) {
for (int j = 0; j < table->columnCount(); ++j) { for (int j = 0; j < table->columnCount(); ++j) {
auto item = new QTableWidgetItem(); auto item = new QTableWidgetItem();
item->setFlags(item->flags() ^ Qt::ItemIsEditable);
item->setTextAlignment(Qt::AlignCenter); item->setTextAlignment(Qt::AlignCenter);
if (j == 8) { if (j == 8) {
QFont font; QFont font;
@ -336,19 +151,17 @@ void BinaryView::setMsg(const QString &id) {
} }
if (msg) { if (msg) {
// set background color
for (int i = 0; i < msg->sigs.size(); ++i) { for (int i = 0; i < msg->sigs.size(); ++i) {
const auto &sig = msg->sigs[i]; const auto &sig = msg->sigs[i];
int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit); int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit);
for (int j = start; j <= start + sig.size - 1; ++j) { 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)])); table->item(j / 8, j % 8)->setBackground(QColor(getColor(i)));
} }
} }
} }
setFixedHeight(table->rowHeight(0) * table->rowCount() + 25); 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) { void BinaryView::setData(const QByteArray &binary) {
@ -357,6 +170,7 @@ void BinaryView::setData(const QByteArray &binary) {
s += std::bitset<8>(binary[j]).to_string(); s += std::bitset<8>(binary[j]).to_string();
} }
setUpdatesEnabled(false);
char hex[3] = {'\0'}; char hex[3] = {'\0'};
for (int i = 0; i < binary.size(); ++i) { for (int i = 0; i < binary.size(); ++i) {
for (int j = 0; j < 8; ++j) { for (int j = 0; j < 8; ++j) {
@ -365,46 +179,58 @@ void BinaryView::setData(const QByteArray &binary) {
sprintf(&hex[0], "%02X", (unsigned char)binary[i]); sprintf(&hex[0], "%02X", (unsigned char)binary[i]);
table->item(i, 8)->setText(hex); table->item(i, 8)->setText(hex);
} }
setUpdatesEnabled(true);
} }
MessagesView::MessagesView(QWidget *parent) : QWidget(parent) { // HistoryLog
HistoryLog::HistoryLog(QWidget *parent) : QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
QLabel *title = new QLabel("MESSAGE TIME BYTES"); QLabel *title = new QLabel("TIME BYTES");
main_layout->addWidget(title); main_layout->addWidget(title);
message_layout = new QVBoxLayout(); QVBoxLayout *message_layout = new QVBoxLayout();
for (int i = 0; i < std::size(labels); ++i) {
labels[i] = new QLabel();
labels[i]->setVisible(false);
message_layout->addWidget(labels[i]);
}
main_layout->addLayout(message_layout); main_layout->addLayout(message_layout);
main_layout->addStretch(); main_layout->addStretch();
} }
void MessagesView::setMessages(const std::list<CanData> &list) { void HistoryLog::updateState() {
auto begin = list.begin(); int i = 0;
std::advance(begin, std::max(0, (int)(list.size() - 100))); for (; i < parser->history_log.size(); ++i) {
int j = 0; const auto &c = parser->history_log[i];
for (auto it = begin; it != list.end(); ++it) { auto label = labels[i];
QLabel *label; label->setVisible(true);
if (j >= messages.size()) { label->setText(QString("%1 %2").arg(c.ts, 0, 'f', 3).arg(c.hex_dat));
label = new QLabel(); }
message_layout->addWidget(label);
messages.push_back(label); for (; i < std::size(labels); ++i) {
} else { labels[i]->setVisible(false);
label = messages[j];
}
label->setText(it->hex_dat);
++j;
} }
} }
EditMessageDialog::EditMessageDialog(const QString &id, QWidget *parent) : QDialog(parent) { void HistoryLog::clear() {
setUpdatesEnabled(false);
for (auto l : labels) l->setVisible(false);
setUpdatesEnabled(true);
}
// EditMessageDialog
EditMessageDialog::EditMessageDialog(const QString &id, QWidget *parent) : id(id), QDialog(parent) {
setWindowTitle(tr("Edit message")); setWindowTitle(tr("Edit message"));
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->addWidget(new QLabel(tr("ID: (%1)").arg(id))); main_layout->addWidget(new QLabel(tr("ID: (%1)").arg(id)));
auto msg = const_cast<Msg *>(parser->getMsg(Parser::addressFromId(id))); auto msg = const_cast<Msg *>(parser->getMsg(id));
QHBoxLayout *h_layout = new QHBoxLayout(); QHBoxLayout *h_layout = new QHBoxLayout();
h_layout->addWidget(new QLabel(tr("Name"))); h_layout->addWidget(new QLabel(tr("Name")));
h_layout->addStretch(); h_layout->addStretch();
QLineEdit *name_edit = new QLineEdit(this); name_edit = new QLineEdit(this);
name_edit->setText(msg ? msg->name.c_str() : "untitled"); name_edit->setText(msg ? msg->name.c_str() : "untitled");
h_layout->addWidget(name_edit); h_layout->addWidget(name_edit);
main_layout->addLayout(h_layout); main_layout->addLayout(h_layout);
@ -412,47 +238,30 @@ EditMessageDialog::EditMessageDialog(const QString &id, QWidget *parent) : QDial
h_layout = new QHBoxLayout(); h_layout = new QHBoxLayout();
h_layout->addWidget(new QLabel(tr("Size"))); h_layout->addWidget(new QLabel(tr("Size")));
h_layout->addStretch(); h_layout->addStretch();
QSpinBox *size_spin = new QSpinBox(this); size_spin = new QSpinBox(this);
size_spin->setValue(msg ? msg->size : parser->can_msgs[id].back().dat.size()); size_spin->setValue(msg ? msg->size : parser->can_msgs[id].dat.size());
h_layout->addWidget(size_spin); h_layout->addWidget(size_spin);
main_layout->addLayout(h_layout); main_layout->addLayout(h_layout);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
main_layout->addWidget(buttonBox); main_layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, [=]() { connect(buttonBox, &QDialogButtonBox::accepted, this, &EditMessageDialog::save);
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); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
} }
AddSignalDialog::AddSignalDialog(const QString &id, QWidget *parent) : QDialog(parent) { void EditMessageDialog::save() {
setWindowTitle(tr("Add signal to %1").arg(parser->getMsg(id)->name.c_str())); if (size_spin->value() <= 0 || name_edit->text().isEmpty()) return;
QVBoxLayout *main_layout = new QVBoxLayout(this);
Signal sig = {.name = "untitled"}; if (auto msg = const_cast<Msg *>(parser->getMsg(id))) {
auto form = new SignalForm(sig, this); msg->name = name_edit->text().toStdString();
main_layout->addWidget(form); msg->size = size_spin->value();
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); } else {
main_layout->addWidget(buttonBox); Msg m = {};
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); m.address = Parser::addressFromId(id);
connect(buttonBox, &QDialogButtonBox::accepted, [=]() { m.name = name_edit->text().toStdString();
if (auto msg = const_cast<Msg *>(parser->getMsg(id))) { m.size = size_spin->value();
if (auto signal = form->getSignal()) { parser->addNewMsg(m);
msg->sigs.push_back(*signal); }
} QDialog::accept();
}
QDialog::accept();
});
} }

@ -1,102 +1,70 @@
#pragma once #pragma once
#include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QDialogButtonBox>
#include <QLabel> #include <QLabel>
#include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QSpinBox>
#include <QTableWidget> #include <QTableWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <optional>
#include "opendbc/can/common.h" #include "opendbc/can/common.h"
#include "opendbc/can/common_dbc.h" #include "opendbc/can/common_dbc.h"
#include "selfdrive/ui/qt/widgets/controls.h"
#include "tools/cabana/parser.h" #include "tools/cabana/parser.h"
#include "tools/cabana/signaledit.h"
class SignalForm : public QWidget { class HistoryLog : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
SignalForm(const Signal &sig, QWidget *parent); HistoryLog(QWidget *parent);
std::optional<Signal> getSignal(); void clear();
QLineEdit *name, *unit, *comment, *val_desc; void updateState();
QSpinBox *size, *msb, *lsb, *factor, *offset, *min_val, *max_val;
QComboBox *sign, *endianness;
};
class MessagesView : public QWidget {
Q_OBJECT
public: private:
MessagesView(QWidget *parent); QLabel *labels[LOG_SIZE] = {};
void setMessages(const std::list<CanData> &data);
std::vector<QLabel *> messages;
QVBoxLayout *message_layout;
}; };
class BinaryView : public QWidget { class BinaryView : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
BinaryView(QWidget *parent); BinaryView(QWidget *parent);
void setMsg(const QString &id); void setMsg(const CanData *can_data);
void setData(const QByteArray &binary); void setData(const QByteArray &binary);
QTableWidget *table; QTableWidget *table;
}; };
class SignalEdit : public QWidget { class EditMessageDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
SignalEdit(const QString &id, const Signal &sig, int idx, QWidget *parent); EditMessageDialog(const QString &id, QWidget *parent);
protected:
void save(); void save();
signals: QLineEdit *name_edit;
void removed(); QSpinBox *size_spin;
protected:
void remove();
QString id; QString id;
QString name_;
ElidedLabel *title;
SignalForm *form;
QWidget *edit_container;
QPushButton *remove_btn;
}; };
class DetailWidget : public QWidget { class DetailWidget : public QWidget {
Q_OBJECT Q_OBJECT
public:
public:
DetailWidget(QWidget *parent); DetailWidget(QWidget *parent);
void setMsg(const QString &id); void setMsg(const CanData *c);
public slots: private:
void updateState(); void updateState();
void addSignal();
void editMsg();
protected: const CanData *can_data = nullptr;
QLabel *name_label = nullptr; QLabel *name_label, *time_label;
QPushButton *edit_btn, *add_sig_btn; QPushButton *edit_btn, *add_sig_btn;
QVBoxLayout *signal_edit_layout; QVBoxLayout *signal_edit_layout;
Signal *sig = nullptr; HistoryLog *history_log;
MessagesView *messages_view;
QString msg_id;
BinaryView *binary_view; 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);
}; };

@ -4,19 +4,15 @@
#include <QVBoxLayout> #include <QVBoxLayout>
MainWindow::MainWindow() : QWidget() { MainWindow::MainWindow() : QWidget() {
assert(parser != nullptr);
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
QHBoxLayout *h_layout = new QHBoxLayout(); QHBoxLayout *h_layout = new QHBoxLayout();
main_layout->addLayout(h_layout); main_layout->addLayout(h_layout);
messages_widget = new MessagesWidget(this); messages_widget = new MessagesWidget(this);
QObject::connect(messages_widget, &MessagesWidget::msgChanged, [=](const QString &id) {
detail_widget->setMsg(id);
});
h_layout->addWidget(messages_widget); h_layout->addWidget(messages_widget);
detail_widget = new DetailWidget(this); detail_widget = new DetailWidget(this);
detail_widget->setFixedWidth(600);
h_layout->addWidget(detail_widget); h_layout->addWidget(detail_widget);
// right widget // right widget
@ -30,9 +26,12 @@ MainWindow::MainWindow() : QWidget() {
QScrollArea *scroll = new QScrollArea(this); QScrollArea *scroll = new QScrollArea(this);
scroll->setWidget(charts_widget); scroll->setWidget(charts_widget);
scroll->setWidgetResizable(true); scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
r_layout->addWidget(scroll); r_layout->addWidget(scroll);
h_layout->addWidget(right_container); h_layout->addWidget(right_container);
QObject::connect(messages_widget, &MessagesWidget::msgChanged, detail_widget, &DetailWidget::setMsg);
} }

@ -11,10 +11,10 @@
class MainWindow : public QWidget { class MainWindow : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
MainWindow(); MainWindow();
protected: protected:
VideoWidget *video_widget; VideoWidget *video_widget;
MessagesWidget *messages_widget; MessagesWidget *messages_widget;
DetailWidget *detail_widget; DetailWidget *detail_widget;

@ -1,11 +1,9 @@
#include "tools/cabana/messageswidget.h" #include "tools/cabana/messageswidget.h"
#include <QComboBox> #include <QComboBox>
#include <QDebug>
#include <QHeaderView> #include <QHeaderView>
#include <QPushButton> #include <QPushButton>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <bitset>
MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
@ -47,8 +45,9 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
table_widget->setHorizontalHeaderLabels({tr("Name"), tr("ID"), tr("Count"), tr("Bytes")}); table_widget->setHorizontalHeaderLabels({tr("Name"), tr("ID"), tr("Count"), tr("Bytes")});
table_widget->horizontalHeader()->setStretchLastSection(true); table_widget->horizontalHeader()->setStretchLastSection(true);
QObject::connect(table_widget, &QTableWidget::itemSelectionChanged, [=]() { QObject::connect(table_widget, &QTableWidget::itemSelectionChanged, [=]() {
auto id = table_widget->selectedItems()[0]->data(Qt::UserRole); const CanData *c = &(parser->can_msgs[table_widget->selectedItems()[1]->text()]);
emit msgChanged(id.toString()); parser->setCurrentMsg(c->id);
emit msgChanged(c);
}); });
main_layout->addWidget(table_widget); main_layout->addWidget(table_widget);
@ -60,6 +59,7 @@ void MessagesWidget::updateState() {
auto item = table_widget->item(row, col); auto item = table_widget->item(row, col);
if (!item) { if (!item) {
item = new QTableWidgetItem(); item = new QTableWidgetItem();
item->setFlags(item->flags() ^ Qt::ItemIsEditable);
table_widget->setItem(row, col, item); table_widget->setItem(row, col, item);
} }
return item; return item;
@ -67,28 +67,27 @@ void MessagesWidget::updateState() {
table_widget->setRowCount(parser->can_msgs.size()); table_widget->setRowCount(parser->can_msgs.size());
int i = 0; int i = 0;
const QString filter_str = filter->text().toLower(); QString name, untitled = tr("untitled");
for (const auto &[id, list] : parser->can_msgs) { const QString filter_str = filter->text();
assert(!list.empty()); for (const auto &[_, c] : parser->can_msgs) {
if (auto msg = parser->getMsg(c.address)) {
QString name;
if (auto msg = parser->getMsg(list.back().address)) {
name = msg->name.c_str(); name = msg->name.c_str();
} else { } else {
name = tr("untitled"); name = untitled;
} }
if (!filter_str.isEmpty() && !name.toLower().contains(filter_str)) { if (!filter_str.isEmpty() && !name.contains(filter_str, Qt::CaseInsensitive)) {
table_widget->hideRow(i++); table_widget->hideRow(i++);
continue; continue;
} }
auto item = getTableItem(i, 0); getTableItem(i, 0)->setText(name);
item->setText(name); getTableItem(i, 1)->setText(c.id);
item->setData(Qt::UserRole, id); getTableItem(i, 2)->setText(QString::number(parser->counters[c.id]));
getTableItem(i, 1)->setText(id); getTableItem(i, 3)->setText(c.hex_dat);
getTableItem(i, 2)->setText(QString("%1").arg(parser->counters[id]));
getTableItem(i, 3)->setText(list.back().hex_dat);
table_widget->showRow(i); table_widget->showRow(i);
i++; i++;
} }
if (table_widget->currentRow() == -1) {
table_widget->selectRow(0);
}
} }

@ -16,7 +16,7 @@ class MessagesWidget : public QWidget {
void updateState(); void updateState();
signals: signals:
void msgChanged(const QString &id); void msgChanged(const CanData *id);
protected: protected:
QLineEdit *filter; QLineEdit *filter;

@ -4,7 +4,11 @@
#include "cereal/messaging/messaging.h" #include "cereal/messaging/messaging.h"
Parser *parser = nullptr;
Parser::Parser(QObject *parent) : QObject(parent) { Parser::Parser(QObject *parent) : QObject(parent) {
parser = this;
qRegisterMetaType<std::vector<CanData>>(); qRegisterMetaType<std::vector<CanData>>();
QObject::connect(this, &Parser::received, this, &Parser::process, Qt::QueuedConnection); QObject::connect(this, &Parser::received, this, &Parser::process, Qt::QueuedConnection);
@ -23,32 +27,48 @@ Parser::~Parser() {
bool Parser::loadRoute(const QString &route, const QString &data_dir, bool use_qcam) { 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); replay = new Replay(route, {"can", "roadEncodeIdx"}, {}, nullptr, use_qcam ? REPLAY_FLAG_QCAMERA : 0, data_dir, this);
if (!replay->load()) { QObject::connect(replay, &Replay::segmentsMerged, this, &Parser::segmentsMerged);
return false; if (replay->load()) {
replay->start();
return true;
} }
replay->start(); return false;
return true;
} }
void Parser::openDBC(const QString &name) { void Parser::openDBC(const QString &name) {
dbc_name = name; dbc_name = name;
dbc = const_cast<DBC *>(dbc_lookup(name.toStdString())); dbc = const_cast<DBC *>(dbc_lookup(name.toStdString()));
counters.clear();
msg_map.clear(); msg_map.clear();
for (auto &msg : dbc->msgs) { for (auto &msg : dbc->msgs) {
msg_map[msg.address] = &msg; msg_map[msg.address] = &msg;
} }
} }
void Parser::process(std::vector<CanData> can) { void Parser::process(std::vector<CanData> msgs) {
for (auto &data : can) { static double prev_update_ts = 0;
++counters[data.id]; for (const auto &can_data : msgs) {
auto &list = can_msgs[data.id]; can_msgs[can_data.id] = can_data;
while (list.size() > DATA_LIST_SIZE) { current_sec = can_data.ts;
list.pop_front(); ++counters[can_data.id];
if (can_data.id == current_msg_id) {
while (history_log.size() >= LOG_SIZE) {
history_log.pop_back();
}
history_log.push_front(can_data);
} }
list.push_back(data);
} }
emit updated(); double current_ts = millis_since_boot();
if ((current_ts - prev_update_ts) > 1000.0 / FPS) {
prev_update_ts = current_ts;
emit updated();
}
if (current_sec < begin_sec || current_sec > end_sec) {
// loop replay in selected range.
replay->seekTo(begin_sec, false);
}
} }
void Parser::recvThread() { void Parser::recvThread() {
@ -56,13 +76,16 @@ void Parser::recvThread() {
std::unique_ptr<Context> context(Context::create()); std::unique_ptr<Context> context(Context::create());
std::unique_ptr<SubSocket> subscriber(SubSocket::create(context.get(), "can")); std::unique_ptr<SubSocket> subscriber(SubSocket::create(context.get(), "can"));
subscriber->setTimeout(100); subscriber->setTimeout(100);
std::vector<CanData> can;
while (!exit) { while (!exit) {
std::unique_ptr<Message> msg(subscriber->receive()); std::unique_ptr<Message> msg(subscriber->receive());
if (!msg) continue; if (!msg) continue;
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(msg.get())); capnp::FlatArrayMessageReader cmsg(aligned_buf.align(msg.get()));
cereal::Event::Reader event = cmsg.getRoot<cereal::Event>(); cereal::Event::Reader event = cmsg.getRoot<cereal::Event>();
std::vector<CanData> can;
can.clear();
can.reserve(event.getCan().size()); can.reserve(event.getCan().size());
for (const auto &c : event.getCan()) { for (const auto &c : event.getCan()) {
CanData &data = can.emplace_back(); CanData &data = can.emplace_back();
@ -72,7 +95,7 @@ void Parser::recvThread() {
data.dat.append((char *)c.getDat().begin(), c.getDat().size()); data.dat.append((char *)c.getDat().begin(), c.getDat().size());
data.hex_dat = data.dat.toHex(' ').toUpper(); data.hex_dat = data.dat.toHex(' ').toUpper();
data.id = QString("%1:%2").arg(data.source).arg(data.address, 1, 16); data.id = QString("%1:%2").arg(data.source).arg(data.address, 1, 16);
data.ts = (event.getLogMonoTime() - replay->routeStartTime()) / (double)1e6; data.ts = (event.getLogMonoTime() - replay->routeStartTime()) / (double)1e9; // seconds
} }
emit received(can); emit received(can);
} }
@ -90,9 +113,72 @@ void Parser::removeSignal(const QString &id, const QString &sig_name) {
auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); 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()) { if (it != msg->sigs.end()) {
msg->sigs.erase(it); msg->sigs.erase(it);
emit signalRemoved(id, sig_name);
} }
} }
uint32_t Parser::addressFromId(const QString &id) { uint32_t Parser::addressFromId(const QString &id) {
return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16); return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16);
} }
const Signal *Parser::getSig(const QString &id, const QString &sig_name) {
if (auto msg = getMsg(id)) {
auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [&](auto &s) { return sig_name == s.name.c_str(); });
if (it != msg->sigs.end()) {
return &(*it);
}
}
return nullptr;
}
void Parser::setRange(double min, double max) {
if (begin_sec != min || end_sec != max) {
begin_sec = min;
end_sec = max;
is_zoomed = begin_sec != event_begin_sec || end_sec != event_end_sec;
emit rangeChanged(min, max);
}
}
void Parser::segmentsMerged() {
auto events = replay->events();
if (!events || events->empty()) return;
auto it = std::find_if(events->begin(), events->end(), [=](const Event *e) { return e->which == cereal::Event::Which::CAN; });
event_begin_sec = it == events->end() ? 0 : ((*it)->mono_time - replay->routeStartTime()) / (double)1e9;
event_end_sec = double(events->back()->mono_time - replay->routeStartTime()) / 1e9;
if (!is_zoomed) {
begin_sec = event_begin_sec;
end_sec = event_end_sec;
}
emit eventsMerged();
}
void Parser::resetRange() {
setRange(event_begin_sec, event_end_sec);
}
void Parser::setCurrentMsg(const QString &id) {
current_msg_id = id;
history_log.clear();
}
// helper functions
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;
}();
int bigEndianStartBitsIndex(int start_bit) {
return BIG_ENDIAN_START_BITS[start_bit];
}
int bigEndianBitIndex(int index) {
return BIG_ENDIAN_START_BITS.indexOf(index);
}

@ -1,17 +1,20 @@
#pragma once #pragma once
#include <atomic>
#include <map>
#include <QApplication> #include <QApplication>
#include <QHash>
#include <QObject> #include <QObject>
#include <QThread> #include <QThread>
#include <atomic>
#include <map>
#include "opendbc/can/common.h" #include "opendbc/can/common.h"
#include "opendbc/can/common_dbc.h" #include "opendbc/can/common_dbc.h"
#include "tools/replay/replay.h" #include "tools/replay/replay.h"
const int DATA_LIST_SIZE = 500; const int DATA_LIST_SIZE = 50;
// const int FPS = 20; const int FPS = 20;
const static int LOG_SIZE = 25;
struct CanData { struct CanData {
QString id; QString id;
@ -26,7 +29,7 @@ struct CanData {
class Parser : public QObject { class Parser : public QObject {
Q_OBJECT Q_OBJECT
public: public:
Parser(QObject *parent); Parser(QObject *parent);
~Parser(); ~Parser();
static uint32_t addressFromId(const QString &id); static uint32_t addressFromId(const QString &id);
@ -35,32 +38,57 @@ class Parser : public QObject {
void saveDBC(const QString &name) {} void saveDBC(const QString &name) {}
void addNewMsg(const Msg &msg); void addNewMsg(const Msg &msg);
void removeSignal(const QString &id, const QString &sig_name); void removeSignal(const QString &id, const QString &sig_name);
const Msg *getMsg(const QString &id) { const Signal *getSig(const QString &id, const QString &sig_name);
return getMsg(addressFromId(id)); void setRange(double min, double max);
} void resetRange();
const Msg *getMsg(uint32_t address) { void setCurrentMsg(const QString &id);
inline std::pair<double, double> range() const { return {begin_sec, end_sec}; }
inline double currentSec() const { return current_sec; }
inline bool isZoomed() const { return is_zoomed; }
inline const Msg *getMsg(const QString &id) { return getMsg(addressFromId(id)); }
inline const Msg *getMsg(uint32_t address) {
auto it = msg_map.find(address); auto it = msg_map.find(address);
return it != msg_map.end() ? it->second : nullptr; return it != msg_map.end() ? it->second : nullptr;
} }
signals:
signals:
void showPlot(const QString &id, const QString &name); void showPlot(const QString &id, const QString &name);
void hidePlot(const QString &id, const QString &name); void hidePlot(const QString &id, const QString &name);
void signalRemoved(const QString &id, const QString &sig_name);
void eventsMerged();
void rangeChanged(double min, double max);
void received(std::vector<CanData> can); void received(std::vector<CanData> can);
void updated(); void updated();
public: public:
Replay *replay = nullptr;
QHash<QString, uint64_t> counters;
std::map<QString, CanData> can_msgs;
QList<CanData> history_log;
protected:
void recvThread(); void recvThread();
void process(std::vector<CanData> can); void process(std::vector<CanData> can);
void segmentsMerged();
double current_sec = 0.;
std::atomic<bool> exit = false;
QThread *thread; QThread *thread;
QString dbc_name; QString dbc_name;
std::atomic<bool> exit = false; double begin_sec = 0;
std::map<QString, std::list<CanData>> can_msgs; double end_sec = 0;
std::map<QString, uint64_t> counters; double event_begin_sec = 0;
Replay *replay = nullptr; double event_end_sec = 0;
bool is_zoomed = false;
DBC *dbc = nullptr; DBC *dbc = nullptr;
std::map<uint32_t, const Msg *> msg_map; std::map<uint32_t, const Msg *> msg_map;
QString current_msg_id;
}; };
Q_DECLARE_METATYPE(std::vector<CanData>); Q_DECLARE_METATYPE(std::vector<CanData>);
// TODO: Add helper function in dbc.h
int bigEndianStartBitsIndex(int start_bit);
int bigEndianBitIndex(int index);
extern Parser *parser; extern Parser *parser;

@ -0,0 +1,203 @@
#include "tools/cabana/signaledit.h"
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QVBoxLayout>
// SignalForm
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 = bigEndianStartBitsIndex(bigEndianBitIndex(sig.start_bit) + sig.size - 1);
sig.msb = sig.start_bit;
}
return (sig.name.empty() || sig.size <= 0) ? std::nullopt : std::optional(sig);
}
// SignalEdit
SignalEdit::SignalEdit(const QString &id, const Signal &sig, const QString &color, 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(color));
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();
plot_btn = new QPushButton("📈");
plot_btn->setStyleSheet("font-size:16px");
plot_btn->setToolTip(tr("Show Plot"));
plot_btn->setContentsMargins(5, 5, 5, 5);
plot_btn->setFixedSize(30, 30);
QObject::connect(plot_btn, &QPushButton::clicked, [=]() { emit parser->showPlot(id, name_); });
title_layout->addWidget(plot_btn);
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() {
if (auto sig = const_cast<Signal *>(parser->getSig(id, name_))) {
if (auto s = form->getSignal()) {
*sig = *s;
// TODO: reset the chart for sig
}
}
}
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_);
deleteLater();
}
}
// AddSignalDialog
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,50 @@
#pragma once
#include <optional>
#include <QComboBox>
#include <QDialog>
#include <QLineEdit>
#include <QPushButton>
#include <QSpinBox>
#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 SignalEdit : public QWidget {
Q_OBJECT
public:
SignalEdit(const QString &id, const Signal &sig, const QString &color, QWidget *parent = nullptr);
void save();
protected:
void remove();
QString id;
QString name_;
QPushButton *plot_btn;
ElidedLabel *title;
SignalForm *form;
QWidget *edit_container;
QPushButton *remove_btn;
};
class AddSignalDialog : public QDialog {
Q_OBJECT
public:
AddSignalDialog(const QString &id, QWidget *parent);
};

@ -3,9 +3,7 @@
#include <QButtonGroup> #include <QButtonGroup>
#include <QDateTime> #include <QDateTime>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "tools/cabana/parser.h" #include "tools/cabana/parser.h"
@ -17,17 +15,19 @@ inline QString formatTime(int seconds) {
VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
cam_widget = new CameraViewWidget("camerad", VISION_STREAM_ROAD, true, this); cam_widget = new CameraViewWidget("camerad", VISION_STREAM_ROAD, false, this);
cam_widget->setFixedSize(640, 480); cam_widget->setFixedSize(parent->width(), parent->width() / 1.596);
main_layout->addWidget(cam_widget); main_layout->addWidget(cam_widget);
// slider controls // slider controls
QHBoxLayout *slider_layout = new QHBoxLayout(); QHBoxLayout *slider_layout = new QHBoxLayout();
QLabel *time_label = new QLabel("00:00"); time_label = new QLabel("00:00");
slider_layout->addWidget(time_label); slider_layout->addWidget(time_label);
slider = new QSlider(Qt::Horizontal, this); slider = new QSlider(Qt::Horizontal, this);
// slider->setFixedWidth(640); QObject::connect(slider, &QSlider::sliderMoved, [=]() {
time_label->setText(formatTime(slider->value()));
});
slider->setSingleStep(1); slider->setSingleStep(1);
slider->setMaximum(parser->replay->totalSeconds()); slider->setMaximum(parser->replay->totalSeconds());
QObject::connect(slider, &QSlider::sliderReleased, [=]() { QObject::connect(slider, &QSlider::sliderReleased, [=]() {
@ -36,7 +36,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
}); });
slider_layout->addWidget(slider); slider_layout->addWidget(slider);
QLabel *total_time_label = new QLabel(formatTime(parser->replay->totalSeconds())); total_time_label = new QLabel(formatTime(parser->replay->totalSeconds()));
slider_layout->addWidget(total_time_label); slider_layout->addWidget(total_time_label);
main_layout->addLayout(slider_layout); main_layout->addLayout(slider_layout);
@ -57,9 +57,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
for (float speed : {0.1, 0.5, 1., 2.}) { for (float speed : {0.1, 0.5, 1., 2.}) {
QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this); QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this);
btn->setCheckable(true); btn->setCheckable(true);
QObject::connect(btn, &QPushButton::clicked, [=]() { QObject::connect(btn, &QPushButton::clicked, [=]() { parser->replay->setSpeed(speed); });
parser->replay->setSpeed(speed);
});
control_layout->addWidget(btn); control_layout->addWidget(btn);
group->addButton(btn); group->addButton(btn);
if (speed == 1.0) btn->setChecked(true); if (speed == 1.0) btn->setChecked(true);
@ -67,14 +65,25 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
main_layout->addLayout(control_layout); main_layout->addLayout(control_layout);
QTimer *timer = new QTimer(this); QObject::connect(parser, &Parser::rangeChanged, this, &VideoWidget::rangeChanged);
timer->setInterval(1000); QObject::connect(parser, &Parser::updated, this, &VideoWidget::updateState);
timer->callOnTimeout([=]() { }
int current_seconds = parser->replay->currentSeconds();
time_label->setText(formatTime(current_seconds)); void VideoWidget::rangeChanged(double min, double max) {
if (!slider->isSliderDown()) { if (!parser->isZoomed()) {
slider->setValue(current_seconds); min = 0;
} max = parser->replay->totalSeconds();
}); }
timer->start(); time_label->setText(formatTime(min));
total_time_label->setText(formatTime(max));
slider->setMaximum(max);
slider->setValue(parser->currentSec());
}
void VideoWidget::updateState() {
if (!slider->isSliderDown()) {
int current_sec = parser->currentSec();
time_label->setText(formatTime(current_sec));
slider->setValue(current_sec);
}
} }

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <QLabel>
#include <QSlider> #include <QSlider>
#include <QWidget> #include <QWidget>
@ -12,6 +13,10 @@ public:
VideoWidget(QWidget *parnet = nullptr); VideoWidget(QWidget *parnet = nullptr);
protected: protected:
void rangeChanged(double min, double max);
void updateState();
CameraViewWidget *cam_widget; CameraViewWidget *cam_widget;
QLabel *time_label, *total_time_label;
QSlider *slider; QSlider *slider;
}; };

@ -46,7 +46,9 @@ LogReader::~LogReader() {
#endif #endif
} }
bool LogReader::load(const std::string &url, std::atomic<bool> *abort, bool local_cache, int chunk_size, int retries) { bool LogReader::load(const std::string &url, std::atomic<bool> *abort,
const std::set<cereal::Event::Which> &allow,
bool local_cache, int chunk_size, int retries) {
raw_ = FileReader(local_cache, chunk_size, retries).read(url, abort); raw_ = FileReader(local_cache, chunk_size, retries).read(url, abort);
if (raw_.empty()) return false; if (raw_.empty()) return false;
@ -54,18 +56,26 @@ bool LogReader::load(const std::string &url, std::atomic<bool> *abort, bool loca
raw_ = decompressBZ2(raw_, abort); raw_ = decompressBZ2(raw_, abort);
if (raw_.empty()) return false; if (raw_.empty()) return false;
} }
return parse(abort); return parse(allow, abort);
} }
bool LogReader::load(const std::byte *data, size_t size, std::atomic<bool> *abort) { bool LogReader::load(const std::byte *data, size_t size, std::atomic<bool> *abort) {
raw_.assign((const char *)data, size); raw_.assign((const char *)data, size);
return parse(abort); return parse({}, abort);
} }
bool LogReader::parse(std::atomic<bool> *abort) { bool LogReader::parse(const std::set<cereal::Event::Which> &allow, std::atomic<bool> *abort) {
try { try {
kj::ArrayPtr<const capnp::word> words((const capnp::word *)raw_.data(), raw_.size() / sizeof(capnp::word)); kj::ArrayPtr<const capnp::word> words((const capnp::word *)raw_.data(), raw_.size() / sizeof(capnp::word));
while (words.size() > 0 && !(abort && *abort)) { while (words.size() > 0 && !(abort && *abort)) {
if (!allow.empty()) {
capnp::FlatArrayMessageReader reader(words);
auto which = reader.getRoot<cereal::Event>().which();
if (allow.find(which) == allow.end()) {
words = kj::arrayPtr(reader.getEnd(), words.end());
continue;
}
}
#ifdef HAS_MEMORY_RESOURCE #ifdef HAS_MEMORY_RESOURCE
Event *evt = new (mbr_) Event(words); Event *evt = new (mbr_) Event(words);

@ -5,6 +5,8 @@
#include <memory_resource> #include <memory_resource>
#endif #endif
#include <set>
#include "cereal/gen/cpp/log.capnp.h" #include "cereal/gen/cpp/log.capnp.h"
#include "system/camerad/cameras/camera_common.h" #include "system/camerad/cameras/camera_common.h"
#include "tools/replay/filereader.h" #include "tools/replay/filereader.h"
@ -50,12 +52,13 @@ class LogReader {
public: public:
LogReader(size_t memory_pool_block_size = DEFAULT_EVENT_MEMORY_POOL_BLOCK_SIZE); LogReader(size_t memory_pool_block_size = DEFAULT_EVENT_MEMORY_POOL_BLOCK_SIZE);
~LogReader(); ~LogReader();
bool load(const std::string &url, std::atomic<bool> *abort = nullptr, bool local_cache = false, int chunk_size = -1, int retries = 0); bool load(const std::string &url, std::atomic<bool> *abort = nullptr, const std::set<cereal::Event::Which> &allow = {},
bool local_cache = false, int chunk_size = -1, int retries = 0);
bool load(const std::byte *data, size_t size, std::atomic<bool> *abort = nullptr); bool load(const std::byte *data, size_t size, std::atomic<bool> *abort = nullptr);
std::vector<Event*> events; std::vector<Event*> events;
private: private:
bool parse(std::atomic<bool> *abort); bool parse(const std::set<cereal::Event::Which> &allow, std::atomic<bool> *abort);
std::string raw_; std::string raw_;
#ifdef HAS_MEMORY_RESOURCE #ifdef HAS_MEMORY_RESOURCE
std::pmr::monotonic_buffer_resource *mbr_ = nullptr; std::pmr::monotonic_buffer_resource *mbr_ = nullptr;

@ -19,6 +19,9 @@ Replay::Replay(QString route, QStringList allow, QStringList block, SubMaster *s
if ((allow.empty() || allow.contains(it.name)) && !block.contains(it.name)) { if ((allow.empty() || allow.contains(it.name)) && !block.contains(it.name)) {
uint16_t which = event_struct.getFieldByName(it.name).getProto().getDiscriminantValue(); uint16_t which = event_struct.getFieldByName(it.name).getProto().getDiscriminantValue();
sockets_[which] = it.name; sockets_[which] = it.name;
if (!allow.empty() || !block.empty()) {
allow_list.insert((cereal::Event::Which)which);
}
s.push_back(it.name); s.push_back(it.name);
} }
} }
@ -91,17 +94,17 @@ void Replay::updateEvents(const std::function<bool()> &lambda) {
stream_cv_.notify_one(); stream_cv_.notify_one();
} }
void Replay::seekTo(int seconds, bool relative) { void Replay::seekTo(double seconds, bool relative) {
seconds = relative ? seconds + currentSeconds() : seconds; seconds = relative ? seconds + currentSeconds() : seconds;
updateEvents([&]() { updateEvents([&]() {
seconds = std::max(0, seconds); seconds = std::max(double(0.0), seconds);
int seg = seconds / 60; int seg = (int)seconds / 60;
if (segments_.find(seg) == segments_.end()) { if (segments_.find(seg) == segments_.end()) {
rWarning("can't seek to %d s segment %d is invalid", seconds, seg); rWarning("can't seek to %d s segment %d is invalid", seconds, seg);
return true; return true;
} }
rInfo("seeking to %d s, segment %d", seconds, seg); rInfo("seeking to %d s, segment %d", (int)seconds, seg);
current_segment_ = seg; current_segment_ = seg;
cur_mono_time_ = route_start_ts_ + seconds * 1e9; cur_mono_time_ = route_start_ts_ + seconds * 1e9;
return isSegmentMerged(seg); return isSegmentMerged(seg);
@ -122,7 +125,9 @@ void Replay::buildTimeline() {
for (int i = 0; i < segments_.size() && !exit_; ++i) { for (int i = 0; i < segments_.size() && !exit_; ++i) {
LogReader log; LogReader log;
if (!log.load(route_->at(i).qlog.toStdString(), &exit_, !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue; if (!log.load(route_->at(i).qlog.toStdString(), &exit_,
{cereal::Event::Which::CONTROLS_STATE, cereal::Event::Which::USER_FLAG},
!hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue;
for (const Event *e : log.events) { for (const Event *e : log.events) {
if (e->which == cereal::Event::Which::CONTROLS_STATE) { if (e->which == cereal::Event::Which::CONTROLS_STATE) {
@ -215,7 +220,7 @@ void Replay::queueSegment() {
if ((seg && !seg->isLoaded()) || !seg) { if ((seg && !seg->isLoaded()) || !seg) {
if (!seg) { if (!seg) {
rDebug("loading segment %d...", n); rDebug("loading segment %d...", n);
seg = std::make_unique<Segment>(n, route_->at(n), flags_); seg = std::make_unique<Segment>(n, route_->at(n), flags_, allow_list);
QObject::connect(seg.get(), &Segment::loadFinished, this, &Replay::segmentLoadFinished); QObject::connect(seg.get(), &Segment::loadFinished, this, &Replay::segmentLoadFinished);
} }
break; break;
@ -270,6 +275,9 @@ void Replay::mergeSegments(const SegmentMap::iterator &begin, const SegmentMap::
segments_merged_ = segments_need_merge; segments_merged_ = segments_need_merge;
return true; return true;
}); });
if (stream_thread_) {
emit segmentsMerged();
}
} }
} }
@ -306,6 +314,7 @@ void Replay::startStream(const Segment *cur_segment) {
camera_server_ = std::make_unique<CameraServer>(camera_size); camera_server_ = std::make_unique<CameraServer>(camera_size);
} }
emit segmentsMerged();
// start stream thread // start stream thread
stream_thread_ = new QThread(); stream_thread_ = new QThread();
QObject::connect(stream_thread_, &QThread::started, [=]() { stream(); }); QObject::connect(stream_thread_, &QThread::started, [=]() { stream(); });

@ -45,18 +45,19 @@ public:
void stop(); void stop();
void pause(bool pause); void pause(bool pause);
void seekToFlag(FindFlag flag); void seekToFlag(FindFlag flag);
void seekTo(int seconds, bool relative); void seekTo(double seconds, bool relative);
inline bool isPaused() const { return paused_; } inline bool isPaused() const { return paused_; }
inline bool hasFlag(REPLAY_FLAGS flag) const { return flags_ & flag; } inline bool hasFlag(REPLAY_FLAGS flag) const { return flags_ & flag; }
inline void addFlag(REPLAY_FLAGS flag) { flags_ |= flag; } inline void addFlag(REPLAY_FLAGS flag) { flags_ |= flag; }
inline void removeFlag(REPLAY_FLAGS flag) { flags_ &= ~flag; } inline void removeFlag(REPLAY_FLAGS flag) { flags_ &= ~flag; }
inline const Route* route() const { return route_.get(); } inline const Route* route() const { return route_.get(); }
inline int currentSeconds() const { return (cur_mono_time_ - route_start_ts_) / 1e9; } inline double currentSeconds() const { return double(cur_mono_time_ - route_start_ts_) / 1e9; }
inline uint64_t routeStartTime() const { return route_start_ts_; } 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 toSeconds(uint64_t mono_time) const { return (mono_time - route_start_ts_) / 1e9; }
inline int totalSeconds() const { return segments_.size() * 60; } inline int totalSeconds() const { return segments_.size() * 60; }
inline void setSpeed(float speed) { speed_ = speed; } inline void setSpeed(float speed) { speed_ = speed; }
inline float getSpeed() const { return speed_; } inline float getSpeed() const { return speed_; }
inline const std::vector<Event *> *events() const { return events_.get(); }
inline const std::string &carFingerprint() const { return car_fingerprint_; } inline const std::string &carFingerprint() const { return car_fingerprint_; }
inline const std::vector<std::tuple<int, int, TimelineType>> getTimeline() { inline const std::vector<std::tuple<int, int, TimelineType>> getTimeline() {
std::lock_guard lk(timeline_lock); std::lock_guard lk(timeline_lock);
@ -65,6 +66,7 @@ public:
signals: signals:
void streamStarted(); void streamStarted();
void segmentsMerged();
protected slots: protected slots:
void segmentLoadFinished(bool success); void segmentLoadFinished(bool success);
@ -98,7 +100,7 @@ protected:
bool paused_ = false; bool paused_ = false;
bool events_updated_ = false; bool events_updated_ = false;
uint64_t route_start_ts_ = 0; uint64_t route_start_ts_ = 0;
uint64_t cur_mono_time_ = 0; std::atomic<uint64_t> cur_mono_time_ = 0;
std::unique_ptr<std::vector<Event *>> events_; std::unique_ptr<std::vector<Event *>> events_;
std::unique_ptr<std::vector<Event *>> new_events_; std::unique_ptr<std::vector<Event *>> new_events_;
std::vector<int> segments_merged_; std::vector<int> segments_merged_;
@ -114,6 +116,7 @@ protected:
std::mutex timeline_lock; std::mutex timeline_lock;
QFuture<void> timeline_future; QFuture<void> timeline_future;
std::vector<std::tuple<int, int, TimelineType>> timeline; std::vector<std::tuple<int, int, TimelineType>> timeline;
std::set<cereal::Event::Which> allow_list;
std::string car_fingerprint_; std::string car_fingerprint_;
float speed_ = 1.0; float speed_ = 1.0;
}; };

@ -99,7 +99,9 @@ void Route::addFileToSegment(int n, const QString &file) {
// class Segment // class Segment
Segment::Segment(int n, const SegmentFile &files, uint32_t flags) : seg_num(n), flags(flags) { Segment::Segment(int n, const SegmentFile &files, uint32_t flags,
const std::set<cereal::Event::Which> &allow)
: seg_num(n), flags(flags), allow(allow) {
// [RoadCam, DriverCam, WideRoadCam, log]. fallback to qcamera/qlog // [RoadCam, DriverCam, WideRoadCam, log]. fallback to qcamera/qlog
const std::array file_list = { const std::array file_list = {
(flags & REPLAY_FLAG_QCAMERA) || files.road_cam.isEmpty() ? files.qcamera : files.road_cam, (flags & REPLAY_FLAG_QCAMERA) || files.road_cam.isEmpty() ? files.qcamera : files.road_cam,
@ -130,7 +132,7 @@ void Segment::loadFile(int id, const std::string file) {
success = frames[id]->load(file, flags & REPLAY_FLAG_NO_HW_DECODER, &abort_, local_cache, 20 * 1024 * 1024, 3); success = frames[id]->load(file, flags & REPLAY_FLAG_NO_HW_DECODER, &abort_, local_cache, 20 * 1024 * 1024, 3);
} else { } else {
log = std::make_unique<LogReader>(); log = std::make_unique<LogReader>();
success = log->load(file, &abort_, local_cache, 0, 3); success = log->load(file, &abort_, allow, local_cache, 0, 3);
} }
if (!success) { if (!success) {

@ -47,7 +47,7 @@ class Segment : public QObject {
Q_OBJECT Q_OBJECT
public: public:
Segment(int n, const SegmentFile &files, uint32_t flags); Segment(int n, const SegmentFile &files, uint32_t flags, const std::set<cereal::Event::Which> &allow = {});
~Segment(); ~Segment();
inline bool isLoaded() const { return !loading_ && !abort_; } inline bool isLoaded() const { return !loading_ && !abort_; }
@ -65,4 +65,5 @@ protected:
std::atomic<int> loading_ = 0; std::atomic<int> loading_ = 0;
QFutureSynchronizer<void> synchronizer_; QFutureSynchronizer<void> synchronizer_;
uint32_t flags; uint32_t flags;
std::set<cereal::Event::Which> allow;
}; };

Loading…
Cancel
Save