Skip to content

Commit

Permalink
cabana: add live and time-window heatmap modes for enhanced signal an…
Browse files Browse the repository at this point in the history
…alysis (#34296)

add live and time-window heatmap modes
  • Loading branch information
deanlee authored Dec 20, 2024
1 parent 3363881 commit 7ac011c
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 51 deletions.
45 changes: 37 additions & 8 deletions tools/cabana/binaryview.cc
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ std::tuple<int, int, bool> 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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
Expand All @@ -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;
Expand All @@ -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 {
if (orientation == Qt::Vertical) {
switch (role) {
Expand Down
9 changes: 8 additions & 1 deletion tools/cabana/binaryview.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 {
QColor bg_color = QColor(102, 86, 169, 255);
Expand All @@ -49,7 +55,7 @@ class BinaryViewModel : public QAbstractTableModel {
bool valid = false;
};
std::vector<Item> items;

bool heatmap_live_mode = true;
MessageId msg_id;
int row_count = 0;
const int column_count = 9;
Expand All @@ -65,6 +71,7 @@ class BinaryView : public QTableView {
QSet<const cabana::Signal*> 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);
Expand Down
8 changes: 1 addition & 7 deletions tools/cabana/chart/sparkline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@
#include <QPainter>

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);
Expand Down
59 changes: 39 additions & 20 deletions tools/cabana/detailwidget.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

#include <QFormLayout>
#include <QMenu>
#include <QSpacerItem>
#include <QRadioButton>
#include <QToolBar>

#include "tools/cabana/commands.h"
#include "tools/cabana/mainwin.h"
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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<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) {
int index = tabbar->tabAt(pt);
if (index >= 0) {
Expand Down Expand Up @@ -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'));
Expand Down
3 changes: 2 additions & 1 deletion tools/cabana/detailwidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class DetailWidget : public QWidget {
void refresh();

private:
void createToolBar();
void showTabBarContextMenu(const QPoint &pt);
void editMsg();
void removeMsg();
Expand All @@ -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;
Expand Down
26 changes: 12 additions & 14 deletions tools/cabana/streams/abstractstream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {

enum Color { GREYISH_BLUE, CYAN, RED};
Expand All @@ -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;

Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions tools/cabana/streams/abstractstream.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct CompareCanEvent {
};

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

class AbstractStream : public QObject {
Q_OBJECT
Expand Down Expand Up @@ -85,6 +86,7 @@ class AbstractStream : public QObject {
inline const std::vector<const CanEvent *> &allEvents() const { return all_events_; }
const CanData &lastMessage(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();
void clearSuppressed();
Expand Down

0 comments on commit 7ac011c

Please sign in to comment.