commit
2b3d1f2cfc
118 changed files with 2386 additions and 416 deletions
@ -1 +1 @@ |
||||
Subproject commit d4cf8728e2fa2d87d90098efa7ddeaf8f98a03db |
||||
Subproject commit b29717c4c328d5cf34d46f682f25267150f82637 |
@ -1 +1 @@ |
||||
Subproject commit c8bc1fa01be9f22592efb991ee52d3d965d21968 |
||||
Subproject commit e1049cde0a68f7d4a70b1ebd76befdc0e163ad55 |
@ -1 +1 @@ |
||||
Subproject commit eaac172af9cb342204e69ec52339cdf3c6a8ac4e |
||||
Subproject commit c35e8139bf9e9d87b9efb6764ab7e65983e8d33e |
@ -1 +1 @@ |
||||
Subproject commit 1910db8d4c3f932fe85b186fba1d24795cb2b742 |
||||
Subproject commit 1303af2db29a72eee180b10c6097fa5b19c29207 |
@ -1,3 +1,3 @@ |
||||
version https://git-lfs.github.com/spec/v1 |
||||
oid sha256:50c7fc8565ac69a4b9a0de122e961326820e78bf13659255a89d0ed04be030d5 |
||||
size 95167481 |
||||
oid sha256:c4d37af666344af6bb218e0b939b1152ad3784c15ac79e37bcf0124643c8286a |
||||
size 58539563 |
||||
|
@ -0,0 +1,49 @@ |
||||
#!/usr/bin/env python3 |
||||
import json |
||||
import time |
||||
import unittest |
||||
import subprocess |
||||
|
||||
import cereal.messaging as messaging |
||||
from system.hardware import TICI |
||||
from selfdrive.manager.process_config import managed_processes |
||||
|
||||
|
||||
class TestRawgpsd(unittest.TestCase): |
||||
@classmethod |
||||
def setUpClass(cls): |
||||
if not TICI: |
||||
raise unittest.SkipTest |
||||
|
||||
def tearDown(self): |
||||
managed_processes['rawgpsd'].stop() |
||||
|
||||
def test_startup_time(self): |
||||
for _ in range(5): |
||||
sm = messaging.SubMaster(['qcomGnss']) |
||||
managed_processes['rawgpsd'].start() |
||||
|
||||
start_time = time.monotonic() |
||||
for __ in range(10): |
||||
sm.update(1 * 1000) |
||||
if sm.updated['qcomGnss']: |
||||
break |
||||
assert sm.rcv_frame['qcomGnss'] > 0, "rawgpsd didn't start outputting messages in time" |
||||
|
||||
et = time.monotonic() - start_time |
||||
assert et < 5, f"rawgpsd took {et:.1f}s to start" |
||||
managed_processes['rawgpsd'].stop() |
||||
|
||||
def test_turns_off_gnss(self): |
||||
for s in (0.1, 0.5, 1, 5): |
||||
managed_processes['rawgpsd'].start() |
||||
time.sleep(s) |
||||
managed_processes['rawgpsd'].stop() |
||||
|
||||
ls = subprocess.check_output("mmcli -m any --location-status --output-json", shell=True, encoding='utf-8') |
||||
loc_status = json.loads(ls) |
||||
assert set(loc_status['modem']['location']['enabled']) <= {'3gpp-lac-ci'} |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
unittest.main() |
@ -1 +1 @@ |
||||
c40319a454840d8a2196ec1227d27b536ee14375 |
||||
c171250d2cc013b3eca1cda4fb62f3d0dda28d4d |
||||
|
@ -1 +1 @@ |
||||
53079010a5db8105854212157b5ee90029df7b92 |
||||
f636c68e2b4ed88d3731930cf15b6dee984eb6dd |
||||
|
@ -1 +1 @@ |
||||
Subproject commit 2e9b7637b3c3c8895fda9f964215db3a35fe3441 |
||||
Subproject commit 870ea766eec7a38d7d590c81436f15271ba2667e |
@ -0,0 +1,4 @@ |
||||
moc_* |
||||
*.moc |
||||
|
||||
_cabana |
@ -0,0 +1,9 @@ |
||||
# Cabana |
||||
|
||||
<img src="https://cabana.comma.ai/img/cabana.jpg" width="640" height="267" /> |
||||
|
||||
Cabana is a tool developed to view raw CAN data. One use for this is creating and editing [CAN Dictionaries](http://socialledge.com/sjsu/index.php/DBC_Format) (DBC files), and the tool provides direct integration with [commaai/opendbc](https://github.com/commaai/opendbc) (a collection of DBC files), allowing you to load the DBC files direct from source, and save to your fork. In addition, you can load routes from [comma connect](https://connect.comma.ai). |
||||
|
||||
## Usage Instructions |
||||
|
||||
See [openpilot wiki](https://github.com/commaai/openpilot/wiki/Cabana) |
@ -0,0 +1,20 @@ |
||||
import os |
||||
Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', |
||||
'cereal', 'transformations', 'widgets', 'opendbc') |
||||
|
||||
base_frameworks = qt_env['FRAMEWORKS'] |
||||
base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', |
||||
'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] |
||||
|
||||
if arch == "Darwin": |
||||
base_frameworks.append('OpenCL') |
||||
else: |
||||
base_libs.append('OpenCL') |
||||
|
||||
qt_libs = ['qt_util', 'Qt5Charts'] + base_libs |
||||
if arch in ['x86_64', 'Darwin'] and GetOption('extras'): |
||||
qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] |
||||
|
||||
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) |
@ -0,0 +1,4 @@ |
||||
#!/bin/sh |
||||
cd "$(dirname "$0")" |
||||
export LD_LIBRARY_PATH="../../opendbc/can:$LD_LIBRARY_PATH" |
||||
exec ./_cabana "$1" |
@ -0,0 +1,32 @@ |
||||
#include <QApplication> |
||||
#include <QCommandLineParser> |
||||
|
||||
#include "selfdrive/ui/qt/util.h" |
||||
#include "tools/cabana/mainwin.h" |
||||
|
||||
const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; |
||||
|
||||
int main(int argc, char *argv[]) { |
||||
initApp(argc, argv); |
||||
QApplication app(argc, argv); |
||||
|
||||
QCommandLineParser cmd_parser; |
||||
cmd_parser.addHelpOption(); |
||||
cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai"); |
||||
cmd_parser.addOption({"demo", "use a demo route instead of providing your own"}); |
||||
cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"}); |
||||
cmd_parser.process(app); |
||||
const QStringList args = cmd_parser.positionalArguments(); |
||||
if (args.empty() && !cmd_parser.isSet("demo")) { |
||||
cmd_parser.showHelp(); |
||||
} |
||||
|
||||
const QString route = args.empty() ? DEMO_ROUTE : args.first(); |
||||
Parser p(&app); |
||||
if (!p.loadRoute(route, cmd_parser.value("data_dir"), true)) { |
||||
return 0; |
||||
} |
||||
MainWindow w; |
||||
w.showMaximized(); |
||||
return app.exec(); |
||||
} |
@ -0,0 +1,271 @@ |
||||
#include "tools/cabana/chartswidget.h" |
||||
|
||||
#include <QGraphicsLayout> |
||||
#include <QLabel> |
||||
#include <QRubberBand> |
||||
#include <QStackedLayout> |
||||
#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) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setContentsMargins(0, 0, 0, 0); |
||||
|
||||
// title bar
|
||||
title_bar = new QWidget(this); |
||||
QHBoxLayout *title_layout = new QHBoxLayout(title_bar); |
||||
title_label = new QLabel(tr("Charts")); |
||||
|
||||
title_layout->addWidget(title_label); |
||||
title_layout->addStretch(); |
||||
|
||||
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->setVisible(false); |
||||
remove_all_btn->setToolTip(tr("Remove all charts")); |
||||
remove_all_btn->setFixedSize(30, 30); |
||||
title_layout->addWidget(remove_all_btn); |
||||
|
||||
dock_btn = new QPushButton(); |
||||
dock_btn->setFixedSize(30, 30); |
||||
updateDockButton(); |
||||
title_layout->addWidget(dock_btn); |
||||
|
||||
main_layout->addWidget(title_bar, 0, Qt::AlignTop); |
||||
|
||||
// charts
|
||||
QWidget *charts_container = new QWidget(this); |
||||
QVBoxLayout *charts_main = new QVBoxLayout(charts_container); |
||||
charts_layout = new QVBoxLayout(); |
||||
charts_main->addLayout(charts_layout); |
||||
charts_main->addStretch(); |
||||
|
||||
QScrollArea *charts_scroll = new QScrollArea(this); |
||||
charts_scroll->setWidgetResizable(true); |
||||
charts_scroll->setWidget(charts_container); |
||||
charts_scroll->setFrameShape(QFrame::NoFrame); |
||||
charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
||||
|
||||
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); |
||||
QObject::connect(remove_all_btn, &QPushButton::clicked, this, &ChartsWidget::removeAll); |
||||
QObject::connect(dock_btn, &QPushButton::clicked, [=]() { |
||||
emit dock(!docking); |
||||
docking = !docking; |
||||
updateDockButton(); |
||||
}); |
||||
} |
||||
|
||||
void ChartsWidget::updateDockButton() { |
||||
dock_btn->setText(docking ? "⬈" : "⬋"); |
||||
dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); |
||||
} |
||||
|
||||
void ChartsWidget::addChart(const QString &id, const QString &sig_name) { |
||||
const QString char_name = id + sig_name; |
||||
if (charts.find(char_name) == charts.end()) { |
||||
auto chart = new ChartWidget(id, sig_name, this); |
||||
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())); |
||||
} |
||||
|
||||
void ChartsWidget::removeChart(const QString &id, const QString &sig_name) { |
||||
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())); |
||||
} |
||||
|
||||
void ChartsWidget::removeAll() { |
||||
for (auto [_, chart] : charts) |
||||
chart->deleteLater(); |
||||
charts.clear(); |
||||
remove_all_btn->setVisible(false); |
||||
reset_zoom_btn->setVisible(false); |
||||
} |
||||
|
||||
// ChartWidget
|
||||
|
||||
ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *parent) : id(id), sig_name(sig_name), QWidget(parent) { |
||||
QStackedLayout *stacked = new QStackedLayout(this); |
||||
stacked->setStackingMode(QStackedLayout::StackAll); |
||||
|
||||
QWidget *chart_widget = new QWidget(this); |
||||
QVBoxLayout *chart_layout = new QVBoxLayout(chart_widget); |
||||
chart_layout->setSpacing(0); |
||||
chart_layout->setContentsMargins(0, 0, 0, 0); |
||||
|
||||
QWidget *header = new QWidget(this); |
||||
header->setStyleSheet("background-color:white"); |
||||
QHBoxLayout *header_layout = new QHBoxLayout(header); |
||||
header_layout->setContentsMargins(11, 11, 11, 0); |
||||
QLabel *title = new QLabel(tr("%1 %2").arg(parser->getMsg(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); |
||||
}); |
||||
header_layout->addWidget(remove_btn); |
||||
chart_layout->addWidget(header); |
||||
|
||||
QLineSeries *series = new QLineSeries(); |
||||
series->setUseOpenGL(true); |
||||
auto chart = new QChart(); |
||||
chart->setTitle(sig_name); |
||||
chart->addSeries(series); |
||||
chart->createDefaultAxes(); |
||||
chart->legend()->hide(); |
||||
QFont font; |
||||
font.setBold(true); |
||||
chart->setTitleFont(font); |
||||
chart->setMargins({0, 0, 0, 0}); |
||||
chart->layout()->setContentsMargins(0, 0, 0, 0); |
||||
QObject::connect(dynamic_cast<QValueAxis *>(chart->axisX()), &QValueAxis::rangeChanged, parser, &Parser::setRange); |
||||
|
||||
chart_view = new QChartView(chart); |
||||
chart_view->setFixedHeight(300); |
||||
chart_view->setRenderHint(QPainter::Antialiasing); |
||||
chart_view->setRubberBand(QChartView::HorizontalRubberBand); |
||||
if (auto rubber = chart_view->findChild<QRubberBand *>()) { |
||||
QPalette pal; |
||||
pal.setBrush(QPalette::Base, QColor(0, 0, 0, 80)); |
||||
rubber->setPalette(pal); |
||||
} |
||||
chart_layout->addWidget(chart_view); |
||||
chart_layout->addStretch(); |
||||
|
||||
stacked->addWidget(chart_widget); |
||||
line_marker = new LineMarker(this); |
||||
stacked->addWidget(line_marker); |
||||
line_marker->setAttribute(Qt::WA_TransparentForMouseEvents, true); |
||||
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); |
||||
|
||||
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; |
||||
} |
||||
} |
||||
|
||||
void ChartWidget::updateSeries() { |
||||
const Signal *sig = parser->getSig(id, sig_name); |
||||
auto events = parser->replay->events(); |
||||
if (!sig || !events) return; |
||||
|
||||
auto l = id.split(':'); |
||||
int bus = l[0].toInt(); |
||||
uint32_t address = l[1].toUInt(nullptr, 16); |
||||
|
||||
vals.clear(); |
||||
vals.reserve(3 * 60 * 100); |
||||
uint64_t route_start_time = parser->replay->routeStartTime(); |
||||
for (auto &evt : *events) { |
||||
if (evt->which == cereal::Event::Which::CAN) { |
||||
for (auto c : evt->event.getCan()) { |
||||
if (bus == c.getSrc() && address == c.getAddress()) { |
||||
auto dat = c.getDat(); |
||||
int64_t val = get_raw_value((uint8_t *)dat.begin(), dat.size(), *sig); |
||||
if (sig->is_signed) { |
||||
val -= ((val >> (sig->size - 1)) & 0x1) ? (1ULL << sig->size) : 0; |
||||
} |
||||
double value = val * sig->factor + sig->offset; |
||||
double ts = (evt->mono_time - route_start_time) / (double)1e9; // seconds
|
||||
vals.push_back({ts, value}); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
QLineSeries *series = (QLineSeries *)chart_view->chart()->series()[0]; |
||||
series->replace(vals); |
||||
auto [begin, end] = parser->range(); |
||||
chart_view->chart()->axisX()->setRange(begin, end); |
||||
updateAxisY(); |
||||
} |
||||
|
||||
void ChartWidget::rangeChanged(qreal min, qreal max) { |
||||
auto axis_x = dynamic_cast<QValueAxis *>(chart_view->chart()->axisX()); |
||||
if (axis_x->min() != min || axis_x->max() != max) { |
||||
axis_x->setRange(min, max); |
||||
} |
||||
updateAxisY(); |
||||
} |
||||
|
||||
// auto zoom on yaxis
|
||||
void ChartWidget::updateAxisY() { |
||||
const auto axis_x = dynamic_cast<QValueAxis *>(chart_view->chart()->axisX()); |
||||
// 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()) |
||||
return; |
||||
|
||||
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()); |
||||
} |
||||
|
||||
// LineMarker
|
||||
|
||||
void LineMarker::setX(double x) { |
||||
x_pos = x; |
||||
update(); |
||||
} |
||||
|
||||
void LineMarker::paintEvent(QPaintEvent *event) { |
||||
QPainter p(this); |
||||
p.setPen(QPen(Qt::black, 2)); |
||||
p.drawLine(QPointF{x_pos, 50.}, QPointF{x_pos, (qreal)height() - 11}); |
||||
} |
@ -0,0 +1,77 @@ |
||||
#pragma once |
||||
|
||||
#include <map> |
||||
|
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
#include <QVBoxLayout> |
||||
#include <QWidget> |
||||
#include <QtCharts/QChartView> |
||||
#include <QtCharts/QLineSeries> |
||||
|
||||
#include "tools/cabana/parser.h" |
||||
|
||||
using namespace QtCharts; |
||||
|
||||
class LineMarker : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
LineMarker(QWidget *parent) : QWidget(parent) {} |
||||
void setX(double x); |
||||
|
||||
private: |
||||
void paintEvent(QPaintEvent *event) override; |
||||
double x_pos = 0.0; |
||||
}; |
||||
|
||||
class ChartWidget : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
ChartWidget(const QString &id, const QString &sig_name, QWidget *parent); |
||||
inline QChart *chart() const { return chart_view->chart(); } |
||||
|
||||
private: |
||||
void updateState(); |
||||
void addData(const CanData &can_data, const Signal &sig); |
||||
void updateSeries(); |
||||
void rangeChanged(qreal min, qreal max); |
||||
void updateAxisY(); |
||||
|
||||
QString id; |
||||
QString sig_name; |
||||
QChartView *chart_view = nullptr; |
||||
LineMarker *line_marker = nullptr; |
||||
double line_marker_x = 0.0; |
||||
QList<QPointF> vals; |
||||
}; |
||||
|
||||
class ChartsWidget : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
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(); |
||||
} |
||||
|
||||
signals: |
||||
void dock(bool floating); |
||||
|
||||
private: |
||||
void updateState(); |
||||
void updateDockButton(); |
||||
|
||||
QWidget *title_bar; |
||||
QLabel *title_label; |
||||
bool docking = true; |
||||
QPushButton *dock_btn; |
||||
QPushButton *reset_zoom_btn; |
||||
QPushButton *remove_all_btn; |
||||
QVBoxLayout *charts_layout; |
||||
std::map<QString, ChartWidget *> charts; |
||||
}; |
@ -0,0 +1,264 @@ |
||||
|
||||
#include "tools/cabana/detailwidget.h" |
||||
|
||||
#include <QDebug> |
||||
#include <QDialogButtonBox> |
||||
#include <QFormLayout> |
||||
#include <QHeaderView> |
||||
#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(); |
||||
time_label = new QLabel(this); |
||||
title_layout->addWidget(time_label); |
||||
title_layout->addStretch(); |
||||
|
||||
edit_btn = new QPushButton(tr("Edit"), this); |
||||
edit_btn->setVisible(false); |
||||
title_layout->addWidget(edit_btn); |
||||
main_layout->addLayout(title_layout); |
||||
|
||||
// binary view
|
||||
binary_view = new BinaryView(this); |
||||
main_layout->addWidget(binary_view); |
||||
|
||||
// scroll area
|
||||
QHBoxLayout *signals_layout = new QHBoxLayout(); |
||||
signals_layout->addWidget(new QLabel(tr("Signals"))); |
||||
signals_layout->addStretch(); |
||||
add_sig_btn = new QPushButton(tr("Add signal"), this); |
||||
add_sig_btn->setVisible(false); |
||||
signals_layout->addWidget(add_sig_btn); |
||||
main_layout->addLayout(signals_layout); |
||||
|
||||
QWidget *container = new QWidget(this); |
||||
QVBoxLayout *container_layout = new QVBoxLayout(container); |
||||
signal_edit_layout = new QVBoxLayout(); |
||||
signal_edit_layout->setSpacing(2); |
||||
container_layout->addLayout(signal_edit_layout); |
||||
|
||||
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); |
||||
|
||||
QObject::connect(add_sig_btn, &QPushButton::clicked, this, &DetailWidget::addSignal); |
||||
QObject::connect(edit_btn, &QPushButton::clicked, this, &DetailWidget::editMsg); |
||||
QObject::connect(parser, &Parser::updated, this, &DetailWidget::updateState); |
||||
} |
||||
|
||||
void DetailWidget::setMsg(const CanData *c) { |
||||
can_data = c; |
||||
clearLayout(signal_edit_layout); |
||||
edit_btn->setVisible(true); |
||||
|
||||
if (auto msg = parser->getMsg(can_data->address)) { |
||||
name_label->setText(msg->name.c_str()); |
||||
add_sig_btn->setVisible(true); |
||||
for (int i = 0; i < msg->sigs.size(); ++i) { |
||||
signal_edit_layout->addWidget(new SignalEdit(can_data->id, msg->sigs[i], getColor(i))); |
||||
} |
||||
} else { |
||||
name_label->setText(tr("untitled")); |
||||
add_sig_btn->setVisible(false); |
||||
} |
||||
|
||||
binary_view->setMsg(can_data); |
||||
history_log->clear(); |
||||
} |
||||
|
||||
void DetailWidget::updateState() { |
||||
if (!can_data) return; |
||||
|
||||
time_label->setText(QString("time: %1").arg(can_data->ts, 0, 'f', 3)); |
||||
binary_view->setData(can_data->dat); |
||||
history_log->updateState(); |
||||
} |
||||
|
||||
void DetailWidget::editMsg() { |
||||
EditMessageDialog dlg(can_data->id, this); |
||||
if (dlg.exec()) { |
||||
setMsg(can_data); |
||||
} |
||||
} |
||||
|
||||
void DetailWidget::addSignal() { |
||||
AddSignalDialog dlg(can_data->id, this); |
||||
if (dlg.exec()) { |
||||
setMsg(can_data); |
||||
} |
||||
} |
||||
|
||||
// BinaryView
|
||||
|
||||
BinaryView::BinaryView(QWidget *parent) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
table = new QTableWidget(this); |
||||
table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); |
||||
table->horizontalHeader()->hide(); |
||||
table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
||||
table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
||||
main_layout->addWidget(table); |
||||
table->setColumnCount(9); |
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); |
||||
} |
||||
|
||||
void BinaryView::setMsg(const CanData *can_data) { |
||||
const Msg *msg = parser->getMsg(can_data->address); |
||||
int row_count = msg ? msg->size : can_data->dat.size(); |
||||
|
||||
table->setRowCount(row_count); |
||||
table->setColumnCount(9); |
||||
for (int i = 0; i < table->rowCount(); ++i) { |
||||
for (int j = 0; j < table->columnCount(); ++j) { |
||||
auto item = new QTableWidgetItem(); |
||||
item->setFlags(item->flags() ^ Qt::ItemIsEditable); |
||||
item->setTextAlignment(Qt::AlignCenter); |
||||
if (j == 8) { |
||||
QFont font; |
||||
font.setBold(true); |
||||
item->setFont(font); |
||||
} |
||||
table->setItem(i, j, item); |
||||
} |
||||
} |
||||
|
||||
if (msg) { |
||||
// set background color
|
||||
for (int i = 0; i < msg->sigs.size(); ++i) { |
||||
const auto &sig = msg->sigs[i]; |
||||
int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit); |
||||
for (int j = start; j <= start + sig.size - 1; ++j) { |
||||
table->item(j / 8, j % 8)->setBackground(QColor(getColor(i))); |
||||
} |
||||
} |
||||
} |
||||
|
||||
setFixedHeight(table->rowHeight(0) * table->rowCount() + 25); |
||||
} |
||||
|
||||
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(); |
||||
} |
||||
|
||||
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])); |
||||
} |
||||
sprintf(&hex[0], "%02X", (unsigned char)binary[i]); |
||||
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) { |
||||
setWindowTitle(tr("Edit message")); |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
|
||||
QFormLayout *form_layout = new QFormLayout(); |
||||
form_layout->addRow("ID", new QLabel(id)); |
||||
|
||||
auto msg = const_cast<Msg *>(parser->getMsg(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()); |
||||
form_layout->addRow(tr("Size"), size_spin); |
||||
|
||||
main_layout->addLayout(form_layout); |
||||
|
||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); |
||||
main_layout->addWidget(buttonBox); |
||||
|
||||
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; |
||||
|
||||
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); |
||||
} |
||||
QDialog::accept(); |
||||
} |
@ -0,0 +1,70 @@ |
||||
#pragma once |
||||
|
||||
#include <QDialog> |
||||
#include <QLabel> |
||||
#include <QPushButton> |
||||
#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/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); |
||||
|
||||
QTableWidget *table; |
||||
}; |
||||
|
||||
class EditMessageDialog : public QDialog { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
EditMessageDialog(const QString &id, QWidget *parent); |
||||
|
||||
protected: |
||||
void save(); |
||||
|
||||
QLineEdit *name_edit; |
||||
QSpinBox *size_spin; |
||||
QString id; |
||||
}; |
||||
|
||||
class DetailWidget : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
DetailWidget(QWidget *parent); |
||||
void setMsg(const CanData *c); |
||||
|
||||
private: |
||||
void updateState(); |
||||
void addSignal(); |
||||
void editMsg(); |
||||
|
||||
const CanData *can_data = nullptr; |
||||
QLabel *name_label, *time_label; |
||||
QPushButton *edit_btn, *add_sig_btn; |
||||
QVBoxLayout *signal_edit_layout; |
||||
HistoryLog *history_log; |
||||
BinaryView *binary_view; |
||||
}; |
@ -0,0 +1,58 @@ |
||||
#include "tools/cabana/mainwin.h" |
||||
|
||||
#include <QHBoxLayout> |
||||
#include <QScreen> |
||||
#include <QVBoxLayout> |
||||
|
||||
MainWindow::MainWindow() : QWidget() { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
|
||||
QHBoxLayout *h_layout = new QHBoxLayout(); |
||||
main_layout->addLayout(h_layout); |
||||
|
||||
messages_widget = new MessagesWidget(this); |
||||
h_layout->addWidget(messages_widget); |
||||
|
||||
detail_widget = new DetailWidget(this); |
||||
detail_widget->setFixedWidth(600); |
||||
h_layout->addWidget(detail_widget); |
||||
|
||||
// right widgets
|
||||
QWidget *right_container = new QWidget(this); |
||||
right_container->setFixedWidth(640); |
||||
r_layout = new QVBoxLayout(right_container); |
||||
|
||||
video_widget = new VideoWidget(this); |
||||
r_layout->addWidget(video_widget, 0, Qt::AlignTop); |
||||
|
||||
charts_widget = new ChartsWidget(this); |
||||
r_layout->addWidget(charts_widget); |
||||
|
||||
h_layout->addWidget(right_container); |
||||
|
||||
QObject::connect(messages_widget, &MessagesWidget::msgChanged, detail_widget, &DetailWidget::setMsg); |
||||
QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts); |
||||
} |
||||
|
||||
void MainWindow::dockCharts(bool dock) { |
||||
charts_widget->setUpdatesEnabled(false); |
||||
if (dock && floating_window) { |
||||
r_layout->addWidget(charts_widget); |
||||
floating_window->deleteLater(); |
||||
floating_window = nullptr; |
||||
} else if (!dock && !floating_window) { |
||||
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->setMinimumSize(QGuiApplication::primaryScreen()->size() / 2); |
||||
floating_window->showMaximized(); |
||||
} |
||||
charts_widget->setUpdatesEnabled(true); |
||||
} |
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event) { |
||||
if (floating_window) |
||||
floating_window->deleteLater(); |
||||
QWidget::closeEvent(event); |
||||
} |
@ -0,0 +1,25 @@ |
||||
#pragma once |
||||
|
||||
#include "tools/cabana/chartswidget.h" |
||||
#include "tools/cabana/detailwidget.h" |
||||
#include "tools/cabana/messageswidget.h" |
||||
#include "tools/cabana/parser.h" |
||||
#include "tools/cabana/videowidget.h" |
||||
|
||||
class MainWindow : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
MainWindow(); |
||||
void dockCharts(bool dock); |
||||
|
||||
protected: |
||||
void closeEvent(QCloseEvent *event) override; |
||||
|
||||
VideoWidget *video_widget; |
||||
MessagesWidget *messages_widget; |
||||
DetailWidget *detail_widget; |
||||
ChartsWidget *charts_widget; |
||||
QWidget *floating_window = nullptr; |
||||
QVBoxLayout *r_layout; |
||||
}; |
@ -0,0 +1,93 @@ |
||||
#include "tools/cabana/messageswidget.h" |
||||
|
||||
#include <QComboBox> |
||||
#include <QHeaderView> |
||||
#include <QPushButton> |
||||
#include <QVBoxLayout> |
||||
|
||||
MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
|
||||
QHBoxLayout *dbc_file_layout = new QHBoxLayout(); |
||||
QComboBox *combo = new QComboBox(this); |
||||
auto dbc_names = get_dbc_names(); |
||||
for (const auto &name : dbc_names) { |
||||
combo->addItem(QString::fromStdString(name)); |
||||
} |
||||
dbc_file_layout->addWidget(combo); |
||||
|
||||
dbc_file_layout->addStretch(); |
||||
QPushButton *save_btn = new QPushButton(tr("Save DBC"), this); |
||||
dbc_file_layout->addWidget(save_btn); |
||||
main_layout->addLayout(dbc_file_layout); |
||||
|
||||
filter = new QLineEdit(this); |
||||
filter->setPlaceholderText(tr("filter messages")); |
||||
main_layout->addWidget(filter); |
||||
|
||||
table_widget = new QTableWidget(this); |
||||
table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); |
||||
table_widget->setSelectionMode(QAbstractItemView::SingleSelection); |
||||
table_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); |
||||
table_widget->setColumnCount(4); |
||||
table_widget->setColumnWidth(0, 250); |
||||
table_widget->setColumnWidth(1, 80); |
||||
table_widget->setColumnWidth(2, 80); |
||||
table_widget->setHorizontalHeaderLabels({tr("Name"), tr("ID"), tr("Count"), tr("Bytes")}); |
||||
table_widget->horizontalHeader()->setStretchLastSection(true); |
||||
main_layout->addWidget(table_widget); |
||||
|
||||
QObject::connect(parser, &Parser::updated, this, &MessagesWidget::updateState); |
||||
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); |
||||
}); |
||||
|
||||
// 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; |
||||
}; |
||||
|
||||
table_widget->setRowCount(parser->can_msgs.size()); |
||||
int i = 0; |
||||
QString name, untitled = tr("untitled"); |
||||
const QString filter_str = filter->text(); |
||||
for (const auto &[_, c] : parser->can_msgs) { |
||||
if (auto msg = parser->getMsg(c.address)) { |
||||
name = msg->name.c_str(); |
||||
} else { |
||||
name = untitled; |
||||
} |
||||
if (!filter_str.isEmpty() && !name.contains(filter_str, Qt::CaseInsensitive)) { |
||||
table_widget->hideRow(i++); |
||||
continue; |
||||
} |
||||
|
||||
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++; |
||||
} |
||||
if (table_widget->currentRow() == -1) { |
||||
table_widget->selectRow(0); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue