diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index e8406192d29eac..ec39ac470cc46c 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -248,6 +248,7 @@ std::tuple BinaryView::getSelection(QModelIndex index) { void BinaryViewModel::refresh() { beginResetModel(); + bit_flip_tracker = {}; items.clear(); if (auto dbc_msg = dbc()->msg(msg_id)) { row_count = dbc_msg->size; @@ -292,11 +293,6 @@ void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &co } } -// TODO: -// 1. Detect instability through frequent bit flips and highlight stable bits to indicate steady signals. -// 2. Track message sequence and timestamps to understand how patterns evolve. -// 3. Identify time-based or periodic bit state changes to spot recurring patterns. -// 4. Support multiple time windows for short-term and long-term analysis, helping to observe changes in different time frames. void BinaryViewModel::updateState() { const auto &last_msg = can->lastMessage(msg_id); const auto &binary = last_msg.dat; @@ -308,10 +304,11 @@ void BinaryViewModel::updateState() { endInsertRows(); } + auto &bit_flips = heatmap_live_mode ? last_msg.bit_flip_counts : getBitFlipChanges(binary.size()); // Find the maximum bit flip count across the message uint32_t max_bit_flip_count = 1; // Default to 1 to avoid division by zero - for (const auto &row : last_msg.bit_flip_counts) { - for (auto count : row) { + for (const auto &row : bit_flips) { + for (uint32_t count : row) { max_bit_flip_count = std::max(max_bit_flip_count, count); } } @@ -328,7 +325,7 @@ void BinaryViewModel::updateState() { int bit_val = (binary[i] >> (7 - j)) & 1; double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal; - uint32_t flip_count = last_msg.bit_flip_counts[i][j]; + uint32_t flip_count = bit_flips[i][j]; if (flip_count > 0) { double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler; double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal; @@ -343,6 +340,38 @@ void BinaryViewModel::updateState() { } } +const std::vector> &BinaryViewModel::getBitFlipChanges(size_t msg_size) { + // Return cached results if time range and data are unchanged + auto time_range = can->timeRange(); + if (bit_flip_tracker.time_range == time_range && !bit_flip_tracker.flip_counts.empty()) + return bit_flip_tracker.flip_counts; + + bit_flip_tracker.time_range = time_range; + bit_flip_tracker.flip_counts.assign(msg_size, std::array{}); + + // Iterate over events within the specified time range and calculate bit flips + auto [first, last] = can->eventsInRange(msg_id, time_range); + if (std::distance(first, last) <= 1) return bit_flip_tracker.flip_counts; + + std::vector prev_values((*first)->dat, (*first)->dat + (*first)->size); + for (auto it = std::next(first); it != last; ++it) { + const CanEvent *event = *it; + int size = std::min(msg_size, event->size); + for (int i = 0; i < size; ++i) { + const uint8_t diff = event->dat[i] ^ prev_values[i]; + if (!diff) continue; + + auto &bit_flips = bit_flip_tracker.flip_counts[i]; + for (int bit = 0; bit < 8; ++bit) { + if (diff & (1u << bit)) ++bit_flips[7 - bit]; + } + prev_values[i] = event->dat[i]; + } + } + + return bit_flip_tracker.flip_counts; +} + QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Vertical) { switch (role) { diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h index 584910dc8304a7..3802bf14d42572 100644 --- a/tools/cabana/binaryview.h +++ b/tools/cabana/binaryview.h @@ -39,6 +39,12 @@ class BinaryViewModel : public QAbstractTableModel { Qt::ItemFlags flags(const QModelIndex &index) const override { return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable; } + const std::vector> &getBitFlipChanges(size_t msg_size); + + struct BitFlipTracker { + std::optional> time_range; + std::vector> flip_counts; + } bit_flip_tracker; struct Item { QColor bg_color = QColor(102, 86, 169, 255); @@ -49,7 +55,7 @@ class BinaryViewModel : public QAbstractTableModel { bool valid = false; }; std::vector items; - + bool heatmap_live_mode = true; MessageId msg_id; int row_count = 0; const int column_count = 9; @@ -65,6 +71,7 @@ class BinaryView : public QTableView { QSet getOverlappingSignals() const; inline void updateState() { model->updateState(); } QSize minimumSizeHint() const override; + void setHeatmapLiveMode(bool live) { model->heatmap_live_mode = live; updateState(); } signals: void signalClicked(const cabana::Signal *sig); diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 094fe96182777c..09f86a095a6c42 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -5,15 +5,9 @@ #include void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { - const auto &msgs = can->events(msg_id); - - auto range_start = can->toMonoTime(last_msg_ts - range); - auto range_end = can->toMonoTime(last_msg_ts); - auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), range_start, CompareCanEvent()); - auto last = std::upper_bound(first, msgs.cend(), range_end, CompareCanEvent()); - points.clear(); double value = 0; + auto [first, last] = can->eventsInRange(msg_id, std::make_pair(last_msg_ts -range, last_msg_ts)); for (auto it = first; it != last; ++it) { if (sig->getValue((*it)->dat, (*it)->size, &value)) { points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value); diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 7befadb7227169..ae9d12e8622be1 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -2,7 +2,8 @@ #include #include -#include +#include +#include #include "tools/cabana/commands.h" #include "tools/cabana/mainwin.h" @@ -20,19 +21,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart tabbar->setContextMenuPolicy(Qt::CustomContextMenu); main_layout->addWidget(tabbar); - // message title - QHBoxLayout *title_layout = new QHBoxLayout(); - title_layout->setContentsMargins(3, 6, 3, 0); - auto spacer = new QSpacerItem(0, 1); - title_layout->addItem(spacer); - title_layout->addWidget(name_label = new ElidedLabel(this), 1); - name_label->setStyleSheet("QLabel{font-weight:bold;}"); - name_label->setAlignment(Qt::AlignCenter); - auto edit_btn = new ToolButton("pencil", tr("Edit Message")); - title_layout->addWidget(edit_btn); - title_layout->addWidget(remove_btn = new ToolButton("x-lg", tr("Remove Message"))); - spacer->changeSize(edit_btn->sizeHint().width() * 2 + 9, 1); - main_layout->addLayout(title_layout); + createToolBar(); // warning warning_widget = new QWidget(this); @@ -58,8 +47,6 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart tab_widget->addTab(history_log = new LogsWidget(this), utils::icon("stopwatch"), "&Logs"); main_layout->addWidget(tab_widget); - QObject::connect(edit_btn, &QToolButton::clicked, this, &DetailWidget::editMsg); - QObject::connect(remove_btn, &QToolButton::clicked, this, &DetailWidget::removeMsg); QObject::connect(binary_view, &BinaryView::signalHovered, signal_view, &SignalView::signalHovered); QObject::connect(binary_view, &BinaryView::signalClicked, [this](const cabana::Signal *s) { signal_view->selectSignal(s, true); }); QObject::connect(binary_view, &BinaryView::editSignal, signal_view->model, &SignalModel::saveSignal); @@ -80,6 +67,41 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(charts, &ChartsWidget::seriesChanged, signal_view, &SignalView::updateChartState); } +void DetailWidget::createToolBar() { + QToolBar *toolbar = new QToolBar(this); + int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); + toolbar->setIconSize({icon_size, icon_size}); + toolbar->addWidget(name_label = new ElidedLabel(this)); + name_label->setStyleSheet("QLabel{font-weight:bold;}"); + + QWidget *spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolbar->addWidget(spacer); + +// Heatmap label and radio buttons + toolbar->addWidget(new QLabel(tr("Heatmap:"), this)); + auto *heatmap_live = new QRadioButton(tr("Live"), this); + auto *heatmap_all = new QRadioButton(tr("All"), this); + heatmap_live->setChecked(true); + + toolbar->addWidget(heatmap_live); + toolbar->addWidget(heatmap_all); + + // Edit and remove buttons + toolbar->addSeparator(); + toolbar->addAction(utils::icon("pencil"), tr("Edit Message"), this, &DetailWidget::editMsg); + action_remove_msg = toolbar->addAction(utils::icon("x-lg"), tr("Remove Message"), this, &DetailWidget::removeMsg); + + layout()->addWidget(toolbar); + + connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); }); + connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional> &range) { + auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All"; + heatmap_all->setText(text); + (range ? heatmap_all : heatmap_live)->setChecked(true); + }); +} + void DetailWidget::showTabBarContextMenu(const QPoint &pt) { int index = tabbar->tabAt(pt); if (index >= 0) { @@ -131,14 +153,11 @@ void DetailWidget::refresh() { for (auto s : binary_view->getOverlappingSignals()) { warnings.push_back(tr("%1 has overlapping bits.").arg(s->name)); } - } else { - warnings.push_back(tr("Drag-Select in binary view to create new signal.")); } - QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id); name_label->setText(msg_name); name_label->setToolTip(msg_name); - remove_btn->setEnabled(msg != nullptr); + action_remove_msg->setEnabled(msg != nullptr); if (!warnings.isEmpty()) { warning_label->setText(warnings.join('\n')); diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index 15e1ee5f2f954b..47304fbdc50cf4 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -36,6 +36,7 @@ class DetailWidget : public QWidget { void refresh(); private: + void createToolBar(); void showTabBarContextMenu(const QPoint &pt); void editMsg(); void removeMsg(); @@ -47,7 +48,7 @@ class DetailWidget : public QWidget { QWidget *warning_widget; TabBar *tabbar; QTabWidget *tab_widget; - QToolButton *remove_btn; + QAction *action_remove_msg; LogsWidget *history_log; BinaryView *binary_view; SignalView *signal_view; diff --git a/tools/cabana/streams/abstractstream.cc b/tools/cabana/streams/abstractstream.cc index 73329c4dfb8e0f..fa71beb2ea696a 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/tools/cabana/streams/abstractstream.cc @@ -61,10 +61,7 @@ size_t AbstractStream::suppressHighlighted() { } cnt += last_change.suppressed; } - - for (auto &row_bit_flips : m.bit_flip_counts) { - row_bit_flips.fill(0); - } + for (auto &flip_counts : m.bit_flip_counts) flip_counts.fill(0); } return cnt; } @@ -203,6 +200,15 @@ void AbstractStream::mergeEvents(const std::vector &events) { } } +std::pair AbstractStream::eventsInRange(const MessageId &id, std::optional> time_range) const { + const auto &events = can->events(id); + if (!time_range) return {events.begin(), events.end()}; + + auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent()); + auto last = std::upper_bound(events.begin(), events.end(), can->toMonoTime(time_range->second), CompareCanEvent()); + return {first, last}; +} + namespace { enum Color { GREYISH_BLUE, CYAN, RED}; @@ -222,15 +228,7 @@ inline QColor blend(const QColor &a, const QColor &b) { // Calculate the frequency from the past one minute data double calc_freq(const MessageId &msg_id, double current_sec) { - const auto &events = can->events(msg_id); - if (events.empty()) return 0.0; - - auto current_mono_time = can->toMonoTime(current_sec); - auto start_mono_time = can->toMonoTime(current_sec - 59); - - auto first = std::lower_bound(events.begin(), events.end(), start_mono_time, CompareCanEvent()); - auto last = std::upper_bound(first, events.end(), current_mono_time, CompareCanEvent()); - + auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec)); int count = std::distance(first, last); if (count <= 1) return 0.0; @@ -251,7 +249,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in } if (dat.size() != size) { - dat.resize(size); + dat.assign(can_data, can_data + size); colors.assign(size, QColor(0, 0, 0, 0)); last_changes.resize(size); bit_flip_counts.resize(size); diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index a53c9374a0238e..2824199161e853 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -53,6 +53,7 @@ struct CompareCanEvent { }; typedef std::unordered_map> MessageEventsMap; +using CanEventIter = std::vector::const_iterator; class AbstractStream : public QObject { Q_OBJECT @@ -85,6 +86,7 @@ class AbstractStream : public QObject { inline const std::vector &allEvents() const { return all_events_; } const CanData &lastMessage(const MessageId &id) const; const std::vector &events(const MessageId &id) const; + std::pair eventsInRange(const MessageId &id, std::optional> time_range) const; size_t suppressHighlighted(); void clearSuppressed();