Cabana: stable initial release (#26004)

* increase form size & fix wrong charts number

* set max axisy to 1.0 if no value

* show 'close' button in floating window

* alwasy show scroll bar

* complete the logs

* more

* increase size to 50

* keep logs for all messages

* more

* rename signal

* better height

* avoid flicker

* dont call setupdatesenabled

* filter dbc files bye typing

* remove all charts if dbc file changed

* fix wrong idx

* bolder dbc filename

* update chart if signal has been edited

* new signals signalAdded,signalUpdated

* split class Parser into CanMessages and DBCManager

* cleanup

* updateState after set message

* cleanup

* emit msgUpdated

* clear history log if selected range changed

* always update time

* change title layout

* show selected range

hide title bar if no charts

less space between title and chart

* custome historylogmodel for extreme fast update

* move historylog to seperate file

* 2 decimal

* cleanup

cleanup

* left click on the chart to set start time

* todo

* show tooltip for header item&cleanup binaryview

add hline to signal form

* better paint

* cleanup signals/slots

* better range if min==max

* set historylog's minheight to 300

* 3x faster,sortable message list.

* zero copy in queued connection

* proxymodel

* clear log if loop to the begin

* simplify history log

* remove icon

* remove assets

* hide linemarker on initialization

* rubber width may less than 0

* dont zoom char if selected range is too small

* cleanup messageslist

* don't zoom chart if selected range less than 500ms

* typo

* check boundary

* check msg_id

* capital first letter

* move history log out of scrollarea

* Show only one form at a time

* auto scroll to header

d

* reduce msg size
entire row clickable

rename filter_msgs
old-commit-hash: 0fa1588f6c
taco
Dean Lee 3 years ago committed by GitHub
parent 92fab023b3
commit 62adf87f40
  1. 9
      tools/cabana/SConscript
  2. 6
      tools/cabana/cabana.cc
  3. 123
      tools/cabana/canmessages.cc
  4. 84
      tools/cabana/canmessages.h
  5. 174
      tools/cabana/chartswidget.cc
  6. 25
      tools/cabana/chartswidget.h
  7. 117
      tools/cabana/dbcmanager.cc
  8. 51
      tools/cabana/dbcmanager.h
  9. 224
      tools/cabana/detailwidget.cc
  10. 53
      tools/cabana/detailwidget.h
  11. 91
      tools/cabana/historylog.cc
  12. 37
      tools/cabana/historylog.h
  13. 7
      tools/cabana/mainwin.cc
  14. 1
      tools/cabana/mainwin.h
  15. 132
      tools/cabana/messageswidget.cc
  16. 38
      tools/cabana/messageswidget.h
  17. 181
      tools/cabana/parser.cc
  18. 95
      tools/cabana/parser.h
  19. 69
      tools/cabana/signaledit.cc
  20. 20
      tools/cabana/signaledit.h
  21. 115
      tools/cabana/videowidget.cc
  22. 16
      tools/cabana/videowidget.h
  23. 2
      tools/replay/main.cc
  24. 2
      tools/replay/replay.h
  25. 1
      tools/replay/tests/test_replay.cc

@ -1,5 +1,4 @@
import os Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib',
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc',
'cereal', 'transformations', 'widgets', 'opendbc') 'cereal', 'transformations', 'widgets', 'opendbc')
base_frameworks = qt_env['FRAMEWORKS'] base_frameworks = qt_env['FRAMEWORKS']
@ -13,8 +12,6 @@ else:
qt_libs = ['qt_util', 'Qt5Charts'] + base_libs qt_libs = ['qt_util', 'Qt5Charts'] + base_libs
if arch in ['x86_64', 'Darwin'] and GetOption('extras'): 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 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)

@ -1,14 +1,14 @@
#include <QApplication> #include <QApplication>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QStyleFactory>
#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/util.h"
#include "tools/cabana/mainwin.h" #include "tools/cabana/mainwin.h"
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
initApp(argc, argv); initApp(argc, argv);
QApplication app(argc, argv); QApplication app(argc, argv);
app.setStyle(QStyleFactory::create("Fusion"));
QCommandLineParser cmd_parser; QCommandLineParser cmd_parser;
cmd_parser.addHelpOption(); cmd_parser.addHelpOption();
@ -22,7 +22,7 @@ int main(int argc, char *argv[]) {
} }
const QString route = args.empty() ? DEMO_ROUTE : args.first(); const QString route = args.empty() ? DEMO_ROUTE : args.first();
Parser p(&app); CANMessages p(&app);
if (!p.loadRoute(route, cmd_parser.value("data_dir"), true)) { if (!p.loadRoute(route, cmd_parser.value("data_dir"), true)) {
return 0; return 0;
} }

@ -0,0 +1,123 @@
#include "tools/cabana/canmessages.h"
#include <QDebug>
Q_DECLARE_METATYPE(std::vector<CanData>);
CANMessages *can = nullptr;
CANMessages::CANMessages(QObject *parent) : QObject(parent) {
can = this;
qRegisterMetaType<std::vector<CanData>>();
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<QString, std::deque<CanData>> *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<QString, std::deque<CanData>>);
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);
}

@ -0,0 +1,84 @@
#pragma once
#include <atomic>
#include <deque>
#include <map>
#include <QHash>
#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<double, double> 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<CanData> &messages(const QString &id) { return can_msgs[id]; }
inline const CanData &lastMessage(const QString &id) { return can_msgs[id].front(); }
inline const std::vector<Event *> *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<std::tuple<int, int, TimelineType>> getTimeline() { return replay->getTimeline(); }
signals:
void eventsMerged();
void rangeChanged(double min, double max);
void updated();
void received(QHash<QString, std::deque<CanData>> *);
public:
QMap<QString, std::deque<CanData>> can_msgs;
std::unique_ptr<QHash<QString, std::deque<CanData>>> received_msgs = nullptr;
QHash<QString, uint32_t> counters;
protected:
void process(QHash<QString, std::deque<CanData>> *);
void segmentsMerged();
std::atomic<double> current_sec = 0.;
std::atomic<bool> 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;

