diff --git a/.github/workflows/repo.yml b/.github/workflows/repo.yml new file mode 100644 index 0000000000..1445aa635d --- /dev/null +++ b/.github/workflows/repo.yml @@ -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 diff --git a/cereal b/cereal index 4f5502c865..911cfac392 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 4f5502c8657813ccb538e9575ca1a7b258b17af9 +Subproject commit 911cfac392d6dc05162a44a3af9d28307185ecd7 diff --git a/selfdrive/boardd/SConscript b/selfdrive/boardd/SConscript index d99e67a9f0..ac92d7d710 100644 --- a/selfdrive/boardd/SConscript +++ b/selfdrive/boardd/SConscript @@ -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"]) diff --git a/selfdrive/boardd/panda.cc b/selfdrive/boardd/panda.cc index 4ad4b5e652..4873040f37 100644 --- a/selfdrive/boardd/panda.cc +++ b/selfdrive/boardd/panda.cc @@ -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); diff --git a/selfdrive/car/ford/fordcan.py b/selfdrive/car/ford/fordcan.py index 594d50f59f..d2fb6fad55 100644 --- a/selfdrive/car/ford/fordcan.py +++ b/selfdrive/car/ford/fordcan.py @@ -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, diff --git a/selfdrive/car/volkswagen/mqbcan.py b/selfdrive/car/volkswagen/mqbcan.py index 89de933ae8..b461fd02ae 100644 --- a/selfdrive/car/volkswagen/mqbcan.py +++ b/selfdrive/car/volkswagen/mqbcan.py @@ -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) diff --git a/tools/cabana/README.md b/tools/cabana/README.md index 4faa7c61d0..921decff3c 100644 --- a/tools/cabana/README.md +++ b/tools/cabana/README.md @@ -11,19 +11,23 @@ $ ./cabana -h Usage: ./cabana [options] route Options: - -h, --help Displays this help. - --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 the ip address on which to receive zmq messages - --data_dir local directory with routes - --no-vipc do not output video - --dbc dbc file to open + -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 + --panda read can messages from panda + --panda-serial read can messages from panda with given serial + --zmq the ip address on which to receive zmq + messages + --data_dir local directory with routes + --no-vipc do not output video + --dbc dbc file to open Arguments: - route the drive to replay. find your drives at - connect.comma.ai + route the drive to replay. find your drives at + connect.comma.ai ``` See [openpilot wiki](https://github.com/commaai/openpilot/wiki/Cabana) diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 77738afae3..4acbc4fa66 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -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) diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index d0576615c9..3a03ee0b70 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -239,7 +239,19 @@ std::tuple 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]); } } diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h index aa1f8c656b..f80b4520ed 100644 --- a/tools/cabana/binaryview.h +++ b/tools/cabana/binaryview.h @@ -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; } diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 10b394cec5..2f7a560600 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -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 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__ diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index 84bd3885cf..d015e97c9f 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -314,7 +314,12 @@ void ChartsWidget::updateLayout(bool force) { } for (int i = 0; i < current_charts.size(); ++i) { charts_layout->addWidget(current_charts[i], i / n, i % n); - current_charts[i]->setVisible(true); + 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); } diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc new file mode 100644 index 0000000000..dd2720aa54 --- /dev/null +++ b/tools/cabana/chart/sparkline.cc @@ -0,0 +1,68 @@ +#include "tools/cabana/chart/sparkline.h" + +#include + +#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(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::max(); + max_val = std::numeric_limits::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()); + } +} diff --git a/tools/cabana/chart/sparkline.h b/tools/cabana/chart/sparkline.h new file mode 100644 index 0000000000..69d4f4bc55 --- /dev/null +++ b/tools/cabana/chart/sparkline.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#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 values; + std::vector points; +}; diff --git a/tools/cabana/dbc/dbcfile.cc b/tools/cabana/dbc/dbcfile.cc index c4025725ac..8d6b0f656f 100644 --- a/tools/cabana/dbc/dbcfile.cc +++ b/tools/cabana/dbc/dbcfile.cc @@ -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 DBCFile::getMessages() { return msgs; } diff --git a/tools/cabana/dbc/dbcfile.h b/tools/cabana/dbc/dbcfile.h index 96fbe6a9f1..e048dc5839 100644 --- a/tools/cabana/dbc/dbcfile.h +++ b/tools/cabana/dbc/dbcfile.h @@ -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 getMessages(); const cabana::Msg *msg(const MessageId &id) const; const cabana::Msg *msg(uint32_t address) const; diff --git a/tools/cabana/dbc/dbcmanager.cc b/tools/cabana/dbc/dbcmanager.cc index 123d783986..b17d535131 100644 --- a/tools/cabana/dbc/dbcmanager.cc +++ b/tools/cabana/dbc/dbcmanager.cc @@ -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 DBCManager::getMessages(uint8_t source) { std::map ret; diff --git a/tools/cabana/dbc/dbcmanager.h b/tools/cabana/dbc/dbcmanager.h index 60d8a072ec..a1b9e2ae3a 100644 --- a/tools/cabana/dbc/dbcmanager.h +++ b/tools/cabana/dbc/dbcmanager.h @@ -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 getMessages(uint8_t source); const cabana::Msg *msg(const MessageId &id) const; const cabana::Msg* msg(uint8_t source, const QString &name); diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 5b6d436dca..922ea7a9e2 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -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 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()) { - name = tr("%1").arg(d->second->name()); - } else if (d) { - name = "untitled"; + if (d) { + if (!d->second->name().isEmpty()) { + name = tr("%1").arg(d->second->name()); + } 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) { diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index abc8c755ff..c11d9d7d78 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -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(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 &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); } diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index cc48b70bf4..a30adc5dcd 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -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 &roles = QVector()) override; }; class MessagesWidget : public QWidget { diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index 4d8c4fcc5a..07fb312c3b 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -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(); } diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index 07aafe1682..11548ef5cc 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -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; diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index ef05decda6..32b0a39408 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -8,7 +8,10 @@ #include #include #include +#include #include +#include +#include #include #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 *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)) { - UndoStack::push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size())); - msg = dbc()->msg(msg_id); - } + 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,120 +324,68 @@ 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()); } - // 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); - painter->setPen(item->highlight ? Qt::white : Qt::black); - painter->setFont(label_font); - painter->drawText(rc, Qt::AlignCenter, QString::number(item->row() + 1)); - - // signal name - painter->setFont(option.font); - 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()); + 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 + 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->fillPath(path, getColor(item->sig).darker(item->highlight ? 125 : 0)); + painter->drawText(icon_rect, Qt::AlignCenter, QString::number(item->row() + 1)); + + 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)); + 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)); + 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); + 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); } - - 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(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->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 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(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::max(); - double max = std::numeric_limits::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); - } - } - - if (item->highlight || option.state & QStyle::State_Selected) { - 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)); - } - } -} - 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 || @@ -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"( Signal view
@@ -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(); 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 *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(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 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) { diff --git a/tools/cabana/signalview.h b/tools/cabana/signalview.h index a2102ff9a7..02741234a6 100644 --- a/tools/cabana/signalview.h +++ b/tools/cabana/signalview.h @@ -9,6 +9,7 @@ #include #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 *msgs); MessageId msg_id; QString filter_str; std::unique_ptr 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; + 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 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 *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 &roles = QVector()) 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; }; diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index caa17ab177..fa1c5f54e8 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -17,7 +17,7 @@ struct CanData { double ts = 0.; uint32_t count = 0; - uint32_t freq = 0; + double freq = 0; QByteArray dat; QVector colors; QVector last_change_t; diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc new file mode 100644 index 0000000000..137fe0cee6 --- /dev/null +++ b/tools/cabana/streams/devicestream.cc @@ -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::create()); + std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); + std::unique_ptr 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); + } +} diff --git a/tools/cabana/streams/devicestream.h b/tools/cabana/streams/devicestream.h new file mode 100644 index 0000000000..715dcd17e0 --- /dev/null +++ b/tools/cabana/streams/devicestream.h @@ -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; +}; diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc index 0fcee4455a..a7aab1ab6d 100644 --- a/tools/cabana/streams/livestream.cc +++ b/tools/cabana/streams/livestream.cc @@ -2,7 +2,13 @@ #include -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 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::create()); - std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); - std::unique_ptr 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; - } - - if (fs) { - fs->write(msg->getData(), msg->getSize()); - } - std::lock_guard lk(lock); - handleEvent(messages.emplace_back(msg).event); +void LiveStream::handleEvent(Event *evt) { + if (fs) { + 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"; diff --git a/tools/cabana/streams/livestream.h b/tools/cabana/streams/livestream.h index 598c0b9365..880e5b95ab 100644 --- a/tools/cabana/streams/livestream.h +++ b/tools/cabana/streams/livestream.h @@ -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(1.0, speed); } @@ -19,7 +16,7 @@ public: protected: virtual void handleEvent(Event *evt); - virtual void streamThread(); + virtual void streamThread() = 0; void process(QHash *) 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 pause_ = false; uint64_t last_update_ts = 0; - const QString zmq_address; + std::unique_ptr fs; + QThread *stream_thread; }; diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc new file mode 100644 index 0000000000..7b1bc28354 --- /dev/null +++ b/tools/cabana/streams/pandastream.cc @@ -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 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; isend_heartbeat(false); + } +} diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h new file mode 100644 index 0000000000..fe90de2068 --- /dev/null +++ b/tools/cabana/streams/pandastream.h @@ -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 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; + PandaStreamConfig config = {}; +}; + diff --git a/tools/cabana/util.cc b/tools/cabana/util.cc index 584938d0d9..f89219f9c2 100644 --- a/tools/cabana/util.cc +++ b/tools/cabana/util.cc @@ -3,6 +3,7 @@ #include #include #include +#include #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>(); - 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) { diff --git a/tools/cabana/util.h b/tools/cabana/util.h index f9eaf7bc2b..ff7cab9a48 100644 --- a/tools/cabana/util.h +++ b/tools/cabana/util.h @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -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: