Merge remote-tracking branch 'upstream/master' into subaru-fix-console-message

pull/27829/head
Shane Smiskol 2 years ago
commit 640662d49a
  1. 28
      .github/workflows/repo.yml
  2. 2
      cereal
  3. 4
      selfdrive/boardd/SConscript
  4. 3
      selfdrive/boardd/panda.cc
  5. 3
      selfdrive/car/ford/fordcan.py
  6. 15
      selfdrive/car/volkswagen/mqbcan.py
  7. 8
      tools/cabana/README.md
  8. 6
      tools/cabana/SConscript
  9. 48
      tools/cabana/binaryview.cc
  10. 1
      tools/cabana/binaryview.h
  11. 13
      tools/cabana/cabana.cc
  12. 5
      tools/cabana/chart/chartswidget.cc
  13. 68
      tools/cabana/chart/sparkline.cc
  14. 24
      tools/cabana/chart/sparkline.h
  15. 18
      tools/cabana/dbc/dbcfile.cc
  16. 3
      tools/cabana/dbc/dbcfile.h
  17. 14
      tools/cabana/dbc/dbcmanager.cc
  18. 3
      tools/cabana/dbc/dbcmanager.h
  19. 17
      tools/cabana/mainwin.cc
  20. 32
      tools/cabana/messageswidget.cc
  21. 2
      tools/cabana/messageswidget.h
  22. 16
      tools/cabana/settings.cc
  23. 9
      tools/cabana/settings.h
  24. 255
      tools/cabana/signalview.cc
  25. 20
      tools/cabana/signalview.h
  26. 2
      tools/cabana/streams/abstractstream.h
  27. 27
      tools/cabana/streams/devicestream.cc
  28. 17
      tools/cabana/streams/devicestream.h
  29. 41
      tools/cabana/streams/livestream.cc
  30. 13
      tools/cabana/streams/livestream.h
  31. 85
      tools/cabana/streams/pandastream.cc
  32. 33
      tools/cabana/streams/pandastream.h
  33. 42
      tools/cabana/util.cc
  34. 2
      tools/cabana/util.h

@ -0,0 +1,28 @@
name: repo
on:
schedule:
- cron: "0 15 * * 2"
workflow_dispatch:
jobs:
pre-commit-autoupdate:
name: pre-commit autoupdate
runs-on: ubuntu-20.04
container:
image: ghcr.io/commaai/openpilot-base:latest
steps:
- uses: actions/checkout@v3
- name: pre-commit autoupdate
run: |
git config --global --add safe.directory '*'
pre-commit autoupdate
- name: Create Pull Request
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5
with:
token: ${{ secrets.ACTIONS_CREATE_PR_PAT }}
commit-message: Update pre-commit hook versions
title: 'pre-commit: autoupdate hooks'
branch: pre-commit-updates
base: master
delete-branch: true

@ -1 +1 @@
Subproject commit 4f5502c8657813ccb538e9575ca1a7b258b17af9
Subproject commit 911cfac392d6dc05162a44a3af9d28307185ecd7

@ -1,7 +1,9 @@
Import('env', 'envCython', 'common', 'cereal', 'messaging')
libs = ['usb-1.0', common, cereal, messaging, 'pthread', 'zmq', 'capnp', 'kj']
env.Program('boardd', ['main.cc', 'boardd.cc', 'panda.cc', 'panda_comms.cc', 'spi.cc'], LIBS=libs)
panda = env.Library('panda', ['panda.cc', 'panda_comms.cc', 'spi.cc'])
env.Program('boardd', ['main.cc', 'boardd.cc'], LIBS=[panda] + libs)
env.Library('libcan_list_to_can_capnp', ['can_list_to_can_capnp.cc'])
envCython.Program('boardd_api_impl.so', 'boardd_api_impl.pyx', LIBS=["can_list_to_can_capnp", 'capnp', 'kj'] + envCython["LIBS"])

