mirror of https://github.com/commaai/openpilot.git
475 lines
18 KiB
C++
475 lines
18 KiB
C++
#include "tools/cabana/messageswidget.h"
|
|
|
|
#include <limits>
|
|
#include <utility>
|
|
|
|
#include <QCheckBox>
|
|
#include <QHBoxLayout>
|
|
#include <QPainter>
|
|
#include <QPalette>
|
|
#include <QPushButton>
|
|
#include <QScrollBar>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "tools/cabana/commands.h"
|
|
|
|
static bool isMessageActive(const MessageId &id) {
|
|
if (auto dummy_stream = dynamic_cast<DummyStream *>(can)) {
|
|
return true;
|
|
}
|
|
if (id.source == INVALID_SOURCE) {
|
|
return false;
|
|
}
|
|
// Check if the message is active based on time difference and frequency
|
|
const auto &m = can->lastMessage(id);
|
|
float delta = can->currentSec() - m.ts;
|
|
return (m.freq == 0 && delta < 1.5) || (m.freq > 0 && ((delta - 1.0 / settings.fps) < (5.0 / m.freq)));
|
|
}
|
|
|
|
MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) {
|
|
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
main_layout->setContentsMargins(0, 0, 0, 0);
|
|
// toolbar
|
|
main_layout->addWidget(createToolBar());
|
|
// message table
|
|
main_layout->addWidget(view = new MessageView(this));
|
|
view->setItemDelegate(delegate = new MessageBytesDelegate(view, settings.multiple_lines_hex));
|
|
view->setModel(model = new MessageListModel(this));
|
|
view->setHeader(header = new MessageViewHeader(this));
|
|
view->setSortingEnabled(true);
|
|
view->sortByColumn(MessageListModel::Column::NAME, Qt::AscendingOrder);
|
|
view->setAllColumnsShowFocus(true);
|
|
view->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
view->setItemsExpandable(false);
|
|
view->setIndentation(0);
|
|
view->setRootIsDecorated(false);
|
|
|
|
// Must be called before setting any header parameters to avoid overriding
|
|
restoreHeaderState(settings.message_header_state);
|
|
header->setSectionsMovable(true);
|
|
header->setSectionResizeMode(MessageListModel::Column::DATA, QHeaderView::Fixed);
|
|
header->setStretchLastSection(true);
|
|
header->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
// signals/slots
|
|
QObject::connect(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow);
|
|
QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent);
|
|
QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions);
|
|
QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived);
|
|
QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::dbcModified);
|
|
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &MessageListModel::dbcModified);
|
|
QObject::connect(model, &MessageListModel::modelReset, [this]() {
|
|
if (current_msg_id) {
|
|
selectMessage(*current_msg_id);
|
|
}
|
|
view->updateBytesSectionSize();
|
|
updateTitle();
|
|
});
|
|
QObject::connect(view->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) {
|
|
if (current.isValid() && current.row() < model->items_.size()) {
|
|
const auto &id = model->items_[current.row()].id;
|
|
if (!current_msg_id || id != *current_msg_id) {
|
|
current_msg_id = id;
|
|
emit msgSelectionChanged(*current_msg_id);
|
|
}
|
|
}
|
|
});
|
|
|
|
setWhatsThis(tr(R"(
|
|
<b>Message View</b><br/>
|
|
<!-- TODO: add descprition here -->
|
|
<span style="color:gray">Byte color</span><br />
|
|
<span style="color:gray;">■ </span> constant changing<br />
|
|
<span style="color:blue;">■ </span> increasing<br />
|
|
<span style="color:red;">■ </span> decreasing<br />
|
|
<span style="color:gray">Shortcuts</span><br />
|
|
Horizontal Scrolling: <span style="background-color:lightGray;color:gray"> shift+wheel </span>
|
|
)"));
|
|
}
|
|
|
|
QWidget *MessagesWidget::createToolBar() {
|
|
QWidget *toolbar = new QWidget(this);
|
|
QHBoxLayout *layout = new QHBoxLayout(toolbar);
|
|
layout->setContentsMargins(0, 9, 0, 0);
|
|
layout->addWidget(suppress_add = new QPushButton("Suppress Highlighted"));
|
|
layout->addWidget(suppress_clear = new QPushButton());
|
|
suppress_clear->setToolTip(tr("Clear suppressed"));
|
|
layout->addStretch(1);
|
|
QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Signals"), this);
|
|
suppress_defined_signals->setToolTip(tr("Suppress defined signals"));
|
|
suppress_defined_signals->setChecked(settings.suppress_defined_signals);
|
|
layout->addWidget(suppress_defined_signals);
|
|
|
|
auto view_button = new ToolButton("three-dots", tr("View..."));
|
|
view_button->setMenu(menu);
|
|
view_button->setPopupMode(QToolButton::InstantPopup);
|
|
view_button->setStyleSheet("QToolButton::menu-indicator { image: none; }");
|
|
layout->addWidget(view_button);
|
|
|
|
QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted);
|
|
QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted);
|
|
QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals);
|
|
|
|
suppressHighlighted();
|
|
return toolbar;
|
|
}
|
|
|
|
void MessagesWidget::updateTitle() {
|
|
auto stats = std::accumulate(
|
|
model->items_.begin(), model->items_.end(), std::pair<size_t, size_t>(),
|
|
[](const auto &pair, const auto &item) {
|
|
auto m = dbc()->msg(item.id);
|
|
return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair;
|
|
});
|
|
emit titleChanged(tr("%1 Messages (%2 DBC Messages, %3 Signals)")
|
|
.arg(model->items_.size()).arg(stats.first).arg(stats.second));
|
|
}
|
|
|
|
void MessagesWidget::selectMessage(const MessageId &msg_id) {
|
|
auto it = std::find_if(model->items_.cbegin(), model->items_.cend(),
|
|
[&msg_id](auto &item) { return item.id == msg_id; });
|
|
if (it != model->items_.cend()) {
|
|
view->setCurrentIndex(model->index(std::distance(model->items_.cbegin(), it), 0));
|
|
}
|
|
}
|
|
|
|
void MessagesWidget::suppressHighlighted() {
|
|
int n = sender() == suppress_add ? can->suppressHighlighted() : (can->clearSuppressed(), 0);
|
|
suppress_clear->setText(n > 0 ? tr("Clear (%1)").arg(n) : tr("Clear"));
|
|
suppress_clear->setEnabled(n > 0);
|
|
}
|
|
|
|
void MessagesWidget::headerContextMenuEvent(const QPoint &pos) {
|
|
menu->exec(header->mapToGlobal(pos));
|
|
}
|
|
|
|
void MessagesWidget::menuAboutToShow() {
|
|
menu->clear();
|
|
for (int i = 0; i < header->count(); ++i) {
|
|
int logical_index = header->logicalIndex(i);
|
|
auto action = menu->addAction(model->headerData(logical_index, Qt::Horizontal).toString(),
|
|
[=](bool checked) { header->setSectionHidden(logical_index, !checked); });
|
|
action->setCheckable(true);
|
|
action->setChecked(!header->isSectionHidden(logical_index));
|
|
// Can't hide the name column
|
|
action->setEnabled(logical_index > 0);
|
|
}
|
|
menu->addSeparator();
|
|
auto action = menu->addAction(tr("Multi-Line bytes"), this, &MessagesWidget::setMultiLineBytes);
|
|
action->setCheckable(true);
|
|
action->setChecked(settings.multiple_lines_hex);
|
|
|
|
action = menu->addAction(tr("Show inactive Messages"), model, &MessageListModel::showInactivemessages);
|
|
action->setCheckable(true);
|
|
action->setChecked(model->show_inactive_messages);
|
|
}
|
|
|
|
void MessagesWidget::setMultiLineBytes(bool multi) {
|
|
settings.multiple_lines_hex = multi;
|
|
delegate->setMultipleLines(multi);
|
|
view->updateBytesSectionSize();
|
|
view->doItemsLayout();
|
|
}
|
|
|
|
// MessageListModel
|
|
|
|
QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
|
|
switch (section) {
|
|
case Column::NAME: return tr("Name");
|
|
case Column::SOURCE: return tr("Bus");
|
|
case Column::ADDRESS: return tr("ID");
|
|
case Column::NODE: return tr("Node");
|
|
case Column::FREQ: return tr("Freq");
|
|
case Column::COUNT: return tr("Count");
|
|
case Column::DATA: return tr("Bytes");
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
|
if (!index.isValid() || index.row() >= items_.size()) return {};
|
|
|
|
auto getFreq = [](float freq) {
|
|
if (freq > 0) {
|
|
return freq >= 0.95 ? QString::number(std::nearbyint(freq)) : QString::number(freq, 'f', 2);
|
|
} else {
|
|
return QStringLiteral("--");
|
|
}
|
|
};
|
|
|
|
const static QString NA = QStringLiteral("N/A");
|
|
const auto &item = items_[index.row()];
|
|
if (role == Qt::DisplayRole) {
|
|
switch (index.column()) {
|
|
case Column::NAME: return item.name;
|
|
case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA;
|
|
case Column::ADDRESS: return QString::number(item.id.address, 16);
|
|
case Column::NODE: return item.node;
|
|
case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA;
|
|
case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA;
|
|
case Column::DATA: return item.id.source != INVALID_SOURCE ? "" : NA;
|
|
}
|
|
} else if (role == ColorsRole) {
|
|
return QVariant::fromValue((void*)(&can->lastMessage(item.id).colors));
|
|
} else if (role == BytesRole && index.column() == Column::DATA && item.id.source != INVALID_SOURCE) {
|
|
return QVariant::fromValue((void*)(&can->lastMessage(item.id).dat));
|
|
} else if (role == Qt::ForegroundRole && !item.active) {
|
|
return settings.theme == DARK_THEME ? QApplication::palette().color(QPalette::Text).darker(150) : QColor(Qt::gray);
|
|
} else if (role == Qt::ToolTipRole && index.column() == Column::NAME) {
|
|
auto msg = dbc()->msg(item.id);
|
|
auto tooltip = item.name;
|
|
if (msg && !msg->comment.isEmpty()) tooltip += "<br /><span style=\"color:gray;\">" + msg->comment + "</span>";
|
|
return tooltip;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void MessageListModel::setFilterStrings(const QMap<int, QString> &filters) {
|
|
filters_ = filters;
|
|
filterAndSort();
|
|
}
|
|
|
|
void MessageListModel::showInactivemessages(bool show) {
|
|
show_inactive_messages = show;
|
|
filterAndSort();
|
|
}
|
|
|
|
void MessageListModel::dbcModified() {
|
|
dbc_messages_.clear();
|
|
for (const auto &[_, m] : dbc()->getMessages(-1)) {
|
|
dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address});
|
|
}
|
|
filterAndSort();
|
|
}
|
|
|
|
void MessageListModel::sortItems(std::vector<MessageListModel::Item> &items) {
|
|
auto compare = [this](const auto &l, const auto &r) {
|
|
switch (sort_column) {
|
|
case Column::NAME: return std::tie(l.name, l.id) < std::tie(r.name, r.id);
|
|
case Column::SOURCE: return std::tie(l.id.source, l.id.address) < std::tie(r.id.source, r.id.address);
|
|
case Column::ADDRESS: return std::tie(l.id.address, l.id.source) < std::tie(r.id.address, r.id.source);
|
|
case Column::NODE: return std::tie(l.node, l.id) < std::tie(r.node, r.id);
|
|
case Column::FREQ: return std::tie(can->lastMessage(l.id).freq, l.id) < std::tie(can->lastMessage(r.id).freq, r.id);
|
|
case Column::COUNT: return std::tie(can->lastMessage(l.id).count, l.id) < std::tie(can->lastMessage(r.id).count, r.id);
|
|
default: return false; // Default case to suppress compiler warning
|
|
}
|
|
};
|
|
|
|
if (sort_order == Qt::DescendingOrder)
|
|
std::stable_sort(items.rbegin(), items.rend(), compare);
|
|
else
|
|
std::stable_sort(items.begin(), items.end(), compare);
|
|
}
|
|
|
|
static bool parseRange(const QString &filter, uint32_t value, int base = 10) {
|
|
// Parse out filter string into a range (e.g. "1" -> {1, 1}, "1-3" -> {1, 3}, "1-" -> {1, inf})
|
|
unsigned int min = std::numeric_limits<unsigned int>::min();
|
|
unsigned int max = std::numeric_limits<unsigned int>::max();
|
|
auto s = filter.split('-');
|
|
bool ok = s.size() >= 1 && s.size() <= 2;
|
|
if (ok && !s[0].isEmpty()) min = s[0].toUInt(&ok, base);
|
|
if (ok && s.size() == 1) {
|
|
max = min;
|
|
} else if (ok && s.size() == 2 && !s[1].isEmpty()) {
|
|
max = s[1].toUInt(&ok, base);
|
|
}
|
|
return ok && value >= min && value <= max;
|
|
}
|
|
|
|
bool MessageListModel::match(const MessageListModel::Item &item) {
|
|
if (filters_.isEmpty())
|
|
return true;
|
|
|
|
bool match = true;
|
|
const auto &data = can->lastMessage(item.id);
|
|
for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) {
|
|
const QString &txt = it.value();
|
|
switch (it.key()) {
|
|
case Column::NAME: {
|
|
match = item.name.contains(txt, Qt::CaseInsensitive);
|
|
if (!match) {
|
|
const auto m = dbc()->msg(item.id);
|
|
match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(),
|
|
[&txt](const auto &s) { return s->name.contains(txt, Qt::CaseInsensitive); });
|
|
}
|
|
break;
|
|
}
|
|
case Column::SOURCE:
|
|
match = parseRange(txt, item.id.source);
|
|
break;
|
|
case Column::ADDRESS:
|
|
match = QString::number(item.id.address, 16).contains(txt, Qt::CaseInsensitive);
|
|
match = match || parseRange(txt, item.id.address, 16);
|
|
break;
|
|
case Column::NODE:
|
|
match = item.node.contains(txt, Qt::CaseInsensitive);
|
|
break;
|
|
case Column::FREQ:
|
|
match = parseRange(txt, data.freq);
|
|
break;
|
|
case Column::COUNT:
|
|
match = parseRange(txt, data.count);
|
|
break;
|
|
case Column::DATA:
|
|
match = utils::toHex(data.dat).contains(txt, Qt::CaseInsensitive);
|
|
break;
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
bool MessageListModel::filterAndSort() {
|
|
// merge CAN and DBC messages
|
|
std::vector<MessageId> all_messages;
|
|
all_messages.reserve(can->lastMessages().size() + dbc_messages_.size());
|
|
auto dbc_msgs = dbc_messages_;
|
|
for (const auto &[id, m] : can->lastMessages()) {
|
|
all_messages.push_back(id);
|
|
dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address});
|
|
}
|
|
all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end());
|
|
|
|
// filter and sort
|
|
std::vector<Item> items;
|
|
items.reserve(all_messages.size());
|
|
for (const auto &id : all_messages) {
|
|
bool active = isMessageActive(id);
|
|
if (active || show_inactive_messages) {
|
|
auto msg = dbc()->msg(id);
|
|
Item item = {.id = id,
|
|
.active = active,
|
|
.name = msg ? msg->name : UNTITLED,
|
|
.node = msg ? msg->transmitter : QString()};
|
|
if (match(item))
|
|
items.emplace_back(item);
|
|
}
|
|
}
|
|
sortItems(items);
|
|
|
|
if (items_ != items) {
|
|
beginResetModel();
|
|
items_ = std::move(items);
|
|
endResetModel();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MessageListModel::msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids) {
|
|
if (has_new_ids || ((filters_.count(Column::FREQ) || filters_.count(Column::COUNT) || filters_.count(Column::DATA)) &&
|
|
++sort_threshold_ == settings.fps)) {
|
|
sort_threshold_ = 0;
|
|
if (filterAndSort()) return;
|
|
}
|
|
|
|
for (auto &item : items_) {
|
|
item.active = isMessageActive(item.id);
|
|
}
|
|
// Update viewport
|
|
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
|
|
}
|
|
|
|
void MessageListModel::sort(int column, Qt::SortOrder order) {
|
|
if (column != Column::DATA) {
|
|
sort_column = column;
|
|
sort_order = order;
|
|
filterAndSort();
|
|
}
|
|
}
|
|
|
|
// MessageView
|
|
|
|
void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
|
QTreeView::drawRow(painter, option, index);
|
|
|
|
QPen oldPen = painter->pen();
|
|
const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this);
|
|
painter->setPen(QColor::fromRgba(static_cast<QRgb>(gridHint)));
|
|
// Draw bottom border for the row
|
|
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
|
|
// Draw vertical borders for each column
|
|
for (int i = 0; i < header()->count(); ++i) {
|
|
int sectionX = header()->sectionViewportPosition(i);
|
|
painter->drawLine(sectionX, option.rect.top(), sectionX, option.rect.bottom());
|
|
}
|
|
painter->setPen(oldPen);
|
|
}
|
|
|
|
void MessageView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
|
|
// Bypass the slow call to QTreeView::dataChanged.
|
|
// QTreeView::dataChanged will invalidate the height cache and that's what we don't need in MessageView.
|
|
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
|
|
}
|
|
|
|
void MessageView::updateBytesSectionSize() {
|
|
auto delegate = ((MessageBytesDelegate *)itemDelegate());
|
|
int max_bytes = 8;
|
|
if (!delegate->multipleLines()) {
|
|
for (const auto &[_, m] : can->lastMessages()) {
|
|
max_bytes = std::max<int>(max_bytes, m.dat.size());
|
|
}
|
|
}
|
|
setUniformRowHeights(!delegate->multipleLines());
|
|
header()->resizeSection(MessageListModel::Column::DATA, delegate->sizeForBytes(max_bytes).width());
|
|
}
|
|
|
|
void MessageView::wheelEvent(QWheelEvent *event) {
|
|
if (event->modifiers() == Qt::ShiftModifier) {
|
|
QApplication::sendEvent(horizontalScrollBar(), event);
|
|
} else {
|
|
QTreeView::wheelEvent(event);
|
|
}
|
|
}
|
|
|
|
// MessageViewHeader
|
|
|
|
MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizontal, parent) {
|
|
QObject::connect(this, &QHeaderView::sectionResized, this, &MessageViewHeader::updateHeaderPositions);
|
|
QObject::connect(this, &QHeaderView::sectionMoved, this, &MessageViewHeader::updateHeaderPositions);
|
|
}
|
|
|
|
void MessageViewHeader::updateFilters() {
|
|
QMap<int, QString> filters;
|
|
for (int i = 0; i < count(); i++) {
|
|
if (editors[i] && !editors[i]->text().isEmpty()) {
|
|
filters[i] = editors[i]->text();
|
|
}
|
|
}
|
|
qobject_cast<MessageListModel*>(model())->setFilterStrings(filters);
|
|
}
|
|
|
|
void MessageViewHeader::updateHeaderPositions() {
|
|
QSize sz = QHeaderView::sizeHint();
|
|
for (int i = 0; i < count(); i++) {
|
|
if (editors[i]) {
|
|
int h = editors[i]->sizeHint().height();
|
|
editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h);
|
|
editors[i]->setHidden(isSectionHidden(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
void MessageViewHeader::updateGeometries() {
|
|
for (int i = 0; i < count(); i++) {
|
|
if (!editors[i]) {
|
|
QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
|
|
editors[i] = new QLineEdit(this);
|
|
editors[i]->setClearButtonEnabled(true);
|
|
editors[i]->setPlaceholderText(tr("Filter %1").arg(column_name));
|
|
|
|
QObject::connect(editors[i], &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters);
|
|
}
|
|
}
|
|
setViewportMargins(0, 0, 0, editors[0] ? editors[0]->sizeHint().height() : 0);
|
|
|
|
QHeaderView::updateGeometries();
|
|
updateHeaderPositions();
|
|
}
|
|
|
|
QSize MessageViewHeader::sizeHint() const {
|
|
QSize sz = QHeaderView::sizeHint();
|
|
return editors[0] ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz;
|
|
}
|