From 3a13b67b6f92c0716616342f285165a215ab7900 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:44:11 -0700 Subject: [PATCH] bye bye Qt! (#38753) * cabana: remove Qt implementation --- SConstruct | 1 - openpilot/tools/cabana/.gitignore | 7 - openpilot/tools/cabana/README.md | 40 +- openpilot/tools/cabana/SConscript | 123 +-- openpilot/tools/cabana/assets/assets.qrc | 5 - openpilot/tools/cabana/assets/cabana-icon.png | 3 - openpilot/tools/cabana/binaryview.cc | 510 ------------ openpilot/tools/cabana/binaryview.h | 106 --- openpilot/tools/cabana/cabana | 47 +- openpilot/tools/cabana/cabana.cc | 196 ----- openpilot/tools/cabana/cameraview.cc | 121 --- openpilot/tools/cabana/cameraview.h | 49 -- openpilot/tools/cabana/chart/chart.cc | 769 ------------------ openpilot/tools/cabana/chart/chart.h | 132 --- openpilot/tools/cabana/chart/chartswidget.cc | 660 --------------- openpilot/tools/cabana/chart/chartswidget.h | 140 ---- .../tools/cabana/chart/signalselector.cc | 107 --- openpilot/tools/cabana/chart/signalselector.h | 30 - openpilot/tools/cabana/chart/sparkline.cc | 101 --- openpilot/tools/cabana/chart/sparkline.h | 26 - openpilot/tools/cabana/chart/tiplabel.cc | 58 -- openpilot/tools/cabana/chart/tiplabel.h | 12 - openpilot/tools/cabana/detailwidget.cc | 323 -------- openpilot/tools/cabana/detailwidget.h | 78 -- openpilot/tools/cabana/historylog.cc | 251 ------ openpilot/tools/cabana/historylog.h | 83 -- openpilot/tools/cabana/mainwin.cc | 752 ----------------- openpilot/tools/cabana/mainwin.h | 122 --- openpilot/tools/cabana/messageswidget.cc | 467 ----------- openpilot/tools/cabana/messageswidget.h | 131 --- openpilot/tools/cabana/routesdialog.cc | 110 --- openpilot/tools/cabana/routesdialog.h | 31 - openpilot/tools/cabana/settingsdialog.cc | 88 -- openpilot/tools/cabana/settingsdialog.h | 20 - openpilot/tools/cabana/signalview.cc | 719 ---------------- openpilot/tools/cabana/signalview.h | 155 ---- openpilot/tools/cabana/streamselector.cc | 330 -------- openpilot/tools/cabana/streamselector.h | 99 --- openpilot/tools/cabana/tools/findsignal.cc | 286 ------- openpilot/tools/cabana/tools/findsignal.h | 71 -- .../tools/cabana/tools/findsimilarbits.cc | 161 ---- .../tools/cabana/tools/findsimilarbits.h | 36 - openpilot/tools/cabana/tools/routeinfo.cc | 40 - openpilot/tools/cabana/tools/routeinfo.h | 8 - openpilot/tools/cabana/utils/elidedlabel.cc | 29 - openpilot/tools/cabana/utils/elidedlabel.h | 25 - openpilot/tools/cabana/utils/qtutil.cc | 236 ------ openpilot/tools/cabana/utils/qtutil.h | 139 ---- openpilot/tools/cabana/videowidget.cc | 434 ---------- openpilot/tools/cabana/videowidget.h | 82 -- pyproject.toml | 2 +- tools/op.sh | 2 +- uv.lock | 16 +- 53 files changed, 50 insertions(+), 8519 deletions(-) delete mode 100644 openpilot/tools/cabana/assets/assets.qrc delete mode 100644 openpilot/tools/cabana/assets/cabana-icon.png delete mode 100644 openpilot/tools/cabana/binaryview.cc delete mode 100644 openpilot/tools/cabana/binaryview.h delete mode 100644 openpilot/tools/cabana/cabana.cc delete mode 100644 openpilot/tools/cabana/cameraview.cc delete mode 100644 openpilot/tools/cabana/cameraview.h delete mode 100644 openpilot/tools/cabana/chart/chart.cc delete mode 100644 openpilot/tools/cabana/chart/chart.h delete mode 100644 openpilot/tools/cabana/chart/chartswidget.cc delete mode 100644 openpilot/tools/cabana/chart/chartswidget.h delete mode 100644 openpilot/tools/cabana/chart/signalselector.cc delete mode 100644 openpilot/tools/cabana/chart/signalselector.h delete mode 100644 openpilot/tools/cabana/chart/sparkline.cc delete mode 100644 openpilot/tools/cabana/chart/sparkline.h delete mode 100644 openpilot/tools/cabana/chart/tiplabel.cc delete mode 100644 openpilot/tools/cabana/chart/tiplabel.h delete mode 100644 openpilot/tools/cabana/detailwidget.cc delete mode 100644 openpilot/tools/cabana/detailwidget.h delete mode 100644 openpilot/tools/cabana/historylog.cc delete mode 100644 openpilot/tools/cabana/historylog.h delete mode 100644 openpilot/tools/cabana/mainwin.cc delete mode 100644 openpilot/tools/cabana/mainwin.h delete mode 100644 openpilot/tools/cabana/messageswidget.cc delete mode 100644 openpilot/tools/cabana/messageswidget.h delete mode 100644 openpilot/tools/cabana/routesdialog.cc delete mode 100644 openpilot/tools/cabana/routesdialog.h delete mode 100644 openpilot/tools/cabana/settingsdialog.cc delete mode 100644 openpilot/tools/cabana/settingsdialog.h delete mode 100644 openpilot/tools/cabana/signalview.cc delete mode 100644 openpilot/tools/cabana/signalview.h delete mode 100644 openpilot/tools/cabana/streamselector.cc delete mode 100644 openpilot/tools/cabana/streamselector.h delete mode 100644 openpilot/tools/cabana/tools/findsignal.cc delete mode 100644 openpilot/tools/cabana/tools/findsignal.h delete mode 100644 openpilot/tools/cabana/tools/findsimilarbits.cc delete mode 100644 openpilot/tools/cabana/tools/findsimilarbits.h delete mode 100644 openpilot/tools/cabana/tools/routeinfo.cc delete mode 100644 openpilot/tools/cabana/tools/routeinfo.h delete mode 100644 openpilot/tools/cabana/utils/elidedlabel.cc delete mode 100644 openpilot/tools/cabana/utils/elidedlabel.h delete mode 100644 openpilot/tools/cabana/utils/qtutil.cc delete mode 100644 openpilot/tools/cabana/utils/qtutil.h delete mode 100644 openpilot/tools/cabana/videowidget.cc delete mode 100644 openpilot/tools/cabana/videowidget.h diff --git a/SConstruct b/SConstruct index bb5c35feb7..c6d758b318 100644 --- a/SConstruct +++ b/SConstruct @@ -87,7 +87,6 @@ acados_include_dirs = [ # vendored in commaai/dependencies. allowed_system_libs = { "EGL", "GLESv2", "GL", - "Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets", "dl", "drm", "gbm", "m", "pthread", } diff --git a/openpilot/tools/cabana/.gitignore b/openpilot/tools/cabana/.gitignore index c5c752ba9c..788b286372 100644 --- a/openpilot/tools/cabana/.gitignore +++ b/openpilot/tools/cabana/.gitignore @@ -1,12 +1,5 @@ -moc_* -*.moc -*.generated.qrc - -assets.cc bootstrap_icons.cc -_cabana _cabana_ui dbc/car_fingerprint_to_dbc.json tests/test_cabana -tests/test_dbc_core diff --git a/openpilot/tools/cabana/README.md b/openpilot/tools/cabana/README.md index fbdfbb40e5..6fcce59eb0 100644 --- a/openpilot/tools/cabana/README.md +++ b/openpilot/tools/cabana/README.md @@ -6,29 +6,27 @@ Cabana is a tool developed to view raw CAN data. One use for this is creating an ```bash $ ./cabana -h -Usage: ./cabana [options] route +Usage: ./cabana [options] [route] + + route the drive to replay. find your drives at connect.comma.ai Options: - -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 - --auto Auto load the route from the best available source (no video): - internal, openpilotci, comma_api, car_segments, testing_closet - --qcam load qcamera - --wide-road load wide road camera - --msgq read can messages from msgq - --panda read can messages from panda - --panda-serial read can messages from panda with given serial - --socketcan read can messages from given SocketCAN device - --zmq read can messages from zmq at the specified ip-address - 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 + --help show this help + --demo use a demo route instead of providing your own + --auto Auto load the route from the best available source (no video): + internal, openpilotci, comma_api, car_segments, testing_closet + --qcam load qcamera + --wide-road load wide road camera (alias: --ecam) + --cabin load cabin camera (alias: --dcam) + --msgq read can messages from the msgq + --panda read can messages from panda + --panda-serial read can messages from panda with given serial + --socketcan read can messages from given SocketCAN device + --zmq read can messages from zmq at the specified ip-address + --data_dir local directory with routes + --no-vipc do not output video + --no-cache turn off the local route file cache + --dbc dbc file to open ``` ## Examples diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index a324e09d67..81fd8877c4 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -1,6 +1,4 @@ -import subprocess import os -import shutil import bootstrap_icons import imgui @@ -27,8 +25,7 @@ def build_bootstrap_icons_src(target, source, env): bootstrap_icons_src = env.Command('assets/bootstrap_icons.cc', str(bootstrap_icons.SVG_PATH), build_bootstrap_icons_src) -# Qt-free sources shared by the imgui frontend and the core test. Compiled in the base env, -# so the object names must not collide with the Qt build of the same sources. +# sources shared by the imgui frontend and the core test core_srcs = ['streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'commands.cc', 'settings.cc', 'routes.cc', 'panda.cc'] @@ -56,109 +53,25 @@ else: ui_libs += ['GL', 'dl'] cabana_ui = ui_env.Program('_cabana_ui', ui_objs, LIBS=ui_libs) -# Detect Qt - skip build if not available -if arch == "Darwin": - try: - brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip() - has_qt = os.path.isdir(os.path.join(brew_prefix, "opt/qt@5")) - except (FileNotFoundError, subprocess.CalledProcessError): - has_qt = False -else: - has_qt = shutil.which('qmake') is not None -if not has_qt: - Return() - -qt_env = env.Clone() -qt_modules = ["Widgets", "Gui", "Core"] - -qt_libs = [] -if arch == "Darwin": - qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5" - qt_dirs = [ - os.path.join(qt_env['QTDIR'], "include"), - ] - qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules] - qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")] - qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] - qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin")) -else: - qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip() - qt_install_headers = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip() - - qt_env['QTDIR'] = qt_install_prefix - qt_dirs = [ - f"{qt_install_headers}", - ] - - qt_gui_path = os.path.join(qt_install_headers, "QtGui") - qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))] - qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else [] - qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules] - - qt_libs = [f"Qt5{m}" for m in qt_modules] -qt_env['QT3DIR'] = qt_env['QTDIR'] -qt_env.Tool('qt3') - -qt_env['CPPPATH'] += qt_dirs -qt_flags = [ - "-D_REENTRANT", - "-DQT_NO_DEBUG", - "-DQT_WIDGETS_LIB", - "-DQT_GUI_LIB", - "-DQT_CORE_LIB", - "-DQT_MESSAGELOGCONTEXT", -] -qt_env['CXXFLAGS'] += qt_flags -qt_env['LIBPATH'] += ['#openpilot/selfdrive/ui', ] -qt_env['LIBS'] = qt_libs - -base_frameworks = qt_env['FRAMEWORKS'] -base_libs = [common, messaging, cereal, visionipc, 'm', 'pthread'] + qt_env["LIBS"] - -if arch == "Darwin": - base_frameworks += ['CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox'] - -cabana_env = qt_env.Clone() -cabana_env['CPPPATH'] += [libusb.INCLUDE_DIR] -cabana_env['LIBPATH'] += [libusb.LIB_DIR] - -cabana_libs = [cereal, messaging, visionipc, replay_lib] + ffmpeg_libs + ['usb-1.0'] + base_libs -cabana_env['CXXFLAGS'] += [opendbc_path] - -# build assets -assets = "assets/assets.cc" -cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") -cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) - -cabana_srcs = ['mainwin.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'routesdialog.cc', - 'utils/qtutil.cc', 'utils/elidedlabel.cc', - 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', - 'messageswidget.cc', 'streamselector.cc', 'settingsdialog.cc', - 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] + core_srcs -cabana_lib = cabana_env.Library("cabana_lib", cabana_srcs + [bootstrap_icons_src], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) -cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) - if GetOption('extras'): - # This target deliberately uses the base environment and links no Qt libraries. - # It prevents Qt dependencies from creeping back into the DBC core. - dbc_core_test_env = env.Clone() - dbc_core_test_env['CXXFLAGS'] += [opendbc_path] - dbc_core_test_objects = [ - dbc_core_test_env.Object('tests/dbc_core_tests', 'tests/test_cabana.cc'), - dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'), - dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), - dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), - dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'), - dbc_core_test_env.Object('tests/dbc_core_util', 'utils/util.cc'), - dbc_core_test_env.Object('tests/dbc_core_icons', bootstrap_icons_src), - dbc_core_test_env.Object('tests/dbc_core_routes', 'routes.cc'), - dbc_core_test_env.Object('tests/dbc_core_qtstate', 'ui/qtstate.cc'), + cabana_core_test_env = env.Clone() + cabana_core_test_env['CXXFLAGS'] += [opendbc_path] + cabana_core_test_objects = [ + cabana_core_test_env.Object('tests/cabana_core_tests', 'tests/test_cabana.cc'), + cabana_core_test_env.Object('tests/cabana_core_model', 'dbc/dbc.cc'), + cabana_core_test_env.Object('tests/cabana_core_file', 'dbc/dbcfile.cc'), + cabana_core_test_env.Object('tests/cabana_core_manager', 'dbc/dbcmanager.cc'), + cabana_core_test_env.Object('tests/cabana_core_strings', 'utils/strings.cc'), + cabana_core_test_env.Object('tests/cabana_core_util', 'utils/util.cc'), + cabana_core_test_env.Object('tests/cabana_core_icons', bootstrap_icons_src), + cabana_core_test_env.Object('tests/cabana_core_routes', 'routes.cc'), + cabana_core_test_env.Object('tests/cabana_core_qtstate', 'ui/qtstate.cc'), ] - dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects, LIBS=[replay_lib, common]) + cabana_core_test_env.Program('tests/test_cabana', cabana_core_test_objects, LIBS=[replay_lib, common]) output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json' -generate_dbc = cabana_env.Command('#' + output_json_file, - ['dbc/generate_dbc_json.py'], - "python3 openpilot/tools/cabana/dbc/generate_dbc_json.py --out " + output_json_file) -cabana_env.Depends(generate_dbc, ["#openpilot/common", '#opendbc_repo', "#openpilot/cereal", "#msgq_repo"]) +generate_dbc = ui_env.Command('#' + output_json_file, + ['dbc/generate_dbc_json.py'], + "python3 openpilot/tools/cabana/dbc/generate_dbc_json.py --out " + output_json_file) +ui_env.Depends(generate_dbc, ["#openpilot/common", '#opendbc_repo', "#openpilot/cereal", "#msgq_repo"]) ui_env.Depends(cabana_ui, generate_dbc) diff --git a/openpilot/tools/cabana/assets/assets.qrc b/openpilot/tools/cabana/assets/assets.qrc deleted file mode 100644 index 009d63f008..0000000000 --- a/openpilot/tools/cabana/assets/assets.qrc +++ /dev/null @@ -1,5 +0,0 @@ - - - cabana-icon.png - - diff --git a/openpilot/tools/cabana/assets/cabana-icon.png b/openpilot/tools/cabana/assets/cabana-icon.png deleted file mode 100644 index 2c3cfcd7d1..0000000000 --- a/openpilot/tools/cabana/assets/cabana-icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:015cea9d425f747bce856fd2f5a6a57fca322159dde54554de11eda076273595 -size 18008 diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc deleted file mode 100644 index 20fd2ec1ec..0000000000 --- a/openpilot/tools/cabana/binaryview.cc +++ /dev/null @@ -1,510 +0,0 @@ -#include "tools/cabana/binaryview.h" - -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/commands.h" -#include "tools/cabana/utils/qtutil.h" - -// BinaryView - -const int CELL_HEIGHT = 36; -const int VERTICAL_HEADER_WIDTH = 30; -inline int get_bit_pos(const QModelIndex &index) { return flipBitPos(index.row() * 8 + index.column()); } - -BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { - model = new BinaryViewModel(this); - setModel(model); - delegate = new BinaryItemDelegate(this); - setItemDelegate(delegate); - horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - horizontalHeader()->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); - verticalHeader()->setSectionsClickable(false); - verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); - verticalHeader()->setDefaultSectionSize(CELL_HEIGHT); - horizontalHeader()->hide(); - setShowGrid(false); - setMouseTracking(true); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); - connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); })); - - addShortcuts(); - setWhatsThis(R"( - Binary View
- - Shortcuts
- Delete Signal: -  x , -  Backspace , -  Delete 
- Change endianness:  e 
- Change signedness:  s 
- Open chart: -  c , -  p , -  g  - )"); -} - -void BinaryView::addShortcuts() { - // Delete (x, backspace, delete) - QShortcut *shortcut_delete_x = new QShortcut(QKeySequence(Qt::Key_X), this); - QShortcut *shortcut_delete_backspace = new QShortcut(QKeySequence(Qt::Key_Backspace), this); - QShortcut *shortcut_delete_delete = new QShortcut(QKeySequence(Qt::Key_Delete), this); - QObject::connect(shortcut_delete_delete, &QShortcut::activated, shortcut_delete_x, &QShortcut::activated); - QObject::connect(shortcut_delete_backspace, &QShortcut::activated, shortcut_delete_x, &QShortcut::activated); - QObject::connect(shortcut_delete_x, &QShortcut::activated, [=]{ - if (hovered_sig != nullptr) { - UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, hovered_sig)); - hovered_sig = nullptr; - } - }); - - // Change endianness (e) - QShortcut *shortcut_endian = new QShortcut(QKeySequence(Qt::Key_E), this); - QObject::connect(shortcut_endian, &QShortcut::activated, [=]{ - if (hovered_sig != nullptr) { - cabana::Signal s = *hovered_sig; - s.is_little_endian = !s.is_little_endian; - emit editSignal(hovered_sig, s); - } - }); - - // Change signedness (s) - QShortcut *shortcut_sign = new QShortcut(QKeySequence(Qt::Key_S), this); - QObject::connect(shortcut_sign, &QShortcut::activated, [=]{ - if (hovered_sig != nullptr) { - cabana::Signal s = *hovered_sig; - s.is_signed = !s.is_signed; - emit editSignal(hovered_sig, s); - } - }); - - // Open chart (c, p, g) - QShortcut *shortcut_plot = new QShortcut(QKeySequence(Qt::Key_P), this); - QShortcut *shortcut_plot_g = new QShortcut(QKeySequence(Qt::Key_G), this); - QShortcut *shortcut_plot_c = new QShortcut(QKeySequence(Qt::Key_C), this); - QObject::connect(shortcut_plot_g, &QShortcut::activated, shortcut_plot, &QShortcut::activated); - QObject::connect(shortcut_plot_c, &QShortcut::activated, shortcut_plot, &QShortcut::activated); - QObject::connect(shortcut_plot, &QShortcut::activated, [=]{ - if (hovered_sig != nullptr) { - emit showChart(model->msg_id, hovered_sig, true, false); - } - }); -} - -QSize BinaryView::minimumSizeHint() const { - return {(horizontalHeader()->minimumSectionSize() + 1) * 9 + VERTICAL_HEADER_WIDTH + 2, - CELL_HEIGHT * std::min(model->rowCount(), 10) + 2}; -} - -void BinaryView::highlight(const cabana::Signal *sig) { - if (sig != hovered_sig) { - for (int i = 0; i < model->items.size(); ++i) { - auto &item_sigs = model->items[i].sigs; - auto has = [](const auto &v, auto p) { return std::find(v.begin(), v.end(), p) != v.end(); }; - if ((sig && has(item_sigs, sig)) || (hovered_sig && has(item_sigs, hovered_sig))) { - auto index = model->index(i / model->columnCount(), i % model->columnCount()); - emit model->dataChanged(index, index, {Qt::DisplayRole}); - } - } - - hovered_sig = sig; - emit signalHovered(hovered_sig); - } -} - -void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) { - auto index = indexAt(last_mouse_pos); - if (!anchor_index.isValid() || !index.isValid()) - return; - - QItemSelection selection; - auto [start, size, is_lb] = getSelection(index); - for (int i = 0; i < size; ++i) { - int pos = is_lb ? flipBitPos(start + i) : flipBitPos(start) + i; - selection << QItemSelectionRange{model->index(pos / 8, pos % 8)}; - } - selectionModel()->select(selection, flags); -} - -void BinaryView::mousePressEvent(QMouseEvent *event) { - resize_sig = nullptr; - if (auto index = indexAt(last_mouse_pos = event->pos()); index.isValid() && index.column() != 8) { - anchor_index = index; - auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); - int bit_pos = get_bit_pos(anchor_index); - for (auto s : item->sigs) { - if (bit_pos == s->lsb || bit_pos == s->msb) { - int idx = flipBitPos(bit_pos == s->lsb ? s->msb : s->lsb); - anchor_index = model->index(idx / 8, idx % 8); - resize_sig = s; - break; - } - } - } - event->accept(); -} - -void BinaryView::highlightPosition(const QPoint &pos) { - if (auto index = indexAt(pos); index.isValid()) { - auto item = (BinaryViewModel::Item *)index.internalPointer(); - const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back(); - highlight(sig); - } -} - -void BinaryView::mouseMoveEvent(QMouseEvent *event) { - highlightPosition(last_mouse_pos = event->pos()); - QTableView::mouseMoveEvent(event); -} - -void BinaryView::mouseReleaseEvent(QMouseEvent *event) { - QTableView::mouseReleaseEvent(event); - - auto release_index = indexAt(event->pos()); - if (release_index.isValid() && anchor_index.isValid()) { - if (selectionModel()->hasSelection()) { - auto sig = resize_sig ? *resize_sig : cabana::Signal{}; - std::tie(sig.start_bit, sig.size, sig.is_little_endian) = getSelection(release_index); - resize_sig ? emit editSignal(resize_sig, sig) - : UndoStack::instance()->push(new AddSigCommand(model->msg_id, sig)); - } else { - auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); - if (item && item->sigs.size() > 0) - emit signalClicked(item->sigs.back()); - } - } - clearSelection(); - anchor_index = QModelIndex(); - resize_sig = nullptr; -} - -void BinaryView::leaveEvent(QEvent *event) { - highlight(nullptr); - QTableView::leaveEvent(event); -} - -void BinaryView::setMessage(const MessageId &message_id) { - model->msg_id = message_id; - verticalScrollBar()->setValue(0); - refresh(); -} - -void BinaryView::refresh() { - clearSelection(); - anchor_index = QModelIndex(); - resize_sig = nullptr; - hovered_sig = nullptr; - model->refresh(); - if (underMouse()) highlightPosition(last_mouse_pos); -} - -std::set BinaryView::getOverlappingSignals() const { - std::set overlapping; - for (const auto &item : model->items) { - if (item.sigs.size() > 1) { - for (auto s : item.sigs) { - if (s->type == cabana::Signal::Type::Normal) overlapping.insert(s); - } - } - } - return overlapping; -} - -std::tuple BinaryView::getSelection(QModelIndex index) { - if (index.column() == 8) { - index = model->index(index.row(), 7); - } - 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_pos = get_bit_pos(index); - int anchor_bit_pos = get_bit_pos(anchor_index); - int start_bit = is_lb ? std::min(cur_bit_pos, anchor_bit_pos) : get_bit_pos(std::min(index, anchor_index)); - int size = is_lb ? std::abs(cur_bit_pos - anchor_bit_pos) + 1 : std::abs(flipBitPos(cur_bit_pos) - flipBitPos(anchor_bit_pos)) + 1; - return {start_bit, size, is_lb}; -} - -// BinaryViewModel - -void BinaryViewModel::refresh() { - beginResetModel(); - bit_flip_tracker = {}; - items.clear(); - if (auto dbc_msg = dbc()->msg(msg_id)) { - row_count = dbc_msg->size; - items.resize(row_count * column_count); - for (auto sig : dbc_msg->getSignals()) { - for (int j = 0; j < sig->size; ++j) { - int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j; - int idx = column_count * (pos / 8) + pos % 8; - if (idx >= items.size()) { - fprintf(stderr, "signal %s out of bounds.start_bit: %d size: %d\n", - sig->name.c_str(), sig->start_bit, sig->size); - break; - } - if (j == 0) sig->is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true; - if (j == sig->size - 1) sig->is_little_endian ? items[idx].is_msb = true : items[idx].is_lsb = true; - - auto &sigs = items[idx].sigs; - sigs.push_back(sig); - if (sigs.size() > 1) { - std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) { return l->size > r->size; }); - } - } - } - } else { - row_count = can->lastMessage(msg_id).dat.size(); - items.resize(row_count * column_count); - } - endResetModel(); - updateState(); -} - -void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &color) { - auto &item = items[row * column_count + col]; - item.valid = true; - 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() { - const auto &last_msg = can->lastMessage(msg_id); - const auto &binary = last_msg.dat; - // Handle size changes in binary data - if (binary.size() > row_count) { - beginInsertRows({}, row_count, binary.size() - 1); - row_count = binary.size(); - items.resize(row_count * column_count); - endInsertRows(); - } - - auto &bit_flips = heatmap_live_mode ? last_msg.bit_flip_counts : getBitFlipChanges(binary.size()); - // Find the maximum bit flip count across the message - uint32_t max_bit_flip_count = 1; // Default to 1 to avoid division by zero - for (const auto &row : bit_flips) { - for (uint32_t count : row) { - max_bit_flip_count = std::max(max_bit_flip_count, count); - } - } - - const double max_alpha = 255.0; - const double min_alpha_with_signal = 25.0; // Base alpha for small flip counts - const double min_alpha_no_signal = 10.0; // Base alpha for small flip counts for no signal bits - const double log_factor = 1.0 + 0.2; // Factor for logarithmic scaling - const double log_scaler = max_alpha / log2(log_factor * max_bit_flip_count); - - for (size_t i = 0; i < binary.size(); ++i) { - for (int j = 0; j < 8; ++j) { - auto &item = items[i * column_count + j]; - int bit_val = (binary[i] >> (7 - j)) & 1; - - double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal; - uint32_t flip_count = bit_flips[i][j]; - if (flip_count > 0) { - double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler; - double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal; - alpha = std::clamp(normalized_alpha, min_alpha, max_alpha); - } - - auto color = item.bg_color; - color.setAlpha(alpha); - updateItem(i, j, bit_val, color); - } - updateItem(i, 8, binary[i], toQColor(last_msg.colors[i])); - } -} - -const std::vector> &BinaryViewModel::getBitFlipChanges(size_t msg_size) { - // Return cached results if time range and data are unchanged - auto time_range = can->timeRange(); - if (bit_flip_tracker.time_range == time_range && !bit_flip_tracker.flip_counts.empty()) - return bit_flip_tracker.flip_counts; - - bit_flip_tracker.time_range = time_range; - bit_flip_tracker.flip_counts.assign(msg_size, std::array{}); - - // Iterate over events within the specified time range and calculate bit flips - auto [first, last] = can->eventsInRange(msg_id, time_range); - if (std::distance(first, last) <= 1) return bit_flip_tracker.flip_counts; - - std::vector prev_values((*first)->dat, (*first)->dat + (*first)->size); - for (auto it = std::next(first); it != last; ++it) { - const CanEvent *event = *it; - int size = std::min(msg_size, event->size); - for (int i = 0; i < size; ++i) { - const uint8_t diff = event->dat[i] ^ prev_values[i]; - if (!diff) continue; - - auto &bit_flips = bit_flip_tracker.flip_counts[i]; - for (int bit = 0; bit < 8; ++bit) { - if (diff & (1u << bit)) ++bit_flips[7 - bit]; - } - prev_values[i] = event->dat[i]; - } - } - - return bit_flip_tracker.flip_counts; -} - -QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation == Qt::Vertical) { - switch (role) { - case Qt::DisplayRole: return section; - case Qt::SizeHintRole: return QSize(VERTICAL_HEADER_WIDTH, 0); - case Qt::TextAlignmentRole: return Qt::AlignCenter; - } - } - return {}; -} - -QVariant BinaryViewModel::data(const QModelIndex &index, int role) const { - auto item = (const BinaryViewModel::Item *)index.internalPointer(); - return role == Qt::ToolTipRole && item && !item->sigs.empty() ? QString::fromStdString(utils::signalToolTip(item->sigs.back())) : QVariant(); -} - -// BinaryItemDelegate - -BinaryItemDelegate::BinaryItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { - small_font.setPixelSize(8); - hex_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - hex_font.setBold(true); - - bin_text_table[0].setText("0"); - bin_text_table[1].setText("1"); - for (int i = 0; i < 256; ++i) { - hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); - hex_text_table[i].prepare({}, hex_font); - } -} - -bool BinaryItemDelegate::hasSignal(const QModelIndex &index, int dx, int dy, const cabana::Signal *sig) const { - if (!index.isValid()) return false; - auto model = (const BinaryViewModel*)(index.model()); - int idx = (index.row() + dy) * model->columnCount() + index.column() + dx; - if (idx < 0 || idx >= (int)model->items.size()) return false; - auto &s = model->items[idx].sigs; - return std::find(s.begin(), s.end(), sig) != s.end(); -} - -void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - auto item = (const BinaryViewModel::Item *)index.internalPointer(); - BinaryView *bin_view = (BinaryView *)parent(); - painter->save(); - - if (index.column() == 8) { - if (item->valid) { - painter->setFont(hex_font); - painter->fillRect(option.rect, item->bg_color); - } - } else if (option.state & QStyle::State_Selected) { - auto color = bin_view->resize_sig ? toQColor(bin_view->resize_sig->color) : option.palette.color(QPalette::Active, QPalette::Highlight); - painter->fillRect(option.rect, color); - painter->setPen(option.palette.color(QPalette::BrightText)); - } else if (!bin_view->selectionModel()->hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), bin_view->resize_sig) == item->sigs.end()) { // not resizing - if (item->sigs.size() > 0) { - for (auto &s : item->sigs) { - if (s == bin_view->hovered_sig) { - painter->fillRect(option.rect, toQColor(s->color.darker(125))); // 4/5x brightness - } else { - drawSignalCell(painter, option, index, s); - } - } - } else if (item->valid && item->bg_color.alpha() > 0) { - painter->fillRect(option.rect, item->bg_color); - } - auto color_role = (std::find(item->sigs.begin(), item->sigs.end(), bin_view->hovered_sig) != item->sigs.end()) ? QPalette::BrightText : QPalette::Text; - painter->setPen(option.palette.color(bin_view->is_message_active ? QPalette::Normal : QPalette::Disabled, color_role)); - } - - if (item->sigs.size() > 1) { - painter->fillRect(option.rect, QBrush(Qt::darkGray, Qt::Dense7Pattern)); - } else if (!item->valid) { - painter->fillRect(option.rect, QBrush(Qt::darkGray, Qt::BDiagPattern)); - } - if (item->valid) { - utils::drawStaticText(painter, option.rect, index.column() == 8 ? hex_text_table[item->val] : bin_text_table[item->val]); - } - if (item->is_msb || item->is_lsb) { - painter->setFont(small_font); - painter->drawText(option.rect.adjusted(8, 0, -8, -3), Qt::AlignRight | Qt::AlignBottom, item->is_msb ? "M" : "L"); - } - painter->restore(); -} - -// Draw border on edge of signal -void BinaryItemDelegate::drawSignalCell(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index, const cabana::Signal *sig) const { - bool draw_left = !hasSignal(index, -1, 0, sig); - bool draw_top = !hasSignal(index, 0, -1, sig); - bool draw_right = !hasSignal(index, 1, 0, sig); - bool draw_bottom = !hasSignal(index, 0, 1, sig); - - const int spacing = 2; - QRect rc = option.rect.adjusted(draw_left * 3, draw_top * spacing, draw_right * -3, draw_bottom * -spacing); - QRegion subtract; - if (!draw_top) { - if (!draw_left && !hasSignal(index, -1, -1, sig)) { - subtract += QRect{rc.left(), rc.top(), 3, spacing}; - } else if (!draw_right && !hasSignal(index, 1, -1, sig)) { - subtract += QRect{rc.right() - 2, rc.top(), 3, spacing}; - } - } - if (!draw_bottom) { - if (!draw_left && !hasSignal(index, -1, 1, sig)) { - subtract += QRect{rc.left(), rc.bottom() - (spacing - 1), 3, spacing}; - } else if (!draw_right && !hasSignal(index, 1, 1, sig)) { - subtract += QRect{rc.right() - 2, rc.bottom() - (spacing - 1), 3, spacing}; - } - } - painter->setClipRegion(QRegion(rc).subtracted(subtract)); - - auto item = (const BinaryViewModel::Item *)index.internalPointer(); - QColor color = toQColor(sig->color); - color.setAlpha(item->bg_color.alpha()); - // Mixing the signal color with the Base background color to fade it - painter->fillRect(rc, option.palette.color(QPalette::Base)); - painter->fillRect(rc, color); - - // Draw edges - color = toQColor(sig->color.darker(125)); - painter->setPen(QPen(color, 1)); - if (draw_left) painter->drawLine(rc.topLeft(), rc.bottomLeft()); - if (draw_right) painter->drawLine(rc.topRight(), rc.bottomRight()); - if (draw_bottom) painter->drawLine(rc.bottomLeft(), rc.bottomRight()); - if (draw_top) painter->drawLine(rc.topLeft(), rc.topRight()); - - if (!subtract.isEmpty()) { - // fill gaps inside corners. - painter->setPen(QPen(color, 2, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin)); - for (auto &r : subtract) { - painter->drawRect(r); - } - } -} diff --git a/openpilot/tools/cabana/binaryview.h b/openpilot/tools/cabana/binaryview.h deleted file mode 100644 index c918ee01e7..0000000000 --- a/openpilot/tools/cabana/binaryview.h +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" - -class BinaryItemDelegate : public QStyledItemDelegate { -public: - BinaryItemDelegate(QObject *parent); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - bool hasSignal(const QModelIndex &index, int dx, int dy, const cabana::Signal *sig) const; - void drawSignalCell(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index, const cabana::Signal *sig) const; - - QFont small_font, hex_font; - std::array hex_text_table; - std::array bin_text_table; -}; - -class BinaryViewModel : public QAbstractTableModel { -public: - BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {} - void refresh(); - void updateState(); - void updateItem(int row, int col, uint8_t 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 override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } - int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; } - QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { - return createIndex(row, column, (void *)&items[row * column_count + column]); - } - Qt::ItemFlags flags(const QModelIndex &index) const override { - return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable; - } - const std::vector> &getBitFlipChanges(size_t msg_size); - - struct BitFlipTracker { - std::optional> time_range; - std::vector> flip_counts; - } bit_flip_tracker; - - struct Item { - QColor bg_color = QColor(102, 86, 169, 255); - bool is_msb = false; - bool is_lsb = false; - uint8_t val; - std::vector sigs; - bool valid = false; - }; - std::vector items; - bool heatmap_live_mode = true; - MessageId msg_id; - int row_count = 0; - const int column_count = 9; -}; - -class BinaryView : public QTableView { - Q_OBJECT - -public: - BinaryView(QWidget *parent = nullptr); - void setMessage(const MessageId &message_id); - void highlight(const cabana::Signal *sig); - std::set getOverlappingSignals() const; - void updateState() { model->updateState(); } - void paintEvent(QPaintEvent *event) override { - is_message_active = can->isMessageActive(model->msg_id); - QTableView::paintEvent(event); - } - QSize minimumSizeHint() const override; - void setHeatmapLiveMode(bool live) { model->heatmap_live_mode = live; updateState(); } - -signals: - void signalClicked(const cabana::Signal *sig); - void signalHovered(const cabana::Signal *sig); - void editSignal(const cabana::Signal *origin_s, cabana::Signal &s); - void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge); - -private: - void addShortcuts(); - void refresh(); - std::tuple getSelection(QModelIndex index); - void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) override; - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void leaveEvent(QEvent *event) override; - void highlightPosition(const QPoint &pt); - - QModelIndex anchor_index; - QPoint last_mouse_pos{-1, -1}; - BinaryViewModel *model; - BinaryItemDelegate *delegate; - bool is_message_active = false; - const cabana::Signal *resize_sig = nullptr; - const cabana::Signal *hovered_sig = nullptr; - Connections connections_; - friend class BinaryItemDelegate; -}; diff --git a/openpilot/tools/cabana/cabana b/openpilot/tools/cabana/cabana index fb995a05c2..bdec614366 100755 --- a/openpilot/tools/cabana/cabana +++ b/openpilot/tools/cabana/cabana @@ -4,50 +4,7 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ROOT="$(cd "$DIR/../../../" && pwd)" -install_qt() { - if [[ "$(uname)" == "Darwin" ]]; then - brew install qt@5 - brew link qt@5 || true - else - SUDO="" - if [[ ! $(id -u) -eq 0 ]]; then - SUDO="sudo" - fi - $SUDO apt-get install -y --no-install-recommends \ - qtbase5-dev \ - qtbase5-dev-tools \ - qttools5-dev-tools \ - libqt5charts5-dev \ - libqt5svg5-dev \ - libqt5serialbus5-dev \ - libqt5x11extras5-dev \ - libqt5opengl5-dev - fi -} - -# --legacy runs the old Qt cabana until it is deprecated -LEGACY=0 -ARGS=() -for arg in "$@"; do - if [[ "$arg" == "--legacy" ]]; then - LEGACY=1 - else - ARGS+=("$arg") - fi -done - -if [[ $LEGACY -eq 1 ]]; then - # Install Qt if not found - if ! command -v qmake &> /dev/null; then - echo "Qt not found, installing dependencies..." - install_qt - fi - TARGET="_cabana" -else - TARGET="_cabana_ui" -fi - cd "$ROOT" -scons -u "openpilot/tools/cabana/$TARGET" openpilot/cereal/messaging/bridge +scons -u "openpilot/tools/cabana/_cabana_ui" openpilot/cereal/messaging/bridge -exec "$DIR/$TARGET" "${ARGS[@]}" +exec "$DIR/_cabana_ui" "$@" diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc deleted file mode 100644 index b18484c556..0000000000 --- a/openpilot/tools/cabana/cabana.cc +++ /dev/null @@ -1,196 +0,0 @@ -#include -#include -#include -#include - -#include - -#include "tools/cabana/mainwin.h" -#include "tools/cabana/streams/devicestream.h" -#include "tools/cabana/streams/pandastream.h" -#include "tools/cabana/streams/replaystream.h" -#ifdef __linux__ -#include "tools/cabana/streams/socketcanstream.h" -#endif -#include "tools/cabana/utils/qtutil.h" - -namespace { - -struct CabanaArgs { - bool demo = false; - bool auto_source = false; - bool qcam = false; - bool wide_road = false; - bool cabin = false; - bool msgq = false; - bool panda = false; - bool no_vipc = false; - std::string panda_serial; - std::string socketcan; - std::string zmq; - std::string data_dir; - std::string dbc; - std::string route; -}; - -void printUsage(const char *argv0) { - fprintf(stderr, - "Usage: %s [options] [route]\n" - "\n" - " route the drive to replay. find your drives at connect.comma.ai\n" - "\n" - "Options:\n" - " --help show this help\n" - " --demo use a demo route instead of providing your own\n" - " --auto Auto load the route from the best available source (no video):\n" - " internal, openpilotci, comma_api, car_segments, testing_closet\n" - " --qcam load qcamera\n" - " --wide-road load wide road camera (alias: --ecam)\n" - " --cabin load cabin camera (alias: --dcam)\n" - " --msgq read can messages from the msgq\n" - " --panda read can messages from panda\n" - " --panda-serial read can messages from panda with given serial\n" -#ifdef __linux__ - " --socketcan read can messages from given SocketCAN device\n" -#endif - " --zmq read can messages from zmq at the specified ip-address\n" - " --data_dir local directory with routes\n" - " --no-vipc do not output video\n" - " --dbc dbc file to open\n", - argv0); -} - -// Returns true if value was consumed from argv[i+1]. -bool takeValue(int argc, char *argv[], int &i, std::string &out) { - if (i + 1 >= argc) { - fprintf(stderr, "error: %s requires a value\n", argv[i]); - return false; - } - out = argv[++i]; - return true; -} - -// Returns 0 to continue, or a process exit code (0 for --help, 1 for errors). -int parseArgs(int argc, char *argv[], CabanaArgs &args, bool &ok) { - ok = false; - for (int i = 1; i < argc; ++i) { - const char *a = argv[i]; - if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) { - printUsage(argv[0]); - return 0; - } else if (std::strcmp(a, "--demo") == 0) { - args.demo = true; - } else if (std::strcmp(a, "--auto") == 0) { - args.auto_source = true; - } else if (std::strcmp(a, "--qcam") == 0) { - args.qcam = true; - } else if (std::strcmp(a, "--wide-road") == 0 || std::strcmp(a, "--ecam") == 0) { - args.wide_road = true; - } else if (std::strcmp(a, "--cabin") == 0 || std::strcmp(a, "--dcam") == 0) { - args.cabin = true; - } else if (std::strcmp(a, "--msgq") == 0) { - args.msgq = true; - } else if (std::strcmp(a, "--panda") == 0) { - args.panda = true; - } else if (std::strcmp(a, "--panda-serial") == 0) { - if (!takeValue(argc, argv, i, args.panda_serial)) return 1; - args.panda = true; - } else if (std::strcmp(a, "--socketcan") == 0) { - if (!takeValue(argc, argv, i, args.socketcan)) return 1; -#ifdef __linux__ -#else - fprintf(stderr, "error: --socketcan is only supported on Linux\n"); - return 1; -#endif - } else if (std::strcmp(a, "--zmq") == 0) { - if (!takeValue(argc, argv, i, args.zmq)) return 1; - } else if (std::strcmp(a, "--data_dir") == 0) { - if (!takeValue(argc, argv, i, args.data_dir)) return 1; - } else if (std::strcmp(a, "--no-vipc") == 0) { - args.no_vipc = true; - } else if (std::strcmp(a, "--dbc") == 0) { - if (!takeValue(argc, argv, i, args.dbc)) return 1; - } else if (a[0] == '-') { - fprintf(stderr, "error: unknown option %s\n", a); - printUsage(argv[0]); - return 1; - } else if (args.route.empty()) { - args.route = a; - } else { - fprintf(stderr, "error: unexpected argument %s\n", a); - printUsage(argv[0]); - return 1; - } - } - ok = true; - return 0; -} - -} // namespace - -int main(int argc, char *argv[]) { - QCoreApplication::setApplicationName("Cabana"); - initApp(argc, argv, false); - QApplication app(argc, argv); - app.setApplicationDisplayName("Cabana"); - //app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui - - // Marshal exit onto the GUI thread (qApp methods are not thread-safe). - UnixSignalHandler signalHandler([]() { - QMetaObject::invokeMethod(qApp, []() { - printf("\nexiting...\n"); - qApp->closeAllWindows(); - qApp->exit(); - }, Qt::QueuedConnection); - }); - utils::setTheme(settings.theme); - - CabanaArgs args; - bool args_ok = false; - if (const int code = parseArgs(argc, argv, args, args_ok); !args_ok) { - return code; - } - - AbstractStream *stream = nullptr; - - if (args.msgq) { - stream = new DeviceStream(); - } else if (!args.zmq.empty()) { - stream = new DeviceStream(args.zmq); - } else if (args.panda || !args.panda_serial.empty()) { - try { - stream = new PandaStream({.serial = args.panda_serial}); - } catch (std::exception &e) { - fprintf(stderr, "%s\n", e.what()); - return 0; - } -#ifdef __linux__ - } else if (SocketCanStream::available() && !args.socketcan.empty()) { - stream = new SocketCanStream({.device = args.socketcan}); -#endif - } else { - uint32_t replay_flags = REPLAY_FLAG_NONE; - if (args.wide_road) replay_flags |= REPLAY_FLAG_WIDE_ROAD; - if (args.qcam) replay_flags |= REPLAY_FLAG_QCAMERA; - if (args.cabin) replay_flags |= REPLAY_FLAG_CABIN_CAMERA; - if (args.no_vipc) replay_flags |= REPLAY_FLAG_NO_VIPC; - - QString route; - if (!args.route.empty()) { - route = QString::fromStdString(args.route); - } else if (args.demo) { - route = DEMO_ROUTE; - } - if (!route.isEmpty()) { - auto replay_stream = std::make_unique(); - Connection err = replay_stream->error.connect([](const std::string &msg) { fprintf(stderr, "%s\n", msg.c_str()); }); - if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) { - return 0; - } - stream = replay_stream.release(); - } - } - - MainWindow w(stream, QString::fromStdString(args.dbc)); - return app.exec(); -} diff --git a/openpilot/tools/cabana/cameraview.cc b/openpilot/tools/cabana/cameraview.cc deleted file mode 100644 index b8c70ab1bf..0000000000 --- a/openpilot/tools/cabana/cameraview.cc +++ /dev/null @@ -1,121 +0,0 @@ -#include "tools/cabana/cameraview.h" - -#include -#include -#include -#include - -#include -#include - -#include "common/yuv.h" -#include "tools/cabana/utils/util.h" - -CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : - stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QWidget(parent) { - setAttribute(Qt::WA_OpaquePaintEvent); - QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); -} - -CameraWidget::~CameraWidget() { - stopVipcThread(); -} - -void CameraWidget::showEvent(QShowEvent *event) { - if (!vipc_thread.joinable()) { - clearFrames(); - vipc_exit = false; - vipc_thread = std::thread(&CameraWidget::vipcThread, this); - } -} - -void CameraWidget::stopVipcThread() { - vipc_exit = true; - if (vipc_thread.joinable()) { - vipc_thread.join(); - } -} - -void CameraWidget::paintEvent(QPaintEvent *event) { - QPainter p(this); - p.fillRect(rect(), bg); - - std::lock_guard lk(frame_lock); - if (rgb_frame.isNull()) return; - - // Scale for aspect ratio - float widget_ratio = (float)width() / height(); - float frame_ratio = (float)rgb_frame.width() / rgb_frame.height(); - int w = std::lround(width() * std::min(frame_ratio / widget_ratio, 1.0f)); - int h = std::lround(height() * std::min(widget_ratio / frame_ratio, 1.0f)); - QRect video_rect((width() - w) / 2, (height() - h) / 2, w, h); - - p.setRenderHint(QPainter::SmoothPixmapTransform); - if (active_stream_type == VISION_STREAM_CABIN) { - // mirror cabin camera horizontally - const qreal cx = video_rect.x() + video_rect.width() / 2.0; - p.translate(cx, 0); - p.scale(-1, 1); - p.translate(-cx, 0); - } - p.drawImage(video_rect, rgb_frame); -} - -void CameraWidget::vipcThread() { - VisionStreamType cur_stream = requested_stream_type; - std::unique_ptr vipc_client; - VisionIpcBufExtra frame_meta = {}; - - while (!vipc_exit) { - if (!vipc_client || cur_stream != requested_stream_type) { - clearFrames(); - fprintf(stderr, "connecting to stream %d, was connected to %d\n", - (int)requested_stream_type, (int)cur_stream); - cur_stream = requested_stream_type; - vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); - } - active_stream_type = cur_stream; - - if (!vipc_client->connected) { - clearFrames(); - auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); - if (streams.empty()) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - continue; - } - utils::runOnMainThread([this, alive = std::weak_ptr(alive_), streams]() { - if (alive.expired()) return; - available_streams = streams; - availableStreamsUpdated(streams); - }); - - if (!vipc_client->connect(false)) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - continue; - } - } - - if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) { - // NV12 -> RGBA once per frame on the receive thread; paint just draws the image - if (rgb_back.width() != (int)buf->width || rgb_back.height() != (int)buf->height) { - rgb_back = QImage(buf->width, buf->height, QImage::Format_RGBA8888); - } - yuv::nv12_to_rgba(buf->y, buf->stride, buf->uv, buf->stride, - rgb_back.bits(), rgb_back.bytesPerLine(), buf->width, buf->height); - { - std::lock_guard lk(frame_lock); - rgb_frame.swap(rgb_back); - } - utils::runOnMainThread([this, alive = std::weak_ptr(alive_)]() { - if (!alive.expired()) update(); - }); - } - } -} - -void CameraWidget::clearFrames() { - std::lock_guard lk(frame_lock); - rgb_frame = QImage(); - rgb_back = QImage(); - available_streams.clear(); -} diff --git a/openpilot/tools/cabana/cameraview.h b/openpilot/tools/cabana/cameraview.h deleted file mode 100644 index ac292a701c..0000000000 --- a/openpilot/tools/cabana/cameraview.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "openpilot/cereal/visionstream.h" -#include "tools/cabana/core/observable.h" -#include "msgq/visionipc/visionipc_client.h" - -class CameraWidget : public QWidget { -public: - explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); - ~CameraWidget(); - void setStreamType(VisionStreamType type) { requested_stream_type = type; } - VisionStreamType getStreamType() { return active_stream_type; } - void stopVipcThread(); - - Observable<> clicked; - Observable> availableStreamsUpdated; // invoked on the main thread - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override { stopVipcThread(); } - void mouseReleaseEvent(QMouseEvent *event) override { clicked(); } - void vipcThread(); - void clearFrames(); - - QColor bg = Qt::black; - QImage rgb_frame; // written by vipc thread, drawn by GUI thread; guarded by frame_lock - QImage rgb_back; // vipc thread only - - std::string stream_name; - std::atomic active_stream_type; - std::atomic requested_stream_type; - std::set available_streams; - std::thread vipc_thread; - std::atomic vipc_exit = false; - std::mutex frame_lock; - std::shared_ptr alive_ = std::make_shared(true); -}; diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc deleted file mode 100644 index 478ff3ce9d..0000000000 --- a/openpilot/tools/cabana/chart/chart.cc +++ /dev/null @@ -1,769 +0,0 @@ -#include "tools/cabana/chart/chart.h" - -#include -#include -#include - -#include -#include -#include -#include - -#include "tools/cabana/chart/chartswidget.h" - -const int AXIS_X_TOP_MARGIN = 4; -const int X_TICK_COUNT = 5; -const double MIN_ZOOM_SECONDS = 0.01; // 10ms -// Define a small value of epsilon to compare double values -const float EPSILON = 0.000001; -static inline bool xLessThan(const QPointF &p, float x) { return p.x() < (x - EPSILON); } - -static QMargins layoutMargins(const QStyle *style) { - return { - style->pixelMetric(QStyle::PM_LayoutLeftMargin), - style->pixelMetric(QStyle::PM_LayoutTopMargin), - style->pixelMetric(QStyle::PM_LayoutRightMargin), - style->pixelMetric(QStyle::PM_LayoutBottomMargin), - }; -} - -ChartView::ChartView(const std::pair &x_range, ChartsWidget *parent) - : x_min(x_range.first), x_max(x_range.second), charts_widget(parent), QWidget(parent) { - series_type = (SeriesType)settings.chart_series_type; - align_to = 50; - setMouseTracking(true); - tip_label = new TipLabel(this); - createToolButtons(); - signal_value_font.setPointSize(9); - - connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { signalRemoved(sig); })); - connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { signalUpdated(sig); })); - connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { msgRemoved(id); })); - connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { msgUpdated(id); })); -} - -void ChartView::createToolButtons() { - close_btn = new ToolButton("x", tr("Remove Chart"), this); - - menu = new QMenu(this); - // series types - auto change_series_group = new QActionGroup(menu); - change_series_group->setExclusive(true); - QStringList types{tr("Line"), tr("Step Line"), tr("Scatter")}; - for (int i = 0; i < types.size(); ++i) { - QAction *act = new QAction(types[i], change_series_group); - act->setData(i); - act->setCheckable(true); - act->setChecked(i == (int)series_type); - menu->addAction(act); - } - menu->addSeparator(); - menu->addAction(tr("Manage Signals"), this, &ChartView::manageSignals); - split_chart_act = menu->addAction(tr("Split Chart"), [this]() { charts_widget->splitChart(this); }); - - manage_btn = new ToolButton("list", "", this); - manage_btn->setMenu(menu); - manage_btn->setPopupMode(QToolButton::InstantPopup); - manage_btn->setStyleSheet("QToolButton::menu-indicator { image: none; }"); - - close_act = new QAction(tr("Close"), this); - QObject::connect(close_act, &QAction::triggered, [this] () { charts_widget->removeChart(this); }); - QObject::connect(close_btn, &QToolButton::clicked, close_act, &QAction::triggered); - QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) { - setSeriesType((SeriesType)action->data().toInt()); - }); -} - -QSize ChartView::sizeHint() const { - return {CHART_MIN_WIDTH, settings.chart_height}; -} - -void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) { - if (hasSignal(msg_id, sig)) return; - - sigs.push_back({.msg_id = msg_id, .sig = sig, .color = uniqueColor(toQColor(sig->color))}); - updateSeries(sig); - updateTitle(); - emit charts_widget->seriesChanged(); -} - -bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const { - return std::any_of(sigs.cbegin(), sigs.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); -} - -void ChartView::removeIf(std::function predicate) { - int prev_size = sigs.size(); - sigs.erase(std::remove_if(sigs.begin(), sigs.end(), predicate), sigs.end()); - if (sigs.empty()) { - charts_widget->removeChart(this); - } else if (sigs.size() != prev_size) { - emit charts_widget->seriesChanged(); - updateAxisY(); - updateTitle(); - } -} - -void ChartView::signalUpdated(const cabana::Signal *sig) { - auto it = std::find_if(sigs.begin(), sigs.end(), [sig](auto &s) { return s.sig == sig; }); - if (it != sigs.end()) { - if (it->color != toQColor(sig->color)) { - it->color = uniqueColor(toQColor(sig->color), sig); - } - updateTitle(); - updateSeries(sig); - } -} - -void ChartView::msgUpdated(MessageId id) { - if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id.address == id.address; })) { - updateTitle(); - } -} - -void ChartView::manageSignals() { - SignalSelector dlg(tr("Manage Chart"), this); - for (auto &s : sigs) { - dlg.addSelected(s.msg_id, s.sig); - } - if (dlg.exec() == QDialog::Accepted) { - auto items = dlg.seletedItems(); - for (auto s : items) { - addSignal(s->msg_id, s->sig); - } - removeIf([&](auto &s) { - return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it->msg_id && s.sig == it->sig; }); - }); - } -} - -void ChartView::resizeEvent(QResizeEvent *event) { - QWidget::resizeEvent(event); - const auto margins = layoutMargins(style()); - QPixmap grip = utils::icon("grip-horizontal"); - move_icon_rect = QRect(QPoint(margins.left(), margins.top()), grip.size() / grip.devicePixelRatio()); - close_btn->resize(close_btn->sizeHint()); - manage_btn->resize(manage_btn->sizeHint()); - close_btn->move(rect().right() - margins.right() - close_btn->width(), margins.top()); - manage_btn->move(close_btn->x() - manage_btn->width() - style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), margins.top()); - updatePlotArea(align_to, true); -} - -void ChartView::updatePlotArea(int left_pos, bool force) { - if (align_to != left_pos || force) { - align_to = left_pos; - - const auto margins = layoutMargins(style()); - QFont bold_font = font(); - bold_font.setBold(true); - QFontMetrics fm(font()), bfm(bold_font); - const int marker_size = fm.height() - 4; - const int row_height = std::max(marker_size, fm.height()) + QFontMetrics(signal_value_font).height() + 3; - const int legend_left = move_icon_rect.right() + margins.left(); - const int legend_right = std::max(manage_btn->x() - margins.right(), legend_left + 10); - - // layout legend entries left-to-right, wrapping between the move icon and the buttons - legend_rects.clear(); - int x = legend_left, y = margins.top(); - for (auto &s : sigs) { - int w = marker_size + 5 + bfm.horizontalAdvance(QString::fromStdString(s.sig->name)) + - fm.horizontalAdvance(QString::fromStdString(" " + msgName(s.msg_id) + " " + s.msg_id.toString())); - w = std::min(w, legend_right - legend_left); // keep oversized entries clear of the header buttons - if (x + w > legend_right && x > legend_left) { - x = legend_left; - y += row_height; - } - legend_rects.emplace_back(x, y, w, std::max(marker_size, fm.height())); - x += w + 12; - } - - // add top space for the legend and signal values - int adjust_top = (y + row_height) - margins.top(); - adjust_top = std::max(adjust_top, manage_btn->geometry().bottom() + style()->pixelMetric(QStyle::PM_LayoutTopMargin)); - // add right space for x-axis label - QSizeF x_label_size = fm.size(Qt::TextSingleLine, QString::number(x_max, 'f', xAxisPrecision())) + QSizeF{5, 5}; - plot_area = rect().adjusted(align_to + margins.left(), adjust_top + margins.top(), - -x_label_size.width() / 2 - margins.right(), - -x_label_size.height() - margins.bottom()); - resetChartCache(); - } -} - -void ChartView::updateTitle() { - split_chart_act->setEnabled(sigs.size() > 1); - updatePlotArea(align_to, true); -} - -void ChartView::updatePlot(double cur, double min, double max) { - cur_sec = cur; - if (min != x_min || max != x_max) { - x_min = min; - x_max = max; - updateAxisY(); - // update tooltip - if (tooltip_x >= 0) { - showTip(secondsAtPoint({tooltip_x, 0})); - } - resetChartCache(); - } - update(); -} - -void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector &events, - std::vector &vals, std::vector &step_vals) { - vals.reserve(vals.size() + events.capacity()); - step_vals.reserve(step_vals.size() + events.capacity() * 2); - - double value = 0; - for (const CanEvent *e : events) { - if (sig->getValue(e->dat, e->size, &value)) { - const double ts = can->toSeconds(e->mono_time); - vals.emplace_back(ts, value); - if (!step_vals.empty()) - step_vals.emplace_back(ts, step_vals.back().y()); - step_vals.emplace_back(ts, value); - } - } -} - -void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *msg_new_events) { - for (auto &s : sigs) { - if (!sig || s.sig == sig) { - if (!msg_new_events) { - s.vals.clear(); - s.step_vals.clear(); - } - auto events = msg_new_events ? msg_new_events : &can->eventsMap(); - auto it = events->find(s.msg_id); - if (it == events->end() || it->second.empty()) continue; - - if (s.vals.empty() || can->toSeconds(it->second.back()->mono_time) > s.vals.back().x()) { - appendCanEvents(s.sig, it->second, s.vals, s.step_vals); - } else { - std::vector vals, step_vals; - appendCanEvents(s.sig, it->second, vals, step_vals); - s.vals.insert(std::lower_bound(s.vals.begin(), s.vals.end(), vals.front().x(), xLessThan), - vals.begin(), vals.end()); - s.step_vals.insert(std::lower_bound(s.step_vals.begin(), s.step_vals.end(), step_vals.front().x(), xLessThan), - step_vals.begin(), step_vals.end()); - } - - if (!can->liveStreaming()) { - s.segment_tree.build(s.vals.size(), [&vals = s.vals](int i) { return vals[i].y(); }); - } - } - } - updateAxisY(); - // invoke resetChartCache in ui thread - QMetaObject::invokeMethod(this, &ChartView::resetChartCache, Qt::QueuedConnection); -} - -// auto zoom on yaxis -void ChartView::updateAxisY() { - if (sigs.empty()) return; - - double min = std::numeric_limits::max(); - double max = std::numeric_limits::lowest(); - QString unit = QString::fromStdString(sigs[0].sig->unit); - - for (auto &s : sigs) { - if (!s.visible) continue; - - // Only show unit when all signals have the same unit - if (unit != QString::fromStdString(s.sig->unit)) { - unit.clear(); - } - - auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), x_min, xLessThan); - auto last = std::lower_bound(first, s.vals.cend(), x_max, xLessThan); - s.min = std::numeric_limits::max(); - s.max = std::numeric_limits::lowest(); - if (can->liveStreaming()) { - for (auto it = first; it != last; ++it) { - if (it->y() < s.min) s.min = it->y(); - if (it->y() > s.max) s.max = it->y(); - } - } else { - std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last)); - } - min = std::min(min, s.min); - max = std::max(max, s.max); - } - if (min == std::numeric_limits::max()) min = 0; - if (max == std::numeric_limits::lowest()) max = 0; - - if (y_unit != unit) { - y_unit = unit; - y_label_width = 0; // recalc width - } - - double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05; - auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3); - if (min_y != y_min || max_y != y_max || y_label_width == 0) { - y_min = min_y; - y_max = max_y; - y_tick_count = tick_count; - y_precision = std::max(int(-std::floor(std::log10((max_y - min_y) / (tick_count - 1)))), 0); - - QFontMetrics fm(font()); - int max_label_width = 0; - for (int i = 0; i < tick_count; i++) { - qreal value = min_y + (i * (max_y - min_y) / (tick_count - 1)); - max_label_width = std::max(max_label_width, fm.horizontalAdvance(QString::number(value, 'f', y_precision))); - } - - int title_spacing = y_unit.isEmpty() ? 0 : fm.size(Qt::TextSingleLine, y_unit).height(); - y_label_width = title_spacing + max_label_width + 15; - emit axisYLabelWidthChanged(y_label_width); - } -} - -std::tuple ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) { - qreal range = niceNumber((max - min), true); // range with ceiling - qreal step = niceNumber(range / (tick_count - 1), false); - min = std::floor(min / step); - max = std::ceil(max / step); - tick_count = int(max - min) + 1; - return {min * step, max * step, tick_count}; -} - -int ChartView::xAxisPrecision() const { - return std::max(int(-std::floor(std::log10((x_max - x_min) / (X_TICK_COUNT - 1)))), 2); -} - -// nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n -qreal ChartView::niceNumber(qreal x, bool ceiling) { - qreal z = std::pow(10, std::floor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x - qreal q = x / z; //q<10 && q>=1; - if (ceiling) { - if (q <= 1.0) q = 1; - else if (q <= 2.0) q = 2; - else if (q <= 5.0) q = 5; - else q = 10; - } else { - if (q < 1.5) q = 1; - else if (q < 3.0) q = 2; - else if (q < 7.0) q = 5; - else q = 10; - } - return q * z; -} - -void ChartView::contextMenuEvent(QContextMenuEvent *event) { - QMenu context_menu(this); - context_menu.addActions(menu->actions()); - context_menu.addSeparator(); - context_menu.addAction(charts_widget->undo_zoom_action); - context_menu.addAction(charts_widget->redo_zoom_action); - context_menu.addSeparator(); - context_menu.addAction(close_act); - context_menu.exec(event->globalPos()); -} - -void ChartView::mousePressEvent(QMouseEvent *event) { - press_pos = event->pos(); - if (event->button() == Qt::LeftButton && move_icon_rect.contains(event->pos())) { - charts_widget->startChartDrag(this, event->globalPos()); - } else if (event->button() == Qt::LeftButton && event->modifiers().testFlag(Qt::ShiftModifier)) { - // Save current playback state when scrubbing - resume_after_scrub = !can->isPaused(); - if (resume_after_scrub) { - can->pause(true); - } - mouse_mode = MouseMode::Scrub; - } else if (event->button() == Qt::LeftButton && plot_area.contains(event->pos())) { - mouse_mode = MouseMode::Rubber; - rubber_rect = QRect(); - } else { - QWidget::mousePressEvent(event); - } -} - -void ChartView::mouseMoveEvent(QMouseEvent *ev) { - // Scrubbing - if (mouse_mode == MouseMode::Scrub && ev->modifiers().testFlag(Qt::ShiftModifier)) { - if (plot_area.contains(ev->pos())) { - can->seekTo(std::clamp(secondsAtPoint(ev->pos()), can->minSeconds(), can->maxSeconds())); - } - } - - if (mouse_mode == MouseMode::Rubber) { - // horizontal selection, clamped to the plot area - int left = std::clamp(std::min(press_pos.x(), ev->pos().x()), plot_area.left(), plot_area.right()); - int right = std::clamp(std::max(press_pos.x(), ev->pos().x()), plot_area.left(), plot_area.right()); - rubber_rect = QRect(left, plot_area.top(), right - left, plot_area.height()); - update(); - } - - clearTrackPoints(); - if (mouse_mode != MouseMode::Rubber && plot_area.contains(ev->pos()) && isActiveWindow()) { - charts_widget->showValueTip(secondsAtPoint(ev->pos())); - } else if (tip_label->isVisible()) { - charts_widget->showValueTip(-1); - } - QWidget::mouseMoveEvent(ev); -} - -void ChartView::mouseReleaseEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton && mouse_mode == MouseMode::Rubber) { - mouse_mode = MouseMode::None; - // Prevent zooming/seeking past the end of the route - double min = std::clamp(secondsAtPoint(rubber_rect.topLeft()), can->minSeconds(), can->maxSeconds()); - double max = std::clamp(secondsAtPoint(rubber_rect.bottomRight()), can->minSeconds(), can->maxSeconds()); - if (rubber_rect.width() <= 0) { - // no rubber dragged, seek to mouse position - can->seekTo(std::clamp(secondsAtPoint(press_pos), can->minSeconds(), can->maxSeconds())); - } else if (rubber_rect.width() > 10 && (max - min) > MIN_ZOOM_SECONDS) { - charts_widget->zoom_undo_stack.push(new ZoomCommand({min, max})); - } - rubber_rect = QRect(); - update(); - } else if (event->button() == Qt::LeftButton && mouse_mode == MouseMode::None && sigs.size() > 1) { - // toggle series visibility by clicking its legend entry - for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { - if (legend_rects[i].contains(press_pos) && legend_rects[i].contains(event->pos())) { - sigs[i].visible = !sigs[i].visible; - updateAxisY(); - updateTitle(); - break; - } - } - } else if (event->button() == Qt::RightButton) { - charts_widget->zoom_undo_stack.undo(); - } else { - QWidget::mouseReleaseEvent(event); - } - - // Resume playback if we were scrubbing - if (mouse_mode == MouseMode::Scrub) { - mouse_mode = MouseMode::None; - if (resume_after_scrub) { - can->pause(false); - resume_after_scrub = false; - } - } -} - -void ChartView::takeSignalsFrom(ChartView *source) { - for (auto &s : source->sigs) { - sigs.push_back(std::move(s)); - sigs.back().color = uniqueColor(sigs.back().color, sigs.back().sig); - } - source->sigs.clear(); - updateAxisY(); - updateTitle(); - charts_widget->removeChart(source); -} - -void ChartView::showTip(double sec) { - QRect tip_area(0, plot_area.top(), rect().width(), plot_area.height()); - QRect visible_rect = charts_widget->chartVisibleRect(this).intersected(tip_area); - if (visible_rect.isEmpty()) { - tip_label->hide(); - return; - } - - tooltip_x = xPos(sec); - qreal x = -1; - QStringList text_list; - for (auto &s : sigs) { - if (s.visible) { - QString value = "--"; - // use reverse iterator to find last item <= sec. - auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double v) { return p.x() > v; }); - if (it != s.vals.crend() && it->x() >= x_min) { - value = QString::fromStdString(s.sig->formatValue(it->y(), false)); - s.track_pt = *it; - x = std::max(x, xPos(it->x())); - } - QString name = sigs.size() > 1 ? QString::fromStdString(s.sig->name) + ": " : ""; - QString min = s.min == std::numeric_limits::max() ? "--" : QString::number(s.min); - QString max = s.max == std::numeric_limits::lowest() ? "--" : QString::number(s.max); - text_list << QString("■ %2%3 (%4, %5)") - .arg(s.color.name(), name, value, min, max); - } - } - if (x < 0) { - x = tooltip_x; - } - QPoint pt(x, plot_area.top()); - text_list.push_front(QString::number(secondsAtPoint({x, 0}), 'f', 3)); - QString text = "