@ -21,9 +21,6 @@ Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) {
hw_type = get_hw_type();
assert((hw_type != cereal::PandaState::PandaType::WHITE_PANDA) &&
(hw_type != cereal::PandaState::PandaType::GREY_PANDA));
has_rtc = (hw_type == cereal::PandaState::PandaType::UNO) ||
(hw_type == cereal::PandaState::PandaType::DOS) ||
(hw_type == cereal::PandaState::PandaType::TRES);

@ -158,8 +158,7 @@ def create_lkas_ui_msg(packer, main_on: bool, enabled: bool, steer_alert: bool,
else:
lines = 30 # LA_Off
# TODO: use level 1 for no sound when less severe?
hands_on_wheel_dsply = 2 if steer_alert else 0
hands_on_wheel_dsply = 1 if steer_alert else 0
values = {
**stock_values,

@ -1,14 +1,11 @@
def create_steering_control(packer, bus, apply_steer, lkas_enabled):
values = {
"SET_ME_0X3": 0x3,
"Assist_Torque": abs(apply_steer),
"Assist_Requested": lkas_enabled,
"Assist_VZ": 1 if apply_steer < 0 else 0,
"HCA_Available": 1,
"HCA_Standby": not lkas_enabled,
"HCA_Active": lkas_enabled,
"SET_ME_0XFE": 0xFE,
"SET_ME_0X07": 0x07,
"HCA_01_Status_HCA": 5 if lkas_enabled else 3,
"HCA_01_LM_Offset": abs(apply_steer),
"HCA_01_LM_OffSign": 1 if apply_steer < 0 else 0,
"HCA_01_Vib_Freq": 18,
"HCA_01_Sendestatus": 1 if lkas_enabled else 0,
"EA_ACC_Wunschgeschwindigkeit": 327.36,
}
return packer.make_can_msg("HCA_01", bus, values)

@ -11,12 +11,16 @@ $ ./cabana -h
Usage: ./cabana [options] route
Options:
-h, --help Displays this help.
-h, --help Displays help on commandline options.
--help-all Displays help including Qt specific options.
--demo use a demo route instead of providing your own
--qcam load qcamera
--ecam load wide road camera
--stream read can messages from live streaming
--zmq <zmq> the ip address on which to receive zmq messages
--panda read can messages from panda
--panda-serial <panda-serial> read can messages from panda with given serial
--zmq <zmq> the ip address on which to receive zmq
messages
--data_dir <data_dir> local directory with routes
--no-vipc do not output video
--dbc <dbc> dbc file to open

@ -17,7 +17,7 @@ qt_libs = ['qt_util'] + base_libs
cabana_env = qt_env.Clone()
cabana_env["LIBPATH"] += ['../../opendbc/can']
cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'libdbc_static', 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_libs
cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'panda', 'libdbc_static', 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'usb-1.0'] + qt_libs
opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc").abspath)
cabana_env['CXXFLAGS'] += [opendbc_path]
@ -29,9 +29,9 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "asset
prev_moc_path = cabana_env['QT_MOCHPREFIX']
cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_'
cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc',
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
'commands.cc', 'messageswidget.cc', 'route.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)

@ -239,7 +239,19 @@ std::tuple<int, int, bool> BinaryView::getSelection(QModelIndex index) {
if (index.column() == 8) {
index = model->index(index.row(), 7);
}
bool is_lb = (resize_sig && resize_sig->is_little_endian) || (!resize_sig && index < anchor_index);
bool is_lb = true;
if (resize_sig) {
is_lb = resize_sig->is_little_endian;
} else if (settings.drag_direction == Settings::DragDirection::MsbFirst) {
is_lb = index < anchor_index;
} else if (settings.drag_direction == Settings::DragDirection::LsbFirst) {
is_lb = !(index < anchor_index);
} else if (settings.drag_direction == Settings::DragDirection::AlwaysLE) {
is_lb = true;
} else if (settings.drag_direction == Settings::DragDirection::AlwaysBE) {
is_lb = false;
}
int cur_bit_idx = get_bit_index(index, is_lb);
int anchor_bit_idx = get_bit_index(anchor_index, is_lb);
auto [start_bit, end_bit] = std::minmax(cur_bit_idx, anchor_bit_idx);
@ -281,11 +293,19 @@ void BinaryViewModel::refresh() {
updateState();
}
void BinaryViewModel::updateItem(int row, int col, const QString &val, const QColor &color) {
auto &item = items[row * column_count + col];
if (item.val != val || item.bg_color != color) {
item.val = val;
item.bg_color = color;
auto idx = index(row, col);
emit dataChanged(idx, idx, {Qt::DisplayRole});
}
}
void BinaryViewModel::updateState() {
auto prev_items = items;
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);
@ -294,29 +314,23 @@ void BinaryViewModel::updateState() {
endInsertRows();
}
double max_f = 255.0;
double factor = 0.25;
double scaler = max_f / log2(1.0 + factor);
const double max_f = 255.0;
const double factor = 0.25;
const double scaler = max_f / log2(1.0 + factor);
for (int i = 0; i < binary.size(); ++i) {
for (int j = 0; j < 8; ++j) {
auto &item = items[i * column_count + j];
item.val = ((binary[i] >> (7 - j)) & 1) != 0 ? '1' : '0';
QString val = ((binary[i] >> (7 - j)) & 1) != 0 ? "1" : "0";
// Bit update frequency based highlighting
double offset = !item.sigs.empty() ? 50 : 0;
auto n = last_msg.bit_change_counts[i][7 - j];
double min_f = n == 0 ? offset : offset + 25;
double alpha = std::clamp(offset + log2(1.0 + factor * (double)n / (double)last_msg.count) * scaler, min_f, max_f);
item.bg_color.setAlpha(alpha);
}
items[i * column_count + 8].val = toHex(binary[i]);
items[i * column_count + 8].bg_color = last_msg.colors[i];
}
for (int i = 0; i < items.size(); ++i) {
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);
auto color = item.bg_color;
color.setAlpha(alpha);
updateItem(i, j, val, color);
}
updateItem(i, 8, toHex(binary[i]), last_msg.colors[i]);
}
}

@ -26,6 +26,7 @@ public:
BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {}
void refresh();
void updateState();
void updateItem(int row, int col, const QString &val, const QColor &color);
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { return {}; }
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; }

@ -5,7 +5,8 @@
#include "selfdrive/ui/qt/util.h"
#include "tools/cabana/mainwin.h"
#include "tools/cabana/route.h"
#include "tools/cabana/streams/livestream.h"
#include "tools/cabana/streams/devicestream.h"
#include "tools/cabana/streams/pandastream.h"
#include "tools/cabana/streams/replaystream.h"
int main(int argc, char *argv[]) {
@ -24,6 +25,8 @@ int main(int argc, char *argv[]) {
cmd_parser.addOption({"qcam", "load qcamera"});
cmd_parser.addOption({"ecam", "load wide road camera"});
cmd_parser.addOption({"stream", "read can messages from live streaming"});
cmd_parser.addOption({"panda", "read can messages from panda"});
cmd_parser.addOption({"panda-serial", "read can messages from panda with given serial", "panda-serial"});
cmd_parser.addOption({"zmq", "the ip address on which to receive zmq messages", "zmq"});
cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"});
cmd_parser.addOption({"no-vipc", "do not output video"});
@ -34,7 +37,13 @@ int main(int argc, char *argv[]) {
std::unique_ptr<AbstractStream> stream;
if (cmd_parser.isSet("stream")) {
stream.reset(new LiveStream(&app, cmd_parser.value("zmq")));
stream.reset(new DeviceStream(&app, cmd_parser.value("zmq")));
} else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) {
PandaStreamConfig config = {};
if (cmd_parser.isSet("panda-serial")) {
config.serial = cmd_parser.value("panda-serial");
}
stream.reset(new PandaStream(&app, config));
} else {
// TODO: Remove when OpenpilotPrefix supports ZMQ
#ifndef __APPLE__

@ -314,8 +314,13 @@ void ChartsWidget::updateLayout(bool force) {
}
for (int i = 0; i < current_charts.size(); ++i) {
charts_layout->addWidget(current_charts[i], i / n, i % n);
if (current_charts[i]->sigs.isEmpty()) {
// the chart will be resized after add signal. delay setVisible to reduce flicker.
QTimer::singleShot(0, [c = current_charts[i]]() { c->setVisible(true); });
} else {
current_charts[i]->setVisible(true);
}
}
charts_container->setUpdatesEnabled(true);
}
}

@ -0,0 +1,68 @@
#include "tools/cabana/chart/sparkline.h"
#include <QPainter>
#include "tools/cabana/streams/abstractstream.h"
void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) {
const auto &msgs = can->events().at(msg_id);
uint64_t ts = (last_msg_ts + can->routeStartTime()) * 1e9;
auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), CanEvent{.mono_time = (uint64_t)std::max<int64_t>(ts - range * 1e9, 0)});
auto last = std::upper_bound(first, msgs.cend(), CanEvent{.mono_time = ts});
bool update_values = last_ts != last_msg_ts || time_range != range;
last_ts = last_msg_ts;
time_range = range;
if (first != last) {
if (update_values) {
values.clear();
values.reserve(std::distance(first, last));
min_val = std::numeric_limits<double>::max();
max_val = std::numeric_limits<double>::lowest();
for (auto it = first; it != last; ++it) {
double value = get_raw_value(it->dat, it->size, *sig);
values.emplace_back((it->mono_time - first->mono_time) / 1e9, value);
if (min_val > value) min_val = value;
if (max_val < value) max_val = value;
}
if (min_val == max_val) {
min_val -= 1;
max_val += 1;
}
}
render(getColor(sig), size);
} else {
pixmap = QPixmap();
min_val = -1;
max_val = 1;
}
}
void Sparkline::render(const QColor &color, QSize size) {
const double xscale = (size.width() - 1) / (double)time_range;
const double yscale = (size.height() - 3) / (max_val - min_val);
points.clear();
points.reserve(values.size());
for (auto &v : values) {
points.emplace_back(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale);
}
qreal dpr = qApp->devicePixelRatio();
size *= dpr;
if (size != pixmap.size()) {
pixmap = QPixmap(size);
}
pixmap.setDevicePixelRatio(dpr);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, points.size() < 500);
painter.setPen(color);
painter.drawPolyline(points.data(), points.size());
painter.setPen(QPen(color, 3));
if ((points.back().x() - points.front().x()) / points.size() > 8) {
painter.drawPoints(points.data(), points.size());
} else {
painter.drawPoint(points.back());
}
}

