bye bye Qt! (#38753)

* cabana: remove Qt implementation
This commit is contained in:
Trey Moen
2026-09-02 20:44:11 -07:00
committed by GitHub
parent 7bade9a67a
commit 3a13b67b6f
53 changed files with 50 additions and 8519 deletions
-1
View File
@@ -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",
}
-7
View File
@@ -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
+19 -21
View File
@@ -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 <panda-serial> read can messages from panda with given serial
--socketcan <socketcan> read can messages from given SocketCAN device
--zmq <ip-address> read can messages from zmq at the specified ip-address
messages
--data_dir <data_dir> local directory with routes
--no-vipc do not output video
--dbc <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 <serial> read can messages from panda with given serial
--socketcan <device> read can messages from given SocketCAN device
--zmq <ip-address> read can messages from zmq at the specified ip-address
--data_dir <dir> local directory with routes
--no-vipc do not output video
--no-cache turn off the local route file cache
--dbc <file> dbc file to open
```
## Examples
+18 -105
View File
@@ -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)
-5
View File
@@ -1,5 +0,0 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>cabana-icon.png</file>
</qresource>
</RCC>
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:015cea9d425f747bce856fd2f5a6a57fca322159dde54554de11eda076273595
size 18008
-510
View File
@@ -1,510 +0,0 @@
#include "tools/cabana/binaryview.h"
#include <algorithm>
#include <cstdio>
#include <QFontDatabase>
#include <QHeaderView>
#include <QMouseEvent>
#include <QPainter>
#include <QScrollBar>
#include <QShortcut>
#include <QToolTip>
#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"(
<b>Binary View</b><br/>
<!-- TODO: add description here -->
<span style="color:gray">Shortcuts</span><br />
Delete Signal:
<span style="background-color:lightGray;color:gray">&nbsp;x&nbsp;</span>,
<span style="background-color:lightGray;color:gray">&nbsp;Backspace&nbsp;</span>,
<span style="background-color:lightGray;color:gray">&nbsp;Delete&nbsp;</span><br />
Change endianness: <span style="background-color:lightGray;color:gray">&nbsp;e&nbsp; </span><br />
Change signedness: <span style="background-color:lightGray;color:gray">&nbsp;s&nbsp;</span><br />
Open chart:
<span style="background-color:lightGray;color:gray">&nbsp;c&nbsp;</span>,
<span style="background-color:lightGray;color:gray">&nbsp;p&nbsp;</span>,
<span style="background-color:lightGray;color:gray">&nbsp;g&nbsp;</span>
)");
}
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<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
std::set<const cabana::Signal *> 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<int, int, bool> 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<std::array<uint32_t, 8>> &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<uint32_t, 8>{});
// 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<uint8_t> 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<int>(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);
}
}
}
-106
View File
@@ -1,106 +0,0 @@
#pragma once
#include <set>
#include <tuple>
#include <vector>
#include <QStaticText>
#include <QStyledItemDelegate>
#include <QTableView>
#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<QStaticText, 256> hex_text_table;
std::array<QStaticText, 2> 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<std::array<uint32_t, 8>> &getBitFlipChanges(size_t msg_size);
struct BitFlipTracker {
std::optional<std::pair<double, double>> time_range;
std::vector<std::array<uint32_t, 8>> 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<const cabana::Signal *> sigs;
bool valid = false;
};
std::vector<Item> 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<const cabana::Signal*> 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<int, int, bool> 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;
};
+2 -45
View File
@@ -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" "$@"
-196
View File
@@ -1,196 +0,0 @@
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <QApplication>
#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 <serial> read can messages from panda with given serial\n"
#ifdef __linux__
" --socketcan <device> read can messages from given SocketCAN device\n"
#endif
" --zmq <ip-address> read can messages from zmq at the specified ip-address\n"
" --data_dir <dir> local directory with routes\n"
" --no-vipc do not output video\n"
" --dbc <file> 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<ReplayStream>();
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();
}
-121
View File
@@ -1,121 +0,0 @@
#include "tools/cabana/cameraview.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <QApplication>
#include <QPainter>
#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<VisionIpcClient> 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<bool>(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<bool>(alive_)]() {
if (!alive.expired()) update();
});
}
}
}
void CameraWidget::clearFrames() {
std::lock_guard lk(frame_lock);
rgb_frame = QImage();
rgb_back = QImage();
available_streams.clear();
}
-49
View File
@@ -1,49 +0,0 @@
#pragma once
#include <atomic>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <QImage>
#include <QWidget>
#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<std::set<VisionStreamType>> 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<VisionStreamType> active_stream_type;
std::atomic<VisionStreamType> requested_stream_type;
std::set<VisionStreamType> available_streams;
std::thread vipc_thread;
std::atomic<bool> vipc_exit = false;
std::mutex frame_lock;
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
};
-769
View File
@@ -1,769 +0,0 @@
#include "tools/cabana/chart/chart.h"
#include <algorithm>
#include <limits>
#include <random>
#include <QActionGroup>
#include <QContextMenuEvent>
#include <QMouseEvent>
#include <QPainterPath>
#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<double, double> &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<bool(const SigItem &s)> 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<const CanEvent *> &events,
std::vector<QPointF> &vals, std::vector<QPointF> &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<QPointF> 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<double>::max();
double max = std::numeric_limits<double>::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<double>::max();
s.max = std::numeric_limits<double>::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<double>::max()) min = 0;
if (max == std::numeric_limits<double>::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<double, double, int> 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<double>::max() ? "--" : QString::number(s.min);
QString max = s.max == std::numeric_limits<double>::lowest() ? "--" : QString::number(s.max);
text_list << QString("<span style=\"color:%1;\">■ </span>%2<b>%3</b> (%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 = "<p style='white-space:pre'>" % text_list.join("<br />") % "</p>";
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<int>(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<QPointF> 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<int> sat(35, 99);
std::uniform_int_distribution<int> 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();
}
}
-132
View File
@@ -1,132 +0,0 @@
#pragma once
#include <functional>
#include <tuple>
#include <utility>
#include <vector>
#include <QMenu>
#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<double, double> &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<QPointF> vals;
std::vector<QPointF> 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<const CanEvent *> &events,
std::vector<QPointF> &vals, std::vector<QPointF> &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<double, double, int> 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<bool(const SigItem &)> 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<QRect> 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<SigItem> 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;
};
@@ -1,660 +0,0 @@
#include "tools/cabana/chart/chartswidget.h"
#include <algorithm>
#include <future>
#include <QApplication>
#include <QMenu>
#include <QMouseEvent>
#include <QScrollBar>
#include <QToolBar>
#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<QToolButton *>(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<QToolButton*>(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<MessageId> *, 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"(
<b>Chart View</b><br />
<b>Click</b>: Click to seek to a corresponding time.<br />
<b>Drag</b>: Zoom into the chart.<br />
<b>Shift + Drag</b>: Scrub through the chart to view values.<br />
<b>Right Mouse</b>: Open the context menu.<br />
)"));
}
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<std::future<void>> 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<std::pair<double, double>> &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<float>(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 &current_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<QMouseEvent *>(e)->globalPos());
return true;
} else if (e->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(e)->button() == Qt::LeftButton) {
dragChartRelease(static_cast<QMouseEvent *>(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<TipLabel *>(o) != nullptr;
auto global_pos = static_cast<QMouseEvent *>(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<TipLabel *>(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<QMouseEvent *>(event)->button() == Qt::BackButton;
break;
case QEvent::NativeGesture:
back_button = (static_cast<QNativeGestureEvent *>(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;
}
-140
View File
@@ -1,140 +0,0 @@
#pragma once
#include <unordered_map>
#include <utility>
#include <QGridLayout>
#include <QLabel>
#include <QScrollArea>
#include <QTimer>
#include <QToolBar>
#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<std::pair<double, double>> &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<ChartView *> &currentCharts() { 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<ChartView *> charts;
std::unordered_map<int, std::vector<ChartView *>> tab_charts;
TabBar *tabbar;
ChartsContainer *charts_container;
QScrollArea *charts_scroll;
uint32_t max_chart_range = 0;
std::pair<double, double> 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<double, double> range) : range(range) {
prev_range = can->timeRange();
}
void undo() override { can->setTimeRange(prev_range); }
void redo() override { can->setTimeRange(range); }
std::optional<std::pair<double, double>> prev_range, range;
};
@@ -1,107 +0,0 @@
#include "tools/cabana/chart/signalselector.h"
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#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<int>(&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<MessageId>()) {
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<MessageId>();
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("<span style=\"color:%0;\">■ </span> %1").arg(toQColor(sig->color).name(), QString::fromStdString(sig->name));
if (show_msg_name) text += QString(" <font color=\"gray\">%0 %1</font>").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::ListItem *> SignalSelector::seletedItems() {
std::vector<SignalSelector::ListItem *> ret;
for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i));
return ret;
}
@@ -1,30 +0,0 @@
#pragma once
#include <QComboBox>
#include <QDialog>
#include <QListWidget>
#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<ListItem *> 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;
};
-101
View File
@@ -1,101 +0,0 @@
#include "tools/cabana/chart/sparkline.h"
#include <algorithm>
#include <limits>
#include <QPainter>
#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<double>::max();
max_val = std::numeric_limits<double>::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());
}
}
-26
View File
@@ -1,26 +0,0 @@
#pragma once
#include <QPixmap>
#include <QPointF>
#include <vector>
#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<QPointF> points_;
std::vector<QPointF> render_points_;
double freq_ = 0;
};
-58
View File
@@ -1,58 +0,0 @@
#include "tools/cabana/chart/tiplabel.h"
#include <utility>
#include <QApplication>
#include <QStylePainter>
#include <QToolTip>
#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);
}
-12
View File
@@ -1,12 +0,0 @@
#pragma once
#include <QLabel>
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;
};
-323
View File
@@ -1,323 +0,0 @@
#include "tools/cabana/detailwidget.h"
#include <QFormLayout>
#include <QMenu>
#include <QRadioButton>
#include <QPushButton>
#include <QToolBar>
#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<MessageId> *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<MessageId>());
}
});
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<std::pair<double, double>> &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<MessageId>() == 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<QString, QStringList> DetailWidget::serializeMessageIds() const {
QStringList msgs;
for (int i = 0; i < tabbar->count(); ++i) {
MessageId id = tabbar->tabData(i).value<MessageId>();
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<MessageId> *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;
}
-78
View File
@@ -1,78 +0,0 @@
#pragma once
#include <QDialogButtonBox>
#include <QSpinBox>
#include <QSplitter>
#include <QTabWidget>
#include <QTextEdit>
#include <set>
#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<QString, QStringList> 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<MessageId> *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;
};
-251
View File
@@ -1,251 +0,0 @@
#include "tools/cabana/historylog.h"
#include <functional>
#include <QFileDialog>
#include <QPainter>
#include <QVBoxLayout>
#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<bool(double, double)> 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<Message>::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<HistoryLogModel::Message> msgs;
std::vector<double> 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<uint8_t> 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<QBrush>());
}
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<int>(&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<bool(double, double)> cmp = nullptr;
switch (comp_box->currentIndex()) {
case 0: cmp = std::greater<double>{}; break;
case 1: cmp = std::equal_to<double>{}; break;
case 2: cmp = [](double l, double r) { return l != r; }; break; // not equal
case 3: cmp = std::less<double>{}; 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);
}
}
-83
View File
@@ -1,83 +0,0 @@
#pragma once
#include <deque>
#include <vector>
#include <QComboBox>
#include <QHeaderView>
#include <QLineEdit>
#include <QTableView>
#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<bool(double, double)> 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<double> sig_values;
std::vector<uint8_t> data;
std::vector<CabanaColor> colors;
};
void fetchData(std::deque<Message>::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<bool(double, double)> filter_cmp = nullptr;
std::deque<Message> messages;
std::vector<cabana::Signal *> 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;
};
-752
View File
@@ -1,752 +0,0 @@
#include "tools/cabana/mainwin.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <QFileDialog>
#include <QMenuBar>
#include <QMessageBox>
#include <QProgressDialog>
#include <QResizeEvent>
#include <QShortcut>
#include <QTextDocument>
#include <QVBoxLayout>
#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<char>(json_file), std::istreambuf_iterator<char>()};
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<std::string> 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<DummyStream *>(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<DummyStream *>(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<int>(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<HelpOverlay*>()) {
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<MessagesWidget *>());
drawHelpForWidget(painter, parent->findChild<BinaryView *>());
drawHelpForWidget(painter, parent->findChild<SignalView *>());
drawHelpForWidget(painter, parent->findChild<ChartsWidget *>());
drawHelpForWidget(painter, parent->findChild<VideoWidget *>());
}
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();
}
-122
View File
@@ -1,122 +0,0 @@
#pragma once
#include <QDockWidget>
#include <QMainWindow>
#include <QMenu>
#include <QProgressBar>
#include <QSplitter>
#include <QStatusBar>
#include <cstdint>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
#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<std::string, std::string> 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<uint8_t> 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;
};
-467
View File
@@ -1,467 +0,0 @@
#include "tools/cabana/messageswidget.h"
#include <limits>
#include <utility>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QPainter>
#include <QPalette>
#include <QPushButton>
#include <QScrollBar>
#include <QVBoxLayout>
#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 &current, 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"(
<b>Message View</b><br/>
<!-- TODO: add description here -->
<span style="color:gray">Byte color</span><br />
<span style="color:gray;">■ </span> constant changing<br />
<span style="color:blue;">■ </span> increasing<br />
<span style="color:red;">■ </span> decreasing<br />
<span style="color:gray">Shortcuts</span><br />
Horizontal Scrolling: <span style="background-color:lightGray;color:gray">&nbsp;shift+wheel&nbsp;</span>
)"));
}
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<size_t, size_t>(),
[](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<MessageId> *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 += "<br /><span style=\"color:gray;\">" + QString::fromStdString(msg->comment) + "</span>";
return tooltip;
}
return {};
}
void MessageListModel::setFilterStrings(const std::map<int, QString> &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<MessageListModel::Item> &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<unsigned int>::min();
unsigned int max = std::numeric_limits<unsigned int>::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<MessageId> 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<Item> 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<MessageId> *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<QRgb>(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<int>(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<int, QString> filters;
for (int i = 0; i < (int)editors.size(); i++) {
if (!editors[i]->text().isEmpty()) {
filters[i] = editors[i]->text();
}
}
qobject_cast<MessageListModel*>(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;
}
-131
View File
@@ -1,131 +0,0 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <map>
#include <optional>
#include <set>
#include <vector>
#include <QAbstractTableModel>
#include <QHeaderView>
#include <QLineEdit>
#include <QMenu>
#include <QPushButton>
#include <QTreeView>
#include <QWheelEvent>
#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<int, QString> &filters);
void showInactiveMessages(bool show);
void msgsReceived(const std::set<MessageId> *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<Item> items_;
bool show_inactive_messages = true;
private:
void sortItems(std::vector<MessageListModel::Item> &items);
bool match(const MessageListModel::Item &id);
std::map<int, QString> filters_;
std::set<MessageId> 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<QLineEdit *> editors;
};
class MessagesWidget : public QWidget {
Q_OBJECT
public:
MessagesWidget(QWidget *parent);
void selectMessage(const MessageId &message_id);
std::vector<uint8_t> saveHeaderState() const {
const auto state = view->header()->saveState();
return {state.begin(), state.end()};
}
bool restoreHeaderState(const std::vector<uint8_t> &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<MessageId> current_msg_id;
MessageListModel *model;
QPushButton *suppress_add;
QPushButton *suppress_clear;
QMenu *menu;
};
-110
View File
@@ -1,110 +0,0 @@
#include "tools/cabana/routesdialog.h"
#include <string>
#include <utility>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QListWidget>
#include <QMessageBox>
#include <QPainter>
#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<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
connect(period_selector_, QOverload<int>::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<bool>(alive_)](std::vector<routes::DeviceInfo> 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<routes::DeviceInfo> &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<bool>(alive_), request_id](std::vector<routes::RouteInfo> 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<routes::RouteInfo> &list, bool success) {
if (success) {
for (const auto &route : list) {
const int mins = static_cast<int>((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() : "";
}
-31
View File
@@ -1,31 +0,0 @@
#pragma once
#include <atomic>
#include <memory>
#include <vector>
#include <QComboBox>
#include <QDialog>
#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<routes::DeviceInfo> &devices, bool success, int error_code);
void setRouteList(const std::vector<routes::RouteInfo> &list, bool success);
void fetchRoutes();
QComboBox *device_list_;
QComboBox *period_selector_;
RouteListWidget *route_list_;
std::atomic<int> fetch_id_{0};
// expires on destruction; guards main-thread callbacks from detached worker threads
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
};
-88
View File
@@ -1,88 +0,0 @@
#include "tools/cabana/settingsdialog.h"
#include <utility>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QPushButton>
#include <QVBoxLayout>
#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();
}
-20
View File
@@ -1,20 +0,0 @@
#pragma once
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QLineEdit>
#include <QSpinBox>
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;
};
-719
View File
@@ -1,719 +0,0 @@
#include "tools/cabana/signalview.h"
#include <algorithm>
#include <future>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QScrollBar>
#include <QSpinBox>
#include <QVBoxLayout>
#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<ValueDescription>(); 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<int>(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<SignalModel::Item*>(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<MessageId> *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"(
<b>Signal view</b><br />
<!-- TODO: add description here -->
)"));
}
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<QToolButton *>();
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<QModelIndex, QModelIndex> 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<MessageId> *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<int>(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<std::future<void>> 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;
}
-155
View File
@@ -1,155 +0,0 @@
#pragma once
#include <memory>
#include <set>
#include <utility>
#include <QAbstractItemModel>
#include <QLabel>
#include <QLineEdit>
#include <QSlider>
#include <QStyledItemDelegate>
#include <QTableWidget>
#include <QTreeView>
#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<Item *> 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<Item> 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<MessageId> *msgs = nullptr);
std::pair<QModelIndex, QModelIndex> 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<SignalView *>(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_;
};
-330
View File
@@ -1,330 +0,0 @@
#include "tools/cabana/streamselector.h"
#include <filesystem>
#include <fstream>
#include <QFileDialog>
#include <QGridLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QRadioButton>
#include <QTimer>
#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<ReplayStream>();
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<PandaStream *>(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<int>(&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<int>(&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<QAbstractButton *, bool>(&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);
}
-99
View File
@@ -1,99 +0,0 @@
#pragma once
#include <vector>
#include <QButtonGroup>
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDialog>
#include <QFormLayout>
#include <QLineEdit>
#include <QTabWidget>
#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<QCheckBox *> 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;
};
-286
View File
@@ -1,286 +0,0 @@
#include "tools/cabana/tools/findsignal.h"
#include <set>
#include <thread>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMenu>
#include <QTimer>
#include <QVBoxLayout>
#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<bool(double)> 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<std::thread> 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<uint64_t>::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<int>(&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<bool(double)> 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<ushort> buses;
for (auto bus : bus_edit->text().trimmed().split(",")) {
bus = bus.trimmed();
if (!bus.isEmpty()) buses.insert(bus.toUShort());
}
std::set<uint32_t> 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<uint64_t>::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);
}
}
}
-71
View File
@@ -1,71 +0,0 @@
#pragma once
#include <algorithm>
#include <limits>
#include <string>
#include <vector>
#include <QAbstractTableModel>
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSpinBox>
#include <QTableView>
#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<bool(double)> cmp);
void reset();
void undo();
std::vector<SearchSignal> filtered_signals;
std::vector<SearchSignal> initial_signals;
std::vector<std::vector<SearchSignal>> histories;
uint64_t last_time = std::numeric_limits<uint64_t>::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;
};
@@ -1,161 +0,0 @@
#include "tools/cabana/tools/findsimilarbits.h"
#include <algorithm>
#include <unordered_map>
#include <QGridLayout>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QIntValidator>
#include <QLabel>
#include <QPushButton>
#include <QRadioButton>
#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::mismatched_struct> 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<uint32_t, std::vector<uint32_t>> mismatches;
std::unordered_map<uint32_t, uint32_t> 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<mismatched_struct> 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;
}
@@ -1,36 +0,0 @@
#pragma once
#include <vector>
#include <QComboBox>
#include <QDialog>
#include <QLineEdit>
#include <QSpinBox>
#include <QTableWidget>
#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<mismatched_struct> 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;
};
-40
View File
@@ -1,40 +0,0 @@
#include "tools/cabana/tools/routeinfo.h"
#include <QHeaderView>
#include <QScrollBar>
#include <QTableWidget>
#include <QVBoxLayout>
#include "tools/cabana/streams/replaystream.h"
RouteInfoDlg::RouteInfoDlg(QWidget *parent) : QDialog(parent) {
auto *replay = dynamic_cast<ReplayStream *>(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);
}
-8
View File
@@ -1,8 +0,0 @@
#pragma once
#include <QDialog>
class RouteInfoDlg : public QDialog {
Q_OBJECT
public:
RouteInfoDlg(QWidget *parent = nullptr);
};
@@ -1,29 +0,0 @@
#include "tools/cabana/utils/elidedlabel.h"
#include <QPainter>
#include <QStyleOption>
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());
}
@@ -1,25 +0,0 @@
#pragma once
#include <QLabel>
#include <QMouseEvent>
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_;
};
-236
View File
@@ -1,236 +0,0 @@
#include "tools/cabana/utils/qtutil.h"
#include <algorithm>
#include <cmath>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <QColor>
#include <QFontDatabase>
#include <QPixmapCache>
#include <QPainterPath>
// 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<std::vector<uint8_t> *>(data.value<void *>())->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<QColor>();
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<std::vector<uint8_t> *>(data.value<void *>());
const auto &colors = *static_cast<std::vector<CabanaColor> *>(index.data(ColorsRole).value<void *>());
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;
}
-139
View File
@@ -1,139 +0,0 @@
#pragma once
#include <array>
#include <utility>
#include <QApplication>
#include <QColor>
#include <QFont>
#include <QFontMetrics>
#include <QPainter>
#include <QStaticText>
#include <QStringBuilder>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QValidator>
#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<QStaticText, 256> 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<uint8_t> &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);
-434
View File
@@ -1,434 +0,0 @@
#include "tools/cabana/videowidget.h"
#include <algorithm>
#include <thread>
#include <QAction>
#include <QActionGroup>
#include <QMenu>
#include <QMouseEvent>
#include <QPainter>
#include <QStyleOptionSlider>
#include <QVBoxLayout>
#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<ReplayStream *>(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<MessageId> *, 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"(
<b>Video</b><br />
<!-- TODO: add description here -->
<span style="color:gray">Timeline color</span>
<table>
<tr><td><span style="color:%1;">■ </span>Disengaged </td>
<td><span style="color:%2;">■ </span>Engaged</td></tr>
<tr><td><span style="color:%3;">■ </span>User Flag </td>
<td><span style="color:%4;">■ </span>Info</td></tr>
<tr><td><span style="color:%5;">■ </span>Warning </td>
<td><span style="color:%6;">■ </span>Critical</td></tr>
</table>
<span style="color:gray">Shortcuts</span><br/>
Pause/Resume: <span style="background-color:lightGray;color:gray">&nbsp;space&nbsp;</span>
)").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<VisionStreamType> 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<ReplayStream *>(can)->qLogLoaded.connect([this](std::shared_ptr<LogReader> qlog) { cam_widget->parseQLog(qlog); }));
slider->installEventFilter(this);
return w;
}
void VideoWidget::vipcAvailableStreamsUpdated(std::set<VisionStreamType> 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<QAction*>(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<double>(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<QMouseEvent *>(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<LogReader> 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<std::thread> 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<cereal::Event>().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<int>(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);
}
-82
View File
@@ -1,82 +0,0 @@
#pragma once
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <QFrame>
#include <QSlider>
#include <QToolBar>
#include <QTabBar>
#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<LogReader> 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<uint64_t, QPixmap> big_thumbnails;
std::map<uint64_t, QPixmap> 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<VisionStreamType> 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;
};
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"
Generated
+9 -7
View File
@@ -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"