diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index b98c75d452..b6a5fa75aa 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -177,7 +177,9 @@ void BinaryViewModel::setMessage(const QString &message_id) { void BinaryViewModel::updateState() { auto prev_items = items; - const auto &binary = can->lastMessage(msg_id).dat; + const auto &last_msg = can->lastMessage(msg_id); + const auto &binary = last_msg.dat; + // data size may changed. if (binary.size() > row_count) { beginInsertRows({}, row_count, binary.size() - 1); @@ -193,6 +195,7 @@ void BinaryViewModel::updateState() { hex[0] = toHex(binary[i] >> 4); hex[1] = toHex(binary[i] & 0xf); items[i * column_count + 8].val = hex; + items[i * column_count + 8].bg_color = last_msg.colors[i]; } for (int i = binary.size(); i < row_count; ++i) { for (int j = 0; j < column_count; ++j) { @@ -201,7 +204,7 @@ void BinaryViewModel::updateState() { } for (int i = 0; i < row_count * column_count; ++i) { - if (i >= prev_items.size() || prev_items[i].val != items[i].val) { + if (i >= prev_items.size() || prev_items[i].val != items[i].val || prev_items[i].bg_color != items[i].bg_color) { auto idx = index(i / column_count, i % column_count); emit dataChanged(idx, idx); } @@ -234,6 +237,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op if (index.column() == 8) { painter->setFont(hex_font); + painter->fillRect(option.rect, item->bg_color); } else if (option.state & QStyle::State_Selected) { painter->fillRect(option.rect, selection_color); painter->setPen(option.palette.color(QPalette::BrightText)); diff --git a/tools/cabana/canmessages.cc b/tools/cabana/canmessages.cc index a473313f90..9b5f41c60a 100644 --- a/tools/cabana/canmessages.cc +++ b/tools/cabana/canmessages.cc @@ -18,6 +18,10 @@ static bool event_filter(const Event *e, void *opaque) { return c->eventFilter(e); } +static QColor blend(QColor a, QColor b) { + return QColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2); +} + bool CANMessages::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags) { replay = new Replay(route, {"can", "roadEncodeIdx", "wideRoadEncodeIdx", "carParams"}, {}, nullptr, replay_flags, data_dir, this); replay->setSegmentCacheLimit(settings.cached_segment_limit); @@ -49,6 +53,9 @@ void CANMessages::process(QHash *messages) { bool CANMessages::eventFilter(const Event *event) { static std::unique_ptr new_msgs = std::make_unique>(); + static QHash prev_dat; + static QHash> colors; + static QHash> last_change_t; static double prev_update_ts = 0; if (event->which == cereal::Event::Which::CAN) { @@ -71,6 +78,59 @@ bool CANMessages::eventFilter(const Event *event) { if (double delta = (current_sec - counters_begin_sec); delta > 0) { data.freq = data.count / delta; } + + // Init colors + if (colors[id].size() != data.dat.size()) { + colors[id].clear(); + for (int i = 0; i < data.dat.size(); i++){ + colors[id].append(QColor(0, 0, 0, 0)); + } + } + + // Init last_change_t + if (last_change_t[id].size() != data.dat.size()) { + last_change_t[id].clear(); + for (int i = 0; i < data.dat.size(); i++){ + last_change_t[id].append(data.ts); + } + } + + // Compute background color for the bytes based on changes + if (prev_dat[id].size() == data.dat.size()) { + for (int i = 0; i < data.dat.size(); i++){ + uint8_t last = prev_dat[id][i]; + uint8_t cur = data.dat[i]; + + const int periodic_threshold = 10; + const int start_alpha = 128; + const float fade_time = 2.0; + + if (last != cur) { + double delta_t = data.ts - last_change_t[id][i]; + + if (delta_t * data.freq > periodic_threshold) { + // Last change was while ago, choose color based on delta up or down + if (cur > last) { + colors[id][i] = QColor(0, 187, 255, start_alpha); // Cyan + } else { + colors[id][i] = QColor(255, 0, 0, start_alpha); // Red + } + } else { + // Periodic changes + colors[id][i] = blend(colors[id][i], QColor(102, 86, 169, start_alpha / 2)); // Greyish/Blue + } + + last_change_t[id][i] = data.ts; + } else { + // Fade out + float alpha_delta = 1.0 / (data.freq + 1) / fade_time; + colors[id][i].setAlphaF(std::max(0.0, colors[id][i].alphaF() - alpha_delta)); + } + } + } + + data.colors = colors[id]; + prev_dat[id] = data.dat; } double ts = millis_since_boot(); diff --git a/tools/cabana/canmessages.h b/tools/cabana/canmessages.h index 2cdc010d8c..c30361af41 100644 --- a/tools/cabana/canmessages.h +++ b/tools/cabana/canmessages.h @@ -4,6 +4,7 @@ #include #include +#include #include "opendbc/can/common_dbc.h" #include "tools/cabana/settings.h" @@ -16,6 +17,7 @@ struct CanData { uint32_t count = 0; uint32_t freq = 0; QByteArray dat; + QList colors; }; class CANMessages : public QObject { diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 3477abe37b..10a9c2e849 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include "tools/cabana/dbcmanager.h" @@ -21,6 +23,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { table_widget = new QTableView(this); model = new MessageListModel(this); table_widget->setModel(model); + table_widget->setItemDelegateForColumn(4, new MessageBytesDelegate(table_widget)); table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); table_widget->setSelectionMode(QAbstractItemView::SingleSelection); table_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); @@ -31,6 +34,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { table_widget->setColumnWidth(2, 80); table_widget->horizontalHeader()->setStretchLastSection(true); table_widget->verticalHeader()->hide(); + main_layout->addWidget(table_widget); // signals/slots @@ -65,9 +69,10 @@ QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, } QVariant MessageListModel::data(const QModelIndex &index, int role) const { + const auto &id = msgs[index.row()]; + auto &can_data = can->lastMessage(id); + if (role == Qt::DisplayRole) { - const auto &id = msgs[index.row()]; - auto &can_data = can->lastMessage(id); switch (index.column()) { case 0: return msgName(id); case 1: return id; @@ -77,6 +82,13 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { } } else if (role == Qt::FontRole && index.column() == columnCount() - 1) { return QFontDatabase::systemFont(QFontDatabase::FixedFont); + } else if (role == Qt::UserRole && index.column() == 4) { + + QList colors; + for (int i = 0; i < can_data.dat.size(); i++){ + colors.append(can_data.colors[i]); + } + return colors; } return {}; } @@ -146,3 +158,40 @@ void MessageListModel::sort(int column, Qt::SortOrder order) { sortMessages(); } } + + +MessageBytesDelegate::MessageBytesDelegate(QObject *parent) : QStyledItemDelegate(parent) { +} + +void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + QList colors = index.data(Qt::UserRole).toList(); + + QStyleOptionViewItemV4 opt = option; + initStyleOption(&opt, index); + + const QFont font = index.data(Qt::FontRole).value(); + painter->setFont(font); + + QRect rect = opt.rect; + QString bytes = QString(opt.text); + + QRect pos = rect; + QRect space = painter->boundingRect(pos, opt.displayAlignment, " "); + pos.setX(pos.x() + space.width()); + + if ((option.state & QStyle::State_Selected) && (option.state & QStyle::State_Active)) { + painter->setPen(option.palette.color(QPalette::HighlightedText)); + } else { + painter->setPen(option.palette.color(QPalette::Text)); + } + + int i = 0; + for (auto &byte : bytes.split(" ")) { + QRect sz = painter->boundingRect(pos, opt.displayAlignment, byte); + const int m = space.width() / 2; + painter->fillRect(sz.marginsAdded(QMargins(m + 1, m, m, m)), colors[i].value()); + painter->drawText(pos, opt.displayAlignment, byte); + pos.setX(pos.x() + sz.width() + space.width()); + i++; + } +} diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index 62aa23c02e..8b74f1427e 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -2,6 +2,7 @@ #include #include +#include #include "tools/cabana/canmessages.h" @@ -41,3 +42,10 @@ protected: QString current_msg_id; MessageListModel *model; }; + +class MessageBytesDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + MessageBytesDelegate(QObject *parent); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; +};