openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

461 lines
17 KiB

#include "tools/cabana/signaledit.h"
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QPushButton>
#include <QToolButton>
#include <QVBoxLayout>
#include "tools/cabana/commands.h"
// SignalModel
SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) {
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &SignalModel::refresh);
QObject::connect(dbc(), &DBCManager::msgUpdated, this, &SignalModel::handleMsgChanged);
QObject::connect(dbc(), &DBCManager::msgRemoved, this, &SignalModel::handleMsgChanged);
QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalModel::handleSignalAdded);
QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalModel::handleSignalUpdated);
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &SignalModel::handleSignalRemoved);
QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalModel::updateState);
}
void SignalModel::insertItem(SignalModel::Item *parent_item, int pos, const Signal *sig) {
Item *item = new Item{.sig = sig, .parent = parent_item, .title = sig->name.c_str(), .type = Item::Sig};
parent_item->children.insert(pos, item);
QString titles[]{"Name", "Size", "Little Endian", "Signed", "Offset", "Factor", "Extra Info", "Unit", "Comment", "Minimum", "Maximum", "Description"};
for (int i = 0; i < std::size(titles); ++i) {
item->children.push_back(new Item{.sig = sig, .parent = item, .title = titles[i], .type = (Item::Type)(i + Item::Name)});
}
}
void SignalModel::setMessage(const MessageId &id) {
msg_id = id;
filter_str = "";
refresh();
updateState(nullptr);
}
void SignalModel::setFilter(const QString &txt) {
filter_str = txt;
refresh();
}
void SignalModel::refresh() {
beginResetModel();
root.reset(new SignalModel::Item);
if (auto msg = dbc()->msg(msg_id)) {
for (auto &s : msg->getSignals()) {
if (filter_str.isEmpty() || QString::fromStdString(s->name).contains(filter_str, Qt::CaseInsensitive)) {
insertItem(root.get(), root->children.size(), s);
}
}
}
endResetModel();
}
void SignalModel::updateState(const QHash<MessageId, CanData> *msgs) {
if (!msgs || (msgs->contains(msg_id))) {
auto &dat = can->lastMessage(msg_id).dat;
int row = 0;
for (auto item : root->children) {
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *item->sig);
item->sig_val = QString::number(value);
emit dataChanged(index(row, 1), index(row, 1), {Qt::DisplayRole});
++row;
}
}
}
int SignalModel::rowCount(const QModelIndex &parent) const {
if (parent.column() > 0) return 0;
auto parent_item = getItem(parent);
int row_count = parent_item->children.size();
if (parent_item->type == Item::Sig && !parent_item->extra_expanded) {
row_count -= (Item::Desc - Item::ExtraInfo);
}
return row_count;
}
Qt::ItemFlags SignalModel::flags(const QModelIndex &index) const {
if (!index.isValid()) return Qt::NoItemFlags;
auto item = getItem(index);
Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
if (index.column() == 1 && item->type != Item::Sig && item->type != Item::ExtraInfo) {
flags |= (item->type == Item::Endian || item->type == Item::Signed) ? Qt::ItemIsUserCheckable : Qt::ItemIsEditable;
}
return flags;
}
int SignalModel::signalRow(const Signal *sig) const {
auto &children = root->children;
for (int i = 0; i < children.size(); ++i) {
if (children[i]->sig == sig) return i;
}
return -1;
}
QModelIndex SignalModel::index(int row, int column, const QModelIndex &parent) const {
if (!hasIndex(row, column, parent)) return {};
return createIndex(row, column, getItem(parent)->children[row]);
}
QModelIndex SignalModel::parent(const QModelIndex &index) const {
if (!index.isValid()) return {};
Item *parent_item = getItem(index)->parent;
return parent_item == root.get() ? QModelIndex() : createIndex(parent_item->row(), 0, parent_item);
}
QVariant SignalModel::data(const QModelIndex &index, int role) const {
if (index.isValid()) {
const Item *item = getItem(index);
if (role == Qt::DisplayRole || role == Qt::EditRole) {
if (index.column() == 0) {
return item->type == Item::Sig ? QString::fromStdString(item->sig->name) : item->title;
} else {
switch (item->type) {
case Item::Sig: return item->sig_val;
case Item::Name: return QString::fromStdString(item->sig->name);
case Item::Size: return item->sig->size;
case Item::Offset: return QString::number(item->sig->offset, 'f', 6);
case Item::Factor: return QString::number(item->sig->factor, 'f', 6);
default: break;
}
}
} else if (role == Qt::CheckStateRole && index.column() == 1) {
if (item->type == Item::Endian) return item->sig->is_little_endian ? Qt::Checked : Qt::Unchecked;
if (item->type == Item::Signed) return item->sig->is_signed ? Qt::Checked : Qt::Unchecked;
} else if (role == Qt::DecorationRole && index.column() == 0 && item->type == Item::ExtraInfo) {
return utils::icon(item->parent->extra_expanded ? "chevron-compact-down" : "chevron-compact-up");
}
}
return {};
}
bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (role != Qt::EditRole && role != Qt::CheckStateRole) return false;
Item *item = getItem(index);
Signal s = *item->sig;
switch (item->type) {
case Item::Name: s.name = value.toString().toStdString(); break;
case Item::Size: s.size = value.toInt(); break;
case Item::Endian: s.is_little_endian = value.toBool(); break;
case Item::Signed: s.is_signed = value.toBool(); break;
case Item::Offset: s.offset = value.toDouble(); break;
case Item::Factor: s.factor = value.toDouble(); break;
default: return false;
}
bool ret = saveSignal(item->sig, s);
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole});
return ret;
}
void SignalModel::showExtraInfo(const QModelIndex &index) {
auto item = getItem(index);
if (item->type == Item::ExtraInfo) {
if (!item->parent->extra_expanded) {
item->parent->extra_expanded = true;
beginInsertRows(index.parent(), 7, 13);
endInsertRows();
} else {
item->parent->extra_expanded = false;
beginRemoveRows(index.parent(), 7, 13);
endRemoveRows();
}
}
}
bool SignalModel::saveSignal(const Signal *origin_s, Signal &s) {
auto msg = dbc()->msg(msg_id);
if (s.name != origin_s->name && msg->sigs.count(s.name.c_str()) != 0) {
QString text = tr("There is already a signal with the same name '%1'").arg(s.name.c_str());
QMessageBox::warning(nullptr, tr("Failed to save signal"), text);
return false;
}
if (s.is_little_endian != origin_s->is_little_endian) {
int start = std::floor(s.start_bit / 8);
if (s.is_little_endian) {
int end = std::floor((s.start_bit - s.size + 1) / 8);
s.start_bit = start == end ? s.start_bit - s.size + 1 : bigEndianStartBitsIndex(s.start_bit);
} else {
int end = std::floor((s.start_bit + s.size - 1) / 8);
s.start_bit = start == end ? s.start_bit + s.size - 1 : bigEndianBitIndex(s.start_bit);
}
}
if (s.is_little_endian) {
s.lsb = s.start_bit;
s.msb = s.start_bit + s.size - 1;
} else {
s.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(s.start_bit) + s.size - 1);
s.msb = s.start_bit;
}
UndoStack::push(new EditSignalCommand(msg_id, origin_s, s));
return true;
}
void SignalModel::addSignal(int start_bit, int size, bool little_endian) {
auto msg = dbc()->msg(msg_id);
for (int i = 1; !msg; ++i) {
QString name = QString("NEW_MSG_%1").arg(i);
if (std::none_of(dbc()->messages().begin(), dbc()->messages().end(), [&](auto &m) { return m.second.name == name; })) {
UndoStack::push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size()));
msg = dbc()->msg(msg_id);
}
}
Signal sig = {.is_little_endian = little_endian, .factor = 1};
for (int i = 1; /**/; ++i) {
sig.name = "NEW_SIGNAL_" + std::to_string(i);
if (msg->sigs.count(sig.name.c_str()) == 0) break;
}
updateSigSizeParamsFromRange(sig, start_bit, size);
UndoStack::push(new AddSigCommand(msg_id, sig));
}
void SignalModel::resizeSignal(const Signal *sig, int start_bit, int size) {
Signal s = *sig;
updateSigSizeParamsFromRange(s, start_bit, size);
saveSignal(sig, s);
}
void SignalModel::removeSignal(const Signal *sig) {
UndoStack::push(new RemoveSigCommand(msg_id, sig));
}
void SignalModel::handleMsgChanged(uint32_t address) {
if (address == msg_id.address) {
refresh();
}
}
void SignalModel::handleSignalAdded(uint32_t address, const Signal *sig) {
if (address == msg_id.address) {
int i = 0;
for (; i < root->children.size(); ++i) {
if (sig->start_bit < root->children[i]->sig->start_bit) break;
}
beginInsertRows({}, i, i);
insertItem(root.get(), i, sig);
endInsertRows();
}
}
void SignalModel::handleSignalUpdated(const Signal *sig) {
if (int row = signalRow(sig); row != -1) {
emit dataChanged(index(row, 0), index(row, 1), {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole});
}
}
void SignalModel::handleSignalRemoved(const Signal *sig) {
if (int row = signalRow(sig); row != -1) {
beginRemoveRows({}, row, row);
delete root->children.takeAt(row);
endRemoveRows();
}
}
// SignalItemDelegate
SignalItemDelegate::SignalItemDelegate(QObject *parent) {
name_validator = new NameValidator(this);
double_validator = new QDoubleValidator(this);
small_font.setPointSize(8);
double_validator->setLocale(QLocale::C); // Match locale of QString::toDouble() instead of system
}
void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
auto item = (SignalModel::Item *)index.internalPointer();
if (item && !index.parent().isValid() && index.column() == 0) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
}
// color label
auto bg_color = getColor(item->sig);
QRect rc{option.rect.left(), option.rect.top(), 18, option.rect.height()};
painter->setPen(Qt::NoPen);
painter->setBrush(item->highlight ? bg_color.darker(125) : bg_color);
painter->drawRoundedRect(rc.adjusted(0, 2, 0, -2), 3, 3);
painter->setPen(item->highlight ? Qt::white : Qt::black);
painter->setFont(small_font);
painter->drawText(rc, Qt::AlignCenter, QString::number(item->row() + 1));
// signal name
painter->setFont(option.font);
painter->setPen((option.state & QStyle::State_Selected ? option.palette.highlightedText() : option.palette.text()).color());
QString text = index.data(Qt::DisplayRole).toString();
QRect text_rect = option.rect.adjusted(rc.width() + 6, 0, 0, 0);
text = painter->fontMetrics().elidedText(text, Qt::ElideRight, text_rect.width());
painter->drawText(text_rect, option.displayAlignment, text);
painter->restore();
} else {
QStyledItemDelegate::paint(painter, option, index);
}
}
QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
auto item = (SignalModel::Item *)index.internalPointer();
if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Offset || item->type == SignalModel::Item::Factor) {
QLineEdit *e = new QLineEdit(parent);
e->setFrame(false);
e->setValidator(index.row() == 0 ? name_validator : double_validator);
return e;
} else if (item->type == SignalModel::Item::Size) {
QSpinBox *spin = new QSpinBox(parent);
spin->setFrame(false);
spin->setRange(1, 64);
return spin;
}
return QStyledItemDelegate::createEditor(parent, option, index);
}
// SignalView
SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) {
// title bar
QWidget *title_bar = new QWidget(this);
title_bar->setAutoFillBackground(true);
QHBoxLayout *hl = new QHBoxLayout(title_bar);
hl->addWidget(signal_count_lb = new QLabel());
filter_edit = new QLineEdit(this);
filter_edit->setClearButtonEnabled(true);
filter_edit->setPlaceholderText(tr("filter signals by name"));
hl->addWidget(filter_edit);
hl->addStretch(1);
auto collapse_btn = new QToolButton();
collapse_btn->setIcon(utils::icon("dash-square"));
collapse_btn->setIconSize({12, 12});
collapse_btn->setAutoRaise(true);
collapse_btn->setToolTip(tr("Collapse All"));
hl->addWidget(collapse_btn);
// tree view
tree = new QTreeView(this);
tree->setModel(model = new SignalModel(this));
tree->setItemDelegate(new SignalItemDelegate(this));
tree->setFrameShape(QFrame::NoFrame);
tree->setHeaderHidden(true);
tree->setMouseTracking(true);
tree->setExpandsOnDoubleClick(false);
tree->header()->setSectionResizeMode(QHeaderView::Stretch);
tree->setMinimumHeight(300);
tree->setStyleSheet("QSpinBox{background-color:white;border:none;} QLineEdit{background-color:white;}");
QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0);
main_layout->setSpacing(0);
main_layout->addWidget(title_bar);
main_layout->addWidget(tree);
QObject::connect(filter_edit, &QLineEdit::textEdited, model, &SignalModel::setFilter);
QObject::connect(collapse_btn, &QPushButton::clicked, tree, &QTreeView::collapseAll);
QObject::connect(tree, &QAbstractItemView::clicked, this, &SignalView::rowClicked);
QObject::connect(tree, &QTreeView::viewportEntered, [this]() { emit highlight(nullptr); });
QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); });
QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged);
QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &SignalView::rowsChanged);
QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged);
QObject::connect(dbc(), &DBCManager::signalAdded, [this](uint32_t address, const Signal *sig) { expandSignal(sig); });
setWhatsThis(tr(R"(
<b>Signal view</b><br />
<!-- TODO: add descprition here -->
)"));
}
void SignalView::setMessage(const MessageId &id) {
msg_id = id;
filter_edit->clear();
model->setMessage(id);
}
void SignalView::rowsChanged() {
auto create_btn = [](const QString &id, const QString &tooltip) {
auto btn = new QToolButton();
btn->setIcon(utils::icon(id));
btn->setToolTip(tooltip);
btn->setAutoRaise(true);
return btn;
};
signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount()));
for (int i = 0; i < model->rowCount(); ++i) {
auto index = model->index(i, 1);
if (!tree->indexWidget(index)) {
QWidget *w = new QWidget(this);
QHBoxLayout *h = new QHBoxLayout(w);
h->setContentsMargins(0, 2, 0, 2);
h->addStretch(1);
auto remove_btn = create_btn("x", tr("Remove signal"));
auto plot_btn = create_btn("graph-up", "");
plot_btn->setCheckable(true);
h->addWidget(plot_btn);
h->addWidget(remove_btn);
tree->setIndexWidget(index, w);
auto sig = model->getItem(index)->sig;
QObject::connect(remove_btn, &QToolButton::clicked, [=]() { model->removeSignal(sig); });
QObject::connect(plot_btn, &QToolButton::clicked, [=](bool checked) {
emit showChart(msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier);
});
}
}
updateChartState();
}
void SignalView::rowClicked(const QModelIndex &index) {
auto item = model->getItem(index);
if (item->type == SignalModel::Item::Sig) {
auto sig_index = model->index(index.row(), 0, index.parent());
tree->setExpanded(sig_index, !tree->isExpanded(sig_index));
} else if (item->type == SignalModel::Item::ExtraInfo) {
model->showExtraInfo(index);
}
}
void SignalView::expandSignal(const Signal *sig) {
if (int row = model->signalRow(sig); row != -1) {
auto idx = model->index(row, 0);
bool expand = !tree->isExpanded(idx);
tree->setExpanded(idx, expand);
tree->scrollTo(idx, QAbstractItemView::PositionAtTop);
if (expand) tree->setCurrentIndex(idx);
}
}
void SignalView::updateChartState() {
int i = 0;
for (auto item : model->root->children) {
auto plot_btn = tree->indexWidget(model->index(i, 1))->findChildren<QToolButton*>()[0];
bool chart_opened = charts->hasSignal(msg_id, item->sig);
plot_btn->setChecked(chart_opened);
plot_btn->setToolTip(chart_opened ? tr("Close Plot") : tr("Show Plot\nSHIFT click to add to previous opened plot"));
++i;
}
}
void SignalView::signalHovered(const Signal *sig) {
auto &children = model->root->children;
for (int i = 0; i < children.size(); ++i) {
bool highlight = children[i]->sig == sig;
if (std::exchange(children[i]->highlight, highlight) != highlight) {
emit model->dataChanged(model->index(i, 0), model->index(i, 0));
}
}
}
void SignalView::leaveEvent(QEvent *event) {
emit highlight(nullptr);
QWidget::leaveEvent(event);
}