diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 0caae14e92..c87e2cdd94 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -16,6 +16,5 @@ if arch in ['x86_64', 'Darwin'] and GetOption('extras'): qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] 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 - 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) diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 0adc744b49..20cd889023 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -5,7 +5,6 @@ #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); @@ -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.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")) { @@ -24,8 +22,8 @@ int main(int argc, char *argv[]) { } 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"))) { + Parser p(&app); + if (!p.loadRoute(route, cmd_parser.value("data_dir"), true)) { return 0; } MainWindow w; diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc index a8fe39968e..836eb34946 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chartswidget.cc @@ -1,20 +1,24 @@ #include "tools/cabana/chartswidget.h" +#include +#include +#include +#include +#include #include +#include -using namespace QtCharts; - -int64_t get_raw_value(const QByteArray &msg, const Signal &sig) { +int64_t get_raw_value(uint8_t *data, size_t data_size, const Signal &sig) { int64_t ret = 0; int i = sig.msb / 8; 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 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); + uint64_t d = (data[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1); ret |= d << (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) { 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); + connect(parser, &Parser::signalRemoved, 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}; + auto chart = new ChartWidget(id, sig_name, this); + main_layout->addWidget(chart); + charts[char_name] = chart; } } 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); + if (auto it = charts.find(id + sig_name); it != charts.end()) { + it->second->deleteLater(); + 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; +ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *parent) : id(id), sig_name(sig_name), QWidget(parent) { + QStackedLayout *stacked = new QStackedLayout(this); + stacked->setStackingMode(QStackedLayout::StackAll); + + QWidget *chart_widget = new QWidget(this); + 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(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()) { + 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 * { - 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; + stacked->addWidget(chart_widget); + line_marker = new LineMarker(chart, this); + stacked->addWidget(line_marker); + line_marker->setAttribute(Qt::WA_TransparentForMouseEvents, true); + line_marker->raise(); - if (value > c.max_y) c.max_y = value; - if (value < c.min_y) c.min_y = value; + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + 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) { - 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); + updateSeries(); +} + +void ChartWidget::updateState() { + line_marker->update(); +} + +void ChartWidget::updateSeries() { + const Signal *sig = parser->getSig(id, sig_name); + auto events = parser->replay->events(); + 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(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(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}); } diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h index 7bc8335a32..1be5fdeecb 100644 --- a/tools/cabana/chartswidget.h +++ b/tools/cabana/chartswidget.h @@ -1,17 +1,53 @@ #pragma once +#include + +#include #include #include #include #include -#include #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 vals; +}; + class ChartsWidget : public QWidget { Q_OBJECT - public: +public: ChartsWidget(QWidget *parent = nullptr); inline bool hasChart(const QString &id, const QString &sig_name) { 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 updateState(); - protected: +protected: QVBoxLayout *main_layout; - struct SignalChart { - QString id; - QString sig_name; - int max_y = 0; - int min_y = 0; - QList data; - QtCharts::QChartView *chart_view = nullptr; - }; - std::map charts; + std::map charts; }; diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 6799376577..e573a01970 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -1,68 +1,53 @@ + #include "tools/cabana/detailwidget.h" #include +#include #include -#include #include #include #include +#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/widgets/scrollview.h" -const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"}; - -static QVector BIG_ENDIAN_START_BITS = []() { - QVector 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); +inline const QString &getColor(int i) { + static const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"}; + return SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)]; } +// DetailWidget + 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(); + name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + 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->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); + title_layout->addWidget(edit_btn); + main_layout->addLayout(title_layout); + // binary view binary_view = new BinaryView(this); main_layout->addWidget(binary_view); + // scroll area 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); @@ -72,238 +57,67 @@ DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) { signal_edit_layout->setSpacing(2); container_layout->addLayout(signal_edit_layout); - messages_view = new MessagesView(this); - container_layout->addWidget(messages_view); + history_log = new HistoryLog(this); + container_layout->addWidget(history_log); QScrollArea *scroll = new QScrollArea(this); scroll->setWidget(container); scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); + scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); 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() { - 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 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"); +void DetailWidget::setMsg(const CanData *c) { + can_data = c; + clearLayout(signal_edit_layout); + edit_btn->setVisible(true); - 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); + if (auto msg = parser->getMsg(can_data->address)) { + name_label->setText(msg->name.c_str()); + add_sig_btn->setVisible(true); + for (int i = 0; i < msg->sigs.size(); ++i) { + signal_edit_layout->addWidget(new SignalEdit(can_data->id, msg->sigs[i], getColor(i))); } - 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); - add_sig_btn->setVisible(msg != nullptr); + binary_view->setMsg(can_data); + history_log->clear(); } -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); +void DetailWidget::updateState() { + if (!can_data) return; - 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); + time_label->setText(QString("time: %1").arg(can_data->ts, 0, 'f', 3)); + binary_view->setData(can_data->dat); + history_log->updateState(); } -void SignalEdit::save() { - Msg *msg = const_cast(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 DetailWidget::editMsg() { + EditMessageDialog dlg(can_data->id, this); + if (dlg.exec()) { + setMsg(can_data); } } -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(); +void DetailWidget::addSignal() { + AddSignalDialog dlg(can_data->id, this); + if (dlg.exec()) { + setMsg(can_data); } } +// BinaryView + BinaryView::BinaryView(QWidget *parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); table = new QTableWidget(this); @@ -316,15 +130,16 @@ BinaryView::BinaryView(QWidget *parent) { 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(); +void BinaryView::setMsg(const CanData *can_data) { + const Msg *msg = parser->getMsg(can_data->address); + int row_count = msg ? msg->size : can_data->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->setFlags(item->flags() ^ Qt::ItemIsEditable); item->setTextAlignment(Qt::AlignCenter); if (j == 8) { QFont font; @@ -336,19 +151,17 @@ void BinaryView::setMsg(const QString &id) { } if (msg) { + // set background color 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)])); + table->item(j / 8, j % 8)->setBackground(QColor(getColor(i))); } } } 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) { @@ -357,6 +170,7 @@ void BinaryView::setData(const QByteArray &binary) { s += std::bitset<8>(binary[j]).to_string(); } + setUpdatesEnabled(false); char hex[3] = {'\0'}; for (int i = 0; i < binary.size(); ++i) { 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]); 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); - QLabel *title = new QLabel("MESSAGE TIME BYTES"); + QLabel *title = new QLabel("TIME BYTES"); 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->addStretch(); } -void MessagesView::setMessages(const std::list &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; +void HistoryLog::updateState() { + int i = 0; + for (; i < parser->history_log.size(); ++i) { + const auto &c = parser->history_log[i]; + auto label = labels[i]; + label->setVisible(true); + label->setText(QString("%1 %2").arg(c.ts, 0, 'f', 3).arg(c.hex_dat)); + } + + for (; i < std::size(labels); ++i) { + labels[i]->setVisible(false); } } -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")); QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->addWidget(new QLabel(tr("ID: (%1)").arg(id))); - auto msg = const_cast(parser->getMsg(Parser::addressFromId(id))); + auto msg = const_cast(parser->getMsg(id)); QHBoxLayout *h_layout = new QHBoxLayout(); h_layout->addWidget(new QLabel(tr("Name"))); h_layout->addStretch(); - QLineEdit *name_edit = new QLineEdit(this); + name_edit = new QLineEdit(this); name_edit->setText(msg ? msg->name.c_str() : "untitled"); h_layout->addWidget(name_edit); main_layout->addLayout(h_layout); @@ -412,47 +238,30 @@ EditMessageDialog::EditMessageDialog(const QString &id, QWidget *parent) : QDial 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()); + size_spin = new QSpinBox(this); + size_spin->setValue(msg ? msg->size : parser->can_msgs[id].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::accepted, this, &EditMessageDialog::save); 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(parser->getMsg(id))) { - if (auto signal = form->getSignal()) { - msg->sigs.push_back(*signal); - } - } - QDialog::accept(); - }); +void EditMessageDialog::save() { + if (size_spin->value() <= 0 || name_edit->text().isEmpty()) return; + + if (auto msg = const_cast(parser->getMsg(id))) { + 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(); } diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index 70f2804f70..b2e7cbf3b7 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -1,102 +1,70 @@ #pragma once -#include + #include -#include #include -#include #include -#include #include #include #include -#include #include "opendbc/can/common.h" #include "opendbc/can/common_dbc.h" -#include "selfdrive/ui/qt/widgets/controls.h" #include "tools/cabana/parser.h" +#include "tools/cabana/signaledit.h" -class SignalForm : public QWidget { +class HistoryLog : public QWidget { Q_OBJECT - public: - SignalForm(const Signal &sig, QWidget *parent); - std::optional 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: + HistoryLog(QWidget *parent); + void clear(); + void updateState(); - public: - MessagesView(QWidget *parent); - void setMessages(const std::list &data); - std::vector messages; - QVBoxLayout *message_layout; +private: + QLabel *labels[LOG_SIZE] = {}; }; class BinaryView : public QWidget { Q_OBJECT - public: +public: BinaryView(QWidget *parent); - void setMsg(const QString &id); + void setMsg(const CanData *can_data); void setData(const QByteArray &binary); QTableWidget *table; }; -class SignalEdit : public QWidget { +class EditMessageDialog : public QDialog { Q_OBJECT - public: - SignalEdit(const QString &id, const Signal &sig, int idx, QWidget *parent); +public: + EditMessageDialog(const QString &id, QWidget *parent); + +protected: void save(); -signals: - void removed(); - protected: - void remove(); + QLineEdit *name_edit; + QSpinBox *size_spin; QString id; - QString name_; - ElidedLabel *title; - SignalForm *form; - QWidget *edit_container; - QPushButton *remove_btn; }; class DetailWidget : public QWidget { Q_OBJECT - public: + +public: DetailWidget(QWidget *parent); - void setMsg(const QString &id); + void setMsg(const CanData *c); - public slots: +private: void updateState(); + void addSignal(); + void editMsg(); - protected: - QLabel *name_label = nullptr; + const CanData *can_data = nullptr; + QLabel *name_label, *time_label; QPushButton *edit_btn, *add_sig_btn; QVBoxLayout *signal_edit_layout; - Signal *sig = nullptr; - MessagesView *messages_view; - QString msg_id; + HistoryLog *history_log; BinaryView *binary_view; - std::vector 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); }; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index d1b0d98f5f..8852987fbe 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -4,19 +4,15 @@ #include 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); + detail_widget->setFixedWidth(600); h_layout->addWidget(detail_widget); // right widget @@ -30,9 +26,12 @@ MainWindow::MainWindow() : QWidget() { QScrollArea *scroll = new QScrollArea(this); scroll->setWidget(charts_widget); scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); r_layout->addWidget(scroll); h_layout->addWidget(right_container); + + QObject::connect(messages_widget, &MessagesWidget::msgChanged, detail_widget, &DetailWidget::setMsg); } diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index f19c48297e..82ecceb02b 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -11,10 +11,10 @@ class MainWindow : public QWidget { Q_OBJECT - public: +public: MainWindow(); - protected: +protected: VideoWidget *video_widget; MessagesWidget *messages_widget; DetailWidget *detail_widget; diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index d81d3e9ede..840ea25810 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -1,11 +1,9 @@ #include "tools/cabana/messageswidget.h" #include -#include #include #include #include -#include MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { 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->horizontalHeader()->setStretchLastSection(true); QObject::connect(table_widget, &QTableWidget::itemSelectionChanged, [=]() { - auto id = table_widget->selectedItems()[0]->data(Qt::UserRole); - emit msgChanged(id.toString()); + const CanData *c = &(parser->can_msgs[table_widget->selectedItems()[1]->text()]); + parser->setCurrentMsg(c->id); + emit msgChanged(c); }); main_layout->addWidget(table_widget); @@ -60,6 +59,7 @@ void MessagesWidget::updateState() { auto item = table_widget->item(row, col); if (!item) { item = new QTableWidgetItem(); + item->setFlags(item->flags() ^ Qt::ItemIsEditable); table_widget->setItem(row, col, item); } return item; @@ -67,28 +67,27 @@ void MessagesWidget::updateState() { 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)) { + QString name, untitled = tr("untitled"); + const QString filter_str = filter->text(); + for (const auto &[_, c] : parser->can_msgs) { + if (auto msg = parser->getMsg(c.address)) { name = msg->name.c_str(); } 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++); 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); + getTableItem(i, 0)->setText(name); + getTableItem(i, 1)->setText(c.id); + getTableItem(i, 2)->setText(QString::number(parser->counters[c.id])); + getTableItem(i, 3)->setText(c.hex_dat); table_widget->showRow(i); i++; } + if (table_widget->currentRow() == -1) { + table_widget->selectRow(0); + } } diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index dca725199d..1dbb4a1af3 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -16,7 +16,7 @@ class MessagesWidget : public QWidget { void updateState(); signals: - void msgChanged(const QString &id); + void msgChanged(const CanData *id); protected: QLineEdit *filter; diff --git a/tools/cabana/parser.cc b/tools/cabana/parser.cc index 481f0dfbdf..3950d8bd57 100644 --- a/tools/cabana/parser.cc +++ b/tools/cabana/parser.cc @@ -4,7 +4,11 @@ #include "cereal/messaging/messaging.h" +Parser *parser = nullptr; + Parser::Parser(QObject *parent) : QObject(parent) { + parser = this; + qRegisterMetaType>(); 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) { replay = new Replay(route, {"can", "roadEncodeIdx"}, {}, nullptr, use_qcam ? REPLAY_FLAG_QCAMERA : 0, data_dir, this); - if (!replay->load()) { - return false; + QObject::connect(replay, &Replay::segmentsMerged, this, &Parser::segmentsMerged); + if (replay->load()) { + replay->start(); + return true; } - replay->start(); - return true; + return false; } void Parser::openDBC(const QString &name) { dbc_name = name; dbc = const_cast(dbc_lookup(name.toStdString())); + counters.clear(); msg_map.clear(); for (auto &msg : dbc->msgs) { msg_map[msg.address] = &msg; } } -void Parser::process(std::vector can) { - for (auto &data : can) { - ++counters[data.id]; - auto &list = can_msgs[data.id]; - while (list.size() > DATA_LIST_SIZE) { - list.pop_front(); +void Parser::process(std::vector msgs) { + static double prev_update_ts = 0; + for (const auto &can_data : msgs) { + can_msgs[can_data.id] = can_data; + current_sec = can_data.ts; + ++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() { @@ -56,13 +76,16 @@ void Parser::recvThread() { std::unique_ptr context(Context::create()); std::unique_ptr subscriber(SubSocket::create(context.get(), "can")); subscriber->setTimeout(100); + + std::vector can; while (!exit) { std::unique_ptr msg(subscriber->receive()); if (!msg) continue; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(msg.get())); cereal::Event::Reader event = cmsg.getRoot(); - std::vector can; + + can.clear(); can.reserve(event.getCan().size()); for (const auto &c : event.getCan()) { CanData &data = can.emplace_back(); @@ -72,7 +95,7 @@ void Parser::recvThread() { 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; + data.ts = (event.getLogMonoTime() - replay->routeStartTime()) / (double)1e9; // seconds } 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(); }); if (it != msg->sigs.end()) { msg->sigs.erase(it); + emit signalRemoved(id, sig_name); } } uint32_t Parser::addressFromId(const QString &id) { 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 BIG_ENDIAN_START_BITS = []() { + QVector 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); +} diff --git a/tools/cabana/parser.h b/tools/cabana/parser.h index fd5ded7c5e..2f8c441059 100644 --- a/tools/cabana/parser.h +++ b/tools/cabana/parser.h @@ -1,17 +1,20 @@ #pragma once +#include +#include + #include +#include #include #include -#include -#include #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; +const int DATA_LIST_SIZE = 50; +const int FPS = 20; +const static int LOG_SIZE = 25; struct CanData { QString id; @@ -26,7 +29,7 @@ struct CanData { class Parser : public QObject { Q_OBJECT - public: +public: Parser(QObject *parent); ~Parser(); static uint32_t addressFromId(const QString &id); @@ -35,32 +38,57 @@ class Parser : public QObject { 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) { + const Signal *getSig(const QString &id, const QString &sig_name); + void setRange(double min, double max); + void resetRange(); + void setCurrentMsg(const QString &id); + inline std::pair 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); return it != msg_map.end() ? it->second : nullptr; } - signals: + +signals: void showPlot(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 can); void updated(); - public: +public: + Replay *replay = nullptr; + QHash counters; + std::map can_msgs; + QList history_log; + +protected: void recvThread(); void process(std::vector can); + void segmentsMerged(); + + double current_sec = 0.; + std::atomic exit = false; QThread *thread; QString dbc_name; - std::atomic exit = false; - std::map> can_msgs; - std::map counters; - Replay *replay = nullptr; + double begin_sec = 0; + double end_sec = 0; + double event_begin_sec = 0; + double event_end_sec = 0; + bool is_zoomed = false; DBC *dbc = nullptr; std::map msg_map; + QString current_msg_id; }; Q_DECLARE_METATYPE(std::vector); +// TODO: Add helper function in dbc.h +int bigEndianStartBitsIndex(int start_bit); +int bigEndianBitIndex(int index); + extern Parser *parser; diff --git a/tools/cabana/signaledit.cc b/tools/cabana/signaledit.cc new file mode 100644 index 0000000000..080b7920de --- /dev/null +++ b/tools/cabana/signaledit.cc @@ -0,0 +1,203 @@ +#include "tools/cabana/signaledit.h" + +#include +#include +#include +#include +#include + +// 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 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(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(parser->getMsg(id))) { + if (auto signal = form->getSignal()) { + msg->sigs.push_back(*signal); + } + } + QDialog::accept(); + }); +} diff --git a/tools/cabana/signaledit.h b/tools/cabana/signaledit.h new file mode 100644 index 0000000000..b8140cc93b --- /dev/null +++ b/tools/cabana/signaledit.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#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 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); +}; diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index caa109eab0..dbf988d8f2 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -3,9 +3,7 @@ #include #include #include -#include #include -#include #include #include "tools/cabana/parser.h" @@ -17,17 +15,19 @@ inline QString formatTime(int seconds) { 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); + cam_widget = new CameraViewWidget("camerad", VISION_STREAM_ROAD, false, this); + cam_widget->setFixedSize(parent->width(), parent->width() / 1.596); main_layout->addWidget(cam_widget); // slider controls QHBoxLayout *slider_layout = new QHBoxLayout(); - QLabel *time_label = new QLabel("00:00"); + time_label = new QLabel("00:00"); slider_layout->addWidget(time_label); slider = new QSlider(Qt::Horizontal, this); - // slider->setFixedWidth(640); + QObject::connect(slider, &QSlider::sliderMoved, [=]() { + time_label->setText(formatTime(slider->value())); + }); slider->setSingleStep(1); slider->setMaximum(parser->replay->totalSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [=]() { @@ -36,7 +36,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { }); 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); main_layout->addLayout(slider_layout); @@ -57,9 +57,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { 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); - }); + QObject::connect(btn, &QPushButton::clicked, [=]() { parser->replay->setSpeed(speed); }); control_layout->addWidget(btn); group->addButton(btn); if (speed == 1.0) btn->setChecked(true); @@ -67,14 +65,25 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { 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(); + QObject::connect(parser, &Parser::rangeChanged, this, &VideoWidget::rangeChanged); + QObject::connect(parser, &Parser::updated, this, &VideoWidget::updateState); +} + +void VideoWidget::rangeChanged(double min, double max) { + if (!parser->isZoomed()) { + min = 0; + max = parser->replay->totalSeconds(); + } + 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); + } } diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index f0b9e458bd..813516e78f 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,6 +13,10 @@ public: VideoWidget(QWidget *parnet = nullptr); protected: + void rangeChanged(double min, double max); + void updateState(); + CameraViewWidget *cam_widget; + QLabel *time_label, *total_time_label; QSlider *slider; }; diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index 9b7a07a83f..e3d5071412 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -46,7 +46,9 @@ LogReader::~LogReader() { #endif } -bool LogReader::load(const std::string &url, std::atomic *abort, bool local_cache, int chunk_size, int retries) { +bool LogReader::load(const std::string &url, std::atomic *abort, + const std::set &allow, + bool local_cache, int chunk_size, int retries) { raw_ = FileReader(local_cache, chunk_size, retries).read(url, abort); if (raw_.empty()) return false; @@ -54,18 +56,26 @@ bool LogReader::load(const std::string &url, std::atomic *abort, bool loca raw_ = decompressBZ2(raw_, abort); if (raw_.empty()) return false; } - return parse(abort); + return parse(allow, abort); } bool LogReader::load(const std::byte *data, size_t size, std::atomic *abort) { raw_.assign((const char *)data, size); - return parse(abort); + return parse({}, abort); } -bool LogReader::parse(std::atomic *abort) { +bool LogReader::parse(const std::set &allow, std::atomic *abort) { try { kj::ArrayPtr words((const capnp::word *)raw_.data(), raw_.size() / sizeof(capnp::word)); while (words.size() > 0 && !(abort && *abort)) { + if (!allow.empty()) { + capnp::FlatArrayMessageReader reader(words); + auto which = reader.getRoot().which(); + if (allow.find(which) == allow.end()) { + words = kj::arrayPtr(reader.getEnd(), words.end()); + continue; + } + } #ifdef HAS_MEMORY_RESOURCE Event *evt = new (mbr_) Event(words); diff --git a/tools/replay/logreader.h b/tools/replay/logreader.h index bd666d0a74..010839af22 100644 --- a/tools/replay/logreader.h +++ b/tools/replay/logreader.h @@ -5,6 +5,8 @@ #include #endif +#include + #include "cereal/gen/cpp/log.capnp.h" #include "system/camerad/cameras/camera_common.h" #include "tools/replay/filereader.h" @@ -50,12 +52,13 @@ class LogReader { public: LogReader(size_t memory_pool_block_size = DEFAULT_EVENT_MEMORY_POOL_BLOCK_SIZE); ~LogReader(); - bool load(const std::string &url, std::atomic *abort = nullptr, bool local_cache = false, int chunk_size = -1, int retries = 0); + bool load(const std::string &url, std::atomic *abort = nullptr, const std::set &allow = {}, + bool local_cache = false, int chunk_size = -1, int retries = 0); bool load(const std::byte *data, size_t size, std::atomic *abort = nullptr); std::vector events; private: - bool parse(std::atomic *abort); + bool parse(const std::set &allow, std::atomic *abort); std::string raw_; #ifdef HAS_MEMORY_RESOURCE std::pmr::monotonic_buffer_resource *mbr_ = nullptr; diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index 1684dfaca9..b64e87a03e 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -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)) { uint16_t which = event_struct.getFieldByName(it.name).getProto().getDiscriminantValue(); sockets_[which] = it.name; + if (!allow.empty() || !block.empty()) { + allow_list.insert((cereal::Event::Which)which); + } s.push_back(it.name); } } @@ -91,17 +94,17 @@ void Replay::updateEvents(const std::function &lambda) { stream_cv_.notify_one(); } -void Replay::seekTo(int seconds, bool relative) { +void Replay::seekTo(double seconds, bool relative) { seconds = relative ? seconds + currentSeconds() : seconds; updateEvents([&]() { - seconds = std::max(0, seconds); - int seg = seconds / 60; + seconds = std::max(double(0.0), seconds); + int seg = (int)seconds / 60; if (segments_.find(seg) == segments_.end()) { rWarning("can't seek to %d s segment %d is invalid", seconds, seg); return true; } - rInfo("seeking to %d s, segment %d", seconds, seg); + rInfo("seeking to %d s, segment %d", (int)seconds, seg); current_segment_ = seg; cur_mono_time_ = route_start_ts_ + seconds * 1e9; return isSegmentMerged(seg); @@ -122,7 +125,9 @@ void Replay::buildTimeline() { for (int i = 0; i < segments_.size() && !exit_; ++i) { 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) { if (e->which == cereal::Event::Which::CONTROLS_STATE) { @@ -215,7 +220,7 @@ void Replay::queueSegment() { if ((seg && !seg->isLoaded()) || !seg) { if (!seg) { rDebug("loading segment %d...", n); - seg = std::make_unique(n, route_->at(n), flags_); + seg = std::make_unique(n, route_->at(n), flags_, allow_list); QObject::connect(seg.get(), &Segment::loadFinished, this, &Replay::segmentLoadFinished); } break; @@ -270,6 +275,9 @@ void Replay::mergeSegments(const SegmentMap::iterator &begin, const SegmentMap:: segments_merged_ = segments_need_merge; return true; }); + if (stream_thread_) { + emit segmentsMerged(); + } } } @@ -306,6 +314,7 @@ void Replay::startStream(const Segment *cur_segment) { camera_server_ = std::make_unique(camera_size); } + emit segmentsMerged(); // start stream thread stream_thread_ = new QThread(); QObject::connect(stream_thread_, &QThread::started, [=]() { stream(); }); diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 3327362f97..aa0bbc33e7 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -45,18 +45,19 @@ public: void stop(); void pause(bool pause); void seekToFlag(FindFlag flag); - void seekTo(int seconds, bool relative); + void seekTo(double seconds, bool relative); inline bool isPaused() const { return paused_; } inline bool hasFlag(REPLAY_FLAGS flag) const { return flags_ & flag; } inline void addFlag(REPLAY_FLAGS flag) { flags_ |= flag; } inline void removeFlag(REPLAY_FLAGS flag) { flags_ &= ~flag; } inline const Route* route() const { return route_.get(); } - inline int currentSeconds() const { return (cur_mono_time_ - route_start_ts_) / 1e9; } + inline double currentSeconds() const { return double(cur_mono_time_ - route_start_ts_) / 1e9; } inline uint64_t routeStartTime() const { return route_start_ts_; } inline int toSeconds(uint64_t mono_time) const { return (mono_time - route_start_ts_) / 1e9; } inline int totalSeconds() const { return segments_.size() * 60; } inline void setSpeed(float speed) { speed_ = speed; } inline float getSpeed() const { return speed_; } + inline const std::vector *events() const { return events_.get(); } inline const std::string &carFingerprint() const { return car_fingerprint_; } inline const std::vector> getTimeline() { std::lock_guard lk(timeline_lock); @@ -65,6 +66,7 @@ public: signals: void streamStarted(); + void segmentsMerged(); protected slots: void segmentLoadFinished(bool success); @@ -98,7 +100,7 @@ protected: bool paused_ = false; bool events_updated_ = false; uint64_t route_start_ts_ = 0; - uint64_t cur_mono_time_ = 0; + std::atomic cur_mono_time_ = 0; std::unique_ptr> events_; std::unique_ptr> new_events_; std::vector segments_merged_; @@ -114,6 +116,7 @@ protected: std::mutex timeline_lock; QFuture timeline_future; std::vector> timeline; + std::set allow_list; std::string car_fingerprint_; float speed_ = 1.0; }; diff --git a/tools/replay/route.cc b/tools/replay/route.cc index c91b27ae81..f0d6ec5a12 100644 --- a/tools/replay/route.cc +++ b/tools/replay/route.cc @@ -99,7 +99,9 @@ void Route::addFileToSegment(int n, const QString &file) { // 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 &allow) + : seg_num(n), flags(flags), allow(allow) { // [RoadCam, DriverCam, WideRoadCam, log]. fallback to qcamera/qlog const std::array file_list = { (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); } else { log = std::make_unique(); - success = log->load(file, &abort_, local_cache, 0, 3); + success = log->load(file, &abort_, allow, local_cache, 0, 3); } if (!success) { diff --git a/tools/replay/route.h b/tools/replay/route.h index 6ca9c3b883..6b78ebad87 100644 --- a/tools/replay/route.h +++ b/tools/replay/route.h @@ -47,7 +47,7 @@ class Segment : public QObject { Q_OBJECT public: - Segment(int n, const SegmentFile &files, uint32_t flags); + Segment(int n, const SegmentFile &files, uint32_t flags, const std::set &allow = {}); ~Segment(); inline bool isLoaded() const { return !loading_ && !abort_; } @@ -65,4 +65,5 @@ protected: std::atomic loading_ = 0; QFutureSynchronizer synchronizer_; uint32_t flags; + std::set allow; };