" % text_list.join("
") % "

"; - tip_label->showText(pt, text, this, visible_rect); - update(); -} - -void ChartView::hideTip() { - clearTrackPoints(); - tooltip_x = -1; - tip_label->hide(); - update(); -} - -void ChartView::resetChartCache() { - chart_pixmap = QPixmap(); - update(); -} - -void ChartView::paintEvent(QPaintEvent *event) { - QPainter painter(this); - painter.setRenderHints(QPainter::Antialiasing); - - // the static layer is invalidated on x-range change and data merge, so cache it in live mode too - const qreal dpr = devicePixelRatioF(); - if (chart_pixmap.isNull() || chart_pixmap.size() != size() * dpr) { - chart_pixmap = QPixmap(size() * dpr); - chart_pixmap.setDevicePixelRatio(dpr); - QPainter p(&chart_pixmap); - p.setRenderHints(QPainter::Antialiasing); - p.setFont(font()); - drawStaticLayer(&p); - } - painter.drawPixmap(QPoint(), chart_pixmap); - - if (can_drop) { - painter.setPen(QPen(palette().color(QPalette::Highlight), 4)); - painter.drawRect(rect()); - } - drawForeground(&painter); -} - -void ChartView::drawStaticLayer(QPainter *painter) { - painter->fillRect(rect(), palette().color(QPalette::Base)); - painter->drawPixmap(move_icon_rect.topLeft(), utils::icon("grip-horizontal")); - drawAxes(painter); - drawLegend(painter); - drawSeries(painter); -} - -void ChartView::drawAxes(QPainter *painter) { - const QColor text_color = palette().color(QPalette::Text); - QColor grid_color = text_color; - grid_color.setAlpha(50); - QFontMetrics fm(font()); - painter->setFont(font()); - - // y grid lines and tick labels - for (int i = 0; i < y_tick_count; ++i) { - double value = y_min + i * (y_max - y_min) / (y_tick_count - 1); - qreal y = yPos(value); - painter->setPen(grid_color); - painter->drawLine(QPointF(plot_area.left(), y), QPointF(plot_area.right(), y)); - painter->setPen(text_color); - QRectF label_rect(0, y - fm.height() / 2.0, plot_area.left() - 6, fm.height()); - painter->drawText(label_rect, Qt::AlignRight | Qt::AlignVCenter, QString::number(value, 'f', y_precision)); - } - - // rotated y axis title (unit) - if (!y_unit.isEmpty()) { - painter->save(); - painter->translate(plot_area.left() - y_label_width + fm.height() / 2.0, plot_area.center().y()); - painter->rotate(-90); - painter->drawText(QRectF(-plot_area.height() / 2.0, -fm.height() / 2.0, plot_area.height(), fm.height()), - Qt::AlignCenter, y_unit); - painter->restore(); - } - - // x grid lines and tick labels - const int x_precision = xAxisPrecision(); - for (int i = 0; i < X_TICK_COUNT; ++i) { - double sec = x_min + i * (x_max - x_min) / (X_TICK_COUNT - 1); - qreal x = xPos(sec); - painter->setPen(grid_color); - painter->drawLine(QPointF(x, plot_area.top()), QPointF(x, plot_area.bottom())); - painter->setPen(text_color); - QString label = QString::number(sec, 'f', x_precision); - QRectF label_rect(x - 100, plot_area.bottom() + AXIS_X_TOP_MARGIN, 200, fm.height()); - painter->drawText(label_rect, Qt::AlignHCenter | Qt::AlignTop, label); - } -} - -void ChartView::drawLegend(QPainter *painter) { - QColor title_color = palette().color(QPalette::WindowText); - // Draw message details in similar color, but slightly fade it to the background - QColor msg_color = title_color; - msg_color.setAlpha(180); - QFont bold_font = font(); - bold_font.setBold(true); - const int marker_size = QFontMetrics(font()).height() - 4; - - for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { - const auto &s = sigs[i]; - const QRect &r = legend_rects[i]; - painter->setPen(Qt::NoPen); - painter->setBrush(s.color); - QRectF marker_rect(r.left(), r.center().y() - marker_size / 2.0, marker_size, marker_size); - series_type == SeriesType::Scatter ? painter->drawEllipse(marker_rect) : painter->drawRect(marker_rect); - - bold_font.setStrikeOut(!s.visible); - QFont normal_font = font(); - normal_font.setStrikeOut(!s.visible); - - qreal x = r.left() + marker_size + 5; - painter->setFont(bold_font); - painter->setPen(title_color); - QString name = QFontMetrics(bold_font).elidedText(QString::fromStdString(s.sig->name), Qt::ElideRight, r.right() - x); - painter->drawText(QRectF(x, r.top(), r.right() - x, r.height()), Qt::AlignLeft | Qt::AlignVCenter, name); - x += QFontMetrics(bold_font).horizontalAdvance(name); - painter->setFont(normal_font); - painter->setPen(msg_color); - QString msg = QFontMetrics(normal_font).elidedText(QString::fromStdString(" " + msgName(s.msg_id) + " " + s.msg_id.toString()), - Qt::ElideRight, r.right() - x); - painter->drawText(QRectF(x, r.top(), r.right() - x, r.height()), Qt::AlignLeft | Qt::AlignVCenter, msg); - } -} - -void ChartView::drawSeries(QPainter *painter) { - painter->save(); - painter->setClipRect(plot_area); - for (auto &s : sigs) { - if (!s.visible) continue; - - // visible points in vals to compute point density - auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), x_min, xLessThan); - auto last = std::lower_bound(first, s.vals.cend(), x_max, xLessThan); - int num_points = std::max(last - first, 1); - double pixels_per_point = 0; - if (first != last) { - const QPointF &right_pt = last == s.vals.cend() ? s.vals.back() : *last; - pixels_per_point = (xPos(right_pt.x()) - xPos(first->x())) / num_points; - } - - if (series_type == SeriesType::Scatter) { - qreal radius = std::clamp(pixels_per_point / 2.0, 2.0, 8.0) / 2.0; - painter->setPen(Qt::NoPen); - painter->setBrush(s.color); - for (auto it = first; it != last; ++it) { - painter->drawEllipse(QPointF(xPos(it->x()), yPos(it->y())), radius, radius); - } - } else { - const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; - auto begin = std::lower_bound(points.cbegin(), points.cend(), x_min, xLessThan); - if (begin != points.cbegin()) --begin; - auto end = std::lower_bound(begin, points.cend(), x_max, xLessThan); - if (end != points.cend()) ++end; - if (begin == end) continue; - - std::vector polyline; - polyline.reserve(end - begin); - for (auto it = begin; it != end; ++it) { - polyline.emplace_back(xPos(it->x()), yPos(it->y())); - } - painter->setPen(QPen(s.color, 2)); - painter->setBrush(Qt::NoBrush); - painter->drawPolyline(polyline.data(), polyline.size()); - - // show points when zoomed in enough - if (num_points == 1 || pixels_per_point > 20) { - painter->setPen(Qt::NoPen); - painter->setBrush(s.color); - for (auto it = first; it != last; ++it) { - painter->drawEllipse(QPointF(xPos(it->x()), yPos(it->y())), 4, 4); - } - } - } - } - painter->restore(); -} - -void ChartView::drawForeground(QPainter *painter) { - drawTimeline(painter); - drawSignalValue(painter); - // draw track points - painter->setPen(Qt::NoPen); - qreal track_line_x = -1; - for (auto &s : sigs) { - if (!s.track_pt.isNull() && s.visible) { - painter->setBrush(s.color.darker(125)); - QPointF pos(xPos(s.track_pt.x()), yPos(s.track_pt.y())); - painter->drawEllipse(pos, 5.5, 5.5); - track_line_x = std::max(track_line_x, pos.x()); - } - } - if (track_line_x > 0) { - painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); - painter->drawLine(QPointF{track_line_x, (qreal)plot_area.top()}, QPointF{track_line_x, (qreal)plot_area.bottom()}); - } - - drawRubberBandTimeRange(painter); -} - -void ChartView::drawRubberBandTimeRange(QPainter *painter) { - if (rubber_rect.width() <= 1) return; - - // selection rect - QColor highlight = palette().color(QPalette::Highlight); - QColor fill = highlight; - fill.setAlpha(50); - painter->fillRect(rubber_rect, fill); - painter->setPen(highlight); - painter->setBrush(Qt::NoBrush); - painter->drawRect(rubber_rect); - - // time labels at the bottom corners - painter->setPen(Qt::white); - painter->setFont(font()); - for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) { - QString sec = QString::number(secondsAtPoint(pt), 'f', 2); - auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN); - pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2}); - painter->fillRect(r, Qt::gray); - painter->drawText(r, Qt::AlignCenter, sec); - } -} - -void ChartView::drawTimeline(QPainter *painter) { - // draw vertical time line - qreal x = std::clamp(xPos(cur_sec), (qreal)plot_area.left(), (qreal)plot_area.right()); - painter->setPen(QPen(palette().color(QPalette::Text), 1)); - painter->drawLine(QPointF{x, plot_area.top() - 1.0}, QPointF{x, plot_area.bottom() + 1.0}); - - // draw current time under the axis-x - QString time_str = QString::number(cur_sec, 'f', 2); - QSize time_str_size = QFontMetrics(font()).size(Qt::TextSingleLine, time_str) + QSize(8, 2); - QRectF time_str_rect(QPointF(x - time_str_size.width() / 2.0, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); - QPainterPath path; - path.addRoundedRect(time_str_rect, 3, 3); - painter->fillPath(path, utils::isDarkTheme() ? Qt::darkGray : Qt::gray); - painter->setPen(palette().color(QPalette::BrightText)); - painter->setFont(font()); - painter->drawText(time_str_rect, Qt::AlignCenter, time_str); -} - -void ChartView::drawSignalValue(QPainter *painter) { - painter->setFont(signal_value_font); - painter->setPen(palette().color(QPalette::Text)); - for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { - const auto &s = sigs[i]; - auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, - [](auto &p, double x) { return p.x() > x + EPSILON; }); - QString value = (it != s.vals.crend() && it->x() >= x_min) ? QString::fromStdString(s.sig->formatValue(it->y())) : "--"; - QRectF value_rect(legend_rects[i].bottomLeft() - QPoint(0, 1), legend_rects[i].size()); - QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width()); - painter->drawText(value_rect, Qt::AlignHCenter | Qt::AlignTop, elided_val); - } -} - -QColor ChartView::uniqueColor(QColor color, const cabana::Signal *exclude) const { - for (auto &s : sigs) { - if (s.sig != exclude && std::abs(color.hueF() - s.color.hueF()) < 0.1) { - // use different color to distinguish it from others. - auto last_color = sigs.back().color; - static thread_local std::mt19937 rng{std::random_device{}()}; - std::uniform_int_distribution sat(35, 99); - std::uniform_int_distribution val(85, 99); - color.setHsvF(std::fmod(last_color.hueF() + 60 / 360.0, 1.0), - sat(rng) / 100.0, - val(rng) / 100.0); - break; - } - } - return color; -} - -void ChartView::setSeriesType(SeriesType type) { - if (type != series_type) { - series_type = type; - menu->actions()[(int)type]->setChecked(true); - updateTitle(); - } -} diff --git a/openpilot/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h deleted file mode 100644 index 3188e1f401..0000000000 --- a/openpilot/tools/cabana/chart/chart.h +++ /dev/null @@ -1,132 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "tools/cabana/chart/tiplabel.h" -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/utils/qtutil.h" - -enum class SeriesType { - Line = 0, - StepLine, - Scatter -}; - -class ChartsWidget; -class ChartView : public QWidget { - Q_OBJECT - -public: - ChartView(const std::pair &x_range, ChartsWidget *parent = nullptr); - void addSignal(const MessageId &msg_id, const cabana::Signal *sig); - bool hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const; - void updateSeries(const cabana::Signal *sig = nullptr, const MessageEventsMap *msg_new_events = nullptr); - void updatePlot(double cur, double min, double max); - void setSeriesType(SeriesType type); - void updatePlotArea(int left, bool force = false); - void showTip(double sec); - void hideTip(); - double secondsAtPoint(const QPointF &pt) const { - return x_min + (pt.x() - plot_area.left()) * (x_max - x_min) / std::max(plot_area.width(), 1); - } - - struct SigItem { - MessageId msg_id; - const cabana::Signal *sig = nullptr; - QColor color; - bool visible = true; - std::vector vals; - std::vector step_vals; - QPointF track_pt{}; - SegmentTree segment_tree; - double min = 0; - double max = 0; - }; - -signals: - void axisYLabelWidthChanged(int w); - -private slots: - void signalUpdated(const cabana::Signal *sig); - void manageSignals(); - void msgUpdated(MessageId id); - void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); } - void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } - -private: - void appendCanEvents(const cabana::Signal *sig, const std::vector &events, - std::vector &vals, std::vector &step_vals); - void createToolButtons(); - void contextMenuEvent(QContextMenuEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - QSize sizeHint() const override; - void updateAxisY(); - void updateTitle(); - void resetChartCache(); - void paintEvent(QPaintEvent *event) override; - void drawStaticLayer(QPainter *painter); - void drawAxes(QPainter *painter); - void drawLegend(QPainter *painter); - void drawSeries(QPainter *painter); - void drawForeground(QPainter *painter); - void drawSignalValue(QPainter *painter); - void drawTimeline(QPainter *painter); - void drawRubberBandTimeRange(QPainter *painter); - int xAxisPrecision() const; - std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); - qreal niceNumber(qreal x, bool ceiling); - QColor uniqueColor(QColor color, const cabana::Signal *exclude = nullptr) const; - void removeIf(std::function predicate); - void takeSignalsFrom(ChartView *source); - void setDropHighlight(bool highlight) { if (std::exchange(can_drop, highlight) != highlight) update(); } - inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } - inline qreal xPos(double sec) const { return plot_area.left() + (sec - x_min) / (x_max - x_min) * plot_area.width(); } - inline qreal yPos(double val) const { return plot_area.bottom() - (val - y_min) / (y_max - y_min) * plot_area.height(); } - - // layout - QRect plot_area; - QRect move_icon_rect; - std::vector legend_rects; - // axes - double x_min; - double x_max; - double y_min = 0; - double y_max = 1; - int y_tick_count = 3; - int y_precision = 0; - QString y_unit; - int y_label_width = 0; - int align_to = 0; - // interaction - enum class MouseMode { None, Rubber, Scrub }; - MouseMode mouse_mode = MouseMode::None; - QPoint press_pos; - QRect rubber_rect; - bool resume_after_scrub = false; - - QMenu *menu; - QAction *split_chart_act; - QAction *close_act; - ToolButton *manage_btn; - ToolButton *close_btn; - TipLabel *tip_label; - std::vector sigs; - double cur_sec = 0; - SeriesType series_type = SeriesType::Line; - QPixmap chart_pixmap; - bool can_drop = false; - double tooltip_x = -1; - QFont signal_value_font; - ChartsWidget *charts_widget; - Connections connections_; - friend class ChartsWidget; -}; diff --git a/openpilot/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc deleted file mode 100644 index 2e3e86bab0..0000000000 --- a/openpilot/tools/cabana/chart/chartswidget.cc +++ /dev/null @@ -1,660 +0,0 @@ -#include "tools/cabana/chart/chartswidget.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include "tools/cabana/chart/chart.h" - -const int MAX_COLUMN_COUNT = 4; -const int CHART_SPACING = 4; - -ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { - align_timer = new QTimer(this); - auto_scroll_timer = new QTimer(this); - setFrameStyle(QFrame::StyledPanel | QFrame::Plain); - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->setSpacing(0); - - // toolbar - toolbar = new QToolBar(tr("Charts"), this); - int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); - toolbar->setIconSize({icon_size, icon_size}); - - auto new_plot_btn = new ToolButton("file-plus", tr("New Chart")); - auto new_tab_btn = new ToolButton("window-stack", tr("New Tab")); - toolbar->addWidget(new_plot_btn); - toolbar->addWidget(new_tab_btn); - toolbar->addWidget(title_label = new QLabel()); - title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0); - - auto chart_type_action = toolbar->addAction(""); - QMenu *chart_type_menu = new QMenu(this); - auto types = std::array{tr("Line"), tr("Step"), tr("Scatter")}; - for (int i = 0; i < types.size(); ++i) { - QString type_text = types[i]; - chart_type_menu->addAction(type_text, this, [=]() { - settings.chart_series_type = i; - chart_type_action->setText("Type: " + type_text); - settingChanged(); - }); - } - chart_type_action->setText("Type: " + types[settings.chart_series_type]); - chart_type_action->setMenu(chart_type_menu); - qobject_cast(toolbar->widgetForAction(chart_type_action))->setPopupMode(QToolButton::InstantPopup); - - QMenu *menu = new QMenu(this); - for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { - menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); }); - } - columns_action = toolbar->addAction(""); - columns_action->setMenu(menu); - qobject_cast(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup); - - QWidget *spacer = new QWidget(this); - spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - toolbar->addWidget(spacer); - - range_lb_action = toolbar->addWidget(range_lb = new QLabel(this)); - range_slider = new LogSlider(1000, Qt::Horizontal, this); - range_slider->setFixedWidth(150 * qApp->devicePixelRatio()); - range_slider->setToolTip(tr("Set the chart range")); - range_slider->setRange(1, settings.max_cached_minutes * 60); - range_slider->setSingleStep(1); - range_slider->setPageStep(60); // 1 min - range_slider_action = toolbar->addWidget(range_slider); - - // zoom controls - undo_zoom_action = toolbar->addAction(utils::icon("arrow-counterclockwise"), tr("Undo Zoom"), [this]() { zoom_undo_stack.undo(); }); - redo_zoom_action = toolbar->addAction(utils::icon("arrow-clockwise"), tr("Redo Zoom"), [this]() { zoom_undo_stack.redo(); }); - undo_zoom_action->setEnabled(false); - redo_zoom_action->setEnabled(false); - connections_.push_back(zoom_undo_stack.indexChanged.connect([this]() { - undo_zoom_action->setEnabled(zoom_undo_stack.canUndo()); - redo_zoom_action->setEnabled(zoom_undo_stack.canRedo()); - })); - reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom"))); - reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - - toolbar->addWidget(remove_all_btn = new ToolButton("x-square", tr("Remove all charts"))); - toolbar->addWidget(dock_btn = new ToolButton("")); - main_layout->addWidget(toolbar); - - // tabbar - tabbar = new TabBar(this); - tabbar->setAutoHide(true); - tabbar->setExpanding(false); - tabbar->setDrawBase(true); - tabbar->setUsesScrollButtons(true); - main_layout->addWidget(tabbar); - - // charts - charts_container = new ChartsContainer(this); - charts_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - charts_scroll = new QScrollArea(this); - charts_scroll->viewport()->setBackgroundRole(QPalette::Base); - charts_scroll->setFrameStyle(QFrame::NoFrame); - charts_scroll->setWidgetResizable(true); - charts_scroll->setWidget(charts_container); - charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - main_layout->addWidget(charts_scroll); - - // chart drag preview - drag_preview = new QLabel(this); - drag_preview->setAttribute(Qt::WA_TransparentForMouseEvents); - drag_preview->hide(); - - // init settings - current_theme = settings.theme; - column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT); - max_chart_range = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60); - display_range = std::make_pair(can->minSeconds(), can->minSeconds() + max_chart_range); - range_slider->setValue(max_chart_range); - updateToolBar(); - - align_timer->setSingleShot(true); - QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); - QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); - connections_.push_back(dbc()->fileChanged.connect([this]() { removeAll(); })); - connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { eventsMerged(events); })); - connections_.push_back(can->msgsReceived.connect([this](const std::set *, bool) { updateState(); })); - connections_.push_back(can->seeking.connect([this](double) { updateState(); })); - connections_.push_back(can->timeRangeChanged.connect([this](const auto &range) { timeRangeChanged(range); })); - QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange); - QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart); - QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll); - QObject::connect(reset_zoom_btn, &QToolButton::clicked, this, &ChartsWidget::zoomReset); - connections_.push_back(settings.changed.connect([this]() { settingChanged(); })); - QObject::connect(new_tab_btn, &QToolButton::clicked, this, &ChartsWidget::newTab); - QObject::connect(this, &ChartsWidget::seriesChanged, this, &ChartsWidget::updateTabBar); - QObject::connect(tabbar, &QTabBar::tabCloseRequested, this, &ChartsWidget::removeTab); - QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { - if (index != -1) updateLayout(true); - }); - QObject::connect(dock_btn, &QToolButton::clicked, this, &ChartsWidget::toggleChartsDocking); - - setIsDocked(true); - newTab(); - qApp->installEventFilter(this); - setWhatsThis(tr(R"( - Chart View
- Click: Click to seek to a corresponding time.
- Drag: Zoom into the chart.
- Shift + Drag: Scrub through the chart to view values.
- Right Mouse: Open the context menu.
- )")); -} - -void ChartsWidget::newTab() { - static int tab_unique_id = 0; - int idx = tabbar->addTab(""); - tabbar->setTabData(idx, tab_unique_id++); - tabbar->setCurrentIndex(idx); - updateTabBar(); -} - -void ChartsWidget::removeTab(int index) { - int id = tabbar->tabData(index).toInt(); - for (auto &c : tab_charts[id]) { - removeChart(c); - } - tab_charts.erase(id); - tabbar->removeTab(index); - updateTabBar(); -} - -void ChartsWidget::updateTabBar() { - for (int i = 0; i < tabbar->count(); ++i) { - const auto &charts_in_tab = tab_charts[tabbar->tabData(i).toInt()]; - tabbar->setTabText(i, QString("Tab %1 (%2)").arg(i + 1).arg((int)charts_in_tab.size())); - } -} - -void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) { - std::vector> futures; - for (auto c : charts) { - futures.push_back(std::async(std::launch::async, &ChartView::updateSeries, c, nullptr, &new_events)); - } - for (auto &f : futures) f.get(); -} - -void ChartsWidget::timeRangeChanged(const std::optional> &time_range) { - updateToolBar(); - updateState(); -} - -void ChartsWidget::zoomReset() { - can->setTimeRange(std::nullopt); - zoom_undo_stack.clear(); -} - -QRect ChartsWidget::chartVisibleRect(ChartView *chart) { - const QRect visible_rect(-charts_container->pos(), charts_scroll->viewport()->size()); - return chart->rect().intersected(QRect(chart->mapFrom(charts_container, visible_rect.topLeft()), visible_rect.size())); -} - -void ChartsWidget::showValueTip(double sec) { - emit showTip(sec); - if (sec < 0 && !value_tip_visible_) return; - - value_tip_visible_ = sec >= 0; - for (auto c : currentCharts()) { - value_tip_visible_ ? c->showTip(sec) : c->hideTip(); - } -} - -void ChartsWidget::updateState() { - if (charts.empty()) return; - - const auto &time_range = can->timeRange(); - const double cur_sec = can->currentSec(); - if (!time_range.has_value()) { - double pos = (cur_sec - display_range.first) / std::max(1.0, max_chart_range); - if (pos < 0 || pos > 0.8) { - display_range.first = std::max(can->minSeconds(), cur_sec - max_chart_range * 0.1); - } - double max_sec = std::min(display_range.first + max_chart_range, can->maxSeconds()); - display_range.first = std::max(can->minSeconds(), max_sec - max_chart_range); - display_range.second = display_range.first + max_chart_range; - } - - const auto &range = time_range ? *time_range : display_range; - for (auto c : charts) { - c->updatePlot(cur_sec, range.first, range.second); - } -} - -void ChartsWidget::setMaxChartRange(int value) { - max_chart_range = settings.chart_range = range_slider->value(); - updateToolBar(); - updateState(); -} - -void ChartsWidget::setIsDocked(bool docked) { - is_docked = docked; - dock_btn->setIcon(is_docked ? "arrow-up-right-square" : "arrow-down-left-square"); - dock_btn->setToolTip(is_docked ? tr("Float the charts window") : tr("Dock the charts window")); -} - -void ChartsWidget::updateToolBar() { - title_label->setText(tr("Charts: %1").arg(charts.size())); - columns_action->setText(tr("Columns: %1").arg(column_count)); - range_lb->setText(QString::fromStdString(utils::formatSeconds(max_chart_range))); - - bool is_zoomed = can->timeRange().has_value(); - range_lb_action->setVisible(!is_zoomed); - range_slider_action->setVisible(!is_zoomed); - undo_zoom_action->setVisible(is_zoomed); - redo_zoom_action->setVisible(is_zoomed); - reset_zoom_action->setVisible(is_zoomed); - reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(can->timeRange()->first, 0, 'f', 2).arg(can->timeRange()->second, 0, 'f', 2) : ""); - remove_all_btn->setEnabled(!charts.empty()); -} - -void ChartsWidget::settingChanged() { - if (std::exchange(current_theme, settings.theme) != current_theme) { - undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); - redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); - } - if (range_slider->maximum() != settings.max_cached_minutes * 60) { - range_slider->setRange(1, settings.max_cached_minutes * 60); - } - for (auto c : charts) { - c->setFixedHeight(settings.chart_height); - c->setSeriesType((SeriesType)settings.chart_series_type); - c->resetChartCache(); - } -} - -ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) { - for (auto c : charts) - if (c->hasSignal(id, sig)) return c; - return nullptr; -} - -ChartView *ChartsWidget::createChart(int pos) { - auto chart = new ChartView(can->timeRange().value_or(display_range), this); - chart->setFixedHeight(settings.chart_height); - chart->setMinimumWidth(CHART_MIN_WIDTH); - chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - QObject::connect(chart, &ChartView::axisYLabelWidthChanged, align_timer, qOverload<>(&QTimer::start)); - pos = std::clamp(pos, 0, (int)charts.size()); - charts.insert(charts.begin() + pos, chart); - currentCharts().insert(currentCharts().begin() + pos, chart); - updateLayout(true); - updateToolBar(); - return chart; -} - -void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { - ChartView *chart = findChart(id, sig); - if (show && !chart) { - chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart(); - chart->addSignal(id, sig); - updateState(); - } else if (!show && chart) { - chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; }); - } -} - -void ChartsWidget::splitChart(ChartView *src_chart) { - if (src_chart->sigs.size() > 1) { - int pos = std::find(charts.begin(), charts.end(), src_chart) - charts.begin() + 1; - for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) { - auto c = createChart(pos); - // Restore to the original color - it->color = toQColor(it->sig->color); - c->sigs.emplace_back(std::move(*it)); - c->updateAxisY(); - c->updateTitle(); - it = src_chart->sigs.erase(it); - } - src_chart->updateAxisY(); - src_chart->updateTitle(); - updateState(); - QTimer::singleShot(0, src_chart, &ChartView::resetChartCache); - } -} - -QStringList ChartsWidget::serializeChartIds() const { - QStringList chart_ids; - for (auto c : charts) { - QStringList ids; - for (const auto& s : c->sigs) - ids += QString("%1|%2").arg(QString::fromStdString(s.msg_id.toString()), QString::fromStdString(s.sig->name)); - chart_ids += ids.join(','); - } - std::reverse(chart_ids.begin(), chart_ids.end()); - return chart_ids; -} - -void ChartsWidget::restoreChartsFromIds(const QStringList& chart_ids) { - for (const auto& chart_id : chart_ids) { - int index = 0; - for (const auto& part : chart_id.split(',')) { - const auto sig_parts = part.split('|'); - if (sig_parts.size() != 2) continue; - MessageId msg_id = MessageId::fromString(sig_parts[0].toStdString()); - if (auto* msg = dbc()->msg(msg_id)) - if (auto* sig = msg->sig(sig_parts[1].toStdString())) - showChart(msg_id, sig, true, index++ > 0); - } - } -} - -void ChartsWidget::setColumnCount(int n) { - n = std::clamp(n, 1, MAX_COLUMN_COUNT); - if (column_count != n) { - column_count = settings.chart_column_count = n; - updateToolBar(); - updateLayout(); - } -} - -void ChartsWidget::updateLayout(bool force) { - auto charts_layout = charts_container->charts_layout; - int n = MAX_COLUMN_COUNT; - for (; n > 1; --n) { - if ((n * CHART_MIN_WIDTH + (n - 1) * charts_layout->horizontalSpacing()) < charts_layout->geometry().width()) break; - } - - bool show_column_cb = n > 1; - columns_action->setVisible(show_column_cb); - - n = std::min(column_count, n); - auto ¤t_charts = currentCharts(); - if ((current_charts.size() != charts_layout->count() || n != current_column_count) || force) { - current_column_count = n; - charts_container->setUpdatesEnabled(false); - for (auto c : charts) { - c->setVisible(false); - } - for (int i = 0; i < current_charts.size(); ++i) { - charts_layout->addWidget(current_charts[i], i / n, i % n); - if (current_charts[i]->sigs.empty()) { - // the chart will be resized after add signal. delay setVisible to reduce flicker. - QTimer::singleShot(0, current_charts[i], [c = current_charts[i]]() { c->setVisible(true); }); - } else { - current_charts[i]->setVisible(true); - } - } - charts_container->setUpdatesEnabled(true); - } -} - -void ChartsWidget::startChartDrag(ChartView *chart, const QPoint &global_pos) { - stopAutoScroll(); - drag = {.source = chart, .press_pos = global_pos}; - QPixmap px = chart->grab().scaledToWidth(CHART_MIN_WIDTH * chart->devicePixelRatio(), Qt::SmoothTransformation); - drag_preview->setPixmap(px); - drag_preview->resize(px.size() / px.devicePixelRatio()); -} - -void ChartsWidget::dragChartMove(const QPoint &global_pos) { - if (!drag.active) { - if ((global_pos - drag.press_pos).manhattanLength() < QApplication::startDragDistance()) return; - drag.active = true; - drag_preview->show(); - drag_preview->raise(); - } - drag_preview->move(mapFromGlobal(global_pos) + QPoint(5, 5)); - - // hovering a tab switches to it so the chart can be dropped into another tab - int tab = tabbar->tabAt(tabbar->mapFromGlobal(global_pos)); - if (tab >= 0 && tab != tabbar->currentIndex()) { - tabbar->setCurrentIndex(tab); - } - - const QPoint container_pos = charts_container->mapFromGlobal(global_pos); - ChartView *target = nullptr; - for (auto c : currentCharts()) { - if (c != drag.source && c->isVisible() && c->geometry().contains(container_pos)) { - target = c; - break; - } - } - if (std::exchange(drop_target, target) != target) { - for (auto c : charts) c->setDropHighlight(c == target); - } - bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos)); - bool on_background = !target && in_viewport && !charts_container->childAt(container_pos); - charts_container->drawDropIndicator(on_background ? container_pos : QPoint()); - - if (in_viewport) { - startAutoScroll(global_pos); - } -} - -void ChartsWidget::cancelChartDrag() { - drag = {}; - stopAutoScroll(); - drag_preview->hide(); - charts_container->drawDropIndicator({}); - if (auto target = std::exchange(drop_target, nullptr)) target->setDropHighlight(false); -} - -void ChartsWidget::dragChartRelease(const QPoint &global_pos) { - ChartView *source = drag.source; - bool active = drag.active; - ChartView *target = drop_target; - cancelChartDrag(); - if (!active) return; - - const QPoint container_pos = charts_container->mapFromGlobal(global_pos); - bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos)); - if (target) { - // merge source into target - target->takeSignalsFrom(source); - } else if (in_viewport && !charts_container->childAt(container_pos)) { - // reorder within the current tab - auto w = charts_container->getDropAfter(container_pos); - if (w != source) { - for (auto &[_, list] : tab_charts) { - list.erase(std::remove(list.begin(), list.end(), source), list.end()); - } - auto &cur = currentCharts(); - int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0; - cur.insert(cur.begin() + to, source); - updateLayout(true); - updateTabBar(); - } - } -} - -void ChartsWidget::startAutoScroll(const QPoint &global_pos) { - auto_scroll_pos = global_pos; - auto_scroll_timer->start(50); -} - -void ChartsWidget::stopAutoScroll() { - auto_scroll_timer->stop(); - auto_scroll_count = 0; -} - -void ChartsWidget::doAutoScroll() { - QScrollBar *scroll = charts_scroll->verticalScrollBar(); - if (auto_scroll_count < scroll->pageStep()) { - ++auto_scroll_count; - } - - int value = scroll->value(); - QPoint pos = charts_scroll->viewport()->mapFromGlobal(auto_scroll_pos); - QRect area = charts_scroll->viewport()->rect(); - - if (pos.y() - area.top() < settings.chart_height / 2) { - scroll->setValue(value - auto_scroll_count); - } else if (area.bottom() - pos.y() < settings.chart_height / 2) { - scroll->setValue(value + auto_scroll_count); - } - if (value == scroll->value()) { - stopAutoScroll(); - } else if (chartDragActive()) { - // refresh the drop indicator/target at the new scroll position - dragChartMove(auto_scroll_pos); - } -} - -QSize ChartsWidget::minimumSizeHint() const { - return QSize(CHART_MIN_WIDTH * 1.5, QWidget::minimumSizeHint().height()); -} - -void ChartsWidget::newChart() { - SignalSelector dlg(tr("New Chart"), this); - if (dlg.exec() == QDialog::Accepted) { - auto items = dlg.seletedItems(); - if (!items.empty()) { - auto c = createChart(); - for (auto it : items) { - c->addSignal(it->msg_id, it->sig); - } - updateState(); - } - } -} - -void ChartsWidget::removeChart(ChartView *chart) { - if (drag.source == chart) cancelChartDrag(); - if (drop_target == chart) drop_target = nullptr; - charts.erase(std::remove(charts.begin(), charts.end(), chart), charts.end()); - chart->deleteLater(); - for (auto &[_, list] : tab_charts) { - list.erase(std::remove(list.begin(), list.end(), chart), list.end()); - } - updateToolBar(); - updateLayout(true); - alignCharts(); - emit seriesChanged(); -} - -void ChartsWidget::removeAll() { - while (tabbar->count() > 1) { - tabbar->removeTab(1); - } - tab_charts.clear(); - - if (!charts.empty()) { - for (auto c : charts) { - delete c; - } - charts.clear(); - emit seriesChanged(); - } - zoomReset(); -} - -void ChartsWidget::alignCharts() { - int plot_left = 0; - for (auto c : charts) { - plot_left = std::max(plot_left, c->y_label_width); - } - plot_left = std::max((plot_left / 10) * 10 + 10, 50); - for (auto c : charts) { - c->updatePlotArea(plot_left); - } -} - -bool ChartsWidget::eventFilter(QObject *o, QEvent *e) { - // route all mouse events to the chart drag, even when the source chart is hidden by a tab switch - if (chartDragActive()) { - if (e->type() == QEvent::MouseMove) { - dragChartMove(static_cast(e)->globalPos()); - return true; - } else if (e->type() == QEvent::MouseButtonRelease && static_cast(e)->button() == Qt::LeftButton) { - dragChartRelease(static_cast(e)->globalPos()); - return false; // let the release through so Qt clears the implicit mouse grab - } else if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonRelease) { - return true; // swallow other buttons during the drag - } - } - - if (!value_tip_visible_) return false; - - if (e->type() == QEvent::MouseMove) { - bool on_tip = qobject_cast(o) != nullptr; - auto global_pos = static_cast(e)->globalPos(); - - for (const auto &c : charts) { - auto local_pos = c->mapFromGlobal(global_pos); - if (c->plot_area.contains(local_pos)) { - if (on_tip) { - showValueTip(c->secondsAtPoint(local_pos)); - } - return false; - } - } - - showValueTip(-1); - } else if (e->type() == QEvent::Wheel) { - if (auto tip = qobject_cast(o)) { - // Forward the event to the parent widget - QCoreApplication::sendEvent(tip->parentWidget(), e); - } - } - return false; -} - -bool ChartsWidget::event(QEvent *event) { - bool back_button = false; - switch (event->type()) { - case QEvent::Resize: - updateLayout(); - break; - case QEvent::MouseButtonPress: - back_button = static_cast(event)->button() == Qt::BackButton; - break; - case QEvent::NativeGesture: - back_button = (static_cast(event)->value() == 180); - break; - case QEvent::WindowDeactivate: - case QEvent::FocusOut: - if (chartDragActive()) cancelChartDrag(); - showValueTip(-1); - default: - break; - } - - if (back_button) { - zoom_undo_stack.undo(); - return true; // Return true since the event has been handled - } - return QFrame::event(event); -} - -// ChartsContainer - -ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) { - setBackgroundRole(QPalette::Window); - QVBoxLayout *charts_main_layout = new QVBoxLayout(this); - charts_main_layout->setContentsMargins(0, CHART_SPACING, 0, CHART_SPACING); - charts_layout = new QGridLayout(); - charts_layout->setSpacing(CHART_SPACING); - charts_main_layout->addLayout(charts_layout); - charts_main_layout->addStretch(0); -} - -void ChartsContainer::paintEvent(QPaintEvent *ev) { - if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) { - QRect r = geometry(); - r.setHeight(CHART_SPACING); - if (auto insert_after = getDropAfter(drop_indictor_pos)) { - r.moveTop(insert_after->geometry().bottom()); - } - - QPainter p(this); - p.fillRect(r, palette().highlight()); - } -} - -ChartView *ChartsContainer::getDropAfter(const QPoint &pos) const { - auto it = std::find_if(charts_widget->currentCharts().crbegin(), charts_widget->currentCharts().crend(), [&pos](auto c) { - auto area = c->geometry(); - return pos.x() >= area.left() && pos.x() <= area.right() && pos.y() >= area.bottom(); - }); - return it == charts_widget->currentCharts().crend() ? nullptr : *it; -} diff --git a/openpilot/tools/cabana/chart/chartswidget.h b/openpilot/tools/cabana/chart/chartswidget.h deleted file mode 100644 index 17e18a90d2..0000000000 --- a/openpilot/tools/cabana/chart/chartswidget.h +++ /dev/null @@ -1,140 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include - -#include "tools/cabana/chart/signalselector.h" -#include "tools/cabana/commands.h" -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/utils/qtutil.h" - -const int CHART_MIN_WIDTH = 300; - -class ChartView; -class ChartsWidget; - -class ChartsContainer : public QWidget { -public: - ChartsContainer(ChartsWidget *parent); - void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); } - void paintEvent(QPaintEvent *ev) override; - ChartView *getDropAfter(const QPoint &pos) const; - - QGridLayout *charts_layout; - ChartsWidget *charts_widget; - QPoint drop_indictor_pos; -}; - -class ChartsWidget : public QFrame { - Q_OBJECT - -public: - ChartsWidget(QWidget *parent = nullptr); - void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge); - inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; } - QStringList serializeChartIds() const; - void restoreChartsFromIds(const QStringList &chart_ids); - -public slots: - void setColumnCount(int n); - void removeAll(); - void timeRangeChanged(const std::optional> &time_range); - void setIsDocked(bool dock); - -signals: - void toggleChartsDocking(); - void seriesChanged(); - void showTip(double seconds); - -private: - QSize minimumSizeHint() const override; - bool event(QEvent *event) override; - void alignCharts(); - void newChart(); - ChartView *createChart(int pos = 0); - void removeChart(ChartView *chart); - void splitChart(ChartView *chart); - QRect chartVisibleRect(ChartView *chart); - void eventsMerged(const MessageEventsMap &new_events); - void updateState(); - void zoomReset(); - void startChartDrag(ChartView *chart, const QPoint &global_pos); - void dragChartMove(const QPoint &global_pos); - void dragChartRelease(const QPoint &global_pos); - void cancelChartDrag(); - bool chartDragActive() const { return drag.source != nullptr; } - void startAutoScroll(const QPoint &global_pos); - void stopAutoScroll(); - void doAutoScroll(); - void updateToolBar(); - void updateTabBar(); - void setMaxChartRange(int value); - void updateLayout(bool force = false); - void settingChanged(); - void showValueTip(double sec); - bool eventFilter(QObject *obj, QEvent *event) override; - void newTab(); - void removeTab(int index); - inline std::vector ¤tCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; } - ChartView *findChart(const MessageId &id, const cabana::Signal *sig); - - QLabel *title_label; - QLabel *range_lb; - LogSlider *range_slider; - QAction *range_lb_action; - QAction *range_slider_action; - bool is_docked = true; - ToolButton *dock_btn; - - QToolBar *toolbar; - QAction *undo_zoom_action; - QAction *redo_zoom_action; - QAction *reset_zoom_action; - ToolButton *reset_zoom_btn; - UndoStack zoom_undo_stack; - - ToolButton *remove_all_btn; - std::vector charts; - std::unordered_map> tab_charts; - TabBar *tabbar; - ChartsContainer *charts_container; - QScrollArea *charts_scroll; - uint32_t max_chart_range = 0; - std::pair display_range; - QAction *columns_action; - int column_count = 1; - int current_column_count = 0; - struct ChartDrag { - ChartView *source = nullptr; - QPoint press_pos; // global - bool active = false; - } drag; - QLabel *drag_preview; - ChartView *drop_target = nullptr; - int auto_scroll_count = 0; - QPoint auto_scroll_pos; - QTimer *auto_scroll_timer; - QTimer *align_timer; - int current_theme = 0; - bool value_tip_visible_ = false; - Connections connections_; - friend class ChartView; - friend class ChartsContainer; -}; - -class ZoomCommand : public UndoCommand { -public: - ZoomCommand(std::pair range) : range(range) { - prev_range = can->timeRange(); - } - void undo() override { can->setTimeRange(prev_range); } - void redo() override { can->setTimeRange(range); } - std::optional> prev_range, range; -}; diff --git a/openpilot/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc deleted file mode 100644 index 90825dc402..0000000000 --- a/openpilot/tools/cabana/chart/signalselector.cc +++ /dev/null @@ -1,107 +0,0 @@ -#include "tools/cabana/chart/signalselector.h" - -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/utils/qtutil.h" - -SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) { - setWindowTitle(title); - QGridLayout *main_layout = new QGridLayout(this); - - // left column - main_layout->addWidget(new QLabel(tr("Available Signals")), 0, 0); - main_layout->addWidget(msgs_combo = new QComboBox(this), 1, 0); - msgs_combo->setEditable(true); - msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg...")); - msgs_combo->setInsertPolicy(QComboBox::NoInsert); - - main_layout->addWidget(available_list = new QListWidget(this), 2, 0); - - // buttons - QVBoxLayout *btn_layout = new QVBoxLayout(); - QPushButton *add_btn = new QPushButton(utils::icon("chevron-right"), "", this); - add_btn->setEnabled(false); - QPushButton *remove_btn = new QPushButton(utils::icon("chevron-left"), "", this); - remove_btn->setEnabled(false); - btn_layout->addStretch(0); - btn_layout->addWidget(add_btn); - btn_layout->addWidget(remove_btn); - btn_layout->addStretch(0); - main_layout->addLayout(btn_layout, 0, 1, 3, 1); - - // right column - main_layout->addWidget(new QLabel(tr("Selected Signals")), 0, 2); - main_layout->addWidget(selected_list = new QListWidget(this), 1, 2, 2, 1); - - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox, 3, 2); - - for (const auto &[id, _] : can->lastMessages()) { - if (auto m = dbc()->msg(id)) { - msgs_combo->addItem(QString("%1 (%2)").arg(QString::fromStdString(m->name)).arg(QString::fromStdString(id.toString())), QVariant::fromValue(id)); - } - } - msgs_combo->model()->sort(0); - msgs_combo->setCurrentIndex(-1); - - QObject::connect(msgs_combo, qOverload(&QComboBox::currentIndexChanged), this, &SignalSelector::updateAvailableList); - QObject::connect(available_list, &QListWidget::currentRowChanged, [=](int row) { add_btn->setEnabled(row != -1); }); - QObject::connect(selected_list, &QListWidget::currentRowChanged, [=](int row) { remove_btn->setEnabled(row != -1); }); - QObject::connect(available_list, &QListWidget::itemDoubleClicked, this, &SignalSelector::add); - QObject::connect(selected_list, &QListWidget::itemDoubleClicked, this, &SignalSelector::remove); - QObject::connect(add_btn, &QPushButton::clicked, [this]() { if (auto item = available_list->currentItem()) add(item); }); - QObject::connect(remove_btn, &QPushButton::clicked, [this]() { if (auto item = selected_list->currentItem()) remove(item); }); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -void SignalSelector::add(QListWidgetItem *item) { - auto it = (ListItem *)item; - addItemToList(selected_list, it->msg_id, it->sig, true); - delete item; -} - -void SignalSelector::remove(QListWidgetItem *item) { - auto it = (ListItem *)item; - if (it->msg_id == msgs_combo->currentData().value()) { - addItemToList(available_list, it->msg_id, it->sig); - } - delete item; -} - -void SignalSelector::updateAvailableList(int index) { - if (index == -1) return; - available_list->clear(); - MessageId msg_id = msgs_combo->itemData(index).value(); - auto selected_items = seletedItems(); - for (auto s : dbc()->msg(msg_id)->getSignals()) { - bool is_selected = std::any_of(selected_items.begin(), selected_items.end(), - [sig = s, &msg_id](auto it) { return it->msg_id == msg_id && it->sig == sig; }); - if (!is_selected) { - addItemToList(available_list, msg_id, s); - } - } -} - -void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { - QString text = QString("■ %1").arg(toQColor(sig->color).name(), QString::fromStdString(sig->name)); - if (show_msg_name) text += QString(" %0 %1").arg(QString::fromStdString(msgName(id)), QString::fromStdString(id.toString())); - - QLabel *label = new QLabel(text); - label->setContentsMargins(5, 0, 5, 0); - auto new_item = new ListItem(id, sig, parent); - new_item->setSizeHint(label->sizeHint()); - parent->setItemWidget(new_item, label); -} - -std::vector SignalSelector::seletedItems() { - std::vector ret; - for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i)); - return ret; -} diff --git a/openpilot/tools/cabana/chart/signalselector.h b/openpilot/tools/cabana/chart/signalselector.h deleted file mode 100644 index 5b6e37e56a..0000000000 --- a/openpilot/tools/cabana/chart/signalselector.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" - -class SignalSelector : public QDialog { -public: - struct ListItem : public QListWidgetItem { - ListItem(const MessageId &msg_id, const cabana::Signal *sig, QListWidget *parent) : msg_id(msg_id), sig(sig), QListWidgetItem(parent) {} - MessageId msg_id; - const cabana::Signal *sig; - }; - - SignalSelector(QString title, QWidget *parent); - std::vector seletedItems(); - inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); } - -private: - void updateAvailableList(int index); - void addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name = false); - void add(QListWidgetItem *item); - void remove(QListWidgetItem *item); - - QComboBox *msgs_combo; - QListWidget *available_list; - QListWidget *selected_list; -}; diff --git a/openpilot/tools/cabana/chart/sparkline.cc b/openpilot/tools/cabana/chart/sparkline.cc deleted file mode 100644 index 587a35956d..0000000000 --- a/openpilot/tools/cabana/chart/sparkline.cc +++ /dev/null @@ -1,101 +0,0 @@ -#include "tools/cabana/chart/sparkline.h" - -#include -#include -#include -#include "tools/cabana/utils/qtutil.h" - -void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size) { - if (first == last || size.isEmpty()) { - pixmap = QPixmap(); - return; - } - - points_.clear(); - min_val = std::numeric_limits::max(); - max_val = std::numeric_limits::lowest(); - points_.reserve(std::distance(first, last)); - - uint64_t start_time = (*first)->mono_time; - double value = 0.0; - for (auto it = first; it != last; ++it) { - if (sig->getValue((*it)->dat, (*it)->size, &value)) { - min_val = std::min(min_val, value); - max_val = std::max(max_val, value); - points_.emplace_back(((*it)->mono_time - start_time) / 1e9, value); - } - } - - if (points_.empty()) { - pixmap = QPixmap(); - return; - } - - freq_ = points_.size() / std::max(points_.back().x() - points_.front().x(), 1.0); - render(toQColor(sig->color), range, size); -} - -void Sparkline::render(const QColor &color, int range, QSize size) { - // Adjust for flat lines - bool is_flat_line = min_val == max_val; - if (is_flat_line) { - min_val -= 1.0; - max_val += 1.0; - } - - // Calculate scaling - const double xscale = (size.width() - 1) / (double)range; - const double yscale = (size.height() - 3) / (max_val - min_val); - bool draw_individual_points = (points_.back().x() * xscale / points_.size()) > 8.0; - - // Transform or downsample points - render_points_.reserve(points_.size()); - render_points_.clear(); - if (draw_individual_points) { - for (const auto &p : points_) { - render_points_.emplace_back(p.x() * xscale, 1.0 + (max_val - p.y()) * yscale); - } - } else if (is_flat_line) { - double y = size.height() / 2.0; - render_points_.emplace_back(0.0, y); - render_points_.emplace_back(points_.back().x() * xscale, y); - } else { - double prev_y = points_.front().y(); - render_points_.emplace_back(points_.front().x() * xscale, 1.0 + (max_val - prev_y) * yscale); - bool in_flat = false; - - for (size_t i = 1; i < points_.size(); ++i) { - const auto &p = points_[i]; - double y = p.y(); - if (std::abs(y - prev_y) < 1e-6) { - in_flat = true; - } else { - if (in_flat) render_points_.emplace_back(points_[i - 1].x() * xscale, 1.0 + (max_val - prev_y) * yscale); - render_points_.emplace_back(p.x() * xscale, 1.0 + (max_val - y) * yscale); - in_flat = false; - } - prev_y = y; - } - if (in_flat) render_points_.emplace_back(points_.back().x() * xscale, 1.0 + (max_val - prev_y) * yscale); - } - - // Render to pixmap - qreal dpr = qApp->devicePixelRatio(); - const QSize pixmap_size = size * dpr; - if (pixmap.size() != pixmap_size) { - pixmap = QPixmap(pixmap_size); - } - pixmap.setDevicePixelRatio(dpr); - pixmap.fill(Qt::transparent); - QPainter painter(&pixmap); - painter.setRenderHint(QPainter::Antialiasing, render_points_.size() <= 500); - painter.setPen(color); - painter.drawPolyline(render_points_.data(), render_points_.size()); - - painter.setPen(QPen(color, 3)); - if (draw_individual_points) { - painter.drawPoints(render_points_.data(), render_points_.size()); - } else { - painter.drawPoint(render_points_.back()); - } -} diff --git a/openpilot/tools/cabana/chart/sparkline.h b/openpilot/tools/cabana/chart/sparkline.h deleted file mode 100644 index 7f30047d0d..0000000000 --- a/openpilot/tools/cabana/chart/sparkline.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "tools/cabana/dbc/dbc.h" -#include "tools/cabana/streams/abstractstream.h" - -class Sparkline { -public: - void update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size); - inline double freq() const { return freq_; } - bool isEmpty() const { return pixmap.isNull(); } - - QPixmap pixmap; - double min_val = 0; - double max_val = 0; - -private: - void render(const QColor &color, int range, QSize size); - - std::vector points_; - std::vector render_points_; - double freq_ = 0; -}; diff --git a/openpilot/tools/cabana/chart/tiplabel.cc b/openpilot/tools/cabana/chart/tiplabel.cc deleted file mode 100644 index c250ebf877..0000000000 --- a/openpilot/tools/cabana/chart/tiplabel.cc +++ /dev/null @@ -1,58 +0,0 @@ -#include "tools/cabana/chart/tiplabel.h" - -#include - -#include -#include -#include - -#include "tools/cabana/settings.h" -#include "tools/cabana/utils/qtutil.h" - -TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { - setAttribute(Qt::WA_ShowWithoutActivating); - setAttribute(Qt::WA_TransparentForMouseEvents); - - setForegroundRole(QPalette::ToolTipText); - setBackgroundRole(QPalette::ToolTipBase); - - QFont font; - font.setPointSizeF(8.34563465); - setFont(font); - auto palette = QToolTip::palette(); - if (!utils::isDarkTheme()) { - palette.setColor(QPalette::ToolTipBase, QApplication::palette().color(QPalette::Base)); - palette.setColor(QPalette::ToolTipText, QRgb(0x404044)); // same color as chart label brush - } - setPalette(palette); - ensurePolished(); - setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this)); - setTextFormat(Qt::RichText); -} - -void TipLabel::showText(const QPoint &pt, const QString &text, QWidget *w, const QRect &rect) { - setText(text); - if (!text.isEmpty()) { - QSize extra(1, 1); - resize(sizeHint() + extra); - QPoint tip_pos(pt.x() + 8, rect.top() + 2); - if (tip_pos.x() + size().width() >= rect.right()) { - tip_pos.rx() = pt.x() - size().width() - 8; - } - if (rect.contains({tip_pos, size()})) { - move(w->mapToGlobal(tip_pos)); - setVisible(true); - return; - } - } - setVisible(false); -} - -void TipLabel::paintEvent(QPaintEvent *ev) { - QStylePainter p(this); - QStyleOptionFrame opt; - opt.init(this); - p.drawPrimitive(QStyle::PE_PanelTipLabel, opt); - p.end(); - QLabel::paintEvent(ev); -} diff --git a/openpilot/tools/cabana/chart/tiplabel.h b/openpilot/tools/cabana/chart/tiplabel.h deleted file mode 100644 index cc96aa9864..0000000000 --- a/openpilot/tools/cabana/chart/tiplabel.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -class TipLabel : public QLabel { - Q_OBJECT - -public: - TipLabel(QWidget *parent = nullptr); - void showText(const QPoint &pt, const QString &sec, QWidget *w, const QRect &rect); - void paintEvent(QPaintEvent *ev) override; -}; diff --git a/openpilot/tools/cabana/detailwidget.cc b/openpilot/tools/cabana/detailwidget.cc deleted file mode 100644 index d0cccb5992..0000000000 --- a/openpilot/tools/cabana/detailwidget.cc +++ /dev/null @@ -1,323 +0,0 @@ -#include "tools/cabana/detailwidget.h" - -#include -#include -#include -#include -#include - -#include "tools/cabana/commands.h" -#include "tools/cabana/mainwin.h" - -// DetailWidget - -DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - - // tabbar - tabbar = new TabBar(this); - tabbar->setUsesScrollButtons(true); - tabbar->setAutoHide(true); - tabbar->setContextMenuPolicy(Qt::CustomContextMenu); - main_layout->addWidget(tabbar); - - createToolBar(); - - // warning - warning_widget = new QWidget(this); - QHBoxLayout *warning_hlayout = new QHBoxLayout(warning_widget); - warning_hlayout->addWidget(warning_icon = new QLabel(this), 0, Qt::AlignTop); - warning_hlayout->addWidget(warning_label = new QLabel(this), 1, Qt::AlignLeft); - warning_widget->hide(); - main_layout->addWidget(warning_widget); - - // msg widget - splitter = new QSplitter(Qt::Vertical, this); - splitter->addWidget(binary_view = new BinaryView(this)); - splitter->addWidget(signal_view = new SignalView(charts, this)); - binary_view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); - signal_view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - splitter->setStretchFactor(0, 0); - splitter->setStretchFactor(1, 1); - - tab_widget = new QTabWidget(this); - tab_widget->setStyleSheet("QTabWidget::pane {border: none; margin-bottom: -2px;}"); - tab_widget->setTabPosition(QTabWidget::South); - tab_widget->addTab(splitter, utils::icon("file-earmark-ruled"), "&Msg"); - tab_widget->addTab(history_log = new LogsWidget(this), utils::icon("stopwatch"), "&Logs"); - main_layout->addWidget(tab_widget); - - QObject::connect(binary_view, &BinaryView::signalHovered, signal_view, &SignalView::signalHovered); - QObject::connect(binary_view, &BinaryView::signalClicked, [this](const cabana::Signal *s) { signal_view->selectSignal(s, true); }); - QObject::connect(binary_view, &BinaryView::editSignal, signal_view->model, &SignalModel::saveSignal); - QObject::connect(binary_view, &BinaryView::showChart, charts, &ChartsWidget::showChart); - QObject::connect(signal_view, &SignalView::showChart, charts, &ChartsWidget::showChart); - QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight); - QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); }); - connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool) { updateState(msgs); })); - connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); - connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); })); - QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu); - QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { - if (index != -1) { - setMessage(tabbar->tabData(index).value()); - } - }); - QObject::connect(tabbar, &QTabBar::tabCloseRequested, tabbar, &QTabBar::removeTab); - QObject::connect(charts, &ChartsWidget::seriesChanged, signal_view, &SignalView::updateChartState); -} - -void DetailWidget::createToolBar() { - QToolBar *toolbar = new QToolBar(this); - int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); - toolbar->setIconSize({icon_size, icon_size}); - toolbar->addWidget(name_label = new ElidedLabel(this)); - name_label->setStyleSheet("QLabel{font-weight:bold;}"); - - QWidget *spacer = new QWidget(); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->addWidget(spacer); - -// Heatmap label and radio buttons - toolbar->addWidget(new QLabel(tr("Heatmap:"), this)); - auto *heatmap_live = new QRadioButton(tr("Live"), this); - auto *heatmap_all = new QRadioButton(tr("All"), this); - heatmap_live->setChecked(true); - - toolbar->addWidget(heatmap_live); - toolbar->addWidget(heatmap_all); - - // Edit and remove buttons - toolbar->addSeparator(); - toolbar->addAction(utils::icon("pencil"), tr("Edit Message"), this, &DetailWidget::editMsg); - action_remove_msg = toolbar->addAction(utils::icon("x-lg"), tr("Remove Message"), this, &DetailWidget::removeMsg); - - layout()->addWidget(toolbar); - - connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); }); - connections_.push_back(can->timeRangeChanged.connect([=](const std::optional> &range) { - auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All"; - heatmap_all->setText(text); - (range ? heatmap_all : heatmap_live)->setChecked(true); - })); -} - -void DetailWidget::showTabBarContextMenu(const QPoint &pt) { - int index = tabbar->tabAt(pt); - if (index >= 0) { - QMenu menu(this); - menu.addAction(tr("Close Other Tabs")); - if (menu.exec(tabbar->mapToGlobal(pt))) { - tabbar->moveTab(index, 0); - tabbar->setCurrentIndex(0); - while (tabbar->count() > 1) { - tabbar->removeTab(1); - } - } - } -} - -int DetailWidget::findOrAddTab(const MessageId& message_id) { - int index = tabbar->count() - 1; - for (/**/; index >= 0; --index) { - if (tabbar->tabData(index).value() == message_id) break; - } - if (index == -1) { - index = tabbar->addTab(QString::fromStdString(message_id.toString())); - tabbar->setTabData(index, QVariant::fromValue(message_id)); - tabbar->setTabToolTip(index, QString::fromStdString(msgName(message_id))); - } - return index; -} - -void DetailWidget::setMessage(const MessageId &message_id) { - if (std::exchange(msg_id, message_id) == message_id) return; - - tabbar->blockSignals(true); - int index = findOrAddTab(message_id); - tabbar->setCurrentIndex(index); - tabbar->blockSignals(false); - - setUpdatesEnabled(false); - signal_view->setMessage(msg_id); - binary_view->setMessage(msg_id); - history_log->setMessage(msg_id); - refresh(); - setUpdatesEnabled(true); -} - -std::pair DetailWidget::serializeMessageIds() const { - QStringList msgs; - for (int i = 0; i < tabbar->count(); ++i) { - MessageId id = tabbar->tabData(i).value(); - msgs.append(QString::fromStdString(id.toString())); - } - return std::make_pair(QString::fromStdString(msg_id.toString()), msgs); -} - -void DetailWidget::restoreTabs(const QString active_msg_id, const QStringList& msg_ids) { - tabbar->blockSignals(true); - for (const auto& str_id : msg_ids) { - MessageId id = MessageId::fromString(str_id.toStdString()); - if (dbc()->msg(id) != nullptr) - findOrAddTab(id); - } - tabbar->blockSignals(false); - - auto active_id = MessageId::fromString(active_msg_id.toStdString()); - if (dbc()->msg(active_id) != nullptr) - setMessage(active_id); -} - -void DetailWidget::refresh() { - QStringList warnings; - auto msg = dbc()->msg(msg_id); - if (msg) { - if (msg_id.source == INVALID_SOURCE) { - warnings.push_back(tr("No messages received.")); - } else if (msg->size != can->lastMessage(msg_id).dat.size()) { - warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size)); - } - for (auto s : binary_view->getOverlappingSignals()) { - warnings.push_back(tr("%1 has overlapping bits.").arg(QString::fromStdString(s->name))); - } - } - QString msg_name = msg ? QString("%1 (%2)").arg(QString::fromStdString(msg->name), QString::fromStdString(msg->transmitter)) : QString::fromStdString(msgName(msg_id)); - name_label->setText(msg_name); - name_label->setToolTip(msg_name); - action_remove_msg->setEnabled(msg != nullptr); - - if (!warnings.isEmpty()) { - warning_label->setText(warnings.join('\n')); - warning_icon->setPixmap(utils::icon(msg ? "exclamation-triangle" : "info-circle")); - } - warning_widget->setVisible(!warnings.isEmpty()); -} - -void DetailWidget::updateState(const std::set *msgs) { - if ((msgs && !msgs->count(msg_id))) - return; - - if (tab_widget->currentIndex() == 0) - binary_view->updateState(); - else - history_log->updateState(); -} - -void DetailWidget::editMsg() { - auto msg = dbc()->msg(msg_id); - int size = msg ? msg->size : can->lastMessage(msg_id).dat.size(); - EditMessageDialog dlg(msg_id, QString::fromStdString(msgName(msg_id)), size, this); - if (dlg.exec()) { - UndoStack::instance()->push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(), - dlg.node->text().trimmed().toStdString(), dlg.comment_edit->toPlainText().trimmed().toStdString())); - } -} - -void DetailWidget::removeMsg() { - UndoStack::instance()->push(new RemoveMsgCommand(msg_id)); -} - -// EditMessageDialog - -EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent) - : original_name(title), msg_id(msg_id), QDialog(parent) { - setWindowTitle(tr("Edit message: %1").arg(QString::fromStdString(msg_id.toString()))); - QFormLayout *form_layout = new QFormLayout(this); - - form_layout->addRow("", error_label = new QLabel); - error_label->setVisible(false); - form_layout->addRow(tr("Name"), name_edit = new QLineEdit(title, this)); - name_edit->setValidator(new NameValidator(name_edit)); - - form_layout->addRow(tr("Size"), size_spin = new QSpinBox(this)); - size_spin->setRange(1, CAN_MAX_DATA_BYTES); - size_spin->setValue(size); - - form_layout->addRow(tr("Node"), node = new QLineEdit(this)); - node->setValidator(new NameValidator(name_edit)); - form_layout->addRow(tr("Comment"), comment_edit = new QTextEdit(this)); - form_layout->addRow(btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel)); - - if (auto msg = dbc()->msg(msg_id)) { - node->setText(QString::fromStdString(msg->transmitter)); - comment_edit->setText(QString::fromStdString(msg->comment)); - } - validateName(name_edit->text()); - setFixedWidth(parent->width() * 0.9); - connect(name_edit, &QLineEdit::textEdited, this, &EditMessageDialog::validateName); - connect(btn_box, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -void EditMessageDialog::validateName(const QString &text) { - bool valid = text.compare(QString::fromStdString(UNTITLED), Qt::CaseInsensitive) != 0; - error_label->setVisible(false); - if (!text.isEmpty() && valid && text != original_name) { - valid = dbc()->msg(msg_id.source, text.toStdString()) == nullptr; - if (!valid) { - error_label->setText(tr("Name already exists")); - error_label->setVisible(true); - } - } - btn_box->button(QDialogButtonBox::Ok)->setEnabled(valid); -} - -// CenterWidget - -CenterWidget::CenterWidget(QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->addWidget(welcome_widget = createWelcomeWidget()); -} - -DetailWidget* CenterWidget::ensureDetailWidget() { - if (!detail_widget) { - delete welcome_widget; - welcome_widget = nullptr; - layout()->addWidget(detail_widget = new DetailWidget(((MainWindow*)parentWidget())->charts_widget, this)); - } - return detail_widget; -} - -void CenterWidget::clear() { - delete detail_widget; - detail_widget = nullptr; - if (!welcome_widget) { - layout()->addWidget(welcome_widget = createWelcomeWidget()); - } -} - -QWidget *CenterWidget::createWelcomeWidget() { - QWidget *w = new QWidget(this); - QVBoxLayout *main_layout = new QVBoxLayout(w); - main_layout->addStretch(0); - QLabel *logo = new QLabel("CABANA"); - logo->setAlignment(Qt::AlignCenter); - logo->setStyleSheet("font-size:50px;font-weight:bold;"); - main_layout->addWidget(logo); - - auto newShortcutRow = [](const QString &title, const QString &key) { - QHBoxLayout *hlayout = new QHBoxLayout(); - auto btn = new QToolButton(); - btn->setText(key); - btn->setEnabled(false); - hlayout->addWidget(new QLabel(title), 0, Qt::AlignRight); - hlayout->addWidget(btn, 0, Qt::AlignLeft); - return hlayout; - }; - - auto lb = new QLabel(tr("<-Select a message to view details")); - lb->setAlignment(Qt::AlignHCenter); - main_layout->addWidget(lb); - main_layout->addLayout(newShortcutRow("Pause", "Space")); - main_layout->addLayout(newShortcutRow("Help", "F1")); - main_layout->addLayout(newShortcutRow("WhatsThis", "Shift+F1")); - main_layout->addStretch(0); - - w->setStyleSheet("QLabel{color:darkGray;}"); - w->setBackgroundRole(QPalette::Base); - w->setAutoFillBackground(true); - return w; -} diff --git a/openpilot/tools/cabana/detailwidget.h b/openpilot/tools/cabana/detailwidget.h deleted file mode 100644 index c003548da4..0000000000 --- a/openpilot/tools/cabana/detailwidget.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/binaryview.h" -#include "tools/cabana/chart/chartswidget.h" -#include "tools/cabana/historylog.h" -#include "tools/cabana/signalview.h" -#include "tools/cabana/utils/elidedlabel.h" -#include "tools/cabana/utils/qtutil.h" - -class EditMessageDialog : public QDialog { -public: - EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent); - void validateName(const QString &text); - - MessageId msg_id; - QString original_name; - QDialogButtonBox *btn_box; - QLineEdit *name_edit; - QLineEdit *node; - QTextEdit *comment_edit; - QLabel *error_label; - QSpinBox *size_spin; -}; - -class DetailWidget : public QWidget { - Q_OBJECT - -public: - DetailWidget(ChartsWidget *charts, QWidget *parent); - void setMessage(const MessageId &message_id); - void refresh(); - std::pair serializeMessageIds() const; - void restoreTabs(const QString active_msg_id, const QStringList &msg_ids); - -private: - void createToolBar(); - int findOrAddTab(const MessageId& message_id); - void showTabBarContextMenu(const QPoint &pt); - void editMsg(); - void removeMsg(); - void updateState(const std::set *msgs = nullptr); - - MessageId msg_id; - QLabel *warning_icon, *warning_label; - ElidedLabel *name_label; - QWidget *warning_widget; - TabBar *tabbar; - QTabWidget *tab_widget; - QAction *action_remove_msg; - LogsWidget *history_log; - BinaryView *binary_view; - SignalView *signal_view; - ChartsWidget *charts; - QSplitter *splitter; - Connections connections_; -}; - -class CenterWidget : public QWidget { - Q_OBJECT -public: - CenterWidget(QWidget *parent); - void setMessage(const MessageId &message_id) { ensureDetailWidget()->setMessage(message_id); } - DetailWidget* getDetailWidget() { return detail_widget; } - DetailWidget* ensureDetailWidget(); - void clear(); - -private: - QWidget *createWelcomeWidget(); - DetailWidget *detail_widget = nullptr; - QWidget *welcome_widget = nullptr; -}; diff --git a/openpilot/tools/cabana/historylog.cc b/openpilot/tools/cabana/historylog.cc deleted file mode 100644 index 26c0f4168f..0000000000 --- a/openpilot/tools/cabana/historylog.cc +++ /dev/null @@ -1,251 +0,0 @@ -#include "tools/cabana/historylog.h" - -#include - -#include -#include -#include - -#include "tools/cabana/commands.h" -#include "tools/cabana/utils/export.h" - -HistoryLogModel::HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) { - connections_.push_back(can->seekedTo.connect([this](double) { reset(); })); - connections_.push_back(dbc()->fileChanged.connect([this]() { reset(); })); - connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { reset(); })); -} - -QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { - const auto &m = messages[index.row()]; - const int col = index.column(); - if (role == Qt::DisplayRole) { - if (col == 0) return QString::number(can->toSeconds(m.mono_time), 'f', 3); - if (!isHexMode()) return QString::fromStdString(sigs[col - 1]->formatValue(m.sig_values[col - 1], false)); - } else if (role == Qt::TextAlignmentRole) { - return (uint32_t)(Qt::AlignRight | Qt::AlignVCenter); - } - - if (isHexMode() && col == 1) { - if (role == ColorsRole) return QVariant::fromValue((void *)(&m.colors)); - if (role == BytesRole) return QVariant::fromValue((void *)(&m.data)); - } - return {}; -} - -void HistoryLogModel::setMessage(const MessageId &message_id) { - msg_id = message_id; - reset(); -} - -void HistoryLogModel::reset() { - beginResetModel(); - sigs.clear(); - if (auto dbc_msg = dbc()->msg(msg_id)) { - sigs = dbc_msg->getSignals(); - } - messages.clear(); - hex_colors = {}; - endResetModel(); - setFilter(0, "", nullptr); -} - -QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation == Qt::Horizontal) { - if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { - if (section == 0) return "Time"; - if (isHexMode()) return "Data"; - - QString name = QString::fromStdString(sigs[section - 1]->name); - QString unit = QString::fromStdString(sigs[section - 1]->unit); - return unit.isEmpty() ? name : QString("%1 (%2)").arg(name, unit); - } else if (role == Qt::BackgroundRole && section > 0 && !isHexMode()) { - // Alpha-blend the signal color with the background to ensure contrast - QColor sigColor = toQColor(sigs[section - 1]->color); - sigColor.setAlpha(128); - return QBrush(sigColor); - } - } - return {}; -} - -void HistoryLogModel::setHexMode(bool hex) { - hex_mode = hex; - reset(); -} - -void HistoryLogModel::setFilter(int sig_idx, const QString &value, std::function cmp) { - filter_sig_idx = sig_idx; - filter_value = value.toDouble(); - filter_cmp = value.isEmpty() ? nullptr : cmp; - updateState(true); -} - -void HistoryLogModel::updateState(bool clear) { - if (clear && !messages.empty()) { - beginRemoveRows({}, 0, messages.size() - 1); - messages.clear(); - endRemoveRows(); - } - uint64_t current_time = can->toMonoTime(can->lastMessage(msg_id).ts) + 1; - fetchData(messages.begin(), current_time, messages.empty() ? 0 : messages.front().mono_time); -} - -bool HistoryLogModel::canFetchMore(const QModelIndex &parent) const { - const auto &events = can->events(msg_id); - return !events.empty() && !messages.empty() && messages.back().mono_time > events.front()->mono_time; -} - -void HistoryLogModel::fetchMore(const QModelIndex &parent) { - if (!messages.empty()) - fetchData(messages.end(), messages.back().mono_time, 0); -} - -void HistoryLogModel::fetchData(std::deque::iterator insert_pos, uint64_t from_time, uint64_t min_time) { - const auto &events = can->events(msg_id); - auto first = std::upper_bound(events.rbegin(), events.rend(), from_time, [](uint64_t ts, auto e) { - return ts > e->mono_time; - }); - - std::vector msgs; - std::vector values(sigs.size()); - msgs.reserve(batch_size); - for (; first != events.rend() && (*first)->mono_time > min_time; ++first) { - const CanEvent *e = *first; - for (int i = 0; i < sigs.size(); ++i) { - sigs[i]->getValue(e->dat, e->size, &values[i]); - } - if (!filter_cmp || filter_cmp(values[filter_sig_idx], filter_value)) { - msgs.emplace_back(Message{e->mono_time, values, {e->dat, e->dat + e->size}}); - if (msgs.size() >= batch_size && min_time == 0) { - break; - } - } - } - - if (!msgs.empty()) { - if (isHexMode() && (min_time > 0 || messages.empty())) { - const auto freq = can->lastMessage(msg_id).freq; - const std::vector no_mask; - for (auto &m : msgs) { - hex_colors.compute(msg_id, m.data.data(), m.data.size(), m.mono_time / (double)1e9, can->getSpeed(), no_mask, freq); - m.colors = hex_colors.colors; - } - } - int pos = std::distance(messages.begin(), insert_pos); - beginInsertRows({}, pos , pos + msgs.size() - 1); - messages.insert(insert_pos, std::move_iterator(msgs.begin()), std::move_iterator(msgs.end())); - endInsertRows(); - } -} - -// HeaderView - -QSize HeaderView::sectionSizeFromContents(int logicalIndex) const { - static const QSize time_col_size = fontMetrics().size(Qt::TextSingleLine, "000000.000") + QSize(10, 6); - if (logicalIndex == 0) { - return time_col_size; - } else { - int default_size = qMax(100, (rect().width() - time_col_size.width()) / (model()->columnCount() - 1)); - QString text = model()->headerData(logicalIndex, this->orientation(), Qt::DisplayRole).toString(); - const QRect rect = fontMetrics().boundingRect({0, 0, default_size, 2000}, defaultAlignment(), text.replace(QChar('_'), ' ')); - QSize size = rect.size() + QSize{10, 6}; - return QSize{qMax(size.width(), default_size), size.height()}; - } -} - -void HeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const { - auto bg_role = model()->headerData(logicalIndex, Qt::Horizontal, Qt::BackgroundRole); - if (bg_role.isValid()) { - painter->fillRect(rect, bg_role.value()); - } - QString text = model()->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString(); - painter->setPen(palette().color(utils::isDarkTheme() ? QPalette::BrightText : QPalette::Text)); - painter->drawText(rect.adjusted(5, 3, -5, -3), defaultAlignment(), text.replace(QChar('_'), ' ')); -} - -// LogsWidget - -LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) { - setFrameStyle(QFrame::StyledPanel | QFrame::Plain); - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->setSpacing(0); - - QWidget *toolbar = new QWidget(this); - toolbar->setAutoFillBackground(true); - QHBoxLayout *h = new QHBoxLayout(toolbar); - - filters_widget = new QWidget(this); - QHBoxLayout *filter_layout = new QHBoxLayout(filters_widget); - filter_layout->setContentsMargins(0, 0, 0, 0); - filter_layout->addWidget(display_type_cb = new QComboBox(this)); - filter_layout->addWidget(signals_cb = new QComboBox(this)); - filter_layout->addWidget(comp_box = new QComboBox(this)); - filter_layout->addWidget(value_edit = new QLineEdit(this)); - h->addWidget(filters_widget); - h->addStretch(0); - export_btn = new ToolButton("filetype-csv", tr("Export to CSV file...")); - h->addWidget(export_btn, 0, Qt::AlignRight); - - display_type_cb->addItems({"Signal", "Hex"}); - display_type_cb->setToolTip(tr("Display signal value or raw hex value")); - comp_box->addItems({">", "=", "!=", "<"}); - value_edit->setClearButtonEnabled(true); - value_edit->setValidator(new DoubleValidator(this)); - - main_layout->addWidget(toolbar); - QFrame *line = new QFrame(this); - line->setFrameStyle(QFrame::HLine | QFrame::Sunken); - main_layout->addWidget(line); - main_layout->addWidget(logs = new QTableView(this)); - logs->setModel(model = new HistoryLogModel(this)); - logs->setItemDelegate(delegate = new MessageBytesDelegate(this)); - logs->setHorizontalHeader(new HeaderView(Qt::Horizontal, this)); - logs->horizontalHeader()->setDefaultAlignment(Qt::AlignRight | (Qt::Alignment)Qt::TextWordWrap); - logs->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - logs->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); - logs->verticalHeader()->setDefaultSectionSize(delegate->sizeForBytes(8).height()); - logs->setFrameShape(QFrame::NoFrame); - - QObject::connect(display_type_cb, qOverload(&QComboBox::activated), model, &HistoryLogModel::setHexMode); - QObject::connect(signals_cb, SIGNAL(activated(int)), this, SLOT(filterChanged())); - QObject::connect(comp_box, SIGNAL(activated(int)), this, SLOT(filterChanged())); - QObject::connect(value_edit, &QLineEdit::textEdited, this, &LogsWidget::filterChanged); - QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV); - QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset); - QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); }); -} - -void LogsWidget::modelReset() { - signals_cb->clear(); - for (auto s : model->sigs) { - signals_cb->addItem(QString::fromStdString(s->name)); - } - export_btn->setEnabled(false); - value_edit->clear(); - comp_box->setCurrentIndex(0); - filters_widget->setVisible(!model->sigs.empty()); -} - -void LogsWidget::filterChanged() { - if (value_edit->text().isEmpty() && !value_edit->isModified()) return; - - std::function cmp = nullptr; - switch (comp_box->currentIndex()) { - case 0: cmp = std::greater{}; break; - case 1: cmp = std::equal_to{}; break; - case 2: cmp = [](double l, double r) { return l != r; }; break; // not equal - case 3: cmp = std::less{}; break; - } - model->setFilter(signals_cb->currentIndex(), value_edit->text(), cmp); -} - -void LogsWidget::exportToCSV() { - QString dir = QString("%1/%2_%3.csv").arg(QString::fromStdString(settings.last_dir)).arg(QString::fromStdString(can->routeName())).arg(QString::fromStdString(msgName(model->msg_id))); - QString fn = QFileDialog::getSaveFileName(this, QString("Export %1 to CSV file").arg(QString::fromStdString(msgName(model->msg_id))), - dir, tr("csv (*.csv)")); - if (!fn.isEmpty()) { - model->isHexMode() ? utils::exportToCSV(fn.toStdString(), model->msg_id) - : utils::exportSignalsToCSV(fn.toStdString(), model->msg_id); - } -} diff --git a/openpilot/tools/cabana/historylog.h b/openpilot/tools/cabana/historylog.h deleted file mode 100644 index 1b75e3b8a9..0000000000 --- a/openpilot/tools/cabana/historylog.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/utils/qtutil.h" - -class HeaderView : public QHeaderView { -public: - HeaderView(Qt::Orientation orientation, QWidget *parent = nullptr) : QHeaderView(orientation, parent) {} - QSize sectionSizeFromContents(int logicalIndex) const override; - void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const; -}; - -class HistoryLogModel : public QAbstractTableModel { - Q_OBJECT - -public: - HistoryLogModel(QObject *parent); - void setMessage(const MessageId &message_id); - void updateState(bool clear = false); - void setFilter(int sig_idx, const QString &value, std::function cmp); - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - void fetchMore(const QModelIndex &parent) override; - bool canFetchMore(const QModelIndex &parent) const override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override { return messages.size(); } - int columnCount(const QModelIndex &parent = QModelIndex()) const override { return !isHexMode() ? sigs.size() + 1 : 2; } - inline bool isHexMode() const { return sigs.empty() || hex_mode; } - void reset(); - void setHexMode(bool hex_mode); - - struct Message { - uint64_t mono_time = 0; - std::vector sig_values; - std::vector data; - std::vector colors; - }; - - void fetchData(std::deque::iterator insert_pos, uint64_t from_time, uint64_t min_time); - - MessageId msg_id; - CanData hex_colors; - const int batch_size = 50; - int filter_sig_idx = -1; - double filter_value = 0; - std::function filter_cmp = nullptr; - std::deque messages; - std::vector sigs; - bool hex_mode = false; - Connections connections_; -}; - -class LogsWidget : public QFrame { - Q_OBJECT - -public: - LogsWidget(QWidget *parent); - void setMessage(const MessageId &message_id) { model->setMessage(message_id); } - void updateState() { model->updateState(); } - void showEvent(QShowEvent *event) override { model->updateState(true); } - -private slots: - void filterChanged(); - void exportToCSV(); - void modelReset(); - -private: - QTableView *logs; - HistoryLogModel *model; - QComboBox *signals_cb, *comp_box, *display_type_cb; - QLineEdit *value_edit; - QWidget *filters_widget; - ToolButton *export_btn; - MessageBytesDelegate *delegate; -}; diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc deleted file mode 100644 index c7a838a756..0000000000 --- a/openpilot/tools/cabana/mainwin.cc +++ /dev/null @@ -1,752 +0,0 @@ -#include "tools/cabana/mainwin.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "json11/json11.hpp" -#include "tools/cabana/commands.h" -#include "tools/cabana/settingsdialog.h" -#include "tools/cabana/streamselector.h" -#include "tools/cabana/tools/findsignal.h" -#include "tools/cabana/utils/export.h" -#include "tools/cabana/utils/qtutil.h" -#include "tools/replay/py_downloader.h" -#include "tools/replay/util.h" - -MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainWindow() { - loadFingerprints(); - createDockWindows(); - setCentralWidget(center_widget = new CenterWidget(this)); - createActions(); - createStatusBar(); - createShortcuts(); - - // save default window state to allow resetting it - default_state = utils::toBytes(saveState()); - - // restore states; restoreGeometry() itself corrects stale off-screen geometry - restoreGeometry(utils::qbytes(settings.geometry)); - restoreState(utils::qbytes(settings.window_state)); - - // download handlers are called from download threads - static auto static_main_win = this; - installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) { - utils::runOnMainThread([=]() { static_main_win->updateDownloadProgress(cur, total, success); }); - }); - installMessageHandler([](ReplyMsgType type, const std::string msg) { - utils::runOnMainThread([=]() { static_main_win->statusBar()->showMessage(QString::fromStdString(msg), 2000); }); - }); - - setStyleSheet(QString(R"(QMainWindow::separator { - width: %1px; /* when vertical */ - height: %1px; /* when horizontal */ - })").arg(style()->pixelMetric(QStyle::PM_SplitterWidth))); - - connections_.push_back(dbc()->fileChanged.connect([this]() { DBCFileChanged(); })); - connections_.push_back(UndoStack::instance()->cleanChanged.connect([this](bool clean) { undoStackCleanChanged(clean); })); - connections_.push_back(settings.changed.connect([this]() { updateStatus(); })); - - // temporary pump for the non-Qt main thread queue until imgui owns the loop - auto *queue_timer = new QTimer(this); - QObject::connect(queue_timer, &QTimer::timeout, utils::drainMainThreadQueue); - queue_timer->start(10); - - QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); }); - show(); -} - -void MainWindow::loadFingerprints() { - std::ifstream json_file((QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json").toStdString()); - if (!json_file) return; - const std::string contents{std::istreambuf_iterator(json_file), std::istreambuf_iterator()}; - std::string err; - auto doc = json11::Json::parse(contents, err); - if (!err.empty() || !doc.is_object()) return; - fingerprint_to_dbc.clear(); - for (const auto &kv : doc.object_items()) { - if (kv.second.is_string()) { - fingerprint_to_dbc.emplace(kv.first, kv.second.string_value()); - } - } -} - -void MainWindow::createActions() { - // File menu - QMenu *file_menu = menuBar()->addMenu(tr("&File")); - file_menu->addAction(tr("Open Stream..."), this, &MainWindow::selectAndOpenStream); - close_stream_act = file_menu->addAction(tr("Close stream"), this, &MainWindow::closeStream); - export_to_csv_act = file_menu->addAction(tr("Export to CSV..."), this, &MainWindow::exportToCSV); - close_stream_act->setEnabled(false); - export_to_csv_act->setEnabled(false); - file_menu->addSeparator(); - - file_menu->addAction(tr("New DBC File"), [this]() { newFile(); }, QKeySequence::New); - file_menu->addAction(tr("Open DBC File..."), [this]() { openFile(); }, QKeySequence::Open); - - manage_dbcs_menu = file_menu->addMenu(tr("Manage &DBC Files")); - QObject::connect(manage_dbcs_menu, &QMenu::aboutToShow, this, &MainWindow::updateLoadSaveMenus); - - open_recent_menu = file_menu->addMenu(tr("Open &Recent")); - QObject::connect(open_recent_menu, &QMenu::aboutToShow, this, &MainWindow::updateRecentFileMenu); - - file_menu->addSeparator(); - QMenu *load_opendbc_menu = file_menu->addMenu(tr("Load DBC from commaai/opendbc")); - // load_opendbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }"); - std::vector dbc_names; - std::error_code ec; - for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH, ec)) { - if (entry.is_regular_file() && entry.path().extension() == ".dbc") { - dbc_names.push_back(entry.path().filename().string()); - } - } - std::sort(dbc_names.begin(), dbc_names.end()); - for (const auto &dbc_name : dbc_names) { - QString name = QString::fromStdString(dbc_name); - load_opendbc_menu->addAction(name, [this, name]() { loadDBCFromOpendbc(name); }); - } - - file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); }); - - file_menu->addSeparator(); - save_dbc = file_menu->addAction(tr("Save DBC..."), this, &MainWindow::save, QKeySequence::Save); - save_dbc_as = file_menu->addAction(tr("Save DBC As..."), this, &MainWindow::saveAs, QKeySequence::SaveAs); - copy_dbc_to_clipboard = file_menu->addAction(tr("Copy DBC To Clipboard"), this, &MainWindow::saveToClipboard); - - file_menu->addSeparator(); - file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption, QKeySequence::Preferences); - - file_menu->addSeparator(); - file_menu->addAction(tr("E&xit"), qApp, &QApplication::closeAllWindows, QKeySequence::Quit); - - // Edit Menu - QMenu *edit_menu = menuBar()->addMenu(tr("&Edit")); - undo_act = edit_menu->addAction(tr("&Undo"), []() { UndoStack::instance()->undo(); }); - undo_act->setShortcuts(QKeySequence::Undo); - redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); }); - redo_act->setShortcuts(QKeySequence::Redo); - connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { updateUndoRedoActions(); })); - updateUndoRedoActions(); - - // View Menu - QMenu *view_menu = menuBar()->addMenu(tr("&View")); - auto act = view_menu->addAction(tr("Full Screen"), this, &MainWindow::toggleFullScreen, QKeySequence::FullScreen); - addAction(act); - view_menu->addSeparator(); - view_menu->addAction(messages_dock->toggleViewAction()); - view_menu->addAction(video_dock->toggleViewAction()); - view_menu->addSeparator(); - view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(utils::qbytes(default_state)); }); - - // Tools Menu - tools_menu = menuBar()->addMenu(tr("&Tools")); - tools_menu->addAction(tr("Find &Similar Bits"), this, &MainWindow::findSimilarBits); - tools_menu->addAction(tr("&Find Signal"), this, &MainWindow::findSignal); - - // Help Menu - QMenu *help_menu = menuBar()->addMenu(tr("&Help")); - help_menu->addAction(tr("Help"), this, &MainWindow::onlineHelp, QKeySequence::HelpContents); - help_menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt); -} - -void MainWindow::createDockWindows() { - messages_dock = new QDockWidget(tr("MESSAGES"), this); - messages_dock->setObjectName("MessagesPanel"); - messages_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea); - messages_dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); - addDockWidget(Qt::LeftDockWidgetArea, messages_dock); - - video_dock = new QDockWidget("", this); - video_dock->setObjectName(tr("VideoPanel")); - video_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); - video_dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); - addDockWidget(Qt::RightDockWidgetArea, video_dock); -} - -void MainWindow::createDockWidgets() { - messages_widget = new MessagesWidget(this); - messages_dock->setWidget(messages_widget); - QObject::connect(messages_widget, &MessagesWidget::titleChanged, messages_dock, &QDockWidget::setWindowTitle); - QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, center_widget, &CenterWidget::setMessage); - - // right panel - charts_widget = new ChartsWidget(this); - QWidget *charts_container = new QWidget(this); - charts_layout = new QVBoxLayout(charts_container); - charts_layout->setContentsMargins(0, 0, 0, 0); - charts_layout->addWidget(charts_widget); - - // splitter between video and charts - video_splitter = new QSplitter(Qt::Vertical, this); - video_widget = new VideoWidget(this); - video_splitter->addWidget(video_widget); - - video_splitter->addWidget(charts_container); - video_splitter->setStretchFactor(1, 1); - video_splitter->restoreState(utils::qbytes(settings.video_splitter_state)); - video_splitter->handle(1)->setEnabled(!can->liveStreaming()); - video_dock->setWidget(video_splitter); - QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking); - QObject::connect(charts_widget, &ChartsWidget::showTip, video_widget, &VideoWidget::showThumbnail); -} - -void MainWindow::createStatusBar() { - progress_bar = new QProgressBar(); - progress_bar->setRange(0, 100); - progress_bar->setTextVisible(true); - progress_bar->setFixedSize({300, 16}); - progress_bar->setVisible(false); - statusBar()->addWidget(new QLabel(tr("For Help, Press F1"))); - statusBar()->addPermanentWidget(progress_bar); - statusBar()->addPermanentWidget(status_label = new QLabel(this)); - updateStatus(); -} - -void MainWindow::createShortcuts() { - auto shortcut = new QShortcut(QKeySequence(Qt::Key_Space), this, nullptr, nullptr, Qt::ApplicationShortcut); - QObject::connect(shortcut, &QShortcut::activated, this, []() { - if (can) can->pause(!can->isPaused()); - }); - // TODO: add more shortcuts here. -} - -void MainWindow::undoStackCleanChanged(bool clean) { - setWindowModified(!clean); -} - -void MainWindow::updateUndoRedoActions() { - auto stack = UndoStack::instance(); - undo_act->setEnabled(stack->canUndo()); - undo_act->setText(stack->canUndo() ? tr("&Undo %1").arg(QString::fromStdString(stack->undoText())) : tr("&Undo")); - redo_act->setEnabled(stack->canRedo()); - redo_act->setText(stack->canRedo() ? tr("&Redo %1").arg(QString::fromStdString(stack->redoText())) : tr("&Redo")); -} - -void MainWindow::DBCFileChanged() { - UndoStack::instance()->clear(); - - // Update file menu - int cnt = dbc()->nonEmptyDBCCount(); - save_dbc->setText(cnt > 1 ? tr("Save %1 DBCs...").arg(cnt) : tr("Save DBC...")); - save_dbc->setEnabled(cnt > 0); - save_dbc_as->setEnabled(cnt == 1); - // TODO: Support clipboard for multiple files - copy_dbc_to_clipboard->setEnabled(cnt == 1); - manage_dbcs_menu->setEnabled(dynamic_cast(can) == nullptr); - - QStringList title; - for (auto f : dbc()->allDBCFiles()) { - title.push_back(tr("(%1) %2").arg(QString::fromStdString(toString(dbc()->sources(f))), QString::fromStdString(f->name()))); - } - setWindowFilePath(title.join(" | ")); - - QTimer::singleShot(0, this, &::MainWindow::restoreSessionState); -} - -void MainWindow::selectAndOpenStream() { - StreamSelector dlg(this); - if (dlg.exec()) { - openStream(dlg.stream(), dlg.dbcFile()); - } else if (!can) { - openStream(new DummyStream()); - } -} - -void MainWindow::closeStream() { - openStream(new DummyStream()); - if (dbc()->nonEmptyDBCCount() > 0) { - dbc()->fileChanged(); - } - statusBar()->showMessage(tr("stream closed")); -} - -void MainWindow::exportToCSV() { - QString dir = QString("%1/%2.csv").arg(QString::fromStdString(settings.last_dir)).arg(QString::fromStdString(can->routeName())); - QString fn = QFileDialog::getSaveFileName(this, "Export stream to CSV file", dir, tr("csv (*.csv)")); - if (!fn.isEmpty()) { - utils::exportToCSV(fn.toStdString()); - } -} - -void MainWindow::newFile(SourceSet s) { - closeFile(s); - dbc()->open(s, std::string(""), std::string("")); -} - -void MainWindow::openFile(SourceSet s) { - remindSaveChanges(); - QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)"); - if (!fn.isEmpty()) { - loadFile(fn, s); - } -} - -void MainWindow::loadFile(const QString &fn, SourceSet s) { - if (!fn.isEmpty()) { - closeFile(s); - - std::string error; - if (dbc()->open(s, fn.toStdString(), &error)) { - updateRecentFiles(fn); - statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000); - } else { - QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC file"), tr("Failed to parse DBC file %1").arg(fn)); - msg_box.setDetailedText(QString::fromStdString(error)); - msg_box.exec(); - } - } -} - -void MainWindow::loadDBCFromOpendbc(const QString &name) { - loadFile(QString("%1/%2").arg(OPENDBC_FILE_PATH, name)); -} - -void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { - std::string text; - if (!utils::getClipboardText(&text)) { - QMessageBox::warning(this, tr("Load From Clipboard"), tr("No clipboard tool found. Install xclip (X11) or wl-clipboard (Wayland).")); - return; - } - if (text.empty()) { - QMessageBox::warning(this, tr("Load From Clipboard"), tr("Clipboard is empty.")); - return; - } - - closeFile(s); - - std::string error; - bool ret = dbc()->open(s, std::string(""), text, &error); - if (ret && dbc()->nonEmptyDBCCount() > 0) { - QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!")); - } else { - QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC from clipboard"), tr("Make sure that you paste the text with correct format.")); - msg_box.setDetailedText(QString::fromStdString(error)); - msg_box.exec(); - } -} - -// stream threads read the global `can` until its destructor joins them -MainWindow::~MainWindow() { - delete can; - can = nullptr; -} - -void MainWindow::openStream(AbstractStream *stream, const QString &dbc_file) { - stream_connections_.clear(); - if (wait_dlg_) wait_dlg_->deleteLater(); - wait_dlg_ = nullptr; - delete can; - can = nullptr; - startStream(stream, dbc_file); -} - -void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { - center_widget->clear(); - delete messages_widget; - delete video_splitter; - - can = stream; // take ownership - stream_connections_.push_back(can->error.connect([this](const std::string &msg) { - QMessageBox::warning(this, tr("Error"), QString::fromStdString(msg)); - })); - can->start(); - - loadFile(dbc_file); - statusBar()->showMessage(tr("Stream [%1] started").arg(QString::fromStdString(can->routeName())), 2000); - - bool has_stream = dynamic_cast(can) == nullptr; - close_stream_act->setEnabled(has_stream); - export_to_csv_act->setEnabled(has_stream); - tools_menu->setEnabled(has_stream); - createDockWidgets(); - - video_dock->setWindowTitle(QString::fromStdString(can->routeName())); - if (can->liveStreaming() || video_splitter->sizes()[0] == 0) { - // display video at minimum size. - video_splitter->setSizes({1, 1}); - } - // Don't overwrite already loaded DBC - if (!dbc()->nonEmptyDBCCount()) { - newFile(); - } - - stream_connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { eventsMerged(); })); - - if (has_stream) { - wait_dlg_ = new QProgressDialog( - can->liveStreaming() ? tr("Waiting for the live stream to start...") : tr("Loading segment data..."), - tr("&Abort"), 0, 100, this); - wait_dlg_->setWindowModality(Qt::WindowModal); - wait_dlg_->setFixedSize(400, wait_dlg_->sizeHint().height()); - QObject::connect(wait_dlg_, &QProgressDialog::canceled, this, &MainWindow::close); - wait_dlg_connection_ = can->eventsMerged.connect([this](const MessageEventsMap &) { - wait_dlg_->deleteLater(); - wait_dlg_ = nullptr; - wait_dlg_connection_.disconnect(); - }); - } -} - -void MainWindow::eventsMerged() { - if (!can->liveStreaming() && std::exchange(car_fingerprint, QString::fromStdString(can->carFingerprint())) != car_fingerprint) { - video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPRINT: %2") - .arg(QString::fromStdString(can->routeName())) - .arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint)); - // Don't overwrite already loaded DBC - auto it = fingerprint_to_dbc.find(car_fingerprint.toStdString()); - if (!dbc()->nonEmptyDBCCount() && it != fingerprint_to_dbc.end()) { - QTimer::singleShot(0, this, [this, dbc_name = QString::fromStdString(it->second)]() { - loadDBCFromOpendbc(dbc_name + ".dbc"); - }); - } - } -} - -void MainWindow::save() { - // Save all open DBC files - for (auto dbc_file : dbc()->allDBCFiles()) { - if (dbc_file->isEmpty()) continue; - saveFile(dbc_file); - } -} - -void MainWindow::saveAs() { - // Save as all open DBC files. Should not be called with more than 1 file open - for (auto dbc_file : dbc()->allDBCFiles()) { - if (dbc_file->isEmpty()) continue; - saveFileAs(dbc_file); - } -} - -void MainWindow::closeFile(SourceSet s) { - remindSaveChanges(); - if (s == SOURCE_ALL) { - dbc()->closeAll(); - } else { - dbc()->close(s); - } -} - -void MainWindow::closeFile(DBCFile *dbc_file) { - assert(dbc_file != nullptr); - remindSaveChanges(); - dbc()->close(dbc_file); - // Ensure we always have at least one file open - if (dbc()->dbcCount() == 0) { - newFile(); - } -} - -void MainWindow::saveFile(DBCFile *dbc_file) { - assert(dbc_file != nullptr); - if (!dbc_file->filename.empty()) { - dbc_file->save(); - UndoStack::instance()->setClean(); - statusBar()->showMessage(tr("File saved"), 2000); - } else if (!dbc_file->isEmpty()) { - saveFileAs(dbc_file); - } -} - -void MainWindow::saveFileAs(DBCFile *dbc_file) { - QString title = tr("Save File (bus: %1)").arg(QString::fromStdString(toString(dbc()->sources(dbc_file)))); - QString fn = QFileDialog::getSaveFileName(this, title, QString::fromStdString((std::filesystem::path(settings.last_dir) / "untitled.dbc").string()), tr("DBC (*.dbc)")); - if (!fn.isEmpty()) { - dbc_file->saveAs(fn.toStdString()); - UndoStack::instance()->setClean(); - statusBar()->showMessage(tr("File saved as %1").arg(fn), 2000); - updateRecentFiles(fn); - } -} - -void MainWindow::saveToClipboard() { - // Copy all open DBC files to clipboard. Should not be called with more than 1 file open - for (auto dbc_file : dbc()->allDBCFiles()) { - if (dbc_file->isEmpty()) continue; - saveFileToClipboard(dbc_file); - } -} - -void MainWindow::saveFileToClipboard(DBCFile *dbc_file) { - assert(dbc_file != nullptr); - if (utils::setClipboardText(dbc_file->generateDBC())) { - QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!")); - } else { - QMessageBox::warning(this, tr("Copy To Clipboard"), tr("Failed to copy DBC to clipboard. Install xclip (X11) or wl-clipboard (Wayland).")); - } -} - -void MainWindow::updateLoadSaveMenus() { - manage_dbcs_menu->clear(); - - for (int source : can->sources) { - if (source >= 64) continue; // Sent and blocked buses are handled implicitly - - SourceSet ss = {source, uint8_t(source + 128), uint8_t(source + 192)}; - - QMenu *bus_menu = new QMenu(this); - bus_menu->addAction(tr("New DBC File..."), [=]() { newFile(ss); }); - bus_menu->addAction(tr("Open DBC File..."), [=]() { openFile(ss); }); - bus_menu->addAction(tr("Load DBC From Clipboard..."), [=]() { loadFromClipboard(ss, false); }); - - // Show sub-menu for each dbc for this source. - auto dbc_file = dbc()->findDBCFile(source); - if (dbc_file) { - bus_menu->addSeparator(); - bus_menu->addAction(QString::fromStdString(dbc_file->name()) + " (" + QString::fromStdString(toString(dbc()->sources(dbc_file))) + ")")->setEnabled(false); - bus_menu->addAction(tr("Save..."), [=]() { saveFile(dbc_file); }); - bus_menu->addAction(tr("Save As..."), [=]() { saveFileAs(dbc_file); }); - bus_menu->addAction(tr("Copy to Clipboard..."), [=]() { saveFileToClipboard(dbc_file); }); - bus_menu->addAction(tr("Remove from this bus..."), [=]() { closeFile(ss); }); - bus_menu->addAction(tr("Remove from all buses..."), [=]() { closeFile(dbc_file); }); - } - bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(dbc_file ? QString::fromStdString(dbc_file->name()) : "No DBCs loaded")); - - manage_dbcs_menu->addMenu(bus_menu); - } -} - -void MainWindow::updateRecentFiles(const QString &fn) { - const std::string filename = fn.toStdString(); - settings.recent_files.erase(std::remove(settings.recent_files.begin(), settings.recent_files.end(), filename), settings.recent_files.end()); - settings.recent_files.insert(settings.recent_files.begin(), filename); - while (settings.recent_files.size() > MAX_RECENT_FILES) { - settings.recent_files.pop_back(); - } - settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string(); -} - -void MainWindow::updateRecentFileMenu() { - open_recent_menu->clear(); - - int num_recent_files = std::min(settings.recent_files.size(), MAX_RECENT_FILES); - if (!num_recent_files) { - open_recent_menu->addAction(tr("No Recent Files"))->setEnabled(false); - return; - } - - for (int i = 0; i < num_recent_files; ++i) { - QString text = tr("&%1 %2").arg(i + 1).arg(QString::fromStdString(std::filesystem::path(settings.recent_files[i]).filename().string())); - open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(QString::fromStdString(file)); }); - } -} - -void MainWindow::remindSaveChanges() { - while (!UndoStack::instance()->isClean()) { - QString text = tr("You have unsaved changes. Press ok to save them, cancel to discard."); - int ret = QMessageBox::question(this, tr("Unsaved Changes"), text, QMessageBox::Ok | QMessageBox::Cancel); - if (ret != QMessageBox::Ok) break; - save(); - } - UndoStack::instance()->clear(); -} - -void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) { - if (wait_dlg_) wait_dlg_->setValue((int)((cur / (double)total) * 100)); - if (success && cur < total) { - progress_bar->setValue((cur / (double)total) * 100); - progress_bar->setFormat(tr("Downloading %p% (%1)").arg(formattedDataSize(total).c_str())); - progress_bar->show(); - } else { - progress_bar->hide(); - } -} - -void MainWindow::updateStatus() { - status_label->setText(tr("Cached Minutes:%1").arg(settings.max_cached_minutes)); -} - -bool MainWindow::eventFilter(QObject *obj, QEvent *event) { - if (obj == floating_window && event->type() == QEvent::Close) { - toggleChartsDocking(); - return true; - } - return QMainWindow::eventFilter(obj, event); -} - -void MainWindow::toggleChartsDocking() { - if (floating_window) { - // Dock the charts widget back to the main window - floating_window->removeEventFilter(this); - charts_layout->insertWidget(0, charts_widget, 1); - floating_window->deleteLater(); - floating_window = nullptr; - charts_widget->setIsDocked(true); - } else { - // Float the charts widget in a separate window - floating_window = new QWidget(this, Qt::Window); - floating_window->setWindowTitle("Charts"); - floating_window->setLayout(new QVBoxLayout()); - floating_window->layout()->addWidget(charts_widget); - floating_window->installEventFilter(this); - floating_window->showMaximized(); - charts_widget->setIsDocked(false); - } -} - -void MainWindow::closeEvent(QCloseEvent *event) { - remindSaveChanges(); - - installDownloadProgressHandler(nullptr); - installMessageHandler(nullptr); - - if (floating_window) - floating_window->deleteLater(); - - // save states - settings.geometry = utils::toBytes(saveGeometry()); - settings.window_state = utils::toBytes(saveState()); - if (can && !can->liveStreaming()) { - settings.video_splitter_state = utils::toBytes(video_splitter->saveState()); - } - if (messages_widget) { - settings.message_header_state = messages_widget->saveHeaderState(); - } - - saveSessionState(); - settings.save(); - QWidget::closeEvent(event); -} - -void MainWindow::setOption() { - SettingsDialog dlg(this); - dlg.exec(); -} - -void MainWindow::findSimilarBits() { - FindSimilarBitsDlg *dlg = new FindSimilarBitsDlg(this); - QObject::connect(dlg, &FindSimilarBitsDlg::openMessage, messages_widget, &MessagesWidget::selectMessage); - dlg->show(); -} - -void MainWindow::findSignal() { - FindSignalDlg *dlg = new FindSignalDlg(this); - QObject::connect(dlg, &FindSignalDlg::openMessage, messages_widget, &MessagesWidget::selectMessage); - dlg->show(); -} - -void MainWindow::onlineHelp() { - if (auto help = findChild()) { - help->close(); - } else { - help = new HelpOverlay(this); - help->setGeometry(rect()); - help->show(); - help->raise(); - } -} - -void MainWindow::toggleFullScreen() { - if (isFullScreen()) { - menuBar()->show(); - statusBar()->show(); - showNormal(); - showMaximized(); - } else { - menuBar()->hide(); - statusBar()->hide(); - showFullScreen(); - } -} - -void MainWindow::saveSessionState() { - settings.recent_dbc_file = ""; - settings.active_msg_id = ""; - settings.selected_msg_ids.clear(); - settings.active_charts.clear(); - - for (auto &f : dbc()->allDBCFiles()) - if (!f->isEmpty()) { settings.recent_dbc_file = f->filename; break; } - - if (auto *detail = center_widget->getDetailWidget()) { - auto [active_id, ids] = detail->serializeMessageIds(); - settings.active_msg_id = active_id.toStdString(); - settings.selected_msg_ids.clear(); - for (const auto &id : ids) settings.selected_msg_ids.push_back(id.toStdString()); - } - if (charts_widget) { - settings.active_charts.clear(); - for (const auto &id : charts_widget->serializeChartIds()) settings.active_charts.push_back(id.toStdString()); - } -} - -void MainWindow::restoreSessionState() { - if (settings.recent_dbc_file.empty() || dbc()->nonEmptyDBCCount() == 0) return; - - QString dbc_file; - for (auto& f : dbc()->allDBCFiles()) - if (!f->isEmpty()) { dbc_file = QString::fromStdString(f->filename); break; } - if (dbc_file.toStdString() != settings.recent_dbc_file) return; - - if (!settings.selected_msg_ids.empty()) { - QStringList ids; - for (const auto &id : settings.selected_msg_ids) ids.push_back(QString::fromStdString(id)); - center_widget->ensureDetailWidget()->restoreTabs(QString::fromStdString(settings.active_msg_id), ids); - } - - if (charts_widget != nullptr && !settings.active_charts.empty()) { - QStringList ids; - for (const auto &id : settings.active_charts) ids.push_back(QString::fromStdString(id)); - charts_widget->restoreChartsFromIds(ids); - } -} - -// HelpOverlay -HelpOverlay::HelpOverlay(MainWindow *parent) : QWidget(parent) { - setAttribute(Qt::WA_NoSystemBackground, true); - setAttribute(Qt::WA_TranslucentBackground, true); - setAttribute(Qt::WA_DeleteOnClose); - parent->installEventFilter(this); -} - -void HelpOverlay::paintEvent(QPaintEvent *event) { - QPainter painter(this); - painter.fillRect(rect(), QColor(0, 0, 0, 50)); - auto parent = parentWidget(); - drawHelpForWidget(painter, parent->findChild()); - drawHelpForWidget(painter, parent->findChild()); - drawHelpForWidget(painter, parent->findChild()); - drawHelpForWidget(painter, parent->findChild()); - drawHelpForWidget(painter, parent->findChild()); -} - -void HelpOverlay::drawHelpForWidget(QPainter &painter, QWidget *w) { - if (w && w->isVisible() && !w->whatsThis().isEmpty()) { - QPoint pt = mapFromGlobal(w->mapToGlobal(w->rect().center())); - if (rect().contains(pt)) { - QTextDocument document; - document.setHtml(w->whatsThis()); - QSize doc_size = document.size().toSize(); - QPoint topleft = {pt.x() - doc_size.width() / 2, pt.y() - doc_size.height() / 2}; - painter.translate(topleft); - painter.fillRect(QRect{{0, 0}, doc_size}, palette().toolTipBase()); - document.drawContents(&painter); - painter.translate(-topleft); - } - } -} - -bool HelpOverlay::eventFilter(QObject *obj, QEvent *event) { - if (obj == parentWidget() && event->type() == QEvent::Resize) { - QResizeEvent *resize_event = (QResizeEvent *)(event); - setGeometry(QRect{QPoint(0, 0), resize_event->size()}); - } - return false; -} - -void HelpOverlay::mouseReleaseEvent(QMouseEvent *event) { - close(); -} diff --git a/openpilot/tools/cabana/mainwin.h b/openpilot/tools/cabana/mainwin.h deleted file mode 100644 index a57232ceb2..0000000000 --- a/openpilot/tools/cabana/mainwin.h +++ /dev/null @@ -1,122 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/chart/chartswidget.h" -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/detailwidget.h" -#include "tools/cabana/messageswidget.h" -#include "tools/cabana/videowidget.h" -#include "tools/cabana/tools/findsimilarbits.h" - -class QProgressDialog; - -class MainWindow : public QMainWindow { - Q_OBJECT - -public: - MainWindow(AbstractStream *stream, const QString &dbc_file); - ~MainWindow(); - void toggleChartsDocking(); - void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); } - void loadFile(const QString &fn, SourceSet s = SOURCE_ALL); - ChartsWidget *charts_widget = nullptr; - -public slots: - void selectAndOpenStream(); - void openStream(AbstractStream *stream, const QString &dbc_file = {}); - void closeStream(); - void exportToCSV(); - - void newFile(SourceSet s = SOURCE_ALL); - void openFile(SourceSet s = SOURCE_ALL); - void loadDBCFromOpendbc(const QString &name); - void save(); - void saveAs(); - void saveToClipboard(); - -protected: - void startStream(AbstractStream *stream, QString dbc_file); - bool eventFilter(QObject *obj, QEvent *event) override; - void remindSaveChanges(); - void closeFile(SourceSet s = SOURCE_ALL); - void closeFile(DBCFile *dbc_file); - void saveFile(DBCFile *dbc_file); - void saveFileAs(DBCFile *dbc_file); - void saveFileToClipboard(DBCFile *dbc_file); - void loadFingerprints(); - void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true); - void updateRecentFiles(const QString &fn); - void updateRecentFileMenu(); - void createActions(); - void createDockWindows(); - void createStatusBar(); - void createShortcuts(); - void closeEvent(QCloseEvent *event) override; - void DBCFileChanged(); - void updateDownloadProgress(uint64_t cur, uint64_t total, bool success); - void setOption(); - void findSimilarBits(); - void findSignal(); - void undoStackCleanChanged(bool clean); - void updateUndoRedoActions(); - void onlineHelp(); - void toggleFullScreen(); - void updateStatus(); - void updateLoadSaveMenus(); - void createDockWidgets(); - void eventsMerged(); - void saveSessionState(); - void restoreSessionState(); - - VideoWidget *video_widget = nullptr; - QDockWidget *video_dock; - QDockWidget *messages_dock; - MessagesWidget *messages_widget = nullptr; - CenterWidget *center_widget; - QWidget *floating_window = nullptr; - QVBoxLayout *charts_layout; - QProgressBar *progress_bar; - QLabel *status_label; - std::unordered_map fingerprint_to_dbc; - QSplitter *video_splitter = nullptr; - enum { MAX_RECENT_FILES = 15 }; - QMenu *open_recent_menu = nullptr; - QMenu *manage_dbcs_menu = nullptr; - QMenu *tools_menu = nullptr; - QAction *close_stream_act = nullptr; - QAction *export_to_csv_act = nullptr; - QAction *save_dbc = nullptr; - QAction *save_dbc_as = nullptr; - QAction *copy_dbc_to_clipboard = nullptr; - QAction *undo_act = nullptr; - QAction *redo_act = nullptr; - QString car_fingerprint; - std::vector default_state; - Connections connections_; - Connections stream_connections_; - Connection wait_dlg_connection_; - QProgressDialog *wait_dlg_ = nullptr; -}; - -class HelpOverlay : public QWidget { - Q_OBJECT -public: - HelpOverlay(MainWindow *parent); - -protected: - void drawHelpForWidget(QPainter &painter, QWidget *w); - void paintEvent(QPaintEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - bool eventFilter(QObject *obj, QEvent *event) override; -}; diff --git a/openpilot/tools/cabana/messageswidget.cc b/openpilot/tools/cabana/messageswidget.cc deleted file mode 100644 index e2d0eba079..0000000000 --- a/openpilot/tools/cabana/messageswidget.cc +++ /dev/null @@ -1,467 +0,0 @@ -#include "tools/cabana/messageswidget.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/commands.h" - -MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - // toolbar - main_layout->addWidget(createToolBar()); - // message table - main_layout->addWidget(view = new MessageView(this)); - view->setItemDelegate(delegate = new MessageBytesDelegate(view, settings.multiple_lines_hex)); - view->setModel(model = new MessageListModel(this)); - view->setHeader(header = new MessageViewHeader(this)); - view->setSortingEnabled(true); - view->sortByColumn(MessageListModel::Column::NAME, Qt::AscendingOrder); - view->setAllColumnsShowFocus(true); - view->setEditTriggers(QAbstractItemView::NoEditTriggers); - view->setItemsExpandable(false); - view->setIndentation(0); - view->setRootIsDecorated(false); - - // Must be called before setting any header parameters to avoid overriding - restoreHeaderState(settings.message_header_state); - header->setSectionsMovable(true); - header->setSectionResizeMode(MessageListModel::Column::DATA, QHeaderView::Fixed); - header->setStretchLastSection(true); - header->setContextMenuPolicy(Qt::CustomContextMenu); - - // signals/slots - QObject::connect(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow); - QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); - QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); - QObject::connect(model, &MessageListModel::modelReset, [this]() { - if (current_msg_id) { - selectMessage(*current_msg_id); - } - view->updateBytesSectionSize(); - updateTitle(); - }); - QObject::connect(view->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { - if (current.isValid() && current.row() < model->items_.size()) { - const auto &id = model->items_[current.row()].id; - if (!current_msg_id || id != *current_msg_id) { - current_msg_id = id; - emit msgSelectionChanged(*current_msg_id); - } - } - }); - - setWhatsThis(tr(R"( - Message View
- - Byte color
- ■ constant changing
- ■ increasing
- ■ decreasing
- Shortcuts
- Horizontal Scrolling:  shift+wheel  - )")); -} - -QWidget *MessagesWidget::createToolBar() { - QWidget *toolbar = new QWidget(this); - QHBoxLayout *layout = new QHBoxLayout(toolbar); - layout->setContentsMargins(0, 9, 0, 0); - layout->addWidget(suppress_add = new QPushButton("Suppress Highlighted")); - layout->addWidget(suppress_clear = new QPushButton()); - suppress_clear->setToolTip(tr("Clear suppressed")); - layout->addStretch(1); - QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Signals"), this); - suppress_defined_signals->setToolTip(tr("Suppress defined signals")); - suppress_defined_signals->setChecked(settings.suppress_defined_signals); - layout->addWidget(suppress_defined_signals); - - auto view_button = new ToolButton("three-dots", tr("View...")); - view_button->setMenu(menu); - view_button->setPopupMode(QToolButton::InstantPopup); - view_button->setStyleSheet("QToolButton::menu-indicator { image: none; }"); - layout->addWidget(view_button); - - QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); - QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); - QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, this, [](int state) { can->suppressDefinedSignals(state); }); - - suppressHighlighted(); - return toolbar; -} - -void MessagesWidget::updateTitle() { - auto stats = std::accumulate( - model->items_.begin(), model->items_.end(), std::pair(), - [](const auto &pair, const auto &item) { - auto m = dbc()->msg(item.id); - return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair; - }); - emit titleChanged(tr("%1 Messages (%2 DBC Messages, %3 Signals)") - .arg(model->items_.size()).arg(stats.first).arg(stats.second)); -} - -void MessagesWidget::selectMessage(const MessageId &msg_id) { - auto it = std::find_if(model->items_.cbegin(), model->items_.cend(), - [&msg_id](auto &item) { return item.id == msg_id; }); - if (it != model->items_.cend()) { - view->setCurrentIndex(model->index(std::distance(model->items_.cbegin(), it), 0)); - } -} - -void MessagesWidget::suppressHighlighted() { - int n = sender() == suppress_add ? can->suppressHighlighted() : (can->clearSuppressed(), 0); - suppress_clear->setText(n > 0 ? tr("Clear (%1)").arg(n) : tr("Clear")); - suppress_clear->setEnabled(n > 0); -} - -void MessagesWidget::headerContextMenuEvent(const QPoint &pos) { - menu->exec(header->mapToGlobal(pos)); -} - -void MessagesWidget::menuAboutToShow() { - menu->clear(); - for (int i = 0; i < header->count(); ++i) { - int logical_index = header->logicalIndex(i); - auto action = menu->addAction(model->headerData(logical_index, Qt::Horizontal).toString(), - [=](bool checked) { header->setSectionHidden(logical_index, !checked); }); - action->setCheckable(true); - action->setChecked(!header->isSectionHidden(logical_index)); - // Can't hide the name column - action->setEnabled(logical_index > 0); - } - menu->addSeparator(); - auto action = menu->addAction(tr("Multi-Line bytes"), this, &MessagesWidget::setMultiLineBytes); - action->setCheckable(true); - action->setChecked(settings.multiple_lines_hex); - - action = menu->addAction(tr("Show inactive messages"), model, &MessageListModel::showInactiveMessages); - action->setCheckable(true); - action->setChecked(model->show_inactive_messages); -} - -void MessagesWidget::setMultiLineBytes(bool multi) { - settings.multiple_lines_hex = multi; - delegate->setMultipleLines(multi); - view->updateBytesSectionSize(); - view->doItemsLayout(); -} - -// MessageListModel - -MessageListModel::MessageListModel(QObject *parent) : QAbstractTableModel(parent) { - connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool has_new_ids) { msgsReceived(msgs, has_new_ids); })); - connections_.push_back(dbc()->fileChanged.connect([this]() { dbcModified(); })); - connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { dbcModified(); })); -} - -QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { - switch (section) { - case Column::NAME: return tr("Name"); - case Column::SOURCE: return tr("Bus"); - case Column::ADDRESS: return tr("ID"); - case Column::NODE: return tr("Node"); - case Column::FREQ: return tr("Freq"); - case Column::COUNT: return tr("Count"); - case Column::DATA: return tr("Bytes"); - } - } - return {}; -} - -QVariant MessageListModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || index.row() >= items_.size()) return {}; - - auto getFreq = [](float freq) { - if (freq > 0) { - return freq >= 0.95 ? QString::number(std::nearbyint(freq)) : QString::number(freq, 'f', 2); - } else { - return QStringLiteral("--"); - } - }; - - const static QString NA = QStringLiteral("N/A"); - const auto &item = items_[index.row()]; - if (role == Qt::DisplayRole) { - switch (index.column()) { - case Column::NAME: return item.name; - case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA; - case Column::ADDRESS: return QString::fromStdString(utils::toHexString(item.id.address)); - case Column::NODE: return item.node; - case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA; - case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA; - case Column::DATA: return item.id.source != INVALID_SOURCE ? "" : NA; - } - } else if (role == ColorsRole) { - return QVariant::fromValue((void*)(&can->lastMessage(item.id).colors)); - } else if (role == BytesRole && index.column() == Column::DATA && item.id.source != INVALID_SOURCE) { - return QVariant::fromValue((void*)(&can->lastMessage(item.id).dat)); - } else if (role == Qt::ToolTipRole && index.column() == Column::NAME) { - auto msg = dbc()->msg(item.id); - auto tooltip = item.name; - if (msg && !msg->comment.empty()) tooltip += "
" + QString::fromStdString(msg->comment) + ""; - return tooltip; - } - return {}; -} - -void MessageListModel::setFilterStrings(const std::map &filters) { - filters_ = filters; - filterAndSort(); -} - -void MessageListModel::showInactiveMessages(bool show) { - show_inactive_messages = show; - filterAndSort(); -} - -void MessageListModel::dbcModified() { - dbc_messages_.clear(); - for (const auto &[_, m] : dbc()->getMessages(-1)) { - dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address}); - } - filterAndSort(); -} - -void MessageListModel::sortItems(std::vector &items) { - auto compare = [this](const auto &l, const auto &r) { - switch (sort_column) { - case Column::NAME: return std::tie(l.name, l.id) < std::tie(r.name, r.id); - case Column::SOURCE: return std::tie(l.id.source, l.id.address) < std::tie(r.id.source, r.id.address); - case Column::ADDRESS: return std::tie(l.id.address, l.id.source) < std::tie(r.id.address, r.id.source); - case Column::NODE: return std::tie(l.node, l.id) < std::tie(r.node, r.id); - case Column::FREQ: return std::tie(can->lastMessage(l.id).freq, l.id) < std::tie(can->lastMessage(r.id).freq, r.id); - case Column::COUNT: return std::tie(can->lastMessage(l.id).count, l.id) < std::tie(can->lastMessage(r.id).count, r.id); - default: return false; // Default case to suppress compiler warning - } - }; - - if (sort_order == Qt::DescendingOrder) - std::stable_sort(items.rbegin(), items.rend(), compare); - else - std::stable_sort(items.begin(), items.end(), compare); -} - -static bool parseRange(const QString &filter, uint32_t value, int base = 10) { - // Parse out filter string into a range (e.g. "1" -> {1, 1}, "1-3" -> {1, 3}, "1-" -> {1, inf}) - unsigned int min = std::numeric_limits::min(); - unsigned int max = std::numeric_limits::max(); - auto s = filter.split('-'); - bool ok = s.size() >= 1 && s.size() <= 2; - if (ok && !s[0].isEmpty()) min = s[0].toUInt(&ok, base); - if (ok && s.size() == 1) { - max = min; - } else if (ok && s.size() == 2 && !s[1].isEmpty()) { - max = s[1].toUInt(&ok, base); - } - return ok && value >= min && value <= max; -} - -bool MessageListModel::match(const MessageListModel::Item &item) { - if (filters_.empty()) - return true; - - bool match = true; - const auto &data = can->lastMessage(item.id); - for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) { - const QString &txt = it->second; - switch (it->first) { - case Column::NAME: { - match = item.name.contains(txt, Qt::CaseInsensitive); - if (!match) { - const auto m = dbc()->msg(item.id); - match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(), - [&txt](const auto &s) { return QString::fromStdString(s->name).contains(txt, Qt::CaseInsensitive); }); - } - break; - } - case Column::SOURCE: - match = parseRange(txt, item.id.source); - break; - case Column::ADDRESS: - match = QString::fromStdString(utils::toHexString(item.id.address)).contains(txt, Qt::CaseInsensitive); - match = match || parseRange(txt, item.id.address, 16); - break; - case Column::NODE: - match = item.node.contains(txt, Qt::CaseInsensitive); - break; - case Column::FREQ: - match = parseRange(txt, data.freq); - break; - case Column::COUNT: - match = parseRange(txt, data.count); - break; - case Column::DATA: - match = QString::fromStdString(utils::toHex(data.dat)).contains(txt, Qt::CaseInsensitive); - break; - } - } - return match; -} - -bool MessageListModel::filterAndSort() { - // merge CAN and DBC messages - std::vector all_messages; - all_messages.reserve(can->lastMessages().size() + dbc_messages_.size()); - auto dbc_msgs = dbc_messages_; - for (const auto &[id, m] : can->lastMessages()) { - all_messages.push_back(id); - dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address}); - } - all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end()); - - // filter and sort - std::vector items; - items.reserve(all_messages.size()); - for (const auto &id : all_messages) { - if (show_inactive_messages || can->isMessageActive(id)) { - auto msg = dbc()->msg(id); - Item item = {.id = id, - .name = msg ? QString::fromStdString(msg->name) : QString::fromStdString(UNTITLED), - .node = msg ? QString::fromStdString(msg->transmitter) : QString()}; - if (match(item)) - items.emplace_back(item); - } - } - sortItems(items); - - if (items_ != items) { - beginResetModel(); - items_ = std::move(items); - endResetModel(); - return true; - } - return false; -} - -void MessageListModel::msgsReceived(const std::set *new_msgs, bool has_new_ids) { - if (has_new_ids || ((filters_.count(Column::FREQ) || filters_.count(Column::COUNT) || filters_.count(Column::DATA)) && - ++sort_threshold_ == STREAM_UPDATE_FPS)) { - sort_threshold_ = 0; - if (filterAndSort()) return; - } - - // Update viewport - emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); -} - -void MessageListModel::sort(int column, Qt::SortOrder order) { - if (column != Column::DATA) { - sort_column = column; - sort_order = order; - filterAndSort(); - } -} - -// MessageView - -void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - const auto &item = ((MessageListModel*)model())->items_[index.row()]; - if (!can->isMessageActive(item.id)) { - QStyleOptionViewItem custom_option = option; - custom_option.palette.setBrush(QPalette::Text, custom_option.palette.color(QPalette::Disabled, QPalette::Text)); - auto color = QApplication::palette().color(QPalette::HighlightedText); - color.setAlpha(100); - custom_option.palette.setBrush(QPalette::HighlightedText, color); - QTreeView::drawRow(painter, custom_option, index); - } else { - QTreeView::drawRow(painter, option, index); - } - - QPen oldPen = painter->pen(); - const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this); - painter->setPen(QColor::fromRgba(static_cast(gridHint))); - // Draw bottom border for the row - painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight()); - // Draw vertical borders for each column - for (int i = 0; i < header()->count(); ++i) { - int sectionX = header()->sectionViewportPosition(i); - painter->drawLine(sectionX, option.rect.top(), sectionX, option.rect.bottom()); - } - painter->setPen(oldPen); -} - -void MessageView::setModel(QAbstractItemModel *model) { - QTreeView::setModel(model); - // Bypass the slow call to QTreeView::dataChanged. - // QTreeView::dataChanged will invalidate the height cache and that's what we don't need in MessageView. - QObject::disconnect(model, &QAbstractItemModel::dataChanged, this, nullptr); - QObject::connect(model, &QAbstractItemModel::dataChanged, this, - [this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); }); -} - -void MessageView::updateBytesSectionSize() { - auto delegate = ((MessageBytesDelegate *)itemDelegate()); - int max_bytes = 8; - if (!delegate->multipleLines()) { - for (const auto &[_, m] : can->lastMessages()) { - max_bytes = std::max(max_bytes, m.dat.size()); - } - } - setUniformRowHeights(!delegate->multipleLines()); - header()->resizeSection(MessageListModel::Column::DATA, delegate->sizeForBytes(max_bytes).width()); -} - -void MessageView::wheelEvent(QWheelEvent *event) { - if (event->modifiers() == Qt::ShiftModifier) { - QApplication::sendEvent(horizontalScrollBar(), event); - } else { - QTreeView::wheelEvent(event); - } -} - -// MessageViewHeader - -MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizontal, parent) { - QObject::connect(this, &QHeaderView::sectionResized, this, &MessageViewHeader::updateHeaderPositions); - QObject::connect(this, &QHeaderView::sectionMoved, this, &MessageViewHeader::updateHeaderPositions); -} - -void MessageViewHeader::updateFilters() { - std::map filters; - for (int i = 0; i < (int)editors.size(); i++) { - if (!editors[i]->text().isEmpty()) { - filters[i] = editors[i]->text(); - } - } - qobject_cast(model())->setFilterStrings(filters); -} - -void MessageViewHeader::updateHeaderPositions() { - QSize sz = QHeaderView::sizeHint(); - for (int i = 0; i < (int)editors.size(); i++) { - int h = editors[i]->sizeHint().height(); - editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h); - editors[i]->setHidden(isSectionHidden(i)); - } -} - -void MessageViewHeader::updateGeometries() { - for (int i = (int)editors.size(); i < count(); i++) { - QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); - auto edit = new QLineEdit(this); - edit->setClearButtonEnabled(true); - edit->setPlaceholderText(tr("Filter %1").arg(column_name)); - - QObject::connect(edit, &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters); - editors.push_back(edit); - } - setViewportMargins(0, 0, 0, !editors.empty() ? editors[0]->sizeHint().height() : 0); - - QHeaderView::updateGeometries(); - updateHeaderPositions(); -} - -QSize MessageViewHeader::sizeHint() const { - QSize sz = QHeaderView::sizeHint(); - return !editors.empty() ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz; -} diff --git a/openpilot/tools/cabana/messageswidget.h b/openpilot/tools/cabana/messageswidget.h deleted file mode 100644 index 7c9fc0253f..0000000000 --- a/openpilot/tools/cabana/messageswidget.h +++ /dev/null @@ -1,131 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/utils/qtutil.h" - -class MessageListModel : public QAbstractTableModel { -Q_OBJECT - -public: - enum Column { - NAME = 0, - SOURCE, - ADDRESS, - NODE, - FREQ, - COUNT, - DATA, - }; - - MessageListModel(QObject *parent); - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column::DATA + 1; } - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; - int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); } - void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; - void setFilterStrings(const std::map &filters); - void showInactiveMessages(bool show); - void msgsReceived(const std::set *new_msgs, bool has_new_ids); - bool filterAndSort(); - void dbcModified(); - - struct Item { - MessageId id; - QString name; - QString node; - bool operator==(const Item &other) const { - return id == other.id && name == other.name && node == other.node; - } - }; - std::vector items_; - bool show_inactive_messages = true; - -private: - void sortItems(std::vector &items); - bool match(const MessageListModel::Item &id); - - std::map filters_; - std::set dbc_messages_; - int sort_column = 0; - Qt::SortOrder sort_order = Qt::AscendingOrder; - int sort_threshold_ = 0; - Connections connections_; -}; - -class MessageView : public QTreeView { - Q_OBJECT -public: - MessageView(QWidget *parent) : QTreeView(parent) {} - void updateBytesSectionSize(); - void setModel(QAbstractItemModel *model) override; - -protected: - 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 wheelEvent(QWheelEvent *event) override; -}; - -class MessageViewHeader : public QHeaderView { - // https://stackoverflow.com/a/44346317 - Q_OBJECT -public: - MessageViewHeader(QWidget *parent); - void updateHeaderPositions(); - void updateGeometries() override; - QSize sizeHint() const override; - void updateFilters(); - - std::vector editors; -}; - -class MessagesWidget : public QWidget { - Q_OBJECT - -public: - MessagesWidget(QWidget *parent); - void selectMessage(const MessageId &message_id); - std::vector saveHeaderState() const { - const auto state = view->header()->saveState(); - return {state.begin(), state.end()}; - } - bool restoreHeaderState(const std::vector &state) const { - return view->header()->restoreState({(const char *)state.data(), (int)state.size()}); - } - void suppressHighlighted(); - -signals: - void msgSelectionChanged(const MessageId &message_id); - void titleChanged(const QString &title); - -protected: - QWidget *createToolBar(); - void headerContextMenuEvent(const QPoint &pos); - void menuAboutToShow(); - void setMultiLineBytes(bool multi); - void updateTitle(); - - MessageView *view; - MessageViewHeader *header; - MessageBytesDelegate *delegate; - std::optional current_msg_id; - MessageListModel *model; - QPushButton *suppress_add; - QPushButton *suppress_clear; - QMenu *menu; -}; diff --git a/openpilot/tools/cabana/routesdialog.cc b/openpilot/tools/cabana/routesdialog.cc deleted file mode 100644 index 33b773edb9..0000000000 --- a/openpilot/tools/cabana/routesdialog.cc +++ /dev/null @@ -1,110 +0,0 @@ -#include "tools/cabana/routesdialog.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include "tools/cabana/utils/util.h" - -// The RouteListWidget class extends QListWidget to display a custom message when empty -class RouteListWidget : public QListWidget { -public: - RouteListWidget(QWidget *parent = nullptr) : QListWidget(parent) {} - void setEmptyText(const QString &text) { - empty_text_ = text; - viewport()->update(); - } - void paintEvent(QPaintEvent *event) override { - QListWidget::paintEvent(event); - if (count() == 0) { - QPainter painter(viewport()); - painter.drawText(viewport()->rect(), Qt::AlignCenter, empty_text_); - } - } - QString empty_text_ = tr("No items"); -}; - -RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("Remote routes")); - - QFormLayout *layout = new QFormLayout(this); - layout->addRow(tr("Device"), device_list_ = new QComboBox(this)); - layout->addRow(period_selector_ = new QComboBox(this)); - layout->addRow(route_list_ = new RouteListWidget(this)); - auto button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - layout->addRow(button_box); - - device_list_->addItem(tr("Loading...")); - period_selector_->addItem(tr("Last week"), 7); - period_selector_->addItem(tr("Last 2 weeks"), 14); - period_selector_->addItem(tr("Last month"), 30); - period_selector_->addItem(tr("Last 6 months"), 180); - period_selector_->addItem(tr("Preserved"), -1); - - connect(device_list_, QOverload::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes); - connect(period_selector_, QOverload::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes); - connect(route_list_, &QListWidget::itemDoubleClicked, this, &QDialog::accept); - connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - - routes::fetchDevices([this, alive = std::weak_ptr(alive_)](std::vector devices, bool success, int error_code) { - utils::runOnMainThread([this, alive, devices = std::move(devices), success, error_code]() { - if (!alive.expired()) setDeviceList(devices, success, error_code); - }); - }); -} - -void RoutesDialog::setDeviceList(const std::vector &devices, bool success, int error_code) { - if (success) { - device_list_->clear(); - for (const auto &device : devices) { - QString dongle_id = QString::fromStdString(device.dongle_id); - device_list_->addItem(dongle_id, dongle_id); - } - } else { - QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with openpilot/tools/lib/auth.py") : tr("Network error")); - reject(); - } -} - -void RoutesDialog::fetchRoutes() { - if (device_list_->currentIndex() == -1 || device_list_->currentData().isNull()) - return; - - route_list_->clear(); - route_list_->setEmptyText(tr("Loading...")); - - int request_id = ++fetch_id_; - auto on_routes = [this, alive = std::weak_ptr(alive_), request_id](std::vector list, bool success, int) { - utils::runOnMainThread([this, alive, list = std::move(list), success, request_id]() { - if (!alive.expired() && fetch_id_ == request_id) setRouteList(list, success); - }); - }; - routes::fetchRoutes(device_list_->currentText().toStdString(), period_selector_->currentData().toInt(), std::move(on_routes)); -} - -void RoutesDialog::setRouteList(const std::vector &list, bool success) { - if (success) { - for (const auto &route : list) { - const int mins = static_cast((route.end_ms - route.start_ms) / 60000); - auto item = new QListWidgetItem(QString::fromStdString(routes::formatUnixMs(route.start_ms) + " " + std::to_string(mins) + "min")); - item->setData(Qt::UserRole, QString::fromStdString(route.name)); - route_list_->addItem(item); - } - if (route_list_->count() > 0) route_list_->setCurrentRow(0); - } else { - QMessageBox::warning(this, tr("Error"), tr("Failed to fetch routes. Check your network connection.")); - reject(); - } - route_list_->setEmptyText(tr("No items")); -} - -std::string RoutesDialog::route() { - auto current_item = route_list_->currentItem(); - return current_item ? current_item->data(Qt::UserRole).toString().toStdString() : ""; -} diff --git a/openpilot/tools/cabana/routesdialog.h b/openpilot/tools/cabana/routesdialog.h deleted file mode 100644 index 4983f1b558..0000000000 --- a/openpilot/tools/cabana/routesdialog.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include - -#include "tools/cabana/routes.h" - -class RouteListWidget; - -class RoutesDialog : public QDialog { - Q_OBJECT -public: - RoutesDialog(QWidget *parent); - std::string route(); - -protected: - void setDeviceList(const std::vector &devices, bool success, int error_code); - void setRouteList(const std::vector &list, bool success); - void fetchRoutes(); - - QComboBox *device_list_; - QComboBox *period_selector_; - RouteListWidget *route_list_; - std::atomic fetch_id_{0}; - // expires on destruction; guards main-thread callbacks from detached worker threads - std::shared_ptr alive_ = std::make_shared(true); -}; diff --git a/openpilot/tools/cabana/settingsdialog.cc b/openpilot/tools/cabana/settingsdialog.cc deleted file mode 100644 index 0fbc82c508..0000000000 --- a/openpilot/tools/cabana/settingsdialog.cc +++ /dev/null @@ -1,88 +0,0 @@ -#include "tools/cabana/settingsdialog.h" - -#include - -#include -#include -#include -#include -#include - -#include "tools/cabana/settings.h" -#include "tools/cabana/utils/qtutil.h" - -const int MIN_CACHE_MINIUTES = 30; -const int MAX_CACHE_MINIUTES = 120; - -SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("Settings")); - QVBoxLayout *main_layout = new QVBoxLayout(this); - QGroupBox *groupbox = new QGroupBox("General"); - QFormLayout *form_layout = new QFormLayout(groupbox); - - form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); - theme->setToolTip(tr("You may need to restart cabana after changes theme")); - theme->addItems({tr("Light"), tr("Dark")}); - theme->setCurrentIndex(settings.theme - LIGHT_THEME); - - form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); - cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES); - cached_minutes->setSingleStep(1); - cached_minutes->setValue(settings.max_cached_minutes); - main_layout->addWidget(groupbox); - - groupbox = new QGroupBox("New Signal Settings"); - form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Drag Direction"), 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); - main_layout->addWidget(groupbox); - - groupbox = new QGroupBox("Chart"); - form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); - chart_height->setRange(100, 500); - chart_height->setSingleStep(10); - chart_height->setValue(settings.chart_height); - main_layout->addWidget(groupbox); - - log_livestream = new QGroupBox(tr("Enable live stream logging"), this); - log_livestream->setCheckable(true); - log_livestream->setChecked(settings.log_livestream); - QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); - path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); - log_path->setReadOnly(true); - auto browse_btn = new QPushButton(tr("B&rowse...")); - path_layout->addWidget(browse_btn); - main_layout->addWidget(log_livestream); - - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox); - setFixedSize(400, sizeHint().height()); - - QObject::connect(browse_btn, &QPushButton::clicked, [this]() { - QString fn = QFileDialog::getExistingDirectory( - this, tr("Log File Location"), - QString::fromStdString(utils::homePath()), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - if (!fn.isEmpty()) { - log_path->setText(fn); - } - }); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::save); -} - -void SettingsDialog::save() { - if (std::exchange(settings.theme, theme->currentIndex() + LIGHT_THEME) != settings.theme) { - // set theme before emit changed - utils::setTheme(settings.theme); - } - settings.max_cached_minutes = cached_minutes->value(); - settings.chart_height = chart_height->value(); - settings.log_livestream = log_livestream->isChecked(); - settings.log_path = log_path->text().toStdString(); - settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); - settings.changed(); - QDialog::accept(); -} diff --git a/openpilot/tools/cabana/settingsdialog.h b/openpilot/tools/cabana/settingsdialog.h deleted file mode 100644 index 895b789f30..0000000000 --- a/openpilot/tools/cabana/settingsdialog.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -class SettingsDialog : public QDialog { -public: - SettingsDialog(QWidget *parent); - void save(); - QSpinBox *cached_minutes; - QSpinBox *chart_height; - QComboBox *chart_series_type; - QComboBox *theme; - QGroupBox *log_livestream; - QLineEdit *log_path; - QComboBox *drag_direction; -}; diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc deleted file mode 100644 index de80ce2a3f..0000000000 --- a/openpilot/tools/cabana/signalview.cc +++ /dev/null @@ -1,719 +0,0 @@ -#include "tools/cabana/signalview.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/commands.h" -#include "tools/cabana/utils/qtutil.h" - -// SignalModel - -static QString signalTypeToString(cabana::Signal::Type type) { - if (type == cabana::Signal::Type::Multiplexor) return "Multiplexor Signal"; - else if (type == cabana::Signal::Type::Multiplexed) return "Multiplexed Signal"; - else return "Normal Signal"; -} - -SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) { - connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); - connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { handleMsgChanged(id); })); - connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { handleMsgChanged(id); })); - connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); })); - connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); })); - connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { handleSignalRemoved(sig); })); -} - -void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) { - Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = QString::fromStdString(sig->name)}; - root_item->children.insert(root_item->children.begin() + pos, parent_item); - QString titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type", - "Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"}; - for (int i = 0; i < std::size(titles); ++i) { - auto item = new Item{.type = (Item::Type)(i + Item::Name), .parent = parent_item, .sig = sig, .title = titles[i]}; - parent_item->children.push_back(item); - if (item->type == Item::ExtraInfo) { - parent_item = item; - } - } -} - -void SignalModel::setMessage(const MessageId &id) { - msg_id = id; - filter_str = ""; - refresh(); -} - -void SignalModel::setFilter(const QString &txt) { - filter_str = txt; - refresh(); -} - -void SignalModel::refresh() { - beginResetModel(); - root.reset(new SignalModel::Item); - if (auto msg = dbc()->msg(msg_id)) { - for (auto s : msg->getSignals()) { - if (filter_str.isEmpty() || QString::fromStdString(s->name).contains(filter_str, Qt::CaseInsensitive)) { - insertItem(root.get(), root->children.size(), s); - } - } - } - endResetModel(); -} - -SignalModel::Item *SignalModel::getItem(const QModelIndex &index) const { - auto item = index.isValid() ? (SignalModel::Item *)index.internalPointer() : nullptr; - return item ? item : root.get(); -} - -int SignalModel::rowCount(const QModelIndex &parent) const { - if (parent.isValid() && parent.column() > 0) return 0; - - return getItem(parent)->children.size(); -} - -Qt::ItemFlags SignalModel::flags(const QModelIndex &index) const { - if (!index.isValid()) return Qt::NoItemFlags; - - auto item = getItem(index); - Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; - if (index.column() == 1 && item->children.empty()) { - flags |= (item->type == Item::Endian || item->type == Item::Signed) ? Qt::ItemIsUserCheckable : Qt::ItemIsEditable; - } - if (item->type == Item::MultiplexValue && item->sig->type != cabana::Signal::Type::Multiplexed) { - flags &= ~Qt::ItemIsEnabled; - } - return flags; -} - -int SignalModel::signalRow(const cabana::Signal *sig) const { - for (int i = 0; i < root->children.size(); ++i) { - if (root->children[i]->sig == sig) return i; - } - return -1; -} - -QModelIndex SignalModel::index(int row, int column, const QModelIndex &parent) const { - if (parent.isValid() && parent.column() != 0) return {}; - - auto parent_item = getItem(parent); - if (parent_item && row < parent_item->children.size()) { - return createIndex(row, column, parent_item->children[row]); - } - return {}; -} - -QModelIndex SignalModel::parent(const QModelIndex &index) const { - if (!index.isValid()) return {}; - Item *parent_item = getItem(index)->parent; - return !parent_item || parent_item == root.get() ? QModelIndex() : createIndex(parent_item->row(), 0, parent_item); -} - -QVariant SignalModel::data(const QModelIndex &index, int role) const { - if (index.isValid()) { - const Item *item = getItem(index); - if (role == Qt::DisplayRole || role == Qt::EditRole) { - if (index.column() == 0) { - return item->type == Item::Sig ? QString::fromStdString(item->sig->name) : item->title; - } else { - switch (item->type) { - case Item::Sig: return item->sig_val; - case Item::Name: return QString::fromStdString(item->sig->name); - case Item::Size: return item->sig->size; - case Item::Node: return QString::fromStdString(item->sig->receiver_name); - case Item::SignalType: return signalTypeToString(item->sig->type); - case Item::MultiplexValue: return item->sig->multiplex_value; - case Item::Offset: return QString::fromStdString(doubleToString(item->sig->offset)); - case Item::Factor: return QString::fromStdString(doubleToString(item->sig->factor)); - case Item::Unit: return QString::fromStdString(item->sig->unit); - case Item::Comment: return QString::fromStdString(item->sig->comment); - case Item::Min: return QString::fromStdString(doubleToString(item->sig->min)); - case Item::Max: return QString::fromStdString(doubleToString(item->sig->max)); - case Item::Desc: { - QStringList val_desc; - for (auto &[val, desc] : item->sig->val_desc) { - val_desc << QString("%1 \"%2\"").arg(val).arg(QString::fromStdString(desc)); - } - return val_desc.join(" "); - } - default: break; - } - } - } else if (role == Qt::CheckStateRole && index.column() == 1) { - if (item->type == Item::Endian) return item->sig->is_little_endian ? Qt::Checked : Qt::Unchecked; - if (item->type == Item::Signed) return item->sig->is_signed ? Qt::Checked : Qt::Unchecked; - } else if (role == Qt::ToolTipRole && item->type == Item::Sig) { - return (index.column() == 0) ? QString::fromStdString(utils::signalToolTip(item->sig)) : QString(); - } - } - return {}; -} - -bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int role) { - if (role != Qt::EditRole && role != Qt::CheckStateRole) return false; - - Item *item = getItem(index); - cabana::Signal s = *item->sig; - switch (item->type) { - case Item::Name: s.name = value.toString().toStdString(); break; - case Item::Size: s.size = value.toInt(); break; - case Item::Node: s.receiver_name = value.toString().trimmed().toStdString(); break; - case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break; - case Item::MultiplexValue: s.multiplex_value = value.toInt(); break; - case Item::Endian: s.is_little_endian = value.toBool(); break; - case Item::Signed: s.is_signed = value.toBool(); break; - case Item::Offset: s.offset = value.toDouble(); break; - case Item::Factor: s.factor = value.toDouble(); break; - case Item::Unit: s.unit = value.toString().toStdString(); break; - case Item::Comment: s.comment = value.toString().toStdString(); break; - case Item::Min: s.min = value.toDouble(); break; - case Item::Max: s.max = value.toDouble(); break; - case Item::Desc: s.val_desc = value.value(); break; - default: return false; - } - bool ret = saveSignal(item->sig, s); - emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole}); - return ret; -} - -bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) { - auto msg = dbc()->msg(msg_id); - if (s.name != origin_s->name && msg->sig(s.name) != nullptr) { - QString text = tr("There is already a signal with the same name '%1'").arg(QString::fromStdString(s.name)); - QMessageBox::warning(nullptr, tr("Failed to save signal"), text); - return false; - } - - if (s.is_little_endian != origin_s->is_little_endian) { - s.start_bit = flipBitPos(s.start_bit); - } - UndoStack::instance()->push(new EditSignalCommand(msg_id, origin_s, s)); - return true; -} - -void SignalModel::handleMsgChanged(MessageId id) { - if (id.address == msg_id.address) { - refresh(); - } -} - -void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) { - if (id == msg_id) { - if (filter_str.isEmpty()) { - int i = dbc()->msg(msg_id)->indexOf(sig); - beginInsertRows({}, i, i); - insertItem(root.get(), i, sig); - endInsertRows(); - } else if (QString::fromStdString(sig->name).contains(filter_str, Qt::CaseInsensitive)) { - refresh(); - } - } -} - -void SignalModel::handleSignalUpdated(const cabana::Signal *sig) { - if (int row = signalRow(sig); row != -1) { - emit dataChanged(index(row, 0), index(row, 1), {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole}); - - if (filter_str.isEmpty()) { - // move row when the order changes. - int to = dbc()->msg(msg_id)->indexOf(sig); - if (to != row) { - beginMoveRows({}, row, row, {}, to > row ? to + 1 : to); - auto item = root->children[row]; - root->children.erase(root->children.begin() + row); - root->children.insert(root->children.begin() + to, item); - endMoveRows(); - } - } - } -} - -void SignalModel::handleSignalRemoved(const cabana::Signal *sig) { - if (int row = signalRow(sig); row != -1) { - beginRemoveRows({}, row, row); - delete root->children[row]; - root->children.erase(root->children.begin() + row); - endRemoveRows(); - } -} - -// SignalItemDelegate - -SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { - name_validator = new NameValidator(this); - node_validator = new NodeValidator(this); - double_validator = new DoubleValidator(this); - - label_font.setPointSize(8); - minmax_font.setPixelSize(10); -} - -QSize SignalItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - int width = option.widget->size().width() / 2; - if (index.column() == 0) { - int spacing = option.widget->style()->pixelMetric(QStyle::PM_TreeViewIndentation) + color_label_width + 8; - auto text = index.data(Qt::DisplayRole).toString(); - auto item = (SignalModel::Item *)index.internalPointer(); - if (item->type == SignalModel::Item::Sig && item->sig->type != cabana::Signal::Type::Normal) { - text += item->sig->type == cabana::Signal::Type::Multiplexor ? QString(" M ") : QString(" m%1 ").arg(item->sig->multiplex_value); - spacing += (option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1) * 2; - } - width = std::min(option.widget->size().width() / 3.0, option.fontMetrics.horizontalAdvance(text) + spacing); - } - return {width, option.fontMetrics.height() + option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2}; -} - -void SignalItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { - auto item = (SignalModel::Item *)index.internalPointer(); - if (editor && item->type == SignalModel::Item::Sig && index.column() == 1) { - 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 { - const int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; - const int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin); - auto item = static_cast(index.internalPointer()); - - QRect rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); - painter->setRenderHint(QPainter::Antialiasing); - if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); - } - - if (index.column() == 0) { - if (item->type == SignalModel::Item::Sig) { - // color label - QPainterPath path; - QRect icon_rect{rect.x(), rect.y(), color_label_width, rect.height()}; - path.addRoundedRect(icon_rect, 3, 3); - painter->setPen(item->highlight ? Qt::white : Qt::black); - painter->setFont(label_font); - painter->fillPath(path, toQColor(item->sig->color.darker(item->highlight ? 125 : 0))); - painter->drawText(icon_rect, Qt::AlignCenter, QString::number(item->row() + 1)); - - rect.setLeft(icon_rect.right() + h_margin * 2); - // multiplexer indicator - if (item->sig->type != cabana::Signal::Type::Normal) { - QString indicator = item->sig->type == cabana::Signal::Type::Multiplexor ? QString(" M ") : QString(" m%1 ").arg(item->sig->multiplex_value); - QRect indicator_rect{rect.x(), rect.y(), option.fontMetrics.horizontalAdvance(indicator), rect.height()}; - painter->setBrush(Qt::gray); - painter->setPen(Qt::NoPen); - painter->drawRoundedRect(indicator_rect, 3, 3); - painter->setPen(Qt::white); - painter->drawText(indicator_rect, Qt::AlignCenter, indicator); - rect.setLeft(indicator_rect.right() + h_margin * 2); - } - } else { - rect.setLeft(option.widget->style()->pixelMetric(QStyle::PM_TreeViewIndentation) + color_label_width + h_margin * 3); - } - - // name - auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, rect.width()); - painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); - painter->setFont(option.font); - painter->drawText(rect, option.displayAlignment, text); - } else if (index.column() == 1) { - if (!item->sparkline.pixmap.isNull()) { - QSize sparkline_size = item->sparkline.pixmap.size() / item->sparkline.pixmap.devicePixelRatio(); - painter->drawPixmap(QRect(rect.topLeft(), sparkline_size), item->sparkline.pixmap); - // min-max value - painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); - rect.adjust(sparkline_size.width() + 1, 0, 0, 0); - int value_adjust = 10; - if (!item->sparkline.isEmpty() && (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.horizontalAdvance(min), fm.horizontalAdvance(max)) + 5; - } else if (!item->sparkline.isEmpty() && item->sig->type == cabana::Signal::Type::Multiplexed) { - // display freq of multiplexed signal - painter->setFont(label_font); - QString freq = QString("%1 hz").arg(item->sparkline.freq(), 0, 'g', 2); - painter->drawText(rect.adjusted(5, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, freq); - value_adjust = QFontMetrics(label_font).horizontalAdvance(freq) + 10; - } - // signal 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); - } - } -} - -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::Node || item->type == SignalModel::Item::Offset || - item->type == SignalModel::Item::Factor || item->type == SignalModel::Item::MultiplexValue || - item->type == SignalModel::Item::Min || item->type == SignalModel::Item::Max) { - QLineEdit *e = new QLineEdit(parent); - e->setFrame(false); - if (item->type == SignalModel::Item::Name) e->setValidator(name_validator); - else if (item->type == SignalModel::Item::Node) e->setValidator(node_validator); - else e->setValidator(double_validator); - - return e; - } else if (item->type == SignalModel::Item::Size) { - QSpinBox *spin = new QSpinBox(parent); - spin->setFrame(false); - spin->setRange(1, CAN_MAX_DATA_BYTES); - return spin; - } else if (item->type == SignalModel::Item::SignalType) { - QComboBox *c = new QComboBox(parent); - c->addItem(signalTypeToString(cabana::Signal::Type::Normal), (int)cabana::Signal::Type::Normal); - if (!dbc()->msg(((SignalModel *)index.model())->msg_id)->multiplexor) { - c->addItem(signalTypeToString(cabana::Signal::Type::Multiplexor), (int)cabana::Signal::Type::Multiplexor); - } else if (item->sig->type != cabana::Signal::Type::Multiplexor) { - c->addItem(signalTypeToString(cabana::Signal::Type::Multiplexed), (int)cabana::Signal::Type::Multiplexed); - } - return c; - } else if (item->type == SignalModel::Item::Desc) { - ValueDescriptionDlg dlg(item->sig->val_desc, parent); - dlg.setWindowTitle(QString::fromStdString(item->sig->name)); - if (dlg.exec()) { - ((QAbstractItemModel *)index.model())->setData(index, QVariant::fromValue(dlg.val_desc)); - } - return nullptr; - } - return QStyledItemDelegate::createEditor(parent, option, index); -} - -void SignalItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { - auto item = (SignalModel::Item *)index.internalPointer(); - if (item->type == SignalModel::Item::SignalType) { - model->setData(index, ((QComboBox*)editor)->currentData().toInt()); - return; - } - QStyledItemDelegate::setModelData(editor, model, index); -} - -// SignalView - -SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QFrame(parent) { - setFrameStyle(QFrame::StyledPanel | QFrame::Plain); - // title bar - QWidget *title_bar = new QWidget(this); - QHBoxLayout *hl = new QHBoxLayout(title_bar); - hl->addWidget(signal_count_lb = new QLabel()); - filter_edit = new QLineEdit(this); - filter_edit->setValidator(new NonWhitespaceValidator(this)); - filter_edit->setClearButtonEnabled(true); - filter_edit->setPlaceholderText(tr("Filter Signal")); - hl->addWidget(filter_edit); - hl->addStretch(1); - - // WARNING: increasing the maximum range can result in severe performance degradation. - // 30s is a reasonable value at present. - const int max_range = 30; // 30s - settings.sparkline_range = std::clamp(settings.sparkline_range, 1, max_range); - hl->addWidget(sparkline_label = new QLabel()); - hl->addWidget(sparkline_range_slider = new QSlider(Qt::Horizontal, this)); - sparkline_range_slider->setRange(1, max_range); - sparkline_range_slider->setValue(settings.sparkline_range); - sparkline_range_slider->setToolTip(tr("Sparkline time range")); - - auto collapse_btn = new ToolButton("dash-square", tr("Collapse All")); - collapse_btn->setIconSize({12, 12}); - hl->addWidget(collapse_btn); - - // tree view - tree = new TreeView(this); - tree->setModel(model = new SignalModel(this)); - tree->setItemDelegate(delegate = new SignalItemDelegate(this)); - tree->setFrameShape(QFrame::NoFrame); - tree->setHeaderHidden(true); - tree->setMouseTracking(true); - tree->setExpandsOnDoubleClick(false); - tree->setEditTriggers(QAbstractItemView::AllEditTriggers); - tree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); - tree->header()->setStretchLastSection(true); - tree->setMinimumHeight(300); - - // Use a distinctive background for the whole row containing a QSpinBox or QLineEdit - QString nodeBgColor = palette().color(QPalette::AlternateBase).name(QColor::HexArgb); - tree->setStyleSheet(QString("QSpinBox{background-color:%1;border:none;} QLineEdit{background-color:%1;}").arg(nodeBgColor)); - - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->setSpacing(0); - main_layout->addWidget(title_bar); - main_layout->addWidget(tree); - updateToolBar(); - - QObject::connect(filter_edit, &QLineEdit::textEdited, model, &SignalModel::setFilter); - QObject::connect(sparkline_range_slider, &QSlider::valueChanged, this, &SignalView::setSparklineRange); - QObject::connect(collapse_btn, &QPushButton::clicked, tree, &QTreeView::collapseAll); - QObject::connect(tree, &QAbstractItemView::clicked, this, &SignalView::rowClicked); - QObject::connect(tree, &QTreeView::viewportEntered, [this]() { emit highlight(nullptr); }); - QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); }); - QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged); - QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged); - connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); })); - connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); })); - QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); }); - QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); }); - connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool) { updateState(msgs); })); - QObject::connect(tree->header(), &QHeaderView::sectionResized, [this](int logicalIndex, int oldSize, int newSize) { - if (logicalIndex == 1) { - value_column_width = newSize; - updateState(); - } - }); - - setWhatsThis(tr(R"( - Signal view
- - )")); -} - -void SignalView::setMessage(const MessageId &id) { - max_value_width = 0; - filter_edit->clear(); - model->setMessage(id); -} - -void SignalView::rowsChanged() { - for (int i = 0; i < model->rowCount(); ++i) { - auto index = model->index(i, 1); - if (!tree->indexWidget(index)) { - QWidget *w = new QWidget(this); - QHBoxLayout *h = new QHBoxLayout(w); - int v_margin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin); - int h_margin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin); - h->setContentsMargins(0, v_margin, -h_margin, v_margin); - h->setSpacing(style()->pixelMetric(QStyle::PM_ToolBarItemSpacing)); - - auto remove_btn = new ToolButton("x", tr("Remove signal")); - auto plot_btn = new ToolButton("graph-up", ""); - plot_btn->setCheckable(true); - h->addWidget(plot_btn); - h->addWidget(remove_btn); - - tree->setIndexWidget(index, w); - auto sig = model->getItem(index)->sig; - QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, sig)); }); - QObject::connect(plot_btn, &QToolButton::clicked, [=](bool checked) { - emit showChart(model->msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier); - }); - } - } - updateToolBar(); - updateChartState(); - updateState(); -} - -void SignalView::rowClicked(const QModelIndex &index) { - auto item = model->getItem(index); - if (item->type == SignalModel::Item::Sig || item->type == SignalModel::Item::ExtraInfo) { - auto expand_index = model->index(index.row(), 0, index.parent()); - tree->setExpanded(expand_index, !tree->isExpanded(expand_index)); - } -} - -void SignalView::selectSignal(const cabana::Signal *sig, bool expand) { - if (int row = model->signalRow(sig); row != -1) { - auto idx = model->index(row, 0); - if (expand) { - tree->setExpanded(idx, !tree->isExpanded(idx)); - } - tree->scrollTo(idx, QAbstractItemView::PositionAtTop); - tree->setCurrentIndex(idx); - } -} - -void SignalView::updateChartState() { - int i = 0; - for (auto item : model->root->children) { - 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); - buttons[0]->setToolTip(chart_opened ? tr("Close Plot") : tr("Show Plot\nSHIFT click to add to previous opened plot")); - } - ++i; - } -} - -void SignalView::signalHovered(const cabana::Signal *sig) { - auto &children = model->root->children; - for (int i = 0; i < children.size(); ++i) { - bool highlight = children[i]->sig == sig; - if (std::exchange(children[i]->highlight, highlight) != highlight) { - emit model->dataChanged(model->index(i, 0), model->index(i, 0), {Qt::DecorationRole}); - emit model->dataChanged(model->index(i, 1), model->index(i, 1), {Qt::DisplayRole}); - } - } -} - -void SignalView::updateToolBar() { - signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount())); - sparkline_label->setText(QString::fromStdString(utils::formatSeconds(settings.sparkline_range))); -} - -void SignalView::setSparklineRange(int value) { - settings.sparkline_range = value; - updateToolBar(); - updateState(); -} - -void SignalView::handleSignalAdded(MessageId id, const cabana::Signal *sig) { - if (id.address == model->msg_id.address) { - selectSignal(sig); - } -} - -void SignalView::handleSignalUpdated(const cabana::Signal *sig) { - if (int row = model->signalRow(sig); row != -1) - updateState(); -} - -std::pair SignalView::visibleSignalRange() { - auto topLevelIndex = [](QModelIndex index) { - while (index.isValid() && index.parent().isValid()) index = index.parent(); - return index; - }; - - const auto viewport_rect = tree->viewport()->rect(); - QModelIndex first_visible = tree->indexAt(viewport_rect.topLeft()); - if (first_visible.parent().isValid()) { - first_visible = topLevelIndex(first_visible); - first_visible = first_visible.siblingAtRow(first_visible.row() + 1); - } - - QModelIndex last_visible = topLevelIndex(tree->indexAt(viewport_rect.bottomRight())); - if (!last_visible.isValid()) { - last_visible = model->index(model->rowCount() - 1, 0); - } - return {first_visible, last_visible}; -} - -void SignalView::updateState(const std::set *msgs) { - const auto &last_msg = can->lastMessage(model->msg_id); - if (model->rowCount() == 0 || (msgs && !msgs->count(model->msg_id)) || last_msg.dat.size() == 0) return; - - for (auto item : model->root->children) { - double value = 0; - if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) { - item->sig_val = QString::fromStdString(item->sig->formatValue(value)); - max_value_width = std::max(max_value_width, fontMetrics().horizontalAdvance(item->sig_val)); - } - } - - auto [first_visible, last_visible] = visibleSignalRange(); - if (first_visible.isValid() && last_visible.isValid()) { - const static int min_max_width = QFontMetrics(delegate->minmax_font).horizontalAdvance("-000.00") + 5; - int available_width = value_column_width - delegate->button_size.width(); - int value_width = std::min(max_value_width + min_max_width, available_width / 2); - QSize size(available_width - value_width, - delegate->button_size.height() - style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2); - - auto [first, last] = can->eventsInRange(model->msg_id, std::make_pair(last_msg.ts -settings.sparkline_range, last_msg.ts)); - std::vector> futures; - for (int i = first_visible.row(); i <= last_visible.row(); ++i) { - auto item = model->getItem(model->index(i, 1)); - futures.push_back(std::async(std::launch::async, - &Sparkline::update, &item->sparkline, item->sig, first, last, settings.sparkline_range, size)); - } - for (auto &f : futures) f.get(); - } - - 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); -} - -// ValueDescriptionDlg - -ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions, QWidget *parent) : QDialog(parent) { - QHBoxLayout *toolbar_layout = new QHBoxLayout(); - QPushButton *add = new QPushButton(utils::icon("plus"), ""); - QPushButton *remove = new QPushButton(utils::icon("dash"), ""); - remove->setEnabled(false); - toolbar_layout->addWidget(add); - toolbar_layout->addWidget(remove); - toolbar_layout->addStretch(0); - - table = new QTableWidget(descriptions.size(), 2, this); - table->setItemDelegate(new Delegate(this)); - table->setHorizontalHeaderLabels({"Value", "Description"}); - table->horizontalHeader()->setStretchLastSection(true); - table->setSelectionBehavior(QAbstractItemView::SelectRows); - table->setSelectionMode(QAbstractItemView::SingleSelection); - table->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); - table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - - int row = 0; - for (auto &[val, desc] : descriptions) { - table->setItem(row, 0, new QTableWidgetItem(QString::number(val))); - table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(desc))); - ++row; - } - - auto btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->addLayout(toolbar_layout); - main_layout->addWidget(table); - main_layout->addWidget(btn_box); - setMinimumWidth(500); - - QObject::connect(btn_box, &QDialogButtonBox::accepted, this, &ValueDescriptionDlg::save); - QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - QObject::connect(add, &QPushButton::clicked, [this]() { - table->setRowCount(table->rowCount() + 1); - table->setItem(table->rowCount() - 1, 0, new QTableWidgetItem); - table->setItem(table->rowCount() - 1, 1, new QTableWidgetItem); - }); - QObject::connect(remove, &QPushButton::clicked, [this]() { table->removeRow(table->currentRow()); }); - QObject::connect(table, &QTableWidget::itemSelectionChanged, [=]() { - remove->setEnabled(table->currentRow() != -1); - }); -} - -void ValueDescriptionDlg::save() { - for (int i = 0; i < table->rowCount(); ++i) { - QString val = table->item(i, 0)->text().trimmed(); - QString desc = table->item(i, 1)->text().trimmed(); - if (!val.isEmpty() && !desc.isEmpty()) { - val_desc.push_back({val.toDouble(), desc.toStdString()}); - } - } - QDialog::accept(); -} - -QWidget *ValueDescriptionDlg::Delegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { - QLineEdit *edit = new QLineEdit(parent); - edit->setFrame(false); - if (index.column() == 0) { - edit->setValidator(new DoubleValidator(parent)); - } - return edit; -} diff --git a/openpilot/tools/cabana/signalview.h b/openpilot/tools/cabana/signalview.h deleted file mode 100644 index e2a99ebbd1..0000000000 --- a/openpilot/tools/cabana/signalview.h +++ /dev/null @@ -1,155 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/chart/chartswidget.h" -#include "tools/cabana/chart/sparkline.h" - -class SignalModel : public QAbstractItemModel { - Q_OBJECT -public: - struct Item { - enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc }; - ~Item() { for (auto c : children) delete c; } - inline int row() { - auto it = std::find(parent->children.begin(), parent->children.end(), this); - return it != parent->children.end() ? std::distance(parent->children.begin(), it) : -1; - } - - Type type = Type::Root; - Item *parent = nullptr; - std::vector children; - - const cabana::Signal *sig = nullptr; - QString title; - bool highlight = false; - QString sig_val = "-"; - Sparkline sparkline; - }; - - SignalModel(QObject *parent); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 2; } - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; - QModelIndex parent(const QModelIndex &index) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - void setMessage(const MessageId &id); - void setFilter(const QString &txt); - bool saveSignal(const cabana::Signal *origin_s, cabana::Signal &s); - Item *getItem(const QModelIndex &index) const; - int signalRow(const cabana::Signal *sig) const; - -private: - void insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig); - void handleSignalAdded(MessageId id, const cabana::Signal *sig); - void handleSignalUpdated(const cabana::Signal *sig); - void handleSignalRemoved(const cabana::Signal *sig); - void handleMsgChanged(MessageId id); - void refresh(); - - MessageId msg_id; - QString filter_str; - std::unique_ptr root; - Connections connections_; - friend class SignalView; - friend class SignalItemDelegate; -}; - -class ValueDescriptionDlg : public QDialog { -public: - ValueDescriptionDlg(const ValueDescription &descriptions, QWidget *parent); - ValueDescription val_desc; - -private: - struct Delegate : public QStyledItemDelegate { - Delegate(QWidget *parent) : QStyledItemDelegate(parent) {} - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - }; - - void save(); - QTableWidget *table; -}; - -class SignalItemDelegate : public QStyledItemDelegate { -public: - SignalItemDelegate(QObject *parent); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - - QValidator *name_validator, *double_validator, *node_validator; - QFont label_font, minmax_font; - const int color_label_width = 18; - mutable QSize button_size; -}; - -class SignalView : public QFrame { - Q_OBJECT - -public: - SignalView(ChartsWidget *charts, QWidget *parent); - void setMessage(const MessageId &id); - void signalHovered(const cabana::Signal *sig); - void updateChartState(); - void selectSignal(const cabana::Signal *sig, bool expand = false); - void rowClicked(const QModelIndex &index); - SignalModel *model = nullptr; - -signals: - void highlight(const cabana::Signal *sig); - void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge); - -private: - void rowsChanged(); - void resizeEvent(QResizeEvent* event) override; - void updateToolBar(); - void setSparklineRange(int value); - void handleSignalAdded(MessageId id, const cabana::Signal *sig); - void handleSignalUpdated(const cabana::Signal *sig); - void updateState(const std::set *msgs = nullptr); - std::pair visibleSignalRange(); - - struct TreeView : public QTreeView { - TreeView(QWidget *parent) : QTreeView(parent) {} - void rowsInserted(const QModelIndex &parent, int start, int end) override { - ((SignalView *)parentWidget())->rowsChanged(); - // update widget geometries in QTreeView::rowsInserted - QTreeView::rowsInserted(parent, start, end); - } - void setModel(QAbstractItemModel *m) override { - QTreeView::setModel(m); - // Bypass the slow call to QTreeView::dataChanged. - QObject::disconnect(m, &QAbstractItemModel::dataChanged, this, nullptr); - QObject::connect(m, &QAbstractItemModel::dataChanged, this, - [this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); }); - } - void leaveEvent(QEvent *event) override { - emit static_cast(parentWidget())->highlight(nullptr); - QTreeView::leaveEvent(event); - } - }; - int max_value_width = 0; - int value_column_width = 0; - TreeView *tree; - QLabel *sparkline_label; - QSlider *sparkline_range_slider; - QLineEdit *filter_edit; - ChartsWidget *charts; - QLabel *signal_count_lb; - SignalItemDelegate *delegate; - Connections connections_; -}; diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc deleted file mode 100644 index 31532737f3..0000000000 --- a/openpilot/tools/cabana/streamselector.cc +++ /dev/null @@ -1,330 +0,0 @@ -#include "tools/cabana/streamselector.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/streams/devicestream.h" -#include "tools/cabana/streams/replaystream.h" -#include "tools/cabana/routesdialog.h" -#include "tools/cabana/utils/qtutil.h" - -// OpenReplayWidget - -OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QGridLayout *grid_layout = new QGridLayout(this); - grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); - grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); - route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route")); - auto browse_remote_btn = new QPushButton(tr("Remote route..."), this); - grid_layout->addWidget(browse_remote_btn, 0, 2); - auto browse_local_btn = new QPushButton(tr("Local route..."), this); - grid_layout->addWidget(browse_local_btn, 0, 3); - - QHBoxLayout *camera_layout = new QHBoxLayout(); - for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")}) - camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this))); - cameras[0]->setChecked(true); - camera_layout->addStretch(1); - grid_layout->addItem(camera_layout, 1, 1); - - setMinimumWidth(550); - QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() { - QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir)); - if (!dir.isEmpty()) { - route_edit->setText(dir); - settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string(); - } - }); - QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { - RoutesDialog route_dlg(this); - if (route_dlg.exec()) { - route_edit->setText(QString::fromStdString(route_dlg.route())); - } - }); -} - -AbstractStream *OpenReplayWidget::open() { - QString route = route_edit->text(); - QString data_dir; - if (int idx = route.lastIndexOf('/'); idx != -1 && util::file_exists(route.toStdString())) { - data_dir = route.mid(0, idx + 1); - route = route.mid(idx + 1); - } - - bool is_valid_format = Route::parseRoute(route.toStdString()).str.size() > 0; - if (!is_valid_format) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); - } else { - auto replay_stream = std::make_unique(); - Connection err = replay_stream->error.connect([](const std::string &msg) { - QMessageBox::warning(nullptr, tr("Error"), QString::fromStdString(msg)); - }); - uint32_t flags = REPLAY_FLAG_NONE; - if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; - if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; - if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; - - if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) { - return replay_stream.release(); - } - } - return nullptr; -} - -// OpenPandaWidget - -static const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; -static const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; - -OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - form_layout = new QFormLayout(this); - if (can && dynamic_cast(can) != nullptr) { - form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(QString::fromStdString(can->routeName())))); - form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.")); - QTimer::singleShot(0, [this]() { emit enableOpenButton(false); }); - return; - } - - QHBoxLayout *serial_layout = new QHBoxLayout(); - serial_layout->addWidget(serial_edit = new QComboBox()); - - QPushButton *refresh = new QPushButton(tr("Refresh")); - refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); - serial_layout->addWidget(refresh); - form_layout->addRow(tr("Serial"), serial_layout); - - QObject::connect(refresh, &QPushButton::clicked, this, &OpenPandaWidget::refreshSerials); - QObject::connect(serial_edit, &QComboBox::currentTextChanged, this, &OpenPandaWidget::buildConfigForm); - - // Populate serials - refreshSerials(); - buildConfigForm(); -} - -void OpenPandaWidget::refreshSerials() { - serial_edit->clear(); - for (auto serial : Panda::list()) { - serial_edit->addItem(QString::fromStdString(serial)); - } -} - -void OpenPandaWidget::buildConfigForm() { - for (int i = form_layout->rowCount() - 1; i > 0; --i) { - form_layout->removeRow(i); - } - - QString serial = serial_edit->currentText(); - bool has_fd = false; - bool has_panda = !serial.isEmpty(); - if (has_panda) { - try { - Panda panda(serial.toStdString()); - has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2); - } catch (const std::exception& e) { - fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData()); - has_panda = false; - } - } - - if (has_panda) { - config.serial = serial.toStdString(); - config.bus_config.resize(3); - for (int i = 0; i < config.bus_config.size(); i++) { - QHBoxLayout *bus_layout = new QHBoxLayout; - - // CAN Speed - bus_layout->addWidget(new QLabel(tr("CAN Speed (kbps):"))); - QComboBox *can_speed = new QComboBox; - for (int j = 0; j < std::size(speeds); j++) { - can_speed->addItem(QString::number(speeds[j])); - - if (data_speeds[j] == config.bus_config[i].can_speed_kbps) { - can_speed->setCurrentIndex(j); - } - } - QObject::connect(can_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].can_speed_kbps = speeds[index];}); - bus_layout->addWidget(can_speed); - - // CAN-FD Speed - if (has_fd) { - QCheckBox *enable_fd = new QCheckBox("CAN-FD"); - bus_layout->addWidget(enable_fd); - bus_layout->addWidget(new QLabel(tr("Data Speed (kbps):"))); - QComboBox *data_speed = new QComboBox; - for (int j = 0; j < std::size(data_speeds); j++) { - data_speed->addItem(QString::number(data_speeds[j])); - - if (data_speeds[j] == config.bus_config[i].data_speed_kbps) { - data_speed->setCurrentIndex(j); - } - } - - data_speed->setEnabled(false); - bus_layout->addWidget(data_speed); - - QObject::connect(data_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].data_speed_kbps = data_speeds[index];}); - QObject::connect(enable_fd, &QCheckBox::stateChanged, data_speed, &QComboBox::setEnabled); - QObject::connect(enable_fd, &QCheckBox::stateChanged, [=](int state) {config.bus_config[i].can_fd = (bool)state;}); - } - - form_layout->addRow(tr("Bus %1:").arg(i), bus_layout); - } - } else { - config.serial = ""; - form_layout->addWidget(new QLabel(tr("No panda found"))); - } -} - -AbstractStream *OpenPandaWidget::open() { - try { - return new PandaStream(config); - } catch (std::exception &e) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); - return nullptr; - } -} - -// OpenDeviceWidget - -OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QRadioButton *msgq = new QRadioButton(tr("MSGQ")); - QRadioButton *zmq = new QRadioButton(tr("ZMQ")); - ip_address = new QLineEdit(this); - ip_address->setPlaceholderText(tr("Enter device Ip Address")); - ip_address->setValidator(new IpAddressValidator(this)); - - group = new QButtonGroup(this); - group->addButton(msgq, 0); - group->addButton(zmq, 1); - - QFormLayout *form_layout = new QFormLayout(this); - form_layout->addRow(msgq); - form_layout->addRow(zmq, ip_address); - QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { - ip_address->setEnabled(button == zmq && checked); - }); - zmq->setChecked(true); -} - -AbstractStream *OpenDeviceWidget::open() { - std::string ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text().toStdString(); - bool msgq = group->checkedId() == 0; - return new DeviceStream(msgq ? "" : ip); -} - -#ifdef __linux__ -// OpenSocketCanWidget - -OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->addStretch(1); - - QFormLayout *form_layout = new QFormLayout(); - - QHBoxLayout *device_layout = new QHBoxLayout(); - device_edit = new QComboBox(); - device_edit->setFixedWidth(300); - device_layout->addWidget(device_edit); - - QPushButton *refresh = new QPushButton(tr("Refresh")); - refresh->setFixedWidth(100); - device_layout->addWidget(refresh); - form_layout->addRow(tr("Device"), device_layout); - main_layout->addLayout(form_layout); - - main_layout->addStretch(1); - - QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices); - QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); }); - - // Populate devices - refreshDevices(); -} - -void OpenSocketCanWidget::refreshDevices() { - device_edit->clear(); - // Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN) - std::error_code ec; - for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) { - std::ifstream type_file(entry.path() / "type"); - int type = 0; - if (type_file >> type && type == 280) { - device_edit->addItem(QString::fromStdString(entry.path().filename().string())); - } - } -} - -AbstractStream *OpenSocketCanWidget::open() { - try { - return new SocketCanStream(config); - } catch (std::exception &e) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what())); - return nullptr; - } -} -#endif - -// StreamSelector - -StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("Open stream")); - QVBoxLayout *layout = new QVBoxLayout(this); - tab = new QTabWidget(this); - layout->addWidget(tab); - - QHBoxLayout *dbc_layout = new QHBoxLayout(); - dbc_file = new QLineEdit(this); - dbc_file->setReadOnly(true); - dbc_file->setPlaceholderText(tr("Choose a dbc file to open")); - QPushButton *file_btn = new QPushButton(tr("Browse...")); - dbc_layout->addWidget(new QLabel(tr("dbc File"))); - dbc_layout->addWidget(dbc_file); - dbc_layout->addWidget(file_btn); - layout->addLayout(dbc_layout); - - QFrame *line = new QFrame(this); - line->setFrameStyle(QFrame::HLine | QFrame::Sunken); - layout->addWidget(line); - - btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); - layout->addWidget(btn_box); - - addStreamWidget(new OpenReplayWidget, tr("&Replay")); - addStreamWidget(new OpenPandaWidget, tr("&Panda")); -#ifdef __linux__ - if (SocketCanStream::available()) { - addStreamWidget(new OpenSocketCanWidget, tr("&SocketCAN")); - } -#endif - addStreamWidget(new OpenDeviceWidget, tr("&Device")); - - QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() { - setEnabled(false); - if (stream_ = ((AbstractOpenStreamWidget *)tab->currentWidget())->open(); stream_) { - accept(); - } - setEnabled(true); - }); - QObject::connect(file_btn, &QPushButton::clicked, [this]() { - QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)"); - if (!fn.isEmpty()) { - dbc_file->setText(fn); - settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string(); - } - }); -} - -void StreamSelector::addStreamWidget(AbstractOpenStreamWidget *w, const QString &title) { - tab->addTab(w, title); - auto open_btn = btn_box->button(QDialogButtonBox::Open); - QObject::connect(w, &AbstractOpenStreamWidget::enableOpenButton, open_btn, &QPushButton::setEnabled); -} diff --git a/openpilot/tools/cabana/streamselector.h b/openpilot/tools/cabana/streamselector.h deleted file mode 100644 index 1210806a3b..0000000000 --- a/openpilot/tools/cabana/streamselector.h +++ /dev/null @@ -1,99 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/streams/pandastream.h" -#ifdef __linux__ -#include "tools/cabana/streams/socketcanstream.h" -#endif - -class AbstractOpenStreamWidget : public QWidget { - Q_OBJECT -public: - AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {} - virtual AbstractStream *open() = 0; - -signals: - void enableOpenButton(bool); -}; - -class OpenReplayWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenReplayWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - QLineEdit *route_edit; - std::vector cameras; -}; - -class OpenPandaWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenPandaWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - void refreshSerials(); - void buildConfigForm(); - - QComboBox *serial_edit; - QFormLayout *form_layout; - PandaStreamConfig config = {}; -}; - -class OpenDeviceWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenDeviceWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - QLineEdit *ip_address; - QButtonGroup *group; -}; - -#ifdef __linux__ -// no Q_OBJECT: moc does not define __linux__ and would otherwise skip this class -class OpenSocketCanWidget : public AbstractOpenStreamWidget { -public: - OpenSocketCanWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - void refreshDevices(); - - QComboBox *device_edit; - SocketCanStreamConfig config = {}; -}; -#endif - -class StreamSelector : public QDialog { - Q_OBJECT - -public: - StreamSelector(QWidget *parent = nullptr); - void addStreamWidget(AbstractOpenStreamWidget *w, const QString &title); - QString dbcFile() const { return dbc_file->text(); } - AbstractStream *stream() const { return stream_; } - -private: - AbstractStream *stream_ = nullptr; - QLineEdit *dbc_file; - QTabWidget *tab; - QDialogButtonBox *btn_box; -}; diff --git a/openpilot/tools/cabana/tools/findsignal.cc b/openpilot/tools/cabana/tools/findsignal.cc deleted file mode 100644 index 2e533741cc..0000000000 --- a/openpilot/tools/cabana/tools/findsignal.cc +++ /dev/null @@ -1,286 +0,0 @@ -#include "tools/cabana/tools/findsignal.h" - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/utils/qtutil.h" - -// FindSignalModel - -QVariant FindSignalModel::headerData(int section, Qt::Orientation orientation, int role) const { - static QString titles[] = {"Id", "Start Bit, size", "(time, value)"}; - if (role != Qt::DisplayRole) return {}; - return orientation == Qt::Horizontal ? titles[section] : QString::number(section + 1); -} - -QVariant FindSignalModel::data(const QModelIndex &index, int role) const { - if (role == Qt::DisplayRole) { - const auto &s = filtered_signals[index.row()]; - switch (index.column()) { - case 0: return QString::fromStdString(s.id.toString()); - case 1: return QString("%1, %2").arg(s.sig.start_bit).arg(s.sig.size); - case 2: return s.values.join(" "); - } - } - return {}; -} - -void FindSignalModel::search(std::function cmp) { - beginResetModel(); - - std::mutex lock; - const auto prev_sigs = !histories.empty() ? histories.back() : initial_signals; - filtered_signals.clear(); - filtered_signals.reserve(prev_sigs.size()); - - unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency()); - size_t chunk = (prev_sigs.size() + num_threads - 1) / num_threads; - std::vector threads; - for (unsigned int t = 0; t < num_threads && t * chunk < (size_t)prev_sigs.size(); ++t) { - size_t start = t * chunk; - size_t end = std::min(start + chunk, (size_t)prev_sigs.size()); - threads.emplace_back([&, start, end]() { - for (size_t i = start; i < end; ++i) { - const auto &s = prev_sigs[i]; - const auto &events = can->events(s.id); - auto first = std::upper_bound(events.cbegin(), events.cend(), s.mono_time, CompareCanEvent()); - auto last = events.cend(); - if (last_time < std::numeric_limits::max()) { - last = std::upper_bound(events.cbegin(), events.cend(), last_time, CompareCanEvent()); - } - - auto it = std::find_if(first, last, [&](const CanEvent *e) { return cmp(get_raw_value(e->dat, e->size, s.sig)); }); - if (it != last) { - auto values = s.values; - values += QString("(%1, %2)").arg(can->toSeconds((*it)->mono_time), 0, 'f', 3).arg(get_raw_value((*it)->dat, (*it)->size, s.sig)); - std::lock_guard lk(lock); - filtered_signals.push_back({.id = s.id, .mono_time = (*it)->mono_time, .sig = s.sig, .values = values}); - } - } - }); - } - for (auto &th : threads) th.join(); - - histories.push_back(filtered_signals); - - endResetModel(); -} - -void FindSignalModel::undo() { - if (!histories.empty()) { - beginResetModel(); - histories.pop_back(); - filtered_signals.clear(); - if (!histories.empty()) filtered_signals = histories.back(); - endResetModel(); - } -} - -void FindSignalModel::reset() { - beginResetModel(); - histories.clear(); - filtered_signals.clear(); - initial_signals.clear(); - endResetModel(); -} - -// FindSignalDlg -FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags() | Qt::Window) { - setWindowTitle(tr("Find Signal")); - setAttribute(Qt::WA_DeleteOnClose); - QVBoxLayout *main_layout = new QVBoxLayout(this); - - // Messages group - message_group = new QGroupBox(tr("Messages"), this); - QFormLayout *message_layout = new QFormLayout(message_group); - message_layout->addRow(tr("Bus"), bus_edit = new QLineEdit()); - bus_edit->setPlaceholderText(tr("comma-separated values. Leave blank for all")); - message_layout->addRow(tr("Address"), address_edit = new QLineEdit()); - address_edit->setPlaceholderText(tr("comma-separated hex values. Leave blank for all")); - QHBoxLayout *hlayout = new QHBoxLayout(); - hlayout->addWidget(first_time_edit = new QLineEdit("0")); - hlayout->addWidget(new QLabel("-")); - hlayout->addWidget(last_time_edit = new QLineEdit("MAX")); - hlayout->addWidget(new QLabel("seconds")); - hlayout->addStretch(0); - message_layout->addRow(tr("Time"), hlayout); - - // Signal group - properties_group = new QGroupBox(tr("Signal")); - QFormLayout *property_layout = new QFormLayout(properties_group); - property_layout->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint); - - hlayout = new QHBoxLayout(); - hlayout->addWidget(min_size = new QSpinBox); - hlayout->addWidget(new QLabel("-")); - hlayout->addWidget(max_size = new QSpinBox); - hlayout->addWidget(litter_endian = new QCheckBox(tr("Little endian"))); - hlayout->addWidget(is_signed = new QCheckBox(tr("Signed"))); - hlayout->addStretch(0); - min_size->setRange(1, 64); - max_size->setRange(1, 64); - min_size->setValue(8); - max_size->setValue(8); - litter_endian->setChecked(true); - property_layout->addRow(tr("Size"), hlayout); - property_layout->addRow(tr("Factor"), factor_edit = new QLineEdit("1.0")); - property_layout->addRow(tr("Offset"), offset_edit = new QLineEdit("0.0")); - - // find group - QGroupBox *find_group = new QGroupBox(tr("Find signal"), this); - QVBoxLayout *vlayout = new QVBoxLayout(find_group); - hlayout = new QHBoxLayout(); - hlayout->addWidget(new QLabel(tr("Value"))); - hlayout->addWidget(compare_cb = new QComboBox(this)); - hlayout->addWidget(value1 = new QLineEdit); - hlayout->addWidget(to_label = new QLabel("-")); - hlayout->addWidget(value2 = new QLineEdit); - hlayout->addWidget(undo_btn = new QPushButton(tr("Undo prev find"), this)); - hlayout->addWidget(search_btn = new QPushButton(tr("Find"))); - hlayout->addWidget(reset_btn = new QPushButton(tr("Reset"), this)); - vlayout->addLayout(hlayout); - - compare_cb->addItems({"=", ">", ">=", "!=", "<", "<=", "between"}); - value1->setFocus(Qt::OtherFocusReason); - value2->setVisible(false); - to_label->setVisible(false); - undo_btn->setEnabled(false); - reset_btn->setEnabled(false); - - auto double_validator = new DoubleValidator(this); - for (auto edit : {value1, value2, factor_edit, offset_edit, first_time_edit, last_time_edit}) { - edit->setValidator(double_validator); - } - - vlayout->addWidget(view = new QTableView(this)); - view->setContextMenuPolicy(Qt::CustomContextMenu); - view->horizontalHeader()->setStretchLastSection(true); - view->horizontalHeader()->setSelectionMode(QAbstractItemView::NoSelection); - view->setSelectionBehavior(QAbstractItemView::SelectRows); - view->setModel(model = new FindSignalModel(this)); - - hlayout = new QHBoxLayout(); - hlayout->addWidget(message_group); - hlayout->addWidget(properties_group); - main_layout->addLayout(hlayout); - main_layout->addWidget(find_group); - main_layout->addWidget(stats_label = new QLabel()); - - setMinimumSize({700, 650}); - QObject::connect(search_btn, &QPushButton::clicked, this, &FindSignalDlg::search); - QObject::connect(undo_btn, &QPushButton::clicked, model, &FindSignalModel::undo); - QObject::connect(model, &QAbstractItemModel::modelReset, this, &FindSignalDlg::modelReset); - QObject::connect(reset_btn, &QPushButton::clicked, model, &FindSignalModel::reset); - QObject::connect(view, &QTableView::customContextMenuRequested, this, &FindSignalDlg::customMenuRequested); - QObject::connect(view, &QTableView::doubleClicked, [this](const QModelIndex &index) { - if (index.isValid()) emit openMessage(model->filtered_signals[index.row()].id); - }); - QObject::connect(compare_cb, qOverload(&QComboBox::currentIndexChanged), [=](int index) { - to_label->setVisible(index == compare_cb->count() - 1); - value2->setVisible(index == compare_cb->count() - 1); - }); -} - -void FindSignalDlg::search() { - if (model->histories.empty()) { - setInitialSignals(); - } - auto v1 = value1->text().toDouble(); - auto v2 = value2->text().toDouble(); - std::function cmp = nullptr; - switch (compare_cb->currentIndex()) { - case 0: cmp = [v1](double v) { return v == v1;}; break; - case 1: cmp = [v1](double v) { return v > v1;}; break; - case 2: cmp = [v1](double v) { return v >= v1;}; break; - case 3: cmp = [v1](double v) { return v != v1;}; break; - case 4: cmp = [v1](double v) { return v < v1;}; break; - case 5: cmp = [v1](double v) { return v <= v1;}; break; - case 6: cmp = [v1, v2](double v) { return v >= v1 && v <= v2;}; break; - } - properties_group->setEnabled(false); - message_group->setEnabled(false); - search_btn->setEnabled(false); - stats_label->setVisible(false); - search_btn->setText("Finding ...."); - QTimer::singleShot(0, this, [=]() { model->search(cmp); }); -} - -void FindSignalDlg::setInitialSignals() { - std::set buses; - for (auto bus : bus_edit->text().trimmed().split(",")) { - bus = bus.trimmed(); - if (!bus.isEmpty()) buses.insert(bus.toUShort()); - } - - std::set addresses; - for (auto addr : address_edit->text().trimmed().split(",")) { - addr = addr.trimmed(); - if (!addr.isEmpty()) addresses.insert(addr.toULong(nullptr, 16)); - } - - cabana::Signal sig{}; - sig.is_little_endian = litter_endian->isChecked(); - sig.is_signed = is_signed->isChecked(); - sig.factor = factor_edit->text().toDouble(); - sig.offset = offset_edit->text().toDouble(); - - double first_time_val = first_time_edit->text().toDouble(); - double last_time_val = last_time_edit->text().toDouble(); - auto [first_sec, last_sec] = std::minmax(first_time_val, last_time_val); - uint64_t first_time = can->toMonoTime(first_sec); - model->last_time = std::numeric_limits::max(); - if (last_sec > 0) { - model->last_time = can->toMonoTime(last_sec); - } - model->initial_signals.clear(); - - for (const auto &[id, m] : can->lastMessages()) { - if ((buses.empty() || buses.count(id.source)) && (addresses.empty() || addresses.count(id.address))) { - const auto &events = can->events(id); - auto e = std::lower_bound(events.cbegin(), events.cend(), first_time, CompareCanEvent()); - if (e != events.cend()) { - const int total_size = m.dat.size() * 8; - for (int size = min_size->value(); size <= max_size->value(); ++size) { - for (int start = 0; start <= total_size - size; ++start) { - FindSignalModel::SearchSignal s{.id = id, .mono_time = first_time, .sig = sig}; - s.sig.start_bit = start; - s.sig.size = size; - updateMsbLsb(s.sig); - s.value = get_raw_value((*e)->dat, (*e)->size, s.sig); - model->initial_signals.push_back(s); - } - } - } - } - } -} - -void FindSignalDlg::modelReset() { - properties_group->setEnabled(model->histories.empty()); - message_group->setEnabled(model->histories.empty()); - search_btn->setText(model->histories.empty() ? tr("Find") : tr("Find Next")); - reset_btn->setEnabled(!model->histories.empty()); - undo_btn->setEnabled(model->histories.size() > 1); - search_btn->setEnabled(model->rowCount() > 0 || model->histories.empty()); - stats_label->setVisible(true); - stats_label->setText(tr("%1 matches. right click on an item to create signal. double click to open message").arg(model->filtered_signals.size())); -} - -void FindSignalDlg::customMenuRequested(const QPoint &pos) { - if (auto index = view->indexAt(pos); index.isValid()) { - QMenu menu(this); - menu.addAction(tr("Create Signal")); - if (menu.exec(view->mapToGlobal(pos))) { - auto &s = model->filtered_signals[index.row()]; - UndoStack::instance()->push(new AddSigCommand(s.id, s.sig)); - emit openMessage(s.id); - } - } -} diff --git a/openpilot/tools/cabana/tools/findsignal.h b/openpilot/tools/cabana/tools/findsignal.h deleted file mode 100644 index b7cae73b49..0000000000 --- a/openpilot/tools/cabana/tools/findsignal.h +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/commands.h" -#include "tools/cabana/settings.h" - -class FindSignalModel : public QAbstractTableModel { -public: - struct SearchSignal { - MessageId id = {}; - uint64_t mono_time = 0; - cabana::Signal sig = {}; - double value = 0.; - QStringList values; - }; - - FindSignalModel(QObject *parent) : QAbstractTableModel(parent) {} - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 3; } - int rowCount(const QModelIndex &parent = QModelIndex()) const override { return std::min((int)filtered_signals.size(), 300); } - void search(std::function cmp); - void reset(); - void undo(); - - std::vector filtered_signals; - std::vector initial_signals; - std::vector> histories; - uint64_t last_time = std::numeric_limits::max(); -}; - -class FindSignalDlg : public QDialog { - Q_OBJECT -public: - FindSignalDlg(QWidget *parent); - -signals: - void openMessage(const MessageId &id); - -private: - void search(); - void modelReset(); - void setInitialSignals(); - void customMenuRequested(const QPoint &pos); - - QLineEdit *value1, *value2, *factor_edit, *offset_edit; - QLineEdit *bus_edit, *address_edit, *first_time_edit, *last_time_edit; - QComboBox *compare_cb; - QSpinBox *min_size, *max_size; - QCheckBox *litter_endian, *is_signed; - QPushButton *search_btn, *reset_btn, *undo_btn; - QGroupBox *properties_group, *message_group; - QTableView *view; - QLabel *to_label, *stats_label; - FindSignalModel *model; -}; diff --git a/openpilot/tools/cabana/tools/findsimilarbits.cc b/openpilot/tools/cabana/tools/findsimilarbits.cc deleted file mode 100644 index 8062b61199..0000000000 --- a/openpilot/tools/cabana/tools/findsimilarbits.cc +++ /dev/null @@ -1,161 +0,0 @@ -#include "tools/cabana/tools/findsimilarbits.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" - -FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags() | Qt::Window) { - setWindowTitle(tr("Find similar bits")); - setAttribute(Qt::WA_DeleteOnClose); - - QVBoxLayout *main_layout = new QVBoxLayout(this); - - QHBoxLayout *src_layout = new QHBoxLayout(); - src_bus_combo = new QComboBox(this); - find_bus_combo = new QComboBox(this); - for (auto cb : {src_bus_combo, find_bus_combo}) { - for (uint8_t bus : can->sources) { - cb->addItem(QString::number(bus), bus); - } - } - - msg_cb = new QComboBox(this); - // TODO: update when src_bus_combo changes - for (auto &[address, msg] : dbc()->getMessages(-1)) { - msg_cb->addItem(QString::fromStdString(msg.name), address); - } - msg_cb->model()->sort(0); - msg_cb->setCurrentIndex(0); - - byte_idx_sb = new QSpinBox(this); - byte_idx_sb->setFixedWidth(50); - byte_idx_sb->setRange(0, 63); - - bit_idx_sb = new QSpinBox(this); - bit_idx_sb->setFixedWidth(50); - bit_idx_sb->setRange(0, 7); - - src_layout->addWidget(new QLabel(tr("Bus"))); - src_layout->addWidget(src_bus_combo); - src_layout->addWidget(msg_cb); - src_layout->addWidget(new QLabel(tr("Byte Index"))); - src_layout->addWidget(byte_idx_sb); - src_layout->addWidget(new QLabel(tr("Bit Index"))); - src_layout->addWidget(bit_idx_sb); - src_layout->addStretch(0); - - QHBoxLayout *find_layout = new QHBoxLayout(); - find_layout->addWidget(new QLabel(tr("Bus"))); - find_layout->addWidget(find_bus_combo); - find_layout->addWidget(new QLabel(tr("Equal"))); - equal_combo = new QComboBox(this); - equal_combo->addItems({"Yes", "No"}); - find_layout->addWidget(equal_combo); - min_msgs = new QLineEdit(this); - min_msgs->setValidator(new QIntValidator(this)); - min_msgs->setText("100"); - find_layout->addWidget(new QLabel(tr("Min msg count"))); - find_layout->addWidget(min_msgs); - search_btn = new QPushButton(tr("&Find"), this); - find_layout->addWidget(search_btn); - find_layout->addStretch(0); - - QGridLayout *grid_layout = new QGridLayout(); - grid_layout->addWidget(new QLabel("Find From:"), 0, 0); - grid_layout->addLayout(src_layout, 0, 1); - grid_layout->addWidget(new QLabel("Find In:"), 1, 0); - grid_layout->addLayout(find_layout, 1, 1); - main_layout->addLayout(grid_layout); - - table = new QTableWidget(this); - table->setSelectionBehavior(QAbstractItemView::SelectRows); - table->setSelectionMode(QAbstractItemView::SingleSelection); - table->setEditTriggers(QAbstractItemView::NoEditTriggers); - table->horizontalHeader()->setStretchLastSection(true); - main_layout->addWidget(table); - - setMinimumSize({700, 500}); - QObject::connect(search_btn, &QPushButton::clicked, this, &FindSimilarBitsDlg::find); - QObject::connect(table, &QTableWidget::doubleClicked, [this](const QModelIndex &index) { - if (index.isValid()) { - MessageId msg_id = {.source = (uint8_t)find_bus_combo->currentData().toUInt(), .address = table->item(index.row(), 0)->text().toUInt(0, 16)}; - emit openMessage(msg_id); - } - }); -} - -void FindSimilarBitsDlg::find() { - search_btn->setEnabled(false); - table->clear(); - uint32_t selected_address = msg_cb->currentData().toUInt(); - auto msg_mismatched = calcBits(src_bus_combo->currentText().toUInt(), selected_address, byte_idx_sb->value(), bit_idx_sb->value(), - find_bus_combo->currentText().toUInt(), equal_combo->currentIndex() == 0, min_msgs->text().toInt()); - table->setRowCount(msg_mismatched.size()); - table->setColumnCount(6); - table->setHorizontalHeaderLabels({"address", "byte idx", "bit idx", "mismatches", "total msgs", "% mismatched"}); - for (int i = 0; i < msg_mismatched.size(); ++i) { - auto &m = msg_mismatched[i]; - table->setItem(i, 0, new QTableWidgetItem(QString("%1").arg(m.address, 1, 16))); - table->setItem(i, 1, new QTableWidgetItem(QString::number(m.byte_idx))); - table->setItem(i, 2, new QTableWidgetItem(QString::number(m.bit_idx))); - table->setItem(i, 3, new QTableWidgetItem(QString::number(m.mismatches))); - table->setItem(i, 4, new QTableWidgetItem(QString::number(m.total))); - table->setItem(i, 5, new QTableWidgetItem(QString::number(m.perc, 'f', 2))); - } - search_btn->setEnabled(true); -} - -std::vector FindSimilarBitsDlg::calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, - int bit_idx, uint8_t find_bus, bool equal, int min_msgs_cnt) { - std::unordered_map> mismatches; - std::unordered_map msg_count; - const auto &events = can->allEvents(); - int bit_to_find = -1; - for (const CanEvent *e : events) { - if (e->src == bus) { - if (e->address == selected_address && e->size > byte_idx) { - bit_to_find = ((e->dat[byte_idx] >> (7 - bit_idx)) & 1) != 0; - } - } - if (e->src == find_bus) { - ++msg_count[e->address]; - if (bit_to_find == -1) continue; - - auto &mismatched = mismatches[e->address]; - if (mismatched.size() < e->size * 8) { - mismatched.resize(e->size * 8); - } - for (int i = 0; i < e->size; ++i) { - for (int j = 0; j < 8; ++j) { - int bit = ((e->dat[i] >> (7 - j)) & 1) != 0; - mismatched[i * 8 + j] += equal ? (bit != bit_to_find) : (bit == bit_to_find); - } - } - } - } - - std::vector result; - result.reserve(mismatches.size()); - for (auto it = mismatches.begin(); it != mismatches.end(); ++it) { - if (auto cnt = msg_count[it->first]; cnt > (uint32_t)min_msgs_cnt) { - auto &mismatched = it->second; - for (int i = 0; i < (int)mismatched.size(); ++i) { - if (float perc = (mismatched[i] / (double)cnt) * 100; perc < 50) { - result.push_back({it->first, (uint32_t)i / 8, (uint32_t)i % 8, mismatched[i], cnt, perc}); - } - } - } - } - std::sort(result.begin(), result.end(), [](auto &l, auto &r) { return l.perc < r.perc; }); - return result; -} diff --git a/openpilot/tools/cabana/tools/findsimilarbits.h b/openpilot/tools/cabana/tools/findsimilarbits.h deleted file mode 100644 index 3451360654..0000000000 --- a/openpilot/tools/cabana/tools/findsimilarbits.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" - -class FindSimilarBitsDlg : public QDialog { - Q_OBJECT - -public: - FindSimilarBitsDlg(QWidget *parent); - -signals: - void openMessage(const MessageId &msg_id); - -private: - struct mismatched_struct { - uint32_t address, byte_idx, bit_idx, mismatches, total; - float perc; - }; - std::vector calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, uint8_t find_bus, - bool equal, int min_msgs_cnt); - void find(); - - QTableWidget *table; - QComboBox *src_bus_combo, *find_bus_combo, *msg_cb, *equal_combo; - QSpinBox *byte_idx_sb, *bit_idx_sb; - QPushButton *search_btn; - QLineEdit *min_msgs; -}; diff --git a/openpilot/tools/cabana/tools/routeinfo.cc b/openpilot/tools/cabana/tools/routeinfo.cc deleted file mode 100644 index 1037d4a206..0000000000 --- a/openpilot/tools/cabana/tools/routeinfo.cc +++ /dev/null @@ -1,40 +0,0 @@ -#include "tools/cabana/tools/routeinfo.h" -#include -#include -#include -#include -#include "tools/cabana/streams/replaystream.h" - -RouteInfoDlg::RouteInfoDlg(QWidget *parent) : QDialog(parent) { - auto *replay = dynamic_cast(can)->getReplay(); - setWindowTitle(tr("Route: %1").arg(QString::fromStdString(replay->route().name()))); - - auto *table = new QTableWidget(replay->route().segments().size(), 7, this); - table->setToolTip(tr("Click on a row to seek to the corresponding segment.")); - table->setEditTriggers(QAbstractItemView::NoEditTriggers); - table->setSelectionBehavior(QAbstractItemView::SelectRows); - table->setSelectionMode(QAbstractItemView::SingleSelection); - table->setHorizontalHeaderLabels({"", "rlog", "narrow road", "wide road", "driver", "qlog", "qcam"}); - table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - table->verticalHeader()->setVisible(false); - table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - int row = 0; - for (const auto &[seg_num, seg] : replay->route().segments()) { - table->setItem(row, 0, new QTableWidgetItem(QString::number(seg_num))); - table->setItem(row, 1, new QTableWidgetItem(seg.rlog.empty() ? "--" : "Yes")); - table->setItem(row, 2, new QTableWidgetItem(seg.narrow_road_cam.empty() ? "--" : "Yes")); - table->setItem(row, 3, new QTableWidgetItem(seg.wide_road_cam.empty() ? "--" : "Yes")); - table->setItem(row, 4, new QTableWidgetItem(seg.cabin_cam.empty() ? "--" : "Yes")); - table->setItem(row, 5, new QTableWidgetItem(seg.qlog.empty() ? "--" : "Yes")); - table->setItem(row, 6, new QTableWidgetItem(seg.qcamera.empty() ? "--" : "Yes")); - ++row; - } - table->setMinimumWidth(table->horizontalHeader()->length() + table->verticalScrollBar()->sizeHint().width()); - table->setMinimumHeight(table->rowHeight(0) * std::min(table->rowCount(), 13) + table->horizontalHeader()->height() + table->frameWidth() * 2); - - connect(table, &QTableWidget::itemClicked, [](QTableWidgetItem *item) { can->seekTo(item->row() * 60.0); }); - - QVBoxLayout *layout = new QVBoxLayout(this); - layout->addWidget(table); -} diff --git a/openpilot/tools/cabana/tools/routeinfo.h b/openpilot/tools/cabana/tools/routeinfo.h deleted file mode 100644 index 36f32b4bf4..0000000000 --- a/openpilot/tools/cabana/tools/routeinfo.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include - -class RouteInfoDlg : public QDialog { - Q_OBJECT -public: - RouteInfoDlg(QWidget *parent = nullptr); -}; diff --git a/openpilot/tools/cabana/utils/elidedlabel.cc b/openpilot/tools/cabana/utils/elidedlabel.cc deleted file mode 100644 index 629a108b16..0000000000 --- a/openpilot/tools/cabana/utils/elidedlabel.cc +++ /dev/null @@ -1,29 +0,0 @@ -#include "tools/cabana/utils/elidedlabel.h" -#include -#include - -ElidedLabel::ElidedLabel(QWidget *parent) : ElidedLabel({}, parent) {} - -ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) { - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - setMinimumWidth(1); -} - -void ElidedLabel::resizeEvent(QResizeEvent* event) { - QLabel::resizeEvent(event); - lastText_ = elidedText_ = ""; -} - -void ElidedLabel::paintEvent(QPaintEvent *event) { - const QString curText = text(); - if (curText != lastText_) { - elidedText_ = fontMetrics().elidedText(curText, Qt::ElideRight, contentsRect().width()); - lastText_ = curText; - } - - QPainter painter(this); - drawFrame(&painter); - QStyleOption opt; - opt.initFrom(this); - style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole()); -} diff --git a/openpilot/tools/cabana/utils/elidedlabel.h b/openpilot/tools/cabana/utils/elidedlabel.h deleted file mode 100644 index 577eea12a0..0000000000 --- a/openpilot/tools/cabana/utils/elidedlabel.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include - -class ElidedLabel : public QLabel { - Q_OBJECT - -public: - explicit ElidedLabel(QWidget *parent = 0); - explicit ElidedLabel(const QString &text, QWidget *parent = 0); - -signals: - void clicked(); - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent* event) override; - void mouseReleaseEvent(QMouseEvent *event) override { - if (rect().contains(event->pos())) { - emit clicked(); - } - } - QString lastText_, elidedText_; -}; diff --git a/openpilot/tools/cabana/utils/qtutil.cc b/openpilot/tools/cabana/utils/qtutil.cc deleted file mode 100644 index 040c90c624..0000000000 --- a/openpilot/tools/cabana/utils/qtutil.cc +++ /dev/null @@ -1,236 +0,0 @@ -#include "tools/cabana/utils/qtutil.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -// MessageBytesDelegate - -MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) - : font_metrics(QApplication::font()), multiple_lines(multiple_lines), QStyledItemDelegate(parent) { - fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); - for (int i = 0; i < 256; ++i) { - hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); - hex_text_table[i].prepare({}, fixed_font); - } - h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; - v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; -} - -QSize MessageBytesDelegate::sizeForBytes(int n) const { - int rows = multiple_lines ? std::max(1, n / 8) : 1; - return {(n / rows) * byte_size.width() + h_margin * 2, rows * byte_size.height() + v_margin * 2}; -} - -QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - auto data = index.data(BytesRole); - return sizeForBytes(data.isValid() ? static_cast *>(data.value())->size() : 0); -} - -void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); - } - - QRect item_rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); - QColor highlighted_color = option.palette.color(QPalette::HighlightedText); - auto text_color = index.data(Qt::ForegroundRole).value(); - bool inactive = text_color.isValid(); - if (!inactive) { - text_color = option.palette.color(QPalette::Text); - } - auto data = index.data(BytesRole); - if (!data.isValid()) { - painter->setFont(option.font); - painter->setPen(option.state & QStyle::State_Selected ? highlighted_color : text_color); - QString text = font_metrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, item_rect.width()); - painter->drawText(item_rect, Qt::AlignLeft | Qt::AlignVCenter, text); - return; - } - - // Paint hex column - const auto &bytes = *static_cast *>(data.value()); - const auto &colors = *static_cast *>(index.data(ColorsRole).value()); - - painter->setFont(fixed_font); - const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); - const QPoint pt = item_rect.topLeft(); - for (int i = 0; i < bytes.size(); ++i) { - int row = !multiple_lines ? 0 : i / 8; - int column = !multiple_lines ? i : i % 8; - QRect r({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); - - if (!inactive && 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, toQColor(colors[i])); - } else { - painter->setPen(text_pen); - } - utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); - } -} - -// TabBar - -int TabBar::addTab(const QString &text) { - int index = QTabBar::addTab(text); - QToolButton *btn = new ToolButton("x", tr("Close Tab")); - int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, btn); - int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, btn); - btn->setFixedSize({width, height}); - setTabButton(index, QTabBar::RightSide, btn); - QObject::connect(btn, &QToolButton::clicked, this, &TabBar::closeTabClicked); - return index; -} - -void TabBar::closeTabClicked() { - QObject *object = sender(); - for (int i = 0; i < count(); ++i) { - if (tabButton(i, QTabBar::RightSide) == object) { - emit tabCloseRequested(i); - break; - } - } -} - -// validators - -static QValidator::State toQtState(ValidState s) { - switch (s) { - case ValidState::Acceptable: return QValidator::Acceptable; - case ValidState::Intermediate: return QValidator::Intermediate; - default: return QValidator::Invalid; - } -} - -QValidator::State NameValidator::validate(QString &input, int &pos) const { - std::string s = input.toStdString(); - auto state = validateName(s); - input = QString::fromStdString(s); - return toQtState(state); -} - -QValidator::State NodeValidator::validate(QString &input, int &pos) const { - return toQtState(validateNodes(input.toStdString())); -} - -QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { - return toQtState(validateNonWhitespace(input.toStdString())); -} - -QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { - return toQtState(validateIpAddress(input.toStdString())); -} - -QValidator::State DoubleValidator::validate(QString &input, int &pos) const { - return toQtState(validateDouble(input.toLatin1().toStdString())); -} - -namespace utils { - -bool isDarkTheme() { - QColor windowColor = QApplication::palette().color(QPalette::Window); - return windowColor.lightness() < 128; -} - -QPixmap icon(const QString &id) { - bool dark_theme = isDarkTheme(); - - QPixmap pm; - QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); - if (!QPixmapCache::find(key, &pm)) { - pm = bootstrapPixmap(id); - if (dark_theme) { - QPainter p(&pm); - p.setCompositionMode(QPainter::CompositionMode_SourceIn); - p.fillRect(pm.rect(), QColor("#bbbbbb")); - } - QPixmapCache::insert(key, pm); - } - return pm; -} - -void setTheme(int theme) { - auto style = QApplication::style(); - if (!style) return; - - static int prev_theme = 0; - if (theme != prev_theme) { - prev_theme = theme; - QPalette new_palette; - if (theme == DARK_THEME) { - new_palette.setColor(QPalette::Window, toQColor(DarkTheme::window)); - new_palette.setColor(QPalette::WindowText, toQColor(DarkTheme::window_text)); - new_palette.setColor(QPalette::Base, toQColor(DarkTheme::base)); - new_palette.setColor(QPalette::AlternateBase, toQColor(DarkTheme::base)); - new_palette.setColor(QPalette::ToolTipBase, toQColor(DarkTheme::base)); - new_palette.setColor(QPalette::ToolTipText, toQColor(DarkTheme::tooltip_text)); - new_palette.setColor(QPalette::Text, toQColor(DarkTheme::text)); - new_palette.setColor(QPalette::Button, toQColor(DarkTheme::button)); - new_palette.setColor(QPalette::ButtonText, toQColor(DarkTheme::window_text)); - new_palette.setColor(QPalette::Highlight, toQColor(DarkTheme::highlight)); - new_palette.setColor(QPalette::HighlightedText, toQColor(DarkTheme::window_text)); - new_palette.setColor(QPalette::BrightText, toQColor(DarkTheme::bright_text)); - new_palette.setColor(QPalette::Disabled, QPalette::ButtonText, toQColor(DarkTheme::disabled_text)); - new_palette.setColor(QPalette::Disabled, QPalette::WindowText, toQColor(DarkTheme::disabled_text)); - new_palette.setColor(QPalette::Disabled, QPalette::Text, toQColor(DarkTheme::disabled_text)); - new_palette.setColor(QPalette::Light, toQColor(DarkTheme::light)); - new_palette.setColor(QPalette::Dark, toQColor(DarkTheme::dark)); - } else { - new_palette = style->standardPalette(); - } - qApp->setPalette(new_palette); - style->polish(qApp); - for (auto w : QApplication::allWidgets()) { - w->setPalette(new_palette); - } - } -} - -} // namespace utils - -void sigTermHandler(int s) { - std::signal(s, SIG_DFL); - qApp->quit(); -} - -void initApp(int argc, char *argv[], bool disable_hidpi) { - // setup signal handlers to exit gracefully - std::signal(SIGINT, sigTermHandler); - std::signal(SIGTERM, sigTermHandler); - -#ifdef __APPLE__ - // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering - QApplication tmp(argc, argv); - if (disable_hidpi) { - qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); - } -#endif - - qputenv("QT_DBL_CLICK_DIST", "150"); - // ensure the current dir matches the exectuable's directory - std::error_code ec; - std::filesystem::current_path(executableDir(), ec); -} - -QPixmap bootstrapPixmap(const QString &id) { - QPixmap pixmap; - const std::string svg = utils::bootstrapSvg(id.toStdString()); - if (!svg.empty()) { - pixmap.loadFromData((const uchar *)svg.data(), svg.size(), "svg"); - } - return pixmap; -} diff --git a/openpilot/tools/cabana/utils/qtutil.h b/openpilot/tools/cabana/utils/qtutil.h deleted file mode 100644 index db0417ec9a..0000000000 --- a/openpilot/tools/cabana/utils/qtutil.h +++ /dev/null @@ -1,139 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/core/observable.h" -#include "tools/cabana/dbc/dbc.h" -#include "tools/cabana/settings.h" -#include "tools/cabana/utils/strings.h" -#include "tools/cabana/utils/util.h" - -// needed by QVariant::fromValue() in the Qt views; goes away with QVariant -Q_DECLARE_METATYPE(MessageId) -Q_DECLARE_METATYPE(ValueDescription) - -inline QColor toQColor(const CabanaColor &color) { - return QColor(color.r, color.g, color.b, color.a); -} - -class LogSlider : public QSlider { - Q_OBJECT - -public: - LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : scale(factor), QSlider(orientation, parent) {} - - void setRange(double min, double max) { - scale.setRange(min, max); - QSlider::setRange(min, max); - setValue(QSlider::value()); - } - int value() const { return scale.value(QSlider::value(), minimum(), maximum()); } - void setValue(int v) { QSlider::setValue(scale.position(v, minimum(), maximum())); } - -private: - LogScale scale; -}; - -enum { - ColorsRole = Qt::UserRole + 1, - BytesRole = Qt::UserRole + 2 -}; - -class MessageBytesDelegate : public QStyledItemDelegate { - Q_OBJECT -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 multipleLines() const { return multiple_lines; } - void setMultipleLines(bool v) { multiple_lines = v; } - QSize sizeForBytes(int n) const; - -private: - std::array hex_text_table; - QFontMetrics font_metrics; - QFont fixed_font; - QSize byte_size = {}; - bool multiple_lines = false; - int h_margin, v_margin; -}; - -// QValidator wrappers around the std::string validators in util.h -#define CABANA_VALIDATOR(Name) \ - class Name : public QValidator { \ - Q_OBJECT \ - public: \ - Name(QObject *parent = nullptr) : QValidator(parent) {} \ - QValidator::State validate(QString &input, int &pos) const override; \ - }; -CABANA_VALIDATOR(NameValidator) -CABANA_VALIDATOR(NodeValidator) -CABANA_VALIDATOR(NonWhitespaceValidator) -CABANA_VALIDATOR(IpAddressValidator) -CABANA_VALIDATOR(DoubleValidator) -#undef CABANA_VALIDATOR - -namespace utils { - -QPixmap icon(const QString &id); -bool isDarkTheme(); -void setTheme(int theme); -inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { - auto size = (r.size() - text.size()) / 2; - p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); -} -inline auto qbytes(const std::vector &dat) { - return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); -} - -} - -class ToolButton : public QToolButton { - Q_OBJECT -public: - ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) { - setIcon(icon); - setToolTip(tooltip); - setAutoRaise(true); - const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); - setIconSize({metric, metric}); - theme = settings.theme; - settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); - } - void setIcon(const QString &icon) { - icon_str = icon; - QToolButton::setIcon(utils::icon(icon_str)); - } - -private: - void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } - Connection settings_connection_; - QString icon_str; - int theme; -}; - -class TabBar : public QTabBar { - Q_OBJECT - -public: - TabBar(QWidget *parent) : QTabBar(parent) {} - int addTab(const QString &text); - -private: - void closeTabClicked(); -}; - -void initApp(int argc, char *argv[], bool disable_hidpi = true); -QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc deleted file mode 100644 index f7b1b41628..0000000000 --- a/openpilot/tools/cabana/videowidget.cc +++ /dev/null @@ -1,434 +0,0 @@ -#include "tools/cabana/videowidget.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/tools/routeinfo.h" -#include "tools/cabana/utils/qtutil.h" - -const int MIN_VIDEO_HEIGHT = 100; -const int THUMBNAIL_MARGIN = 3; - -// Indexed by TimelineType: None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserBookmark -static const QColor timeline_colors[] = { - QColor(111, 143, 175), - QColor(0, 163, 108), - Qt::green, - QColor(255, 195, 0), - QColor(199, 0, 57), - Qt::magenta, -}; - -static Replay *getReplay() { - auto stream = dynamic_cast(can); - return stream ? stream->getReplay() : nullptr; -} - -VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { - setFrameStyle(QFrame::StyledPanel | QFrame::Plain); - auto main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->setSpacing(0); - if (!can->liveStreaming()) - main_layout->addWidget(createCameraWidget()); - - createPlaybackController(); - - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - connections_.push_back(can->paused.connect([this]() { updatePlayBtnState(); })); - connections_.push_back(can->resume.connect([this]() { updatePlayBtnState(); })); - connections_.push_back(can->msgsReceived.connect([this](const std::set *, bool) { updateState(); })); - connections_.push_back(can->seeking.connect([this](double) { updateState(); })); - connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { timeRangeChanged(); })); - - updatePlayBtnState(); - setWhatsThis(tr(R"( - Video
- - Timeline color - - - - - - - -
■ Disengaged ■ Engaged
■ User Flag ■ Info
■ Warning ■ Critical
- Shortcuts
- Pause/Resume:  space  - )").arg(timeline_colors[(int)TimelineType::None].name(), - timeline_colors[(int)TimelineType::Engaged].name(), - timeline_colors[(int)TimelineType::UserBookmark].name(), - timeline_colors[(int)TimelineType::AlertInfo].name(), - timeline_colors[(int)TimelineType::AlertWarning].name(), - timeline_colors[(int)TimelineType::AlertCritical].name())); -} - -void VideoWidget::createPlaybackController() { - QToolBar *toolbar = new QToolBar(this); - layout()->addWidget(toolbar); - - int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); - toolbar->setIconSize({icon_size, icon_size}); - - toolbar->addAction(utils::icon("rewind"), tr("Seek backward"), []() { can->seekTo(can->currentSec() - 1); }); - play_toggle_action = toolbar->addAction(utils::icon("play"), tr("Play"), []() { can->pause(!can->isPaused()); }); - toolbar->addAction(utils::icon("fast-forward"), tr("Seek forward"), []() { can->seekTo(can->currentSec() + 1); }); - - if (can->liveStreaming()) { - skip_to_end_action = toolbar->addAction(utils::icon("skip-end"), tr("Skip to the end"), this, [this]() { - // set speed to 1.0 - speed_btn->menu()->actions()[7]->setChecked(true); - can->pause(false); - can->seekTo(can->maxSeconds() + 1); - }); - } - - time_display_action = toolbar->addAction("", this, [this]() { - settings.absolute_time = !settings.absolute_time; - time_display_action->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); - updateState(); - }); - - QWidget *spacer = new QWidget(); - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->addWidget(spacer); - - if (!can->liveStreaming()) { - toolbar->addAction(utils::icon("repeat"), tr("Loop playback"), this, &VideoWidget::loopPlaybackClicked); - createSpeedDropdown(toolbar); - toolbar->addSeparator(); - toolbar->addAction(utils::icon("info-circle"), tr("View route details"), this, &VideoWidget::showRouteInfo); - } else { - createSpeedDropdown(toolbar); - } -} - -void VideoWidget::createSpeedDropdown(QToolBar *toolbar) { - toolbar->addWidget(speed_btn = new QToolButton(this)); - speed_btn->setMenu(new QMenu(speed_btn)); - speed_btn->setPopupMode(QToolButton::InstantPopup); - QActionGroup *speed_group = new QActionGroup(this); - speed_group->setExclusive(true); - - for (float speed : {0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 0.8, 1., 2., 3., 5.}) { - auto act = speed_btn->menu()->addAction(QString("%1x").arg(speed), this, [this, speed]() { - can->setSpeed(speed); - speed_btn->setText(QString("%1x ").arg(speed)); - }); - - speed_group->addAction(act); - act->setCheckable(true); - if (speed == 1.0) { - act->setChecked(true); - act->trigger(); - } - } - - QFont font = speed_btn->font(); - font.setBold(true); - speed_btn->setFont(font); - speed_btn->setMinimumWidth(speed_btn->fontMetrics().horizontalAdvance("0.05x ") + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); -} - -QWidget *VideoWidget::createCameraWidget() { - QWidget *w = new QWidget(this); - QVBoxLayout *l = new QVBoxLayout(w); - l->setContentsMargins(0, 0, 0, 0); - l->setSpacing(0); - - l->addWidget(camera_tab = new TabBar(w)); - camera_tab->setAutoHide(true); - camera_tab->setExpanding(false); - - l->addWidget(cam_widget = new StreamCameraView("camerad", VISION_STREAM_NARROW_ROAD)); - cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT); - cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); - - l->addWidget(slider = new Slider(w)); - slider->setSingleStep(0); - slider->setTimeRange(can->minSeconds(), can->maxSeconds()); - - QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); - connections_.push_back(can->paused.connect([this]() { cam_widget->update(); })); - connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { slider->update(); })); - connections_.push_back(cam_widget->clicked.connect([]() { can->pause(!can->isPaused()); })); - connections_.push_back(cam_widget->availableStreamsUpdated.connect([this](std::set streams) { vipcAvailableStreamsUpdated(streams); })); - QObject::connect(camera_tab, &QTabBar::currentChanged, [this](int index) { - if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); - }); - connections_.push_back(static_cast(can)->qLogLoaded.connect([this](std::shared_ptr qlog) { cam_widget->parseQLog(qlog); })); - slider->installEventFilter(this); - return w; -} - -void VideoWidget::vipcAvailableStreamsUpdated(std::set streams) { - static const QString stream_names[] = {"Road camera", "Driver camera", "Wide road camera"}; - for (int i = 0; i < streams.size(); ++i) { - if (camera_tab->count() <= i) { - camera_tab->addTab(QString()); - } - int type = *std::next(streams.begin(), i); - camera_tab->setTabText(i, stream_names[type]); - camera_tab->setTabData(i, type); - } - while (camera_tab->count() > streams.size()) { - camera_tab->removeTab(camera_tab->count() - 1); - } -} - -void VideoWidget::loopPlaybackClicked() { - bool is_looping = getReplay()->loop(); - getReplay()->setLoop(!is_looping); - qobject_cast(sender())->setIcon(utils::icon(!is_looping ? "repeat" : "repeat-1")); -} - -void VideoWidget::timeRangeChanged() { - const auto time_range = can->timeRange(); - if (can->liveStreaming()) { - skip_to_end_action->setEnabled(!time_range.has_value()); - return; - } - time_range ? slider->setTimeRange(time_range->first, time_range->second) - : slider->setTimeRange(can->minSeconds(), can->maxSeconds()); - updateState(); -} - -QString VideoWidget::formatTime(double sec, bool include_milliseconds) { - if (settings.absolute_time) - sec += std::chrono::duration(can->beginDateTime().time_since_epoch()).count(); - return QString::fromStdString(utils::formatSeconds(sec, include_milliseconds, settings.absolute_time)); -} - -void VideoWidget::updateState() { - if (slider) { - if (!slider->isSliderDown()) { - slider->setCurrentSecond(can->currentSec()); - } - if (camera_tab->count() == 0) { // No streams available - cam_widget->update(); // Manually refresh to show alert events - } - time_display_action->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), - formatTime(slider->maximum() / slider->factor))); - } else { - time_display_action->setText(formatTime(can->currentSec(), true)); - } -} - -void VideoWidget::updatePlayBtnState() { - play_toggle_action->setIcon(utils::icon(can->isPaused() ? "play" : "pause")); - play_toggle_action->setToolTip(can->isPaused() ? tr("Play") : tr("Pause")); -} - -void VideoWidget::showThumbnail(double seconds) { - if (can->liveStreaming()) return; - - cam_widget->thumbnail_dispaly_time = seconds; - slider->thumbnail_dispaly_time = seconds; - cam_widget->update(); - slider->update(); -} - -void VideoWidget::showRouteInfo() { - RouteInfoDlg *route_info = new RouteInfoDlg(this); - route_info->setAttribute(Qt::WA_DeleteOnClose); - route_info->show(); -} - -bool VideoWidget::eventFilter(QObject *obj, QEvent *event) { - if (event->type() == QEvent::MouseMove) { - auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); - showThumbnail(min_sec + static_cast(event)->pos().x() * (max_sec - min_sec) / slider->width()); - } else if (event->type() == QEvent::Leave) { - showThumbnail(-1); - } - return false; -} - -// Slider -Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { - setMouseTracking(true); -} - -void Slider::paintEvent(QPaintEvent *ev) { - QPainter p(this); - - QStyleOptionSlider opt; - initStyleOption(&opt); - QRect handle_rect = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); - QRect groove_rect = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - - // Adjust groove height to match handle height - int handle_height = handle_rect.height(); - groove_rect.setHeight(handle_height * 0.5); - groove_rect.moveCenter(QPoint(groove_rect.center().x(), rect().center().y())); - - p.fillRect(groove_rect, timeline_colors[(int)TimelineType::None]); - - double min = minimum() / factor; - double max = maximum() / factor; - - auto fillRange = [&](double begin, double end, const QColor &color) { - if (begin > max || end < min) return; - - QRect r = groove_rect; - r.setLeft(((std::max(min, begin) - min) / (max - min)) * width()); - r.setRight(((std::min(max, end) - min) / (max - min)) * width()); - p.fillRect(r, color); - }; - - if (auto replay = getReplay()) { - for (const auto &entry : *replay->getTimeline()) { - fillRange(entry.start_time, entry.end_time, timeline_colors[(int)entry.type]); - } - - QColor empty_color = palette().color(QPalette::Window); - empty_color.setAlpha(160); - const auto event_data = replay->getEventData(); - for (const auto &[n, _] : replay->route().segments()) { - if (!event_data->isSegmentLoaded(n)) - fillRange(n * 60.0, (n + 1) * 60.0, empty_color); - } - } - - opt.minimum = minimum(); - opt.maximum = maximum(); - opt.subControls = QStyle::SC_SliderHandle; - opt.sliderPosition = value(); - style()->drawComplexControl(QStyle::CC_Slider, &opt, &p); - - if (thumbnail_dispaly_time >= 0) { - int left = (thumbnail_dispaly_time - min) * width() / (max - min) - 1; - QRect rc(left, rect().top() + 1, 2, rect().height() - 2); - p.setBrush(palette().highlight()); - p.setPen(Qt::NoPen); - p.drawRoundedRect(rc, 1.5, 1.5); - } -} - -void Slider::mousePressEvent(QMouseEvent *e) { - QSlider::mousePressEvent(e); - if (e->button() == Qt::LeftButton && !isSliderDown()) { - setValue(minimum() + ((maximum() - minimum()) * e->x()) / width()); - emit sliderReleased(); - } -} - -// StreamCameraView -StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent) - : CameraWidget(stream_name, stream_type, parent) { -} - -void StreamCameraView::parseQLog(std::shared_ptr qlog) { - std::mutex mutex; - const auto &events = qlog->events; - unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency()); - size_t chunk = (events.size() + num_threads - 1) / num_threads; - std::vector threads; - for (unsigned int t = 0; t < num_threads && t * chunk < events.size(); ++t) { - size_t start = t * chunk; - size_t end = std::min(start + chunk, events.size()); - threads.emplace_back([this, &mutex, &events, start, end]() { - for (size_t i = start; i < end; ++i) { - const Event &e = events[i]; - if (e.which == cereal::Event::Which::THUMBNAIL) { - capnp::FlatArrayMessageReader reader(e.data); - auto thumb_data = reader.getRoot().getThumbnail(); - auto image_data = thumb_data.getThumbnail(); - if (QPixmap thumb; thumb.loadFromData(image_data.begin(), image_data.size(), "jpeg")) { - QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof())); - std::lock_guard lock(mutex); - thumbnails[thumb_data.getTimestampEof()] = generated_thumb; - big_thumbnails[thumb_data.getTimestampEof()] = thumb; - } - } - } - }); - } - for (auto &th : threads) th.join(); - update(); -} - -void StreamCameraView::paintEvent(QPaintEvent *event) { - CameraWidget::paintEvent(event); - - QPainter p(this); - bool scrubbing = false; - if (thumbnail_dispaly_time >= 0) { - scrubbing = can->isPaused(); - scrubbing ? drawScrubThumbnail(p) : drawThumbnail(p); - } - if (auto alert = getReplay()->findAlertAtTime(scrubbing ? thumbnail_dispaly_time : can->currentSec())) { - drawAlert(p, rect(), *alert); - } - - if (can->isPaused()) { - p.setPen(QColor(200, 200, 200, static_cast(255 * 0.7f))); - p.setFont(QFont(font().family(), 16, QFont::Bold)); - p.drawText(rect(), Qt::AlignCenter, tr("PAUSED")); - } -} - -QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) { - QPixmap scaled = thumb.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation); - QPainter p(&scaled); - p.setPen(QPen(palette().color(QPalette::BrightText), 2)); - p.drawRect(scaled.rect()); - if (auto alert = getReplay()->findAlertAtTime(seconds)) { - p.setFont(QFont(font().family(), 10)); - drawAlert(p, scaled.rect(), *alert); - } - return scaled; -} - -void StreamCameraView::drawScrubThumbnail(QPainter &p) { - p.fillRect(rect(), Qt::black); - auto it = big_thumbnails.lower_bound(can->toMonoTime(thumbnail_dispaly_time)); - if (it != big_thumbnails.end()) { - QPixmap scaled_thumb = it->second.scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); - QRect thumb_rect(rect().center() - scaled_thumb.rect().center(), scaled_thumb.size()); - p.drawPixmap(thumb_rect.topLeft(), scaled_thumb); - drawTime(p, thumb_rect, thumbnail_dispaly_time); - } -} - -void StreamCameraView::drawThumbnail(QPainter &p) { - auto it = thumbnails.lower_bound(can->toMonoTime(thumbnail_dispaly_time)); - if (it != thumbnails.end()) { - const QPixmap &thumb = it->second; - auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds())); - int pos = (thumbnail_dispaly_time - min_sec) * width() / (max_sec - min_sec); - int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); - int y = height() - thumb.height() - THUMBNAIL_MARGIN; - - p.drawPixmap(x, y, thumb); - drawTime(p, QRect{x, y, thumb.width(), thumb.height()}, thumbnail_dispaly_time); - } -} - -void StreamCameraView::drawTime(QPainter &p, const QRect &rect, double seconds) { - p.setPen(palette().color(QPalette::BrightText)); - p.setFont(QFont(font().family(), 10)); - p.drawText(rect.adjusted(0, 0, 0, -THUMBNAIL_MARGIN), Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3)); -} - -void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert) { - p.setPen(QPen(palette().color(QPalette::BrightText), 2)); - QColor color = timeline_colors[int(alert.type)]; - color.setAlphaF(0.5); - QString text = QString::fromStdString(alert.text1); - if (!alert.text2.empty()) text += "\n" + QString::fromStdString(alert.text2); - - QRect text_rect = rect.adjusted(1, 1, -1, -1); - QRect r = p.fontMetrics().boundingRect(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); - p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color); - p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); -} diff --git a/openpilot/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h deleted file mode 100644 index 8416eece59..0000000000 --- a/openpilot/tools/cabana/videowidget.h +++ /dev/null @@ -1,82 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "tools/cabana/cameraview.h" -#include "tools/cabana/utils/qtutil.h" -#include "tools/replay/logreader.h" -#include "tools/cabana/streams/replaystream.h" - -class Slider : public QSlider { - Q_OBJECT - -public: - Slider(QWidget *parent); - double currentSecond() const { return value() / factor; } - void setCurrentSecond(double sec) { setValue(sec * factor); } - void setTimeRange(double min, double max) { setRange(min * factor, max * factor); } - void mousePressEvent(QMouseEvent *e) override; - void paintEvent(QPaintEvent *ev) override; - const double factor = 1000.0; - double thumbnail_dispaly_time = -1; -}; - -class StreamCameraView : public CameraWidget { - Q_OBJECT - -public: - StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent = nullptr); - void paintEvent(QPaintEvent *event) override; - void parseQLog(std::shared_ptr qlog); - -private: - QPixmap generateThumbnail(QPixmap thumbnail, double seconds); - void drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert); - void drawThumbnail(QPainter &p); - void drawScrubThumbnail(QPainter &p); - void drawTime(QPainter &p, const QRect &rect, double seconds); - - std::map big_thumbnails; - std::map thumbnails; - double thumbnail_dispaly_time = -1; - friend class VideoWidget; -}; - -class VideoWidget : public QFrame { - Q_OBJECT - -public: - VideoWidget(QWidget *parnet = nullptr); - void showThumbnail(double seconds); - -protected: - bool eventFilter(QObject *obj, QEvent *event) override; - QString formatTime(double sec, bool include_milliseconds = false); - void timeRangeChanged(); - void updateState(); - void updatePlayBtnState(); - Connections connections_; - QWidget *createCameraWidget(); - void createPlaybackController(); - void createSpeedDropdown(QToolBar *toolbar); - void loopPlaybackClicked(); - void vipcAvailableStreamsUpdated(std::set streams); - void showRouteInfo(); - - StreamCameraView *cam_widget; - QAction *time_display_action = nullptr; - QAction *play_toggle_action = nullptr; - QToolButton *speed_btn = nullptr; - QAction *skip_to_end_action = nullptr; - Slider *slider = nullptr; - QTabBar *camera_tab = nullptr; -}; diff --git a/pyproject.toml b/pyproject.toml index c1dd803640..78fb53ed1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "tqdm", # cars (fw_versions.py) on start + many one-off uses # core - "scons==4.10.1", # 4.11 removed the qt3 tool still used to build Cabana + "scons", "pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references "numpy >=2.0", diff --git a/tools/op.sh b/tools/op.sh index 43b28e34c1..3d7d17a76b 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -445,7 +445,7 @@ function op_default() { echo -e "${BOLD}${UNDERLINE}Commands [Tooling]:${NC}" echo -e " ${BOLD}juggle${NC} Run PlotJuggler" echo -e " ${BOLD}replay${NC} Run Replay" - echo -e " ${BOLD}cabana${NC} Run Cabana (--legacy for the old Qt version)" + echo -e " ${BOLD}cabana${NC} Run Cabana" echo -e " ${BOLD}clip${NC} Run clip (linux only)" echo -e " ${BOLD}adb${NC} Run adb shell" echo -e " ${BOLD}ssh${NC} comma prime SSH helper" diff --git a/uv.lock b/uv.lock index 89b82b5452..dc9cb69845 100644 --- a/uv.lock +++ b/uv.lock @@ -675,7 +675,7 @@ requires-dist = [ { name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" }, { name = "requests" }, { name = "ruff", marker = "extra == 'testing'" }, - { name = "scons", specifier = "==4.10.1" }, + { name = "scons" }, { name = "sentry-sdk" }, { name = "setproctitle" }, { name = "sounddevice" }, @@ -923,11 +923,11 @@ wheels = [ [[package]] name = "scons" -version = "4.10.1" +version = "4.11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/b9/b7a5c88f348a0c34594d88100872c55fa1cae863ccb222c1c438341b5503/scons-4.11.1.tar.gz", hash = "sha256:4210d1a80a62e986029208117991b6347ccaaaab37b67463a3ff31ee065dc487", size = 3268963, upload-time = "2026-08-27T04:33:18.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/d6285848e893c19682c06e92679dc1a07d37ff7ea148747b1df681ec496c/scons-4.11.1-py3-none-any.whl", hash = "sha256:454cef364348053422696e3d2ecb4fa593c96a624f955842eaaea64f95c8d11d", size = 4123659, upload-time = "2026-08-27T04:33:12.728Z" }, ] [[package]] @@ -1034,7 +1034,7 @@ provides-extras = ["dev"] [[package]] name = "tinygrad" -version = "0.13.0" +version = "0.14.0" source = { editable = "tinygrad_repo" } [package.metadata] @@ -1048,6 +1048,7 @@ requires-dist = [ { name = "hypothesis", marker = "extra == 'testing-minimal'", specifier = ">=6.148.9" }, { name = "influxdb3-python", marker = "extra == 'testing'" }, { name = "librosa", marker = "extra == 'testing'" }, + { name = "mako", marker = "extra == 'autogen'" }, { name = "markdown-callouts", marker = "extra == 'docs'" }, { name = "markdown-exec", extras = ["ansi"], marker = "extra == 'docs'" }, { name = "mkdocs", marker = "extra == 'docs'" }, @@ -1062,7 +1063,7 @@ requires-dist = [ { name = "numpy", marker = "extra == 'testing-minimal'" }, { name = "onnx", marker = "extra == 'testing'", specifier = "==1.19.0" }, { name = "onnx2torch", marker = "extra == 'testing'" }, - { name = "onnxruntime", marker = "extra == 'testing'" }, + { name = "onnxruntime", marker = "extra == 'testing'", specifier = "==1.24.1" }, { name = "openai", marker = "extra == 'testing-unit'" }, { name = "opencv-python", marker = "extra == 'testing'" }, { name = "pandas", marker = "extra == 'testing'" }, @@ -1073,6 +1074,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'testing-minimal'" }, { name = "pytest-split", marker = "extra == 'testing-minimal'" }, { name = "pytest-xdist", marker = "extra == 'testing-minimal'" }, + { name = "pyyaml", marker = "extra == 'autogen'" }, { name = "ruff", marker = "extra == 'linting'", specifier = "==0.14.10" }, { name = "safetensors", marker = "extra == 'testing-unit'" }, { name = "sentencepiece", marker = "extra == 'testing'" }, @@ -1088,7 +1090,7 @@ requires-dist = [ { name = "typing-extensions", marker = "extra == 'linting'" }, { name = "z3-solver", marker = "extra == 'testing-minimal'", specifier = "<4.15.4" }, ] -provides-extras = ["linting", "testing-minimal", "testing-unit", "testing", "docs", "mesa"] +provides-extras = ["linting", "testing-minimal", "testing-unit", "testing", "docs", "mesa", "autogen"] [[package]] name = "tqdm"