cabana: add live and time-window heatmap modes for enhanced signal analysis (#34296)

add live and time-window heatmap modes
pull/34299/head
Dean Lee 4 months ago committed by GitHub
parent 3363881844
commit 7ac011ca89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 45
      tools/cabana/binaryview.cc
  2. 9
      tools/cabana/binaryview.h
  3. 8
      tools/cabana/chart/sparkline.cc
  4. 59
      tools/cabana/detailwidget.cc
  5. 3
      tools/cabana/detailwidget.h
  6. 26
      tools/cabana/streams/abstractstream.cc
  7. 2
      tools/cabana/streams/abstractstream.h

@ -248,6 +248,7 @@ std::tuple<int, int, bool> BinaryView::getSelection(QModelIndex index) {
void BinaryViewModel::refresh() { void BinaryViewModel::refresh() {
beginResetModel(); beginResetModel();
bit_flip_tracker = {};
items.clear(); items.clear();
if (auto dbc_msg = dbc()->msg(msg_id)) { if (auto dbc_msg = dbc()->msg(msg_id)) {
row_count = dbc_msg->size; 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() { void BinaryViewModel::updateState() {
const auto &last_msg = can->lastMessage(msg_id); const auto &last_msg = can->lastMessage(msg_id);
const auto &binary = last_msg.dat; const auto &binary = last_msg.dat;
@ -308,10 +304,11 @@ void BinaryViewModel::updateState() {
endInsertRows(); endInsertRows();
} }
auto &bit_flips = heatmap_live_mode ? last_msg.bit_flip_counts : getBitFlipChanges(binary.size());
// Find the maximum bit flip count across the message // Find the maximum bit flip count across the message
uint32_t max_bit_flip_count = 1; // Default to 1 to avoid division by zero 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 (const auto &row : bit_flips) {
for (auto count : row) { for (uint32_t count : row) {
max_bit_flip_count = std::max(max_bit_flip_count, count); 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; int bit_val = (binary[i] >> (7 - j)) & 1;
double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal; 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) { if (flip_count > 0) {
double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler; 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; double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal;
@ -343,6 +340,38 @@ void BinaryViewModel::updateState() {
} }
} }
const std::vector<std::array<uint32_t, 8>> &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<uint32_t, 8>{});
// 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<uint8_t> 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<int>(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 { QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Vertical) { if (orientation == Qt::Vertical) {
switch (role) { switch (role) {

@ -39,6 +39,12 @@ public:
Qt::ItemFlags flags(const QModelIndex &index) const override { Qt::ItemFlags flags(const QModelIndex &index) const override {
return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable; return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable;
} }
const std::vector<std::array<uint32_t, 8>> &getBitFlipChanges(size_t msg_size);
struct BitFlipTracker {
std::optional<std::pair<double, double>> time_range;
std::vector<std::array<uint32_t, 8>> flip_counts;
} bit_flip_tracker;
struct Item { struct Item {
QColor bg_color = QColor(102, 86, 169, 255); QColor bg_color = QColor(102, 86, 169, 255);
@ -49,7 +55,7 @@ public:
bool valid = false; bool valid = false;
}; };
std::vector<Item> items; std::vector<Item> items;
bool heatmap_live_mode = true;
MessageId msg_id; MessageId msg_id;
int row_count = 0; int row_count = 0;
const int column_count = 9; const int column_count = 9;
@ -65,6 +71,7 @@ public:
QSet<const cabana::Signal*> getOverlappingSignals() const; QSet<const cabana::Signal*> getOverlappingSignals() const;
inline void updateState() { model->updateState(); } inline void updateState() { model->updateState(); }
QSize minimumSizeHint() const override; QSize minimumSizeHint() const override;
void setHeatmapLiveMode(bool live) { model->heatmap_live_mode = live; updateState(); }
signals: signals:
void signalClicked(const cabana::Signal *sig); void signalClicked(const cabana::Signal *sig);

@ -5,15 +5,9 @@
#include <QPainter> #include <QPainter>
void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { 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(); points.clear();
double value = 0; 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) { for (auto it = first; it != last; ++it) {
if (sig->getValue((*it)->dat, (*it)->size, &value)) { if (sig->getValue((*it)->dat, (*it)->size, &value)) {
points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value); points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value);

@ -2,7 +2,8 @@
#include <QFormLayout> #include <QFormLayout>
#include <QMenu> #include <QMenu>
#include <QSpacerItem> #include <QRadioButton>
#include <QToolBar>
#include "tools/cabana/commands.h" #include "tools/cabana/commands.h"
#include "tools/cabana/mainwin.h" #include "tools/cabana/mainwin.h"
@ -20,19 +21,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
tabbar->setContextMenuPolicy(Qt::CustomContextMenu); tabbar->setContextMenuPolicy(Qt::CustomContextMenu);
main_layout->addWidget(tabbar); main_layout->addWidget(tabbar);
// message title createToolBar();
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);
// warning // warning
warning_widget = new QWidget(this); 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"); tab_widget->addTab(history_log = new LogsWidget(this), utils::icon("stopwatch"), "&Logs");
main_layout->addWidget(tab_widget); 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::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::signalClicked, [this](const cabana::Signal *s) { signal_view->selectSignal(s, true); });
QObject::connect(binary_view, &BinaryView::editSignal, signal_view->model, &SignalModel::saveSignal); 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); 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<std::pair<double, double>> &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) { void DetailWidget::showTabBarContextMenu(const QPoint &pt) {
int index = tabbar->tabAt(pt); int index = tabbar->tabAt(pt);
if (index >= 0) { if (index >= 0) {
@ -131,14 +153,11 @@ void DetailWidget::refresh() {
for (auto s : binary_view->getOverlappingSignals()) { for (auto s : binary_view->getOverlappingSignals()) {
warnings.push_back(tr("%1 has overlapping bits.").arg(s->name)); 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); QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id);
name_label->setText(msg_name); name_label->setText(msg_name);
name_label->setToolTip(msg_name); name_label->setToolTip(msg_name);
remove_btn->setEnabled(msg != nullptr); action_remove_msg->setEnabled(msg != nullptr);
if (!warnings.isEmpty()) { if (!warnings.isEmpty()) {
warning_label->setText(warnings.join('\n')); warning_label->setText(warnings.join('\n'));

@ -36,6 +36,7 @@ public:
void refresh(); void refresh();
private: private:
void createToolBar();
void showTabBarContextMenu(const QPoint &pt); void showTabBarContextMenu(const QPoint &pt);
void editMsg(); void editMsg();
void removeMsg(); void removeMsg();
@ -47,7 +48,7 @@ private:
QWidget *warning_widget; QWidget *warning_widget;
TabBar *tabbar; TabBar *tabbar;
QTabWidget *tab_widget; QTabWidget *tab_widget;
QToolButton *remove_btn; QAction *action_remove_msg;
LogsWidget *history_log; LogsWidget *history_log;
BinaryView *binary_view; BinaryView *binary_view;
SignalView *signal_view; SignalView *signal_view;

@ -61,10 +61,7 @@ size_t AbstractStream::suppressHighlighted() {
} }
cnt += last_change.suppressed; cnt += last_change.suppressed;
} }
for (auto &flip_counts : m.bit_flip_counts) flip_counts.fill(0);
for (auto &row_bit_flips : m.bit_flip_counts) {
row_bit_flips.fill(0);
}
} }
return cnt; return cnt;
} }
@ -203,6 +200,15 @@ void AbstractStream::mergeEvents(const std::vector<const CanEvent *> &events) {
} }
} }
std::pair<CanEventIter, CanEventIter> AbstractStream::eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> 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 { namespace {
enum Color { GREYISH_BLUE, CYAN, RED}; 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 // Calculate the frequency from the past one minute data
double calc_freq(const MessageId &msg_id, double current_sec) { double calc_freq(const MessageId &msg_id, double current_sec) {
const auto &events = can->events(msg_id); auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec));
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());
int count = std::distance(first, last); int count = std::distance(first, last);
if (count <= 1) return 0.0; 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) { if (dat.size() != size) {
dat.resize(size); dat.assign(can_data, can_data + size);
colors.assign(size, QColor(0, 0, 0, 0)); colors.assign(size, QColor(0, 0, 0, 0));
last_changes.resize(size); last_changes.resize(size);
bit_flip_counts.resize(size); bit_flip_counts.resize(size);

@ -53,6 +53,7 @@ struct CompareCanEvent {
}; };
typedef std::unordered_map<MessageId, std::vector<const CanEvent *>> MessageEventsMap; typedef std::unordered_map<MessageId, std::vector<const CanEvent *>> MessageEventsMap;
using CanEventIter = std::vector<const CanEvent *>::const_iterator;
class AbstractStream : public QObject { class AbstractStream : public QObject {
Q_OBJECT Q_OBJECT
@ -85,6 +86,7 @@ public:
inline const std::vector<const CanEvent *> &allEvents() const { return all_events_; } inline const std::vector<const CanEvent *> &allEvents() const { return all_events_; }
const CanData &lastMessage(const MessageId &id) const; const CanData &lastMessage(const MessageId &id) const;
const std::vector<const CanEvent *> &events(const MessageId &id) const; const std::vector<const CanEvent *> &events(const MessageId &id) const;
std::pair<CanEventIter, CanEventIter> eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> time_range) const;
size_t suppressHighlighted(); size_t suppressHighlighted();
void clearSuppressed(); void clearSuppressed();

Loading…
Cancel
Save