@ -7,25 +7,6 @@
#include <QtCharts/QLineSeries> #include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis> #include <QtCharts/QValueAxis>
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::ChartsWidget(QWidget *parent) : QWidget(parent) { ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
@ -35,18 +16,22 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
// title bar // title bar
title_bar = new QWidget(this); title_bar = new QWidget(this);
QHBoxLayout *title_layout = new QHBoxLayout(title_bar); QHBoxLayout *title_layout = new QHBoxLayout(title_bar);
title_layout->setContentsMargins(0, 0, 0, 0);
title_label = new QLabel(tr("Charts")); title_label = new QLabel(tr("Charts"));
title_layout->addWidget(title_label); title_layout->addWidget(title_label);
title_layout->addStretch(); title_layout->addStretch();
range_label = new QLabel();
title_layout->addWidget(range_label);
reset_zoom_btn = new QPushButton("", this); reset_zoom_btn = new QPushButton("", this);
reset_zoom_btn->setVisible(false); reset_zoom_btn->setVisible(false);
reset_zoom_btn->setFixedSize(30, 30); reset_zoom_btn->setFixedSize(30, 30);
reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)")); reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)"));
title_layout->addWidget(reset_zoom_btn); 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->setVisible(false);
remove_all_btn->setToolTip(tr("Remove all charts")); remove_all_btn->setToolTip(tr("Remove all charts"));
remove_all_btn->setFixedSize(30, 30); remove_all_btn->setFixedSize(30, 30);
@ -54,7 +39,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
dock_btn = new QPushButton(); dock_btn = new QPushButton();
dock_btn->setFixedSize(30, 30); dock_btn->setFixedSize(30, 30);
updateDockButton();
title_layout->addWidget(dock_btn); title_layout->addWidget(dock_btn);
main_layout->addWidget(title_bar, 0, Qt::AlignTop); main_layout->addWidget(title_bar, 0, Qt::AlignTop);
@ -74,53 +58,80 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
main_layout->addWidget(charts_scroll); main_layout->addWidget(charts_scroll);
QObject::connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart); updateTitleBar();
QObject::connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart);
QObject::connect(parser, &Parser::signalRemoved, this, &ChartsWidget::removeChart); QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartsWidget::removeChart);
QObject::connect(reset_zoom_btn, &QPushButton::clicked, parser, &Parser::resetRange); 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(remove_all_btn, &QPushButton::clicked, this, &ChartsWidget::removeAll);
QObject::connect(dock_btn, &QPushButton::clicked, [=]() { QObject::connect(dock_btn, &QPushButton::clicked, [this]() {
emit dock(!docking); emit dock(!docking);
docking = !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->setText(docking ? "" : "");
dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); 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) { void ChartsWidget::addChart(const QString &id, const QString &sig_name) {
const QString char_name = id + sig_name; const QString char_name = id + ":" + sig_name;
if (charts.find(char_name) == charts.end()) { if (charts.find(char_name) == charts.end()) {
auto chart = new ChartWidget(id, sig_name, this); auto chart = new ChartWidget(id, sig_name, this);
QObject::connect(chart, &ChartWidget::remove, [=]() {
removeChart(id, sig_name);
});
charts_layout->insertWidget(0, chart); charts_layout->insertWidget(0, chart);
charts[char_name] = chart; charts[char_name] = chart;
} }
remove_all_btn->setVisible(true); updateTitleBar();
reset_zoom_btn->setVisible(true);
title_label->setText(tr("Charts (%1)").arg(charts.size()));
} }
void ChartsWidget::removeChart(const QString &id, const QString &sig_name) { 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(); it->second->deleteLater();
charts.erase(it); 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() { void ChartsWidget::removeAll() {
for (auto [_, chart] : charts) for (auto [_, chart] : charts)
chart->deleteLater(); chart->deleteLater();
charts.clear(); charts.clear();
remove_all_btn->setVisible(false); updateTitleBar();
reset_zoom_btn->setVisible(false); }
bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) {
if (obj != this && event->type() == QEvent::Close) {
emit dock_btn->clicked();
return true;
}
return false;
} }
// ChartWidget // ChartWidget
@ -138,16 +149,14 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa
header->setStyleSheet("background-color:white"); header->setStyleSheet("background-color:white");
QHBoxLayout *header_layout = new QHBoxLayout(header); QHBoxLayout *header_layout = new QHBoxLayout(header);
header_layout->setContentsMargins(11, 11, 11, 0); 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->addWidget(title);
header_layout->addStretch(); header_layout->addStretch();
QPushButton *remove_btn = new QPushButton("", this); QPushButton *remove_btn = new QPushButton("", this);
remove_btn->setFixedSize(30, 30); remove_btn->setFixedSize(30, 30);
remove_btn->setToolTip(tr("Remove chart")); remove_btn->setToolTip(tr("Remove chart"));
QObject::connect(remove_btn, &QPushButton::clicked, [=]() { QObject::connect(remove_btn, &QPushButton::clicked, this, &ChartWidget::remove);
emit parser->hidePlot(id, sig_name);
});
header_layout->addWidget(remove_btn); header_layout->addWidget(remove_btn);
chart_layout->addWidget(header); chart_layout->addWidget(header);
@ -163,9 +172,8 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa
chart->setTitleFont(font); chart->setTitleFont(font);
chart->setMargins({0, 0, 0, 0}); chart->setMargins({0, 0, 0, 0});
chart->layout()->setContentsMargins(0, 0, 0, 0); chart->layout()->setContentsMargins(0, 0, 0, 0);
QObject::connect(dynamic_cast<QValueAxis *>(chart->axisX()), &QValueAxis::rangeChanged, parser, &Parser::setRange);
chart_view = new QChartView(chart); chart_view = new ChartView(chart);
chart_view->setFixedHeight(300); chart_view->setFixedHeight(300);
chart_view->setRenderHint(QPainter::Antialiasing); chart_view->setRenderHint(QPainter::Antialiasing);
chart_view->setRubberBand(QChartView::HorizontalRubberBand); chart_view->setRubberBand(QChartView::HorizontalRubberBand);
@ -184,26 +192,28 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa
line_marker->raise(); line_marker->raise();
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
QObject::connect(parser, &Parser::updated, this, &ChartWidget::updateState); QObject::connect(can, &CANMessages::updated, this, &ChartWidget::updateState);
QObject::connect(parser, &Parser::rangeChanged, this, &ChartWidget::rangeChanged); QObject::connect(can, &CANMessages::rangeChanged, this, &ChartWidget::rangeChanged);
QObject::connect(parser, &Parser::eventsMerged, this, &ChartWidget::updateSeries); QObject::connect(can, &CANMessages::eventsMerged, this, &ChartWidget::updateSeries);
QObject::connect(dynamic_cast<QValueAxis *>(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(); updateSeries();
} }
void ChartWidget::updateState() { void ChartWidget::updateState() {
auto chart = chart_view->chart(); auto chart = chart_view->chart();
auto axis_x = dynamic_cast<QValueAxis *>(chart->axisX()); auto axis_x = dynamic_cast<QValueAxis *>(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) { int x = chart->plotArea().left() + chart->plotArea().width() * (can->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min());
line_marker->setX(x); line_marker->setX(x);
line_marker_x = x;
}
} }
void ChartWidget::updateSeries() { void ChartWidget::updateSeries() {
const Signal *sig = parser->getSig(id, sig_name); const Signal *sig = dbc()->signal(id, sig_name);
auto events = parser->replay->events(); auto events = can->events();
if (!sig || !events) return; if (!sig || !events) return;
auto l = id.split(':'); auto l = id.split(':');
@ -212,18 +222,14 @@ void ChartWidget::updateSeries() {
vals.clear(); vals.clear();
vals.reserve(3 * 60 * 100); vals.reserve(3 * 60 * 100);
uint64_t route_start_time = parser->replay->routeStartTime(); uint64_t route_start_time = can->routeStartTime();
for (auto &evt : *events) { for (auto &evt : *events) {
if (evt->which == cereal::Event::Which::CAN) { if (evt->which == cereal::Event::Which::CAN) {
for (auto c : evt->event.getCan()) { for (auto c : evt->event.getCan()) {
if (bus == c.getSrc() && address == c.getAddress()) { if (bus == c.getSrc() && address == c.getAddress()) {
auto dat = c.getDat(); auto dat = c.getDat();
int64_t val = get_raw_value((uint8_t *)dat.begin(), dat.size(), *sig); double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *sig);
if (sig->is_signed) { double ts = (evt->mono_time / (double)1e9) - route_start_time; // seconds
val -= ((val >> (sig->size - 1)) & 0x1) ? (1ULL << sig->size) : 0;
}
double value = val * sig->factor + sig->offset;
double ts = (evt->mono_time - route_start_time) / (double)1e9; // seconds
vals.push_back({ts, value}); vals.push_back({ts, value});
} }
} }
@ -231,7 +237,7 @@ void ChartWidget::updateSeries() {
} }
QLineSeries *series = (QLineSeries *)chart_view->chart()->series()[0]; QLineSeries *series = (QLineSeries *)chart_view->chart()->series()[0];
series->replace(vals); series->replace(vals);
auto [begin, end] = parser->range(); auto [begin, end] = can->range();
chart_view->chart()->axisX()->setRange(begin, end); chart_view->chart()->axisX()->setRange(begin, end);
updateAxisY(); updateAxisY();
} }
@ -247,6 +253,7 @@ void ChartWidget::rangeChanged(qreal min, qreal max) {
// auto zoom on yaxis // auto zoom on yaxis
void ChartWidget::updateAxisY() { void ChartWidget::updateAxisY() {
const auto axis_x = dynamic_cast<QValueAxis *>(chart_view->chart()->axisX()); const auto axis_x = dynamic_cast<QValueAxis *>(chart_view->chart()->axisX());
const auto axis_y = dynamic_cast<QValueAxis *>(chart_view->chart()->axisY());
// vals is a sorted list // 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; }); auto begin = std::lower_bound(vals.begin(), vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; });
if (begin == vals.end()) 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(); }); 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(); }); 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<QRubberBand *>();
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 // LineMarker
void LineMarker::setX(double x) { void LineMarker::setX(double x) {
x_pos = x; if (x != x_pos) {
update(); x_pos = x;
update();
}
} }
void LineMarker::paintEvent(QPaintEvent *event) { void LineMarker::paintEvent(QPaintEvent *event) {

@ -9,7 +9,8 @@
#include <QtCharts/QChartView> #include <QtCharts/QChartView>
#include <QtCharts/QLineSeries> #include <QtCharts/QLineSeries>
#include "tools/cabana/parser.h" #include "tools/cabana/canmessages.h"
#include "tools/cabana/dbcmanager.h"
using namespace QtCharts; using namespace QtCharts;
@ -22,7 +23,15 @@ public:
private: private:
void paintEvent(QPaintEvent *event) override; 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 { class ChartWidget : public QWidget {
@ -32,6 +41,9 @@ public:
ChartWidget(const QString &id, const QString &sig_name, QWidget *parent); ChartWidget(const QString &id, const QString &sig_name, QWidget *parent);
inline QChart *chart() const { return chart_view->chart(); } inline QChart *chart() const { return chart_view->chart(); }
signals:
void remove();
private: private:
void updateState(); void updateState();
void addData(const CanData &can_data, const Signal &sig); void addData(const CanData &can_data, const Signal &sig);
@ -41,9 +53,8 @@ private:
QString id; QString id;
QString sig_name; QString sig_name;
QChartView *chart_view = nullptr; ChartView *chart_view = nullptr;
LineMarker *line_marker = nullptr; LineMarker *line_marker = nullptr;
double line_marker_x = 0.0;
QList<QPointF> vals; QList<QPointF> vals;
}; };
@ -54,7 +65,6 @@ public:
ChartsWidget(QWidget *parent = nullptr); ChartsWidget(QWidget *parent = nullptr);
void addChart(const QString &id, const QString &sig_name); void addChart(const QString &id, const QString &sig_name);
void removeChart(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) { inline bool hasChart(const QString &id, const QString &sig_name) {
return charts.find(id + sig_name) != charts.end(); return charts.find(id + sig_name) != charts.end();
} }
@ -64,10 +74,13 @@ signals:
private: private:
void updateState(); void updateState();
void updateDockButton(); void updateTitleBar();
void removeAll();
bool eventFilter(QObject *obj, QEvent *event);
QWidget *title_bar; QWidget *title_bar;
QLabel *title_label; QLabel *title_label;
QLabel *range_label;
bool docking = true; bool docking = true;
QPushButton *dock_btn; QPushButton *dock_btn;
QPushButton *reset_zoom_btn; QPushButton *reset_zoom_btn;

@ -0,0 +1,117 @@
#include "tools/cabana/dbcmanager.h"
#include <QVector>
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 *>(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 *>(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 *>(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 *>(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 *>(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<int> BIG_ENDIAN_START_BITS = []() {
QVector<int> ret;
for (int i = 0; i < 64; i++)
for (int j = 7; j >= 0; j--)
ret.push_back(j + i * 8);
return ret;
}();
int bigEndianStartBitsIndex(int start_bit) {
return BIG_ENDIAN_START_BITS[start_bit];
}
int bigEndianBitIndex(int index) {
return BIG_ENDIAN_START_BITS.indexOf(index);
}
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;
}

@ -0,0 +1,51 @@
#pragma once
#include <QObject>
#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<std::string> 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<uint32_t, const Msg *> 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();

@ -1,39 +1,29 @@
#include "tools/cabana/detailwidget.h" #include "tools/cabana/detailwidget.h"
#include <QDebug>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QFormLayout> #include <QFormLayout>
#include <QHeaderView> #include <QHeaderView>
#include <QScrollBar>
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <bitset>
#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::DetailWidget(QWidget *parent) : QWidget(parent) { DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); 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 // title
QHBoxLayout *title_layout = new QHBoxLayout(); QHBoxLayout *title_layout = new QHBoxLayout();
title_layout->addWidget(new QLabel("time:"));
time_label = new QLabel(this); time_label = new QLabel(this);
title_layout->addWidget(time_label); 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(); title_layout->addStretch();
edit_btn = new QPushButton(tr("Edit"), this); edit_btn = new QPushButton(tr("Edit"), this);
edit_btn->setVisible(false); edit_btn->setVisible(false);
title_layout->addWidget(edit_btn); title_layout->addWidget(edit_btn);
@ -41,79 +31,104 @@ DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) {
// binary view // binary view
binary_view = new BinaryView(this); 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 // scroll area
QHBoxLayout *signals_layout = new QHBoxLayout(); scroll = new ScrollArea(this);
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);
QWidget *container = new QWidget(this); QWidget *container = new QWidget(this);
container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
QVBoxLayout *container_layout = new QVBoxLayout(container); QVBoxLayout *container_layout = new QVBoxLayout(container);
signal_edit_layout = new QVBoxLayout(); signal_edit_layout = new QVBoxLayout();
signal_edit_layout->setSpacing(2); signal_edit_layout->setSpacing(2);
container_layout->addLayout(signal_edit_layout); 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->setWidget(container);
scroll->setWidgetResizable(true); scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
main_layout->addWidget(scroll); 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(add_sig_btn, &QPushButton::clicked, this, &DetailWidget::addSignal);
QObject::connect(edit_btn, &QPushButton::clicked, this, &DetailWidget::editMsg); 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) { void DetailWidget::setMessage(const QString &message_id) {
can_data = c; msg_id = message_id;
clearLayout(signal_edit_layout); for (auto f : signal_forms) {
edit_btn->setVisible(true); f->deleteLater();
}
signal_forms.clear();
if (auto msg = parser->getMsg(can_data->address)) { if (auto msg = dbc()->msg(msg_id)) {
name_label->setText(msg->name.c_str());
add_sig_btn->setVisible(true);
for (int i = 0; i < msg->sigs.size(); ++i) { 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 { } else {
name_label->setText(tr("untitled")); name_label->setText(tr("untitled"));
add_sig_btn->setVisible(false); signals_header->setVisible(false);
} }
edit_btn->setVisible(true);
binary_view->setMsg(can_data); binary_view->setMessage(msg_id);
history_log->clear(); history_log->setMessage(msg_id);
} }
void DetailWidget::updateState() { 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->updateState();
binary_view->setData(can_data->dat);
history_log->updateState(); history_log->updateState();
} }
void DetailWidget::editMsg() { void DetailWidget::editMsg() {
EditMessageDialog dlg(can_data->id, this); EditMessageDialog dlg(msg_id, this);
if (dlg.exec()) { if (dlg.exec()) {
setMsg(can_data); setMessage(msg_id);
} }
} }
void DetailWidget::addSignal() { void DetailWidget::addSignal() {
AddSignalDialog dlg(can_data->id, this); AddSignalDialog dlg(msg_id, this);
if (dlg.exec()) { if (dlg.exec()) {
setMsg(can_data); setMessage(msg_id);
}
}
void DetailWidget::showForm() {
SignalEdit *sender = qobject_cast<SignalEdit *>(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) { BinaryView::BinaryView(QWidget *parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0);
table = new QTableWidget(this); table = new QTableWidget(this);
table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
table->horizontalHeader()->hide(); table->horizontalHeader()->hide();
@ -131,9 +147,10 @@ BinaryView::BinaryView(QWidget *parent) {
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
} }
void BinaryView::setMsg(const CanData *can_data) { void BinaryView::setMessage(const QString &message_id) {
const Msg *msg = parser->getMsg(can_data->address); msg_id = message_id;
int row_count = msg ? msg->size : can_data->dat.size(); const Msg *msg = dbc()->msg(msg_id);
int row_count = msg ? msg->size : can->lastMessage(msg_id).dat.size();
table->setRowCount(row_count); table->setRowCount(row_count);
table->setColumnCount(9); table->setColumnCount(9);
@ -151,8 +168,8 @@ void BinaryView::setMsg(const CanData *can_data) {
} }
} }
// set background color
if (msg) { if (msg) {
// set background color
for (int i = 0; i < msg->sigs.size(); ++i) { for (int i = 0; i < msg->sigs.size(); ++i) {
const auto &sig = msg->sigs[i]; const auto &sig = msg->sigs[i];
int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit); int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit);
@ -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) { void BinaryView::updateState() {
std::string s; if (msg_id.isEmpty()) return;
for (int j = 0; j < binary.size(); ++j) {
s += std::bitset<8>(binary[j]).to_string(); const auto &binary = can->lastMessage(msg_id).dat;
}
setUpdatesEnabled(false); setUpdatesEnabled(false);
char hex[3] = {'\0'}; char hex[3] = {'\0'};
for (int i = 0; i < binary.size(); ++i) { for (int i = 0; i < binary.size(); ++i) {
for (int j = 0; j < 8; ++j) { for (int j = 0; j < 8; ++j) {
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); table->item(i, 8)->setText(hex);
} }
setUpdatesEnabled(true); 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::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")); setWindowTitle(tr("Edit message"));
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
QFormLayout *form_layout = new QFormLayout(); QFormLayout *form_layout = new QFormLayout();
form_layout->addRow("ID", new QLabel(id)); form_layout->addRow("ID", new QLabel(msg_id));
auto msg = const_cast<Msg *>(parser->getMsg(id)); const auto msg = dbc()->msg(msg_id);
name_edit = new QLineEdit(this); name_edit = new QLineEdit(this);
name_edit->setText(msg ? msg->name.c_str() : "untitled"); name_edit->setText(msg ? msg->name.c_str() : "untitled");
form_layout->addRow(tr("Name"), name_edit); form_layout->addRow(tr("Name"), name_edit);
size_spin = new QSpinBox(this); 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); form_layout->addRow(tr("Size"), size_spin);
main_layout->addLayout(form_layout); 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); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
main_layout->addWidget(buttonBox); main_layout->addWidget(buttonBox);
setFixedWidth(parent->width() * 0.9);
connect(buttonBox, &QDialogButtonBox::accepted, this, &EditMessageDialog::save); connect(buttonBox, &QDialogButtonBox::accepted, this, &EditMessageDialog::save);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
} }
void EditMessageDialog::save() { 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<Msg *>(parser->getMsg(id))) { dbc()->updateMsg(msg_id, name, size_spin->value());
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(); 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);
}

@ -3,35 +3,28 @@
#include <QDialog> #include <QDialog>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QScrollArea>
#include <QTableWidget> #include <QTableWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include "opendbc/can/common.h" #include "opendbc/can/common.h"
#include "opendbc/can/common_dbc.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" #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 { class BinaryView : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
BinaryView(QWidget *parent); BinaryView(QWidget *parent);
void setMsg(const CanData *can_data); void setMessage(const QString &message_id);
void setData(const QByteArray &binary); void updateState();
private:
QString msg_id;
QTableWidget *table; QTableWidget *table;
}; };
@ -39,14 +32,23 @@ class EditMessageDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
EditMessageDialog(const QString &id, QWidget *parent); EditMessageDialog(const QString &msg_id, QWidget *parent);
protected: protected:
void save(); void save();
QString msg_id;
QLineEdit *name_edit; QLineEdit *name_edit;
QSpinBox *size_spin; 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 { class DetailWidget : public QWidget {
@ -54,17 +56,26 @@ class DetailWidget : public QWidget {
public: public:
DetailWidget(QWidget *parent); 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: private:
void updateState();
void addSignal(); void addSignal();
void editMsg(); void editMsg();
void updateState();
const CanData *can_data = nullptr; QString msg_id;
QLabel *name_label, *time_label; QLabel *name_label, *time_label;
QPushButton *edit_btn, *add_sig_btn; QPushButton *edit_btn;
QVBoxLayout *signal_edit_layout; QVBoxLayout *signal_edit_layout;
QWidget *signals_header;
QList<SignalEdit *> signal_forms;
HistoryLog *history_log; HistoryLog *history_log;
BinaryView *binary_view; BinaryView *binary_view;
ScrollArea *scroll;
}; };

@ -0,0 +1,91 @@
#include "tools/cabana/historylog.h"
#include <QHeaderView>
#include <QVBoxLayout>
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();
}

@ -0,0 +1,37 @@
#pragma once
#include <QTableView>
#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;
};

@ -1,5 +1,6 @@
#include "tools/cabana/mainwin.h" #include "tools/cabana/mainwin.h"
#include <QApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QScreen> #include <QScreen>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -30,13 +31,15 @@ MainWindow::MainWindow() : QWidget() {
h_layout->addWidget(right_container); 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); QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts);
} }
void MainWindow::dockCharts(bool dock) { void MainWindow::dockCharts(bool dock) {
charts_widget->setUpdatesEnabled(false); charts_widget->setUpdatesEnabled(false);
if (dock && floating_window) { if (dock && floating_window) {
floating_window->removeEventFilter(charts_widget);
r_layout->addWidget(charts_widget); r_layout->addWidget(charts_widget);
floating_window->deleteLater(); floating_window->deleteLater();
floating_window = nullptr; floating_window = nullptr;
@ -44,7 +47,7 @@ void MainWindow::dockCharts(bool dock) {
floating_window = new QWidget(nullptr); floating_window = new QWidget(nullptr);
floating_window->setLayout(new QVBoxLayout()); floating_window->setLayout(new QVBoxLayout());
floating_window->layout()->addWidget(charts_widget); 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->setMinimumSize(QGuiApplication::primaryScreen()->size() / 2);
floating_window->showMaximized(); floating_window->showMaximized();
} }

@ -3,7 +3,6 @@
#include "tools/cabana/chartswidget.h" #include "tools/cabana/chartswidget.h"
#include "tools/cabana/detailwidget.h" #include "tools/cabana/detailwidget.h"
#include "tools/cabana/messageswidget.h" #include "tools/cabana/messageswidget.h"
#include "tools/cabana/parser.h"
#include "tools/cabana/videowidget.h" #include "tools/cabana/videowidget.h"
class MainWindow : public QWidget { class MainWindow : public QWidget {

@ -1,19 +1,32 @@
#include "tools/cabana/messageswidget.h" #include "tools/cabana/messageswidget.h"
#include <QComboBox> #include <QComboBox>
#include <QCompleter>
#include <QHeaderView> #include <QHeaderView>
#include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QSortFilterProxyModel>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "tools/cabana/dbcmanager.h"
MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
// DBC file selector
QHBoxLayout *dbc_file_layout = new QHBoxLayout(); QHBoxLayout *dbc_file_layout = new QHBoxLayout();
QComboBox *combo = new QComboBox(this); QComboBox *combo = new QComboBox(this);
auto dbc_names = get_dbc_names(); auto dbc_names = dbc()->allDBCNames();
for (const auto &name : dbc_names) { for (const auto &name : dbc_names) {
combo->addItem(QString::fromStdString(name)); 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->addWidget(combo);
dbc_file_layout->addStretch(); dbc_file_layout->addStretch();
@ -21,73 +34,104 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
dbc_file_layout->addWidget(save_btn); dbc_file_layout->addWidget(save_btn);
main_layout->addLayout(dbc_file_layout); main_layout->addLayout(dbc_file_layout);
filter = new QLineEdit(this); // message filter
QLineEdit *filter = new QLineEdit(this);
filter->setPlaceholderText(tr("filter messages")); filter->setPlaceholderText(tr("filter messages"));
main_layout->addWidget(filter); 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->setSelectionBehavior(QAbstractItemView::SelectRows);
table_widget->setSelectionMode(QAbstractItemView::SingleSelection); table_widget->setSelectionMode(QAbstractItemView::SingleSelection);
table_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); table_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
table_widget->setColumnCount(4); table_widget->setSortingEnabled(true);
table_widget->setColumnWidth(0, 250); table_widget->setColumnWidth(0, 250);
table_widget->setColumnWidth(1, 80); table_widget->setColumnWidth(1, 80);
table_widget->setColumnWidth(2, 80); table_widget->setColumnWidth(2, 80);
table_widget->setHorizontalHeaderLabels({tr("Name"), tr("ID"), tr("Count"), tr("Bytes")});
table_widget->horizontalHeader()->setStretchLastSection(true); table_widget->horizontalHeader()->setStretchLastSection(true);
table_widget->verticalHeader()->hide();
table_widget->sortByColumn(0, Qt::AscendingOrder);
main_layout->addWidget(table_widget); 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, [=]() { QObject::connect(save_btn, &QPushButton::clicked, [=]() {
// TODO: save DBC to file // TODO: save DBC to file
}); });
QObject::connect(combo, &QComboBox::currentTextChanged, [=](const QString &dbc) { QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex &current, const QModelIndex &previous) {
parser->openDBC(dbc); if (current.isValid()) {
}); emit msgSelectionChanged(table_widget->model()->data(current, Qt::UserRole).toString());
QObject::connect(table_widget, &QTableWidget::itemSelectionChanged, [=]() { }
const CanData *c = &(parser->can_msgs[table_widget->selectedItems()[1]->text()]);
parser->setCurrentMsg(c->id);
emit msgChanged(c);
}); });
// For test purpose // For test purpose
combo->setCurrentText("toyota_nodsu_pt_generated"); combo->setCurrentText("toyota_nodsu_pt_generated");
} }
void MessagesWidget::updateState() { void MessagesWidget::dbcSelectionChanged(const QString &dbc_file) {
auto getTableItem = [=](int row, int col) -> QTableWidgetItem * { dbc()->open(dbc_file);
auto item = table_widget->item(row, col); // update detailwidget
if (!item) { auto current = table_widget->selectionModel()->currentIndex();
item = new QTableWidgetItem(); if (current.isValid()) {
item->setFlags(item->flags() ^ Qt::ItemIsEditable); emit msgSelectionChanged(table_widget->model()->data(current, Qt::UserRole).toString());
table_widget->setItem(row, col, item); }
} }
return item;
};
table_widget->setRowCount(parser->can_msgs.size()); // MessageListModel
int i = 0;
QString name, untitled = tr("untitled"); QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const {
const QString filter_str = filter->text(); if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
for (const auto &[_, c] : parser->can_msgs) { return (QString[]){"Name", "ID", "Count", "Bytes"}[section];
if (auto msg = parser->getMsg(c.address)) { else if (orientation == Qt::Vertical && role == Qt::DisplayRole) {
name = msg->name.c_str(); // return QString::number(section);
} else { }
name = untitled; return {};
} }
if (!filter_str.isEmpty() && !name.contains(filter_str, Qt::CaseInsensitive)) {
table_widget->hideRow(i++); QVariant MessageListModel::data(const QModelIndex &index, int role) const {
continue; 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); void MessageListModel::updateState() {
getTableItem(i, 1)->setText(c.id); int prev_row_count = row_count;
getTableItem(i, 2)->setText(QString::number(parser->counters[c.id])); row_count = can->can_msgs.size();
getTableItem(i, 3)->setText(toHex(c.dat)); int delta = row_count - prev_row_count;
table_widget->showRow(i); if (delta > 0) {
i++; 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));
} }
} }

@ -1,24 +1,38 @@
#pragma once #pragma once
#include <QLineEdit> #include <QAbstractTableModel>
#include <QTableWidget> #include <QTableView>
#include <QWidget>
#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 { class MessagesWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
MessagesWidget(QWidget *parent); MessagesWidget(QWidget *parent);
public slots: public slots:
void updateState(); void dbcSelectionChanged(const QString &dbc_file);
signals: signals:
void msgChanged(const CanData *id); void msgSelectionChanged(const QString &message_id);
protected: protected:
QLineEdit *filter; QTableView *table_widget;
QTableWidget *table_widget; MessageListModel *model;
}; };

@ -1,181 +0,0 @@
#include "tools/cabana/parser.h"
#include <QDebug>
#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<std::vector<CanData>>();
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 *>(dbc_lookup(name.toStdString()));
counters.clear();
msg_map.clear();
for (auto &msg : dbc->msgs) {
msg_map[msg.address] = &msg;
}
}
void Parser::process(std::vector<CanData> 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<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);
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<int> BIG_ENDIAN_START_BITS = []() {
QVector<int> ret;
for (int i = 0; i < 64; i++) {
for (int j = 7; j >= 0; j--) {
ret.push_back(j + i * 8);
}
}
return ret;
}();
int bigEndianStartBitsIndex(int start_bit) {
return BIG_ENDIAN_START_BITS[start_bit];
}
int bigEndianBitIndex(int index) {
return BIG_ENDIAN_START_BITS.indexOf(index);
}

