mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-12 21:02:13 +08:00
cabana: add live and time-window heatmap modes for enhanced signal analysis (#34296)
add live and time-window heatmap modes
This commit is contained in:
@@ -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;
|
||||
@@ -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<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) {
|
||||
|
||||
@@ -39,6 +39,12 @@ public:
|
||||
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);
|
||||
@@ -49,7 +55,7 @@ public:
|
||||
bool valid = false;
|
||||
};
|
||||
std::vector<Item> items;
|
||||
|
||||
bool heatmap_live_mode = true;
|
||||
MessageId msg_id;
|
||||
int row_count = 0;
|
||||
const int column_count = 9;
|
||||
@@ -65,6 +71,7 @@ public:
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QMenu>
|
||||
#include <QSpacerItem>
|
||||
#include <QRadioButton>
|
||||
#include <QToolBar>
|
||||
|
||||
#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<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) {
|
||||
@@ -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'));
|
||||
|
||||
@@ -36,6 +36,7 @@ public:
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
void createToolBar();
|
||||
void showTabBarContextMenu(const QPoint &pt);
|
||||
void editMsg();
|
||||
void removeMsg();
|
||||
@@ -47,7 +48,7 @@ private:
|
||||
QWidget *warning_widget;
|
||||
TabBar *tabbar;
|
||||
QTabWidget *tab_widget;
|
||||
QToolButton *remove_btn;
|
||||
QAction *action_remove_msg;
|
||||
LogsWidget *history_log;
|
||||
BinaryView *binary_view;
|
||||
SignalView *signal_view;
|
||||
|
||||
@@ -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<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};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -85,6 +86,7 @@ public:
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user