mirror of
https://github.com/MoreTore/openpilot.git
synced 2026-08-03 08:41:47 +08:00
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
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc',
|
||||
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib',
|
||||
'cereal', 'transformations', 'widgets', 'opendbc')
|
||||
|
||||
base_frameworks = qt_env['FRAMEWORKS']
|
||||
@@ -13,8 +12,6 @@ else:
|
||||
|
||||
qt_libs = ['qt_util', 'Qt5Charts'] + base_libs
|
||||
if arch in ['x86_64', 'Darwin'] and GetOption('extras'):
|
||||
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"]
|
||||
|
||||
Import('replay_lib')
|
||||
cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, opendbc,'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_libs
|
||||
qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'videowidget.cc', 'signaledit.cc', 'parser.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
|
||||
qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc',
|
||||
'canmessages.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QStyleFactory>
|
||||
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "tools/cabana/mainwin.h"
|
||||
|
||||
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
initApp(argc, argv);
|
||||
QApplication app(argc, argv);
|
||||
app.setStyle(QStyleFactory::create("Fusion"));
|
||||
|
||||
QCommandLineParser cmd_parser;
|
||||
cmd_parser.addHelpOption();
|
||||
@@ -22,7 +22,7 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
const QString route = args.empty() ? DEMO_ROUTE : args.first();
|
||||
Parser p(&app);
|
||||
CANMessages p(&app);
|
||||
if (!p.loadRoute(route, cmd_parser.value("data_dir"), true)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
+106
-68
@@ -7,25 +7,6 @@
|
||||
#include <QtCharts/QLineSeries>
|
||||
#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(QWidget *parent) : QWidget(parent) {
|
||||
@@ -35,18 +16,22 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
|
||||
// title bar
|
||||
title_bar = new QWidget(this);
|
||||
QHBoxLayout *title_layout = new QHBoxLayout(title_bar);
|
||||
title_layout->setContentsMargins(0, 0, 0, 0);
|
||||
title_label = new QLabel(tr("Charts"));
|
||||
|
||||
title_layout->addWidget(title_label);
|
||||
title_layout->addStretch();
|
||||
|
||||
range_label = new QLabel();
|
||||
title_layout->addWidget(range_label);
|
||||
|
||||
reset_zoom_btn = new QPushButton("⟲", this);
|
||||
reset_zoom_btn->setVisible(false);
|
||||
reset_zoom_btn->setFixedSize(30, 30);
|
||||
reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)"));
|
||||
title_layout->addWidget(reset_zoom_btn);
|
||||
|
||||
remove_all_btn = new QPushButton(tr("✖"));
|
||||
remove_all_btn = new QPushButton("✖", this);
|
||||
remove_all_btn->setVisible(false);
|
||||
remove_all_btn->setToolTip(tr("Remove all charts"));
|
||||
remove_all_btn->setFixedSize(30, 30);
|
||||
@@ -54,7 +39,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
dock_btn = new QPushButton();
|
||||
dock_btn->setFixedSize(30, 30);
|
||||
updateDockButton();
|
||||
title_layout->addWidget(dock_btn);
|
||||
|
||||
main_layout->addWidget(title_bar, 0, Qt::AlignTop);
|
||||
@@ -74,53 +58,80 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
main_layout->addWidget(charts_scroll);
|
||||
|
||||
QObject::connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart);
|
||||
QObject::connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart);
|
||||
QObject::connect(parser, &Parser::signalRemoved, this, &ChartsWidget::removeChart);
|
||||
QObject::connect(reset_zoom_btn, &QPushButton::clicked, parser, &Parser::resetRange);
|
||||
updateTitleBar();
|
||||
|
||||
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartsWidget::removeChart);
|
||||
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll);
|
||||
QObject::connect(can, &CANMessages::rangeChanged, [this]() { updateTitleBar(); });
|
||||
QObject::connect(reset_zoom_btn, &QPushButton::clicked, can, &CANMessages::resetRange);
|
||||
QObject::connect(remove_all_btn, &QPushButton::clicked, this, &ChartsWidget::removeAll);
|
||||
QObject::connect(dock_btn, &QPushButton::clicked, [=]() {
|
||||
QObject::connect(dock_btn, &QPushButton::clicked, [this]() {
|
||||
emit dock(!docking);
|
||||
docking = !docking;
|
||||
updateDockButton();
|
||||
updateTitleBar();
|
||||
});
|
||||
}
|
||||
|
||||
void ChartsWidget::updateDockButton() {
|
||||
void ChartsWidget::updateTitleBar() {
|
||||
if (!charts.size()) {
|
||||
title_bar->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
title_label->setText(tr("Charts (%1)").arg(charts.size()));
|
||||
|
||||
// show select range
|
||||
if (can->isZoomed()) {
|
||||
auto [min, max] = can->range();
|
||||
range_label->setText(tr("%1 - %2").arg(min, 0, 'f', 2).arg(max, 0, 'f', 2));
|
||||
range_label->setVisible(true);
|
||||
reset_zoom_btn->setEnabled(true);
|
||||
} else {
|
||||
reset_zoom_btn->setEnabled(false);
|
||||
range_label->setVisible(false);
|
||||
}
|
||||
|
||||
dock_btn->setText(docking ? "⬈" : "⬋");
|
||||
dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts"));
|
||||
remove_all_btn->setVisible(!charts.empty());
|
||||
reset_zoom_btn->setVisible(!charts.empty());
|
||||
title_bar->setVisible(true);
|
||||
}
|
||||
|
||||
void ChartsWidget::addChart(const QString &id, const QString &sig_name) {
|
||||
const QString char_name = id + sig_name;
|
||||
const QString char_name = id + ":" + sig_name;
|
||||
if (charts.find(char_name) == charts.end()) {
|
||||
auto chart = new ChartWidget(id, sig_name, this);
|
||||
QObject::connect(chart, &ChartWidget::remove, [=]() {
|
||||
removeChart(id, sig_name);
|
||||
});
|
||||
charts_layout->insertWidget(0, chart);
|
||||
charts[char_name] = chart;
|
||||
}
|
||||
remove_all_btn->setVisible(true);
|
||||
reset_zoom_btn->setVisible(true);
|
||||
title_label->setText(tr("Charts (%1)").arg(charts.size()));
|
||||
updateTitleBar();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeChart(const QString &id, const QString &sig_name) {
|
||||
if (auto it = charts.find(id + sig_name); it != charts.end()) {
|
||||
if (auto it = charts.find(id + ":" + sig_name); it != charts.end()) {
|
||||
it->second->deleteLater();
|
||||
charts.erase(it);
|
||||
if (charts.empty()) {
|
||||
remove_all_btn->setVisible(false);
|
||||
reset_zoom_btn->setVisible(false);
|
||||
}
|
||||
}
|
||||
title_label->setText(tr("Charts (%1)").arg(charts.size()));
|
||||
updateTitleBar();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeAll() {
|
||||
for (auto [_, chart] : charts)
|
||||
chart->deleteLater();
|
||||
charts.clear();
|
||||
remove_all_btn->setVisible(false);
|
||||
reset_zoom_btn->setVisible(false);
|
||||
updateTitleBar();
|
||||
}
|
||||
|
||||
bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) {
|
||||
if (obj != this && event->type() == QEvent::Close) {
|
||||
emit dock_btn->clicked();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ChartWidget
|
||||
@@ -138,16 +149,14 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa
|
||||
header->setStyleSheet("background-color:white");
|
||||
QHBoxLayout *header_layout = new QHBoxLayout(header);
|
||||
header_layout->setContentsMargins(11, 11, 11, 0);
|
||||
QLabel *title = new QLabel(tr("%1 %2").arg(parser->getMsg(id)->name.c_str()).arg(id));
|
||||
QLabel *title = new QLabel(tr("%1 %2").arg(dbc()->msg(id)->name.c_str()).arg(id));
|
||||
header_layout->addWidget(title);
|
||||
header_layout->addStretch();
|
||||
|
||||
QPushButton *remove_btn = new QPushButton("✖", this);
|
||||
remove_btn->setFixedSize(30, 30);
|
||||
remove_btn->setToolTip(tr("Remove chart"));
|
||||
QObject::connect(remove_btn, &QPushButton::clicked, [=]() {
|
||||
emit parser->hidePlot(id, sig_name);
|
||||
});
|
||||
QObject::connect(remove_btn, &QPushButton::clicked, this, &ChartWidget::remove);
|
||||
header_layout->addWidget(remove_btn);
|
||||
chart_layout->addWidget(header);
|
||||
|
||||
@@ -163,9 +172,8 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa
|
||||
chart->setTitleFont(font);
|
||||
chart->setMargins({0, 0, 0, 0});
|
||||
chart->layout()->setContentsMargins(0, 0, 0, 0);
|
||||
QObject::connect(dynamic_cast<QValueAxis *>(chart->axisX()), &QValueAxis::rangeChanged, parser, &Parser::setRange);
|
||||
|
||||
chart_view = new QChartView(chart);
|
||||
chart_view = new ChartView(chart);
|
||||
chart_view->setFixedHeight(300);
|
||||
chart_view->setRenderHint(QPainter::Antialiasing);
|
||||
chart_view->setRubberBand(QChartView::HorizontalRubberBand);
|
||||
@@ -184,26 +192,28 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa
|
||||
line_marker->raise();
|
||||
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
QObject::connect(parser, &Parser::updated, this, &ChartWidget::updateState);
|
||||
QObject::connect(parser, &Parser::rangeChanged, this, &ChartWidget::rangeChanged);
|
||||
QObject::connect(parser, &Parser::eventsMerged, this, &ChartWidget::updateSeries);
|
||||
|
||||
QObject::connect(can, &CANMessages::updated, this, &ChartWidget::updateState);
|
||||
QObject::connect(can, &CANMessages::rangeChanged, this, &ChartWidget::rangeChanged);
|
||||
QObject::connect(can, &CANMessages::eventsMerged, this, &ChartWidget::updateSeries);
|
||||
QObject::connect(dynamic_cast<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();
|
||||
}
|
||||
|
||||
void ChartWidget::updateState() {
|
||||
auto chart = chart_view->chart();
|
||||
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) {
|
||||
line_marker->setX(x);
|
||||
line_marker_x = x;
|
||||
}
|
||||
|
||||
int x = chart->plotArea().left() + chart->plotArea().width() * (can->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min());
|
||||
line_marker->setX(x);
|
||||
}
|
||||
|
||||
void ChartWidget::updateSeries() {
|
||||
const Signal *sig = parser->getSig(id, sig_name);
|
||||
auto events = parser->replay->events();
|
||||
const Signal *sig = dbc()->signal(id, sig_name);
|
||||
auto events = can->events();
|
||||
if (!sig || !events) return;
|
||||
|
||||
auto l = id.split(':');
|
||||
@@ -212,18 +222,14 @@ void ChartWidget::updateSeries() {
|
||||
|
||||
vals.clear();
|
||||
vals.reserve(3 * 60 * 100);
|
||||
uint64_t route_start_time = parser->replay->routeStartTime();
|
||||
uint64_t route_start_time = can->routeStartTime();
|
||||
for (auto &evt : *events) {
|
||||
if (evt->which == cereal::Event::Which::CAN) {
|
||||
for (auto c : evt->event.getCan()) {
|
||||
if (bus == c.getSrc() && address == c.getAddress()) {
|
||||
auto dat = c.getDat();
|
||||
int64_t val = get_raw_value((uint8_t *)dat.begin(), dat.size(), *sig);
|
||||
if (sig->is_signed) {
|
||||
val -= ((val >> (sig->size - 1)) & 0x1) ? (1ULL << sig->size) : 0;
|
||||
}
|
||||
double value = val * sig->factor + sig->offset;
|
||||
double ts = (evt->mono_time - route_start_time) / (double)1e9; // seconds
|
||||
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *sig);
|
||||
double ts = (evt->mono_time / (double)1e9) - route_start_time; // seconds
|
||||
vals.push_back({ts, value});
|
||||
}
|
||||
}
|
||||
@@ -231,7 +237,7 @@ void ChartWidget::updateSeries() {
|
||||
}
|
||||
QLineSeries *series = (QLineSeries *)chart_view->chart()->series()[0];
|
||||
series->replace(vals);
|
||||
auto [begin, end] = parser->range();
|
||||
auto [begin, end] = can->range();
|
||||
chart_view->chart()->axisX()->setRange(begin, end);
|
||||
updateAxisY();
|
||||
}
|
||||
@@ -247,6 +253,7 @@ void ChartWidget::rangeChanged(qreal min, qreal max) {
|
||||
// auto zoom on yaxis
|
||||
void ChartWidget::updateAxisY() {
|
||||
const auto axis_x = dynamic_cast<QValueAxis *>(chart_view->chart()->axisX());
|
||||
const auto axis_y = dynamic_cast<QValueAxis *>(chart_view->chart()->axisY());
|
||||
// vals is a sorted list
|
||||
auto begin = std::lower_bound(vals.begin(), vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; });
|
||||
if (begin == vals.end())
|
||||
@@ -254,14 +261,45 @@ void ChartWidget::updateAxisY() {
|
||||
|
||||
auto end = std::upper_bound(vals.begin(), vals.end(), axis_x->max(), [](double x, auto &p) { return x < p.x(); });
|
||||
const auto [min, max] = std::minmax_element(begin, end, [](auto &p1, auto &p2) { return p1.y() < p2.y(); });
|
||||
chart_view->chart()->axisY()->setRange(min->y(), max->y());
|
||||
if (min->y() == max->y()) {
|
||||
if (max->y() < 0) {
|
||||
axis_y->setRange(max->y(), 0);
|
||||
} else {
|
||||
axis_y->setRange(0, max->y() == 0 ? 1 : max->y());
|
||||
}
|
||||
} else {
|
||||
axis_y->setRange(min->y(), max->y());
|
||||
}
|
||||
}
|
||||
|
||||
// ChartView
|
||||
|
||||
void ChartView::mouseReleaseEvent(QMouseEvent *event) {
|
||||
auto rubber = findChild<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
|
||||
|
||||
void LineMarker::setX(double x) {
|
||||
x_pos = x;
|
||||
update();
|
||||
if (x != x_pos) {
|
||||
x_pos = x;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void LineMarker::paintEvent(QPaintEvent *event) {
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
#include <QtCharts/QChartView>
|
||||
#include <QtCharts/QLineSeries>
|
||||
|
||||
#include "tools/cabana/parser.h"
|
||||
#include "tools/cabana/canmessages.h"
|
||||
#include "tools/cabana/dbcmanager.h"
|
||||
|
||||
using namespace QtCharts;
|
||||
|
||||
@@ -22,7 +23,15 @@ public:
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
double x_pos = 0.0;
|
||||
double x_pos = -1;
|
||||
};
|
||||
|
||||
class ChartView : public QChartView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ChartView(QChart *chart, QWidget *parent = nullptr) : QChartView(chart, parent) {}
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
};
|
||||
|
||||
class ChartWidget : public QWidget {
|
||||
@@ -32,6 +41,9 @@ public:
|
||||
ChartWidget(const QString &id, const QString &sig_name, QWidget *parent);
|
||||
inline QChart *chart() const { return chart_view->chart(); }
|
||||
|
||||
signals:
|
||||
void remove();
|
||||
|
||||
private:
|
||||
void updateState();
|
||||
void addData(const CanData &can_data, const Signal &sig);
|
||||
@@ -41,9 +53,8 @@ private:
|
||||
|
||||
QString id;
|
||||
QString sig_name;
|
||||
QChartView *chart_view = nullptr;
|
||||
ChartView *chart_view = nullptr;
|
||||
LineMarker *line_marker = nullptr;
|
||||
double line_marker_x = 0.0;
|
||||
QList<QPointF> vals;
|
||||
};
|
||||
|
||||
@@ -54,7 +65,6 @@ public:
|
||||
ChartsWidget(QWidget *parent = nullptr);
|
||||
void addChart(const QString &id, const QString &sig_name);
|
||||
void removeChart(const QString &id, const QString &sig_name);
|
||||
void removeAll();
|
||||
inline bool hasChart(const QString &id, const QString &sig_name) {
|
||||
return charts.find(id + sig_name) != charts.end();
|
||||
}
|
||||
@@ -64,10 +74,13 @@ signals:
|
||||
|
||||
private:
|
||||
void updateState();
|
||||
void updateDockButton();
|
||||
void updateTitleBar();
|
||||
void removeAll();
|
||||
bool eventFilter(QObject *obj, QEvent *event);
|
||||
|
||||
QWidget *title_bar;
|
||||
QLabel *title_label;
|
||||
QLabel *range_label;
|
||||
bool docking = true;
|
||||
QPushButton *dock_btn;
|
||||
QPushButton *reset_zoom_btn;
|
||||
|
||||
@@ -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();
|
||||
+108
-116
@@ -1,39 +1,29 @@
|
||||
|
||||
#include "tools/cabana/detailwidget.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#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(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
|
||||
name_label = new QLabel(this);
|
||||
name_label->setStyleSheet("font-weight:bold;");
|
||||
name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
name_label->setAlignment(Qt::AlignCenter);
|
||||
main_layout->addWidget(name_label);
|
||||
|
||||
// title
|
||||
QHBoxLayout *title_layout = new QHBoxLayout();
|
||||
title_layout->addWidget(new QLabel("time:"));
|
||||
time_label = new QLabel(this);
|
||||
title_layout->addWidget(time_label);
|
||||
time_label->setStyleSheet("font-weight:bold");
|
||||
title_layout->addStretch();
|
||||
name_label = new QLabel(this);
|
||||
name_label->setStyleSheet("font-weight:bold;");
|
||||
title_layout->addWidget(name_label);
|
||||
title_layout->addStretch();
|
||||
|
||||
edit_btn = new QPushButton(tr("Edit"), this);
|
||||
edit_btn->setVisible(false);
|
||||
title_layout->addWidget(edit_btn);
|
||||
@@ -41,79 +31,104 @@ DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
// binary view
|
||||
binary_view = new BinaryView(this);
|
||||
main_layout->addWidget(binary_view);
|
||||
main_layout->addWidget(binary_view, 0, Qt::AlignTop);
|
||||
|
||||
// signal header
|
||||
signals_header = new QWidget(this);
|
||||
QHBoxLayout *signals_header_layout = new QHBoxLayout(signals_header);
|
||||
signals_header_layout->addWidget(new QLabel(tr("Signals")));
|
||||
signals_header_layout->addStretch();
|
||||
QPushButton *add_sig_btn = new QPushButton(tr("Add signal"), this);
|
||||
signals_header_layout->addWidget(add_sig_btn);
|
||||
signals_header->setVisible(false);
|
||||
main_layout->addWidget(signals_header);
|
||||
|
||||
// scroll area
|
||||
QHBoxLayout *signals_layout = new QHBoxLayout();
|
||||
signals_layout->addWidget(new QLabel(tr("Signals")));
|
||||
signals_layout->addStretch();
|
||||
add_sig_btn = new QPushButton(tr("Add signal"), this);
|
||||
add_sig_btn->setVisible(false);
|
||||
signals_layout->addWidget(add_sig_btn);
|
||||
main_layout->addLayout(signals_layout);
|
||||
|
||||
scroll = new ScrollArea(this);
|
||||
QWidget *container = new QWidget(this);
|
||||
container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
|
||||
QVBoxLayout *container_layout = new QVBoxLayout(container);
|
||||
signal_edit_layout = new QVBoxLayout();
|
||||
signal_edit_layout->setSpacing(2);
|
||||
container_layout->addLayout(signal_edit_layout);
|
||||
|
||||
history_log = new HistoryLog(this);
|
||||
container_layout->addWidget(history_log);
|
||||
|
||||
QScrollArea *scroll = new QScrollArea(this);
|
||||
scroll->setWidget(container);
|
||||
scroll->setWidgetResizable(true);
|
||||
scroll->setFrameShape(QFrame::NoFrame);
|
||||
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
||||
|
||||
main_layout->addWidget(scroll);
|
||||
|
||||
history_log = new HistoryLog(this);
|
||||
main_layout->addWidget(history_log);
|
||||
|
||||
QObject::connect(add_sig_btn, &QPushButton::clicked, this, &DetailWidget::addSignal);
|
||||
QObject::connect(edit_btn, &QPushButton::clicked, this, &DetailWidget::editMsg);
|
||||
QObject::connect(parser, &Parser::updated, this, &DetailWidget::updateState);
|
||||
QObject::connect(can, &CANMessages::updated, this, &DetailWidget::updateState);
|
||||
}
|
||||
|
||||
void DetailWidget::setMsg(const CanData *c) {
|
||||
can_data = c;
|
||||
clearLayout(signal_edit_layout);
|
||||
edit_btn->setVisible(true);
|
||||
void DetailWidget::setMessage(const QString &message_id) {
|
||||
msg_id = message_id;
|
||||
for (auto f : signal_forms) {
|
||||
f->deleteLater();
|
||||
}
|
||||
signal_forms.clear();
|
||||
|
||||
if (auto msg = parser->getMsg(can_data->address)) {
|
||||
name_label->setText(msg->name.c_str());
|
||||
add_sig_btn->setVisible(true);
|
||||
if (auto msg = dbc()->msg(msg_id)) {
|
||||
for (int i = 0; i < msg->sigs.size(); ++i) {
|
||||
signal_edit_layout->addWidget(new SignalEdit(can_data->id, msg->sigs[i], getColor(i)));
|
||||
auto form = new SignalEdit(i, msg_id, msg->sigs[i], getColor(i));
|
||||
signal_edit_layout->addWidget(form);
|
||||
QObject::connect(form, &SignalEdit::showChart, this, &DetailWidget::showChart);
|
||||
QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showForm);
|
||||
signal_forms.push_back(form);
|
||||
}
|
||||
name_label->setText(msg->name.c_str());
|
||||
signals_header->setVisible(true);
|
||||
} else {
|
||||
name_label->setText(tr("untitled"));
|
||||
add_sig_btn->setVisible(false);
|
||||
signals_header->setVisible(false);
|
||||
}
|
||||
edit_btn->setVisible(true);
|
||||
|
||||
binary_view->setMsg(can_data);
|
||||
history_log->clear();
|
||||
binary_view->setMessage(msg_id);
|
||||
history_log->setMessage(msg_id);
|
||||
}
|
||||
|
||||
void DetailWidget::updateState() {
|
||||
if (!can_data) return;
|
||||
time_label->setText(QString::number(can->currentSec(), 'f', 3));
|
||||
if (msg_id.isEmpty()) return;
|
||||
|
||||
time_label->setText(QString("time: %1").arg(can_data->ts, 0, 'f', 3));
|
||||
binary_view->setData(can_data->dat);
|
||||
binary_view->updateState();
|
||||
history_log->updateState();
|
||||
}
|
||||
|
||||
void DetailWidget::editMsg() {
|
||||
EditMessageDialog dlg(can_data->id, this);
|
||||
EditMessageDialog dlg(msg_id, this);
|
||||
if (dlg.exec()) {
|
||||
setMsg(can_data);
|
||||
setMessage(msg_id);
|
||||
}
|
||||
}
|
||||
|
||||
void DetailWidget::addSignal() {
|
||||
AddSignalDialog dlg(can_data->id, this);
|
||||
AddSignalDialog dlg(msg_id, this);
|
||||
if (dlg.exec()) {
|
||||
setMsg(can_data);
|
||||
setMessage(msg_id);
|
||||
}
|
||||
}
|
||||
|
||||
void DetailWidget::showForm() {
|
||||
SignalEdit *sender = qobject_cast<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) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
||||
table = new QTableWidget(this);
|
||||
table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
table->horizontalHeader()->hide();
|
||||
@@ -131,9 +147,10 @@ BinaryView::BinaryView(QWidget *parent) {
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
||||
}
|
||||
|
||||
void BinaryView::setMsg(const CanData *can_data) {
|
||||
const Msg *msg = parser->getMsg(can_data->address);
|
||||
int row_count = msg ? msg->size : can_data->dat.size();
|
||||
void BinaryView::setMessage(const QString &message_id) {
|
||||
msg_id = message_id;
|
||||
const Msg *msg = dbc()->msg(msg_id);
|
||||
int row_count = msg ? msg->size : can->lastMessage(msg_id).dat.size();
|
||||
|
||||
table->setRowCount(row_count);
|
||||
table->setColumnCount(9);
|
||||
@@ -151,8 +168,8 @@ void BinaryView::setMsg(const CanData *can_data) {
|
||||
}
|
||||
}
|
||||
|
||||
// set background color
|
||||
if (msg) {
|
||||
// set background color
|
||||
for (int i = 0; i < msg->sigs.size(); ++i) {
|
||||
const auto &sig = msg->sigs[i];
|
||||
int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit);
|
||||
@@ -162,80 +179,44 @@ void BinaryView::setMsg(const CanData *can_data) {
|
||||
}
|
||||
}
|
||||
|
||||
setFixedHeight(table->rowHeight(0) * table->rowCount() + 25);
|
||||
table->setFixedHeight(table->rowHeight(0) * table->rowCount() + table->horizontalHeader()->height() + 2);
|
||||
updateState();
|
||||
}
|
||||
|
||||
void BinaryView::setData(const QByteArray &binary) {
|
||||
std::string s;
|
||||
for (int j = 0; j < binary.size(); ++j) {
|
||||
s += std::bitset<8>(binary[j]).to_string();
|
||||
}
|
||||
void BinaryView::updateState() {
|
||||
if (msg_id.isEmpty()) return;
|
||||
|
||||
const auto &binary = can->lastMessage(msg_id).dat;
|
||||
|
||||
setUpdatesEnabled(false);
|
||||
char hex[3] = {'\0'};
|
||||
for (int i = 0; i < binary.size(); ++i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
table->item(i, j)->setText(QChar(s[i * 8 + j]));
|
||||
table->item(i, j)->setText(QChar((binary[i] >> (7 - j)) & 1 ? '1' : '0'));
|
||||
}
|
||||
sprintf(&hex[0], "%02X", (unsigned char)binary[i]);
|
||||
hex[0] = toHex(binary[i] >> 4);
|
||||
hex[1] = toHex(binary[i] & 0xf);
|
||||
table->item(i, 8)->setText(hex);
|
||||
}
|
||||
setUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
// HistoryLog
|
||||
|
||||
HistoryLog::HistoryLog(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
QLabel *title = new QLabel("TIME BYTES");
|
||||
main_layout->addWidget(title);
|
||||
|
||||
QVBoxLayout *message_layout = new QVBoxLayout();
|
||||
for (int i = 0; i < std::size(labels); ++i) {
|
||||
labels[i] = new QLabel();
|
||||
labels[i]->setVisible(false);
|
||||
message_layout->addWidget(labels[i]);
|
||||
}
|
||||
main_layout->addLayout(message_layout);
|
||||
main_layout->addStretch();
|
||||
}
|
||||
|
||||
void HistoryLog::updateState() {
|
||||
int i = 0;
|
||||
for (; i < parser->history_log.size(); ++i) {
|
||||
const auto &c = parser->history_log[i];
|
||||
auto label = labels[i];
|
||||
label->setVisible(true);
|
||||
label->setText(QString("%1 %2").arg(c.ts, 0, 'f', 3).arg(toHex(c.dat)));
|
||||
}
|
||||
|
||||
for (; i < std::size(labels); ++i) {
|
||||
labels[i]->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryLog::clear() {
|
||||
setUpdatesEnabled(false);
|
||||
for (auto l : labels) l->setVisible(false);
|
||||
setUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
// EditMessageDialog
|
||||
|
||||
EditMessageDialog::EditMessageDialog(const QString &id, QWidget *parent) : id(id), QDialog(parent) {
|
||||
EditMessageDialog::EditMessageDialog(const QString &msg_id, QWidget *parent) : msg_id(msg_id), QDialog(parent) {
|
||||
setWindowTitle(tr("Edit message"));
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
|
||||
QFormLayout *form_layout = new QFormLayout();
|
||||
form_layout->addRow("ID", new QLabel(id));
|
||||
form_layout->addRow("ID", new QLabel(msg_id));
|
||||
|
||||
auto msg = const_cast<Msg *>(parser->getMsg(id));
|
||||
const auto msg = dbc()->msg(msg_id);
|
||||
name_edit = new QLineEdit(this);
|
||||
name_edit->setText(msg ? msg->name.c_str() : "untitled");
|
||||
form_layout->addRow(tr("Name"), name_edit);
|
||||
|
||||
size_spin = new QSpinBox(this);
|
||||
size_spin->setValue(msg ? msg->size : parser->can_msgs[id].dat.size());
|
||||
size_spin->setValue(msg ? msg->size : can->lastMessage(msg_id).dat.size());
|
||||
form_layout->addRow(tr("Size"), size_spin);
|
||||
|
||||
main_layout->addLayout(form_layout);
|
||||
@@ -243,22 +224,33 @@ EditMessageDialog::EditMessageDialog(const QString &id, QWidget *parent) : id(id
|
||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
main_layout->addWidget(buttonBox);
|
||||
|
||||
setFixedWidth(parent->width() * 0.9);
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &EditMessageDialog::save);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void EditMessageDialog::save() {
|
||||
if (size_spin->value() <= 0 || name_edit->text().isEmpty()) return;
|
||||
const QString name = name_edit->text();
|
||||
if (size_spin->value() <= 0 || name_edit->text().isEmpty() || name == tr("untitled"))
|
||||
return;
|
||||
|
||||
if (auto msg = const_cast<Msg *>(parser->getMsg(id))) {
|
||||
msg->name = name_edit->text().toStdString();
|
||||
msg->size = size_spin->value();
|
||||
} else {
|
||||
Msg m = {};
|
||||
m.address = Parser::addressFromId(id);
|
||||
m.name = name_edit->text().toStdString();
|
||||
m.size = size_spin->value();
|
||||
parser->addNewMsg(m);
|
||||
}
|
||||
dbc()->updateMsg(msg_id, name, size_spin->value());
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
// ScrollArea
|
||||
|
||||
bool ScrollArea::eventFilter(QObject *obj, QEvent *ev) {
|
||||
if (obj == widget() && ev->type() == QEvent::Resize) {
|
||||
int height = widget()->height() + 4;
|
||||
setMinimumHeight(height > 480 ? 480 : height);
|
||||
setMaximumHeight(height);
|
||||
}
|
||||
return QScrollArea::eventFilter(obj, ev);
|
||||
}
|
||||
|
||||
void ScrollArea::setWidget(QWidget *w) {
|
||||
QScrollArea::setWidget(w);
|
||||
w->installEventFilter(this);
|
||||
}
|
||||
|
||||
+32
-21
@@ -3,35 +3,28 @@
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "opendbc/can/common.h"
|
||||
#include "opendbc/can/common_dbc.h"
|
||||
#include "tools/cabana/parser.h"
|
||||
#include "tools/cabana/canmessages.h"
|
||||
#include "tools/cabana/dbcmanager.h"
|
||||
#include "tools/cabana/historylog.h"
|
||||
#include "tools/cabana/signaledit.h"
|
||||
|
||||
class HistoryLog : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HistoryLog(QWidget *parent);
|
||||
void clear();
|
||||
void updateState();
|
||||
|
||||
private:
|
||||
QLabel *labels[LOG_SIZE] = {};
|
||||
};
|
||||
|
||||
class BinaryView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BinaryView(QWidget *parent);
|
||||
void setMsg(const CanData *can_data);
|
||||
void setData(const QByteArray &binary);
|
||||
void setMessage(const QString &message_id);
|
||||
void updateState();
|
||||
|
||||
private:
|
||||
QString msg_id;
|
||||
QTableWidget *table;
|
||||
};
|
||||
|
||||
@@ -39,14 +32,23 @@ class EditMessageDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
EditMessageDialog(const QString &id, QWidget *parent);
|
||||
EditMessageDialog(const QString &msg_id, QWidget *parent);
|
||||
|
||||
protected:
|
||||
void save();
|
||||
|
||||
QString msg_id;
|
||||
QLineEdit *name_edit;
|
||||
QSpinBox *size_spin;
|
||||
QString id;
|
||||
};
|
||||
|
||||
class ScrollArea : public QScrollArea {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ScrollArea(QWidget *parent) : QScrollArea(parent) {}
|
||||
bool eventFilter(QObject *obj, QEvent *ev) override;
|
||||
void setWidget(QWidget *w);
|
||||
};
|
||||
|
||||
class DetailWidget : public QWidget {
|
||||
@@ -54,17 +56,26 @@ class DetailWidget : public QWidget {
|
||||
|
||||
public:
|
||||
DetailWidget(QWidget *parent);
|
||||
void setMsg(const CanData *c);
|
||||
void setMessage(const QString &message_id);
|
||||
|
||||
signals:
|
||||
void showChart(const QString &msg_id, const QString &sig_name);
|
||||
|
||||
private slots:
|
||||
void showForm();
|
||||
|
||||
private:
|
||||
void updateState();
|
||||
void addSignal();
|
||||
void editMsg();
|
||||
void updateState();
|
||||
|
||||
const CanData *can_data = nullptr;
|
||||
QString msg_id;
|
||||
QLabel *name_label, *time_label;
|
||||
QPushButton *edit_btn, *add_sig_btn;
|
||||
QPushButton *edit_btn;
|
||||
QVBoxLayout *signal_edit_layout;
|
||||
QWidget *signals_header;
|
||||
QList<SignalEdit *> signal_forms;
|
||||
HistoryLog *history_log;
|
||||
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 <QApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QScreen>
|
||||
#include <QVBoxLayout>
|
||||
@@ -30,13 +31,15 @@ MainWindow::MainWindow() : QWidget() {
|
||||
|
||||
h_layout->addWidget(right_container);
|
||||
|
||||
QObject::connect(messages_widget, &MessagesWidget::msgChanged, detail_widget, &DetailWidget::setMsg);
|
||||
QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, detail_widget, &DetailWidget::setMessage);
|
||||
QObject::connect(detail_widget, &DetailWidget::showChart, charts_widget, &ChartsWidget::addChart);
|
||||
QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts);
|
||||
}
|
||||
|
||||
void MainWindow::dockCharts(bool dock) {
|
||||
charts_widget->setUpdatesEnabled(false);
|
||||
if (dock && floating_window) {
|
||||
floating_window->removeEventFilter(charts_widget);
|
||||
r_layout->addWidget(charts_widget);
|
||||
floating_window->deleteLater();
|
||||
floating_window = nullptr;
|
||||
@@ -44,7 +47,7 @@ void MainWindow::dockCharts(bool dock) {
|
||||
floating_window = new QWidget(nullptr);
|
||||
floating_window->setLayout(new QVBoxLayout());
|
||||
floating_window->layout()->addWidget(charts_widget);
|
||||
floating_window->setWindowFlags(Qt::WindowTitleHint | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint);
|
||||
floating_window->installEventFilter(charts_widget);
|
||||
floating_window->setMinimumSize(QGuiApplication::primaryScreen()->size() / 2);
|
||||
floating_window->showMaximized();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "tools/cabana/chartswidget.h"
|
||||
#include "tools/cabana/detailwidget.h"
|
||||
#include "tools/cabana/messageswidget.h"
|
||||
#include "tools/cabana/parser.h"
|
||||
#include "tools/cabana/videowidget.h"
|
||||
|
||||
class MainWindow : public QWidget {
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
#include "tools/cabana/messageswidget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QHeaderView>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "tools/cabana/dbcmanager.h"
|
||||
|
||||
MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
|
||||
// DBC file selector
|
||||
QHBoxLayout *dbc_file_layout = new QHBoxLayout();
|
||||
QComboBox *combo = new QComboBox(this);
|
||||
auto dbc_names = get_dbc_names();
|
||||
auto dbc_names = dbc()->allDBCNames();
|
||||
for (const auto &name : dbc_names) {
|
||||
combo->addItem(QString::fromStdString(name));
|
||||
}
|
||||
combo->setEditable(true);
|
||||
combo->setCurrentText(QString());
|
||||
combo->setInsertPolicy(QComboBox::NoInsert);
|
||||
combo->completer()->setCompletionMode(QCompleter::PopupCompletion);
|
||||
QFont font;
|
||||
font.setBold(true);
|
||||
combo->lineEdit()->setFont(font);
|
||||
dbc_file_layout->addWidget(combo);
|
||||
|
||||
dbc_file_layout->addStretch();
|
||||
@@ -21,73 +34,104 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
|
||||
dbc_file_layout->addWidget(save_btn);
|
||||
main_layout->addLayout(dbc_file_layout);
|
||||
|
||||
filter = new QLineEdit(this);
|
||||
// message filter
|
||||
QLineEdit *filter = new QLineEdit(this);
|
||||
filter->setPlaceholderText(tr("filter messages"));
|
||||
main_layout->addWidget(filter);
|
||||
|
||||
table_widget = new QTableWidget(this);
|
||||
// message table
|
||||
table_widget = new QTableView(this);
|
||||
model = new MessageListModel(this);
|
||||
QSortFilterProxyModel *proxy_model = new QSortFilterProxyModel(this);
|
||||
proxy_model->setSourceModel(model);
|
||||
proxy_model->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxy_model->setDynamicSortFilter(false);
|
||||
table_widget->setModel(proxy_model);
|
||||
table_widget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table_widget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
||||
table_widget->setColumnCount(4);
|
||||
table_widget->setSortingEnabled(true);
|
||||
table_widget->setColumnWidth(0, 250);
|
||||
table_widget->setColumnWidth(1, 80);
|
||||
table_widget->setColumnWidth(2, 80);
|
||||
table_widget->setHorizontalHeaderLabels({tr("Name"), tr("ID"), tr("Count"), tr("Bytes")});
|
||||
table_widget->horizontalHeader()->setStretchLastSection(true);
|
||||
table_widget->verticalHeader()->hide();
|
||||
table_widget->sortByColumn(0, Qt::AscendingOrder);
|
||||
main_layout->addWidget(table_widget);
|
||||
|
||||
QObject::connect(parser, &Parser::updated, this, &MessagesWidget::updateState);
|
||||
// signals/slots
|
||||
QObject::connect(filter, &QLineEdit::textChanged, proxy_model, &QSortFilterProxyModel::setFilterFixedString);
|
||||
QObject::connect(can, &CANMessages::updated, model, &MessageListModel::updateState);
|
||||
QObject::connect(combo, SIGNAL(activated(const QString &)), SLOT(dbcSelectionChanged(const QString &)));
|
||||
QObject::connect(save_btn, &QPushButton::clicked, [=]() {
|
||||
// TODO: save DBC to file
|
||||
});
|
||||
QObject::connect(combo, &QComboBox::currentTextChanged, [=](const QString &dbc) {
|
||||
parser->openDBC(dbc);
|
||||
});
|
||||
QObject::connect(table_widget, &QTableWidget::itemSelectionChanged, [=]() {
|
||||
const CanData *c = &(parser->can_msgs[table_widget->selectedItems()[1]->text()]);
|
||||
parser->setCurrentMsg(c->id);
|
||||
emit msgChanged(c);
|
||||
QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) {
|
||||
if (current.isValid()) {
|
||||
emit msgSelectionChanged(table_widget->model()->data(current, Qt::UserRole).toString());
|
||||
}
|
||||
});
|
||||
|
||||
// For test purpose
|
||||
combo->setCurrentText("toyota_nodsu_pt_generated");
|
||||
}
|
||||
|
||||
void MessagesWidget::updateState() {
|
||||
auto getTableItem = [=](int row, int col) -> QTableWidgetItem * {
|
||||
auto item = table_widget->item(row, col);
|
||||
if (!item) {
|
||||
item = new QTableWidgetItem();
|
||||
item->setFlags(item->flags() ^ Qt::ItemIsEditable);
|
||||
table_widget->setItem(row, col, item);
|
||||
}
|
||||
return item;
|
||||
};
|
||||
void MessagesWidget::dbcSelectionChanged(const QString &dbc_file) {
|
||||
dbc()->open(dbc_file);
|
||||
// update detailwidget
|
||||
auto current = table_widget->selectionModel()->currentIndex();
|
||||
if (current.isValid()) {
|
||||
emit msgSelectionChanged(table_widget->model()->data(current, Qt::UserRole).toString());
|
||||
}
|
||||
}
|
||||
|
||||
// MessageListModel
|
||||
|
||||
QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
||||
if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
|
||||
return (QString[]){"Name", "ID", "Count", "Bytes"}[section];
|
||||
else if (orientation == Qt::Vertical && role == Qt::DisplayRole) {
|
||||
// return QString::number(section);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
table_widget->setRowCount(parser->can_msgs.size());
|
||||
int i = 0;
|
||||
QString name, untitled = tr("untitled");
|
||||
const QString filter_str = filter->text();
|
||||
for (const auto &[_, c] : parser->can_msgs) {
|
||||
if (auto msg = parser->getMsg(c.address)) {
|
||||
name = msg->name.c_str();
|
||||
} else {
|
||||
name = untitled;
|
||||
QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
||||
if (role == Qt::DisplayRole) {
|
||||
auto it = std::next(can->can_msgs.begin(), index.row());
|
||||
if (it != can->can_msgs.end() && !it.value().empty()) {
|
||||
const auto &d = it.value().front();
|
||||
const QString &msg_id = it.key();
|
||||
switch (index.column()) {
|
||||
case 0: {
|
||||
auto msg = dbc()->msg(msg_id);
|
||||
QString name = msg ? msg->name.c_str() : "untitled";
|
||||
return name;
|
||||
}
|
||||
case 1: return msg_id;
|
||||
case 2: return can->counters[msg_id];
|
||||
case 3: return toHex(d.dat);
|
||||
}
|
||||
}
|
||||
if (!filter_str.isEmpty() && !name.contains(filter_str, Qt::CaseInsensitive)) {
|
||||
table_widget->hideRow(i++);
|
||||
continue;
|
||||
}
|
||||
} else if (role == Qt::UserRole) {
|
||||
return std::next(can->can_msgs.begin(), index.row()).key();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
getTableItem(i, 0)->setText(name);
|
||||
getTableItem(i, 1)->setText(c.id);
|
||||
getTableItem(i, 2)->setText(QString::number(parser->counters[c.id]));
|
||||
getTableItem(i, 3)->setText(toHex(c.dat));
|
||||
table_widget->showRow(i);
|
||||
i++;
|
||||
void MessageListModel::updateState() {
|
||||
int prev_row_count = row_count;
|
||||
row_count = can->can_msgs.size();
|
||||
int delta = row_count - prev_row_count;
|
||||
if (delta > 0) {
|
||||
beginInsertRows({}, prev_row_count, row_count - 1);
|
||||
endInsertRows();
|
||||
} else if (delta < 0) {
|
||||
beginRemoveRows({}, row_count, prev_row_count - 1);
|
||||
endRemoveRows();
|
||||
}
|
||||
if (table_widget->currentRow() == -1) {
|
||||
table_widget->selectRow(0);
|
||||
|
||||
if (row_count > 0) {
|
||||
emit dataChanged(index(0, 0), index(row_count - 1, 3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QTableWidget>
|
||||
#include <QWidget>
|
||||
#include <QAbstractTableModel>
|
||||
#include <QTableView>
|
||||
|
||||
#include "tools/cabana/parser.h"
|
||||
#include "tools/cabana/canmessages.h"
|
||||
|
||||
class MessageListModel : public QAbstractTableModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MessageListModel(QObject *parent) : QAbstractTableModel(parent) {}
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 4; }
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; }
|
||||
void updateState();
|
||||
|
||||
private:
|
||||
int row_count = 0;
|
||||
};
|
||||
|
||||
class MessagesWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
public:
|
||||
MessagesWidget(QWidget *parent);
|
||||
|
||||
public slots:
|
||||
void updateState();
|
||||
public slots:
|
||||
void dbcSelectionChanged(const QString &dbc_file);
|
||||
|
||||
signals:
|
||||
void msgChanged(const CanData *id);
|
||||
signals:
|
||||
void msgSelectionChanged(const QString &message_id);
|
||||
|
||||
protected:
|
||||
QLineEdit *filter;
|
||||
QTableWidget *table_widget;
|
||||
protected:
|
||||
QTableView *table_widget;
|
||||
MessageListModel *model;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
+38
-31
@@ -3,13 +3,12 @@
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
// SignalForm
|
||||
|
||||
SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) {
|
||||
SignalForm::SignalForm(const Signal &sig, QWidget *parent) : start_bit(sig.start_bit), QWidget(parent) {
|
||||
QFormLayout *form_layout = new QFormLayout(this);
|
||||
|
||||
name = new QLineEdit(sig.name.c_str());
|
||||
@@ -33,7 +32,8 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) {
|
||||
sign->setCurrentIndex(sig.is_signed ? 0 : 1);
|
||||
form_layout->addRow(tr("sign"), sign);
|
||||
|
||||
factor = new QSpinBox();
|
||||
factor = new QDoubleSpinBox();
|
||||
factor->setDecimals(3);
|
||||
factor->setValue(sig.factor);
|
||||
form_layout->addRow(tr("Factor"), factor);
|
||||
|
||||
@@ -46,9 +46,11 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) {
|
||||
form_layout->addRow(tr("Unit"), unit);
|
||||
comment = new QLineEdit();
|
||||
form_layout->addRow(tr("Comment"), comment);
|
||||
min_val = new QSpinBox();
|
||||
min_val = new QDoubleSpinBox();
|
||||
factor->setDecimals(3);
|
||||
form_layout->addRow(tr("Minimum value"), min_val);
|
||||
max_val = new QSpinBox();
|
||||
max_val = new QDoubleSpinBox();
|
||||
factor->setDecimals(3);
|
||||
form_layout->addRow(tr("Maximum value"), max_val);
|
||||
val_desc = new QLineEdit();
|
||||
form_layout->addRow(tr("Value descriptions"), val_desc);
|
||||
@@ -56,11 +58,11 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) {
|
||||
|
||||
std::optional<Signal> SignalForm::getSignal() {
|
||||
Signal sig = {};
|
||||
sig.start_bit = start_bit;
|
||||
sig.name = name->text().toStdString();
|
||||
sig.size = size->text().toInt();
|
||||
sig.offset = offset->text().toDouble();
|
||||
sig.factor = factor->text().toDouble();
|
||||
sig.msb = msb->text().toInt();
|
||||
sig.is_signed = sign->currentIndex() == 0;
|
||||
sig.is_little_endian = endianness->currentIndex() == 0;
|
||||
if (sig.is_little_endian) {
|
||||
@@ -75,30 +77,32 @@ std::optional<Signal> SignalForm::getSignal() {
|
||||
|
||||
// SignalEdit
|
||||
|
||||
SignalEdit::SignalEdit(const QString &id, const Signal &sig, const QString &color, QWidget *parent) : id(id), name_(sig.name.c_str()), QWidget(parent) {
|
||||
SignalEdit::SignalEdit(int index, const QString &id, const Signal &sig, const QString &color, QWidget *parent)
|
||||
: id(id), name_(sig.name.c_str()), QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// title
|
||||
QHBoxLayout *title_layout = new QHBoxLayout();
|
||||
QLabel *icon = new QLabel(">");
|
||||
icon = new QLabel(">");
|
||||
icon->setFixedSize(15, 30);
|
||||
icon->setStyleSheet("font-weight:bold");
|
||||
title_layout->addWidget(icon);
|
||||
title = new ElidedLabel(this);
|
||||
title->setText(sig.name.c_str());
|
||||
title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
|
||||
title->setText(QString("%1. %2").arg(index + 1).arg(sig.name.c_str()));
|
||||
title->setStyleSheet(QString("font-weight:bold; color:%1").arg(color));
|
||||
title_layout->addWidget(title);
|
||||
title_layout->addStretch();
|
||||
|
||||
plot_btn = new QPushButton("📈");
|
||||
plot_btn->setToolTip(tr("Show Plot"));
|
||||
plot_btn->setFixedSize(30, 30);
|
||||
QObject::connect(plot_btn, &QPushButton::clicked, [=]() { emit parser->showPlot(id, name_); });
|
||||
QObject::connect(plot_btn, &QPushButton::clicked, [=]() { emit showChart(id, name_); });
|
||||
title_layout->addWidget(plot_btn);
|
||||
main_layout->addLayout(title_layout);
|
||||
|
||||
edit_container = new QWidget(this);
|
||||
QVBoxLayout *v_layout = new QVBoxLayout(edit_container);
|
||||
form_container = new QWidget(this);
|
||||
QVBoxLayout *v_layout = new QVBoxLayout(form_container);
|
||||
form = new SignalForm(sig, this);
|
||||
v_layout->addWidget(form);
|
||||
|
||||
@@ -110,24 +114,27 @@ SignalEdit::SignalEdit(const QString &id, const Signal &sig, const QString &colo
|
||||
h->addWidget(save_btn);
|
||||
v_layout->addLayout(h);
|
||||
|
||||
edit_container->setVisible(false);
|
||||
main_layout->addWidget(edit_container);
|
||||
form_container->setVisible(false);
|
||||
main_layout->addWidget(form_container);
|
||||
|
||||
QFrame* hline = new QFrame();
|
||||
hline->setFrameShape(QFrame::HLine);
|
||||
hline->setFrameShadow(QFrame::Sunken);
|
||||
main_layout->addWidget(hline);
|
||||
|
||||
QObject::connect(remove_btn, &QPushButton::clicked, this, &SignalEdit::remove);
|
||||
QObject::connect(save_btn, &QPushButton::clicked, this, &SignalEdit::save);
|
||||
QObject::connect(title, &ElidedLabel::clicked, [=]() {
|
||||
edit_container->isVisible() ? edit_container->hide() : edit_container->show();
|
||||
icon->setText(edit_container->isVisible() ? "▼" : ">");
|
||||
});
|
||||
QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked);
|
||||
}
|
||||
|
||||
void SignalEdit::setFormVisible(bool visible) {
|
||||
form_container->setVisible(visible);
|
||||
icon->setText(visible ? "▼" : ">");
|
||||
}
|
||||
|
||||
void SignalEdit::save() {
|
||||
if (auto sig = const_cast<Signal *>(parser->getSig(id, name_))) {
|
||||
if (auto s = form->getSignal()) {
|
||||
*sig = *s;
|
||||
// TODO: reset the chart for sig
|
||||
}
|
||||
}
|
||||
if (auto s = form->getSignal())
|
||||
dbc()->updateSignal(id, name_, *s);
|
||||
}
|
||||
|
||||
void SignalEdit::remove() {
|
||||
@@ -137,7 +144,7 @@ void SignalEdit::remove() {
|
||||
msgbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
|
||||
msgbox.setDefaultButton(QMessageBox::Cancel);
|
||||
if (msgbox.exec()) {
|
||||
parser->removeSignal(id, name_);
|
||||
dbc()->removeSignal(id, name_);
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -145,19 +152,19 @@ void SignalEdit::remove() {
|
||||
// AddSignalDialog
|
||||
|
||||
AddSignalDialog::AddSignalDialog(const QString &id, QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle(tr("Add signal to %1").arg(parser->getMsg(id)->name.c_str()));
|
||||
setWindowTitle(tr("Add signal to %1").arg(dbc()->msg(id)->name.c_str()));
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
Signal sig = {.name = "untitled"};
|
||||
auto form = new SignalForm(sig, this);
|
||||
main_layout->addWidget(form);
|
||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
main_layout->addWidget(buttonBox);
|
||||
setFixedWidth(parent->width() * 0.9);
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, [=]() {
|
||||
if (auto msg = const_cast<Msg *>(parser->getMsg(id))) {
|
||||
if (auto signal = form->getSignal()) {
|
||||
msg->sigs.push_back(*signal);
|
||||
}
|
||||
if (auto signal = form->getSignal()) {
|
||||
dbc()->addSignal(id, *signal);
|
||||
}
|
||||
QDialog::accept();
|
||||
});
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
#include "tools/cabana/parser.h"
|
||||
|
||||
#include "tools/cabana/canmessages.h"
|
||||
#include "tools/cabana/dbcmanager.h"
|
||||
|
||||
class SignalForm : public QWidget {
|
||||
Q_OBJECT
|
||||
@@ -19,17 +22,25 @@ public:
|
||||
std::optional<Signal> getSignal();
|
||||
|
||||
QLineEdit *name, *unit, *comment, *val_desc;
|
||||
QSpinBox *size, *msb, *lsb, *factor, *offset, *min_val, *max_val;
|
||||
QSpinBox *size, *msb, *lsb, *offset;
|
||||
QDoubleSpinBox *factor, *min_val, *max_val;
|
||||
QComboBox *sign, *endianness;
|
||||
int start_bit = 0;
|
||||
};
|
||||
|
||||
class SignalEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SignalEdit(const QString &id, const Signal &sig, const QString &color, QWidget *parent = nullptr);
|
||||
SignalEdit(int index, const QString &id, const Signal &sig, const QString &color, QWidget *parent = nullptr);
|
||||
void setFormVisible(bool show);
|
||||
inline bool isFormVisible() const { return form_container->isVisible(); }
|
||||
void save();
|
||||
|
||||
signals:
|
||||
void showChart(const QString &msg_id, const QString &sig_name);
|
||||
void showFormClicked();
|
||||
|
||||
protected:
|
||||
void remove();
|
||||
|
||||
@@ -38,8 +49,9 @@ protected:
|
||||
QPushButton *plot_btn;
|
||||
ElidedLabel *title;
|
||||
SignalForm *form;
|
||||
QWidget *edit_container;
|
||||
QWidget *form_container;
|
||||
QPushButton *remove_btn;
|
||||
QLabel *icon;
|
||||
};
|
||||
|
||||
class AddSignalDialog : public QDialog {
|
||||
|
||||
+44
-69
@@ -6,11 +6,10 @@
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QStyleOptionSlider>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "tools/cabana/parser.h"
|
||||
|
||||
inline QString formatTime(int seconds) {
|
||||
return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh::mm::ss" : "mm::ss");
|
||||
}
|
||||
@@ -25,18 +24,17 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
// slider controls
|
||||
QHBoxLayout *slider_layout = new QHBoxLayout();
|
||||
time_label = new QLabel("00:00");
|
||||
QLabel *time_label = new QLabel("00:00");
|
||||
slider_layout->addWidget(time_label);
|
||||
|
||||
slider = new Slider(this);
|
||||
slider->setSingleStep(0);
|
||||
slider->setMinimum(0);
|
||||
slider->setMaximum(parser->replay->totalSeconds() * 1000);
|
||||
slider->setMaximum(can->totalSeconds() * 1000);
|
||||
slider_layout->addWidget(slider);
|
||||
|
||||
total_time_label = new QLabel(formatTime(parser->replay->totalSeconds()));
|
||||
slider_layout->addWidget(total_time_label);
|
||||
|
||||
end_time_label = new QLabel(formatTime(can->totalSeconds()));
|
||||
slider_layout->addWidget(end_time_label);
|
||||
main_layout->addLayout(slider_layout);
|
||||
|
||||
// btn controls
|
||||
@@ -50,51 +48,39 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
|
||||
for (float speed : {0.1, 0.5, 1., 2.}) {
|
||||
QPushButton *btn = new QPushButton(QString("%1x").arg(speed), this);
|
||||
btn->setCheckable(true);
|
||||
QObject::connect(btn, &QPushButton::clicked, [=]() { parser->replay->setSpeed(speed); });
|
||||
QObject::connect(btn, &QPushButton::clicked, [=]() { can->setSpeed(speed); });
|
||||
control_layout->addWidget(btn);
|
||||
group->addButton(btn);
|
||||
if (speed == 1.0) btn->setChecked(true);
|
||||
}
|
||||
|
||||
main_layout->addLayout(control_layout);
|
||||
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
|
||||
QObject::connect(parser, &Parser::rangeChanged, this, &VideoWidget::rangeChanged);
|
||||
QObject::connect(parser, &Parser::updated, this, &VideoWidget::updateState);
|
||||
QObject::connect(slider, &QSlider::sliderMoved, [=]() { time_label->setText(formatTime(slider->value() / 1000)); });
|
||||
QObject::connect(slider, &QSlider::sliderReleased, [this]() { setPosition(slider->value()); });
|
||||
QObject::connect(slider, &Slider::setPosition, this, &VideoWidget::setPosition);
|
||||
|
||||
QObject::connect(can, &CANMessages::rangeChanged, this, &VideoWidget::rangeChanged);
|
||||
QObject::connect(can, &CANMessages::updated, this, &VideoWidget::updateState);
|
||||
QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->value() / 1000.0); });
|
||||
QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(formatTime(value / 1000)); });
|
||||
QObject::connect(play, &QPushButton::clicked, [=]() {
|
||||
bool is_paused = parser->replay->isPaused();
|
||||
bool is_paused = can->isPaused();
|
||||
play->setText(is_paused ? "⏸" : "▶");
|
||||
parser->replay->pause(!is_paused);
|
||||
can->pause(!is_paused);
|
||||
});
|
||||
}
|
||||
|
||||
void VideoWidget::setPosition(int value) {
|
||||
time_label->setText(formatTime(value / 1000.0));
|
||||
parser->seekTo(value / 1000.0);
|
||||
}
|
||||
|
||||
void VideoWidget::rangeChanged(double min, double max) {
|
||||
if (!parser->isZoomed()) {
|
||||
if (!can->isZoomed()) {
|
||||
min = 0;
|
||||
max = parser->replay->totalSeconds();
|
||||
max = can->totalSeconds();
|
||||
}
|
||||
time_label->setText(formatTime(min));
|
||||
total_time_label->setText(formatTime(max));
|
||||
end_time_label->setText(formatTime(max));
|
||||
slider->setMinimum(min * 1000);
|
||||
slider->setMaximum(max * 1000);
|
||||
slider->setValue(parser->currentSec() * 1000);
|
||||
}
|
||||
|
||||
void VideoWidget::updateState() {
|
||||
if (!slider->isSliderDown()) {
|
||||
double current_sec = parser->currentSec();
|
||||
time_label->setText(formatTime(current_sec));
|
||||
slider->setValue(current_sec * 1000);
|
||||
}
|
||||
if (!slider->isSliderDown())
|
||||
slider->setValue(can->currentSec() * 1000);
|
||||
}
|
||||
|
||||
// Slider
|
||||
@@ -102,7 +88,7 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) {
|
||||
QTimer *timer = new QTimer(this);
|
||||
timer->setInterval(2000);
|
||||
timer->callOnTimeout([this]() {
|
||||
timeline = parser->replay->getTimeline();
|
||||
timeline = can->getTimeline();
|
||||
update();
|
||||
});
|
||||
timer->start();
|
||||
@@ -110,7 +96,7 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) {
|
||||
|
||||
void Slider::sliderChange(QAbstractSlider::SliderChange change) {
|
||||
if (change == QAbstractSlider::SliderValueChange) {
|
||||
qreal x = width() * ((value() - minimum()) / double(maximum() - minimum()));
|
||||
int x = width() * ((value() - minimum()) / double(maximum() - minimum()));
|
||||
if (x != slider_x) {
|
||||
slider_x = x;
|
||||
update();
|
||||
@@ -121,45 +107,34 @@ void Slider::sliderChange(QAbstractSlider::SliderChange change) {
|
||||
}
|
||||
|
||||
void Slider::paintEvent(QPaintEvent *ev) {
|
||||
auto getPaintRange = [this](double begin, double end) -> std::pair<double, double> {
|
||||
double total_sec = maximum() - minimum();
|
||||
int start_pos = ((std::max((double)minimum(), (double)begin) - minimum()) / total_sec) * width();
|
||||
int end_pos = ((std::min((double)maximum(), (double)end) - minimum()) / total_sec) * width();
|
||||
return {start_pos, end_pos};
|
||||
};
|
||||
static const QColor colors[] = {
|
||||
[(int)TimelineType::None] = QColor(111, 143, 175),
|
||||
[(int)TimelineType::Engaged] = QColor(0, 163, 108),
|
||||
[(int)TimelineType::UserFlag] = Qt::white,
|
||||
[(int)TimelineType::AlertInfo] = Qt::green,
|
||||
[(int)TimelineType::AlertWarning] = QColor(255, 195, 0),
|
||||
[(int)TimelineType::AlertCritical] = QColor(199, 0, 57)};
|
||||
|
||||
QPainter p(this);
|
||||
const int v_margin = 2;
|
||||
p.fillRect(rect().adjusted(0, v_margin, 0, -v_margin), QColor(111, 143, 175));
|
||||
|
||||
QRect r = rect().adjusted(0, 4, 0, -4);
|
||||
p.fillRect(r, colors[(int)TimelineType::None]);
|
||||
double min = minimum() / 1000.0;
|
||||
double max = maximum() / 1000.0;
|
||||
for (auto [begin, end, type] : timeline) {
|
||||
begin *= 1000;
|
||||
end *= 1000;
|
||||
if (begin > maximum() || end < minimum()) continue;
|
||||
|
||||
if (type == TimelineType::Engaged) {
|
||||
auto [start_pos, end_pos] = getPaintRange(begin, end);
|
||||
p.fillRect(QRect(start_pos, v_margin, end_pos - start_pos, height() - v_margin * 2), QColor(0, 163, 108));
|
||||
}
|
||||
if (begin > max || end < min)
|
||||
continue;
|
||||
r.setLeft(((std::max(min, (double)begin) - min) / (max - min)) * width());
|
||||
r.setRight(((std::min(max, (double)end) - min) / (max - min)) * width());
|
||||
p.fillRect(r, colors[(int)type]);
|
||||
}
|
||||
for (auto [begin, end, type] : timeline) {
|
||||
begin *= 1000;
|
||||
end *= 1000;
|
||||
if (type == TimelineType::Engaged || begin > maximum() || end < minimum()) continue;
|
||||
|
||||
auto [start_pos, end_pos] = getPaintRange(begin, end);
|
||||
if (type == TimelineType::UserFlag) {
|
||||
p.fillRect(QRect(start_pos, height() - v_margin - 3, end_pos - start_pos, 3), Qt::white);
|
||||
} else {
|
||||
QColor color(Qt::green);
|
||||
if (type != TimelineType::AlertInfo)
|
||||
color = type == TimelineType::AlertWarning ? QColor(255, 195, 0) : QColor(199, 0, 57);
|
||||
|
||||
p.fillRect(QRect(start_pos, height() - v_margin - 3, end_pos - start_pos, 3), color);
|
||||
}
|
||||
}
|
||||
p.setPen(QPen(QColor(88, 24, 69), 3));
|
||||
p.drawLine(QPoint{slider_x, 0}, QPoint{slider_x, height()});
|
||||
QStyleOptionSlider opt;
|
||||
opt.initFrom(this);
|
||||
opt.minimum = minimum();
|
||||
opt.maximum = maximum();
|
||||
opt.subControls = QStyle::SC_SliderHandle;
|
||||
opt.sliderPosition = value();
|
||||
style()->drawComplexControl(QStyle::CC_Slider, &opt, &p);
|
||||
}
|
||||
|
||||
void Slider::mousePressEvent(QMouseEvent *e) {
|
||||
@@ -167,6 +142,6 @@ void Slider::mousePressEvent(QMouseEvent *e) {
|
||||
if (e->button() == Qt::LeftButton && !isSliderDown()) {
|
||||
int value = minimum() + ((maximum() - minimum()) * e->x()) / width();
|
||||
setValue(value);
|
||||
emit setPosition(value);
|
||||
emit sliderReleased();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,19 @@
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h"
|
||||
#include "tools/replay/replay.h"
|
||||
#include "tools/cabana/canmessages.h"
|
||||
|
||||
class Slider : public QSlider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Slider(QWidget *parent);
|
||||
void mousePressEvent(QMouseEvent* e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void sliderChange(QAbstractSlider::SliderChange change) override;
|
||||
|
||||
signals:
|
||||
void setPosition(int value);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *ev) override;
|
||||
std::vector<std::tuple<int, int, TimelineType>> timeline;
|
||||
void paintEvent(QPaintEvent *ev) override;
|
||||
|
||||
int slider_x = -1;
|
||||
std::vector<std::tuple<int, int, TimelineType>> timeline;
|
||||
};
|
||||
|
||||
class VideoWidget : public QWidget {
|
||||
@@ -34,9 +29,8 @@ public:
|
||||
protected:
|
||||
void rangeChanged(double min, double max);
|
||||
void updateState();
|
||||
void setPosition(int value);
|
||||
|
||||
CameraViewWidget *cam_widget;
|
||||
QLabel *time_label, *total_time_label;
|
||||
QLabel *end_time_label;
|
||||
Slider *slider;
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
#include "tools/replay/consoleui.h"
|
||||
#include "tools/replay/replay.h"
|
||||
|
||||
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "tools/replay/camera.h"
|
||||
#include "tools/replay/route.h"
|
||||
|
||||
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
|
||||
|
||||
// one segment uses about 100M of memory
|
||||
constexpr int FORWARD_SEGS = 5;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "tools/replay/replay.h"
|
||||
#include "tools/replay/util.h"
|
||||
|
||||
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36";
|
||||
const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2";
|
||||
const std::string TEST_RLOG_CHECKSUM = "5b966d4bb21a100a8c4e59195faeb741b975ccbe268211765efd1763d892bfb3";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user