diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 8dbd4f1d1..fd44ecd13 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -12,5 +12,5 @@ else: qt_libs = ['qt_util', 'Qt5Charts'] + base_libs cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, opendbc,'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_libs -qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', +qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', 'canmessages.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc new file mode 100644 index 000000000..3b962b6de --- /dev/null +++ b/tools/cabana/binaryview.cc @@ -0,0 +1,184 @@ +#include "tools/cabana/binaryview.h" + +#include +#include +#include + +#include "tools/cabana/canmessages.h" + +// BinaryView + +const int CELL_HEIGHT = 35; + +BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { + model = new BinaryViewModel(this); + setModel(model); + setItemDelegate(new BinaryItemDelegate(this)); + horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + horizontalHeader()->hide(); + verticalHeader()->setSectionResizeMode(QHeaderView::Stretch); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + // replace selection model + auto old_model = selectionModel(); + setSelectionModel(new BinarySelectionModel(model)); + delete old_model; + + QObject::connect(model, &QAbstractItemModel::modelReset, [this]() { + setFixedHeight((CELL_HEIGHT + 1) * std::min(model->rowCount(), 8) + 2); + }); +} + +void BinaryView::mouseReleaseEvent(QMouseEvent *event) { + QTableView::mouseReleaseEvent(event); + + if (auto indexes = selectedIndexes(); !indexes.isEmpty()) { + int start_bit = indexes.first().row() * 8 + indexes.first().column(); + int size = indexes.back().row() * 8 + indexes.back().column() - start_bit + 1; + emit cellsSelected(start_bit, size); + } +} + +void BinaryView::setMessage(const QString &message_id) { + msg_id = message_id; + model->setMessage(message_id); + resizeRowsToContents(); + clearSelection(); + updateState(); +} + +void BinaryView::updateState() { + model->updateState(); +} + +// BinaryViewModel + +void BinaryViewModel::setMessage(const QString &message_id) { + msg_id = message_id; + + beginResetModel(); + items.clear(); + row_count = 0; + + dbc_msg = dbc()->msg(msg_id); + if (dbc_msg) { + row_count = dbc_msg->size; + items.resize(row_count * column_count); + for (int i = 0; i < dbc_msg->sigs.size(); ++i) { + const auto &sig = dbc_msg->sigs[i]; + const int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit); + const int end = start + sig.size - 1; + for (int j = start; j <= end; ++j) { + int idx = column_count * (j / 8) + j % 8; + if (idx >= items.size()) { + qWarning() << "signal " << sig.name.c_str() << "out of bounds.start_bit:" << sig.start_bit << "size:" << sig.size; + break; + } + if (j == start) { + sig.is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true; + } else if (j == end) { + sig.is_little_endian ? items[idx].is_msb = true : items[idx].is_lsb = true; + } + items[idx].bg_color = QColor(getColor(i)); + } + } + } + + endResetModel(); +} + +QModelIndex BinaryViewModel::index(int row, int column, const QModelIndex &parent) const { + return createIndex(row, column, (void *)&items[row * column_count + column]); +} + +Qt::ItemFlags BinaryViewModel::flags(const QModelIndex &index) const { + return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +void BinaryViewModel::updateState() { + auto prev_items = items; + + const auto &binary = can->lastMessage(msg_id).dat; + // data size may changed. + if (!dbc_msg && binary.size() != row_count) { + beginResetModel(); + row_count = binary.size(); + items.clear(); + items.resize(row_count * column_count); + endResetModel(); + } + + char hex[3] = {'\0'}; + for (int i = 0; i < std::min(binary.size(), row_count); ++i) { + for (int j = 0; j < column_count - 1; ++j) { + items[i * column_count + j].val = QChar((binary[i] >> (7 - j)) & 1 ? '1' : '0'); + } + hex[0] = toHex(binary[i] >> 4); + hex[1] = toHex(binary[i] & 0xf); + items[i * column_count + 8].val = hex; + } + + for (int i = 0; i < items.size(); ++i) { + if (i >= prev_items.size() || prev_items[i].val != items[i].val) { + auto idx = index(i / column_count, i % column_count); + emit dataChanged(idx, idx); + } + } +} + +QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (orientation == Qt::Vertical) { + switch (role) { + case Qt::DisplayRole: return section + 1; + case Qt::SizeHintRole: return QSize(30, CELL_HEIGHT); + case Qt::TextAlignmentRole: return Qt::AlignCenter; + } + } + return {}; +} + +// BinarySelectionModel + +void BinarySelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) { + QItemSelection new_selection = selection; + if (auto indexes = selection.indexes(); !indexes.isEmpty()) { + auto [begin_idx, end_idx] = (QModelIndex[]){indexes.first(), indexes.back()}; + for (int row = begin_idx.row(); row <= end_idx.row(); ++row) { + int left_col = (row == begin_idx.row()) ? begin_idx.column() : 0; + int right_col = (row == end_idx.row()) ? end_idx.column() : 7; + new_selection.merge({model()->index(row, left_col), model()->index(row, right_col)}, command); + } + } + QItemSelectionModel::select(new_selection, command); +} + +// BinaryItemDelegate + +BinaryItemDelegate::BinaryItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { + // cache fonts and color + small_font.setPointSize(7); + bold_font.setBold(true); + highlight_color = QApplication::style()->standardPalette().color(QPalette::Active, QPalette::Highlight); +} + +QSize BinaryItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { + QSize sz = QStyledItemDelegate::sizeHint(option, index); + return {sz.width(), CELL_HEIGHT}; +} + +void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + auto item = (const BinaryViewModel::Item *)index.internalPointer(); + painter->save(); + // TODO: highlight signal cells on mouse over + painter->fillRect(option.rect, option.state & QStyle::State_Selected ? highlight_color : item->bg_color); + if (index.column() == 8) { + painter->setFont(bold_font); + } + painter->drawText(option.rect, Qt::AlignCenter, item->val); + if (item->is_msb || item->is_lsb) { + painter->setFont(small_font); + painter->drawText(option.rect, Qt::AlignHCenter | Qt::AlignBottom, item->is_msb ? "MSB" : "LSB"); + } + + painter->restore(); +} diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h new file mode 100644 index 000000000..631797ca4 --- /dev/null +++ b/tools/cabana/binaryview.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include "tools/cabana/dbcmanager.h" + +class BinaryItemDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + BinaryItemDelegate(QObject *parent); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + QFont small_font, bold_font; + QColor highlight_color; +}; + +class BinaryViewModel : public QAbstractTableModel { + Q_OBJECT + +public: + BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {} + void setMessage(const QString &message_id); + void updateState(); + Qt::ItemFlags flags(const QModelIndex &index) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { return {}; } + int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } + int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; } + + struct Item { + QColor bg_color = QColor(Qt::white); + bool is_msb = false; + bool is_lsb = false; + QString val = "0"; + }; + +private: + QString msg_id; + const Msg *dbc_msg; + int row_count = 0; + const int column_count = 9; + std::vector items; +}; + +// the default QItemSelectionModel does not support our selection mode. +class BinarySelectionModel : public QItemSelectionModel { + public: + BinarySelectionModel(QAbstractItemModel *model = nullptr) : QItemSelectionModel(model) {} + void select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) override; +}; + +class BinaryView : public QTableView { + Q_OBJECT + +public: + BinaryView(QWidget *parent = nullptr); + void mouseReleaseEvent(QMouseEvent *event) override; + void setMessage(const QString &message_id); + void updateState(); + +signals: + void cellsSelected(int start_bit, int size); + +private: + QString msg_id; + BinaryViewModel *model; +}; diff --git a/tools/cabana/canmessages.h b/tools/cabana/canmessages.h index 7f6955369..fe71840d7 100644 --- a/tools/cabana/canmessages.h +++ b/tools/cabana/canmessages.h @@ -91,6 +91,7 @@ inline char toHex(uint value) { } inline const QString &getColor(int i) { + // TODO: add more colors static const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"}; return SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)]; } diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index a9899ec65..60d0632d4 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -1,13 +1,13 @@ #include "tools/cabana/detailwidget.h" #include -#include #include -#include #include -#include #include +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" + // DetailWidget DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) { @@ -141,98 +141,6 @@ void DetailWidget::removeSignal() { } } -// BinaryView - -BinaryView::BinaryView(QWidget *parent) : QTableWidget(parent) { - horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - verticalHeader()->setSectionResizeMode(QHeaderView::Stretch); - horizontalHeader()->hide(); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setColumnCount(9); - - // replace selection model - auto old_model = selectionModel(); - setSelectionModel(new BinarySelectionModel(model())); - delete old_model; -} - -void BinaryView::mouseReleaseEvent(QMouseEvent *event) { - QTableWidget::mouseReleaseEvent(event); - - if (auto items = selectedItems(); !items.isEmpty()) { - int start_bit = items.first()->row() * 8 + items.first()->column(); - int size = items.back()->row() * 8 + items.back()->column() - start_bit + 1; - emit cellsSelected(start_bit, size); - } -} - -void BinaryView::setMessage(const QString &message_id) { - msg_id = message_id; - if (msg_id.isEmpty()) return; - - const Msg *msg = dbc()->msg(msg_id); - int row_count = msg ? msg->size : can->lastMessage(msg_id).dat.size(); - setRowCount(row_count); - setColumnCount(9); - for (int i = 0; i < rowCount(); ++i) { - for (int j = 0; j < columnCount(); ++j) { - auto item = new QTableWidgetItem(); - item->setFlags(item->flags() ^ Qt::ItemIsEditable); - item->setTextAlignment(Qt::AlignCenter); - if (j == 8) { - QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - font.setBold(true); - item->setFont(font); - item->setFlags(item->flags() ^ Qt::ItemIsSelectable); - } - setItem(i, j, item); - } - } - - // set background color - if (msg) { - for (int i = 0; i < msg->sigs.size(); ++i) { - const auto &sig = msg->sigs[i]; - int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit); - for (int j = start; j <= std::min(start + sig.size - 1, rowCount() * columnCount() - 1); ++j) { - item(j / 8, j % 8)->setBackground(QColor(getColor(i))); - } - } - } - - setFixedHeight(rowHeight(0) * std::min(row_count, 8) + 2); - clearSelection(); - updateState(); -} - -void BinaryView::updateState() { - const auto &binary = can->lastMessage(msg_id).dat; - setUpdatesEnabled(false); - char hex[3] = {'\0'}; - for (int i = 0; i < binary.size(); ++i) { - for (int j = 0; j < 8; ++j) { - item(i, j)->setText(QChar((binary[i] >> (7 - j)) & 1 ? '1' : '0')); - } - hex[0] = toHex(binary[i] >> 4); - hex[1] = toHex(binary[i] & 0xf); - item(i, 8)->setText(hex); - } - setUpdatesEnabled(true); -} - -void BinarySelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) { - QItemSelection new_selection = selection; - if (auto indexes = selection.indexes(); !indexes.isEmpty()) { - auto [begin_idx, end_idx] = (QModelIndex[]){indexes.first(), indexes.back()}; - for (int row = begin_idx.row(); row <= end_idx.row(); ++row) { - int left_col = (row == begin_idx.row()) ? begin_idx.column() : 0; - int right_col = (row == end_idx.row()) ? end_idx.column() : 7; - new_selection.merge({model()->index(row, left_col), model()->index(row, right_col)}, command); - } - } - QItemSelectionModel::select(new_selection, command); -} - // EditMessageDialog EditMessageDialog::EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent) : QDialog(parent) { diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index db174873f..9d3b81dcb 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -1,35 +1,11 @@ #pragma once #include -#include -#include "opendbc/can/common.h" -#include "opendbc/can/common_dbc.h" -#include "tools/cabana/canmessages.h" -#include "tools/cabana/dbcmanager.h" +#include "tools/cabana/binaryview.h" #include "tools/cabana/historylog.h" #include "tools/cabana/signaledit.h" -class BinarySelectionModel : public QItemSelectionModel { -public: - BinarySelectionModel(QAbstractItemModel *model = nullptr) : QItemSelectionModel(model) {} - void select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) override; -}; - -class BinaryView : public QTableWidget { - Q_OBJECT -public: - BinaryView(QWidget *parent = nullptr); - void mouseReleaseEvent(QMouseEvent *event) override; - void setMessage(const QString &message_id); - void updateState(); -signals: - void cellsSelected(int start_bit, int size); - -private: - QString msg_id; -}; - class EditMessageDialog : public QDialog { Q_OBJECT diff --git a/tools/cabana/signaledit.cc b/tools/cabana/signaledit.cc index 2da3e2eec..b52f83dab 100644 --- a/tools/cabana/signaledit.cc +++ b/tools/cabana/signaledit.cc @@ -23,6 +23,9 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : start_bit(sig.start endianness->setCurrentIndex(sig.is_little_endian ? 0 : 1); form_layout->addRow(tr("Endianness"), endianness); + form_layout->addRow(tr("lsb"), new QLabel(QString::number(sig.lsb))); + form_layout->addRow(tr("msb"), new QLabel(QString::number(sig.msb))); + sign = new QComboBox(); sign->addItems({"Signed", "Unsigned"}); sign->setCurrentIndex(sig.is_signed ? 0 : 1);