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