@ -0,0 +1,24 @@
#pragma once
#include <QPixmap>
#include <QPointF>
#include <vector>
#include "tools/cabana/dbc/dbcmanager.h"
class Sparkline {
public:
void update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size);
const QSize size() const { return pixmap.size() / pixmap.devicePixelRatio(); }
QPixmap pixmap;
double min_val = 0;
double max_val = 0;
double last_ts = 0;
int time_range = 0;
private:
void render(const QColor &color, QSize size);
std::vector<QPointF> values;
std::vector<QPointF> points;
};

@ -149,6 +149,24 @@ void DBCFile::removeMsg(const MessageId &id) {
msgs.erase(id.address);
}
QString DBCFile::newMsgName(const MessageId &id) {
return QString("NEW_MSG_") + QString::number(id.address, 16).toUpper();
}
QString DBCFile::newSignalName(const MessageId &id) {
auto m = msg(id);
assert(m != nullptr);
QString name;
for (int i = 1; /**/; ++i) {
name = QString("NEW_SIGNAL_%1").arg(i);
if (m->sig(name) == nullptr) break;
}
return name;
}
std::map<uint32_t, cabana::Msg> DBCFile::getMessages() {
return msgs;
}

@ -38,6 +38,9 @@ public:
void updateMsg(const MessageId &id, const QString &name, uint32_t size);
void removeMsg(const MessageId &id);
QString newMsgName(const MessageId &id);
QString newSignalName(const MessageId &id);
std::map<uint32_t, cabana::Msg> getMessages();
const cabana::Msg *msg(const MessageId &id) const;
const cabana::Msg *msg(uint32_t address) const;

@ -133,6 +133,20 @@ void DBCManager::removeMsg(const MessageId &id) {
}
}
QString DBCManager::newMsgName(const MessageId &id) {
auto sources_dbc_file = findDBCFile(id);
assert(sources_dbc_file); // This should be impossible
auto [_, dbc_file] = *sources_dbc_file;
return dbc_file->newMsgName(id);
}
QString DBCManager::newSignalName(const MessageId &id) {
auto sources_dbc_file = findDBCFile(id);
assert(sources_dbc_file); // This should be impossible
auto [_, dbc_file] = *sources_dbc_file;
return dbc_file->newSignalName(id);
}
std::map<MessageId, cabana::Msg> DBCManager::getMessages(uint8_t source) {
std::map<MessageId, cabana::Msg> ret;

@ -33,6 +33,9 @@ public:
void updateMsg(const MessageId &id, const QString &name, uint32_t size);
void removeMsg(const MessageId &id);
QString newMsgName(const MessageId &id);
QString newSignalName(const MessageId &id);
std::map<MessageId, cabana::Msg> getMessages(uint8_t source);
const cabana::Msg *msg(const MessageId &id) const;
const cabana::Msg* msg(uint8_t source, const QString &name);

@ -432,6 +432,7 @@ void MainWindow::saveAs() {
QString fn = QFileDialog::getSaveFileName(this, tr("Save File"), QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)"));
if (!fn.isEmpty()) {
dbc_file->saveAs(fn);
updateRecentFiles(fn);
}
}
}
@ -473,24 +474,34 @@ void MainWindow::updateLoadSaveMenus() {
open_dbc_for_source->setEnabled(sources.size() > 0);
open_dbc_for_source->clear();
std::map<QString, QStringList> dbc_files;
for (uint8_t source : sources_sorted) {
if (source >= 64) continue; // Sent and blocked buses are handled implicitly
QAction *action = new QAction(this);
auto d = dbc()->findDBCFile(source);
QString name = tr("no DBC");
if (d && !d->second->name().isEmpty()) {
if (d) {
if (!d->second->name().isEmpty()) {
name = tr("%1").arg(d->second->name());
} else if (d) {
} else {
name = "untitled";
}
dbc_files[d->second->filename].push_back(QString::number(source));
}
action->setText(tr("Bus %1 (current: %2)").arg(source).arg(name));
action->setData(source);
QObject::connect(action, &QAction::triggered, this, &MainWindow::openFileForSource);
open_dbc_for_source->addAction(action);
}
QStringList title;
for (auto &[filename, sources] : dbc_files) {
QString bus = dbc_files.size() == 1 ? "all" : sources.join(",");
title.push_back("[" + bus + "]" + QFileInfo(filename).baseName());
}
setWindowFilePath(title.join(" | "));
}
void MainWindow::updateRecentFiles(const QString &fn) {

@ -25,14 +25,16 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
view = new MessageView(this);
model = new MessageListModel(this);
auto delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes);
view->setItemDelegateForColumn(5, delegate);
view->setItemDelegate(delegate);
view->setModel(model);
view->setSortingEnabled(true);
view->sortByColumn(0, Qt::AscendingOrder);
view->setAllColumnsShowFocus(true);
view->setEditTriggers(QAbstractItemView::NoEditTriggers);
view->setItemsExpandable(false);
view->setIndentation(0);
view->setRootIsDecorated(false);
view->header()->setStretchLastSection(true);
view->header()->setSectionsMovable(false);
main_layout->addWidget(view);
// suppress
@ -48,6 +50,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
QObject::connect(multiple_lines_bytes, &QCheckBox::stateChanged, [=](int state) {
settings.multiple_lines_bytes = (state == Qt::Checked);
delegate->setMultipleLines(settings.multiple_lines_bytes);
view->setUniformRowHeights(!settings.multiple_lines_bytes);
model->sortMessages();
});
QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived);
@ -129,12 +132,20 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
const auto &id = msgs[index.row()];
auto &can_data = can->lastMessage(id);
auto getFreq = [](const CanData &d) -> QString {
if (d.freq > 0 && (can->currentSec() - d.ts - 1.0 / settings.fps) < (5.0 / d.freq)) {
return d.freq >= 1 ? QString::number(std::nearbyint(d.freq)) : QString::number(d.freq, 'f', 2);
} else {
return "--";
}
};
if (role == Qt::DisplayRole) {
switch (index.column()) {
case 0: return msgName(id);
case 1: return id.source;
case 2: return QString::number(id.address, 16);;
case 3: return can_data.freq;
case 2: return QString::number(id.address, 16);
case 3: return getFreq(can_data);
case 4: return can_data.count;
case 5: return toHex(can_data.dat);
}
@ -148,7 +159,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
}
}
return QVariant::fromValue(colors);
} else if (role == BytesRole) {
} else if (role == BytesRole && index.column() == 5) {
return can_data.dat;
}
return {};
@ -268,9 +279,9 @@ void MessageListModel::reset() {
void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
QTreeView::drawRow(painter, option, index);
painter->save();
const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this);
const QColor gridColor = QColor::fromRgba(static_cast<QRgb>(gridHint));
QPen old_pen = painter->pen();
painter->setPen(gridColor);
painter->drawLine(option.rect.left(), option.rect.bottom(), option.rect.right(), option.rect.bottom());
@ -280,5 +291,12 @@ void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option,
painter->translate(header()->sectionSize(i), 0);
painter->drawLine(0, y, 0, y + option.rect.height());
}
painter->restore();
painter->setPen(old_pen);
painter->resetTransform();
}
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);
}

