diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index c87e2cdd94..b94741ea9c 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -1,5 +1,4 @@ -import os -Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', +Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal', 'transformations', 'widgets', 'opendbc') base_frameworks = qt_env['FRAMEWORKS'] @@ -13,8 +12,6 @@ else: qt_libs = ['qt_util', 'Qt5Charts'] + base_libs if arch in ['x86_64', 'Darwin'] and GetOption('extras'): - qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] - - Import('replay_lib') 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', 'signaledit.cc', 'parser.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) + qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', + 'canmessages.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 20cd889023..88b175663f 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -1,14 +1,14 @@ #include #include +#include #include "selfdrive/ui/qt/util.h" #include "tools/cabana/mainwin.h" -const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; - int main(int argc, char *argv[]) { initApp(argc, argv); QApplication app(argc, argv); + app.setStyle(QStyleFactory::create("Fusion")); QCommandLineParser cmd_parser; cmd_parser.addHelpOption(); @@ -22,7 +22,7 @@ int main(int argc, char *argv[]) { } const QString route = args.empty() ? DEMO_ROUTE : args.first(); - Parser p(&app); + CANMessages p(&app); if (!p.loadRoute(route, cmd_parser.value("data_dir"), true)) { return 0; } diff --git a/tools/cabana/canmessages.cc b/tools/cabana/canmessages.cc new file mode 100644 index 0000000000..a55a045981 --- /dev/null +++ b/tools/cabana/canmessages.cc @@ -0,0 +1,123 @@ +#include "tools/cabana/canmessages.h" + +#include + +Q_DECLARE_METATYPE(std::vector); + +CANMessages *can = nullptr; + +CANMessages::CANMessages(QObject *parent) : QObject(parent) { + can = this; + + qRegisterMetaType>(); + QObject::connect(this, &CANMessages::received, this, &CANMessages::process, Qt::QueuedConnection); +} + +CANMessages::~CANMessages() { + replay->stop(); +} + +static bool event_filter(const Event *e, void *opaque) { + CANMessages *c = (CANMessages *)opaque; + return c->eventFilter(e); +} + +bool CANMessages::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->installEventFilter(event_filter, this); + QObject::connect(replay, &Replay::segmentsMerged, this, &CANMessages::segmentsMerged); + if (replay->load()) { + replay->start(); + return true; + } + return false; +} + +void CANMessages::process(QHash> *messages) { + for (auto it = messages->begin(); it != messages->end(); ++it) { + ++counters[it.key()]; + auto &msgs = can_msgs[it.key()]; + const auto &new_msgs = it.value(); + if (msgs.size() == CAN_MSG_LOG_SIZE || can_msgs[it.key()].size() == 0) { + msgs = std::move(new_msgs); + } else { + msgs.insert(msgs.begin(), std::make_move_iterator(new_msgs.begin()), std::make_move_iterator(new_msgs.end())); + while (msgs.size() >= CAN_MSG_LOG_SIZE) { + msgs.pop_back(); + } + } + } + delete messages; + + if (current_sec < begin_sec || current_sec > end_sec) { + // loop replay in selected range. + seekTo(begin_sec); + } else { + emit updated(); + } +} + +bool CANMessages::eventFilter(const Event *event) { + static double prev_update_sec = 0; + // drop packets when the GUI thread is calling seekTo. to make sure the current_sec is accurate. + if (!seeking && event->which == cereal::Event::Which::CAN) { + if (!received_msgs) { + received_msgs.reset(new QHash>); + received_msgs->reserve(1000); + } + + current_sec = (event->mono_time - replay->routeStartTime()) / (double)1e9; + auto can_events = event->event.getCan(); + for (const auto &c : can_events) { + QString id = QString("%1:%2").arg(c.getSrc()).arg(c.getAddress(), 1, 16); + auto &list = (*received_msgs)[id]; + while (list.size() >= CAN_MSG_LOG_SIZE) { + list.pop_back(); + } + CanData &data = list.emplace_front(); + data.ts = current_sec; + data.bus_time = c.getBusTime(); + data.dat.append((char *)c.getDat().begin(), c.getDat().size()); + } + + if (current_sec < prev_update_sec || (current_sec - prev_update_sec) > 1.0 / FPS) { + prev_update_sec = current_sec; + // use pointer to avoid data copy in queued connection. + emit received(received_msgs.release()); + } + } + return true; +} + +void CANMessages::seekTo(double ts) { + seeking = true; + replay->seekTo(ts, false); + seeking = false; +} + +void CANMessages::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 CANMessages::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 CANMessages::resetRange() { + setRange(event_begin_sec, event_end_sec); +} diff --git a/tools/cabana/canmessages.h b/tools/cabana/canmessages.h new file mode 100644 index 0000000000..a2af2a084c --- /dev/null +++ b/tools/cabana/canmessages.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +#include + +#include "tools/replay/replay.h" + +const int FPS = 10; +const int CAN_MSG_LOG_SIZE = 100; + +struct CanData { + double ts; + uint16_t bus_time; + QByteArray dat; +}; + +class CANMessages : public QObject { + Q_OBJECT + +public: + CANMessages(QObject *parent); + ~CANMessages(); + bool loadRoute(const QString &route, const QString &data_dir, bool use_qcam); + void seekTo(double ts); + void resetRange(); + void setRange(double min, double max); + bool eventFilter(const Event *event); + + inline std::pair range() const { return {begin_sec, end_sec}; } + inline double totalSeconds() const { return replay->totalSeconds(); } + inline double routeStartTime() const { return replay->routeStartTime() / (double)1e9; } + inline double currentSec() const { return current_sec; } + inline bool isZoomed() const { return is_zoomed; } + inline const std::deque &messages(const QString &id) { return can_msgs[id]; } + inline const CanData &lastMessage(const QString &id) { return can_msgs[id].front(); } + + inline const std::vector *events() const { return replay->events(); } + inline void setSpeed(float speed) { replay->setSpeed(speed); } + inline bool isPaused() const { return replay->isPaused(); } + inline void pause(bool pause) { replay->pause(pause); } + inline const std::vector> getTimeline() { return replay->getTimeline(); } + +signals: + void eventsMerged(); + void rangeChanged(double min, double max); + void updated(); + void received(QHash> *); + +public: + QMap> can_msgs; + std::unique_ptr>> received_msgs = nullptr; + QHash counters; + +protected: + void process(QHash> *); + void segmentsMerged(); + + std::atomic current_sec = 0.; + std::atomic seeking = false; + double begin_sec = 0; + double end_sec = 0; + double event_begin_sec = 0; + double event_end_sec = 0; + bool is_zoomed = false; + Replay *replay = nullptr; +}; + +inline QString toHex(const QByteArray &dat) { + return dat.toHex(' ').toUpper(); +} +inline char toHex(uint value) { + return "0123456789ABCDEF"[value & 0xF]; +} + +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)]; +} + +// A global pointer referring to the unique CANMessages object +extern CANMessages *can; diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc index 5caa8d5a43..e8a27ae18c 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chartswidget.cc @@ -7,25 +7,6 @@ #include #include -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 < 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 = (data[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1); - ret |= d << (bits - size); - - bits -= size; - i = sig.is_little_endian ? i - 1 : i + 1; - } - return ret; -} - // ChartsWidget ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { @@ -35,18 +16,22 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { // title bar title_bar = new QWidget(this); QHBoxLayout *title_layout = new QHBoxLayout(title_bar); + title_layout->setContentsMargins(0, 0, 0, 0); title_label = new QLabel(tr("Charts")); title_layout->addWidget(title_label); title_layout->addStretch(); + range_label = new QLabel(); + title_layout->addWidget(range_label); + reset_zoom_btn = new QPushButton("⟲", this); reset_zoom_btn->setVisible(false); reset_zoom_btn->setFixedSize(30, 30); reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)")); title_layout->addWidget(reset_zoom_btn); - remove_all_btn = new QPushButton(tr("✖")); + remove_all_btn = new QPushButton("✖", this); remove_all_btn->setVisible(false); remove_all_btn->setToolTip(tr("Remove all charts")); remove_all_btn->setFixedSize(30, 30); @@ -54,7 +39,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { dock_btn = new QPushButton(); dock_btn->setFixedSize(30, 30); - updateDockButton(); title_layout->addWidget(dock_btn); main_layout->addWidget(title_bar, 0, Qt::AlignTop); @@ -74,53 +58,80 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { main_layout->addWidget(charts_scroll); - QObject::connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart); - QObject::connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart); - QObject::connect(parser, &Parser::signalRemoved, this, &ChartsWidget::removeChart); - QObject::connect(reset_zoom_btn, &QPushButton::clicked, parser, &Parser::resetRange); + updateTitleBar(); + + QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartsWidget::removeChart); + QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); + QObject::connect(can, &CANMessages::rangeChanged, [this]() { updateTitleBar(); }); + QObject::connect(reset_zoom_btn, &QPushButton::clicked, can, &CANMessages::resetRange); QObject::connect(remove_all_btn, &QPushButton::clicked, this, &ChartsWidget::removeAll); - QObject::connect(dock_btn, &QPushButton::clicked, [=]() { + QObject::connect(dock_btn, &QPushButton::clicked, [this]() { emit dock(!docking); docking = !docking; - updateDockButton(); + updateTitleBar(); }); } -void ChartsWidget::updateDockButton() { +void ChartsWidget::updateTitleBar() { + if (!charts.size()) { + title_bar->setVisible(false); + return; + } + + title_label->setText(tr("Charts (%1)").arg(charts.size())); + + // show select range + if (can->isZoomed()) { + auto [min, max] = can->range(); + range_label->setText(tr("%1 - %2").arg(min, 0, 'f', 2).arg(max, 0, 'f', 2)); + range_label->setVisible(true); + reset_zoom_btn->setEnabled(true); + } else { + reset_zoom_btn->setEnabled(false); + range_label->setVisible(false); + } + dock_btn->setText(docking ? "⬈" : "⬋"); dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); + remove_all_btn->setVisible(!charts.empty()); + reset_zoom_btn->setVisible(!charts.empty()); + title_bar->setVisible(true); } 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()) { auto chart = new ChartWidget(id, sig_name, this); + QObject::connect(chart, &ChartWidget::remove, [=]() { + removeChart(id, sig_name); + }); charts_layout->insertWidget(0, chart); charts[char_name] = chart; } - remove_all_btn->setVisible(true); - reset_zoom_btn->setVisible(true); - title_label->setText(tr("Charts (%1)").arg(charts.size())); + updateTitleBar(); } void ChartsWidget::removeChart(const QString &id, const QString &sig_name) { - if (auto it = charts.find(id + sig_name); it != charts.end()) { + if (auto it = charts.find(id + ":" + sig_name); it != charts.end()) { it->second->deleteLater(); charts.erase(it); - if (charts.empty()) { - remove_all_btn->setVisible(false); - reset_zoom_btn->setVisible(false); - } } - title_label->setText(tr("Charts (%1)").arg(charts.size())); + updateTitleBar(); } void ChartsWidget::removeAll() { for (auto [_, chart] : charts) chart->deleteLater(); charts.clear(); - remove_all_btn->setVisible(false); - reset_zoom_btn->setVisible(false); + updateTitleBar(); +} + +bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) { + if (obj != this && event->type() == QEvent::Close) { + emit dock_btn->clicked(); + return true; + } + return false; } // ChartWidget @@ -138,16 +149,14 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa header->setStyleSheet("background-color:white"); QHBoxLayout *header_layout = new QHBoxLayout(header); header_layout->setContentsMargins(11, 11, 11, 0); - QLabel *title = new QLabel(tr("%1 %2").arg(parser->getMsg(id)->name.c_str()).arg(id)); + QLabel *title = new QLabel(tr("%1 %2").arg(dbc()->msg(id)->name.c_str()).arg(id)); header_layout->addWidget(title); header_layout->addStretch(); QPushButton *remove_btn = new QPushButton("✖", this); remove_btn->setFixedSize(30, 30); remove_btn->setToolTip(tr("Remove chart")); - QObject::connect(remove_btn, &QPushButton::clicked, [=]() { - emit parser->hidePlot(id, sig_name); - }); + QObject::connect(remove_btn, &QPushButton::clicked, this, &ChartWidget::remove); header_layout->addWidget(remove_btn); chart_layout->addWidget(header); @@ -163,9 +172,8 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa 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 = new ChartView(chart); chart_view->setFixedHeight(300); chart_view->setRenderHint(QPainter::Antialiasing); chart_view->setRubberBand(QChartView::HorizontalRubberBand); @@ -184,26 +192,28 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa line_marker->raise(); 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); - + QObject::connect(can, &CANMessages::updated, this, &ChartWidget::updateState); + QObject::connect(can, &CANMessages::rangeChanged, this, &ChartWidget::rangeChanged); + QObject::connect(can, &CANMessages::eventsMerged, this, &ChartWidget::updateSeries); + QObject::connect(dynamic_cast(chart->axisX()), &QValueAxis::rangeChanged, can, &CANMessages::setRange); + QObject::connect(dbc(), &DBCManager::signalUpdated, [this](const QString &msg_id, const QString &sig_name) { + if (this->id == msg_id && this->sig_name == sig_name) + updateSeries(); + }); updateSeries(); } void ChartWidget::updateState() { auto chart = chart_view->chart(); auto axis_x = dynamic_cast(chart->axisX()); - int x = chart->plotArea().left() + chart->plotArea().width() * (parser->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min()); - if (line_marker_x != x) { - line_marker->setX(x); - line_marker_x = x; - } + + int x = chart->plotArea().left() + chart->plotArea().width() * (can->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min()); + line_marker->setX(x); } void ChartWidget::updateSeries() { - const Signal *sig = parser->getSig(id, sig_name); - auto events = parser->replay->events(); + const Signal *sig = dbc()->signal(id, sig_name); + auto events = can->events(); if (!sig || !events) return; auto l = id.split(':'); @@ -212,18 +222,14 @@ void ChartWidget::updateSeries() { vals.clear(); vals.reserve(3 * 60 * 100); - uint64_t route_start_time = parser->replay->routeStartTime(); + uint64_t route_start_time = can->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 + double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *sig); + double ts = (evt->mono_time / (double)1e9) - route_start_time; // seconds vals.push_back({ts, value}); } } @@ -231,7 +237,7 @@ void ChartWidget::updateSeries() { } QLineSeries *series = (QLineSeries *)chart_view->chart()->series()[0]; series->replace(vals); - auto [begin, end] = parser->range(); + auto [begin, end] = can->range(); chart_view->chart()->axisX()->setRange(begin, end); updateAxisY(); } @@ -247,6 +253,7 @@ void ChartWidget::rangeChanged(qreal min, qreal max) { // auto zoom on yaxis void ChartWidget::updateAxisY() { const auto axis_x = dynamic_cast(chart_view->chart()->axisX()); + const auto axis_y = dynamic_cast(chart_view->chart()->axisY()); // vals is a sorted list auto begin = std::lower_bound(vals.begin(), vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; }); if (begin == vals.end()) @@ -254,14 +261,45 @@ void ChartWidget::updateAxisY() { auto end = std::upper_bound(vals.begin(), vals.end(), axis_x->max(), [](double x, auto &p) { return x < p.x(); }); const auto [min, max] = std::minmax_element(begin, end, [](auto &p1, auto &p2) { return p1.y() < p2.y(); }); - chart_view->chart()->axisY()->setRange(min->y(), max->y()); + if (min->y() == max->y()) { + if (max->y() < 0) { + axis_y->setRange(max->y(), 0); + } else { + axis_y->setRange(0, max->y() == 0 ? 1 : max->y()); + } + } else { + axis_y->setRange(min->y(), max->y()); + } } +// ChartView + +void ChartView::mouseReleaseEvent(QMouseEvent *event) { + auto rubber = findChild(); + if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) { + auto [begin, end] = can->range(); + if (rubber->width() <= 0) { + double seek_to = begin + ((event->pos().x() - chart()->plotArea().x()) / chart()->plotArea().width()) * (end - begin); + can->seekTo(seek_to); + } else if (((double)rubber->width() / chart()->plotArea().width()) * (end - begin) < 0.5) { + // don't zoom if selected range is less than 0.5s + rubber->hide(); + event->accept(); + return; + } + } + // TODO: right-click to reset zoom + QChartView::mouseReleaseEvent(event); +} + + // LineMarker void LineMarker::setX(double x) { - x_pos = x; - update(); + if (x != x_pos) { + x_pos = x; + update(); + } } void LineMarker::paintEvent(QPaintEvent *event) { diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h index 81df2237bc..7dbf0f108b 100644 --- a/tools/cabana/chartswidget.h +++ b/tools/cabana/chartswidget.h @@ -9,7 +9,8 @@ #include #include -#include "tools/cabana/parser.h" +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" using namespace QtCharts; @@ -22,7 +23,15 @@ public: private: void paintEvent(QPaintEvent *event) override; - double x_pos = 0.0; + double x_pos = -1; +}; + +class ChartView : public QChartView { + Q_OBJECT + +public: + ChartView(QChart *chart, QWidget *parent = nullptr) : QChartView(chart, parent) {} + void mouseReleaseEvent(QMouseEvent *event) override; }; class ChartWidget : public QWidget { @@ -32,6 +41,9 @@ public: ChartWidget(const QString &id, const QString &sig_name, QWidget *parent); inline QChart *chart() const { return chart_view->chart(); } +signals: + void remove(); + private: void updateState(); void addData(const CanData &can_data, const Signal &sig); @@ -41,9 +53,8 @@ private: QString id; QString sig_name; - QChartView *chart_view = nullptr; + ChartView *chart_view = nullptr; LineMarker *line_marker = nullptr; - double line_marker_x = 0.0; QList vals; }; @@ -54,7 +65,6 @@ public: ChartsWidget(QWidget *parent = nullptr); void addChart(const QString &id, const QString &sig_name); void removeChart(const QString &id, const QString &sig_name); - void removeAll(); inline bool hasChart(const QString &id, const QString &sig_name) { return charts.find(id + sig_name) != charts.end(); } @@ -64,10 +74,13 @@ signals: private: void updateState(); - void updateDockButton(); + void updateTitleBar(); + void removeAll(); + bool eventFilter(QObject *obj, QEvent *event); QWidget *title_bar; QLabel *title_label; + QLabel *range_label; bool docking = true; QPushButton *dock_btn; QPushButton *reset_zoom_btn; diff --git a/tools/cabana/dbcmanager.cc b/tools/cabana/dbcmanager.cc new file mode 100644 index 0000000000..1cb6da7fb5 --- /dev/null +++ b/tools/cabana/dbcmanager.cc @@ -0,0 +1,117 @@ +#include "tools/cabana/dbcmanager.h" + +#include + +DBCManager::DBCManager(QObject *parent) : QObject(parent) {} + +DBCManager::~DBCManager() {} + +void DBCManager::open(const QString &dbc_file_name) { + dbc_name = dbc_file_name; + dbc = const_cast(dbc_lookup(dbc_name.toStdString())); + msg_map.clear(); + for (auto &msg : dbc->msgs) { + msg_map[msg.address] = &msg; + } + emit DBCFileChanged(); +} + +void save(const QString &dbc_file_name) { + // TODO: save DBC to file +} + +void DBCManager::updateMsg(const QString &id, const QString &name, uint32_t size) { + auto m = const_cast(msg(id)); + if (m) { + m->name = name.toStdString(); + m->size = size; + } else { + uint32_t address = addressFromId(id); + dbc->msgs.push_back({.address = address, .name = name.toStdString(), .size = size}); + msg_map[address] = &dbc->msgs.back(); + } + emit msgUpdated(id); +} + +void DBCManager::addSignal(const QString &id, const Signal &sig) { + if (Msg *m = const_cast(msg(id))) { + m->sigs.push_back(sig); + emit signalAdded(id, QString::fromStdString(sig.name)); + } +} + +void DBCManager::updateSignal(const QString &id, const QString &sig_name, const Signal &sig) { + if (Signal *s = const_cast(signal(id, sig_name))) { + *s = sig; + emit signalUpdated(id, sig_name); + } +} + +void DBCManager::removeSignal(const QString &id, const QString &sig_name) { + if (Msg *m = const_cast(msg(id))) { + auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); + if (it != m->sigs.end()) { + m->sigs.erase(it); + emit signalRemoved(id, sig_name); + } + } +} + +const Signal *DBCManager::signal(const QString &id, const QString &sig_name) const { + if (auto m = msg(id)) { + auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [&](auto &s) { return sig_name == s.name.c_str(); }); + if (it != m->sigs.end()) + return &(*it); + } + return nullptr; +} + +uint32_t DBCManager::addressFromId(const QString &id) { + return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16); +} + +DBCManager *dbc() { + static DBCManager dbc_manager(nullptr); + return &dbc_manager; +} + +// 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); +} + +double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig) { + int64_t val = 0; + + int i = sig.msb / 8; + int bits = sig.size; + 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 = (data[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1); + val |= d << (bits - size); + + bits -= size; + i = sig.is_little_endian ? i - 1 : i + 1; + } + if (sig.is_signed) { + val -= ((val >> (sig.size - 1)) & 0x1) ? (1ULL << sig.size) : 0; + } + double value = val * sig.factor + sig.offset; + return value; +} diff --git a/tools/cabana/dbcmanager.h b/tools/cabana/dbcmanager.h new file mode 100644 index 0000000000..06c071be82 --- /dev/null +++ b/tools/cabana/dbcmanager.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include "opendbc/can/common_dbc.h" + +class DBCManager : public QObject { + Q_OBJECT + +public: + DBCManager(QObject *parent); + ~DBCManager(); + + void open(const QString &dbc_file_name); + void save(const QString &dbc_file_name); + + const Signal *signal(const QString &id, const QString &sig_name) const; + void addSignal(const QString &id, const Signal &sig); + void updateSignal(const QString &id, const QString &sig_name, const Signal &sig); + void removeSignal(const QString &id, const QString &sig_name); + + static uint32_t addressFromId(const QString &id); + inline static std::vector allDBCNames() { return get_dbc_names(); } + inline QString name() const { return dbc_name; } + + void updateMsg(const QString &id, const QString &name, uint32_t size); + inline const Msg *msg(const QString &id) const { return msg(addressFromId(id)); } + inline const Msg *msg(uint32_t address) const { + auto it = msg_map.find(address); + return it != msg_map.end() ? it->second : nullptr; + } + +signals: + void signalAdded(const QString &id, const QString &sig_name); + void signalRemoved(const QString &id, const QString &sig_name); + void signalUpdated(const QString &id, const QString &sig_name); + void msgUpdated(const QString &id); + void DBCFileChanged(); + +private: + QString dbc_name; + DBC *dbc = nullptr; + std::unordered_map msg_map; +}; + +// TODO: Add helper function in dbc.h +double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig); +int bigEndianStartBitsIndex(int start_bit); +int bigEndianBitIndex(int index); + +DBCManager *dbc(); diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 1b6552804e..7c1847230b 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -1,39 +1,29 @@ #include "tools/cabana/detailwidget.h" -#include #include #include #include +#include #include #include -#include - -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/scrollview.h" - -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); - name_label = new QLabel(this); - name_label->setStyleSheet("font-weight:bold;"); - name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - name_label->setAlignment(Qt::AlignCenter); - main_layout->addWidget(name_label); - // title QHBoxLayout *title_layout = new QHBoxLayout(); + title_layout->addWidget(new QLabel("time:")); time_label = new QLabel(this); title_layout->addWidget(time_label); + time_label->setStyleSheet("font-weight:bold"); + title_layout->addStretch(); + name_label = new QLabel(this); + name_label->setStyleSheet("font-weight:bold;"); + title_layout->addWidget(name_label); title_layout->addStretch(); - edit_btn = new QPushButton(tr("Edit"), this); edit_btn->setVisible(false); title_layout->addWidget(edit_btn); @@ -41,79 +31,104 @@ DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) { // binary view binary_view = new BinaryView(this); - main_layout->addWidget(binary_view); + main_layout->addWidget(binary_view, 0, Qt::AlignTop); + + // signal header + signals_header = new QWidget(this); + QHBoxLayout *signals_header_layout = new QHBoxLayout(signals_header); + signals_header_layout->addWidget(new QLabel(tr("Signals"))); + signals_header_layout->addStretch(); + QPushButton *add_sig_btn = new QPushButton(tr("Add signal"), this); + signals_header_layout->addWidget(add_sig_btn); + signals_header->setVisible(false); + main_layout->addWidget(signals_header); // 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); - signals_layout->addWidget(add_sig_btn); - main_layout->addLayout(signals_layout); - + scroll = new ScrollArea(this); QWidget *container = new QWidget(this); + container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); QVBoxLayout *container_layout = new QVBoxLayout(container); signal_edit_layout = new QVBoxLayout(); signal_edit_layout->setSpacing(2); container_layout->addLayout(signal_edit_layout); - 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); + history_log = new HistoryLog(this); + main_layout->addWidget(history_log); + 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); + QObject::connect(can, &CANMessages::updated, this, &DetailWidget::updateState); } -void DetailWidget::setMsg(const CanData *c) { - can_data = c; - clearLayout(signal_edit_layout); - edit_btn->setVisible(true); +void DetailWidget::setMessage(const QString &message_id) { + msg_id = message_id; + for (auto f : signal_forms) { + f->deleteLater(); + } + signal_forms.clear(); - if (auto msg = parser->getMsg(can_data->address)) { - name_label->setText(msg->name.c_str()); - add_sig_btn->setVisible(true); + if (auto msg = dbc()->msg(msg_id)) { for (int i = 0; i < msg->sigs.size(); ++i) { - signal_edit_layout->addWidget(new SignalEdit(can_data->id, msg->sigs[i], getColor(i))); + auto form = new SignalEdit(i, msg_id, msg->sigs[i], getColor(i)); + signal_edit_layout->addWidget(form); + QObject::connect(form, &SignalEdit::showChart, this, &DetailWidget::showChart); + QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showForm); + signal_forms.push_back(form); } + name_label->setText(msg->name.c_str()); + signals_header->setVisible(true); } else { name_label->setText(tr("untitled")); - add_sig_btn->setVisible(false); + signals_header->setVisible(false); } + edit_btn->setVisible(true); - binary_view->setMsg(can_data); - history_log->clear(); + binary_view->setMessage(msg_id); + history_log->setMessage(msg_id); } void DetailWidget::updateState() { - if (!can_data) return; + time_label->setText(QString::number(can->currentSec(), 'f', 3)); + if (msg_id.isEmpty()) return; - time_label->setText(QString("time: %1").arg(can_data->ts, 0, 'f', 3)); - binary_view->setData(can_data->dat); + binary_view->updateState(); history_log->updateState(); } void DetailWidget::editMsg() { - EditMessageDialog dlg(can_data->id, this); + EditMessageDialog dlg(msg_id, this); if (dlg.exec()) { - setMsg(can_data); + setMessage(msg_id); } } void DetailWidget::addSignal() { - AddSignalDialog dlg(can_data->id, this); + AddSignalDialog dlg(msg_id, this); if (dlg.exec()) { - setMsg(can_data); + setMessage(msg_id); + } +} + +void DetailWidget::showForm() { + SignalEdit *sender = qobject_cast(QObject::sender()); + if (sender->isFormVisible()) { + sender->setFormVisible(false); + } else { + for (auto f : signal_forms) { + f->setFormVisible(f == sender); + if (f == sender) { + // scroll to header + QTimer::singleShot(0, [=]() { + const QPoint p = f->mapTo(scroll, QPoint(0, 0)); + scroll->verticalScrollBar()->setValue(p.y() + scroll->verticalScrollBar()->value()); + }); + } + } } } @@ -121,6 +136,7 @@ void DetailWidget::addSignal() { BinaryView::BinaryView(QWidget *parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); table = new QTableWidget(this); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); table->horizontalHeader()->hide(); @@ -131,9 +147,10 @@ BinaryView::BinaryView(QWidget *parent) { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); } -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(); +void BinaryView::setMessage(const QString &message_id) { + msg_id = message_id; + const Msg *msg = dbc()->msg(msg_id); + int row_count = msg ? msg->size : can->lastMessage(msg_id).dat.size(); table->setRowCount(row_count); table->setColumnCount(9); @@ -151,8 +168,8 @@ void BinaryView::setMsg(const CanData *can_data) { } } + // set background color 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); @@ -162,80 +179,44 @@ void BinaryView::setMsg(const CanData *can_data) { } } - setFixedHeight(table->rowHeight(0) * table->rowCount() + 25); + table->setFixedHeight(table->rowHeight(0) * table->rowCount() + table->horizontalHeader()->height() + 2); + updateState(); } -void BinaryView::setData(const QByteArray &binary) { - std::string s; - for (int j = 0; j < binary.size(); ++j) { - s += std::bitset<8>(binary[j]).to_string(); - } +void BinaryView::updateState() { + if (msg_id.isEmpty()) return; + + const auto &binary = can->lastMessage(msg_id).dat; setUpdatesEnabled(false); char hex[3] = {'\0'}; for (int i = 0; i < binary.size(); ++i) { for (int j = 0; j < 8; ++j) { - table->item(i, j)->setText(QChar(s[i * 8 + j])); + table->item(i, j)->setText(QChar((binary[i] >> (7 - j)) & 1 ? '1' : '0')); } - sprintf(&hex[0], "%02X", (unsigned char)binary[i]); + hex[0] = toHex(binary[i] >> 4); + hex[1] = toHex(binary[i] & 0xf); table->item(i, 8)->setText(hex); } setUpdatesEnabled(true); } -// HistoryLog - -HistoryLog::HistoryLog(QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - QLabel *title = new QLabel("TIME BYTES"); - main_layout->addWidget(title); - - 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 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(toHex(c.dat))); - } - - for (; i < std::size(labels); ++i) { - labels[i]->setVisible(false); - } -} - -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) { +EditMessageDialog::EditMessageDialog(const QString &msg_id, QWidget *parent) : msg_id(msg_id), QDialog(parent) { setWindowTitle(tr("Edit message")); QVBoxLayout *main_layout = new QVBoxLayout(this); QFormLayout *form_layout = new QFormLayout(); - form_layout->addRow("ID", new QLabel(id)); + form_layout->addRow("ID", new QLabel(msg_id)); - auto msg = const_cast(parser->getMsg(id)); + const auto msg = dbc()->msg(msg_id); name_edit = new QLineEdit(this); name_edit->setText(msg ? msg->name.c_str() : "untitled"); form_layout->addRow(tr("Name"), name_edit); size_spin = new QSpinBox(this); - size_spin->setValue(msg ? msg->size : parser->can_msgs[id].dat.size()); + size_spin->setValue(msg ? msg->size : can->lastMessage(msg_id).dat.size()); form_layout->addRow(tr("Size"), size_spin); main_layout->addLayout(form_layout); @@ -243,22 +224,33 @@ EditMessageDialog::EditMessageDialog(const QString &id, QWidget *parent) : id(id auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); main_layout->addWidget(buttonBox); + setFixedWidth(parent->width() * 0.9); + connect(buttonBox, &QDialogButtonBox::accepted, this, &EditMessageDialog::save); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } void EditMessageDialog::save() { - if (size_spin->value() <= 0 || name_edit->text().isEmpty()) return; + const QString name = name_edit->text(); + if (size_spin->value() <= 0 || name_edit->text().isEmpty() || name == tr("untitled")) + 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); - } + dbc()->updateMsg(msg_id, name, size_spin->value()); QDialog::accept(); } + +// ScrollArea + +bool ScrollArea::eventFilter(QObject *obj, QEvent *ev) { + if (obj == widget() && ev->type() == QEvent::Resize) { + int height = widget()->height() + 4; + setMinimumHeight(height > 480 ? 480 : height); + setMaximumHeight(height); + } + return QScrollArea::eventFilter(obj, ev); +} + +void ScrollArea::setWidget(QWidget *w) { + QScrollArea::setWidget(w); + w->installEventFilter(this); +} diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index b2e7cbf3b7..99fe321012 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -3,35 +3,28 @@ #include #include #include +#include #include #include #include #include "opendbc/can/common.h" #include "opendbc/can/common_dbc.h" -#include "tools/cabana/parser.h" +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/historylog.h" #include "tools/cabana/signaledit.h" -class HistoryLog : public QWidget { - Q_OBJECT - -public: - HistoryLog(QWidget *parent); - void clear(); - void updateState(); - -private: - QLabel *labels[LOG_SIZE] = {}; -}; - class BinaryView : public QWidget { Q_OBJECT public: BinaryView(QWidget *parent); - void setMsg(const CanData *can_data); - void setData(const QByteArray &binary); + void setMessage(const QString &message_id); + void updateState(); +private: + QString msg_id; QTableWidget *table; }; @@ -39,14 +32,23 @@ class EditMessageDialog : public QDialog { Q_OBJECT public: - EditMessageDialog(const QString &id, QWidget *parent); + EditMessageDialog(const QString &msg_id, QWidget *parent); protected: void save(); + QString msg_id; QLineEdit *name_edit; QSpinBox *size_spin; - QString id; +}; + +class ScrollArea : public QScrollArea { + Q_OBJECT + +public: + ScrollArea(QWidget *parent) : QScrollArea(parent) {} + bool eventFilter(QObject *obj, QEvent *ev) override; + void setWidget(QWidget *w); }; class DetailWidget : public QWidget { @@ -54,17 +56,26 @@ class DetailWidget : public QWidget { public: DetailWidget(QWidget *parent); - void setMsg(const CanData *c); + void setMessage(const QString &message_id); + +signals: + void showChart(const QString &msg_id, const QString &sig_name); + +private slots: + void showForm(); private: - void updateState(); void addSignal(); void editMsg(); + void updateState(); - const CanData *can_data = nullptr; + QString msg_id; QLabel *name_label, *time_label; - QPushButton *edit_btn, *add_sig_btn; + QPushButton *edit_btn; QVBoxLayout *signal_edit_layout; + QWidget *signals_header; + QList signal_forms; HistoryLog *history_log; BinaryView *binary_view; + ScrollArea *scroll; }; diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc new file mode 100644 index 0000000000..494e281cb1 --- /dev/null +++ b/tools/cabana/historylog.cc @@ -0,0 +1,91 @@ +#include "tools/cabana/historylog.h" + +#include +#include + +QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { + if (role == Qt::DisplayRole) { + const auto &can_msgs = can->messages(msg_id); + if (index.row() < can_msgs.size()) { + const auto &can_data = can_msgs[index.row()]; + auto msg = dbc()->msg(msg_id); + if (msg && index.column() < msg->sigs.size()) { + return get_raw_value((uint8_t *)can_data.dat.begin(), can_data.dat.size(), msg->sigs[index.column()]); + } else { + return toHex(can_data.dat); + } + } + } + return {}; +} + +void HistoryLogModel::setMessage(const QString &message_id) { + beginResetModel(); + msg_id = message_id; + const auto msg = dbc()->msg(message_id); + column_count = msg && !msg->sigs.empty() ? msg->sigs.size() : 1; + row_count = 0; + endResetModel(); + + updateState(); +} + +QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (orientation == Qt::Horizontal) { + auto msg = dbc()->msg(msg_id); + if (msg && section < msg->sigs.size()) { + if (role == Qt::BackgroundRole) { + return QBrush(QColor(getColor(section))); + } else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { + return QString::fromStdString(msg->sigs[section].name); + } + } + } else if (role == Qt::DisplayRole) { + const auto &can_msgs = can->messages(msg_id); + if (section < can_msgs.size()) { + return QString::number(can_msgs[section].ts, 'f', 2); + } + } + return {}; +} + +void HistoryLogModel::updateState() { + if (msg_id.isEmpty()) return; + + const auto &can_msgs = can->messages(msg_id); + int prev_row_count = row_count; + row_count = can_msgs.size(); + int delta = row_count - prev_row_count; + if (delta > 0) { + beginInsertRows({}, prev_row_count, row_count - 1); + endInsertRows(); + } else if (delta < 0) { + beginRemoveRows({}, row_count, prev_row_count - 1); + endRemoveRows(); + } + if (row_count > 0) { + emit dataChanged(index(0, 0), index(row_count - 1, column_count - 1)); + emit headerDataChanged(Qt::Vertical, 0, row_count - 1); + } +} + +HistoryLog::HistoryLog(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + model = new HistoryLogModel(this); + table = new QTableView(this); + table->setModel(model); + table->horizontalHeader()->setStretchLastSection(true); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->setStyleSheet("QTableView::item { border:0px; padding-left:5px; padding-right:5px; }"); + table->verticalHeader()->setStyleSheet("QHeaderView::section {padding-left: 5px; padding-right: 5px;min-width:40px;}"); + main_layout->addWidget(table); +} + +void HistoryLog::setMessage(const QString &message_id) { + model->setMessage(message_id); +} + +void HistoryLog::updateState() { + model->updateState(); +} diff --git a/tools/cabana/historylog.h b/tools/cabana/historylog.h new file mode 100644 index 0000000000..f3a9046bfa --- /dev/null +++ b/tools/cabana/historylog.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" + +class HistoryLogModel : public QAbstractTableModel { +Q_OBJECT + +public: + HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {} + void setMessage(const QString &message_id); + void updateState(); + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } + +private: + QString msg_id; + int row_count = 0; + int column_count = 0; +}; + +class HistoryLog : public QWidget { + Q_OBJECT + +public: + HistoryLog(QWidget *parent); + void setMessage(const QString &message_id); + void updateState(); + +private: + QTableView *table; + HistoryLogModel *model; +}; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 49e6cd2cca..6fa24ea21d 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -1,5 +1,6 @@ #include "tools/cabana/mainwin.h" +#include #include #include #include @@ -30,13 +31,15 @@ MainWindow::MainWindow() : QWidget() { h_layout->addWidget(right_container); - QObject::connect(messages_widget, &MessagesWidget::msgChanged, detail_widget, &DetailWidget::setMsg); + QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, detail_widget, &DetailWidget::setMessage); + QObject::connect(detail_widget, &DetailWidget::showChart, charts_widget, &ChartsWidget::addChart); QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts); } void MainWindow::dockCharts(bool dock) { charts_widget->setUpdatesEnabled(false); if (dock && floating_window) { + floating_window->removeEventFilter(charts_widget); r_layout->addWidget(charts_widget); floating_window->deleteLater(); floating_window = nullptr; @@ -44,7 +47,7 @@ void MainWindow::dockCharts(bool dock) { floating_window = new QWidget(nullptr); floating_window->setLayout(new QVBoxLayout()); floating_window->layout()->addWidget(charts_widget); - floating_window->setWindowFlags(Qt::WindowTitleHint | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint); + floating_window->installEventFilter(charts_widget); floating_window->setMinimumSize(QGuiApplication::primaryScreen()->size() / 2); floating_window->showMaximized(); } diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index bcd15e4d8e..b0d7c273da 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -3,7 +3,6 @@ #include "tools/cabana/chartswidget.h" #include "tools/cabana/detailwidget.h" #include "tools/cabana/messageswidget.h" -#include "tools/cabana/parser.h" #include "tools/cabana/videowidget.h" class MainWindow : public QWidget { diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 7de3507b3d..eaf84fbace 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -1,19 +1,32 @@ #include "tools/cabana/messageswidget.h" #include +#include #include +#include #include +#include #include +#include "tools/cabana/dbcmanager.h" + MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); + // DBC file selector QHBoxLayout *dbc_file_layout = new QHBoxLayout(); QComboBox *combo = new QComboBox(this); - auto dbc_names = get_dbc_names(); + auto dbc_names = dbc()->allDBCNames(); for (const auto &name : dbc_names) { combo->addItem(QString::fromStdString(name)); } + combo->setEditable(true); + combo->setCurrentText(QString()); + combo->setInsertPolicy(QComboBox::NoInsert); + combo->completer()->setCompletionMode(QCompleter::PopupCompletion); + QFont font; + font.setBold(true); + combo->lineEdit()->setFont(font); dbc_file_layout->addWidget(combo); dbc_file_layout->addStretch(); @@ -21,73 +34,104 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { dbc_file_layout->addWidget(save_btn); main_layout->addLayout(dbc_file_layout); - filter = new QLineEdit(this); + // message filter + QLineEdit *filter = new QLineEdit(this); filter->setPlaceholderText(tr("filter messages")); main_layout->addWidget(filter); - table_widget = new QTableWidget(this); + // message table + table_widget = new QTableView(this); + model = new MessageListModel(this); + QSortFilterProxyModel *proxy_model = new QSortFilterProxyModel(this); + proxy_model->setSourceModel(model); + proxy_model->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxy_model->setDynamicSortFilter(false); + table_widget->setModel(proxy_model); table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); table_widget->setSelectionMode(QAbstractItemView::SingleSelection); table_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - table_widget->setColumnCount(4); + table_widget->setSortingEnabled(true); table_widget->setColumnWidth(0, 250); table_widget->setColumnWidth(1, 80); table_widget->setColumnWidth(2, 80); - table_widget->setHorizontalHeaderLabels({tr("Name"), tr("ID"), tr("Count"), tr("Bytes")}); table_widget->horizontalHeader()->setStretchLastSection(true); + table_widget->verticalHeader()->hide(); + table_widget->sortByColumn(0, Qt::AscendingOrder); main_layout->addWidget(table_widget); - QObject::connect(parser, &Parser::updated, this, &MessagesWidget::updateState); + // signals/slots + QObject::connect(filter, &QLineEdit::textChanged, proxy_model, &QSortFilterProxyModel::setFilterFixedString); + QObject::connect(can, &CANMessages::updated, model, &MessageListModel::updateState); + QObject::connect(combo, SIGNAL(activated(const QString &)), SLOT(dbcSelectionChanged(const QString &))); QObject::connect(save_btn, &QPushButton::clicked, [=]() { // TODO: save DBC to file }); - QObject::connect(combo, &QComboBox::currentTextChanged, [=](const QString &dbc) { - parser->openDBC(dbc); - }); - QObject::connect(table_widget, &QTableWidget::itemSelectionChanged, [=]() { - const CanData *c = &(parser->can_msgs[table_widget->selectedItems()[1]->text()]); - parser->setCurrentMsg(c->id); - emit msgChanged(c); + QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { + if (current.isValid()) { + emit msgSelectionChanged(table_widget->model()->data(current, Qt::UserRole).toString()); + } }); // For test purpose combo->setCurrentText("toyota_nodsu_pt_generated"); } -void MessagesWidget::updateState() { - auto getTableItem = [=](int row, int col) -> QTableWidgetItem * { - 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; - }; +void MessagesWidget::dbcSelectionChanged(const QString &dbc_file) { + dbc()->open(dbc_file); + // update detailwidget + auto current = table_widget->selectionModel()->currentIndex(); + if (current.isValid()) { + emit msgSelectionChanged(table_widget->model()->data(current, Qt::UserRole).toString()); + } +} - table_widget->setRowCount(parser->can_msgs.size()); - int i = 0; - 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 = untitled; - } - if (!filter_str.isEmpty() && !name.contains(filter_str, Qt::CaseInsensitive)) { - table_widget->hideRow(i++); - continue; +// MessageListModel + +QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) + return (QString[]){"Name", "ID", "Count", "Bytes"}[section]; + else if (orientation == Qt::Vertical && role == Qt::DisplayRole) { + // return QString::number(section); + } + return {}; +} + +QVariant MessageListModel::data(const QModelIndex &index, int role) const { + if (role == Qt::DisplayRole) { + auto it = std::next(can->can_msgs.begin(), index.row()); + if (it != can->can_msgs.end() && !it.value().empty()) { + const auto &d = it.value().front(); + const QString &msg_id = it.key(); + switch (index.column()) { + case 0: { + auto msg = dbc()->msg(msg_id); + QString name = msg ? msg->name.c_str() : "untitled"; + return name; + } + case 1: return msg_id; + case 2: return can->counters[msg_id]; + case 3: return toHex(d.dat); + } } + } else if (role == Qt::UserRole) { + return std::next(can->can_msgs.begin(), index.row()).key(); + } + return {}; +} - 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(toHex(c.dat)); - table_widget->showRow(i); - i++; +void MessageListModel::updateState() { + int prev_row_count = row_count; + row_count = can->can_msgs.size(); + int delta = row_count - prev_row_count; + if (delta > 0) { + beginInsertRows({}, prev_row_count, row_count - 1); + endInsertRows(); + } else if (delta < 0) { + beginRemoveRows({}, row_count, prev_row_count - 1); + endRemoveRows(); } - if (table_widget->currentRow() == -1) { - table_widget->selectRow(0); + + if (row_count > 0) { + emit dataChanged(index(0, 0), index(row_count - 1, 3)); } } diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index 1dbb4a1af3..f6487ba2bd 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -1,24 +1,38 @@ #pragma once -#include -#include -#include +#include +#include -#include "tools/cabana/parser.h" +#include "tools/cabana/canmessages.h" + +class MessageListModel : public QAbstractTableModel { +Q_OBJECT + +public: + MessageListModel(QObject *parent) : QAbstractTableModel(parent) {} + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 4; } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } + void updateState(); + +private: + int row_count = 0; +}; class MessagesWidget : public QWidget { Q_OBJECT - public: +public: MessagesWidget(QWidget *parent); - public slots: - void updateState(); +public slots: + void dbcSelectionChanged(const QString &dbc_file); - signals: - void msgChanged(const CanData *id); +signals: + void msgSelectionChanged(const QString &message_id); - protected: - QLineEdit *filter; - QTableWidget *table_widget; +protected: + QTableView *table_widget; + MessageListModel *model; }; diff --git a/tools/cabana/parser.cc b/tools/cabana/parser.cc deleted file mode 100644 index f4bacbb86d..0000000000 --- a/tools/cabana/parser.cc +++ /dev/null @@ -1,181 +0,0 @@ -#include "tools/cabana/parser.h" - -#include - -#include "cereal/messaging/messaging.h" - -Parser *parser = nullptr; - -static bool event_filter(const Event *e, void *opaque) { - Parser *p = (Parser*)opaque; - return p->eventFilter(e); -} - -Parser::Parser(QObject *parent) : QObject(parent) { - parser = this; - - qRegisterMetaType>(); - QObject::connect(this, &Parser::received, this, &Parser::process, Qt::QueuedConnection); -} - -Parser::~Parser() { - replay->stop(); -} - -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->installEventFilter(event_filter, this); - QObject::connect(replay, &Replay::segmentsMerged, this, &Parser::segmentsMerged); - if (replay->load()) { - 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 msgs) { - static double prev_update_ts = 0; - - for (const auto &can_data : msgs) { - can_msgs[can_data.id] = can_data; - ++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); - } - } - double now = millis_since_boot(); - if ((now - prev_update_ts) > 1000.0 / FPS) { - prev_update_ts = now; - emit updated(); - } - - if (current_sec < begin_sec || current_sec > end_sec) { - // loop replay in selected range. - seekTo(begin_sec); - } -} - -bool Parser::eventFilter(const Event *event) { - // drop packets when the GUI thread is calling seekTo. to make sure the current_sec is accurate. - if (!seeking && event->which == cereal::Event::Which::CAN) { - current_sec = (event->mono_time - replay->routeStartTime()) / (double)1e9; - - auto can = event->event.getCan(); - msgs_buf.clear(); - msgs_buf.reserve(can.size()); - - for (const auto &c : can) { - CanData &data = msgs_buf.emplace_back(); - data.address = c.getAddress(); - data.bus_time = c.getBusTime(); - data.source = c.getSrc(); - data.dat.append((char *)c.getDat().begin(), c.getDat().size()); - data.id = QString("%1:%2").arg(data.source).arg(data.address, 1, 16); - data.ts = current_sec; - } - emit received(msgs_buf); - } - return true; -} - -void Parser::seekTo(double ts) { - seeking = true; - replay->seekTo(ts, false); - seeking = false; -} - -void Parser::addNewMsg(const Msg &msg) { - dbc->msgs.push_back(msg); - msg_map[dbc->msgs.back().address] = &dbc->msgs.back(); -} - -void Parser::removeSignal(const QString &id, const QString &sig_name) { - Msg *msg = const_cast(getMsg(id)); - if (!msg) return; - - auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); - if (it != msg->sigs.end()) { - msg->sigs.erase(it); - 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 deleted file mode 100644 index 1632fcf6a6..0000000000 --- a/tools/cabana/parser.h +++ /dev/null @@ -1,95 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -#include "opendbc/can/common.h" -#include "opendbc/can/common_dbc.h" -#include "tools/replay/replay.h" - -const int FPS = 20; -const static int LOG_SIZE = 25; - -struct CanData { - QString id; - double ts; - uint32_t address; - uint16_t bus_time; - uint8_t source; - QByteArray dat; -}; - -class Parser : public QObject { - Q_OBJECT - -public: - Parser(QObject *parent); - ~Parser(); - static uint32_t addressFromId(const QString &id); - bool eventFilter(const Event *event); - bool loadRoute(const QString &route, const QString &data_dir, bool use_qcam); - void openDBC(const QString &name); - void saveDBC(const QString &name) {} - void addNewMsg(const Msg &msg); - void removeSignal(const QString &id, const QString &sig_name); - void seekTo(double ts); - 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: - 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: - Replay *replay = nullptr; - QHash counters; - std::map can_msgs; - QList history_log; - -protected: - void process(std::vector can); - void segmentsMerged(); - - std::atomic current_sec = 0.; - std::atomic seeking = false; - QString dbc_name; - 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; - std::vector msgs_buf; -}; - -Q_DECLARE_METATYPE(std::vector); - -// TODO: Add helper function in dbc.h -int bigEndianStartBitsIndex(int start_bit); -int bigEndianBitIndex(int index); -inline QString toHex(const QByteArray &dat) { - return dat.toHex(' ').toUpper(); -} - -extern Parser *parser; diff --git a/tools/cabana/signaledit.cc b/tools/cabana/signaledit.cc index c214adab09..3f48450195 100644 --- a/tools/cabana/signaledit.cc +++ b/tools/cabana/signaledit.cc @@ -3,13 +3,12 @@ #include #include #include -#include #include #include // SignalForm -SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) { +SignalForm::SignalForm(const Signal &sig, QWidget *parent) : start_bit(sig.start_bit), QWidget(parent) { QFormLayout *form_layout = new QFormLayout(this); name = new QLineEdit(sig.name.c_str()); @@ -33,7 +32,8 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) { sign->setCurrentIndex(sig.is_signed ? 0 : 1); form_layout->addRow(tr("sign"), sign); - factor = new QSpinBox(); + factor = new QDoubleSpinBox(); + factor->setDecimals(3); factor->setValue(sig.factor); form_layout->addRow(tr("Factor"), factor); @@ -46,9 +46,11 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) { form_layout->addRow(tr("Unit"), unit); comment = new QLineEdit(); form_layout->addRow(tr("Comment"), comment); - min_val = new QSpinBox(); + min_val = new QDoubleSpinBox(); + factor->setDecimals(3); form_layout->addRow(tr("Minimum value"), min_val); - max_val = new QSpinBox(); + max_val = new QDoubleSpinBox(); + factor->setDecimals(3); form_layout->addRow(tr("Maximum value"), max_val); val_desc = new QLineEdit(); form_layout->addRow(tr("Value descriptions"), val_desc); @@ -56,11 +58,11 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) { std::optional SignalForm::getSignal() { Signal sig = {}; + sig.start_bit = start_bit; 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) { @@ -75,30 +77,32 @@ std::optional SignalForm::getSignal() { // SignalEdit -SignalEdit::SignalEdit(const QString &id, const Signal &sig, const QString &color, QWidget *parent) : id(id), name_(sig.name.c_str()), QWidget(parent) { +SignalEdit::SignalEdit(int index, 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 = new QLabel(">"); + icon->setFixedSize(15, 30); icon->setStyleSheet("font-weight:bold"); title_layout->addWidget(icon); title = new ElidedLabel(this); - title->setText(sig.name.c_str()); + title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); + title->setText(QString("%1. %2").arg(index + 1).arg(sig.name.c_str())); title->setStyleSheet(QString("font-weight:bold; color:%1").arg(color)); title_layout->addWidget(title); - title_layout->addStretch(); plot_btn = new QPushButton("📈"); plot_btn->setToolTip(tr("Show Plot")); plot_btn->setFixedSize(30, 30); - QObject::connect(plot_btn, &QPushButton::clicked, [=]() { emit parser->showPlot(id, name_); }); + QObject::connect(plot_btn, &QPushButton::clicked, [=]() { emit showChart(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_container = new QWidget(this); + QVBoxLayout *v_layout = new QVBoxLayout(form_container); form = new SignalForm(sig, this); v_layout->addWidget(form); @@ -110,24 +114,27 @@ SignalEdit::SignalEdit(const QString &id, const Signal &sig, const QString &colo h->addWidget(save_btn); v_layout->addLayout(h); - edit_container->setVisible(false); - main_layout->addWidget(edit_container); + form_container->setVisible(false); + main_layout->addWidget(form_container); + + QFrame* hline = new QFrame(); + hline->setFrameShape(QFrame::HLine); + hline->setFrameShadow(QFrame::Sunken); + main_layout->addWidget(hline); QObject::connect(remove_btn, &QPushButton::clicked, this, &SignalEdit::remove); QObject::connect(save_btn, &QPushButton::clicked, this, &SignalEdit::save); - QObject::connect(title, &ElidedLabel::clicked, [=]() { - edit_container->isVisible() ? edit_container->hide() : edit_container->show(); - icon->setText(edit_container->isVisible() ? "▼" : ">"); - }); + QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked); +} + +void SignalEdit::setFormVisible(bool visible) { + form_container->setVisible(visible); + icon->setText(visible ? "▼" : ">"); } 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 - } - } + if (auto s = form->getSignal()) + dbc()->updateSignal(id, name_, *s); } void SignalEdit::remove() { @@ -137,7 +144,7 @@ void SignalEdit::remove() { msgbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); msgbox.setDefaultButton(QMessageBox::Cancel); if (msgbox.exec()) { - parser->removeSignal(id, name_); + dbc()->removeSignal(id, name_); deleteLater(); } } @@ -145,19 +152,19 @@ void SignalEdit::remove() { // AddSignalDialog AddSignalDialog::AddSignalDialog(const QString &id, QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("Add signal to %1").arg(parser->getMsg(id)->name.c_str())); + setWindowTitle(tr("Add signal to %1").arg(dbc()->msg(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); + setFixedWidth(parent->width() * 0.9); + 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); - } + if (auto signal = form->getSignal()) { + dbc()->addSignal(id, *signal); } QDialog::accept(); }); diff --git a/tools/cabana/signaledit.h b/tools/cabana/signaledit.h index b8140cc93b..00c13948b7 100644 --- a/tools/cabana/signaledit.h +++ b/tools/cabana/signaledit.h @@ -4,12 +4,15 @@ #include #include +#include #include #include #include #include "selfdrive/ui/qt/widgets/controls.h" -#include "tools/cabana/parser.h" + +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" class SignalForm : public QWidget { Q_OBJECT @@ -19,17 +22,25 @@ public: std::optional getSignal(); QLineEdit *name, *unit, *comment, *val_desc; - QSpinBox *size, *msb, *lsb, *factor, *offset, *min_val, *max_val; + QSpinBox *size, *msb, *lsb, *offset; + QDoubleSpinBox *factor, *min_val, *max_val; QComboBox *sign, *endianness; + int start_bit = 0; }; class SignalEdit : public QWidget { Q_OBJECT public: - SignalEdit(const QString &id, const Signal &sig, const QString &color, QWidget *parent = nullptr); + SignalEdit(int index, const QString &id, const Signal &sig, const QString &color, QWidget *parent = nullptr); + void setFormVisible(bool show); + inline bool isFormVisible() const { return form_container->isVisible(); } void save(); +signals: + void showChart(const QString &msg_id, const QString &sig_name); + void showFormClicked(); + protected: void remove(); @@ -38,8 +49,9 @@ protected: QPushButton *plot_btn; ElidedLabel *title; SignalForm *form; - QWidget *edit_container; + QWidget *form_container; QPushButton *remove_btn; + QLabel *icon; }; class AddSignalDialog : public QDialog { diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 957747584c..193a6f8788 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -6,11 +6,10 @@ #include #include #include +#include #include #include -#include "tools/cabana/parser.h" - inline QString formatTime(int seconds) { return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh::mm::ss" : "mm::ss"); } @@ -25,18 +24,17 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { // slider controls QHBoxLayout *slider_layout = new QHBoxLayout(); - time_label = new QLabel("00:00"); + QLabel *time_label = new QLabel("00:00"); slider_layout->addWidget(time_label); slider = new Slider(this); slider->setSingleStep(0); slider->setMinimum(0); - slider->setMaximum(parser->replay->totalSeconds() * 1000); + slider->setMaximum(can->totalSeconds() * 1000); slider_layout->addWidget(slider); - total_time_label = new QLabel(formatTime(parser->replay->totalSeconds())); - slider_layout->addWidget(total_time_label); - + end_time_label = new QLabel(formatTime(can->totalSeconds())); + slider_layout->addWidget(end_time_label); main_layout->addLayout(slider_layout); // btn controls @@ -50,51 +48,39 @@ 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, [=]() { can->setSpeed(speed); }); control_layout->addWidget(btn); group->addButton(btn); if (speed == 1.0) btn->setChecked(true); } - main_layout->addLayout(control_layout); - setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - QObject::connect(parser, &Parser::rangeChanged, this, &VideoWidget::rangeChanged); - QObject::connect(parser, &Parser::updated, this, &VideoWidget::updateState); - QObject::connect(slider, &QSlider::sliderMoved, [=]() { time_label->setText(formatTime(slider->value() / 1000)); }); - QObject::connect(slider, &QSlider::sliderReleased, [this]() { setPosition(slider->value()); }); - QObject::connect(slider, &Slider::setPosition, this, &VideoWidget::setPosition); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + QObject::connect(can, &CANMessages::rangeChanged, this, &VideoWidget::rangeChanged); + QObject::connect(can, &CANMessages::updated, this, &VideoWidget::updateState); + QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->value() / 1000.0); }); + QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(formatTime(value / 1000)); }); QObject::connect(play, &QPushButton::clicked, [=]() { - bool is_paused = parser->replay->isPaused(); + bool is_paused = can->isPaused(); play->setText(is_paused ? "⏸" : "▶"); - parser->replay->pause(!is_paused); + can->pause(!is_paused); }); } -void VideoWidget::setPosition(int value) { - time_label->setText(formatTime(value / 1000.0)); - parser->seekTo(value / 1000.0); -} - void VideoWidget::rangeChanged(double min, double max) { - if (!parser->isZoomed()) { + if (!can->isZoomed()) { min = 0; - max = parser->replay->totalSeconds(); + max = can->totalSeconds(); } - time_label->setText(formatTime(min)); - total_time_label->setText(formatTime(max)); + end_time_label->setText(formatTime(max)); slider->setMinimum(min * 1000); slider->setMaximum(max * 1000); - slider->setValue(parser->currentSec() * 1000); } void VideoWidget::updateState() { - if (!slider->isSliderDown()) { - double current_sec = parser->currentSec(); - time_label->setText(formatTime(current_sec)); - slider->setValue(current_sec * 1000); - } + if (!slider->isSliderDown()) + slider->setValue(can->currentSec() * 1000); } // Slider @@ -102,7 +88,7 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { QTimer *timer = new QTimer(this); timer->setInterval(2000); timer->callOnTimeout([this]() { - timeline = parser->replay->getTimeline(); + timeline = can->getTimeline(); update(); }); timer->start(); @@ -110,7 +96,7 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { void Slider::sliderChange(QAbstractSlider::SliderChange change) { if (change == QAbstractSlider::SliderValueChange) { - qreal x = width() * ((value() - minimum()) / double(maximum() - minimum())); + int x = width() * ((value() - minimum()) / double(maximum() - minimum())); if (x != slider_x) { slider_x = x; update(); @@ -121,45 +107,34 @@ void Slider::sliderChange(QAbstractSlider::SliderChange change) { } void Slider::paintEvent(QPaintEvent *ev) { - auto getPaintRange = [this](double begin, double end) -> std::pair { - double total_sec = maximum() - minimum(); - int start_pos = ((std::max((double)minimum(), (double)begin) - minimum()) / total_sec) * width(); - int end_pos = ((std::min((double)maximum(), (double)end) - minimum()) / total_sec) * width(); - return {start_pos, end_pos}; - }; + static const QColor colors[] = { + [(int)TimelineType::None] = QColor(111, 143, 175), + [(int)TimelineType::Engaged] = QColor(0, 163, 108), + [(int)TimelineType::UserFlag] = Qt::white, + [(int)TimelineType::AlertInfo] = Qt::green, + [(int)TimelineType::AlertWarning] = QColor(255, 195, 0), + [(int)TimelineType::AlertCritical] = QColor(199, 0, 57)}; QPainter p(this); - const int v_margin = 2; - p.fillRect(rect().adjusted(0, v_margin, 0, -v_margin), QColor(111, 143, 175)); - + QRect r = rect().adjusted(0, 4, 0, -4); + p.fillRect(r, colors[(int)TimelineType::None]); + double min = minimum() / 1000.0; + double max = maximum() / 1000.0; for (auto [begin, end, type] : timeline) { - begin *= 1000; - end *= 1000; - if (begin > maximum() || end < minimum()) continue; - - if (type == TimelineType::Engaged) { - auto [start_pos, end_pos] = getPaintRange(begin, end); - p.fillRect(QRect(start_pos, v_margin, end_pos - start_pos, height() - v_margin * 2), QColor(0, 163, 108)); - } + if (begin > max || end < min) + continue; + r.setLeft(((std::max(min, (double)begin) - min) / (max - min)) * width()); + r.setRight(((std::min(max, (double)end) - min) / (max - min)) * width()); + p.fillRect(r, colors[(int)type]); } - for (auto [begin, end, type] : timeline) { - begin *= 1000; - end *= 1000; - if (type == TimelineType::Engaged || begin > maximum() || end < minimum()) continue; - - auto [start_pos, end_pos] = getPaintRange(begin, end); - if (type == TimelineType::UserFlag) { - p.fillRect(QRect(start_pos, height() - v_margin - 3, end_pos - start_pos, 3), Qt::white); - } else { - QColor color(Qt::green); - if (type != TimelineType::AlertInfo) - color = type == TimelineType::AlertWarning ? QColor(255, 195, 0) : QColor(199, 0, 57); - - p.fillRect(QRect(start_pos, height() - v_margin - 3, end_pos - start_pos, 3), color); - } - } - p.setPen(QPen(QColor(88, 24, 69), 3)); - p.drawLine(QPoint{slider_x, 0}, QPoint{slider_x, height()}); + + QStyleOptionSlider opt; + opt.initFrom(this); + opt.minimum = minimum(); + opt.maximum = maximum(); + opt.subControls = QStyle::SC_SliderHandle; + opt.sliderPosition = value(); + style()->drawComplexControl(QStyle::CC_Slider, &opt, &p); } void Slider::mousePressEvent(QMouseEvent *e) { @@ -167,6 +142,6 @@ void Slider::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton && !isSliderDown()) { int value = minimum() + ((maximum() - minimum()) * e->x()) / width(); setValue(value); - emit setPosition(value); + emit sliderReleased(); } } diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 060565d322..e80e3b48f9 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -5,24 +5,19 @@ #include #include "selfdrive/ui/qt/widgets/cameraview.h" -#include "tools/replay/replay.h" +#include "tools/cabana/canmessages.h" class Slider : public QSlider { Q_OBJECT public: Slider(QWidget *parent); - void mousePressEvent(QMouseEvent* e) override; + void mousePressEvent(QMouseEvent *e) override; void sliderChange(QAbstractSlider::SliderChange change) override; - -signals: - void setPosition(int value); - -private: - void paintEvent(QPaintEvent *ev) override; - std::vector> timeline; + void paintEvent(QPaintEvent *ev) override; int slider_x = -1; + std::vector> timeline; }; class VideoWidget : public QWidget { @@ -34,9 +29,8 @@ public: protected: void rangeChanged(double min, double max); void updateState(); - void setPosition(int value); CameraViewWidget *cam_widget; - QLabel *time_label, *total_time_label; + QLabel *end_time_label; Slider *slider; }; diff --git a/tools/replay/main.cc b/tools/replay/main.cc index d3d6894877..40dace0c91 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -4,8 +4,6 @@ #include "tools/replay/consoleui.h" #include "tools/replay/replay.h" -const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; - int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 725bd1a27e..fbb36bd1ed 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -7,6 +7,8 @@ #include "tools/replay/camera.h" #include "tools/replay/route.h" +const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; + // one segment uses about 100M of memory constexpr int FORWARD_SEGS = 5; diff --git a/tools/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc index d6482c3ca2..5b61b6b6f2 100644 --- a/tools/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -9,7 +9,6 @@ #include "tools/replay/replay.h" #include "tools/replay/util.h" -const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; const std::string TEST_RLOG_CHECKSUM = "5b966d4bb21a100a8c4e59195faeb741b975ccbe268211765efd1763d892bfb3";