@ -1,95 +0,0 @@
#pragma once
#include <atomic>
#include <map>
#include <QApplication>
#include <QHash>
#include <QObject>
#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<double, double> range() const { return {begin_sec, end_sec}; }
inline double currentSec() const { return current_sec; }
inline bool isZoomed() const { return is_zoomed; }
inline const Msg *getMsg(const QString &id) { return getMsg(addressFromId(id)); }
inline const Msg *getMsg(uint32_t address) {
auto it = msg_map.find(address);
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<CanData> can);
void updated();
public:
Replay *replay = nullptr;
QHash<QString, uint64_t> counters;
std::map<QString, CanData> can_msgs;
QList<CanData> history_log;
protected:
void process(std::vector<CanData> can);
void segmentsMerged();
std::atomic<double> current_sec = 0.;
std::atomic<bool> 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<uint32_t, const Msg *> msg_map;
QString current_msg_id;
std::vector<CanData> msgs_buf;
};
Q_DECLARE_METATYPE(std::vector<CanData>);
// 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;

@ -3,13 +3,12 @@
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QFormLayout> #include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <QVBoxLayout> #include <QVBoxLayout>
// SignalForm // 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); QFormLayout *form_layout = new QFormLayout(this);
name = new QLineEdit(sig.name.c_str()); 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); sign->setCurrentIndex(sig.is_signed ? 0 : 1);
form_layout->addRow(tr("sign"), sign); form_layout->addRow(tr("sign"), sign);
factor = new QSpinBox(); factor = new QDoubleSpinBox();
factor->setDecimals(3);
factor->setValue(sig.factor); factor->setValue(sig.factor);
form_layout->addRow(tr("Factor"), 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); form_layout->addRow(tr("Unit"), unit);
comment = new QLineEdit(); comment = new QLineEdit();
form_layout->addRow(tr("Comment"), comment); 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); 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); form_layout->addRow(tr("Maximum value"), max_val);
val_desc = new QLineEdit(); val_desc = new QLineEdit();
form_layout->addRow(tr("Value descriptions"), val_desc); form_layout->addRow(tr("Value descriptions"), val_desc);
@ -56,11 +58,11 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) {
std::optional<Signal> SignalForm::getSignal() { std::optional<Signal> SignalForm::getSignal() {
Signal sig = {}; Signal sig = {};
sig.start_bit = start_bit;
sig.name = name->text().toStdString(); sig.name = name->text().toStdString();
sig.size = size->text().toInt(); sig.size = size->text().toInt();
sig.offset = offset->text().toDouble(); sig.offset = offset->text().toDouble();
sig.factor = factor->text().toDouble(); sig.factor = factor->text().toDouble();
sig.msb = msb->text().toInt();
sig.is_signed = sign->currentIndex() == 0; sig.is_signed = sign->currentIndex() == 0;
sig.is_little_endian = endianness->currentIndex() == 0; sig.is_little_endian = endianness->currentIndex() == 0;
if (sig.is_little_endian) { if (sig.is_little_endian) {
@ -75,30 +77,32 @@ std::optional<Signal> SignalForm::getSignal() {
// SignalEdit // 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); QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0); main_layout->setContentsMargins(0, 0, 0, 0);
// title // title
QHBoxLayout *title_layout = new QHBoxLayout(); QHBoxLayout *title_layout = new QHBoxLayout();
QLabel *icon = new QLabel(">"); icon = new QLabel(">");
icon->setFixedSize(15, 30);
icon->setStyleSheet("font-weight:bold"); icon->setStyleSheet("font-weight:bold");
title_layout->addWidget(icon); title_layout->addWidget(icon);
title = new ElidedLabel(this); 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->setStyleSheet(QString("font-weight:bold; color:%1").arg(color));
title_layout->addWidget(title); title_layout->addWidget(title);
title_layout->addStretch();
plot_btn = new QPushButton("📈"); plot_btn = new QPushButton("📈");
plot_btn->setToolTip(tr("Show Plot")); plot_btn->setToolTip(tr("Show Plot"));
plot_btn->setFixedSize(30, 30); 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); title_layout->addWidget(plot_btn);
main_layout->addLayout(title_layout); main_layout->addLayout(title_layout);
edit_container = new QWidget(this); form_container = new QWidget(this);
QVBoxLayout *v_layout = new QVBoxLayout(edit_container); QVBoxLayout *v_layout = new QVBoxLayout(form_container);
form = new SignalForm(sig, this); form = new SignalForm(sig, this);
v_layout->addWidget(form); v_layout->addWidget(form);
@ -110,24 +114,27 @@ SignalEdit::SignalEdit(const QString &id, const Signal &sig, const QString &colo
h->addWidget(save_btn); h->addWidget(save_btn);
v_layout->addLayout(h); v_layout->addLayout(h);
edit_container->setVisible(false); form_container->setVisible(false);
main_layout->addWidget(edit_container); 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(remove_btn, &QPushButton::clicked, this, &SignalEdit::remove);
QObject::connect(save_btn, &QPushButton::clicked, this, &SignalEdit::save); QObject::connect(save_btn, &QPushButton::clicked, this, &SignalEdit::save);
QObject::connect(title, &ElidedLabel::clicked, [=]() { QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked);
edit_container->isVisible() ? edit_container->hide() : edit_container->show(); }
icon->setText(edit_container->isVisible() ? "" : ">");
}); void SignalEdit::setFormVisible(bool visible) {
form_container->setVisible(visible);
icon->setText(visible ? "" : ">");
} }
void SignalEdit::save() { void SignalEdit::save() {
if (auto sig = const_cast<Signal *>(parser->getSig(id, name_))) { if (auto s = form->getSignal())
if (auto s = form->getSignal()) { dbc()->updateSignal(id, name_, *s);
*sig = *s;
// TODO: reset the chart for sig
}
}
} }
void SignalEdit::remove() { void SignalEdit::remove() {
@ -137,7 +144,7 @@ void SignalEdit::remove() {
msgbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); msgbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgbox.setDefaultButton(QMessageBox::Cancel); msgbox.setDefaultButton(QMessageBox::Cancel);
if (msgbox.exec()) { if (msgbox.exec()) {
parser->removeSignal(id, name_); dbc()->removeSignal(id, name_);
deleteLater(); deleteLater();
} }
} }
@ -145,19 +152,19 @@ void SignalEdit::remove() {
// AddSignalDialog // AddSignalDialog
AddSignalDialog::AddSignalDialog(const QString &id, QWidget *parent) : QDialog(parent) { 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); QVBoxLayout *main_layout = new QVBoxLayout(this);
Signal sig = {.name = "untitled"}; Signal sig = {.name = "untitled"};
auto form = new SignalForm(sig, this); auto form = new SignalForm(sig, this);
main_layout->addWidget(form); main_layout->addWidget(form);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
main_layout->addWidget(buttonBox); main_layout->addWidget(buttonBox);
setFixedWidth(parent->width() * 0.9);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttonBox, &QDialogButtonBox::accepted, [=]() { connect(buttonBox, &QDialogButtonBox::accepted, [=]() {
if (auto msg = const_cast<Msg *>(parser->getMsg(id))) { if (auto signal = form->getSignal()) {
if (auto signal = form->getSignal()) { dbc()->addSignal(id, *signal);
msg->sigs.push_back(*signal);
}
} }
QDialog::accept(); QDialog::accept();
}); });

@ -4,12 +4,15 @@
#include <QComboBox> #include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QSpinBox> #include <QSpinBox>
#include "selfdrive/ui/qt/widgets/controls.h" #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 { class SignalForm : public QWidget {
Q_OBJECT Q_OBJECT
@ -19,17 +22,25 @@ public:
std::optional<Signal> getSignal(); std::optional<Signal> getSignal();
QLineEdit *name, *unit, *comment, *val_desc; 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; QComboBox *sign, *endianness;
int start_bit = 0;
}; };
class SignalEdit : public QWidget { class SignalEdit : public QWidget {
Q_OBJECT Q_OBJECT
public: 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(); void save();
signals:
void showChart(const QString &msg_id, const QString &sig_name);
void showFormClicked();
protected: protected:
void remove(); void remove();
@ -38,8 +49,9 @@ protected:
QPushButton *plot_btn; QPushButton *plot_btn;
ElidedLabel *title; ElidedLabel *title;
SignalForm *form; SignalForm *form;
QWidget *edit_container; QWidget *form_container;
QPushButton *remove_btn; QPushButton *remove_btn;
QLabel *icon;
}; };
class AddSignalDialog : public QDialog { class AddSignalDialog : public QDialog {

@ -6,11 +6,10 @@
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QPushButton> #include <QPushButton>
#include <QStyleOptionSlider>
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "tools/cabana/parser.h"
inline QString formatTime(int seconds) { inline QString formatTime(int seconds) {
return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh::mm::ss" : "mm::ss"); 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 // slider controls
QHBoxLayout *slider_layout = new QHBoxLayout(); QHBoxLayout *slider_layout = new QHBoxLayout();
time_label = new QLabel("00:00"); QLabel *time_label = new QLabel("00:00");
slider_layout->addWidget(time_label); slider_layout->addWidget(time_label);
slider = new Slider(this); slider = new Slider(this);
slider->setSingleStep(0); slider->setSingleStep(0);
slider->setMinimum(0); slider->setMinimum(0);
slider->setMaximum(parser->replay->totalSeconds() * 1000); slider->setMaximum(can->totalSeconds() * 1000);
slider_layout->addWidget(slider); slider_layout->addWidget(slider);
total_time_label = new QLabel(formatTime(parser->replay->totalSeconds())); end_time_label = new QLabel(formatTime(can->totalSeconds()));
slider_layout->addWidget(total_time_label); slider_layout->addWidget(end_time_label);
main_layout->addLayout(slider_layout); main_layout->addLayout(slider_layout);
// btn controls // btn controls
@ -50,51 +48,39 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
for (float speed : {0.1, 0.5, 1., 2.}) { for (float speed : {0.1, 0.5, 1., 2.}) {
QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this); QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this);
btn->setCheckable(true); btn->setCheckable(true);
QObject::connect(btn, &QPushButton::clicked, [=]() { parser->replay->setSpeed(speed); }); QObject::connect(btn, &QPushButton::clicked, [=]() { can->setSpeed(speed); });
control_layout->addWidget(btn); control_layout->addWidget(btn);
group->addButton(btn); group->addButton(btn);
if (speed == 1.0) btn->setChecked(true); if (speed == 1.0) btn->setChecked(true);
} }
main_layout->addLayout(control_layout); main_layout->addLayout(control_layout);
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
QObject::connect(parser, &Parser::rangeChanged, this, &VideoWidget::rangeChanged); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
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);
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, [=]() { QObject::connect(play, &QPushButton::clicked, [=]() {
bool is_paused = parser->replay->isPaused(); bool is_paused = can->isPaused();
play->setText(is_paused ? "" : ""); 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) { void VideoWidget::rangeChanged(double min, double max) {
if (!parser->isZoomed()) { if (!can->isZoomed()) {
min = 0; min = 0;
max = parser->replay->totalSeconds(); max = can->totalSeconds();
} }
time_label->setText(formatTime(min)); end_time_label->setText(formatTime(max));
total_time_label->setText(formatTime(max));
slider->setMinimum(min * 1000); slider->setMinimum(min * 1000);
slider->setMaximum(max * 1000); slider->setMaximum(max * 1000);
slider->setValue(parser->currentSec() * 1000);
} }
void VideoWidget::updateState() { void VideoWidget::updateState() {
if (!slider->isSliderDown()) { if (!slider->isSliderDown())
double current_sec = parser->currentSec(); slider->setValue(can->currentSec() * 1000);
time_label->setText(formatTime(current_sec));
slider->setValue(current_sec * 1000);
}
} }
// Slider // Slider
@ -102,7 +88,7 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) {
QTimer *timer = new QTimer(this); QTimer *timer = new QTimer(this);
timer->setInterval(2000); timer->setInterval(2000);
timer->callOnTimeout([this]() { timer->callOnTimeout([this]() {
timeline = parser->replay->getTimeline(); timeline = can->getTimeline();
update(); update();
}); });
timer->start(); timer->start();
@ -110,7 +96,7 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) {
void Slider::sliderChange(QAbstractSlider::SliderChange change) { void Slider::sliderChange(QAbstractSlider::SliderChange change) {
if (change == QAbstractSlider::SliderValueChange) { if (change == QAbstractSlider::SliderValueChange) {
qreal x = width() * ((value() - minimum()) / double(maximum() - minimum())); int x = width() * ((value() - minimum()) / double(maximum() - minimum()));
if (x != slider_x) { if (x != slider_x) {
slider_x = x; slider_x = x;
update(); update();
@ -121,45 +107,34 @@ void Slider::sliderChange(QAbstractSlider::SliderChange change) {
} }
void Slider::paintEvent(QPaintEvent *ev) { void Slider::paintEvent(QPaintEvent *ev) {
auto getPaintRange = [this](double begin, double end) -> std::pair<double, double> { static const QColor colors[] = {
double total_sec = maximum() - minimum(); [(int)TimelineType::None] = QColor(111, 143, 175),
int start_pos = ((std::max((double)minimum(), (double)begin) - minimum()) / total_sec) * width(); [(int)TimelineType::Engaged] = QColor(0, 163, 108),
int end_pos = ((std::min((double)maximum(), (double)end) - minimum()) / total_sec) * width(); [(int)TimelineType::UserFlag] = Qt::white,
return {start_pos, end_pos}; [(int)TimelineType::AlertInfo] = Qt::green,
}; [(int)TimelineType::AlertWarning] = QColor(255, 195, 0),
[(int)TimelineType::AlertCritical] = QColor(199, 0, 57)};
QPainter p(this); QPainter p(this);
const int v_margin = 2; QRect r = rect().adjusted(0, 4, 0, -4);
p.fillRect(rect().adjusted(0, v_margin, 0, -v_margin), QColor(111, 143, 175)); p.fillRect(r, colors[(int)TimelineType::None]);
double min = minimum() / 1000.0;
double max = maximum() / 1000.0;
for (auto [begin, end, type] : timeline) { for (auto [begin, end, type] : timeline) {
begin *= 1000; if (begin > max || end < min)
end *= 1000; continue;
if (begin > maximum() || end < minimum()) continue; r.setLeft(((std::max(min, (double)begin) - min) / (max - min)) * width());
r.setRight(((std::min(max, (double)end) - min) / (max - min)) * width());
if (type == TimelineType::Engaged) { p.fillRect(r, colors[(int)type]);
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));
}
} }
for (auto [begin, end, type] : timeline) {
begin *= 1000; QStyleOptionSlider opt;
end *= 1000; opt.initFrom(this);
if (type == TimelineType::Engaged || begin > maximum() || end < minimum()) continue; opt.minimum = minimum();
opt.maximum = maximum();
auto [start_pos, end_pos] = getPaintRange(begin, end); opt.subControls = QStyle::SC_SliderHandle;
if (type == TimelineType::UserFlag) { opt.sliderPosition = value();
p.fillRect(QRect(start_pos, height() - v_margin - 3, end_pos - start_pos, 3), Qt::white); style()->drawComplexControl(QStyle::CC_Slider, &opt, &p);
} 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()});
} }
void Slider::mousePressEvent(QMouseEvent *e) { void Slider::mousePressEvent(QMouseEvent *e) {
@ -167,6 +142,6 @@ void Slider::mousePressEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton && !isSliderDown()) { if (e->button() == Qt::LeftButton && !isSliderDown()) {
int value = minimum() + ((maximum() - minimum()) * e->x()) / width(); int value = minimum() + ((maximum() - minimum()) * e->x()) / width();
setValue(value); setValue(value);
emit setPosition(value); emit sliderReleased();
} }
} }

@ -5,24 +5,19 @@
#include <QWidget> #include <QWidget>
#include "selfdrive/ui/qt/widgets/cameraview.h" #include "selfdrive/ui/qt/widgets/cameraview.h"
#include "tools/replay/replay.h" #include "tools/cabana/canmessages.h"
class Slider : public QSlider { class Slider : public QSlider {
Q_OBJECT Q_OBJECT
public: public:
Slider(QWidget *parent); Slider(QWidget *parent);
void mousePressEvent(QMouseEvent* e) override; void mousePressEvent(QMouseEvent *e) override;
void sliderChange(QAbstractSlider::SliderChange change) override; void sliderChange(QAbstractSlider::SliderChange change) override;
void paintEvent(QPaintEvent *ev) override;
signals:
void setPosition(int value);
private:
void paintEvent(QPaintEvent *ev) override;
std::vector<std::tuple<int, int, TimelineType>> timeline;
int slider_x = -1; int slider_x = -1;
std::vector<std::tuple<int, int, TimelineType>> timeline;
}; };
class VideoWidget : public QWidget { class VideoWidget : public QWidget {
@ -34,9 +29,8 @@ public:
protected: protected:
void rangeChanged(double min, double max); void rangeChanged(double min, double max);
void updateState(); void updateState();
void setPosition(int value);
CameraViewWidget *cam_widget; CameraViewWidget *cam_widget;
QLabel *time_label, *total_time_label; QLabel *end_time_label;
Slider *slider; Slider *slider;
}; };

@ -4,8 +4,6 @@
#include "tools/replay/consoleui.h" #include "tools/replay/consoleui.h"
#include "tools/replay/replay.h" #include "tools/replay/replay.h"
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv); QCoreApplication app(argc, argv);

@ -7,6 +7,8 @@
#include "tools/replay/camera.h" #include "tools/replay/camera.h"
#include "tools/replay/route.h" #include "tools/replay/route.h"
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
// one segment uses about 100M of memory // one segment uses about 100M of memory
constexpr int FORWARD_SEGS = 5; constexpr int FORWARD_SEGS = 5;

@ -9,7 +9,6 @@
#include "tools/replay/replay.h" #include "tools/replay/replay.h"
#include "tools/replay/util.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_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"; const std::string TEST_RLOG_CHECKSUM = "5b966d4bb21a100a8c4e59195faeb741b975ccbe268211765efd1763d892bfb3";

Loading…
Cancel
Save