@ -40,6 +40,8 @@ class MessageView : public QTreeView {
public:
MessageView(QWidget *parent) : QTreeView(parent) {}
void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override {}
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
};
class MessagesWidget : public QWidget {

@ -30,13 +30,14 @@ void Settings::save() {
s.setValue("geometry", geometry);
s.setValue("video_splitter_state", video_splitter_state);
s.setValue("recent_files", recent_files);
s.setValue("message_header_state", message_header_state);
s.setValue("message_header_state_v2", message_header_state);
s.setValue("chart_series_type", chart_series_type);
s.setValue("theme", theme);
s.setValue("sparkline_range", sparkline_range);
s.setValue("multiple_lines_bytes", multiple_lines_bytes);
s.setValue("log_livestream", log_livestream);
s.setValue("log_path", log_path);
s.setValue("drag_direction", drag_direction);
}
void Settings::load() {
@ -52,13 +53,14 @@ void Settings::load() {
geometry = s.value("geometry").toByteArray();
video_splitter_state = s.value("video_splitter_state").toByteArray();
recent_files = s.value("recent_files").toStringList();
message_header_state = s.value("message_header_state").toByteArray();
message_header_state = s.value("message_header_state_v2").toByteArray();
chart_series_type = s.value("chart_series_type", 0).toInt();
theme = s.value("theme", 0).toInt();
sparkline_range = s.value("sparkline_range", 15).toInt();
multiple_lines_bytes = s.value("multiple_lines_bytes", true).toBool();
log_livestream = s.value("log_livestream", true).toBool();
log_path = s.value("log_path").toString();
drag_direction = (Settings::DragDirection)s.value("drag_direction", 0).toInt();
if (log_path.isEmpty()) {
log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/";
}
@ -91,6 +93,14 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
form_layout->addRow(tr("Max Cached Minutes"), cached_minutes);
main_layout->addWidget(groupbox);
groupbox = new QGroupBox("New Signal Settings");
form_layout = new QFormLayout(groupbox);
drag_direction = new QComboBox(this);
drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")});
drag_direction->setCurrentIndex(settings.drag_direction);
form_layout->addRow(tr("Drag Direction"), drag_direction);
main_layout->addWidget(groupbox);
groupbox = new QGroupBox("Chart");
form_layout = new QFormLayout(groupbox);
chart_series_type = new QComboBox(this);
@ -114,6 +124,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
path_layout->addWidget(browse_btn);
main_layout->addWidget(log_livestream);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply);
main_layout->addWidget(buttonBox);
main_layout->addStretch(1);
@ -151,6 +162,7 @@ void SettingsDlg::save() {
settings.chart_height = chart_height->value();
settings.log_livestream = log_livestream->isChecked();
settings.log_path = log_path->text();
settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex();
settings.save();
emit settings.changed();
}

@ -15,6 +15,13 @@ class Settings : public QObject {
Q_OBJECT
public:
enum DragDirection {
MsbFirst,
LsbFirst,
AlwaysLE,
AlwaysBE,
};
Settings();
void save();
void load();
@ -37,6 +44,7 @@ public:
QByteArray window_state;
QStringList recent_files;
QByteArray message_header_state;
DragDirection drag_direction;
signals:
void changed();
@ -55,6 +63,7 @@ public:
QComboBox *theme;
QGroupBox *log_livestream;
QLineEdit *log_path;
QComboBox *drag_direction;
};
extern Settings settings;

@ -8,7 +8,10 @@
#include <QHelpEvent>
#include <QMessageBox>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QScrollBar>
#include <QtConcurrent>
#include <QVBoxLayout>
#include "tools/cabana/commands.h"
@ -22,7 +25,6 @@ SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(p
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 cabana::Signal *sig) {
@ -37,9 +39,7 @@ void SignalModel::insertItem(SignalModel::Item *parent_item, int pos, const caba
void SignalModel::setMessage(const MessageId &id) {
msg_id = id;
filter_str = "";
value_width = 0;
refresh();
updateState(nullptr);
}
void SignalModel::setFilter(const QString &txt) {
@ -60,33 +60,6 @@ void SignalModel::refresh() {
endResetModel();
}
void SignalModel::updateState(const QHash<MessageId, CanData> *msgs) {
if (!msgs || msgs->contains(msg_id)) {
auto &dat = can->lastMessage(msg_id).dat;
for (auto item : root->children) {
double value = get_raw_value((uint8_t *)dat.constData(), dat.size(), *item->sig);
item->sig_val = QString::number(value, 'f', item->sig->precision);
// Show unit
if (!item->sig->unit.isEmpty()) {
item->sig_val += " " + item->sig->unit;
}
// Show enum string
for (auto &[val, desc] : item->sig->val_desc) {
if (std::abs(value - val.toInt()) < 1e-6) {
item->sig_val = desc;
}
}
value_width = std::max(value_width, QFontMetrics(QFont()).width(item->sig_val));
}
for (int i = 0; i < root->children.size(); ++i) {
emit dataChanged(index(i, 1), index(i, 1), {Qt::DisplayRole});
}
}
}
SignalModel::Item *SignalModel::getItem(const QModelIndex &index) const {
SignalModel::Item *item = nullptr;
if (index.isValid()) {
@ -251,19 +224,13 @@ bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s)
void SignalModel::addSignal(int start_bit, int size, bool little_endian) {
auto msg = dbc()->msg(msg_id);
for (int i = 0; !msg; ++i) {
QString name = QString("NEW_MSG_") + QString::number(msg_id.address, 16).toUpper();
if (!dbc()->msg(msg_id.source, name)) {
if (!msg) {
QString name = dbc()->newMsgName(msg_id);
UndoStack::push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size()));
msg = dbc()->msg(msg_id);
}
}
cabana::Signal sig = {.is_little_endian = little_endian, .factor = 1, .min = "0", .max = QString::number(std::pow(2, size) - 1)};
for (int i = 1; /**/; ++i) {
sig.name = QString("NEW_SIGNAL_%1").arg(i);
if (msg->sig(sig.name) == nullptr) break;
}
cabana::Signal sig = {.name = dbc()->newSignalName(msg_id), .is_little_endian = little_endian, .factor = 1, .min = "0", .max = QString::number(std::pow(2, size) - 1)};
updateSigSizeParamsFromRange(sig, start_bit, size);
UndoStack::push(new AddSigCommand(msg_id, sig));
}
@ -276,6 +243,9 @@ void SignalModel::resizeSignal(const cabana::Signal *sig, int start_bit, int siz
void SignalModel::removeSignal(const cabana::Signal *sig) {
UndoStack::push(new RemoveSigCommand(msg_id, sig));
if (dbc()->signalCount(msg_id) == 0) {
UndoStack::push(new RemoveMsgCommand(msg_id));
}
}
void SignalModel::handleMsgChanged(MessageId id) {
@ -293,7 +263,6 @@ void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
beginInsertRows({}, i, i);
insertItem(root.get(), i, sig);
endInsertRows();
updateState(nullptr);
}
}
@ -355,117 +324,65 @@ void SignalItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptio
QRect geom = option.rect;
geom.setLeft(geom.right() - editor->sizeHint().width());
editor->setGeometry(geom);
button_size = geom.size();
return;
}
QStyledItemDelegate::updateEditorGeometry(editor, option, index);
}
void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin);
auto item = (SignalModel::Item *)index.internalPointer();
if (index.column() == 0 && item && item->type == SignalModel::Item::Sig) {
painter->save();
if (item && item->type == SignalModel::Item::Sig) {
painter->setRenderHint(QPainter::Antialiasing);
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
}
int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin);
QRect r = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin);
if (index.column() == 0) {
// color label
auto bg_color = getColor(item->sig);
QRect rc{option.rect.left() + h_margin, option.rect.top(), color_label_width, option.rect.height()};
painter->setPen(Qt::NoPen);
painter->setBrush(item->highlight ? bg_color.darker(125) : bg_color);
painter->drawRoundedRect(rc.adjusted(0, v_margin, 0, -v_margin), 3, 3);
QPainterPath path;
QRect icon_rect{r.x(), r.y(), color_label_width, r.height()};
path.addRoundedRect(icon_rect, 3, 3);
painter->setPen(item->highlight ? Qt::white : Qt::black);
painter->setFont(label_font);
painter->drawText(rc, Qt::AlignCenter, QString::number(item->row() + 1));
painter->fillPath(path, getColor(item->sig).darker(item->highlight ? 125 : 0));
painter->drawText(icon_rect, Qt::AlignCenter, QString::number(item->row() + 1));
// signal name
painter->setFont(option.font);
r.setLeft(icon_rect.right() + h_margin * 2);
auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, r.width());
painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text));
QString text = index.data(Qt::DisplayRole).toString();
QRect text_rect = option.rect;
text_rect.setLeft(rc.right() + h_margin * 2);
text = painter->fontMetrics().elidedText(text, Qt::ElideRight, text_rect.width());
painter->drawText(text_rect, option.displayAlignment, text);
painter->restore();
} else if (index.column() == 1 && item && item->type == SignalModel::Item::Sig) {
painter->save();
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
}
SignalModel *model = (SignalModel*)index.model();
int adjust_right = ((SignalView *)parent())->tree->indexWidget(index)->sizeHint().width() + 2 * h_margin;
QRect r = option.rect.adjusted(h_margin, v_margin, -adjust_right, -v_margin);
int value_width = std::min<int>(model->value_width, r.width() * 0.4);
QRect value_rect = r.adjusted(r.width() - value_width - h_margin, 0, 0, 0);
auto text = painter->fontMetrics().elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, value_rect.width());
painter->setFont(option.font);
painter->drawText(r, option.displayAlignment, text);
} else if (index.column() == 1) {
// sparkline
QSize sparkline_size = item->sparkline.pixmap.size() / item->sparkline.pixmap.devicePixelRatio();
painter->drawPixmap(QRect(r.topLeft(), sparkline_size), item->sparkline.pixmap);
// min-max value
painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text));
painter->drawText(value_rect, Qt::AlignRight | Qt::AlignVCenter, text);
drawSparkline(painter, r.adjusted(0, 0, -value_width, 0), option, index);
painter->restore();
} else {
QStyledItemDelegate::paint(painter, option, index);
}
}
void SignalItemDelegate::drawSparkline(QPainter *painter, const QRect &rect, const QStyleOptionViewItem &option, const QModelIndex &index) const {
static std::vector<QPointF> points;
const auto &msg_id = ((SignalView *)parent())->msg_id;
const auto &msgs = can->events().at(msg_id);
uint64_t ts = (can->lastMessage(msg_id).ts + can->routeStartTime()) * 1e9;
auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), CanEvent{.mono_time = (uint64_t)std::max<int64_t>(ts - settings.sparkline_range * 1e9, 0)});
auto last = std::upper_bound(first, msgs.cend(), CanEvent{.mono_time = ts});
if (first != last) {
double min = std::numeric_limits<double>::max();
double max = std::numeric_limits<double>::lowest();
const auto item = (const SignalModel::Item *)index.internalPointer();
const auto sig = item->sig;
points.clear();
for (auto it = first; it != last; ++it) {
double value = get_raw_value(it->dat, it->size, *sig);
points.emplace_back((it->mono_time - first->mono_time) / 1e9, value);
min = std::min(min, value);
max = std::max(max, value);
}
if (min == max) {
min -= 1;
max += 1;
}
const double min_max_width = std::min(rect.width() - 10, QFontMetrics(minmax_font).width("000.00") + 5);
const QRect r = rect.adjusted(0, 0, -min_max_width, 0);
const double xscale = r.width() / (double)settings.sparkline_range;
const double yscale = r.height() / (max - min);
for (auto &pt : points) {
pt.rx() = r.left() + pt.x() * xscale;
pt.ry() = r.top() + std::abs(pt.y() - max) * yscale;
}
auto color = item->highlight ? getColor(sig).darker(125) : getColor(sig);
painter->setPen(color);
painter->drawPolyline(points.data(), points.size());
if ((points.back().x() - points.front().x()) / points.size() > 10) {
painter->setPen(Qt::NoPen);
painter->setBrush(color);
for (const auto &pt : points) {
painter->drawEllipse(pt, 2, 2);
}
}
QRect rect = r.adjusted(sparkline_size.width() + 1, 0, 0, 0);
int value_adjust = 10;
if (item->highlight || option.state & QStyle::State_Selected) {
painter->drawLine(rect.topLeft(), rect.bottomLeft());
rect.adjust(5, -v_margin, 0, v_margin);
painter->setFont(minmax_font);
painter->setPen(option.state & QStyle::State_Selected ? option.palette.color(QPalette::HighlightedText) : Qt::darkGray);
painter->drawLine(r.topRight(), r.bottomRight());
QRect minmax_rect{r.right() + 5, r.top(), 1000, r.height()};
painter->drawText(minmax_rect, Qt::AlignLeft | Qt::AlignTop, QString::number(max));
painter->drawText(minmax_rect, Qt::AlignLeft | Qt::AlignBottom, QString::number(min));
QString min = QString::number(item->sparkline.min_val);
QString max = QString::number(item->sparkline.max_val);
painter->drawText(rect, Qt::AlignLeft | Qt::AlignTop, max);
painter->drawText(rect, Qt::AlignLeft | Qt::AlignBottom, min);
QFontMetrics fm(minmax_font);
value_adjust = std::max(fm.width(min), fm.width(max)) + 5;
}
// value
painter->setFont(option.font);
rect.adjust(value_adjust, 0, -button_size.width(), 0);
auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, rect.width());
painter->drawText(rect, Qt::AlignRight | Qt::AlignVCenter, text);
}
} else {
QStyledItemDelegate::paint(painter, option, index);
}
}
@ -534,7 +451,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts),
// tree view
tree = new TreeView(this);
tree->setModel(model = new SignalModel(this));
tree->setItemDelegate(new SignalItemDelegate(this));
tree->setItemDelegate(delegate = new SignalItemDelegate(this));
tree->setFrameShape(QFrame::NoFrame);
tree->setHeaderHidden(true);
tree->setMouseTracking(true);
@ -560,6 +477,10 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts),
QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged);
QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged);
QObject::connect(dbc(), &DBCManager::signalAdded, [this](MessageId id, const cabana::Signal *sig) { selectSignal(sig); });
QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState);
QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalView::handleSignalUpdated);
QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); });
QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); });
setWhatsThis(tr(R"(
<b>Signal view</b><br />
@ -568,7 +489,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts),
}
void SignalView::setMessage(const MessageId &id) {
msg_id = id;
max_value_width = 0;
filter_edit->clear();
model->setMessage(id);
}
@ -594,12 +515,13 @@ void SignalView::rowsChanged() {
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);
emit showChart(model->msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier);
});
}
}
updateToolBar();
updateChartState();
updateState();
}
void SignalView::rowClicked(const QModelIndex &index) {
@ -626,7 +548,7 @@ void SignalView::selectSignal(const cabana::Signal *sig, bool expand) {
void SignalView::updateChartState() {
int i = 0;
for (auto item : model->root->children) {
bool chart_opened = charts->hasSignal(msg_id, item->sig);
bool chart_opened = charts->hasSignal(model->msg_id, item->sig);
auto buttons = tree->indexWidget(model->index(i, 1))->findChildren<QToolButton *>();
if (buttons.size() > 0) {
buttons[0]->setChecked(chart_opened);
@ -655,7 +577,70 @@ void SignalView::updateToolBar() {
void SignalView::setSparklineRange(int value) {
settings.sparkline_range = value;
updateToolBar();
model->updateState(nullptr);
updateState();
}
void SignalView::handleSignalUpdated(const cabana::Signal *sig) {
if (int row = model->signalRow(sig); row != -1) {
auto item = model->getItem(model->index(row, 1));
// invalidate the sparkline
item->sparkline.last_ts = 0;
updateState();
}
}
void SignalView::updateState(const QHash<MessageId, CanData> *msgs) {
if (model->rowCount() == 0 || (msgs && !msgs->contains(model->msg_id))) return;
const auto &last_msg = can->lastMessage(model->msg_id);
for (auto item : model->root->children) {
double value = get_raw_value((uint8_t *)last_msg.dat.constData(), last_msg.dat.size(), *item->sig);
item->sig_val = QString::number(value, 'f', item->sig->precision);
// Show unit
if (!item->sig->unit.isEmpty()) {
item->sig_val += " " + item->sig->unit;
}
// Show enum string
for (auto &[val, desc] : item->sig->val_desc) {
if (std::abs(value - val.toInt()) < 1e-6) {
item->sig_val = desc;
}
}
max_value_width = std::max(max_value_width, fontMetrics().width(item->sig_val));
}
// update visible sparkline
QSize size(tree->columnWidth(1) - delegate->button_size.width(), delegate->button_size.height());
int min_max_width = std::min(size.width() - 10, QFontMetrics(delegate->minmax_font).width("-000.00") + 5);
int value_width = std::min<int>(max_value_width, size.width() * 0.35);
size -= {value_width + min_max_width, style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2};
QModelIndex top = tree->indexAt(QPoint(0, 0));
QModelIndex bottom = tree->indexAt(tree->viewport()->rect().bottomLeft());
int start_row = top.parent().isValid() ? top.parent().row() + 1 : top.row();
int end_row = model->rowCount() - 1;
if (bottom.isValid()) {
end_row = bottom.parent().isValid() ? bottom.parent().row() : bottom.row();
}
QFutureSynchronizer<void> synchronizer;
for (int i = start_row; i <= end_row; ++i) {
auto item = model->getItem(model->index(i, 1));
auto &s = item->sparkline;
if (s.last_ts != last_msg.ts || s.size() != size || s.time_range != settings.sparkline_range) {
synchronizer.addFuture(QtConcurrent::run(
&s, &Sparkline::update, model->msg_id, item->sig, last_msg.ts, settings.sparkline_range, size));
}
}
synchronizer.waitForFinished();
for (int i = 0; i < model->rowCount(); ++i) {
emit model->dataChanged(model->index(i, 1), model->index(i, 1), {Qt::DisplayRole});
}
}
void SignalView::resizeEvent(QResizeEvent* event) {
updateState();
QFrame::resizeEvent(event);
}
void SignalView::leaveEvent(QEvent *event) {

@ -9,6 +9,7 @@
#include <QTreeView>
#include "tools/cabana/chart/chartswidget.h"
#include "tools/cabana/chart/sparkline.h"
class SignalModel : public QAbstractItemModel {
Q_OBJECT
@ -27,6 +28,7 @@ public:
bool highlight = false;
bool extra_expanded = false;
QString sig_val = "-";
Sparkline sparkline;
};
SignalModel(QObject *parent);
@ -54,12 +56,10 @@ private:
void handleSignalRemoved(const cabana::Signal *sig);
void handleMsgChanged(MessageId id);
void refresh();
void updateState(const QHash<MessageId, CanData> *msgs);
MessageId msg_id;
QString filter_str;
std::unique_ptr<Item> root;
int value_width = 0;
friend class SignalView;
friend class SignalItemDelegate;
};
@ -86,11 +86,12 @@ public:
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override;
void drawSparkline(QPainter *painter, const QRect &rect, const QStyleOptionViewItem &option, const QModelIndex &index) const;
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QValidator *name_validator, *double_validator;
QFont label_font, minmax_font;
const int color_label_width = 18;
mutable QSize button_size;
mutable QHash<QString, int> width_cache;
};
@ -105,7 +106,6 @@ public:
void selectSignal(const cabana::Signal *sig, bool expand = false);
void rowClicked(const QModelIndex &index);
SignalModel *model = nullptr;
MessageId msg_id;
signals:
void highlight(const cabana::Signal *sig);
@ -113,9 +113,12 @@ signals:
private:
void rowsChanged();
void leaveEvent(QEvent *event);
void leaveEvent(QEvent *event) override;
void resizeEvent(QResizeEvent* event) override;
void updateToolBar();
void setSparklineRange(int value);
void handleSignalUpdated(const cabana::Signal *sig);
void updateState(const QHash<MessageId, CanData> *msgs = nullptr);
struct TreeView : public QTreeView {
TreeView(QWidget *parent) : QTreeView(parent) {}
@ -124,13 +127,18 @@ private:
// update widget geometries in QTreeView::rowsInserted
QTreeView::rowsInserted(parent, start, end);
}
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override {
// Bypass the slow call to QTreeView::dataChanged.
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
}
};
int max_value_width = 0;
TreeView *tree;
QLabel *sparkline_label;
QSlider *sparkline_range_slider;
QLineEdit *filter_edit;
ChartsWidget *charts;
QLabel *signal_count_lb;
SignalItemDelegate *delegate;
friend SignalItemDelegate;
};

@ -17,7 +17,7 @@ struct CanData {
double ts = 0.;
uint32_t count = 0;
uint32_t freq = 0;
double freq = 0;
QByteArray dat;
QVector<QColor> colors;
QVector<double> last_change_t;

@ -0,0 +1,27 @@
#include "tools/cabana/streams/devicestream.h"
DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) {
}
void DeviceStream::streamThread() {
if (!zmq_address.isEmpty()) {
setenv("ZMQ", "1", 1);
}
std::unique_ptr<Context> context(Context::create());
std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString();
std::unique_ptr<SubSocket> sock(SubSocket::create(context.get(), "can", address));
assert(sock != NULL);
sock->setTimeout(50);
// run as fast as messages come in
while (!QThread::currentThread()->isInterruptionRequested()) {
Message *msg = sock->receive(true);
if (!msg) {
QThread::msleep(50);
continue;
}
std::lock_guard lk(lock);
handleEvent(messages.emplace_back(msg).event);
}
}

@ -0,0 +1,17 @@
#pragma once
#include "tools/cabana/streams/livestream.h"
class DeviceStream : public LiveStream {
Q_OBJECT
public:
DeviceStream(QObject *parent, QString address = {});
inline QString routeName() const override {
return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address);
}
protected:
void streamThread() override;
const QString zmq_address;
};

@ -2,7 +2,13 @@
#include <QTimer>
LiveStream::LiveStream(QObject *parent, QString address) : zmq_address(address), AbstractStream(parent, true) {
LiveStream::LiveStream(QObject *parent) : AbstractStream(parent, true) {
if (settings.log_livestream) {
std::string path = (settings.log_path + "/" + QDateTime::currentDateTime().toString("yyyy-MM-dd--hh-mm-ss") + "--0").toStdString();
util::create_directories(path, 0755);
fs.reset(new std::ofstream(path + "/rlog" , std::ios::binary | std::ios::out));
}
stream_thread = new QThread(this);
QObject::connect(stream_thread, &QThread::started, [=]() { streamThread(); });
QObject::connect(stream_thread, &QThread::finished, stream_thread, &QThread::deleteLater);
@ -15,39 +21,12 @@ LiveStream::~LiveStream() {
stream_thread->wait();
}
void LiveStream::streamThread() {
if (!zmq_address.isEmpty()) {
setenv("ZMQ", "1", 1);
}
std::unique_ptr<std::ofstream> fs;
if (settings.log_livestream) {
std::string path = (settings.log_path + "/" + QDateTime::currentDateTime().toString("yyyy-MM-dd--hh-mm-ss") + "--0").toStdString();
util::create_directories(path, 0755);
fs.reset(new std::ofstream(path + "/rlog" , std::ios::binary | std::ios::out));
}
std::unique_ptr<Context> context(Context::create());
std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString();
std::unique_ptr<SubSocket> sock(SubSocket::create(context.get(), "can", address));
assert(sock != NULL);
sock->setTimeout(50);
// run as fast as messages come in
while (!QThread::currentThread()->isInterruptionRequested()) {
Message *msg = sock->receive(true);
if (!msg) {
QThread::msleep(50);
continue;
}
void LiveStream::handleEvent(Event *evt) {
if (fs) {
fs->write(msg->getData(), msg->getSize());
}
std::lock_guard lk(lock);
handleEvent(messages.emplace_back(msg).event);
}
auto bytes = evt->words.asChars();
fs->write(bytes.begin(), bytes.size());
}
void LiveStream::handleEvent(Event *evt) {
if (start_ts == 0 || evt->mono_time < start_ts) {
if (evt->mono_time < start_ts) {
qDebug() << "stream is looping back to old time stamp";

@ -6,11 +6,8 @@ class LiveStream : public AbstractStream {
Q_OBJECT
public:
LiveStream(QObject *parent, QString address = {});
LiveStream(QObject *parent);
virtual ~LiveStream();
inline QString routeName() const override {
return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address);
}
inline double routeStartTime() const override { return start_ts / (double)1e9; }
inline double currentSec() const override { return (current_ts - start_ts) / (double)1e9; }
void setSpeed(float speed) override { speed_ = std::min<float>(1.0, speed); }
@ -19,7 +16,7 @@ public:
protected:
virtual void handleEvent(Event *evt);
virtual void streamThread();
virtual void streamThread() = 0;
void process(QHash<MessageId, CanData> *) override;
struct Msg {
@ -27,6 +24,9 @@ protected:
event = ::new Event(aligned_buf.align(m));
delete m;
}
Msg(const char *data, const size_t size) {
event = ::new Event(aligned_buf.align(data, size));
}
~Msg() { ::delete event; }
Event *event;
AlignedBuffer aligned_buf;
@ -41,6 +41,7 @@ protected:
std::atomic<bool> pause_ = false;
uint64_t last_update_ts = 0;
const QString zmq_address;
std::unique_ptr<std::ofstream> fs;
QThread *stream_thread;
};

@ -0,0 +1,85 @@
#include "tools/cabana/streams/pandastream.h"
PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {
if (config.serial.isEmpty()) {
auto serials = Panda::list();
if (serials.size() == 0) {
throw std::runtime_error("No panda found");
}
config.serial = QString::fromStdString(serials[0]);
}
qDebug() << "Connecting to panda with serial" << config.serial;
if (!connect()) {
throw std::runtime_error("Failed to connect to panda");
}
}
bool PandaStream::connect() {
try {
panda.reset(new Panda(config.serial.toStdString()));
config.bus_config.resize(3);
qDebug() << "Connected";
} catch (const std::exception& e) {
return false;
}
panda->set_safety_model(cereal::CarParams::SafetyModel::SILENT);
for (int bus = 0; bus < config.bus_config.size(); bus++) {
panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps);
// CAN-FD
if (panda->hw_type == cereal::PandaState::PandaType::RED_PANDA || panda->hw_type == cereal::PandaState::PandaType::RED_PANDA_V2) {
if (config.bus_config[bus].can_fd) {
panda->set_data_speed_kbps(bus, config.bus_config[bus].data_speed_kbps);
} else {
// Hack to disable can-fd by setting data speed to a low value
panda->set_data_speed_kbps(bus, 10);
}
}
}
return true;
}
void PandaStream::streamThread() {
std::vector<can_frame> raw_can_data;
while (!QThread::currentThread()->isInterruptionRequested()) {
QThread::msleep(1);
if (!panda->connected()) {
qDebug() << "Connection to panda lost. Attempting reconnect.";
if (!connect()){
QThread::msleep(1000);
continue;
}
}
raw_can_data.clear();
if (!panda->can_receive(raw_can_data)) {
qDebug() << "failed to receive";
continue;
}
MessageBuilder msg;
auto evt = msg.initEvent();
auto canData = evt.initCan(raw_can_data.size());
for (uint i = 0; i<raw_can_data.size(); i++) {
canData[i].setAddress(raw_can_data[i].address);
canData[i].setBusTime(raw_can_data[i].busTime);
canData[i].setDat(kj::arrayPtr((uint8_t*)raw_can_data[i].dat.data(), raw_can_data[i].dat.size()));
canData[i].setSrc(raw_can_data[i].src);
}
{
std::lock_guard lk(lock);
auto bytes = msg.toBytes();
handleEvent(messages.emplace_back((const char*)bytes.begin(), bytes.size()).event);
}
panda->send_heartbeat(false);
}
}

@ -0,0 +1,33 @@
#pragma once
#include "tools/cabana/streams/livestream.h"
#include "selfdrive/boardd/panda.h"
struct BusConfig {
int can_speed_kbps = 500;
int data_speed_kbps = 2000;
bool can_fd = false;
};
struct PandaStreamConfig {
QString serial = "";
std::vector<BusConfig> bus_config;
};
class PandaStream : public LiveStream {
Q_OBJECT
public:
PandaStream(QObject *parent, PandaStreamConfig config_ = {});
inline QString routeName() const override {
return QString("Live Streaming From Panda %1").arg(config.serial);
}
protected:
void streamThread() override;
bool connect();
std::unique_ptr<Panda> panda;
PandaStreamConfig config = {};
};

@ -3,6 +3,7 @@
#include <QFontDatabase>
#include <QPainter>
#include <QPixmapCache>
#include <QToolTip>
#include "selfdrive/ui/qt/util.h"
@ -53,13 +54,17 @@ void MessageBytesDelegate::setMultipleLines(bool v) {
}
QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
int n = index.data(BytesRole).toByteArray().size();
if (n <= 0 || n > 64) return {};
int v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1;
auto data = index.data(BytesRole);
if (!data.isValid()) {
return {1, byte_size.height() + 2 * v_margin};
}
int n = data.toByteArray().size();
assert(n > 0 && n <= 64);
QSize size = size_cache[n - 1];
if (size.isEmpty()) {
int h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
int v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1;
if (!multiple_lines) {
size.setWidth(h_margin * 2 + n * byte_size.width());
size.setHeight(byte_size.height() + 2 * v_margin);
@ -72,30 +77,53 @@ QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const Q
return size;
}
bool MessageBytesDelegate::helpEvent(QHelpEvent *e, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) {
if (e->type() == QEvent::ToolTip && index.column() == 0) {
if (view->visualRect(index).width() < QStyledItemDelegate::sizeHint(option, index).width()) {
QToolTip::showText(e->globalPos(), index.data(Qt::DisplayRole).toString(), view);
return true;
}
}
QToolTip::hideText();
return false;
}
void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
auto data = index.data(BytesRole);
if (!data.isValid()) {
return QStyledItemDelegate::paint(painter, option, index);
}
auto byte_list = data.toByteArray();
auto colors = index.data(ColorsRole).value<QVector<QColor>>();
auto byte_list = index.data(BytesRole).toByteArray();
int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin);
int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin);
painter->save();
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
painter->setPen(option.palette.color(QPalette::HighlightedText));
}
const QPoint pt{option.rect.left() + h_margin, option.rect.top() + v_margin};
QFont old_font = painter->font();
QPen old_pen = painter->pen();
painter->setFont(fixed_font);
for (int i = 0; i < byte_list.size(); ++i) {
int row = !multiple_lines ? 0 : i / 8;
int column = !multiple_lines ? i : i % 8;
QRect r = QRect({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size);
if (i < colors.size() && colors[i].alpha() > 0) {
if (option.state & QStyle::State_Selected) {
painter->setPen(option.palette.color(QPalette::Text));
painter->fillRect(r, option.palette.color(QPalette::Window));
}
painter->fillRect(r, colors[i]);
} else if (option.state & QStyle::State_Selected) {
painter->setPen(option.palette.color(QPalette::HighlightedText));
}
painter->drawText(r, Qt::AlignCenter, toHex(byte_list[i]));
}
painter->restore();
painter->setFont(old_font);
painter->setPen(old_pen);
}
QColor getColor(const cabana::Signal *sig) {

@ -2,6 +2,7 @@
#include <cmath>
#include <QAbstractItemView>
#include <QApplication>
#include <QByteArray>
#include <QDateTime>
@ -66,6 +67,7 @@ public:
MessageBytesDelegate(QObject *parent, bool multiple_lines = false);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override;
void setMultipleLines(bool v);
private:

Loading…
Cancel
Save