diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index d9156f229a..7d5129fc16 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -28,7 +28,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "asset prev_moc_path = cabana_env['QT_MOCHPREFIX'] cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_' -cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', +cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'commands.cc', 'messageswidget.cc', 'route.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index 0a4f6fc999..84225bb96c 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -11,7 +11,7 @@ #include #include "tools/cabana/commands.h" -#include "tools/cabana/signaledit.h" +#include "tools/cabana/signalview.h" // BinaryView diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc index ef8524be37..24415c6f95 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chartswidget.cc @@ -189,7 +189,7 @@ void ChartsWidget::setMaxChartRange(int value) { void ChartsWidget::updateToolBar() { title_label->setText(tr("Charts: %1").arg(charts.size())); columns_action->setText(tr("Column: %1").arg(column_count)); - range_lb->setText(QString("Range: %1:%2 ").arg(max_chart_range / 60, 2, 10, QLatin1Char('0')).arg(max_chart_range % 60, 2, 10, QLatin1Char('0'))); + range_lb->setText(QString("Range: %1 ").arg(utils::formatSeconds(max_chart_range))); range_lb_action->setVisible(!is_zoomed); range_slider_action->setVisible(!is_zoomed); undo_zoom_action->setVisible(is_zoomed); @@ -528,7 +528,7 @@ void ChartView::updatePlot(double cur, double min, double max) { updateAxisY(); updateSeriesPoints(); } - scene()->invalidate({}, QGraphicsScene::ForegroundLayer); + update(); } void ChartView::updateSeriesPoints() { @@ -536,14 +536,16 @@ void ChartView::updateSeriesPoints() { for (auto &s : sigs) { auto begin = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); auto end = std::lower_bound(begin, s.vals.end(), axis_x->max(), xLessThan); - - int num_points = std::max(end - begin, 1); - int pixels_per_point = width() / num_points; - - if (series_type == SeriesType::Scatter) { - ((QScatterSeries *)s.series)->setMarkerSize(std::clamp(pixels_per_point / 3, 2, 8) * devicePixelRatioF()); - } else { - s.series->setPointsVisible(pixels_per_point > 20); + if (begin != end) { + int num_points = std::max((end - begin), 1); + QPointF right_pt = end == s.vals.end() ? s.vals.back() : *end; + double pixels_per_point = (chart()->mapToPosition(right_pt).x() - chart()->mapToPosition(*begin).x()) / num_points; + + if (series_type == SeriesType::Scatter) { + ((QScatterSeries *)s.series)->setMarkerSize(std::clamp(pixels_per_point / 2.0, 2.0, 8.0) * devicePixelRatioF()); + } else { + s.series->setPointsVisible(pixels_per_point > 20); + } } } } @@ -715,7 +717,7 @@ void ChartView::mouseReleaseEvent(QMouseEvent *event) { // zoom in if selected range is greater than 0.5s emit zoomIn(min_rounded, max_rounded); } else { - scene()->invalidate({}, QGraphicsScene::ForegroundLayer); + update(); } event->accept(); } else if (!can->liveStreaming() && event->button() == Qt::RightButton) { @@ -773,7 +775,7 @@ void ChartView::mouseMoveEvent(QMouseEvent *ev) { text_list.push_front(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3)); QPointF tooltip_pt(x + 12, plot_area.top() - 20); QToolTip::showText(mapToGlobal(tooltip_pt.toPoint()), text_list.join("
"), this, plot_area.toRect()); - scene()->invalidate({}, QGraphicsScene::ForegroundLayer); + update(); } else { QToolTip::hideText(); } @@ -786,7 +788,7 @@ void ChartView::mouseMoveEvent(QMouseEvent *ev) { if (rubber_rect != rubber->geometry()) { rubber->setGeometry(rubber_rect); } - scene()->invalidate({}, QGraphicsScene::ForegroundLayer); + update(); } } @@ -847,8 +849,8 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { if (s.series->useOpenGL() && s.series->isVisible() && s.series->pointsVisible()) { auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan); + painter->setBrush(s.series->color()); for (auto it = first; it != last; ++it) { - painter->setBrush(s.series->color()); painter->drawEllipse(chart()->mapToPosition(*it), 4, 4); } } diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index cc5bffd534..2794d41b99 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -8,7 +8,7 @@ #include "tools/cabana/binaryview.h" #include "tools/cabana/chartswidget.h" #include "tools/cabana/historylog.h" -#include "tools/cabana/signaledit.h" +#include "tools/cabana/signalview.h" class EditMessageDialog : public QDialog { public: diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index 00bbb94de5..9e829708b1 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -27,6 +27,7 @@ void Settings::save() { s.setValue("recent_files", recent_files); s.setValue("message_header_state", message_header_state); s.setValue("chart_series_type", chart_series_type); + s.setValue("sparkline_range", sparkline_range); } void Settings::load() { @@ -44,6 +45,7 @@ void Settings::load() { recent_files = s.value("recent_files").toStringList(); message_header_state = s.value("message_header_state").toByteArray(); chart_series_type = s.value("chart_series_type", 0).toInt(); + sparkline_range = s.value("sparkline_range", 15).toInt(); } // SettingsDlg diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index c3ba906576..a8b6d189a5 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -17,8 +17,9 @@ public: int max_cached_minutes = 30; int chart_height = 200; int chart_column_count = 1; - int chart_range = 3 * 60; // e minutes + int chart_range = 3 * 60; // 3 minutes int chart_series_type = 0; + int sparkline_range = 15; // 15 seconds QString last_dir; QString last_route_dir; QByteArray geometry; diff --git a/tools/cabana/signaledit.cc b/tools/cabana/signalview.cc similarity index 94% rename from tools/cabana/signaledit.cc rename to tools/cabana/signalview.cc index a2455a0317..3f8a38825a 100644 --- a/tools/cabana/signaledit.cc +++ b/tools/cabana/signalview.cc @@ -1,4 +1,4 @@ -#include "tools/cabana/signaledit.h" +#include "tools/cabana/signalview.h" #include #include @@ -366,17 +366,15 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op void SignalItemDelegate::drawSparkline(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { static std::vector points; - // TODO: get seconds from settings. - const uint64_t chart_seconds = 15; // seconds const auto &msg_id = ((SignalView *)parent())->msg_id; const auto &msgs = can->events().at(msg_id); uint64_t ts = (can->lastMessage(msg_id).ts + can->routeStartTime()) * 1e9; - auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), CanEvent{.mono_time = (uint64_t)std::max(ts - chart_seconds * 1e9, 0)}); - if (first != msgs.cend()) { + auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), CanEvent{.mono_time = (uint64_t)std::max(ts - settings.sparkline_range * 1e9, 0)}); + auto last = std::upper_bound(first, msgs.cend(), CanEvent{.mono_time = ts}); + if (first != last) { double min = std::numeric_limits::max(); double max = std::numeric_limits::lowest(); const auto sig = ((SignalModel::Item *)index.internalPointer())->sig; - auto last = std::lower_bound(first, msgs.cend(), CanEvent{.mono_time = ts}); points.clear(); for (auto it = first; it != last; ++it) { double value = get_raw_value(it->dat, it->size, *sig); @@ -391,7 +389,7 @@ void SignalItemDelegate::drawSparkline(QPainter *painter, const QStyleOptionView int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin); int v_margin = std::max(option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 2, 4); - const double xscale = (option.rect.width() - 175.0 * option.widget->devicePixelRatioF() - h_margin * 2) / chart_seconds; + const double xscale = (option.rect.width() - 175.0 * option.widget->devicePixelRatioF() - h_margin * 2) / settings.sparkline_range; const double yscale = (option.rect.height() - v_margin * 2) / (max - min); const int left = option.rect.left(); const int top = option.rect.top() + v_margin; @@ -401,6 +399,13 @@ void SignalItemDelegate::drawSparkline(QPainter *painter, const QStyleOptionView } painter->setPen(getColor(sig)); painter->drawPolyline(points.data(), points.size()); + if ((points.back().x() - points.front().x()) / points.size() > 10) { + painter->setPen(Qt::NoPen); + painter->setBrush(getColor(sig)); + for (const auto &pt : points) { + painter->drawEllipse(pt, 2, 2); + } + } } } @@ -451,6 +456,17 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), filter_edit->setPlaceholderText(tr("filter signals")); hl->addWidget(filter_edit); hl->addStretch(1); + + // WARNING: increasing the maximum range can result in severe performance degradation. + // 30s is a reasonable value at present. + const int max_range = 30; // 30s + settings.sparkline_range = std::clamp(settings.sparkline_range, 1, max_range); + hl->addWidget(sparkline_label = new QLabel()); + hl->addWidget(sparkline_range_slider = new QSlider(Qt::Horizontal, this)); + sparkline_range_slider->setRange(1, max_range); + sparkline_range_slider->setValue(settings.sparkline_range); + sparkline_range_slider->setToolTip(tr("Sparkline time range")); + auto collapse_btn = toolButton("dash-square", tr("Collapse All")); collapse_btn->setIconSize({12, 12}); hl->addWidget(collapse_btn); @@ -473,8 +489,10 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), main_layout->setSpacing(0); main_layout->addWidget(title_bar); main_layout->addWidget(tree); + updateToolBar(); QObject::connect(filter_edit, &QLineEdit::textEdited, model, &SignalModel::setFilter); + QObject::connect(sparkline_range_slider, &QSlider::valueChanged, this, &SignalView::setSparklineRange); QObject::connect(collapse_btn, &QPushButton::clicked, tree, &QTreeView::collapseAll); QObject::connect(tree, &QAbstractItemView::clicked, this, &SignalView::rowClicked); QObject::connect(tree, &QTreeView::viewportEntered, [this]() { emit highlight(nullptr); }); @@ -521,7 +539,7 @@ void SignalView::rowsChanged() { }); } } - signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount())); + updateToolBar(); updateChartState(); } @@ -569,6 +587,17 @@ void SignalView::signalHovered(const cabana::Signal *sig) { } } +void SignalView::updateToolBar() { + signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount())); + sparkline_label->setText(QString("Range: %1 ").arg(utils::formatSeconds(settings.sparkline_range))); +} + +void SignalView::setSparklineRange(int value) { + settings.sparkline_range = value; + updateToolBar(); + model->updateState(nullptr); +} + void SignalView::leaveEvent(QEvent *event) { emit highlight(nullptr); QWidget::leaveEvent(event); diff --git a/tools/cabana/signaledit.h b/tools/cabana/signalview.h similarity index 96% rename from tools/cabana/signaledit.h rename to tools/cabana/signalview.h index 163edcad69..50ba4e06b8 100644 --- a/tools/cabana/signaledit.h +++ b/tools/cabana/signalview.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,8 @@ signals: private: void rowsChanged(); void leaveEvent(QEvent *event); + void updateToolBar(); + void setSparklineRange(int value); struct TreeView : public QTreeView { TreeView(QWidget *parent) : QTreeView(parent) {} @@ -120,6 +123,8 @@ private: }; TreeView *tree; + QLabel *sparkline_label; + QSlider *sparkline_range_slider; QLineEdit *filter_edit; ChartsWidget *charts; QLabel *signal_count_lb; diff --git a/tools/cabana/util.h b/tools/cabana/util.h index 7f40c42352..e90a838af8 100644 --- a/tools/cabana/util.h +++ b/tools/cabana/util.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -97,6 +98,9 @@ public: namespace utils { QPixmap icon(const QString &id); +inline QString formatSeconds(int seconds) { + return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh:mm:ss" : "mm:ss"); +} } QToolButton *toolButton(const QString &icon, const QString &tooltip); diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 0e9714994e..fe9e00bc45 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -1,7 +1,6 @@ #include "tools/cabana/videowidget.h" #include -#include #include #include #include @@ -20,10 +19,6 @@ static const QColor timeline_colors[] = { [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), }; -static inline QString formatTime(int seconds) { - return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh:mm:ss" : "mm:ss"); -} - VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { setFrameStyle(QFrame::StyledPanel | QFrame::Plain); auto main_layout = new QVBoxLayout(this); @@ -101,11 +96,11 @@ QWidget *VideoWidget::createCameraWidget() { slider_layout->addWidget(end_time_label); l->addLayout(slider_layout); 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(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(utils::formatSeconds(value / 1000)); }); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(can, &AbstractStream::updated, this, &VideoWidget::updateState); QObject::connect(can, &AbstractStream::streamStarted, [this]() { - end_time_label->setText(formatTime(can->totalSeconds())); + end_time_label->setText(utils::formatSeconds(can->totalSeconds())); slider->setRange(0, can->totalSeconds() * 1000); }); return w; @@ -116,7 +111,7 @@ void VideoWidget::rangeChanged(double min, double max, bool is_zoomed) { min = 0; max = can->totalSeconds(); } - end_time_label->setText(formatTime(max)); + end_time_label->setText(utils::formatSeconds(max)); slider->setRange(min * 1000, max * 1000); } @@ -230,7 +225,7 @@ void Slider::mouseMoveEvent(QMouseEvent *e) { } int x = std::clamp(e->pos().x() - thumb.width() / 2, THUMBNAIL_MARGIN, rect().right() - thumb.width() - THUMBNAIL_MARGIN); int y = -thumb.height() - THUMBNAIL_MARGIN - style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); - thumbnail_label.showPixmap(mapToGlobal({x, y}), formatTime(seconds), thumb); + thumbnail_label.showPixmap(mapToGlobal({x, y}), utils::formatSeconds(seconds), thumb); QSlider::mouseMoveEvent(e); }