Skip to content

Commit 7ac011c

Browse files
authored
cabana: add live and time-window heatmap modes for enhanced signal analysis (#34296)
add live and time-window heatmap modes
1 parent 3363881 commit 7ac011c

File tree

7 files changed

+101
-51
lines changed

7 files changed

+101
-51
lines changed

tools/cabana/binaryview.cc

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ std::tuple<int, int, bool> BinaryView::getSelection(QModelIndex index) {
248248

249249
void BinaryViewModel::refresh() {
250250
beginResetModel();
251+
bit_flip_tracker = {};
251252
items.clear();
252253
if (auto dbc_msg = dbc()->msg(msg_id)) {
253254
row_count = dbc_msg->size;
@@ -292,11 +293,6 @@ void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &co
292293
}
293294
}
294295

295-
// TODO:
296-
// 1. Detect instability through frequent bit flips and highlight stable bits to indicate steady signals.
297-
// 2. Track message sequence and timestamps to understand how patterns evolve.
298-
// 3. Identify time-based or periodic bit state changes to spot recurring patterns.
299-
// 4. Support multiple time windows for short-term and long-term analysis, helping to observe changes in different time frames.
300296
void BinaryViewModel::updateState() {
301297
const auto &last_msg = can->lastMessage(msg_id);
302298
const auto &binary = last_msg.dat;
@@ -308,10 +304,11 @@ void BinaryViewModel::updateState() {
308304
endInsertRows();
309305
}
310306

307+
auto &bit_flips = heatmap_live_mode ? last_msg.bit_flip_counts : getBitFlipChanges(binary.size());
311308
// Find the maximum bit flip count across the message
312309
uint32_t max_bit_flip_count = 1; // Default to 1 to avoid division by zero
313-
for (const auto &row : last_msg.bit_flip_counts) {
314-
for (auto count : row) {
310+
for (const auto &row : bit_flips) {
311+
for (uint32_t count : row) {
315312
max_bit_flip_count = std::max(max_bit_flip_count, count);
316313
}
317314
}
@@ -328,7 +325,7 @@ void BinaryViewModel::updateState() {
328325
int bit_val = (binary[i] >> (7 - j)) & 1;
329326

330327
double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal;
331-
uint32_t flip_count = last_msg.bit_flip_counts[i][j];
328+
uint32_t flip_count = bit_flips[i][j];
332329
if (flip_count > 0) {
333330
double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler;
334331
double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal;
@@ -343,6 +340,38 @@ void BinaryViewModel::updateState() {
343340
}
344341
}
345342

343+
const std::vector<std::array<uint32_t, 8>> &BinaryViewModel::getBitFlipChanges(size_t msg_size) {
344+
// Return cached results if time range and data are unchanged
345+
auto time_range = can->timeRange();
346+
if (bit_flip_tracker.time_range == time_range && !bit_flip_tracker.flip_counts.empty())
347+
return bit_flip_tracker.flip_counts;
348+
349+
bit_flip_tracker.time_range = time_range;
350+
bit_flip_tracker.flip_counts.assign(msg_size, std::array<uint32_t, 8>{});
351+
352+
// Iterate over events within the specified time range and calculate bit flips
353+
auto [first, last] = can->eventsInRange(msg_id, time_range);
354+
if (std::distance(first, last) <= 1) return bit_flip_tracker.flip_counts;
355+
356+
std::vector<uint8_t> prev_values((*first)->dat, (*first)->dat + (*first)->size);
357+
for (auto it = std::next(first); it != last; ++it) {
358+
const CanEvent *event = *it;
359+
int size = std::min<int>(msg_size, event->size);
360+
for (int i = 0; i < size; ++i) {
361+
const uint8_t diff = event->dat[i] ^ prev_values[i];
362+
if (!diff) continue;
363+
364+
auto &bit_flips = bit_flip_tracker.flip_counts[i];
365+
for (int bit = 0; bit < 8; ++bit) {
366+
if (diff & (1u << bit)) ++bit_flips[7 - bit];
367+
}
368+
prev_values[i] = event->dat[i];
369+
}
370+
}
371+
372+
return bit_flip_tracker.flip_counts;
373+
}
374+
346375
QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const {
347376
if (orientation == Qt::Vertical) {
348377
switch (role) {

tools/cabana/binaryview.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ class BinaryViewModel : public QAbstractTableModel {
3939
Qt::ItemFlags flags(const QModelIndex &index) const override {
4040
return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable;
4141
}
42+
const std::vector<std::array<uint32_t, 8>> &getBitFlipChanges(size_t msg_size);
43+
44+
struct BitFlipTracker {
45+
std::optional<std::pair<double, double>> time_range;
46+
std::vector<std::array<uint32_t, 8>> flip_counts;
47+
} bit_flip_tracker;
4248

4349
struct Item {
4450
QColor bg_color = QColor(102, 86, 169, 255);
@@ -49,7 +55,7 @@ class BinaryViewModel : public QAbstractTableModel {
4955
bool valid = false;
5056
};
5157
std::vector<Item> items;
52-
58+
bool heatmap_live_mode = true;
5359
MessageId msg_id;
5460
int row_count = 0;
5561
const int column_count = 9;
@@ -65,6 +71,7 @@ class BinaryView : public QTableView {
6571
QSet<const cabana::Signal*> getOverlappingSignals() const;
6672
inline void updateState() { model->updateState(); }
6773
QSize minimumSizeHint() const override;
74+
void setHeatmapLiveMode(bool live) { model->heatmap_live_mode = live; updateState(); }
6875

6976
signals:
7077
void signalClicked(const cabana::Signal *sig);

tools/cabana/chart/sparkline.cc

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,9 @@
55
#include <QPainter>
66

77
void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) {
8-
const auto &msgs = can->events(msg_id);
9-
10-
auto range_start = can->toMonoTime(last_msg_ts - range);
11-
auto range_end = can->toMonoTime(last_msg_ts);
12-
auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), range_start, CompareCanEvent());
13-
auto last = std::upper_bound(first, msgs.cend(), range_end, CompareCanEvent());
14-
158
points.clear();
169
double value = 0;
10+
auto [first, last] = can->eventsInRange(msg_id, std::make_pair(last_msg_ts -range, last_msg_ts));
1711
for (auto it = first; it != last; ++it) {
1812
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
1913
points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value);

tools/cabana/detailwidget.cc

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
#include <QFormLayout>
44
#include <QMenu>
5-
#include <QSpacerItem>
5+
#include <QRadioButton>
6+
#include <QToolBar>
67

78
#include "tools/cabana/commands.h"
89
#include "tools/cabana/mainwin.h"
@@ -20,19 +21,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
2021
tabbar->setContextMenuPolicy(Qt::CustomContextMenu);
2122
main_layout->addWidget(tabbar);
2223

23-
// message title
24-
QHBoxLayout *title_layout = new QHBoxLayout();
25-
title_layout->setContentsMargins(3, 6, 3, 0);
26-
auto spacer = new QSpacerItem(0, 1);
27-
title_layout->addItem(spacer);
28-
title_layout->addWidget(name_label = new ElidedLabel(this), 1);
29-
name_label->setStyleSheet("QLabel{font-weight:bold;}");
30-
name_label->setAlignment(Qt::AlignCenter);
31-
auto edit_btn = new ToolButton("pencil", tr("Edit Message"));
32-
title_layout->addWidget(edit_btn);
33-
title_layout->addWidget(remove_btn = new ToolButton("x-lg", tr("Remove Message")));
34-
spacer->changeSize(edit_btn->sizeHint().width() * 2 + 9, 1);
35-
main_layout->addLayout(title_layout);
24+
createToolBar();
3625

3726
// warning
3827
warning_widget = new QWidget(this);
@@ -58,8 +47,6 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
5847
tab_widget->addTab(history_log = new LogsWidget(this), utils::icon("stopwatch"), "&Logs");
5948
main_layout->addWidget(tab_widget);
6049

61-
QObject::connect(edit_btn, &QToolButton::clicked, this, &DetailWidget::editMsg);
62-
QObject::connect(remove_btn, &QToolButton::clicked, this, &DetailWidget::removeMsg);
6350
QObject::connect(binary_view, &BinaryView::signalHovered, signal_view, &SignalView::signalHovered);
6451
QObject::connect(binary_view, &BinaryView::signalClicked, [this](const cabana::Signal *s) { signal_view->selectSignal(s, true); });
6552
QObject::connect(binary_view, &BinaryView::editSignal, signal_view->model, &SignalModel::saveSignal);
@@ -80,6 +67,41 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
8067
QObject::connect(charts, &ChartsWidget::seriesChanged, signal_view, &SignalView::updateChartState);
8168
}
8269

70+
void DetailWidget::createToolBar() {
71+
QToolBar *toolbar = new QToolBar(this);
72+
int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize);
73+
toolbar->setIconSize({icon_size, icon_size});
74+
toolbar->addWidget(name_label = new ElidedLabel(this));
75+
name_label->setStyleSheet("QLabel{font-weight:bold;}");
76+
77+
QWidget *spacer = new QWidget();
78+
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
79+
toolbar->addWidget(spacer);
80+
81+
// Heatmap label and radio buttons
82+
toolbar->addWidget(new QLabel(tr("Heatmap:"), this));
83+
auto *heatmap_live = new QRadioButton(tr("Live"), this);
84+
auto *heatmap_all = new QRadioButton(tr("All"), this);
85+
heatmap_live->setChecked(true);
86+
87+
toolbar->addWidget(heatmap_live);
88+
toolbar->addWidget(heatmap_all);
89+
90+
// Edit and remove buttons
91+
toolbar->addSeparator();
92+
toolbar->addAction(utils::icon("pencil"), tr("Edit Message"), this, &DetailWidget::editMsg);
93+
action_remove_msg = toolbar->addAction(utils::icon("x-lg"), tr("Remove Message"), this, &DetailWidget::removeMsg);
94+
95+
layout()->addWidget(toolbar);
96+
97+
connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); });
98+
connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional<std::pair<double, double>> &range) {
99+
auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All";
100+
heatmap_all->setText(text);
101+
(range ? heatmap_all : heatmap_live)->setChecked(true);
102+
});
103+
}
104+
83105
void DetailWidget::showTabBarContextMenu(const QPoint &pt) {
84106
int index = tabbar->tabAt(pt);
85107
if (index >= 0) {
@@ -131,14 +153,11 @@ void DetailWidget::refresh() {
131153
for (auto s : binary_view->getOverlappingSignals()) {
132154
warnings.push_back(tr("%1 has overlapping bits.").arg(s->name));
133155
}
134-
} else {
135-
warnings.push_back(tr("Drag-Select in binary view to create new signal."));
136156
}
137-
138157
QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id);
139158
name_label->setText(msg_name);
140159
name_label->setToolTip(msg_name);
141-
remove_btn->setEnabled(msg != nullptr);
160+
action_remove_msg->setEnabled(msg != nullptr);
142161

143162
if (!warnings.isEmpty()) {
144163
warning_label->setText(warnings.join('\n'));

tools/cabana/detailwidget.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class DetailWidget : public QWidget {
3636
void refresh();
3737

3838
private:
39+
void createToolBar();
3940
void showTabBarContextMenu(const QPoint &pt);
4041
void editMsg();
4142
void removeMsg();
@@ -47,7 +48,7 @@ class DetailWidget : public QWidget {
4748
QWidget *warning_widget;
4849
TabBar *tabbar;
4950
QTabWidget *tab_widget;
50-
QToolButton *remove_btn;
51+
QAction *action_remove_msg;
5152
LogsWidget *history_log;
5253
BinaryView *binary_view;
5354
SignalView *signal_view;

tools/cabana/streams/abstractstream.cc

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,7 @@ size_t AbstractStream::suppressHighlighted() {
6161
}
6262
cnt += last_change.suppressed;
6363
}
64-
65-
for (auto &row_bit_flips : m.bit_flip_counts) {
66-
row_bit_flips.fill(0);
67-
}
64+
for (auto &flip_counts : m.bit_flip_counts) flip_counts.fill(0);
6865
}
6966
return cnt;
7067
}
@@ -203,6 +200,15 @@ void AbstractStream::mergeEvents(const std::vector<const CanEvent *> &events) {
203200
}
204201
}
205202

203+
std::pair<CanEventIter, CanEventIter> AbstractStream::eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> time_range) const {
204+
const auto &events = can->events(id);
205+
if (!time_range) return {events.begin(), events.end()};
206+
207+
auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent());
208+
auto last = std::upper_bound(events.begin(), events.end(), can->toMonoTime(time_range->second), CompareCanEvent());
209+
return {first, last};
210+
}
211+
206212
namespace {
207213

208214
enum Color { GREYISH_BLUE, CYAN, RED};
@@ -222,15 +228,7 @@ inline QColor blend(const QColor &a, const QColor &b) {
222228

223229
// Calculate the frequency from the past one minute data
224230
double calc_freq(const MessageId &msg_id, double current_sec) {
225-
const auto &events = can->events(msg_id);
226-
if (events.empty()) return 0.0;
227-
228-
auto current_mono_time = can->toMonoTime(current_sec);
229-
auto start_mono_time = can->toMonoTime(current_sec - 59);
230-
231-
auto first = std::lower_bound(events.begin(), events.end(), start_mono_time, CompareCanEvent());
232-
auto last = std::upper_bound(first, events.end(), current_mono_time, CompareCanEvent());
233-
231+
auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec));
234232
int count = std::distance(first, last);
235233
if (count <= 1) return 0.0;
236234

@@ -251,7 +249,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
251249
}
252250

253251
if (dat.size() != size) {
254-
dat.resize(size);
252+
dat.assign(can_data, can_data + size);
255253
colors.assign(size, QColor(0, 0, 0, 0));
256254
last_changes.resize(size);
257255
bit_flip_counts.resize(size);

tools/cabana/streams/abstractstream.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ struct CompareCanEvent {
5353
};
5454

5555
typedef std::unordered_map<MessageId, std::vector<const CanEvent *>> MessageEventsMap;
56+
using CanEventIter = std::vector<const CanEvent *>::const_iterator;
5657

5758
class AbstractStream : public QObject {
5859
Q_OBJECT
@@ -85,6 +86,7 @@ class AbstractStream : public QObject {
8586
inline const std::vector<const CanEvent *> &allEvents() const { return all_events_; }
8687
const CanData &lastMessage(const MessageId &id) const;
8788
const std::vector<const CanEvent *> &events(const MessageId &id) const;
89+
std::pair<CanEventIter, CanEventIter> eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> time_range) const;
8890

8991
size_t suppressHighlighted();
9092
void clearSuppressed();

0 commit comments

Comments
 (0)