IQ.Pilot Release Commit @ 661a2de

This commit is contained in:
IQ.Lvbs CI [bot]
2026-08-08 14:37:51 -05:00
parent a6c27ac169
commit a1ef7d6c80
211 changed files with 7332 additions and 2756 deletions

View File

@@ -1,6 +1,11 @@
moc_*
*.moc
*.generated.qrc
cabana
assets.cc
bootstrap_icons.cc
_cabana
dbc/car_fingerprint_to_dbc.json
tests/test_cabana
tests/test_dbc_core

View File

@@ -1,6 +1,6 @@
# Cabana
Cabana is a tool developed to view raw CAN data. One use for this is creating and editing [CAN Dictionaries](http://socialledge.com/sjsu/index.php/DBC_Format) (DBC files), and the tool provides direct integration with [commaai/iqdbc](https://github.com/commaai/iqdbc) (a collection of DBC files), allowing you to load the DBC files direct from source, and save to your fork. In addition, you can load routes from [comma connect](https://connect.comma.ai).
Cabana is a tool developed to view raw CAN data. One use for this is creating and editing [CAN Dictionaries](http://socialledge.com/sjsu/index.php/DBC_Format) (DBC files), and the tool provides direct integration with iqdbc (a collection of DBC files), allowing you to load the DBC files direct from source, and save to your fork. In addition, you can load routes from [konn3kt](https://konn3kt.com).
## Usage Instructions
@@ -28,7 +28,7 @@ Options:
Arguments:
route the drive to replay. find your drives at
connect.comma.ai
konn3kt.com
```
## Examples
@@ -45,17 +45,17 @@ cabana --demo
To load a specific route for replay, provide the route as an argument:
```shell
cabana "a2a0ccea32023010|2023-07-27--13-01-19"
cabana "5beb9b58bd12b691/0000010a--a51155e496"
```
Replace "0ccea32023010|2023-07-27--13-01-19" with your desired route identifier.
Replace "5beb9b58bd12b691/0000010a--a51155e496" with your desired route identifier.
### Running Cabana with multiple cameras
To run Cabana with multiple cameras, use the following command:
```shell
cabana "a2a0ccea32023010|2023-07-27--13-01-19" --dcam --ecam
cabana "5beb9b58bd12b691/0000010a--a51155e496" --dcam --ecam
```
### Streaming CAN Messages from a comma Device
@@ -63,8 +63,8 @@ cabana "a2a0ccea32023010|2023-07-27--13-01-19" --dcam --ecam
[SSH into your device](https://github.com/commaai/openpilot/wiki/SSH) and start the bridge with the following command:
```shell
cd /data/openpilot/cereal/messaging/
./bridge &
cd /data/openpilot
./cereal/messaging/bridge &
```
Then Run Cabana with the device's IP address:
@@ -73,7 +73,24 @@ Then Run Cabana with the device's IP address:
cabana --zmq <ipaddress>
```
Replace &lt;ipaddress&gt; with your comma device's IP address.
Replace &lt;ipaddress&gt; with your device's IP address.
If you can't run the bridge on the device, `--bridge <ipaddress>` runs
`cereal/messaging/bridge` locally against the device instead.
### Streaming CAN Messages from a Remote Device over konn3kt
To watch a device that isn't on your network, `tools/cabana/konn3kt_canproxy.py`
re-publishes its live CAN onto a local ZMQ socket:
```shell
export KONN3KT_JWT="<your konn3kt jwt>"
./tools/cabana/konn3kt_canproxy.py <dongle_id>
cabana --zmq 127.0.0.1
```
The device must have `CanLiveStreaming` enabled so `canlived` is running. See the
header of `konn3kt_canproxy.py` for the full topology.
While streaming from the device, Cabana will log the CAN messages to a local directory. By default, this directory is ~/cabana_live_stream/. You can change the log directory in Cabana by navigating to menu -> tools -> settings.

View File

@@ -1,21 +1,40 @@
import subprocess
import os
import shutil
import sys
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'replay_lib', 'cereal')
venv_site_packages = os.path.join(Dir("#").abspath, ".venv", "lib", "python3.12", "site-packages")
if os.path.isdir(venv_site_packages) and venv_site_packages not in sys.path:
sys.path.insert(0, venv_site_packages)
import libusb
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_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", "Network", "Concurrent", "DBus", "Xml"]
qt_modules = ["Widgets", "Gui", "Core"]
qt_libs = []
if arch == "Darwin":
brew_prefix = subprocess.check_output(['brew', '--prefix'], encoding='utf8').strip()
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] + ["OpenGL"]
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()
@@ -32,15 +51,10 @@ else:
qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules]
qt_libs = [f"Qt5{m}" for m in qt_modules]
if arch == "larch64":
qt_libs += ["GLESv2", "wayland-client"]
qt_env.PrependENVPath('PATH', Dir("#third_party/qt5/larch64/bin/").abspath)
elif arch != "Darwin":
qt_libs += ["GL"]
qt_env['QT3DIR'] = qt_env['QTDIR']
qt_env.Tool('qt3')
qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"]
qt_env['CPPPATH'] += qt_dirs
qt_flags = [
"-D_REENTRANT",
"-DQT_NO_DEBUG",
@@ -54,41 +68,74 @@ qt_env['LIBPATH'] += ['#selfdrive/ui', ]
qt_env['LIBS'] = qt_libs
base_frameworks = qt_env['FRAMEWORKS']
base_libs = [common, messaging, cereal, visionipc, 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"]
base_libs = [common, messaging, cereal, visionipc, 'm', 'pthread'] + qt_env["LIBS"]
if arch == "Darwin":
base_frameworks.append('OpenCL')
base_frameworks.append('QtCharts')
base_frameworks.append('QtSerialBus')
else:
base_libs.append('OpenCL')
base_libs.append('Qt5Charts')
base_libs.append('Qt5SerialBus')
qt_libs = base_libs
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, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs
iqdbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../iqdbc/dbc").abspath)
cabana_env['CXXFLAGS'] += [iqdbc_path]
# IQ.Pilot patch: replay_lib fetches routes over libcurl and signs konn3kt JWTs with
# OpenSSL (tools/replay/api.cc), and visionipc references OpenCL — upstream gets all
# three transitively from its Python downloader / vendored wheels, we link them here.
cabana_libs = [cereal, messaging, visionipc, replay_lib] + ffmpeg_libs + \
['bz2', 'zstd', 'curl', 'ssl', 'crypto', 'usb-1.0'] + base_libs
if arch == "Darwin":
base_frameworks += ['OpenCL']
else:
cabana_libs += ['OpenCL']
opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("#iqdbc/dbc").abspath)
cabana_env['CXXFLAGS'] += [opendbc_path]
# embed the bootstrap icons SVG into the binary
def build_bootstrap_icons_src(target, source, env):
data = open(str(source[0]), 'rb').read()
with open(str(target[0]), 'w') as f:
f.write('#include <cstddef>\n')
f.write('extern const unsigned char bootstrap_icons_svg[];\n')
f.write('extern const size_t bootstrap_icons_svg_len;\n')
f.write('const unsigned char bootstrap_icons_svg[] = {\n')
for i in range(0, len(data), 32):
f.write(','.join(str(b) for b in data[i:i+32]) + ',\n')
f.write('};\n')
f.write('const size_t bootstrap_icons_svg_len = sizeof(bootstrap_icons_svg);\n')
return None
# IQ.Pilot patch: no comma-deps-bootstrap-icons wheel here; the SVG is checked in
# under third_party/bootstrap (see its pull.sh).
bootstrap_icons_src = cabana_env.Command('assets/bootstrap_icons.cc', '#third_party/bootstrap/bootstrap-icons.svg',
cabana_env.PrettyAction(build_bootstrap_icons_src, 'GEN'))
# build assets
assets = "assets/assets.cc"
assets_src = "assets/assets.qrc"
cabana_env.Command(assets, assets_src, cabana_env.PrettyAction("rcc $SOURCES -o $TARGET", 'RCC'))
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"]))
cabana_env.Command(assets, "assets/assets.qrc", cabana_env.PrettyAction("rcc $SOURCES -o $TARGET", 'RCC'))
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"]))
cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcanstream.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'utils/api.cc',
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc',
'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc',
'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc',
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc',
'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc']
if arch != "Darwin":
cabana_srcs += ['streams/socketcanstream.cc']
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'):
cabana_env.Program('tests/test_cabana', ['tests/test_runner.cc', 'tests/test_cabana.cc', cabana_lib], LIBS=[cabana_libs])
# 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.Program('tests/test_dbc_core', dbc_core_test_objects)
output_json_file = 'tools/cabana/dbc/car_fingerprint_to_dbc.json'
generate_dbc = cabana_env.Command('#' + output_json_file,

View File

@@ -1,6 +1,5 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="bootstrap-icons.svg">../../../third_party/bootstrap/bootstrap-icons.svg</file>
<file>cabana-icon.png</file>
</qresource>
</RCC>

View File

@@ -1,8 +1,10 @@
#include "tools/cabana/binaryview.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <algorithm>
#include <QDebug>
#include <cstdio>
#include <QFontDatabase>
#include <QHeaderView>
#include <QMouseEvent>
@@ -34,20 +36,20 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) {
setMouseTracking(true);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &BinaryView::refresh);
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &BinaryView::refresh);
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &BinaryView::refresh);
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &BinaryView::refresh);
addShortcuts();
setWhatsThis(R"(
<b>Binary View</b><br/>
<!-- TODO: add descprition here -->
<!-- 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 singedness: <span style="background-color:lightGray;color:gray">&nbsp;s&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>,
@@ -64,7 +66,7 @@ void BinaryView::addShortcuts() {
QObject::connect(shortcut_delete_backspace, &QShortcut::activated, shortcut_delete_x, &QShortcut::activated);
QObject::connect(shortcut_delete_x, &QShortcut::activated, [=]{
if (hovered_sig != nullptr) {
UndoStack::push(new RemoveSigCommand(model->msg_id, hovered_sig));
UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, hovered_sig));
hovered_sig = nullptr;
}
});
@@ -111,7 +113,8 @@ 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;
if ((sig && item_sigs.contains(sig)) || (hovered_sig && item_sigs.contains(hovered_sig))) {
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});
}
@@ -123,7 +126,7 @@ void BinaryView::highlight(const cabana::Signal *sig) {
}
void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) {
auto index = indexAt(viewport()->mapFromGlobal(QCursor::pos()));
auto index = indexAt(last_mouse_pos);
if (!anchor_index.isValid() || !index.isValid())
return;
@@ -138,7 +141,7 @@ void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionF
void BinaryView::mousePressEvent(QMouseEvent *event) {
resize_sig = nullptr;
if (auto index = indexAt(event->pos()); index.isValid() && index.column() != 8) {
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);
@@ -155,15 +158,15 @@ void BinaryView::mousePressEvent(QMouseEvent *event) {
}
void BinaryView::highlightPosition(const QPoint &pos) {
if (auto index = indexAt(viewport()->mapFromGlobal(pos)); index.isValid()) {
if (auto index = indexAt(pos); index.isValid()) {
auto item = (BinaryViewModel::Item *)index.internalPointer();
const cabana::Signal *sig = item->sigs.isEmpty() ? nullptr : item->sigs.back();
const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back();
highlight(sig);
}
}
void BinaryView::mouseMoveEvent(QMouseEvent *event) {
highlightPosition(event->globalPos());
highlightPosition(last_mouse_pos = event->pos());
QTableView::mouseMoveEvent(event);
}
@@ -176,7 +179,7 @@ void BinaryView::mouseReleaseEvent(QMouseEvent *event) {
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::push(new AddSigCommand(model->msg_id, 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)
@@ -205,15 +208,15 @@ void BinaryView::refresh() {
resize_sig = nullptr;
hovered_sig = nullptr;
model->refresh();
highlightPosition(QCursor::pos());
if (underMouse()) highlightPosition(last_mouse_pos);
}
QSet<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
QSet<const cabana::Signal *> overlapping;
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 += s;
if (s->type == cabana::Signal::Type::Normal) overlapping.insert(s);
}
}
}
@@ -258,7 +261,8 @@ void BinaryViewModel::refresh() {
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()) {
qWarning() << "signal " << sig->name << "out of bounds.start_bit:" << sig->start_bit << "size:" << sig->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;
@@ -333,7 +337,7 @@ void BinaryViewModel::updateState() {
color.setAlpha(alpha);
updateItem(i, j, bit_val, color);
}
updateItem(i, 8, binary[i], last_msg.colors[i]);
updateItem(i, 8, binary[i], toQColor(last_msg.colors[i]));
}
}
@@ -404,7 +408,9 @@ bool BinaryItemDelegate::hasSignal(const QModelIndex &index, int dx, int dy, con
if (!index.isValid()) return false;
auto model = (const BinaryViewModel*)(index.model());
int idx = (index.row() + dy) * model->columnCount() + index.column() + dx;
return (idx >=0 && idx < model->items.size()) ? model->items[idx].sigs.contains(sig) : false;
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 {
@@ -418,14 +424,14 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
painter->fillRect(option.rect, item->bg_color);
}
} else if (option.state & QStyle::State_Selected) {
auto color = bin_view->resize_sig ? bin_view->resize_sig->color : option.palette.color(QPalette::Active, QPalette::Highlight);
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() || !item->sigs.contains(bin_view->resize_sig)) { // not resizing
} 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, s->color.darker(125)); // 4/5x brightness
painter->fillRect(option.rect, toQColor(s->color.darker(125))); // 4/5x brightness
} else {
drawSignalCell(painter, option, index, s);
}
@@ -433,7 +439,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
} else if (item->valid && item->bg_color.alpha() > 0) {
painter->fillRect(option.rect, item->bg_color);
}
auto color_role = item->sigs.contains(bin_view->hovered_sig) ? QPalette::BrightText : QPalette::Text;
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));
}
@@ -480,14 +486,14 @@ void BinaryItemDelegate::drawSignalCell(QPainter *painter, const QStyleOptionVie
painter->setClipRegion(QRegion(rc).subtracted(subtract));
auto item = (const BinaryViewModel::Item *)index.internalPointer();
QColor color = sig->color;
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 = sig->color.darker(125);
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());

View File

@@ -1,10 +1,9 @@
#pragma once
#include <set>
#include <tuple>
#include <vector>
#include <QList>
#include <QSet>
#include <QStyledItemDelegate>
#include <QTableView>
@@ -51,7 +50,7 @@ public:
bool is_msb = false;
bool is_lsb = false;
uint8_t val;
QList<const cabana::Signal *> sigs;
std::vector<const cabana::Signal *> sigs;
bool valid = false;
};
std::vector<Item> items;
@@ -68,7 +67,7 @@ public:
BinaryView(QWidget *parent = nullptr);
void setMessage(const MessageId &message_id);
void highlight(const cabana::Signal *sig);
QSet<const cabana::Signal*> getOverlappingSignals() const;
std::set<const cabana::Signal*> getOverlappingSignals() const;
void updateState() { model->updateState(); }
void paintEvent(QPaintEvent *event) override {
is_message_active = can->isMessageActive(model->msg_id);
@@ -95,6 +94,7 @@ private:
void highlightPosition(const QPoint &pt);
QModelIndex anchor_index;
QPoint last_mouse_pos{-1, -1};
BinaryViewModel *model;
BinaryItemDelegate *delegate;
bool is_message_active = false;

38
tools/cabana/cabana Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
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
}
# Install Qt if not found
if ! command -v qmake &> /dev/null; then
echo "Qt not found, installing dependencies..."
install_qt
fi
# Build _cabana
cd "$ROOT"
scons -u tools/cabana/_cabana cereal/messaging/bridge
exec "$DIR/_cabana" "$@"

View File

@@ -1,82 +1,195 @@
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <QApplication>
#include <QCommandLineParser>
#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
namespace {
struct CabanaArgs {
bool demo = false;
bool auto_source = false;
bool qcam = false;
bool ecam = false;
bool dcam = false;
bool msgq = false;
bool panda = false;
bool no_vipc = false;
std::string panda_serial;
std::string socketcan;
std::string zmq;
std::string bridge;
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 konn3kt.com\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"
" --ecam load wide road camera\n"
" --dcam load driver camera\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> subscribe to a zmq 'can' publisher at the specified ip-address\n"
" (a device running cereal/messaging/bridge, or\n"
" tools/cabana/konn3kt_canproxy.py on 127.0.0.1)\n"
" --bridge <ip-address> run cereal/messaging/bridge locally against the device\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, "--ecam") == 0) {
args.ecam = true;
} else if (std::strcmp(a, "--dcam") == 0) {
args.dcam = 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, "--bridge") == 0) {
if (!takeValue(argc, argv, i, args.bridge)) 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");
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
initApp(argc, argv, false);
QApplication app(argc, argv);
app.setApplicationDisplayName("Cabana");
app.setWindowIcon(QIcon(":cabana-icon.png"));
//app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui
UnixSignalHandler signalHandler;
utils::setTheme(settings.theme);
QCommandLineParser cmd_parser;
cmd_parser.addHelpOption();
cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai");
cmd_parser.addOption({"demo", "use a demo route instead of providing your own"});
cmd_parser.addOption({"auto", "Auto load the route from the best available source (no video): internal, openpilotci, comma_api, car_segments, testing_closet"});
cmd_parser.addOption({"qcam", "load qcamera"});
cmd_parser.addOption({"ecam", "load wide road camera"});
cmd_parser.addOption({"dcam", "load driver camera"});
cmd_parser.addOption({"msgq", "read can messages from the msgq"});
cmd_parser.addOption({"panda", "read can messages from panda"});
cmd_parser.addOption({"panda-serial", "read can messages from panda with given serial", "panda-serial"});
if (SocketCanStream::available()) {
cmd_parser.addOption({"socketcan", "read can messages from given SocketCAN device", "socketcan"});
CabanaArgs args;
bool args_ok = false;
if (const int code = parseArgs(argc, argv, args, args_ok); !args_ok) {
return code;
}
cmd_parser.addOption({"zmq", "read can messages from zmq at the specified ip-address", "ip-address"});
cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"});
cmd_parser.addOption({"no-vipc", "do not output video"});
cmd_parser.addOption({"dbc", "dbc file to open", "dbc"});
cmd_parser.process(app);
AbstractStream *stream = nullptr;
if (cmd_parser.isSet("msgq")) {
stream = new DeviceStream(&app);
} else if (cmd_parser.isSet("zmq")) {
stream = new DeviceStream(&app, cmd_parser.value("zmq"));
} else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) {
if (args.msgq) {
stream = new DeviceStream(&app, DeviceStream::Mode::Msgq);
} else if (!args.zmq.empty()) {
stream = new DeviceStream(&app, DeviceStream::Mode::Zmq, QString::fromStdString(args.zmq));
} else if (!args.bridge.empty()) {
stream = new DeviceStream(&app, DeviceStream::Mode::Bridge, QString::fromStdString(args.bridge));
} else if (args.panda || !args.panda_serial.empty()) {
try {
stream = new PandaStream(&app, {.serial = cmd_parser.value("panda-serial")});
stream = new PandaStream(&app, {.serial = args.panda_serial});
} catch (std::exception &e) {
qWarning() << e.what();
fprintf(stderr, "%s\n", e.what());
return 0;
}
} else if (SocketCanStream::available() && cmd_parser.isSet("socketcan")) {
stream = new SocketCanStream(&app, {.device = cmd_parser.value("socketcan")});
#ifdef __linux__
} else if (SocketCanStream::available() && !args.socketcan.empty()) {
stream = new SocketCanStream(&app, {.device = args.socketcan});
#endif
} else {
uint32_t replay_flags = REPLAY_FLAG_NONE;
if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM;
if (cmd_parser.isSet("qcam")) replay_flags |= REPLAY_FLAG_QCAMERA;
if (cmd_parser.isSet("dcam")) replay_flags |= REPLAY_FLAG_DCAM;
if (cmd_parser.isSet("no-vipc")) replay_flags |= REPLAY_FLAG_NO_VIPC;
if (args.ecam) replay_flags |= REPLAY_FLAG_ECAM;
if (args.qcam) replay_flags |= REPLAY_FLAG_QCAMERA;
if (args.dcam) replay_flags |= REPLAY_FLAG_DCAM;
if (args.no_vipc) replay_flags |= REPLAY_FLAG_NO_VIPC;
const QStringList args = cmd_parser.positionalArguments();
QString route;
if (args.size() > 0) {
route = args.first();
} else if (cmd_parser.isSet("demo")) {
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>(&app);
bool auto_source = cmd_parser.isSet("auto");
if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags, auto_source)) {
if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) {
return 0;
}
stream = replay_stream.release();
}
}
MainWindow w(stream, cmd_parser.value("dbc"));
MainWindow w(stream, QString::fromStdString(args.dbc));
return app.exec();
}

View File

@@ -1,137 +1,40 @@
#include "tools/cabana/cameraview.h"
#ifdef __APPLE__
#include <OpenGL/gl3.h>
#else
#include <GLES3/gl3.h>
#endif
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <QApplication>
#include <QPainter>
namespace {
const char frame_vertex_shader[] =
#ifdef __APPLE__
"#version 330 core\n"
#else
"#version 300 es\n"
#endif
"layout(location = 0) in vec4 aPosition;\n"
"layout(location = 1) in vec2 aTexCoord;\n"
"uniform mat4 uTransform;\n"
"out vec2 vTexCoord;\n"
"void main() {\n"
" gl_Position = uTransform * aPosition;\n"
" vTexCoord = aTexCoord;\n"
"}\n";
const char frame_fragment_shader[] =
#ifdef __APPLE__
"#version 330 core\n"
#else
"#version 300 es\n"
"precision mediump float;\n"
#endif
"uniform sampler2D uTextureY;\n"
"uniform sampler2D uTextureUV;\n"
"in vec2 vTexCoord;\n"
"out vec4 colorOut;\n"
"void main() {\n"
" float y = texture(uTextureY, vTexCoord).r;\n"
" vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n"
" float r = y + 1.402 * uv.y;\n"
" float g = y - 0.344 * uv.x - 0.714 * uv.y;\n"
" float b = y + 1.772 * uv.x;\n"
" colorOut = vec4(r, g, b, 1.0);\n"
"}\n";
} // namespace
#include "common/yuv.h"
CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) :
stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QOpenGLWidget(parent) {
stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QWidget(parent) {
setAttribute(Qt::WA_OpaquePaintEvent);
qRegisterMetaType<std::set<VisionStreamType>>("availableStreams");
QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection);
QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection);
QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection);
QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread);
}
CameraWidget::~CameraWidget() {
makeCurrent();
stopVipcThread();
if (isValid()) {
glDeleteVertexArrays(1, &frame_vao);
glDeleteBuffers(1, &frame_vbo);
glDeleteBuffers(1, &frame_ibo);
glDeleteTextures(2, textures);
shader_program_.reset();
}
doneCurrent();
}
void CameraWidget::initializeGL() {
initializeOpenGLFunctions();
shader_program_ = std::make_unique<QOpenGLShaderProgram>(context());
shader_program_->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader);
shader_program_->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader);
shader_program_->link();
GLint frame_pos_loc = shader_program_->attributeLocation("aPosition");
GLint frame_texcoord_loc = shader_program_->attributeLocation("aTexCoord");
auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f);
const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3};
const float frame_coords[4][4] = {
{-1.0, -1.0, x2, y1}, // bl
{-1.0, 1.0, x2, y2}, // tl
{ 1.0, 1.0, x1, y2}, // tr
{ 1.0, -1.0, x1, y1}, // br
};
glGenVertexArrays(1, &frame_vao);
glBindVertexArray(frame_vao);
glGenBuffers(1, &frame_vbo);
glBindBuffer(GL_ARRAY_BUFFER, frame_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW);
glEnableVertexAttribArray(frame_pos_loc);
glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE,
sizeof(frame_coords[0]), (const void *)0);
glEnableVertexAttribArray(frame_texcoord_loc);
glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE,
sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2));
glGenBuffers(1, &frame_ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glGenTextures(2, textures);
shader_program_->bind();
shader_program_->setUniformValue("uTextureY", 0);
shader_program_->setUniformValue("uTextureUV", 1);
shader_program_->release();
}
void CameraWidget::showEvent(QShowEvent *event) {
if (!vipc_thread) {
if (!vipc_thread.joinable()) {
clearFrames();
vipc_thread = new QThread();
connect(vipc_thread, &QThread::started, [=]() { vipcThread(); });
connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater);
vipc_thread->start();
vipc_exit = false;
vipc_thread = std::thread(&CameraWidget::vipcThread, this);
}
}
void CameraWidget::stopVipcThread() {
makeCurrent();
if (vipc_thread) {
vipc_thread->requestInterruption();
vipc_thread->quit();
vipc_thread->wait();
vipc_thread = nullptr;
vipc_exit = true;
if (vipc_thread.joinable()) {
vipc_thread.join();
}
}
@@ -139,74 +42,29 @@ void CameraWidget::availableStreamsUpdated(std::set<VisionStreamType> streams) {
available_streams = streams;
}
void CameraWidget::paintGL() {
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF());
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
void CameraWidget::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.fillRect(rect(), bg);
std::lock_guard lk(frame_lock);
if (!current_frame_) return;
if (rgb_frame.isNull()) return;
// Scale for aspect ratio
float widget_ratio = (float)width() / height();
float frame_ratio = (float)stream_width / stream_height;
float scale_x = std::min(frame_ratio / widget_ratio, 1.0f);
float scale_y = std::min(widget_ratio / frame_ratio, 1.0f);
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);
glViewport(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio());
shader_program_->bind();
QMatrix4x4 transform;
transform.scale(scale_x, scale_y, 1.0f);
shader_program_->setUniformValue("uTransform", transform);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textures[0]);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, current_frame_->y);
glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, textures[1]);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, current_frame_->uv);
glBindVertexArray(frame_vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, nullptr);
glBindVertexArray(0);
// Reset both texture units
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
shader_program_->release();
}
void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) {
makeCurrent();
stream_width = vipc_client->buffers[0].width;
stream_height = vipc_client->buffers[0].height;
stream_stride = vipc_client->buffers[0].stride;
glBindTexture(GL_TEXTURE_2D, textures[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
assert(glGetError() == GL_NO_ERROR);
glBindTexture(GL_TEXTURE_2D, textures[1]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
assert(glGetError() == GL_NO_ERROR);
p.setRenderHint(QPainter::SmoothPixmapTransform);
if (active_stream_type == VISION_STREAM_DRIVER) {
// mirror driver 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::vipcFrameReceived() {
@@ -218,10 +76,11 @@ void CameraWidget::vipcThread() {
std::unique_ptr<VisionIpcClient> vipc_client;
VisionIpcBufExtra frame_meta = {};
while (!QThread::currentThread()->isInterruptionRequested()) {
while (!vipc_exit) {
if (!vipc_client || cur_stream != requested_stream_type) {
clearFrames();
qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream;
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));
}
@@ -231,23 +90,27 @@ void CameraWidget::vipcThread() {
clearFrames();
auto streams = VisionIpcClient::getAvailableStreams(stream_name, false);
if (streams.empty()) {
QThread::msleep(100);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}
emit vipcAvailableStreamsUpdated(streams);
if (!vipc_client->connect(false)) {
QThread::msleep(100);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}
emit vipcThreadConnected(vipc_client.get());
}
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);
current_frame_ = buf;
frame_meta_ = frame_meta;
rgb_frame.swap(rgb_back);
}
emit vipcThreadFrameReceived();
}
@@ -256,6 +119,7 @@ void CameraWidget::vipcThread() {
void CameraWidget::clearFrames() {
std::lock_guard lk(frame_lock);
current_frame_ = nullptr;
rgb_frame = QImage();
rgb_back = QImage();
available_streams.clear();
}

View File

@@ -1,23 +1,21 @@
#pragma once
#include <memory>
#include <atomic>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLWidget>
#include <QThread>
#include <QImage>
#include <QWidget>
#include "msgq/visionipc/visionipc_client.h"
class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions {
class CameraWidget : public QWidget {
Q_OBJECT
public:
using QOpenGLWidget::QOpenGLWidget;
explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr);
~CameraWidget();
void setStreamType(VisionStreamType type) { requested_stream_type = type; }
@@ -26,37 +24,30 @@ public:
signals:
void clicked();
void vipcThreadConnected(VisionIpcClient *);
void vipcThreadFrameReceived();
void vipcAvailableStreamsUpdated(std::set<VisionStreamType>);
protected:
void paintGL() override;
void initializeGL() override;
void paintEvent(QPaintEvent *event) override;
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override { stopVipcThread(); }
void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); }
void vipcThread();
void clearFrames();
GLuint frame_vao, frame_vbo, frame_ibo;
GLuint textures[2];
std::unique_ptr<QOpenGLShaderProgram> shader_program_;
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;
int stream_width = 0;
int stream_height = 0;
int stream_stride = 0;
std::atomic<VisionStreamType> active_stream_type;
std::atomic<VisionStreamType> requested_stream_type;
std::set<VisionStreamType> available_streams;
QThread *vipc_thread = nullptr;
std::recursive_mutex frame_lock;
VisionBuf* current_frame_ = nullptr;
VisionIpcBufExtra frame_meta_ = {};
std::thread vipc_thread;
std::atomic<bool> vipc_exit = false;
std::mutex frame_lock;
protected slots:
void vipcConnected(VisionIpcClient *vipc_client);
void vipcFrameReceived();
void availableStreamsUpdated(std::set<VisionStreamType> streams);
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,11 @@
#pragma once
#include <functional>
#include <tuple>
#include <utility>
#include <vector>
#include <QMenu>
#include <QGraphicsPixmapItem>
#include <QGraphicsProxyWidget>
#include <QtCharts/QChartView>
#include <QtCharts/QLegendMarker>
#include <QtCharts/QLineSeries>
#include <QtCharts/QScatterSeries>
#include <QtCharts/QValueAxis>
using namespace QtCharts;
#include "tools/cabana/chart/tiplabel.h"
#include "tools/cabana/dbc/dbcmanager.h"
@@ -25,7 +18,7 @@ enum class SeriesType {
};
class ChartsWidget;
class ChartView : public QChartView {
class ChartView : public QWidget {
Q_OBJECT
public:
@@ -38,13 +31,15 @@ public:
void updatePlotArea(int left, bool force = false);
void showTip(double sec);
void hideTip();
void startAnimation();
double secondsAtPoint(const QPointF &pt) const { return chart()->mapToValue(pt).x(); }
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;
QXYSeries *series = nullptr;
QColor color;
bool visible = true;
std::vector<QPointF> vals;
std::vector<QPointF> step_vals;
QPointF track_pt{};
@@ -59,7 +54,6 @@ signals:
private slots:
void signalUpdated(const cabana::Signal *sig);
void manageSignals();
void handleMarkerClicked();
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; }); }
@@ -68,52 +62,65 @@ 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 addSeries(QXYSeries *series);
void contextMenuEvent(QContextMenuEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *ev) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator(false); }
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
QSize sizeHint() const override;
void updateAxisY();
void updateTitle();
void resetChartCache();
void setTheme(QChart::ChartTheme theme);
void paintEvent(QPaintEvent *event) override;
void drawForeground(QPainter *painter, const QRectF &rect) override;
void drawBackground(QPainter *painter, const QRectF &rect) override;
void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); }
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);
QXYSeries *createSeries(SeriesType type, QColor color);
void setSeriesColor(QXYSeries *, QColor color);
void updateSeriesPoints();
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;
QValueAxis *axis_x;
QValueAxis *axis_y;
// 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;
QGraphicsPixmapItem *move_icon;
QGraphicsProxyWidget *close_btn_proxy;
QGraphicsProxyWidget *manage_btn_proxy;
ToolButton *manage_btn;
ToolButton *close_btn;
TipLabel *tip_label;
std::vector<SigItem> sigs;
double cur_sec = 0;
SeriesType series_type = SeriesType::Line;
bool is_scrubbing = false;
bool resume_after_scrub = false;
QPixmap chart_pixmap;
bool can_drop = false;
double tooltip_x = -1;

View File

@@ -1,13 +1,14 @@
#include "tools/cabana/chart/chartswidget.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <algorithm>
#include <future>
#include <QApplication>
#include <QFutureSynchronizer>
#include <QMenu>
#include <QMouseEvent>
#include <QScrollBar>
#include <QToolBar>
#include <QtConcurrent>
#include "tools/cabana/chart/chart.h"
@@ -71,11 +72,14 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
range_slider_action = toolbar->addWidget(range_slider);
// zoom controls
zoom_undo_stack = new QUndoStack(this);
toolbar->addAction(undo_zoom_action = zoom_undo_stack->createUndoAction(this));
undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise"));
toolbar->addAction(redo_zoom_action = zoom_undo_stack->createRedoAction(this));
redo_zoom_action->setIcon(utils::icon("arrow-clockwise"));
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);
zoom_undo_stack.setCallbacks({.index_changed = [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);
@@ -88,8 +92,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
tabbar->setAutoHide(true);
tabbar->setExpanding(false);
tabbar->setDrawBase(true);
tabbar->setAcceptDrops(true);
tabbar->setChangeCurrentOnDrag(true);
tabbar->setUsesScrollButtons(true);
main_layout->addWidget(tabbar);
@@ -104,6 +106,11 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
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);
@@ -115,7 +122,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
align_timer->setSingleShot(true);
QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts);
QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll);
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &ChartsWidget::removeAll);
QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged);
QObject::connect(can, &AbstractStream::msgsReceived, this, &ChartsWidget::updateState);
QObject::connect(can, &AbstractStream::seeking, this, &ChartsWidget::updateState);
@@ -166,15 +173,16 @@ void ChartsWidget::removeTab(int index) {
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(charts_in_tab.count()));
tabbar->setTabText(i, QString("Tab %1 (%2)").arg(i + 1).arg((int)charts_in_tab.size()));
}
}
void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) {
QFutureSynchronizer<void> future_synchronizer;
std::vector<std::future<void>> futures;
for (auto c : charts) {
future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, nullptr, &new_events));
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) {
@@ -184,7 +192,7 @@ void ChartsWidget::timeRangeChanged(const std::optional<std::pair<double, double
void ChartsWidget::zoomReset() {
can->setTimeRange(std::nullopt);
zoom_undo_stack->clear();
zoom_undo_stack.clear();
}
QRect ChartsWidget::chartVisibleRect(ChartView *chart) {
@@ -203,7 +211,7 @@ void ChartsWidget::showValueTip(double sec) {
}
void ChartsWidget::updateState() {
if (charts.isEmpty()) return;
if (charts.empty()) return;
const auto &time_range = can->timeRange();
const double cur_sec = can->currentSec();
@@ -247,17 +255,13 @@ void ChartsWidget::updateToolBar() {
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.isEmpty());
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"));
auto theme = utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight;
for (auto c : charts) {
c->setTheme(theme);
}
}
if (range_slider->maximum() != settings.max_cached_minutes * 60) {
range_slider->setRange(1, settings.max_cached_minutes * 60);
@@ -281,9 +285,9 @@ ChartView *ChartsWidget::createChart(int pos) {
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, charts.size());
charts.insert(pos, chart);
currentCharts().insert(pos, chart);
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;
@@ -302,15 +306,11 @@ void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, boo
void ChartsWidget::splitChart(ChartView *src_chart) {
if (src_chart->sigs.size() > 1) {
int pos = charts.indexOf(src_chart) + 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);
src_chart->chart()->removeSeries(it->series);
// Restore to the original color
it->series->setColor(it->sig->color);
c->addSeries(it->series);
it->color = toQColor(it->sig->color);
c->sigs.emplace_back(std::move(*it));
c->updateAxisY();
c->updateTitle();
@@ -318,6 +318,7 @@ void ChartsWidget::splitChart(ChartView *src_chart) {
}
src_chart->updateAxisY();
src_chart->updateTitle();
updateState();
QTimer::singleShot(0, src_chart, &ChartView::resetChartCache);
}
}
@@ -327,7 +328,7 @@ QStringList ChartsWidget::serializeChartIds() const {
for (auto c : charts) {
QStringList ids;
for (const auto& s : c->sigs)
ids += QString("%1|%2").arg(s.msg_id.toString(), s.sig->name);
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());
@@ -340,9 +341,9 @@ void ChartsWidget::restoreChartsFromIds(const QStringList& chart_ids) {
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]);
MessageId msg_id = MessageId::fromString(sig_parts[0].toStdString());
if (auto* msg = dbc()->msg(msg_id))
if (auto* sig = msg->sig(sig_parts[1]))
if (auto* sig = msg->sig(sig_parts[1].toStdString()))
showChart(msg_id, sig, true, index++ > 0);
}
}
@@ -388,7 +389,87 @@ void ChartsWidget::updateLayout(bool force) {
}
}
void ChartsWidget::startAutoScroll() {
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);
}
@@ -404,7 +485,7 @@ void ChartsWidget::doAutoScroll() {
}
int value = scroll->value();
QPoint pos = charts_scroll->viewport()->mapFromGlobal(QCursor::pos());
QPoint pos = charts_scroll->viewport()->mapFromGlobal(auto_scroll_pos);
QRect area = charts_scroll->viewport()->rect();
if (pos.y() - area.top() < settings.chart_height / 2) {
@@ -412,41 +493,39 @@ void ChartsWidget::doAutoScroll() {
} else if (area.bottom() - pos.y() < settings.chart_height / 2) {
scroll->setValue(value + auto_scroll_count);
}
bool vertical_unchanged = value == scroll->value();
if (vertical_unchanged) {
if (value == scroll->value()) {
stopAutoScroll();
} else {
// mouseMoveEvent to updates the drag-selection rectangle
const QPoint globalPos = charts_scroll->viewport()->mapToGlobal(pos);
const QPoint windowPos = charts_scroll->window()->mapFromGlobal(globalPos);
QMouseEvent mm(QEvent::MouseMove, pos, windowPos, globalPos,
Qt::NoButton, Qt::LeftButton, Qt::NoModifier, Qt::MouseEventSynthesizedByQt);
QApplication::sendEvent(charts_scroll->viewport(), &mm);
} 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 * qApp->devicePixelRatio(), QWidget::minimumSizeHint().height());
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.isEmpty()) {
if (!items.empty()) {
auto c = createChart();
for (auto it : items) {
c->addSignal(it->msg_id, it->sig);
}
updateState();
}
}
}
void ChartsWidget::removeChart(ChartView *chart) {
charts.removeOne(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.removeOne(chart);
list.erase(std::remove(list.begin(), list.end(), chart), list.end());
}
updateToolBar();
updateLayout(true);
@@ -460,7 +539,7 @@ void ChartsWidget::removeAll() {
}
tab_charts.clear();
if (!charts.isEmpty()) {
if (!charts.empty()) {
for (auto c : charts) {
delete c;
}
@@ -482,6 +561,19 @@ void ChartsWidget::alignCharts() {
}
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) {
@@ -490,7 +582,7 @@ bool ChartsWidget::eventFilter(QObject *o, QEvent *e) {
for (const auto &c : charts) {
auto local_pos = c->mapFromGlobal(global_pos);
if (c->chart()->plotArea().contains(local_pos)) {
if (c->plot_area.contains(local_pos)) {
if (on_tip) {
showValueTip(c->secondsAtPoint(local_pos));
}
@@ -522,13 +614,14 @@ bool ChartsWidget::event(QEvent *event) {
break;
case QEvent::WindowDeactivate:
case QEvent::FocusOut:
if (chartDragActive()) cancelChartDrag();
showValueTip(-1);
default:
break;
}
if (back_button) {
zoom_undo_stack->undo();
zoom_undo_stack.undo();
return true; // Return true since the event has been handled
}
return QFrame::event(event);
@@ -537,7 +630,6 @@ bool ChartsWidget::event(QEvent *event) {
// ChartsContainer
ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) {
setAcceptDrops(true);
setBackgroundRole(QPalette::Window);
QVBoxLayout *charts_main_layout = new QVBoxLayout(this);
charts_main_layout->setContentsMargins(0, CHART_SPACING, 0, CHART_SPACING);
@@ -547,32 +639,6 @@ ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent),
charts_main_layout->addStretch(0);
}
void ChartsContainer::dragEnterEvent(QDragEnterEvent *event) {
if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) {
event->acceptProposedAction();
drawDropIndicator(event->pos());
}
}
void ChartsContainer::dropEvent(QDropEvent *event) {
if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) {
auto w = getDropAfter(event->pos());
auto chart = qobject_cast<ChartView *>(event->source());
if (w != chart) {
for (auto &[_, list] : charts_widget->tab_charts) {
list.removeOne(chart);
}
int to = w ? charts_widget->currentCharts().indexOf(w) + 1 : 0;
charts_widget->currentCharts().insert(to, chart);
charts_widget->updateLayout(true);
charts_widget->updateTabBar();
event->acceptProposedAction();
chart->startAnimation();
}
drawDropIndicator({});
}
}
void ChartsContainer::paintEvent(QPaintEvent *ev) {
if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) {
QRect r = geometry();

View File

@@ -8,15 +8,13 @@
#include <QScrollArea>
#include <QTimer>
#include <QToolBar>
#include <QUndoCommand>
#include <QUndoStack>
#include "tools/cabana/chart/signalselector.h"
#include "tools/cabana/commands.h"
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
const int CHART_MIN_WIDTH = 300;
const QString CHART_MIME_TYPE = "application/x-cabanachartview";
class ChartView;
class ChartsWidget;
@@ -24,9 +22,6 @@ class ChartsWidget;
class ChartsContainer : public QWidget {
public:
ChartsContainer(ChartsWidget *parent);
void dragEnterEvent(QDragEnterEvent *event) override;
void dropEvent(QDropEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator({}); }
void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); }
void paintEvent(QPaintEvent *ev) override;
ChartView *getDropAfter(const QPoint &pos) const;
@@ -69,7 +64,12 @@ private:
void eventsMerged(const MessageEventsMap &new_events);
void updateState();
void zoomReset();
void startAutoScroll();
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();
@@ -81,7 +81,7 @@ private:
bool eventFilter(QObject *obj, QEvent *event) override;
void newTab();
void removeTab(int index);
inline QList<ChartView *> &currentCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; }
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;
@@ -97,11 +97,11 @@ private:
QAction *redo_zoom_action;
QAction *reset_zoom_action;
ToolButton *reset_zoom_btn;
QUndoStack *zoom_undo_stack;
UndoStack zoom_undo_stack;
ToolButton *remove_all_btn;
QList<ChartView *> charts;
std::unordered_map<int, QList<ChartView *>> tab_charts;
std::vector<ChartView *> charts;
std::unordered_map<int, std::vector<ChartView *>> tab_charts;
TabBar *tabbar;
ChartsContainer *charts_container;
QScrollArea *charts_scroll;
@@ -110,7 +110,15 @@ private:
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;
@@ -119,11 +127,10 @@ private:
friend class ChartsContainer;
};
class ZoomCommand : public QUndoCommand {
class ZoomCommand : public UndoCommand {
public:
ZoomCommand(std::pair<double, double> range) : range(range), QUndoCommand() {
ZoomCommand(std::pair<double, double> range) : range(range) {
prev_range = can->timeRange();
setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 2).arg(range.second, 0, 'f', 2));
}
void undo() override { can->setTimeRange(prev_range); }
void redo() override { can->setTimeRange(range); }

View File

@@ -1,6 +1,6 @@
#include "tools/cabana/chart/signalselector.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <QCompleter>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QLabel>
@@ -20,8 +20,6 @@ SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent)
msgs_combo->setEditable(true);
msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg..."));
msgs_combo->setInsertPolicy(QComboBox::NoInsert);
msgs_combo->completer()->setCompletionMode(QCompleter::PopupCompletion);
msgs_combo->completer()->setFilterMode(Qt::MatchContains);
main_layout->addWidget(available_list = new QListWidget(this), 2, 0);
@@ -46,7 +44,7 @@ SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent)
for (const auto &[id, _] : can->lastMessages()) {
if (auto m = dbc()->msg(id)) {
msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(id.toString()), QVariant::fromValue(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);
@@ -92,8 +90,8 @@ void SignalSelector::updateAvailableList(int index) {
}
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(sig->color.name(), sig->name);
if (show_msg_name) text += QString(" <font color=\"gray\">%0 %1</font>").arg(msgName(id), id.toString());
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);
@@ -102,8 +100,8 @@ void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, cons
parent->setItemWidget(new_item, label);
}
QList<SignalSelector::ListItem *> SignalSelector::seletedItems() {
QList<SignalSelector::ListItem *> ret;
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;
}

View File

@@ -15,7 +15,7 @@ public:
};
SignalSelector(QString title, QWidget *parent);
QList<ListItem *> seletedItems();
std::vector<ListItem *> seletedItems();
inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); }
private:

View File

@@ -31,7 +31,7 @@ void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIt
}
freq_ = points_.size() / std::max(points_.back().x() - points_.front().x(), 1.0);
render(sig->color, range, size);
render(toQColor(sig->color), range, size);
}
void Sparkline::render(const QColor &color, int range, QSize size) {

View File

@@ -1,25 +1,86 @@
#include <QApplication>
#include "tools/cabana/commands.h"
#include <cmath>
// UndoStack
void UndoStack::push(UndoCommand *cmd) {
commands_.resize(index_); // drop any redoable commands
if (clean_index_ > index_) clean_index_ = -1;
commands_.emplace_back(cmd);
cmd->redo();
setIndex(index_ + 1);
}
void UndoStack::undo() {
if (!canUndo()) return;
commands_[index_ - 1]->undo();
setIndex(index_ - 1);
}
void UndoStack::redo() {
if (!canRedo()) return;
commands_[index_]->redo();
setIndex(index_ + 1);
}
void UndoStack::clear() {
bool was_clean = isClean();
commands_.clear();
index_ = clean_index_ = 0;
if (callbacks_.index_changed) callbacks_.index_changed();
if (!was_clean && callbacks_.clean_changed) callbacks_.clean_changed(true);
}
void UndoStack::setClean() {
if (!isClean()) {
clean_index_ = index_;
if (callbacks_.clean_changed) callbacks_.clean_changed(true);
}
}
void UndoStack::setIndex(int index) {
bool was_clean = isClean();
index_ = index;
if (callbacks_.index_changed) callbacks_.index_changed();
if (isClean() != was_clean && callbacks_.clean_changed) callbacks_.clean_changed(isClean());
}
UndoStack *UndoStack::instance() {
static UndoStack undo_stack;
return &undo_stack;
}
QtUndoNotifier::QtUndoNotifier(QObject *parent) : QObject(parent) {
UndoStack::instance()->setCallbacks({
.index_changed = [this]() { emit indexChanged(); },
.clean_changed = [this](bool clean) { emit cleanChanged(clean); },
});
}
QtUndoNotifier *undoNotifier() {
static QtUndoNotifier notifier;
return &notifier;
}
// EditMsgCommand
EditMsgCommand::EditMsgCommand(const MessageId &id, const QString &name, int size,
const QString &node, const QString &comment, QUndoCommand *parent)
: id(id), new_name(name), new_size(size), new_node(node), new_comment(comment), QUndoCommand(parent) {
EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size,
const std::string &node, const std::string &comment)
: id(id), new_name(name), new_size(size), new_node(node), new_comment(comment) {
if (auto msg = dbc()->msg(id)) {
old_name = msg->name;
old_size = msg->size;
old_node = msg->transmitter;
old_comment = msg->comment;
setText(QObject::tr("edit message %1:%2").arg(name).arg(id.address));
text = "edit message " + name + ":" + std::to_string(id.address);
} else {
setText(QObject::tr("new message %1:%2").arg(name).arg(id.address));
text = "new message " + name + ":" + std::to_string(id.address);
}
}
void EditMsgCommand::undo() {
if (old_name.isEmpty())
if (old_name.empty())
dbc()->removeMsg(id);
else
dbc()->updateMsg(id, old_name, old_size, old_node, old_comment);
@@ -31,15 +92,15 @@ void EditMsgCommand::redo() {
// RemoveMsgCommand
RemoveMsgCommand::RemoveMsgCommand(const MessageId &id, QUndoCommand *parent) : id(id), QUndoCommand(parent) {
RemoveMsgCommand::RemoveMsgCommand(const MessageId &id) : id(id) {
if (auto msg = dbc()->msg(id)) {
message = *msg;
setText(QObject::tr("remove message %1:%2").arg(message.name).arg(id.address));
text = "remove message " + message.name + ":" + std::to_string(id.address);
}
}
void RemoveMsgCommand::undo() {
if (!message.name.isEmpty()) {
if (!message.name.empty()) {
dbc()->updateMsg(id, message.name, message.size, message.transmitter, message.comment);
for (auto s : message.getSignals())
dbc()->addSignal(id, *s);
@@ -47,15 +108,15 @@ void RemoveMsgCommand::undo() {
}
void RemoveMsgCommand::redo() {
if (!message.name.isEmpty())
if (!message.name.empty())
dbc()->removeMsg(id);
}
// AddSigCommand
AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent)
: id(id), signal(sig), QUndoCommand(parent) {
setText(QObject::tr("add signal %1 to %2:%3").arg(sig.name).arg(msgName(id)).arg(id.address));
AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig)
: id(id), signal(sig) {
text = "add signal " + sig.name + " to " + msgName(id) + ":" + std::to_string(id.address);
}
void AddSigCommand::undo() {
@@ -75,8 +136,7 @@ void AddSigCommand::redo() {
// RemoveSigCommand
RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent)
: id(id), QUndoCommand(parent) {
RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig) : id(id) {
sigs.push_back(*sig);
if (sig->type == cabana::Signal::Type::Multiplexor) {
for (const auto &s : dbc()->msg(id)->sigs) {
@@ -85,7 +145,7 @@ RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *si
}
}
}
setText(QObject::tr("remove signal %1 from %2:%3").arg(sig->name).arg(msgName(id)).arg(id.address));
text = "remove signal " + sig->name + " from " + msgName(id) + ":" + std::to_string(id.address);
}
void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); }
@@ -93,8 +153,8 @@ void RemoveSigCommand::redo() { for (const auto &s : sigs) dbc()->removeSignal(i
// EditSignalCommand
EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent)
: id(id), QUndoCommand(parent) {
EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig)
: id(id) {
sigs.push_back({*sig, new_sig});
if (sig->type == cabana::Signal::Type::Multiplexor && new_sig.type == cabana::Signal::Type::Normal) {
// convert all multiplexed signals to normal signals
@@ -108,17 +168,8 @@ EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *
}
}
}
setText(QObject::tr("edit signal %1 in %2:%3").arg(sig->name).arg(msgName(id)).arg(id.address));
text = "edit signal " + sig->name + " in " + msgName(id) + ":" + std::to_string(id.address);
}
void EditSignalCommand::undo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.second.name, s.first); }
void EditSignalCommand::redo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.first.name, s.second); }
namespace UndoStack {
QUndoStack *instance() {
static QUndoStack *undo_stack = new QUndoStack(qApp);
return undo_stack;
}
} // namespace UndoStack

View File

@@ -1,29 +1,82 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <QUndoCommand>
#include <QUndoStack>
#include <QObject>
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
class EditMsgCommand : public QUndoCommand {
class UndoCommand {
public:
EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &node,
const QString &comment, QUndoCommand *parent = nullptr);
virtual ~UndoCommand() = default;
virtual void undo() = 0;
virtual void redo() = 0;
std::string text;
};
class UndoStack {
public:
struct Callbacks {
std::function<void()> index_changed;
std::function<void(bool)> clean_changed;
};
void push(UndoCommand *cmd); // takes ownership and calls redo()
void undo();
void redo();
void clear();
void setClean();
bool isClean() const { return clean_index_ == index_; }
bool canUndo() const { return index_ > 0; }
bool canRedo() const { return index_ < (int)commands_.size(); }
std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; }
std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; }
void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); }
static UndoStack *instance();
private:
void setIndex(int index);
std::vector<std::unique_ptr<UndoCommand>> commands_;
int index_ = 0;
int clean_index_ = 0;
Callbacks callbacks_;
};
// emits Qt signals for the global undo stack
class QtUndoNotifier : public QObject {
Q_OBJECT
public:
explicit QtUndoNotifier(QObject *parent = nullptr);
signals:
void indexChanged();
void cleanChanged(bool clean);
};
QtUndoNotifier *undoNotifier();
class EditMsgCommand : public UndoCommand {
public:
EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node,
const std::string &comment);
void undo() override;
void redo() override;
private:
const MessageId id;
QString old_name, new_name, old_comment, new_comment, old_node, new_node;
std::string old_name, new_name, old_comment, new_comment, old_node, new_node;
int old_size = 0, new_size = 0;
};
class RemoveMsgCommand : public QUndoCommand {
class RemoveMsgCommand : public UndoCommand {
public:
RemoveMsgCommand(const MessageId &id, QUndoCommand *parent = nullptr);
RemoveMsgCommand(const MessageId &id);
void undo() override;
void redo() override;
@@ -32,9 +85,9 @@ private:
cabana::Msg message;
};
class AddSigCommand : public QUndoCommand {
class AddSigCommand : public UndoCommand {
public:
AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent = nullptr);
AddSigCommand(const MessageId &id, const cabana::Signal &sig);
void undo() override;
void redo() override;
@@ -44,29 +97,24 @@ private:
cabana::Signal signal = {};
};
class RemoveSigCommand : public QUndoCommand {
class RemoveSigCommand : public UndoCommand {
public:
RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent = nullptr);
RemoveSigCommand(const MessageId &id, const cabana::Signal *sig);
void undo() override;
void redo() override;
private:
const MessageId id;
QList<cabana::Signal> sigs;
std::vector<cabana::Signal> sigs;
};
class EditSignalCommand : public QUndoCommand {
class EditSignalCommand : public UndoCommand {
public:
EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent = nullptr);
EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig);
void undo() override;
void redo() override;
private:
const MessageId id;
QList<std::pair<cabana::Signal, cabana::Signal>> sigs; // QList<{old_sig, new_sig}>
};
namespace UndoStack {
QUndoStack *instance();
inline void push(QUndoCommand *cmd) { instance()->push(cmd); }
std::vector<std::pair<cabana::Signal, cabana::Signal>> sigs; // {old_sig, new_sig}
};

View File

@@ -0,0 +1,46 @@
#pragma once
#include <array>
#include <cstdint>
#include <unordered_map>
#include <vector>
#include "tools/cabana/core/color.h"
#include "tools/cabana/core/message_id.h"
struct CanData {
void compute(const MessageId &msg_id, const uint8_t *data, int size, double current_sec,
double playback_speed, const std::vector<uint8_t> &mask, double frequency = 0);
double ts = 0.;
uint32_t count = 0;
double freq = 0;
std::vector<uint8_t> dat;
std::vector<CabanaColor> colors;
struct ByteLastChange {
double ts = 0;
int delta = 0;
int same_delta_counter = 0;
bool suppressed = false;
};
std::vector<ByteLastChange> last_changes;
std::vector<std::array<uint32_t, 8>> bit_flip_counts;
double last_freq_update_ts = 0;
};
struct CanEvent {
uint8_t src;
uint32_t address;
uint64_t mono_time;
uint8_t size;
uint8_t dat[];
};
struct CompareCanEvent {
constexpr bool operator()(const CanEvent *const event, uint64_t ts) const { return event->mono_time < ts; }
constexpr bool operator()(uint64_t ts, const CanEvent *const event) const { return ts < event->mono_time; }
};
using MessageEventsMap = std::unordered_map<MessageId, std::vector<const CanEvent *>>;
using CanEventIter = std::vector<const CanEvent *>::const_iterator;

79
tools/cabana/core/color.h Normal file
View File

@@ -0,0 +1,79 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
struct CabanaColor {
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
uint8_t a = 255;
constexpr CabanaColor() = default;
constexpr CabanaColor(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = 255)
: r(red), g(green), b(blue), a(alpha) {}
static CabanaColor fromHsv(float hue, float saturation, float value, float alpha = 1.0f) {
const float h = hue - std::floor(hue);
const float c = value * saturation;
const float x = c * (1.0f - std::fabs(std::fmod(h * 6.0f, 2.0f) - 1.0f));
const float m = value - c;
float red = 0, green = 0, blue = 0;
switch (static_cast<int>(h * 6.0f) % 6) {
case 0: red = c; green = x; break;
case 1: red = x; green = c; break;
case 2: green = c; blue = x; break;
case 3: green = x; blue = c; break;
case 4: red = x; blue = c; break;
default: red = c; blue = x; break;
}
auto channel = [m](float v) { return static_cast<uint8_t>(std::clamp((v + m) * 255.0f, 0.0f, 255.0f) + 0.5f); };
return {channel(red), channel(green), channel(blue),
static_cast<uint8_t>(std::clamp(alpha * 255.0f, 0.0f, 255.0f) + 0.5f)};
}
CabanaColor darker(int factor = 200) const {
if (factor <= 0) return *this;
if (factor < 100) return lighter(10000 / factor);
auto [hue, saturation, value] = hsv();
return fromHsv(hue, saturation, value * 100.0f / factor, a / 255.0f);
}
CabanaColor lighter(int factor = 150) const {
if (factor <= 0) return *this;
if (factor < 100) return darker(10000 / factor);
auto [hue, saturation, value] = hsv();
const float scaled_value = value * factor / 100.0f;
if (scaled_value > 1.0f) saturation = std::max(0.0f, saturation - (scaled_value - 1.0f));
return fromHsv(hue, saturation, std::min(1.0f, scaled_value), a / 255.0f);
}
constexpr int red() const { return r; }
constexpr int green() const { return g; }
constexpr int blue() const { return b; }
constexpr int alpha() const { return a; }
float alphaF() const { return a / 255.0f; }
void setAlphaF(float alpha) { a = static_cast<uint8_t>(std::clamp(alpha * 255.0f, 0.0f, 255.0f) + 0.5f); }
constexpr bool operator==(const CabanaColor &other) const {
return r == other.r && g == other.g && b == other.b && a == other.a;
}
private:
struct Hsv { float hue; float saturation; float value; };
Hsv hsv() const {
const float red = r / 255.0f, green = g / 255.0f, blue = b / 255.0f;
const float maximum = std::max({red, green, blue});
const float minimum = std::min({red, green, blue});
const float delta = maximum - minimum;
float hue = 0;
if (delta > 0) {
if (maximum == red) hue = std::fmod((green - blue) / delta, 6.0f) / 6.0f;
else if (maximum == green) hue = ((blue - red) / delta + 2.0f) / 6.0f;
else hue = ((red - green) / delta + 4.0f) / 6.0f;
if (hue < 0) hue += 1.0f;
}
return {hue, maximum == 0 ? 0 : delta / maximum, maximum};
}
};

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <cstdio>
#include <functional>
#include <string>
#include <tuple>
constexpr int INVALID_SOURCE = 0xff;
struct MessageId {
uint8_t source = 0;
uint32_t address = 0;
std::string toString() const { char b[64]; snprintf(b, sizeof(b), "%u:%X", source, address); return b; }
static MessageId fromString(const std::string &s) {
const auto p = s.find(':');
if (p == std::string::npos) return {};
return {.source = static_cast<uint8_t>(std::stoul(s.substr(0, p))), .address = static_cast<uint32_t>(std::stoul(s.substr(p + 1), nullptr, 16))};
}
bool operator==(const MessageId &o) const { return source == o.source && address == o.address; }
bool operator!=(const MessageId &o) const { return !(*this == o); }
bool operator<(const MessageId &o) const { return std::tie(source, address) < std::tie(o.source, o.address); }
bool operator>(const MessageId &o) const { return o < *this; }
};
template <> struct std::hash<MessageId> {
size_t operator()(const MessageId &id) const noexcept { return std::hash<uint8_t>{}(id.source) ^ (std::hash<uint32_t>{}(id.address) << 1); }
};

View File

@@ -0,0 +1,34 @@
#pragma once
#include <string>
#include <vector>
constexpr int LIGHT_THEME = 1;
constexpr int DARK_THEME = 2;
struct CabanaSettingsState {
enum DragDirection { MsbFirst, LsbFirst, AlwaysLE, AlwaysBE };
bool absolute_time = false;
int fps = 10;
int max_cached_minutes = 30;
int chart_height = 200;
int chart_column_count = 1;
int chart_range = 3 * 60;
int chart_series_type = 0;
int theme = 0;
int sparkline_range = 15;
bool multiple_lines_hex = false;
bool log_livestream = true;
bool suppress_defined_signals = false;
std::string log_path;
std::string last_dir;
std::string last_route_dir;
std::vector<std::string> recent_files;
DragDirection drag_direction = MsbFirst;
std::string recent_dbc_file;
std::string active_msg_id;
std::vector<std::string> selected_msg_ids;
std::vector<std::string> active_charts;
};

View File

@@ -1,11 +1,17 @@
#include "tools/cabana/dbc/dbc.h"
#include <algorithm>
#include <cmath>
#include "tools/cabana/utils/util.h"
uint qHash(const MessageId &item) {
return qHash(item.source) ^ qHash(item.address);
namespace {
int numDecimals(double value) {
int decimals = 0;
while (decimals < 6 && std::fabs(value - std::round(value)) > 1e-9) {
value *= 10.0;
++decimals;
}
return decimals;
}
}
// cabana::Msg
@@ -22,7 +28,7 @@ cabana::Signal *cabana::Msg::addSignal(const cabana::Signal &sig) {
return s;
}
cabana::Signal *cabana::Msg::updateSignal(const QString &sig_name, const cabana::Signal &new_sig) {
cabana::Signal *cabana::Msg::updateSignal(const std::string &sig_name, const cabana::Signal &new_sig) {
auto s = sig(sig_name);
if (s) {
*s = new_sig;
@@ -31,7 +37,7 @@ cabana::Signal *cabana::Msg::updateSignal(const QString &sig_name, const cabana:
return s;
}
void cabana::Msg::removeSignal(const QString &sig_name) {
void cabana::Msg::removeSignal(const std::string &sig_name) {
auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s->name == sig_name; });
if (it != sigs.end()) {
delete *it;
@@ -57,7 +63,7 @@ cabana::Msg &cabana::Msg::operator=(const cabana::Msg &other) {
return *this;
}
cabana::Signal *cabana::Msg::sig(const QString &sig_name) const {
cabana::Signal *cabana::Msg::sig(const std::string &sig_name) const {
auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s->name == sig_name; });
return it != sigs.end() ? *it : nullptr;
}
@@ -69,17 +75,17 @@ int cabana::Msg::indexOf(const cabana::Signal *sig) const {
return -1;
}
QString cabana::Msg::newSignalName() {
QString new_name;
std::string cabana::Msg::newSignalName() {
std::string new_name;
for (int i = 1; /**/; ++i) {
new_name = QString("NEW_SIGNAL_%1").arg(i);
new_name = "NEW_SIGNAL_" + std::to_string(i);
if (sig(new_name) == nullptr) break;
}
return new_name;
}
void cabana::Msg::update() {
if (transmitter.isEmpty()) {
if (transmitter.empty()) {
transmitter = DEFAULT_NODE_NAME;
}
mask.assign(size, 0x00);
@@ -129,21 +135,21 @@ void cabana::Msg::update() {
void cabana::Signal::update() {
updateMsbLsb(*this);
if (receiver_name.isEmpty()) {
if (receiver_name.empty()) {
receiver_name = DEFAULT_NODE_NAME;
}
float h = 19 * (float)lsb / 64.0;
h = fmod(h, 1.0);
size_t hash = qHash(name);
size_t hash = std::hash<std::string>{}(name);
float s = 0.25 + 0.25 * (float)(hash & 0xff) / 255.0;
float v = 0.75 + 0.25 * (float)((hash >> 8) & 0xff) / 255.0;
color = QColor::fromHsvF(h, s, v);
precision = std::max(num_decimals(factor), num_decimals(offset));
color = CabanaColor::fromHsv(h, s, v);
precision = std::max(numDecimals(factor), numDecimals(offset));
}
QString cabana::Signal::formatValue(double value, bool with_unit) const {
std::string cabana::Signal::formatValue(double value, bool with_unit) const {
// Show enum string
int64_t raw_value = round((value - offset) / factor);
for (const auto &[val, desc] : val_desc) {
@@ -152,8 +158,10 @@ QString cabana::Signal::formatValue(double value, bool with_unit) const {
}
}
QString val_str = QString::number(value, 'f', precision);
if (with_unit && !unit.isEmpty()) {
char buf[64];
snprintf(buf, sizeof(buf), "%.*f", precision, value);
std::string val_str(buf);
if (with_unit && !unit.empty()) {
val_str += " " + unit;
}
return val_str;

View File

@@ -1,57 +1,21 @@
#pragma once
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include <QColor>
#include <QMetaType>
#include <QString>
#include "tools/cabana/core/color.h"
#include "tools/cabana/core/message_id.h"
const QString UNTITLED = "untitled";
const QString DEFAULT_NODE_NAME = "XXX";
const std::string UNTITLED = "untitled";
const std::string DEFAULT_NODE_NAME = "XXX";
constexpr int CAN_MAX_DATA_BYTES = 64;
struct MessageId {
uint8_t source = 0;
uint32_t address = 0;
QString toString() const {
return QString("%1:%2").arg(source).arg(QString::number(address, 16).toUpper());
}
inline static MessageId fromString(const QString &str) {
auto parts = str.split(':');
if (parts.size() != 2) return {};
return MessageId{.source = uint8_t(parts[0].toUInt()), .address = parts[1].toUInt(nullptr, 16)};
}
bool operator==(const MessageId &other) const {
return source == other.source && address == other.address;
}
bool operator!=(const MessageId &other) const {
return !(*this == other);
}
bool operator<(const MessageId &other) const {
return std::tie(source, address) < std::tie(other.source, other.address);
}
bool operator>(const MessageId &other) const {
return std::tie(source, address) > std::tie(other.source, other.address);
}
};
uint qHash(const MessageId &item);
Q_DECLARE_METATYPE(MessageId);
template <>
struct std::hash<MessageId> {
std::size_t operator()(const MessageId &k) const noexcept { return qHash(k); }
};
typedef std::vector<std::pair<double, QString>> ValueDescription;
typedef std::vector<std::pair<double, std::string>> ValueDescription;
namespace cabana {
@@ -61,7 +25,7 @@ public:
Signal(const Signal &other) = default;
void update();
bool getValue(const uint8_t *data, size_t data_size, double *val) const;
QString formatValue(double value, bool with_unit = true) const;
std::string formatValue(double value, bool with_unit = true) const;
bool operator==(const cabana::Signal &other) const;
inline bool operator!=(const cabana::Signal &other) const { return !(*this == other); }
@@ -72,19 +36,19 @@ public:
};
Type type = Type::Normal;
QString name;
std::string name;
int start_bit, msb, lsb, size;
double factor = 1.0;
double offset = 0;
bool is_signed;
bool is_little_endian;
double min, max;
QString unit;
QString comment;
QString receiver_name;
std::string unit;
std::string comment;
std::string receiver_name;
ValueDescription val_desc;
int precision = 0;
QColor color;
CabanaColor color;
// Multiplexed
int multiplex_value = 0;
@@ -97,20 +61,20 @@ public:
Msg(const Msg &other) { *this = other; }
~Msg();
cabana::Signal *addSignal(const cabana::Signal &sig);
cabana::Signal *updateSignal(const QString &sig_name, const cabana::Signal &sig);
void removeSignal(const QString &sig_name);
cabana::Signal *updateSignal(const std::string &sig_name, const cabana::Signal &sig);
void removeSignal(const std::string &sig_name);
Msg &operator=(const Msg &other);
int indexOf(const cabana::Signal *sig) const;
cabana::Signal *sig(const QString &sig_name) const;
QString newSignalName();
cabana::Signal *sig(const std::string &sig_name) const;
std::string newSignalName();
void update();
inline const std::vector<cabana::Signal *> &getSignals() const { return sigs; }
uint32_t address;
QString name;
std::string name;
uint32_t size;
QString comment;
QString transmitter;
std::string comment;
std::string transmitter;
std::vector<cabana::Signal *> sigs;
std::vector<uint8_t> mask;
@@ -123,4 +87,8 @@ public:
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig);
void updateMsbLsb(cabana::Signal &s);
inline int flipBitPos(int start_bit) { return 8 * (start_bit / 8) + 7 - start_bit % 8; }
inline QString doubleToString(double value) { return QString::number(value, 'g', std::numeric_limits<double>::digits10); }
inline std::string doubleToString(double value) {
char buf[64];
snprintf(buf, sizeof(buf), "%.*g", std::numeric_limits<double>::digits10, value);
return buf;
}

View File

@@ -1,48 +1,86 @@
#include "tools/cabana/dbc/dbcfile.h"
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <algorithm>
#include <cassert>
#include <filesystem>
#include <fstream>
#include <regex>
#include <sstream>
#include <stdexcept>
DBCFile::DBCFile(const QString &dbc_file_name) {
QFile file(dbc_file_name);
if (file.open(QIODevice::ReadOnly)) {
name_ = QFileInfo(dbc_file_name).baseName();
filename = dbc_file_name;
parse(file.readAll());
} else {
throw std::runtime_error("Failed to open file.");
namespace {
std::string trim(const std::string &value) {
const auto first = value.find_first_not_of(" \t\r\n");
if (first == std::string::npos) return {};
return value.substr(first, value.find_last_not_of(" \t\r\n") - first + 1);
}
bool startsWith(const std::string &value, const char *prefix) {
return value.rfind(prefix, 0) == 0;
}
std::string unescapeComment(std::string value) {
for (size_t pos = 0; (pos = value.find("\\\"", pos)) != std::string::npos; ++pos) {
value.replace(pos, 2, "\"");
}
return trim(value);
}
DBCFile::DBCFile(const QString &name, const QString &content) : name_(name), filename("") {
parse(content);
}
bool DBCFile::save() {
assert(!filename.isEmpty());
return writeContents(filename);
}
bool DBCFile::saveAs(const QString &new_filename) {
filename = new_filename;
return save();
}
bool DBCFile::writeContents(const QString &fn) {
QFile file(fn);
if (file.open(QIODevice::WriteOnly)) {
return file.write(generateDBC().toUtf8()) >= 0;
bool commentComplete(const std::string &line) {
bool escaped = false;
for (size_t i = 0; i < line.size(); ++i) {
if (line[i] == '\\' && !escaped) {
escaped = true;
continue;
}
if (line[i] == '"' && !escaped) {
size_t next = line.find_first_not_of(" \t\r\n", i + 1);
if (next != std::string::npos && line[next] == ';') return true;
}
escaped = false;
}
return false;
}
void DBCFile::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment) {
} // namespace
DBCFile::DBCFile(const std::string &dbc_file_name) {
std::ifstream file(dbc_file_name, std::ios::binary);
if (!file) throw std::runtime_error("Failed to open file.");
filename = dbc_file_name;
name_ = std::filesystem::path(dbc_file_name).stem().string();
parse(std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()));
}
DBCFile::DBCFile(const std::string &name, const std::string &content) : name_(name) {
parse(content);
}
bool DBCFile::save() {
assert(!filename.empty());
return writeContents(filename);
}
bool DBCFile::saveAs(const std::string &new_filename) {
filename = new_filename;
return save();
}
bool DBCFile::writeContents(const std::string &fn) {
std::ofstream file(fn, std::ios::binary | std::ios::trunc);
if (!file) return false;
file << generateDBC();
return file.good();
}
void DBCFile::updateMsg(const MessageId &id, const std::string &name, uint32_t size,
const std::string &node, const std::string &comment) {
auto &m = msgs[id.address];
m.address = id.address;
m.name = name;
m.size = size;
m.transmitter = node.isEmpty() ? DEFAULT_NODE_NAME : node;
m.transmitter = node.empty() ? DEFAULT_NODE_NAME : node;
m.comment = comment;
}
@@ -51,221 +89,180 @@ cabana::Msg *DBCFile::msg(uint32_t address) {
return it != msgs.end() ? &it->second : nullptr;
}
cabana::Msg *DBCFile::msg(const QString &name) {
cabana::Msg *DBCFile::msg(const std::string &name) {
auto it = std::find_if(msgs.begin(), msgs.end(), [&name](auto &m) { return m.second.name == name; });
return it != msgs.end() ? &(it->second) : nullptr;
return it != msgs.end() ? &it->second : nullptr;
}
cabana::Signal *DBCFile::signal(uint32_t address, const QString &name) {
cabana::Signal *DBCFile::signal(uint32_t address, const std::string &name) {
auto m = msg(address);
return m ? (cabana::Signal *)m->sig(name) : nullptr;
return m ? m->sig(name) : nullptr;
}
void DBCFile::parse(const QString &content) {
void DBCFile::parse(const std::string &content) {
msgs.clear();
int line_num = 0;
QString line;
header.clear();
std::istringstream input(content);
std::string raw_line;
cabana::Msg *current_msg = nullptr;
int multiplexor_cnt = 0;
int line_num = 0;
bool seen_first = false;
QTextStream stream((QString *)&content);
while (!stream.atEnd()) {
while (std::getline(input, raw_line)) {
++line_num;
QString raw_line = stream.readLine();
line = raw_line.trimmed();
const size_t first_nonspace = raw_line.find_first_not_of(" \t\r");
std::string line = first_nonspace == std::string::npos ? std::string() : raw_line.substr(first_nonspace);
const int statement_line = line_num;
if ((startsWith(line, "CM_ BO_") || startsWith(line, "CM_ SG_ ")) && !commentComplete(line)) {
std::string continuation;
while (std::getline(input, continuation)) {
++line_num;
line += "\n" + continuation;
if (commentComplete(line)) break;
}
}
bool seen = true;
try {
if (line.startsWith("BO_ ")) {
if (startsWith(line, "BO_ ")) {
multiplexor_cnt = 0;
current_msg = parseBO(line);
} else if (line.startsWith("SG_ ")) {
} else if (startsWith(line, "SG_ ")) {
parseSG(line, current_msg, multiplexor_cnt);
} else if (line.startsWith("VAL_ ")) {
} else if (startsWith(line, "VAL_ ")) {
parseVAL(line);
} else if (line.startsWith("CM_ BO_")) {
parseCM_BO(line, content, raw_line, stream);
} else if (line.startsWith("CM_ SG_ ")) {
parseCM_SG(line, content, raw_line, stream);
} else if (startsWith(line, "CM_ BO_")) {
parseCM_BO(line);
} else if (startsWith(line, "CM_ SG_ ")) {
parseCM_SG(line);
} else {
seen = false;
}
} catch (std::exception &e) {
throw std::runtime_error(QString("[%1:%2]%3: %4").arg(filename).arg(line_num).arg(e.what()).arg(line).toStdString());
}
if (seen) {
seen_first = true;
} else if (!seen_first) {
header += raw_line + "\n";
} catch (const std::exception &e) {
throw std::runtime_error("[" + filename + ":" + std::to_string(statement_line) + "]" + e.what() + ": " + line);
}
if (seen) seen_first = true;
else if (!seen_first) header += raw_line + "\n";
}
for (auto &[_, m] : msgs) {
m.update();
}
for (auto &[_, message] : msgs) message.update();
}
cabana::Msg *DBCFile::parseBO(const QString &line) {
static QRegularExpression bo_regexp(R"(^BO_ (?<address>\w+) (?<name>\w+) *: (?<size>\w+) (?<transmitter>\w+))");
QRegularExpressionMatch match = bo_regexp.match(line);
if (!match.hasMatch())
throw std::runtime_error("Invalid BO_ line format");
uint32_t address = match.captured("address").toUInt();
if (msgs.count(address) > 0)
throw std::runtime_error(QString("Duplicate message address: %1").arg(address).toStdString());
// Create a new message object
cabana::Msg *msg = &msgs[address];
msg->address = address;
msg->name = match.captured("name");
msg->size = match.captured("size").toULong();
msg->transmitter = match.captured("transmitter").trimmed();
return msg;
cabana::Msg *DBCFile::parseBO(const std::string &line) {
static const std::regex pattern(R"(^BO_ ([[:alnum:]_]+) ([[:alnum:]_]+) *: ([[:alnum:]_]+) ([[:alnum:]_]+))");
std::smatch match;
if (!std::regex_search(line, match, pattern)) throw std::runtime_error("Invalid BO_ line format");
const uint32_t address = std::stoul(match[1].str());
if (msgs.count(address)) throw std::runtime_error("Duplicate message address: " + std::to_string(address));
auto &message = msgs[address];
message.address = address;
message.name = match[2].str();
message.size = std::stoul(match[3].str());
message.transmitter = trim(match[4].str());
return &message;
}
void DBCFile::parseCM_BO(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream) {
static QRegularExpression msg_comment_regexp(R"(^CM_ BO_ *(?<address>\w+) *\"(?<comment>(?:[^"\\]|\\.)*)\"\s*;)");
void DBCFile::parseSG(const std::string &line, cabana::Msg *current_msg, int &multiplexor_cnt) {
static const std::regex pattern(R"dbc(^SG_ ([[:alnum:]_]+)(?: +([[:alnum:]_]+))? *: ([0-9]+)\|([0-9]+)@([0-9]+)([+-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] "(.*)" (.*))dbc");
if (!current_msg) throw std::runtime_error("No Message");
std::smatch match;
if (!std::regex_search(line, match, pattern)) throw std::runtime_error("Invalid SG_ line format");
if (current_msg->sig(match[1].str())) throw std::runtime_error("Duplicate signal name");
QString parse_line = line;
if (!parse_line.endsWith("\";")) {
int pos = stream.pos() - raw_line.length() - 1;
parse_line = content.mid(pos, content.indexOf("\";", pos));
}
auto match = msg_comment_regexp.match(parse_line);
if (!match.hasMatch())
throw std::runtime_error("Invalid message comment format");
if (auto m = (cabana::Msg *)msg(match.captured("address").toUInt()))
m->comment = match.captured("comment").trimmed().replace("\\\"", "\"");
}
void DBCFile::parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt) {
static QRegularExpression sg_regexp(R"(^SG_ (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))");
static QRegularExpression sgm_regexp(R"(^SG_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))");
if (!current_msg)
throw std::runtime_error("No Message");
int offset = 0;
auto match = sg_regexp.match(line);
if (!match.hasMatch()) {
match = sgm_regexp.match(line);
offset = 1;
}
if (!match.hasMatch())
throw std::runtime_error("Invalid SG_ line format");
QString name = match.captured(1);
if (current_msg->sig(name) != nullptr)
throw std::runtime_error("Duplicate signal name");
cabana::Signal s{};
if (offset == 1) {
auto indicator = match.captured(2);
cabana::Signal signal{};
const std::string indicator = match[2].str();
if (!indicator.empty()) {
if (indicator == "M") {
++multiplexor_cnt;
// Only one signal within a single message can be the multiplexer switch.
if (multiplexor_cnt >= 2)
throw std::runtime_error("Multiple multiplexor");
s.type = cabana::Signal::Type::Multiplexor;
if (++multiplexor_cnt >= 2) throw std::runtime_error("Multiple multiplexor");
signal.type = cabana::Signal::Type::Multiplexor;
} else {
s.type = cabana::Signal::Type::Multiplexed;
s.multiplex_value = indicator.mid(1).toInt();
signal.type = cabana::Signal::Type::Multiplexed;
signal.multiplex_value = indicator.size() > 1 ? std::stoi(indicator.substr(1)) : 0;
}
}
s.name = name;
s.start_bit = match.captured(offset + 2).toInt();
s.size = match.captured(offset + 3).toInt();
s.is_little_endian = match.captured(offset + 4).toInt() == 1;
s.is_signed = match.captured(offset + 5) == "-";
s.factor = match.captured(offset + 6).toDouble();
s.offset = match.captured(offset + 7).toDouble();
s.min = match.captured(8 + offset).toDouble();
s.max = match.captured(9 + offset).toDouble();
s.unit = match.captured(10 + offset);
s.receiver_name = match.captured(11 + offset).trimmed();
current_msg->sigs.push_back(new cabana::Signal(s));
signal.name = match[1].str();
signal.start_bit = std::stoi(match[3].str());
signal.size = std::stoi(match[4].str());
signal.is_little_endian = match[5].str() == "1";
signal.is_signed = match[6].str() == "-";
signal.factor = std::stod(match[7].str());
signal.offset = std::stod(match[8].str());
signal.min = std::stod(match[9].str());
signal.max = std::stod(match[10].str());
signal.unit = match[11].str();
signal.receiver_name = trim(match[12].str());
current_msg->sigs.push_back(new cabana::Signal(signal));
}
void DBCFile::parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream) {
static QRegularExpression sg_comment_regexp(R"(^CM_ SG_ *(\w+) *(\w+) *\"((?:[^"\\]|\\.)*)\"\s*;)");
QString parse_line = line;
if (!parse_line.endsWith("\";")) {
int pos = stream.pos() - raw_line.length() - 1;
parse_line = content.mid(pos, content.indexOf("\";", pos));
void DBCFile::parseCM_BO(const std::string &line) {
std::istringstream prefix(line.substr(7));
uint32_t address = 0;
prefix >> address;
const size_t first_quote = line.find('"');
const size_t last_quote = line.rfind('"');
if (!prefix || first_quote == std::string::npos || last_quote <= first_quote) {
throw std::runtime_error("Invalid message comment format");
}
auto match = sg_comment_regexp.match(parse_line);
if (!match.hasMatch())
if (auto message = msg(address)) message->comment = unescapeComment(line.substr(first_quote + 1, last_quote - first_quote - 1));
}
void DBCFile::parseCM_SG(const std::string &line) {
std::istringstream prefix(line.substr(7));
uint32_t address = 0;
std::string name;
prefix >> address >> name;
const size_t first_quote = line.find('"');
const size_t last_quote = line.rfind('"');
if (!prefix || name.empty() || first_quote == std::string::npos || last_quote <= first_quote) {
throw std::runtime_error("Invalid CM_ SG_ line format");
if (auto s = signal(match.captured(1).toUInt(), match.captured(2))) {
s->comment = match.captured(3).trimmed().replace("\\\"", "\"");
}
if (auto sig = signal(address, name)) sig->comment = unescapeComment(line.substr(first_quote + 1, last_quote - first_quote - 1));
}
void DBCFile::parseVAL(const QString &line) {
static QRegularExpression val_regexp(R"(VAL_ (\w+) (\w+) (\s*[-+]?[0-9]+\s+\".+?\"[^;]*))");
auto match = val_regexp.match(line);
if (!match.hasMatch())
throw std::runtime_error("invalid VAL_ line format");
if (auto s = signal(match.captured(1).toUInt(), match.captured(2))) {
QStringList desc_list = match.captured(3).trimmed().split('"');
for (int i = 0; i < desc_list.size(); i += 2) {
auto val = desc_list[i].trimmed();
if (!val.isEmpty() && (i + 1) < desc_list.size()) {
auto desc = desc_list[i + 1].trimmed();
s->val_desc.push_back({val.toDouble(), desc});
}
void DBCFile::parseVAL(const std::string &line) {
static const std::regex header_pattern(R"(^VAL_ ([[:alnum:]_]+) ([[:alnum:]_]+) (.*))");
static const std::regex entry_pattern(R"dbc(([+-]?[0-9]+(?:\.[0-9]+)?)\s+"([^"]*)")dbc");
std::smatch match;
if (!std::regex_search(line, match, header_pattern)) throw std::runtime_error("invalid VAL_ line format");
if (auto sig = signal(std::stoul(match[1].str()), match[2].str())) {
const std::string entries = match[3].str();
for (std::sregex_iterator it(entries.begin(), entries.end(), entry_pattern), end; it != end; ++it) {
sig->val_desc.emplace_back(std::stod((*it)[1].str()), trim((*it)[2].str()));
}
}
}
QString DBCFile::generateDBC() {
QString dbc_string, comment, val_desc;
std::string DBCFile::generateDBC() {
std::string dbc_string, comment, val_desc;
for (const auto &[address, m] : msgs) {
const QString transmitter = m.transmitter.isEmpty() ? DEFAULT_NODE_NAME : m.transmitter;
dbc_string += QString("BO_ %1 %2: %3 %4\n").arg(address).arg(m.name).arg(m.size).arg(transmitter);
if (!m.comment.isEmpty()) {
comment += QString("CM_ BO_ %1 \"%2\";\n").arg(address).arg(QString(m.comment).replace("\"", "\\\""));
const std::string &transmitter = m.transmitter.empty() ? DEFAULT_NODE_NAME : m.transmitter;
dbc_string += "BO_ " + std::to_string(address) + " " + m.name + ": " + std::to_string(m.size) + " " + transmitter + "\n";
if (!m.comment.empty()) {
std::string escaped = m.comment;
for (size_t pos = 0; (pos = escaped.find('"', pos)) != std::string::npos; pos += 2) escaped.replace(pos, 1, "\\\"");
comment += "CM_ BO_ " + std::to_string(address) + " \"" + escaped + "\";\n";
}
for (auto sig : m.getSignals()) {
QString multiplexer_indicator;
if (sig->type == cabana::Signal::Type::Multiplexor) {
multiplexer_indicator = "M ";
} else if (sig->type == cabana::Signal::Type::Multiplexed) {
multiplexer_indicator = QString("m%1 ").arg(sig->multiplex_value);
}
dbc_string += QString(" SG_ %1 %2: %3|%4@%5%6 (%7,%8) [%9|%10] \"%11\" %12\n")
.arg(sig->name)
.arg(multiplexer_indicator)
.arg(sig->start_bit)
.arg(sig->size)
.arg(sig->is_little_endian ? '1' : '0')
.arg(sig->is_signed ? '-' : '+')
.arg(doubleToString(sig->factor))
.arg(doubleToString(sig->offset))
.arg(doubleToString(sig->min))
.arg(doubleToString(sig->max))
.arg(sig->unit)
.arg(sig->receiver_name.isEmpty() ? DEFAULT_NODE_NAME : sig->receiver_name);
if (!sig->comment.isEmpty()) {
comment += QString("CM_ SG_ %1 %2 \"%3\";\n").arg(address).arg(sig->name).arg(QString(sig->comment).replace("\"", "\\\""));
std::string mux;
if (sig->type == cabana::Signal::Type::Multiplexor) mux = "M ";
else if (sig->type == cabana::Signal::Type::Multiplexed) mux = "m" + std::to_string(sig->multiplex_value) + " ";
const std::string &receiver = sig->receiver_name.empty() ? DEFAULT_NODE_NAME : sig->receiver_name;
dbc_string += " SG_ " + sig->name + " " + mux + ": " + std::to_string(sig->start_bit) + "|" + std::to_string(sig->size) + "@" +
(sig->is_little_endian ? "1" : "0") + (sig->is_signed ? "-" : "+") +
" (" + doubleToString(sig->factor) + "," + doubleToString(sig->offset) + ")" +
" [" + doubleToString(sig->min) + "|" + doubleToString(sig->max) + "] \"" + sig->unit + "\" " + receiver + "\n";
if (!sig->comment.empty()) {
std::string escaped = sig->comment;
for (size_t pos = 0; (pos = escaped.find('"', pos)) != std::string::npos; pos += 2) escaped.replace(pos, 1, "\\\"");
comment += "CM_ SG_ " + std::to_string(address) + " " + sig->name + " \"" + escaped + "\";\n";
}
if (!sig->val_desc.empty()) {
QStringList text;
for (auto &[val, desc] : sig->val_desc) {
text << QString("%1 \"%2\"").arg(val).arg(desc);
std::string text;
for (const auto &[value, description] : sig->val_desc) {
if (!text.empty()) text += " ";
text += doubleToString(value) + " \"" + description + "\"";
}
val_desc += QString("VAL_ %1 %2 %3;\n").arg(address).arg(sig->name).arg(text.join(" "));
val_desc += "VAL_ " + std::to_string(address) + " " + sig->name + " " + text + ";\n";
}
}
dbc_string += "\n";

View File

@@ -1,44 +1,44 @@
#pragma once
#include <map>
#include <QTextStream>
#include <string>
#include "tools/cabana/dbc/dbc.h"
class DBCFile {
public:
DBCFile(const QString &dbc_file_name);
DBCFile(const QString &name, const QString &content);
DBCFile(const std::string &dbc_file_name);
DBCFile(const std::string &name, const std::string &content);
~DBCFile() {}
bool save();
bool saveAs(const QString &new_filename);
bool writeContents(const QString &fn);
QString generateDBC();
bool saveAs(const std::string &new_filename);
bool writeContents(const std::string &fn);
std::string generateDBC();
void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment);
void updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment);
inline void removeMsg(const MessageId &id) { msgs.erase(id.address); }
inline const std::map<uint32_t, cabana::Msg> &getMessages() const { return msgs; }
cabana::Msg *msg(uint32_t address);
cabana::Msg *msg(const QString &name);
cabana::Msg *msg(const std::string &name);
inline cabana::Msg *msg(const MessageId &id) { return msg(id.address); }
cabana::Signal *signal(uint32_t address, const QString &name);
cabana::Signal *signal(uint32_t address, const std::string &name);
inline QString name() const { return name_.isEmpty() ? "untitled" : name_; }
inline bool isEmpty() const { return msgs.empty() && name_.isEmpty(); }
inline std::string name() const { return name_.empty() ? "untitled" : name_; }
inline bool isEmpty() const { return msgs.empty() && name_.empty(); }
QString filename;
std::string filename;
private:
void parse(const QString &content);
cabana::Msg *parseBO(const QString &line);
void parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt);
void parseCM_BO(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream);
void parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream);
void parseVAL(const QString &line);
void parse(const std::string &content);
cabana::Msg *parseBO(const std::string &line);
void parseSG(const std::string &line, cabana::Msg *current_msg, int &multiplexor_cnt);
void parseCM_BO(const std::string &line);
void parseCM_SG(const std::string &line);
void parseVAL(const std::string &line);
QString header;
std::string header;
std::map<uint32_t, cabana::Msg> msgs;
QString name_;
std::string name_;
};

View File

@@ -1,10 +1,10 @@
#include "tools/cabana/dbc/dbcmanager.h"
#include <QSet>
#include <algorithm>
#include <numeric>
#include <cassert>
#include <set>
bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QString *error) {
bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error) {
try {
auto it = std::find_if(dbc_files.begin(), dbc_files.end(),
[&](auto &f) { return f.second && f.second->filename == dbc_file_name; });
@@ -17,11 +17,11 @@ bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QS
return false;
}
emit DBCFileChanged();
if (callbacks_.file_changed) callbacks_.file_changed();
return true;
}
bool DBCManager::open(const SourceSet &sources, const QString &name, const QString &content, QString *error) {
bool DBCManager::open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error) {
try {
auto file = std::make_shared<DBCFile>(name, content);
for (auto s : sources) {
@@ -32,7 +32,7 @@ bool DBCManager::open(const SourceSet &sources, const QString &name, const QStri
return false;
}
emit DBCFileChanged();
if (callbacks_.file_changed) callbacks_.file_changed();
return true;
}
@@ -40,69 +40,71 @@ void DBCManager::close(const SourceSet &sources) {
for (auto s : sources) {
dbc_files[s] = nullptr;
}
emit DBCFileChanged();
if (callbacks_.file_changed) callbacks_.file_changed();
}
void DBCManager::close(DBCFile *dbc_file) {
for (auto &[_, f] : dbc_files) {
if (f.get() == dbc_file) f = nullptr;
}
emit DBCFileChanged();
if (callbacks_.file_changed) callbacks_.file_changed();
}
void DBCManager::closeAll() {
dbc_files.clear();
emit DBCFileChanged();
if (callbacks_.file_changed) callbacks_.file_changed();
}
void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) {
if (auto m = msg(id)) {
if (auto s = m->addSignal(sig)) {
emit signalAdded(id, s);
emit maskUpdated();
if (callbacks_.signal_added) callbacks_.signal_added(id, s);
if (callbacks_.mask_updated) callbacks_.mask_updated();
}
}
}
void DBCManager::updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig) {
void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig) {
if (auto m = msg(id)) {
if (auto s = m->updateSignal(sig_name, sig)) {
emit signalUpdated(s);
emit maskUpdated();
if (callbacks_.signal_updated) callbacks_.signal_updated(s);
if (callbacks_.mask_updated) callbacks_.mask_updated();
}
}
}
void DBCManager::removeSignal(const MessageId &id, const QString &sig_name) {
void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) {
if (auto m = msg(id)) {
if (auto s = m->sig(sig_name)) {
emit signalRemoved(s);
if (callbacks_.signal_removed) callbacks_.signal_removed(s);
m->removeSignal(sig_name);
emit maskUpdated();
if (callbacks_.mask_updated) callbacks_.mask_updated();
}
}
}
void DBCManager::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment) {
void DBCManager::updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment) {
auto dbc_file = findDBCFile(id);
assert(dbc_file); // This should be impossible
dbc_file->updateMsg(id, name, size, node, comment);
emit msgUpdated(id);
if (callbacks_.msg_updated) callbacks_.msg_updated(id);
}
void DBCManager::removeMsg(const MessageId &id) {
auto dbc_file = findDBCFile(id);
assert(dbc_file); // This should be impossible
dbc_file->removeMsg(id);
emit msgRemoved(id);
emit maskUpdated();
if (callbacks_.msg_removed) callbacks_.msg_removed(id);
if (callbacks_.mask_updated) callbacks_.mask_updated();
}
QString DBCManager::newMsgName(const MessageId &id) {
return QString("NEW_MSG_") + QString::number(id.address, 16).toUpper();
std::string DBCManager::newMsgName(const MessageId &id) {
char buf[64];
snprintf(buf, sizeof(buf), "NEW_MSG_%X", id.address);
return buf;
}
QString DBCManager::newSignalName(const MessageId &id) {
std::string DBCManager::newSignalName(const MessageId &id) {
auto m = msg(id);
return m ? m->newSignalName() : "";
}
@@ -118,14 +120,14 @@ cabana::Msg *DBCManager::msg(const MessageId &id) {
return dbc_file ? dbc_file->msg(id) : nullptr;
}
cabana::Msg *DBCManager::msg(uint8_t source, const QString &name) {
cabana::Msg *DBCManager::msg(uint8_t source, const std::string &name) {
auto dbc_file = findDBCFile(source);
return dbc_file ? dbc_file->msg(name) : nullptr;
}
QStringList DBCManager::signalNames() {
std::vector<std::string> DBCManager::signalNames() {
// Used for autocompletion
QSet<QString> names;
std::set<std::string> names;
for (auto &f : allDBCFiles()) {
for (auto &[_, m] : f->getMessages()) {
for (auto sig : m.getSignals()) {
@@ -133,8 +135,8 @@ QStringList DBCManager::signalNames() {
}
}
}
QStringList ret = names.values();
ret.sort();
std::vector<std::string> ret(names.begin(), names.end());
std::sort(ret.begin(), ret.end());
return ret;
}
@@ -165,14 +167,16 @@ const SourceSet DBCManager::sources(const DBCFile *dbc_file) const {
return sources;
}
QString toString(const SourceSet &ss) {
return std::accumulate(ss.cbegin(), ss.cend(), QString(), [](QString str, int source) {
if (!str.isEmpty()) str += ", ";
return str + (source == -1 ? QStringLiteral("all") : QString::number(source));
});
std::string toString(const SourceSet &ss) {
std::string result;
for (int source : ss) {
if (!result.empty()) result += ", ";
result += (source == -1) ? "all" : std::to_string(source);
}
return result;
}
DBCManager *dbc() {
static DBCManager dbc_manager(nullptr);
static DBCManager dbc_manager;
return &dbc_manager;
}

View File

@@ -1,44 +1,52 @@
#pragma once
#include <QObject>
#include <functional>
#include <memory>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "tools/cabana/dbc/dbcfile.h"
typedef std::set<int> SourceSet;
const SourceSet SOURCE_ALL = {-1};
const int INVALID_SOURCE = 0xff;
inline bool operator<(const std::shared_ptr<DBCFile> &l, const std::shared_ptr<DBCFile> &r) { return l.get() < r.get(); }
class DBCManager : public QObject {
Q_OBJECT
class DBCManager {
public:
DBCManager(QObject *parent) : QObject(parent) {}
~DBCManager() {}
bool open(const SourceSet &sources, const QString &dbc_file_name, QString *error = nullptr);
bool open(const SourceSet &sources, const QString &name, const QString &content, QString *error = nullptr);
struct Callbacks {
std::function<void(MessageId, const cabana::Signal *)> signal_added;
std::function<void(const cabana::Signal *)> signal_removed;
std::function<void(const cabana::Signal *)> signal_updated;
std::function<void(MessageId)> msg_updated;
std::function<void(MessageId)> msg_removed;
std::function<void()> file_changed;
std::function<void()> mask_updated;
};
DBCManager() = default;
bool open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error = nullptr);
bool open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error = nullptr);
void close(const SourceSet &sources);
void close(DBCFile *dbc_file);
void closeAll();
void addSignal(const MessageId &id, const cabana::Signal &sig);
void updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig);
void removeSignal(const MessageId &id, const QString &sig_name);
void updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig);
void removeSignal(const MessageId &id, const std::string &sig_name);
void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment);
void updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment);
void removeMsg(const MessageId &id);
QString newMsgName(const MessageId &id);
QString newSignalName(const MessageId &id);
std::string newMsgName(const MessageId &id);
std::string newSignalName(const MessageId &id);
const std::map<uint32_t, cabana::Msg> &getMessages(uint8_t source);
cabana::Msg *msg(const MessageId &id);
cabana::Msg* msg(uint8_t source, const QString &name);
cabana::Msg* msg(uint8_t source, const std::string &name);
QStringList signalNames();
std::vector<std::string> signalNames();
inline int dbcCount() { return allDBCFiles().size(); }
int nonEmptyDBCCount();
@@ -46,24 +54,17 @@ public:
DBCFile *findDBCFile(const uint8_t source);
inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); }
std::set<DBCFile *> allDBCFiles();
signals:
void signalAdded(MessageId id, const cabana::Signal *sig);
void signalRemoved(const cabana::Signal *sig);
void signalUpdated(const cabana::Signal *sig);
void msgUpdated(MessageId id);
void msgRemoved(MessageId id);
void DBCFileChanged();
void maskUpdated();
void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); }
private:
std::map<int, std::shared_ptr<DBCFile>> dbc_files;
Callbacks callbacks_;
};
DBCManager *dbc();
QString toString(const SourceSet &ss);
inline QString msgName(const MessageId &id) {
std::string toString(const SourceSet &ss);
inline std::string msgName(const MessageId &id) {
auto msg = dbc()->msg(id);
return msg ? msg->name : UNTITLED;
}

18
tools/cabana/dbc/dbcqt.cc Normal file
View File

@@ -0,0 +1,18 @@
#include "tools/cabana/dbc/dbcqt.h"
QtDBCNotifier::QtDBCNotifier(QObject *parent) : QObject(parent) {
dbc()->setCallbacks({
.signal_added = [this](MessageId id, const cabana::Signal *sig) { emit signalAdded(id, sig); },
.signal_removed = [this](const cabana::Signal *sig) { emit signalRemoved(sig); },
.signal_updated = [this](const cabana::Signal *sig) { emit signalUpdated(sig); },
.msg_updated = [this](MessageId id) { emit msgUpdated(id); },
.msg_removed = [this](MessageId id) { emit msgRemoved(id); },
.file_changed = [this]() { emit DBCFileChanged(); },
.mask_updated = [this]() { emit maskUpdated(); },
});
}
QtDBCNotifier *dbcNotifier() {
static QtDBCNotifier notifier;
return &notifier;
}

27
tools/cabana/dbc/dbcqt.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <QMetaType>
#include <QObject>
#include "tools/cabana/dbc/dbcmanager.h"
Q_DECLARE_METATYPE(MessageId)
Q_DECLARE_METATYPE(ValueDescription)
class QtDBCNotifier : public QObject {
Q_OBJECT
public:
explicit QtDBCNotifier(QObject *parent = nullptr);
signals:
void signalAdded(MessageId id, const cabana::Signal *sig);
void signalRemoved(const cabana::Signal *sig);
void signalUpdated(const cabana::Signal *sig);
void msgUpdated(MessageId id);
void msgRemoved(MessageId id);
void DBCFileChanged();
void maskUpdated();
};
QtDBCNotifier *dbcNotifier();

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import argparse
import json
from iqdbc.car import Bus
from iqdbc.car.fingerprints import MIGRATION
from iqdbc.car.values import PLATFORMS

53
tools/cabana/deqt.md Normal file
View File

@@ -0,0 +1,53 @@
we're migrating cabana away from Qt and to eventually entirely use imgui
we are doing it incrementally, in small pieces that are easy to execute and verify.
we will repeat this until we're all done.
# Cabana Qt API inventory
these are all still in cabana. we remove them from this list once they're gone.
each bullet is an atomic unit of work.
our workflow is:
- pick the easiest of the bulleted items from below
- implement it and make] sure it builds
- spin up reviewer agents to review the code in a clean context and a separate one to click around in xvfb as a gui test
- then implement the fixes from the above reviewer agents
some rules
- do not add more Qt usage ever
- `QObject`, `QMetaObject`, `QMetaType`
- `QApplication`, `QCoreApplication`, `QGuiApplication`
- `QString`, `QStringList`, `QStringBuilder`, `QChar`, `QLatin1Char`
- `QVariant`
- `QTimer`
- `QWidget`, `QMainWindow`, `QWindow`
- `QDialog`, `QDialogButtonBox`, `QMessageBox`, `QProgressDialog`
- `QFileDialog`
- `QMenu`, `QMenuBar`, `QAction`, `QActionGroup`, `QWidgetAction`
- `QToolBar`, `QToolButton`, `QPushButton`
- `QCheckBox`, `QRadioButton`, `QButtonGroup`, `QAbstractButton`
- `QComboBox`, `QLineEdit`, `QTextEdit`, `QSpinBox`, `QSlider`
- `QLabel`, `QGroupBox`, `QFrame`
- `QTabBar`, `QTabWidget`, `QSplitter`, `QScrollArea`, `QScrollBar`
- `QDockWidget`, `QStatusBar`, `QProgressBar`
- `QFormLayout`, `QGridLayout`, `QHBoxLayout`, `QVBoxLayout`
- `QSizePolicy`
- `QAbstractItemModel`, `QAbstractTableModel`, `QModelIndex`
- `QAbstractItemView`, `QTableView`, `QTreeView`
- `QTableWidget`, `QTableWidgetItem`, `QListWidget`, `QListWidgetItem`
- `QItemSelection`, `QItemSelectionModel`, `QItemSelectionRange`
- `QHeaderView`, `QStyledItemDelegate`, `QStyleOptionViewItem`
- `QValidator`, `QIntValidator`
- `QColor`, `QRgb`, `QPalette`
- `QBrush`, `QPen`
- `QPainter`, `QPainterPath`, `QStylePainter`
- `QImage`, `QPixmap`, `QPixmapCache`, `QStaticText`
- `QFont`, `QFontDatabase`, `QFontMetrics`, `QTextDocument`
- `QStyle`, `QStyleOption`, `QStyleOptionFrame`, `QStyleOptionSlider`
- `QPoint`, `QPointF`, `QRect`, `QRectF`, `QRegion`
- `QSize`, `QSizeF`
- `QEvent`, `QPaintEvent`, `QResizeEvent`, `QShowEvent`, `QCloseEvent`
- `QMouseEvent`, `QWheelEvent`, `QNativeGestureEvent`, `QContextMenuEvent`
- `QKeySequence`, `QShortcut`, `QToolTip`

View File

@@ -1,4 +1,5 @@
#include "tools/cabana/detailwidget.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <QFormLayout>
#include <QMenu>
@@ -56,8 +57,8 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight);
QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); });
QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &DetailWidget::refresh);
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &DetailWidget::refresh);
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &DetailWidget::refresh);
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &DetailWidget::refresh);
QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu);
QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) {
if (index != -1) {
@@ -124,9 +125,9 @@ int DetailWidget::findOrAddTab(const MessageId& message_id) {
if (tabbar->tabData(index).value<MessageId>() == message_id) break;
}
if (index == -1) {
index = tabbar->addTab(message_id.toString());
index = tabbar->addTab(QString::fromStdString(message_id.toString()));
tabbar->setTabData(index, QVariant::fromValue(message_id));
tabbar->setTabToolTip(index, msgName(message_id));
tabbar->setTabToolTip(index, QString::fromStdString(msgName(message_id)));
}
return index;
}
@@ -151,21 +152,21 @@ 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(id.toString());
msgs.append(QString::fromStdString(id.toString()));
}
return std::make_pair(msg_id.toString(), msgs);
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);
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);
auto active_id = MessageId::fromString(active_msg_id.toStdString());
if (dbc()->msg(active_id) != nullptr)
setMessage(active_id);
}
@@ -180,10 +181,10 @@ void DetailWidget::refresh() {
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(s->name));
warnings.push_back(tr("%1 has overlapping bits.").arg(QString::fromStdString(s->name)));
}
}
QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id);
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);
@@ -208,22 +209,22 @@ void DetailWidget::updateState(const std::set<MessageId> *msgs) {
void DetailWidget::editMsg() {
auto msg = dbc()->msg(msg_id);
int size = msg ? msg->size : can->lastMessage(msg_id).dat.size();
EditMessageDialog dlg(msg_id, msgName(msg_id), size, this);
EditMessageDialog dlg(msg_id, QString::fromStdString(msgName(msg_id)), size, this);
if (dlg.exec()) {
UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed(), dlg.size_spin->value(),
dlg.node->text().trimmed(), dlg.comment_edit->toPlainText().trimmed()));
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::push(new RemoveMsgCommand(msg_id));
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(msg_id.toString()));
setWindowTitle(tr("Edit message: %1").arg(QString::fromStdString(msg_id.toString())));
QFormLayout *form_layout = new QFormLayout(this);
form_layout->addRow("", error_label = new QLabel);
@@ -241,8 +242,8 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit
form_layout->addRow(btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel));
if (auto msg = dbc()->msg(msg_id)) {
node->setText(msg->transmitter);
comment_edit->setText(msg->comment);
node->setText(QString::fromStdString(msg->transmitter));
comment_edit->setText(QString::fromStdString(msg->comment));
}
validateName(name_edit->text());
setFixedWidth(parent->width() * 0.9);
@@ -252,10 +253,10 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit
}
void EditMessageDialog::validateName(const QString &text) {
bool valid = text.compare(UNTITLED, Qt::CaseInsensitive) != 0;
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) == nullptr;
valid = dbc()->msg(msg_id.source, text.toStdString()) == nullptr;
if (!valid) {
error_label->setText(tr("Name already exists"));
error_label->setVisible(true);

View File

@@ -1,4 +1,5 @@
#include "tools/cabana/historylog.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <functional>
@@ -14,7 +15,7 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
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 sigs[col - 1]->formatValue(m.sig_values[col - 1], false);
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);
}
@@ -49,12 +50,12 @@ QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, i
if (section == 0) return "Time";
if (isHexMode()) return "Data";
QString name = sigs[section - 1]->name;
QString unit = sigs[section - 1]->unit;
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 = sigs[section - 1]->color;
QColor sigColor = toQColor(sigs[section - 1]->color);
sigColor.setAlpha(128);
return QBrush(sigColor);
}
@@ -207,8 +208,8 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) {
QObject::connect(value_edit, &QLineEdit::textEdited, this, &LogsWidget::filterChanged);
QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV);
QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::reset);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &HistoryLogModel::reset);
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &HistoryLogModel::reset);
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &HistoryLogModel::reset);
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &HistoryLogModel::reset);
QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset);
QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); });
}
@@ -216,7 +217,7 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) {
void LogsWidget::modelReset() {
signals_cb->clear();
for (auto s : model->sigs) {
signals_cb->addItem(s->name);
signals_cb->addItem(QString::fromStdString(s->name));
}
export_btn->setEnabled(false);
value_edit->clear();
@@ -238,11 +239,11 @@ void LogsWidget::filterChanged() {
}
void LogsWidget::exportToCSV() {
QString dir = QString("%1/%2_%3.csv").arg(settings.last_dir).arg(can->routeName()).arg(msgName(model->msg_id));
QString fn = QFileDialog::getSaveFileName(this, QString("Export %1 to CSV file").arg(msgName(model->msg_id)),
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, model->msg_id)
: utils::exportSignalsToCSV(fn, model->msg_id);
model->isHexMode() ? utils::exportToCSV(fn.toStdString(), model->msg_id)
: utils::exportSignalsToCSV(fn.toStdString(), model->msg_id);
}
}

View File

@@ -40,7 +40,7 @@ public:
uint64_t mono_time = 0;
std::vector<double> sig_values;
std::vector<uint8_t> data;
std::vector<QColor> colors;
std::vector<CabanaColor> colors;
};
void fetchData(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time);

110
tools/cabana/konn3kt_canproxy.py Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
konn3kt_canproxy — view a remote device's live CAN in Cabana, through konn3kt.
Run this on your laptop. It connects to the konn3kt CAN stream for a device you have
access to and re-publishes the raw capnp frames onto a LOCAL ZMQ "can" socket. Then you
just open Cabana → Live → Device, pick ZMQ, and enter 127.0.0.1 — exactly the local
workflow, but the frames are coming from a device across the internet.
Flow:
device canlived ──ws──> konn3kt relay ──ws──> THIS PROXY ──zmq──> Cabana (127.0.0.1)
The proxy never parses the CAN bytes; it forwards the exact capnp Event frames the device
produced, so Cabana decodes them identically to a local ZMQ bridge.
Auth: you authenticate as yourself (a konn3kt user JWT), not as the device. Get your JWT
from the konn3kt app/web session and pass it via --token or the KONN3KT_JWT env var.
Access is enforced server-side (owner-only, or a superuser with an explicit owner grant).
Usage:
export KONN3KT_JWT="<your konn3kt jwt>"
./konn3kt_canproxy.py <dongle_id>
# then in Cabana: Live → Device → ZMQ → 127.0.0.1
Requires the iqpilot/openpilot Python env (for cereal.messaging) and websocket-client.
"""
import argparse
import os
import sys
import time
from websocket import create_connection, ABNF, WebSocketException
# Force cereal messaging onto ZMQ so the local "can" publisher binds a TCP port that
# Cabana's ZMQ Device stream connects to. Must be set before importing messaging.
os.environ["ZMQ"] = "1"
import cereal.messaging as messaging # noqa: E402
DEFAULT_HOST = os.getenv("KONN3KT_HOST", "wss://api-iqlabs.konn3kt.com")
RECONNECT_MIN = 1.0
RECONNECT_MAX = 10.0
def _ws_host(host: str) -> str:
host = host.rstrip("/")
if host.startswith("https://"):
return "wss://" + host[len("https://"):]
if host.startswith("http://"):
return "ws://" + host[len("http://"):]
if host.startswith(("ws://", "wss://")):
return host
return "wss://" + host
def run(dongle_id: str, host: str, token: str) -> None:
ws_uri = f"{_ws_host(host)}/v1/devices/{dongle_id}/can-stream?sig={token}"
# Local ZMQ publisher for "can"; Cabana subscribes to this on 127.0.0.1.
pub = messaging.pub_sock("can")
backoff = RECONNECT_MIN
while True:
try:
print(f"[canproxy] connecting to konn3kt for {dongle_id} ...", file=sys.stderr)
ws = create_connection(ws_uri, enable_multithread=True, timeout=30.0)
print("[canproxy] connected. Open Cabana → Live → Device → ZMQ → 127.0.0.1", file=sys.stderr)
backoff = RECONNECT_MIN
n = 0
while True:
opcode, data = ws.recv_data(control_frame=True)
if opcode == ABNF.OPCODE_BINARY:
# Republish the exact capnp Event bytes locally.
pub.send(data)
n += 1
if n % 1000 == 0:
print(f"[canproxy] forwarded {n} frames", file=sys.stderr)
elif opcode == ABNF.OPCODE_CLOSE:
print("[canproxy] server closed the stream", file=sys.stderr)
break
elif opcode == ABNF.OPCODE_PING:
ws.pong(data)
except (WebSocketException, OSError) as e:
print(f"[canproxy] disconnected: {e}; reconnecting in {backoff:.0f}s", file=sys.stderr)
time.sleep(backoff)
backoff = min(backoff * 2, RECONNECT_MAX)
finally:
try:
ws.close()
except Exception:
pass
def main() -> None:
ap = argparse.ArgumentParser(description="Bridge a konn3kt remote CAN stream into local ZMQ for Cabana.")
ap.add_argument("dongle_id", help="dongle id of the device to stream")
ap.add_argument("--host", default=DEFAULT_HOST, help=f"konn3kt host (default: {DEFAULT_HOST})")
ap.add_argument("--token", default=os.getenv("KONN3KT_JWT"),
help="konn3kt user JWT (or set KONN3KT_JWT)")
args = ap.parse_args()
if not args.token:
ap.error("no token: pass --token or set KONN3KT_JWT")
try:
run(args.dongle_id, args.host, args.token)
except KeyboardInterrupt:
print("\n[canproxy] bye", file=sys.stderr)
if __name__ == "__main__":
main()

View File

@@ -1,29 +1,31 @@
#include "tools/cabana/mainwin.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <QClipboard>
#include <QDesktopWidget>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QJsonObject>
#include <QMenuBar>
#include <QMessageBox>
#include <QProgressDialog>
#include <QResizeEvent>
#include <QShortcut>
#include <QTextDocument>
#include <QUndoView>
#include <QVBoxLayout>
#include <QWidgetAction>
#include "third_party/json11/json11.hpp"
#include "tools/cabana/commands.h"
#include "tools/cabana/streamselector.h"
#include "tools/cabana/tools/findsignal.h"
#include "tools/cabana/utils/export.h"
// IQ.Pilot patch: iqpilot has no py_downloader; installDownloadProgressHandler still
// lives in replay/util.h here.
#include "tools/replay/util.h"
MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainWindow() {
loadFingerprints();
@@ -34,14 +36,11 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
createShortcuts();
// save default window state to allow resetting it
default_state = saveState();
default_state = utils::toBytes(saveState());
// restore states
restoreGeometry(settings.geometry);
if (isMaximized()) {
setGeometry(QApplication::desktop()->availableGeometry(this));
}
restoreState(settings.window_state);
// restore states; restoreGeometry() itself corrects stale off-screen geometry
restoreGeometry(utils::qbytes(settings.geometry));
restoreState(utils::qbytes(settings.window_state));
// install handlers
static auto static_main_win = this;
@@ -50,11 +49,9 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) {
emit static_main_win->updateProgressBar(cur, total, success);
});
qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) {
if (type == QtDebugMsg) return;
emit static_main_win->showMessage(msg, 2000);
installMessageHandler([](ReplyMsgType type, const std::string msg) {
emit static_main_win->showMessage(QString::fromStdString(msg), 2000);
});
installMessageHandler([](ReplyMsgType type, const std::string msg) { qInfo() << msg.c_str(); });
setStyleSheet(QString(R"(QMainWindow::separator {
width: %1px; /* when vertical */
@@ -63,8 +60,8 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage);
QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MainWindow::DBCFileChanged);
QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged);
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &MainWindow::DBCFileChanged);
QObject::connect(undoNotifier(), &QtUndoNotifier::cleanChanged, this, &MainWindow::undoStackCleanChanged);
QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus);
QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); });
@@ -72,9 +69,17 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
}
void MainWindow::loadFingerprints() {
QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json");
if (json_file.open(QIODevice::ReadOnly)) {
fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll());
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());
}
}
}
@@ -100,8 +105,17 @@ void MainWindow::createActions() {
file_menu->addSeparator();
QMenu *load_iqdbc_menu = file_menu->addMenu(tr("Load DBC from commaai/iqdbc"));
// load_iqdbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }");
for (const auto &dbc_name : QDir(OPENDBC_FILE_PATH).entryList({"*.dbc"}, QDir::Files, QDir::Name)) {
load_iqdbc_menu->addAction(dbc_name, [this, name = dbc_name]() { loadDBCFromOpendbc(name); });
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_iqdbc_menu->addAction(name, [this, name]() { loadDBCFromOpendbc(name); });
}
file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); });
@@ -119,18 +133,12 @@ void MainWindow::createActions() {
// Edit Menu
QMenu *edit_menu = menuBar()->addMenu(tr("&Edit"));
auto undo_act = UndoStack::instance()->createUndoAction(this, tr("&Undo"));
undo_act = edit_menu->addAction(tr("&Undo"), []() { UndoStack::instance()->undo(); });
undo_act->setShortcuts(QKeySequence::Undo);
edit_menu->addAction(undo_act);
auto redo_act = UndoStack::instance()->createRedoAction(this, tr("&Redo"));
redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); });
redo_act->setShortcuts(QKeySequence::Redo);
edit_menu->addAction(redo_act);
edit_menu->addSeparator();
QMenu *commands_menu = edit_menu->addMenu(tr("Command &List"));
QWidgetAction *commands_act = new QWidgetAction(this);
commands_act->setDefaultWidget(new QUndoView(UndoStack::instance()));
commands_menu->addAction(commands_act);
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &MainWindow::updateUndoRedoActions);
updateUndoRedoActions();
// View Menu
QMenu *view_menu = menuBar()->addMenu(tr("&View"));
@@ -140,7 +148,7 @@ void MainWindow::createActions() {
view_menu->addAction(messages_dock->toggleViewAction());
view_menu->addAction(video_dock->toggleViewAction());
view_menu->addSeparator();
view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(default_state); });
view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(utils::qbytes(default_state)); });
// Tools Menu
tools_menu = menuBar()->addMenu(tr("&Tools"));
@@ -187,7 +195,7 @@ void MainWindow::createDockWidgets() {
video_splitter->addWidget(charts_container);
video_splitter->setStretchFactor(1, 1);
video_splitter->restoreState(settings.video_splitter_state);
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);
@@ -218,6 +226,14 @@ 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();
@@ -232,7 +248,7 @@ void MainWindow::DBCFileChanged() {
QStringList title;
for (auto f : dbc()->allDBCFiles()) {
title.push_back(tr("(%1) %2").arg(toString(dbc()->sources(f)), f->name()));
title.push_back(tr("(%1) %2").arg(QString::fromStdString(toString(dbc()->sources(f))), QString::fromStdString(f->name())));
}
setWindowFilePath(title.join(" | "));
@@ -251,27 +267,27 @@ void MainWindow::selectAndOpenStream() {
void MainWindow::closeStream() {
openStream(new DummyStream(this));
if (dbc()->nonEmptyDBCCount() > 0) {
emit dbc()->DBCFileChanged();
emit dbcNotifier()->DBCFileChanged();
}
statusBar()->showMessage(tr("stream closed"));
}
void MainWindow::exportToCSV() {
QString dir = QString("%1/%2.csv").arg(settings.last_dir).arg(can->routeName());
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);
utils::exportToCSV(fn.toStdString());
}
}
void MainWindow::newFile(SourceSet s) {
closeFile(s);
dbc()->open(s, "", "");
dbc()->open(s, std::string(""), std::string(""));
}
void MainWindow::openFile(SourceSet s) {
remindSaveChanges();
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)");
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)");
if (!fn.isEmpty()) {
loadFile(fn, s);
}
@@ -281,13 +297,13 @@ void MainWindow::loadFile(const QString &fn, SourceSet s) {
if (!fn.isEmpty()) {
closeFile(s);
QString error;
if (dbc()->open(s, fn, &error)) {
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(error);
msg_box.setDetailedText(QString::fromStdString(error));
msg_box.exec();
}
}
@@ -298,16 +314,25 @@ void MainWindow::loadDBCFromOpendbc(const QString &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);
QString dbc_str = QGuiApplication::clipboard()->text();
QString error;
bool ret = dbc()->open(s, "", dbc_str, &error);
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(error);
msg_box.setDetailedText(QString::fromStdString(error));
msg_box.exec();
}
}
@@ -331,7 +356,7 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
can->start();
loadFile(dbc_file);
statusBar()->showMessage(tr("Stream [%1] started").arg(can->routeName()), 2000);
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);
@@ -339,7 +364,7 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
tools_menu->setEnabled(has_stream);
createDockWidgets();
video_dock->setWindowTitle(can->routeName());
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});
@@ -366,13 +391,16 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
}
void MainWindow::eventsMerged() {
if (!can->liveStreaming() && std::exchange(car_fingerprint, can->carFingerprint()) != car_fingerprint) {
if (!can->liveStreaming() && std::exchange(car_fingerprint, QString::fromStdString(can->carFingerprint())) != car_fingerprint) {
video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPRINT: %2")
.arg(can->routeName())
.arg(QString::fromStdString(can->routeName()))
.arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint));
// Don't overwrite already loaded DBC
if (!dbc()->nonEmptyDBCCount() && fingerprint_to_dbc.object().contains(car_fingerprint)) {
QTimer::singleShot(0, this, [this]() { loadDBCFromOpendbc(fingerprint_to_dbc[car_fingerprint].toString() + ".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");
});
}
}
}
@@ -414,7 +442,7 @@ void MainWindow::closeFile(DBCFile *dbc_file) {
void MainWindow::saveFile(DBCFile *dbc_file) {
assert(dbc_file != nullptr);
if (!dbc_file->filename.isEmpty()) {
if (!dbc_file->filename.empty()) {
dbc_file->save();
UndoStack::instance()->setClean();
statusBar()->showMessage(tr("File saved"), 2000);
@@ -424,10 +452,10 @@ void MainWindow::saveFile(DBCFile *dbc_file) {
}
void MainWindow::saveFileAs(DBCFile *dbc_file) {
QString title = tr("Save File (bus: %1)").arg(toString(dbc()->sources(dbc_file)));
QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)"));
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);
dbc_file->saveAs(fn.toStdString());
UndoStack::instance()->setClean();
statusBar()->showMessage(tr("File saved as %1").arg(fn), 2000);
updateRecentFiles(fn);
@@ -444,8 +472,11 @@ void MainWindow::saveToClipboard() {
void MainWindow::saveFileToClipboard(DBCFile *dbc_file) {
assert(dbc_file != nullptr);
QGuiApplication::clipboard()->setText(dbc_file->generateDBC());
QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!"));
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() {
@@ -465,26 +496,27 @@ void MainWindow::updateLoadSaveMenus() {
auto dbc_file = dbc()->findDBCFile(source);
if (dbc_file) {
bus_menu->addSeparator();
bus_menu->addAction(dbc_file->name() + " (" + toString(dbc()->sources(dbc_file)) + ")")->setEnabled(false);
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 ? dbc_file->name() : "No DBCs loaded"));
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) {
settings.recent_files.removeAll(fn);
settings.recent_files.prepend(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.removeLast();
settings.recent_files.pop_back();
}
settings.last_dir = QFileInfo(fn).absolutePath();
settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string();
}
void MainWindow::updateRecentFileMenu() {
@@ -497,8 +529,8 @@ void MainWindow::updateRecentFileMenu() {
}
for (int i = 0; i < num_recent_files; ++i) {
QString text = tr("&%1 %2").arg(i + 1).arg(QFileInfo(settings.recent_files[i]).fileName());
open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(file); });
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)); });
}
}
@@ -558,22 +590,23 @@ void MainWindow::closeEvent(QCloseEvent *event) {
remindSaveChanges();
installDownloadProgressHandler(nullptr);
qInstallMessageHandler(nullptr);
installMessageHandler(nullptr);
if (floating_window)
floating_window->deleteLater();
// save states
settings.geometry = saveGeometry();
settings.window_state = saveState();
settings.geometry = utils::toBytes(saveGeometry());
settings.window_state = utils::toBytes(saveState());
if (can && !can->liveStreaming()) {
settings.video_splitter_state = video_splitter->saveState();
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);
}
@@ -629,26 +662,35 @@ void MainWindow::saveSessionState() {
if (auto *detail = center_widget->getDetailWidget()) {
auto [active_id, ids] = detail->serializeMessageIds();
settings.active_msg_id = active_id;
settings.selected_msg_ids = ids;
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());
}
if (charts_widget)
settings.active_charts = charts_widget->serializeChartIds();
}
void MainWindow::restoreSessionState() {
if (settings.recent_dbc_file.isEmpty() || dbc()->nonEmptyDBCCount() == 0) return;
if (settings.recent_dbc_file.empty() || dbc()->nonEmptyDBCCount() == 0) return;
QString dbc_file;
for (auto& f : dbc()->allDBCFiles())
if (!f->isEmpty()) { dbc_file = f->filename; break; }
if (dbc_file != settings.recent_dbc_file) return;
if (!f->isEmpty()) { dbc_file = QString::fromStdString(f->filename); break; }
if (dbc_file.toStdString() != settings.recent_dbc_file) return;
if (!settings.selected_msg_ids.isEmpty())
center_widget->ensureDetailWidget()->restoreTabs(settings.active_msg_id, settings.selected_msg_ids);
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())
charts_widget->restoreChartsFromIds(settings.active_charts);
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

View File

@@ -1,13 +1,16 @@
#pragma once
#include <QDockWidget>
#include <QJsonDocument>
#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"
@@ -67,6 +70,7 @@ protected:
void findSimilarBits();
void findSignal();
void undoStackCleanChanged(bool clean);
void updateUndoRedoActions();
void onlineHelp();
void toggleFullScreen();
void updateStatus();
@@ -85,7 +89,7 @@ protected:
QVBoxLayout *charts_layout;
QProgressBar *progress_bar;
QLabel *status_label;
QJsonDocument fingerprint_to_dbc;
std::unordered_map<std::string, std::string> fingerprint_to_dbc;
QSplitter *video_splitter = nullptr;
enum { MAX_RECENT_FILES = 15 };
QMenu *open_recent_menu = nullptr;
@@ -96,8 +100,10 @@ protected:
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;
QByteArray default_state;
std::vector<uint8_t> default_state;
};
class HelpOverlay : public QWidget {

View File

@@ -1,4 +1,5 @@
#include "tools/cabana/messageswidget.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <limits>
#include <utility>
@@ -43,8 +44,8 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent);
QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions);
QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::dbcModified);
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &MessageListModel::dbcModified);
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &MessageListModel::dbcModified);
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &MessageListModel::dbcModified);
QObject::connect(model, &MessageListModel::modelReset, [this]() {
if (current_msg_id) {
selectMessage(*current_msg_id);
@@ -64,7 +65,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
setWhatsThis(tr(R"(
<b>Message View</b><br/>
<!-- TODO: add descprition here -->
<!-- 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 />
@@ -146,7 +147,7 @@ void MessagesWidget::menuAboutToShow() {
action->setCheckable(true);
action->setChecked(settings.multiple_lines_hex);
action = menu->addAction(tr("Show inactive Messages"), model, &MessageListModel::showInactivemessages);
action = menu->addAction(tr("Show inactive messages"), model, &MessageListModel::showInactiveMessages);
action->setCheckable(true);
action->setChecked(model->show_inactive_messages);
}
@@ -205,18 +206,18 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
} else if (role == Qt::ToolTipRole && index.column() == Column::NAME) {
auto msg = dbc()->msg(item.id);
auto tooltip = item.name;
if (msg && !msg->comment.isEmpty()) tooltip += "<br /><span style=\"color:gray;\">" + msg->comment + "</span>";
if (msg && !msg->comment.empty()) tooltip += "<br /><span style=\"color:gray;\">" + QString::fromStdString(msg->comment) + "</span>";
return tooltip;
}
return {};
}
void MessageListModel::setFilterStrings(const QMap<int, QString> &filters) {
void MessageListModel::setFilterStrings(const std::map<int, QString> &filters) {
filters_ = filters;
filterAndSort();
}
void MessageListModel::showInactivemessages(bool show) {
void MessageListModel::showInactiveMessages(bool show) {
show_inactive_messages = show;
filterAndSort();
}
@@ -264,20 +265,20 @@ static bool parseRange(const QString &filter, uint32_t value, int base = 10) {
}
bool MessageListModel::match(const MessageListModel::Item &item) {
if (filters_.isEmpty())
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.value();
switch (it.key()) {
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 s->name.contains(txt, Qt::CaseInsensitive); });
[&txt](const auto &s) { return QString::fromStdString(s->name).contains(txt, Qt::CaseInsensitive); });
}
break;
}
@@ -323,8 +324,8 @@ bool MessageListModel::filterAndSort() {
if (show_inactive_messages || can->isMessageActive(id)) {
auto msg = dbc()->msg(id);
Item item = {.id = id,
.name = msg ? msg->name : UNTITLED,
.node = msg ? msg->transmitter : QString()};
.name = msg ? QString::fromStdString(msg->name) : QString::fromStdString(UNTITLED),
.node = msg ? QString::fromStdString(msg->transmitter) : QString()};
if (match(item))
items.emplace_back(item);
}
@@ -387,10 +388,13 @@ void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option,
painter->setPen(oldPen);
}
void MessageView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
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.
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
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() {
@@ -421,9 +425,9 @@ MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizont
}
void MessageViewHeader::updateFilters() {
QMap<int, QString> filters;
for (int i = 0; i < count(); i++) {
if (editors[i] && !editors[i]->text().isEmpty()) {
std::map<int, QString> filters;
for (int i = 0; i < (int)editors.size(); i++) {
if (!editors[i]->text().isEmpty()) {
filters[i] = editors[i]->text();
}
}
@@ -432,27 +436,24 @@ void MessageViewHeader::updateFilters() {
void MessageViewHeader::updateHeaderPositions() {
QSize sz = QHeaderView::sizeHint();
for (int i = 0; i < count(); i++) {
if (editors[i]) {
int h = editors[i]->sizeHint().height();
editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h);
editors[i]->setHidden(isSectionHidden(i));
}
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 = 0; i < count(); i++) {
if (!editors[i]) {
QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
editors[i] = new QLineEdit(this);
editors[i]->setClearButtonEnabled(true);
editors[i]->setPlaceholderText(tr("Filter %1").arg(column_name));
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(editors[i], &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters);
}
QObject::connect(edit, &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters);
editors.push_back(edit);
}
setViewportMargins(0, 0, 0, editors[0] ? editors[0]->sizeHint().height() : 0);
setViewportMargins(0, 0, 0, !editors.empty() ? editors[0]->sizeHint().height() : 0);
QHeaderView::updateGeometries();
updateHeaderPositions();
@@ -460,5 +461,5 @@ void MessageViewHeader::updateGeometries() {
QSize MessageViewHeader::sizeHint() const {
QSize sz = QHeaderView::sizeHint();
return editors[0] ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz;
return !editors.empty() ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz;
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <map>
#include <optional>
#include <set>
#include <vector>
@@ -35,8 +37,8 @@ public:
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 QMap<int, QString> &filters);
void showInactivemessages(bool show);
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();
@@ -56,7 +58,7 @@ private:
void sortItems(std::vector<MessageListModel::Item> &items);
bool match(const MessageListModel::Item &id);
QMap<int, QString> filters_;
std::map<int, QString> filters_;
std::set<MessageId> dbc_messages_;
int sort_column = 0;
Qt::SortOrder sort_order = Qt::AscendingOrder;
@@ -68,11 +70,11 @@ class MessageView : public QTreeView {
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 dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
void wheelEvent(QWheelEvent *event) override;
};
@@ -86,7 +88,7 @@ public:
QSize sizeHint() const override;
void updateFilters();
QMap<int, QLineEdit *> editors;
std::vector<QLineEdit *> editors;
};
class MessagesWidget : public QWidget {
@@ -95,8 +97,13 @@ class MessagesWidget : public QWidget {
public:
MessagesWidget(QWidget *parent);
void selectMessage(const MessageId &message_id);
QByteArray saveHeaderState() const { return view->header()->saveState(); }
bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); }
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:

View File

@@ -26,16 +26,14 @@ static libusb_context *init_usb_ctx() {
return context;
}
Panda::Panda(std::string serial, uint32_t bus_offset, bool passive_mode) : bus_offset(bus_offset), passive_mode(passive_mode) {
Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) {
if (!init_usb_connection(serial)) {
throw std::runtime_error("Error connecting to panda");
}
LOGW("connected to %s over USB", serial.c_str());
hw_type = get_hw_type();
if (!passive_mode) {
can_reset_communications();
}
can_reset_communications();
}
Panda::~Panda() {

View File

@@ -48,12 +48,11 @@ struct can_frame {
class Panda {
public:
Panda(std::string serial="", uint32_t bus_offset=0, bool passive_mode=false);
Panda(std::string serial="", uint32_t bus_offset=0);
~Panda();
cereal::PandaState::PandaType hw_type = cereal::PandaState::PandaType::UNKNOWN;
const uint32_t bus_offset;
const bool passive_mode;
bool connected();
bool comms_healthy();

View File

@@ -1,15 +1,37 @@
#include "tools/cabana/settings.h"
#include <algorithm>
#include <cerrno>
#include <cctype>
#include <charconv>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <map>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
#endif
#include <QAbstractButton>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QFormLayout>
#include <QPushButton>
#include <QSettings>
#include <QStandardPaths>
#include <type_traits>
#include "third_party/json11/json11.hpp"
#include "tools/cabana/utils/util.h"
const int MIN_CACHE_MINIUTES = 30;
@@ -17,9 +39,442 @@ const int MAX_CACHE_MINIUTES = 120;
Settings settings;
template <class SettingOperation>
void settings_op(SettingOperation op) {
QSettings s("cabana");
namespace {
std::filesystem::path settingsFile() {
return utils::configPath() / "cabana.json";
}
struct LoadedSettings {
json11::Json::object values;
bool exists = false;
bool valid = true;
};
class FileLock {
public:
explicit FileLock(const std::filesystem::path &path) {
fd = open(path.c_str(), O_CREAT | O_CLOEXEC, 0600);
if (fd < 0 || flock(fd, LOCK_EX) < 0) {
fprintf(stderr, "failed to lock Cabana settings %s: %s\n", path.c_str(), strerror(errno));
if (fd >= 0) close(fd);
fd = -1;
}
}
~FileLock() {
if (fd >= 0) close(fd);
}
bool isLocked() const { return fd >= 0; }
private:
int fd = -1;
};
LoadedSettings loadSettings() {
std::ifstream input(settingsFile());
if (!input) return {};
const std::string contents{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
std::string error;
auto settings_json = json11::Json::parse(contents, error);
if (!error.empty() || !settings_json.is_object()) {
fprintf(stderr, "failed to read Cabana settings %s%s%s\n", settingsFile().c_str(), error.empty() ? "" : ": ", error.c_str());
return {.exists = true, .valid = false};
}
return {.values = settings_json.object_items(), .exists = true};
}
bool ensureSettingsDirectory() {
const auto path = settingsFile();
std::error_code error;
std::filesystem::create_directories(path.parent_path(), error);
if (error) {
fprintf(stderr, "failed to create Cabana settings directory %s: %s\n", path.parent_path().c_str(), error.message().c_str());
return false;
}
return true;
}
bool writeAll(int fd, const std::string &data) {
size_t written = 0;
while (written < data.size()) {
ssize_t result = write(fd, data.data() + written, data.size() - written);
if (result < 0 && errno == EINTR) continue;
if (result <= 0) return false;
written += result;
}
return true;
}
bool saveSettings(const json11::Json::object &settings_json) {
const auto path = settingsFile();
const std::string contents = json11::Json(settings_json).dump();
std::string temporary_path = path.string() + ".tmp.XXXXXX";
int fd = mkstemp(temporary_path.data());
if (fd < 0) {
fprintf(stderr, "failed to create temporary Cabana settings %s: %s\n", temporary_path.c_str(), strerror(errno));
return false;
}
bool success = writeAll(fd, contents) && fsync(fd) == 0;
if (close(fd) < 0) success = false;
if (success && rename(temporary_path.c_str(), path.c_str()) < 0) success = false;
if (success) {
int dir_fd = open(path.parent_path().c_str(), O_RDONLY | O_CLOEXEC);
success = dir_fd >= 0 && fsync(dir_fd) == 0;
if (dir_fd >= 0 && close(dir_fd) < 0) success = false;
}
if (!success) {
const int saved_errno = errno;
unlink(temporary_path.c_str());
fprintf(stderr, "failed to save Cabana settings to %s: %s\n", path.c_str(), strerror(saved_errno));
}
return success;
}
bool preserveCorruptSettings() {
const auto path = settingsFile();
auto backup = path;
backup += ".corrupt";
for (int i = 1; std::filesystem::exists(backup); ++i) {
backup = path;
backup += ".corrupt." + std::to_string(i);
}
if (rename(path.c_str(), backup.c_str()) < 0) {
fprintf(stderr, "failed to preserve corrupt Cabana settings %s: %s\n", path.c_str(), strerror(errno));
return false;
}
fprintf(stderr, "preserved corrupt Cabana settings at %s\n", backup.c_str());
return true;
}
// TODO: Remove the legacy QSettings migration after users have had time to migrate to cabana.json.
struct LegacyValue {
std::vector<std::string> strings;
std::string bytes;
bool is_byte_array = false;
};
using LegacySettings = std::map<std::string, LegacyValue>;
int hexDigit(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
#ifndef __APPLE__
void appendUtf8(std::string &result, uint32_t codepoint) {
if (codepoint <= 0x7f) {
result.push_back(codepoint);
} else if (codepoint <= 0x7ff) {
result.push_back(0xc0 | (codepoint >> 6));
result.push_back(0x80 | (codepoint & 0x3f));
} else if (codepoint <= 0xffff) {
result.push_back(0xe0 | (codepoint >> 12));
result.push_back(0x80 | ((codepoint >> 6) & 0x3f));
result.push_back(0x80 | (codepoint & 0x3f));
} else {
result.push_back(0xf0 | (codepoint >> 18));
result.push_back(0x80 | ((codepoint >> 12) & 0x3f));
result.push_back(0x80 | ((codepoint >> 6) & 0x3f));
result.push_back(0x80 | (codepoint & 0x3f));
}
}
LegacyValue decodeIniValue(std::string_view encoded) {
std::vector<std::vector<uint32_t>> decoded(1);
std::vector<bool> quoted(1, false);
bool in_quotes = false;
for (size_t i = 0; i < encoded.size();) {
char c = encoded[i++];
if (c == '"') {
in_quotes = !in_quotes;
quoted.back() = true;
} else if (c == ',' && !in_quotes) {
decoded.emplace_back();
quoted.push_back(false);
while (i < encoded.size() && (encoded[i] == ' ' || encoded[i] == '\t')) ++i;
} else if (c == '\\' && i < encoded.size()) {
c = encoded[i++];
static const std::map<char, char> escapes = {
{'a', '\a'}, {'b', '\b'}, {'f', '\f'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'},
{'v', '\v'}, {'"', '"'}, {'?', '?'}, {'\'', '\''}, {'\\', '\\'},
};
if (auto it = escapes.find(c); it != escapes.end()) {
decoded.back().push_back(static_cast<unsigned char>(it->second));
} else if (c == 'x' && i < encoded.size() && hexDigit(encoded[i]) >= 0) {
uint32_t value = 0;
while (i < encoded.size() && hexDigit(encoded[i]) >= 0) value = (value << 4) + hexDigit(encoded[i++]);
decoded.back().push_back(value & 0xffff);
} else if (c >= '0' && c <= '7') {
uint32_t value = c - '0';
while (i < encoded.size() && encoded[i] >= '0' && encoded[i] <= '7') value = (value << 3) + (encoded[i++] - '0');
decoded.back().push_back(value & 0xffff);
}
} else {
decoded.back().push_back(static_cast<unsigned char>(c));
}
}
LegacyValue result;
for (size_t i = 0; i < decoded.size(); ++i) {
auto &value = decoded[i];
if (!quoted[i]) {
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.erase(value.begin());
while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.pop_back();
}
std::string string_value;
for (size_t j = 0; j < value.size(); ++j) {
uint32_t codepoint = value[j];
if (codepoint >= 0xd800 && codepoint <= 0xdbff && j + 1 < value.size() && value[j + 1] >= 0xdc00 && value[j + 1] <= 0xdfff) {
codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (value[++j] - 0xdc00);
}
appendUtf8(string_value, codepoint);
}
result.strings.push_back(std::move(string_value));
}
if (result.strings.size() == 1 && result.strings[0] == "@Invalid()") {
result.strings.clear();
} else if (decoded.size() == 1) {
static constexpr std::string_view prefix = "@ByteArray(";
const auto &value = decoded[0];
if (value.size() >= prefix.size() + 1 && std::equal(prefix.begin(), prefix.end(), value.begin()) && value.back() == ')') {
result.is_byte_array = true;
result.bytes.reserve(value.size() - prefix.size() - 1);
for (size_t i = prefix.size(); i + 1 < value.size(); ++i) result.bytes.push_back(value[i] & 0xff);
}
}
if (!result.is_byte_array) {
for (auto &value : result.strings) {
if (value.compare(0, 2, "@@") == 0) value.erase(0, 1);
}
}
return result;
}
LegacySettings loadLegacySettings() {
auto path = settingsFile();
path.replace_filename("cabana.conf");
std::ifstream input(path);
if (!input) return {};
LegacySettings settings;
bool in_general_section = false;
std::string line;
while (std::getline(input, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line == "[General]") {
in_general_section = true;
continue;
}
if (!line.empty() && line.front() == '[') {
in_general_section = false;
continue;
}
if (!in_general_section || line.empty() || line.front() == ';') continue;
if (auto separator = line.find('='); separator != std::string::npos) {
settings[line.substr(0, separator)] = decodeIniValue(std::string_view(line).substr(separator + 1));
}
}
return settings;
}
#else
std::string cfStringToUtf8(CFStringRef value) {
CFIndex length = CFStringGetLength(value);
CFIndex size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
std::string result(size, '\0');
if (!CFStringGetCString(value, result.data(), size, kCFStringEncodingUTF8)) return {};
result.resize(strlen(result.c_str()));
return result;
}
LegacyValue cfStringValue(CFStringRef string) {
LegacyValue value;
if (CFStringHasPrefix(string, CFSTR("@ByteArray(")) && CFStringHasSuffix(string, CFSTR(")"))) {
CFRange range{11, CFStringGetLength(string) - 12};
std::vector<UniChar> data(range.length);
CFStringGetCharacters(string, range, data.data());
value.is_byte_array = true;
value.bytes.reserve(data.size());
for (UniChar c : data) value.bytes.push_back(c & 0xff);
} else {
std::string string_value = cfStringToUtf8(string);
if (string_value.compare(0, 2, "@@") == 0) string_value.erase(0, 1);
value.strings.push_back(std::move(string_value));
}
return value;
}
LegacySettings loadLegacySettings() {
LegacySettings settings;
CFDictionaryRef values = CFPreferencesCopyMultiple(nullptr, CFSTR("com.cabana"),
kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
if (values == nullptr) return settings;
CFIndex count = CFDictionaryGetCount(values);
std::vector<const void *> keys(count);
std::vector<const void *> objects(count);
CFDictionaryGetKeysAndValues(values, keys.data(), objects.data());
for (CFIndex i = 0; i < count; ++i) {
if (CFGetTypeID(keys[i]) != CFStringGetTypeID()) continue;
std::string key = cfStringToUtf8(static_cast<CFStringRef>(keys[i]));
CFTypeRef object = objects[i];
LegacyValue value;
if (CFGetTypeID(object) == CFBooleanGetTypeID()) {
value.strings.push_back(CFBooleanGetValue(static_cast<CFBooleanRef>(object)) ? "true" : "false");
} else if (CFGetTypeID(object) == CFNumberGetTypeID()) {
int number = 0;
if (CFNumberGetValue(static_cast<CFNumberRef>(object), kCFNumberIntType, &number)) value.strings.push_back(std::to_string(number));
} else if (CFGetTypeID(object) == CFStringGetTypeID()) {
value = cfStringValue(static_cast<CFStringRef>(object));
} else if (CFGetTypeID(object) == CFDataGetTypeID()) {
auto data = static_cast<CFDataRef>(object);
value.is_byte_array = true;
value.bytes.assign(reinterpret_cast<const char *>(CFDataGetBytePtr(data)), CFDataGetLength(data));
} else if (CFGetTypeID(object) == CFArrayGetTypeID()) {
auto array = static_cast<CFArrayRef>(object);
for (CFIndex j = 0; j < CFArrayGetCount(array); ++j) {
CFTypeRef item = CFArrayGetValueAtIndex(array, j);
if (CFGetTypeID(item) != CFStringGetTypeID()) {
value.strings.clear();
break;
}
auto item_value = cfStringValue(static_cast<CFStringRef>(item));
if (item_value.strings.size() != 1) {
value.strings.clear();
break;
}
value.strings.push_back(std::move(item_value.strings[0]));
}
}
if (!value.strings.empty() || value.is_byte_array || CFGetTypeID(object) == CFArrayGetTypeID()) {
settings.emplace(std::move(key), std::move(value));
}
}
CFRelease(values);
return settings;
}
#endif
template <typename T>
void readLegacySetting(const LegacySettings &legacy_settings, const char *key, T &value) {
auto it = legacy_settings.find(key);
if (it == legacy_settings.end() || it->second.strings.size() != 1) return;
const auto &stored = it->second.strings[0];
if constexpr (std::is_same_v<T, bool>) {
if (stored == "true") value = true;
if (stored == "false") value = false;
} else if constexpr (std::is_integral_v<T> || std::is_enum_v<T>) {
int number = 0;
auto [end, error] = std::from_chars(stored.data(), stored.data() + stored.size(), number);
if (error == std::errc{} && end == stored.data() + stored.size()) value = static_cast<T>(number);
}
}
void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::string &value) {
auto it = legacy_settings.find(key);
if (it != legacy_settings.end() && it->second.strings.size() == 1) value = it->second.strings[0];
}
void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector<std::string> &value) {
auto it = legacy_settings.find(key);
if (it != legacy_settings.end() && !it->second.is_byte_array) value = it->second.strings;
}
void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector<uint8_t> &value) {
auto it = legacy_settings.find(key);
if (it != legacy_settings.end() && it->second.is_byte_array) {
value.assign(it->second.bytes.begin(), it->second.bytes.end());
}
}
template <typename T>
void readSetting(const json11::Json::object &settings_json, const char *key, T &value) {
auto it = settings_json.find(key);
if (it == settings_json.end()) return;
if constexpr (std::is_same_v<T, bool>) {
if (it->second.is_bool()) value = it->second.bool_value();
} else if constexpr (std::is_integral_v<T>) {
if (it->second.is_number()) value = it->second.int_value();
} else if constexpr (std::is_enum_v<T>) {
if (it->second.is_number()) value = static_cast<T>(it->second.int_value());
}
}
void readSetting(const json11::Json::object &settings_json, const char *key, std::string &value) {
auto it = settings_json.find(key);
if (it != settings_json.end() && it->second.is_string()) value = it->second.string_value();
}
void readSetting(const json11::Json::object &settings_json, const char *key, std::vector<std::string> &value) {
auto it = settings_json.find(key);
if (it == settings_json.end() || !it->second.is_array()) return;
std::vector<std::string> stored;
for (const auto &item : it->second.array_items()) {
if (!item.is_string()) return;
stored.push_back(item.string_value());
}
value = std::move(stored);
}
void readSetting(const json11::Json::object &settings_json, const char *key, std::vector<uint8_t> &value) {
auto it = settings_json.find(key);
if (it == settings_json.end() || !it->second.is_string()) return;
const auto &hex = it->second.string_value();
if (hex.size() % 2 == 0 && std::all_of(hex.begin(), hex.end(), [](unsigned char c) { return std::isxdigit(c); })) {
value.clear();
value.reserve(hex.size() / 2);
for (size_t i = 0; i < hex.size(); i += 2) {
value.push_back((hexDigit(hex[i]) << 4) | hexDigit(hex[i + 1]));
}
}
}
template <typename T>
void writeSetting(json11::Json::object &settings_json, const char *key, const T &value) {
if constexpr (std::is_same_v<T, bool>) {
settings_json[key] = value;
} else if constexpr (std::is_integral_v<T> || std::is_enum_v<T>) {
settings_json[key] = static_cast<int>(value);
}
}
void writeSetting(json11::Json::object &settings_json, const char *key, const std::string &value) {
settings_json[key] = value;
}
void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector<std::string> &value) {
settings_json[key] = value;
}
void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector<uint8_t> &value) {
static const char digits[] = "0123456789abcdef";
std::string hex;
hex.reserve(value.size() * 2);
for (uint8_t b : value) {
hex.push_back(digits[b >> 4]);
hex.push_back(digits[b & 0xf]);
}
settings_json[key] = hex;
}
template <class Store, class SettingOperation>
void settingsOp(Store &s, SettingOperation op) {
op(s, "absolute_time", settings.absolute_time);
op(s, "fps", settings.fps);
op(s, "max_cached_minutes", settings.max_cached_minutes);
@@ -47,17 +502,38 @@ void settings_op(SettingOperation op) {
op(s, "active_charts", settings.active_charts);
}
} // namespace
Settings::Settings() {
last_dir = last_route_dir = QDir::homePath();
log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/";
settings_op([](QSettings &s, const QString &key, auto &value) {
if (auto v = s.value(key); v.canConvert<std::decay_t<decltype(value)>>())
value = v.value<std::decay_t<decltype(value)>>();
});
last_dir = last_route_dir = utils::homePath();
log_path = utils::homePath() + "/cabana_live_stream/";
const auto stored_settings = loadSettings();
if (stored_settings.valid) {
if (stored_settings.exists) {
settingsOp(stored_settings.values, [](const auto &s, const char *key, auto &value) { readSetting(s, key, value); });
} else {
auto legacy_settings = loadLegacySettings();
settingsOp(legacy_settings, [](const auto &s, const char *key, auto &value) { readLegacySetting(s, key, value); });
}
}
fps = std::clamp(fps, 1, 100);
}
Settings::~Settings() {
settings_op([](QSettings &s, const QString &key, auto &v) { s.setValue(key, v); });
// Must be called before main() returns: json11's internal statistics are constructed on first
// use at runtime, so they are destroyed before this pre-main global. Saving from ~Settings
// would use them after destruction and corrupt the heap.
void Settings::save() {
if (!ensureSettingsDirectory()) return;
auto lock_path = settingsFile();
lock_path += ".lock";
FileLock lock(lock_path);
if (!lock.isLocked()) return;
auto stored_settings = loadSettings();
if (!stored_settings.valid && !preserveCorruptSettings()) return;
settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); });
saveSettings(stored_settings.values);
}
// SettingsDlg
@@ -101,8 +577,9 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
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(settings.log_path, this));
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);
@@ -115,7 +592,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
QObject::connect(browse_btn, &QPushButton::clicked, [this]() {
QString fn = QFileDialog::getExistingDirectory(
this, tr("Log File Location"),
QStandardPaths::writableLocation(QStandardPaths::HomeLocation),
QString::fromStdString(utils::homePath()),
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!fn.isEmpty()) {
log_path->setText(fn);
@@ -134,7 +611,7 @@ void SettingsDlg::save() {
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();
settings.log_path = log_path->text().toStdString();
settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex();
emit settings.changed();
QDialog::accept();

View File

@@ -1,56 +1,28 @@
#pragma once
#include <QByteArray>
#include <cstdint>
#include <vector>
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QLineEdit>
#include <QSpinBox>
#define LIGHT_THEME 1
#define DARK_THEME 2
#include "tools/cabana/core/settings.h"
class Settings : public QObject {
class Settings : public QObject, public CabanaSettingsState {
Q_OBJECT
public:
enum DragDirection {
MsbFirst,
LsbFirst,
AlwaysLE,
AlwaysBE,
};
Settings();
~Settings();
void save();
bool absolute_time = false;
int fps = 10;
int max_cached_minutes = 30;
int chart_height = 200;
int chart_column_count = 1;
int chart_range = 3 * 60; // 3 minutes
int chart_series_type = 0;
int theme = 0;
int sparkline_range = 15; // 15 seconds
bool multiple_lines_hex = false;
bool log_livestream = true;
bool suppress_defined_signals = false;
QString log_path;
QString last_dir;
QString last_route_dir;
QByteArray geometry;
QByteArray video_splitter_state;
QByteArray window_state;
QStringList recent_files;
QByteArray message_header_state;
DragDirection drag_direction = MsbFirst;
// session data
QString recent_dbc_file;
QString active_msg_id;
QStringList selected_msg_ids;
QStringList active_charts;
// Qt frontend layout state. This intentionally stays outside CabanaSettingsState.
std::vector<uint8_t> geometry;
std::vector<uint8_t> video_splitter_state;
std::vector<uint8_t> window_state;
std::vector<uint8_t> message_header_state;
signals:
void changed();

View File

@@ -1,8 +1,9 @@
#include "tools/cabana/signalview.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <algorithm>
#include <future>
#include <QCompleter>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QHeaderView>
@@ -11,10 +12,10 @@
#include <QPainterPath>
#include <QPushButton>
#include <QScrollBar>
#include <QtConcurrent>
#include <QVBoxLayout>
#include "tools/cabana/commands.h"
#include "tools/cabana/utils/util.h"
// SignalModel
@@ -25,21 +26,21 @@ static QString signalTypeToString(cabana::Signal::Type type) {
}
SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) {
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &SignalModel::refresh);
QObject::connect(dbc(), &DBCManager::msgUpdated, this, &SignalModel::handleMsgChanged);
QObject::connect(dbc(), &DBCManager::msgRemoved, this, &SignalModel::handleMsgChanged);
QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalModel::handleSignalAdded);
QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalModel::handleSignalUpdated);
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &SignalModel::handleSignalRemoved);
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &SignalModel::refresh);
QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &SignalModel::handleMsgChanged);
QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &SignalModel::handleMsgChanged);
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalModel::handleSignalAdded);
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalModel::handleSignalUpdated);
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &SignalModel::handleSignalRemoved);
}
void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) {
Item *parent_item = new Item{.sig = sig, .parent = root_item, .title = sig->name, .type = Item::Sig};
root_item->children.insert(pos, parent_item);
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{.sig = sig, .parent = parent_item, .title = titles[i], .type = (Item::Type)(i + Item::Name)};
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;
@@ -63,7 +64,7 @@ void SignalModel::refresh() {
root.reset(new SignalModel::Item);
if (auto msg = dbc()->msg(msg_id)) {
for (auto s : msg->getSignals()) {
if (filter_str.isEmpty() || s->name.contains(filter_str, Qt::CaseInsensitive)) {
if (filter_str.isEmpty() || QString::fromStdString(s->name).contains(filter_str, Qt::CaseInsensitive)) {
insertItem(root.get(), root->children.size(), s);
}
}
@@ -124,25 +125,25 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const {
const Item *item = getItem(index);
if (role == Qt::DisplayRole || role == Qt::EditRole) {
if (index.column() == 0) {
return item->type == Item::Sig ? item->sig->name : item->title;
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 item->sig->name;
case Item::Name: return QString::fromStdString(item->sig->name);
case Item::Size: return item->sig->size;
case Item::Node: return item->sig->receiver_name;
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 doubleToString(item->sig->offset);
case Item::Factor: return doubleToString(item->sig->factor);
case Item::Unit: return item->sig->unit;
case Item::Comment: return item->sig->comment;
case Item::Min: return doubleToString(item->sig->min);
case Item::Max: return doubleToString(item->sig->max);
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(desc);
val_desc << QString("%1 \"%2\"").arg(val).arg(QString::fromStdString(desc));
}
return val_desc.join(" ");
}
@@ -165,17 +166,17 @@ bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int r
Item *item = getItem(index);
cabana::Signal s = *item->sig;
switch (item->type) {
case Item::Name: s.name = value.toString(); break;
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(); 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(); break;
case Item::Comment: s.comment = value.toString(); 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;
@@ -189,7 +190,7 @@ bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int r
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(s.name);
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;
}
@@ -197,7 +198,7 @@ bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s)
if (s.is_little_endian != origin_s->is_little_endian) {
s.start_bit = flipBitPos(s.start_bit);
}
UndoStack::push(new EditSignalCommand(msg_id, origin_s, s));
UndoStack::instance()->push(new EditSignalCommand(msg_id, origin_s, s));
return true;
}
@@ -214,7 +215,7 @@ void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
beginInsertRows({}, i, i);
insertItem(root.get(), i, sig);
endInsertRows();
} else if (sig->name.contains(filter_str, Qt::CaseInsensitive)) {
} else if (QString::fromStdString(sig->name).contains(filter_str, Qt::CaseInsensitive)) {
refresh();
}
}
@@ -229,7 +230,9 @@ void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
int to = dbc()->msg(msg_id)->indexOf(sig);
if (to != row) {
beginMoveRows({}, row, row, {}, to > row ? to + 1 : to);
root->children.move(row, to);
auto item = root->children[row];
root->children.erase(root->children.begin() + row);
root->children.insert(root->children.begin() + to, item);
endMoveRows();
}
}
@@ -239,7 +242,8 @@ void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
void SignalModel::handleSignalRemoved(const cabana::Signal *sig) {
if (int row = signalRow(sig); row != -1) {
beginRemoveRows({}, row, row);
delete root->children.takeAt(row);
delete root->children[row];
root->children.erase(root->children.begin() + row);
endRemoveRows();
}
}
@@ -248,7 +252,7 @@ void SignalModel::handleSignalRemoved(const cabana::Signal *sig) {
SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {
name_validator = new NameValidator(this);
node_validator = new QRegExpValidator(QRegExp("^\\w+(,\\w+)*$"), this);
node_validator = new NodeValidator(this);
double_validator = new DoubleValidator(this);
label_font.setPointSize(8);
@@ -301,7 +305,7 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
path.addRoundedRect(icon_rect, 3, 3);
painter->setPen(item->highlight ? Qt::white : Qt::black);
painter->setFont(label_font);
painter->fillPath(path, item->sig->color.darker(item->highlight ? 125 : 0));
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);
@@ -372,12 +376,6 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie
else if (item->type == SignalModel::Item::Node) e->setValidator(node_validator);
else e->setValidator(double_validator);
if (item->type == SignalModel::Item::Name) {
QCompleter *completer = new QCompleter(dbc()->signalNames(), e);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
e->setCompleter(completer);
}
return e;
} else if (item->type == SignalModel::Item::Size) {
QSpinBox *spin = new QSpinBox(parent);
@@ -395,7 +393,7 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie
return c;
} else if (item->type == SignalModel::Item::Desc) {
ValueDescriptionDlg dlg(item->sig->val_desc, parent);
dlg.setWindowTitle(item->sig->name);
dlg.setWindowTitle(QString::fromStdString(item->sig->name));
if (dlg.exec()) {
((QAbstractItemModel *)index.model())->setData(index, QVariant::fromValue(dlg.val_desc));
}
@@ -422,8 +420,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts),
QHBoxLayout *hl = new QHBoxLayout(title_bar);
hl->addWidget(signal_count_lb = new QLabel());
filter_edit = new QLineEdit(this);
QRegularExpression re("\\S+");
filter_edit->setValidator(new QRegularExpressionValidator(re, this));
filter_edit->setValidator(new NonWhitespaceValidator(this));
filter_edit->setClearButtonEnabled(true);
filter_edit->setPlaceholderText(tr("Filter Signal"));
hl->addWidget(filter_edit);
@@ -475,8 +472,8 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts),
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);
QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalView::handleSignalAdded);
QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalView::handleSignalUpdated);
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalView::handleSignalAdded);
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalView::handleSignalUpdated);
QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); });
QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); });
QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState);
@@ -489,7 +486,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts),
setWhatsThis(tr(R"(
<b>Signal view</b><br />
<!-- TODO: add descprition here -->
<!-- TODO: add description here -->
)"));
}
@@ -518,7 +515,7 @@ void SignalView::rowsChanged() {
tree->setIndexWidget(index, w);
auto sig = model->getItem(index)->sig;
QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::push(new RemoveSigCommand(model->msg_id, 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);
});
@@ -621,7 +618,7 @@ void SignalView::updateState(const std::set<MessageId> *msgs) {
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 = item->sig->formatValue(value);
item->sig_val = QString::fromStdString(item->sig->formatValue(value));
max_value_width = std::max(max_value_width, fontMetrics().horizontalAdvance(item->sig_val));
}
}
@@ -635,13 +632,13 @@ void SignalView::updateState(const std::set<MessageId> *msgs) {
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));
QFutureSynchronizer<void> synchronizer;
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));
synchronizer.addFuture(QtConcurrent::run(
&item->sparkline, &Sparkline::update, item->sig, first, last, settings.sparkline_range, size));
futures.push_back(std::async(std::launch::async,
&Sparkline::update, &item->sparkline, item->sig, first, last, settings.sparkline_range, size));
}
synchronizer.waitForFinished();
for (auto &f : futures) f.get();
}
for (int i = 0; i < model->rowCount(); ++i) {
@@ -677,7 +674,7 @@ ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions, Q
int row = 0;
for (auto &[val, desc] : descriptions) {
table->setItem(row, 0, new QTableWidgetItem(QString::number(val)));
table->setItem(row, 1, new QTableWidgetItem(desc));
table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(desc)));
++row;
}
@@ -706,7 +703,7 @@ void ValueDescriptionDlg::save() {
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});
val_desc.push_back({val.toDouble(), desc.toStdString()});
}
}
QDialog::accept();

View File

@@ -20,12 +20,15 @@ class SignalModel : public QAbstractItemModel {
public:
struct Item {
enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc };
~Item() { qDeleteAll(children); }
inline int row() { return parent->children.indexOf(this); }
~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;
QList<Item *> children;
std::vector<Item *> children;
const cabana::Signal *sig = nullptr;
QString title;
@@ -126,9 +129,12 @@ private:
// update widget geometries in QTreeView::rowsInserted
QTreeView::rowsInserted(parent, start, end);
}
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override {
void setModel(QAbstractItemModel *m) override {
QTreeView::setModel(m);
// Bypass the slow call to QTreeView::dataChanged.
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
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);

View File

@@ -1,4 +1,5 @@
#include "tools/cabana/streams/abstractstream.h"
#include "tools/cabana/dbc/dbcqt.h"
#include <limits>
#include <utility>
@@ -18,8 +19,8 @@ AbstractStream::AbstractStream(QObject *parent) : QObject(parent) {
QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection);
QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo);
QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; });
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &AbstractStream::updateMasks);
QObject::connect(dbc(), &DBCManager::maskUpdated, this, &AbstractStream::updateMasks);
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &AbstractStream::updateMasks);
QObject::connect(dbcNotifier(), &QtDBCNotifier::maskUpdated, this, &AbstractStream::updateMasks);
}
void AbstractStream::updateMasks() {
@@ -233,18 +234,18 @@ std::pair<CanEventIter, CanEventIter> AbstractStream::eventsInRange(const Messag
namespace {
enum Color { GREYISH_BLUE, CYAN, RED};
QColor getColor(int c) {
CabanaColor getColor(int c) {
constexpr int start_alpha = 128;
static const QColor colors[] = {
[GREYISH_BLUE] = QColor(102, 86, 169, start_alpha / 2),
[CYAN] = QColor(0, 187, 255, start_alpha),
[RED] = QColor(255, 0, 0, start_alpha),
static const CabanaColor colors[] = {
[GREYISH_BLUE] = CabanaColor(102, 86, 169, start_alpha / 2),
[CYAN] = CabanaColor(0, 187, 255, start_alpha),
[RED] = CabanaColor(255, 0, 0, start_alpha),
};
return settings.theme == LIGHT_THEME ? colors[c] : colors[c].lighter(135);
}
inline QColor blend(const QColor &a, const QColor &b) {
return QColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2);
inline CabanaColor blend(const CabanaColor &a, const CabanaColor &b) {
return CabanaColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2);
}
// Calculate the frequency from the past one minute data
@@ -271,7 +272,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
if (dat.size() != size) {
dat.assign(can_data, can_data + size);
colors.assign(size, QColor(0, 0, 0, 0));
colors.assign(size, CabanaColor(0, 0, 0, 0));
last_changes.resize(size);
bit_flip_counts.resize(size);
std::for_each(last_changes.begin(), last_changes.end(), [current_sec](auto &c) { c.ts = current_sec; });
@@ -317,7 +318,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
last_change.delta = delta;
} else {
// Fade out
colors[i].setAlphaF(std::max(0.0, colors[i].alphaF() - alpha_delta));
colors[i].setAlphaF(std::max(0.0f, colors[i].alphaF() - alpha_delta));
}
}
}

View File

@@ -3,6 +3,7 @@
#include <algorithm>
#include <array>
#include <condition_variable>
#include <chrono>
#include <memory>
#include <mutex>
#include <optional>
@@ -11,51 +12,12 @@
#include <utility>
#include <vector>
#include <QColor>
#include <QDateTime>
#include "cereal/messaging/messaging.h"
#include "tools/cabana/core/can_data.h"
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/utils/util.h"
#include "tools/replay/util.h"
struct CanData {
void compute(const MessageId &msg_id, const uint8_t *dat, const int size, double current_sec,
double playback_speed, const std::vector<uint8_t> &mask, double in_freq = 0);
double ts = 0.;
uint32_t count = 0;
double freq = 0;
std::vector<uint8_t> dat;
std::vector<QColor> colors;
struct ByteLastChange {
double ts = 0;
int delta = 0;
int same_delta_counter = 0;
bool suppressed = false;
};
std::vector<ByteLastChange> last_changes;
std::vector<std::array<uint32_t, 8>> bit_flip_counts;
double last_freq_update_ts = 0;
};
struct CanEvent {
uint8_t src;
uint32_t address;
uint64_t mono_time;
uint8_t size;
uint8_t dat[];
};
struct CompareCanEvent {
constexpr bool operator()(const CanEvent *const e, uint64_t ts) const { return e->mono_time < ts; }
constexpr bool operator()(uint64_t ts, const CanEvent *const e) const { return ts < e->mono_time; }
};
typedef std::unordered_map<MessageId, std::vector<const CanEvent *>> MessageEventsMap;
using CanEventIter = std::vector<const CanEvent *>::const_iterator;
class AbstractStream : public QObject {
Q_OBJECT
@@ -65,9 +27,9 @@ public:
virtual void start() = 0;
virtual bool liveStreaming() const { return true; }
virtual void seekTo(double ts) {}
virtual QString routeName() const = 0;
virtual QString carFingerprint() const { return ""; }
virtual QDateTime beginDateTime() const { return {}; }
virtual std::string routeName() const = 0;
virtual std::string carFingerprint() const { return ""; }
virtual std::chrono::system_clock::time_point beginDateTime() const { return {}; }
virtual uint64_t beginMonoTime() const { return 0; }
virtual double minSeconds() const { return 0; }
virtual double maxSeconds() const { return 0; }
@@ -113,12 +75,12 @@ protected:
const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c);
void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size);
void waitForSeekFinshed();
virtual void updateLastMessages();
std::vector<const CanEvent *> all_events_;
double current_sec_ = 0;
std::optional<std::pair<double, double>> time_range_;
private:
void updateLastMessages();
void updateLastMsgsTo(double sec);
void updateMasks();
@@ -149,7 +111,7 @@ class DummyStream : public AbstractStream {
Q_OBJECT
public:
DummyStream(QObject *parent) : AbstractStream(parent) {}
QString routeName() const override { return tr("No Stream"); }
std::string routeName() const override { return "No Stream"; }
void start() override {}
};

View File

@@ -1,34 +1,123 @@
#include "tools/cabana/streams/devicestream.h"
#include <cerrno>
#include <chrono>
#include <csignal>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <memory>
#include <string>
#include <thread>
#include <unistd.h>
#include <sys/wait.h>
#include "cereal/services.h"
#include <QButtonGroup>
#include <QFormLayout>
#include <QMessageBox>
#include <QRadioButton>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QThread>
#include "tools/cabana/utils/util.h"
// DeviceStream
DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) {
DeviceStream::DeviceStream(QObject *parent, Mode mode, QString address)
: mode_(mode), address_(address.isEmpty() ? "127.0.0.1" : address), LiveStream(parent) {
}
DeviceStream::~DeviceStream() {
stop();
stopBridge();
}
void DeviceStream::stopBridge() {
if (bridge_pid <= 0) return;
::kill(bridge_pid, SIGTERM);
for (int i = 0; i < 30; ++i) {
int status = 0;
pid_t r = ::waitpid(bridge_pid, &status, WNOHANG);
if (r == bridge_pid || (r < 0 && errno == ECHILD)) {
bridge_pid = -1;
return;
}
usleep(100000); // 100ms, up to ~3s
}
::kill(bridge_pid, SIGKILL);
::waitpid(bridge_pid, nullptr, 0);
bridge_pid = -1;
}
void DeviceStream::start() {
if (mode_ == Mode::Bridge) {
stopBridge();
const std::string path = (std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()) /
"../../cereal/messaging/bridge").lexically_normal().string();
const std::string addr = address_.toStdString();
const char *can_filter = "/\"can/\"";
// Self-pipe: write end is CLOEXEC so it closes on successful exec. If exec
// fails, the child writes errno and the parent aborts stream start.
int err_pipe[2] = {-1, -1};
if (::pipe(err_pipe) != 0) {
QMessageBox::warning(nullptr, tr("Error"),
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno))));
return;
}
pid_t pid = ::fork();
if (pid == 0) {
::close(err_pipe[0]);
::fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC);
execl(path.c_str(), path.c_str(), addr.c_str(), can_filter, static_cast<char *>(nullptr));
const int err = errno;
(void)!::write(err_pipe[1], &err, sizeof(err));
_exit(127);
}
::close(err_pipe[1]);
if (pid < 0) {
::close(err_pipe[0]);
QMessageBox::warning(nullptr, tr("Error"),
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno))));
return;
}
int exec_errno = 0;
const ssize_t n = ::read(err_pipe[0], &exec_errno, sizeof(exec_errno));
::close(err_pipe[0]);
if (n == static_cast<ssize_t>(sizeof(exec_errno))) {
// Child failed to exec; reap and surface the error.
int status = 0;
::waitpid(pid, &status, 0);
QMessageBox::warning(nullptr, tr("Error"),
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno))));
return;
}
bridge_pid = pid;
}
LiveStream::start();
}
void DeviceStream::streamThread() {
zmq_address.isEmpty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1);
// Bridge mode republishes into local msgq, so only the direct Zmq mode talks ZMQ.
// (Upstream sets ZMQ=1 for its bridge path too, which reads nothing — the bridge
// publishes to msgq.)
mode_ == Mode::Zmq ? setenv("ZMQ", "1", 1) : unsetenv("ZMQ");
const std::string address = mode_ == Mode::Zmq ? address_.toStdString() : "127.0.0.1";
std::unique_ptr<Context> context(Context::create());
std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString();
std::unique_ptr<SubSocket> sock(SubSocket::create(context.get(), "can", address, false, true, services.at("can").queue_size));
assert(sock != NULL);
// run as fast as messages come in
while (!QThread::currentThread()->isInterruptionRequested()) {
while (!exit_) {
std::unique_ptr<Message> msg(sock->receive(true));
if (!msg) {
QThread::msleep(50);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
handleEvent(kj::ArrayPtr<capnp::word>((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word)));
@@ -40,28 +129,30 @@ void DeviceStream::streamThread() {
OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
QRadioButton *msgq = new QRadioButton(tr("MSGQ"));
QRadioButton *zmq = new QRadioButton(tr("ZMQ"));
QRadioButton *bridge = new QRadioButton(tr("Bridge"));
zmq->setToolTip(tr("Subscribe directly to a ZMQ 'can' publisher: a device running "
"cereal/messaging/bridge, or konn3kt_canproxy.py on 127.0.0.1."));
bridge->setToolTip(tr("Run cereal/messaging/bridge locally against the device and read msgq."));
ip_address = new QLineEdit(this);
ip_address->setPlaceholderText(tr("Enter device Ip Address"));
QString ip_range = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])";
QString pattern("^" + ip_range + "\\." + ip_range + "\\." + ip_range + "\\." + ip_range + "$");
QRegularExpression re(pattern);
ip_address->setValidator(new QRegularExpressionValidator(re, this));
ip_address->setValidator(new IpAddressValidator(this));
group = new QButtonGroup(this);
group->addButton(msgq, 0);
group->addButton(zmq, 1);
group->addButton(msgq, static_cast<int>(DeviceStream::Mode::Msgq));
group->addButton(zmq, static_cast<int>(DeviceStream::Mode::Zmq));
group->addButton(bridge, static_cast<int>(DeviceStream::Mode::Bridge));
QFormLayout *form_layout = new QFormLayout(this);
form_layout->addRow(msgq);
form_layout->addRow(zmq, ip_address);
form_layout->addRow(bridge);
QObject::connect(group, qOverload<QAbstractButton *, bool>(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) {
ip_address->setEnabled(button == zmq && checked);
if (checked) ip_address->setEnabled(button != msgq);
});
zmq->setChecked(true);
}
AbstractStream *OpenDeviceWidget::open() {
QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text();
bool msgq = group->checkedId() == 0;
return new DeviceStream(qApp, msgq ? "" : ip);
auto mode = static_cast<DeviceStream::Mode>(group->checkedId());
return new DeviceStream(qApp, mode, mode == DeviceStream::Mode::Msgq ? "" : ip_address->text());
}

View File

@@ -2,17 +2,37 @@
#include "tools/cabana/streams/livestream.h"
#include <sys/types.h>
// IQ.Pilot patch: upstream (#38484) folded the ZMQ path into "fork a local bridge and
// read msgq". iqpilot needs the direct ZMQ attach kept as a first-class mode, because
// tools/cabana/konn3kt_canproxy.py publishes a remote device's CAN onto a LOCAL ZMQ
// "can" socket and Cabana attaches to it — see that script's header for the topology.
// So the mode is explicit rather than inferred from whether an address was entered:
//
// Msgq - local msgq, no address (cabana running on the device)
// Zmq - ZMQ subscribe straight to <address> (konn3kt_canproxy, or `bridge` on the device)
// Bridge - fork cereal/messaging/bridge <address>, (upstream's convenience path)
// which ZMQ-subscribes there and republishes
// to local msgq, then read msgq
class DeviceStream : public LiveStream {
Q_OBJECT
public:
DeviceStream(QObject *parent, QString address = {});
inline QString routeName() const override {
return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address);
enum class Mode { Msgq, Zmq, Bridge };
DeviceStream(QObject *parent, Mode mode = Mode::Msgq, QString address = {});
~DeviceStream();
inline std::string routeName() const override {
return "Live Streaming From " + address_.toStdString();
}
protected:
void start() override;
void streamThread() override;
const QString zmq_address;
void stopBridge();
pid_t bridge_pid = -1;
const Mode mode_;
const QString address_;
};
class OpenDeviceWidget : public AbstractOpenStreamWidget {

View File

@@ -1,9 +1,11 @@
#include "tools/cabana/streams/livestream.h"
#include <QThread>
#include <algorithm>
#include <chrono>
#include <fstream>
#include <iomanip>
#include <memory>
#include <sstream>
#include "common/timing.h"
#include "common/util.h"
@@ -14,9 +16,14 @@ struct LiveStream::Logger {
void write(kj::ArrayPtr<capnp::word> data) {
int n = (seconds_since_epoch() - start_ts) / 60.0;
if (std::exchange(segment_num, n) != segment_num) {
const time_t start_time = start_ts;
std::tm local_time = {};
localtime_r(&start_time, &local_time);
std::ostringstream date;
date << std::put_time(&local_time, "%Y-%m-%d--%H-%M-%S");
QString dir = QString("%1/%2--%3")
.arg(settings.log_path)
.arg(QDateTime::fromSecsSinceEpoch(start_ts).toString("yyyy-MM-dd--hh-mm-ss"))
.arg(QString::fromStdString(settings.log_path))
.arg(QString::fromStdString(date.str()))
.arg(n);
util::create_directories(dir.toStdString(), 0755);
fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out));
@@ -35,37 +42,34 @@ LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) {
if (settings.log_livestream) {
logger = std::make_unique<Logger>();
}
stream_thread = new QThread(this);
QObject::connect(&settings, &Settings::changed, this, &LiveStream::startUpdateTimer);
QObject::connect(stream_thread, &QThread::started, [=]() { streamThread(); });
QObject::connect(stream_thread, &QThread::finished, stream_thread, &QThread::deleteLater);
}
LiveStream::~LiveStream() {
stop();
}
void LiveStream::startUpdateTimer() {
update_timer.stop();
update_timer.start(1000.0 / settings.fps, this);
timer_id = update_timer.timerId();
}
void LiveStream::start() {
stream_thread->start();
startUpdateTimer();
begin_date_time = QDateTime::currentDateTime();
begin_date_time = std::chrono::system_clock::now();
fps_ = settings.fps;
exit_ = false;
stream_thread = std::thread(&LiveStream::streamThread, this);
update_thread = std::thread(&LiveStream::updateThread, this);
}
void LiveStream::stop() {
if (!stream_thread) return;
exit_ = true;
if (stream_thread.joinable()) stream_thread.join();
if (update_thread.joinable()) update_thread.join();
}
update_timer.stop();
stream_thread->requestInterruption();
stream_thread->quit();
stream_thread->wait();
stream_thread = nullptr;
void LiveStream::updateThread() {
while (!exit_) {
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / fps_));
// coalesce: skip the emit if the main thread hasn't processed the previous one yet.
if (!update_pending_.exchange(true)) {
emit privateUpdateLastMsgsSignal();
}
}
}
// called in streamThread
@@ -85,23 +89,22 @@ void LiveStream::handleEvent(kj::ArrayPtr<capnp::word> data) {
}
}
void LiveStream::timerEvent(QTimerEvent *event) {
if (event->timerId() == timer_id) {
{
// merge events received from live stream thread.
std::lock_guard lk(lock);
mergeEvents(received_events_);
uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0;
lastest_event_ts = std::max(lastest_event_ts, last_received_ts);
received_events_.clear();
}
if (!all_events_.empty()) {
begin_event_ts = all_events_.front()->mono_time;
updateEvents();
return;
}
// called on the main thread by the queued privateUpdateLastMsgsSignal connection
void LiveStream::updateLastMessages() {
update_pending_ = false;
fps_ = settings.fps;
{
// merge events received from live stream thread.
std::lock_guard lk(lock);
mergeEvents(received_events_);
uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0;
lastest_event_ts = std::max(lastest_event_ts, last_received_ts);
received_events_.clear();
}
if (!all_events_.empty()) {
begin_event_ts = all_events_.front()->mono_time;
updateEvents();
}
QObject::timerEvent(event);
}
void LiveStream::updateEvents() {
@@ -131,7 +134,7 @@ void LiveStream::updateEvents() {
updateEvent(id, (e->mono_time - begin_event_ts) / 1e9, e->dat, e->size);
current_event_ts = e->mono_time;
}
emit privateUpdateLastMsgsSignal();
AbstractStream::updateLastMessages();
}
void LiveStream::seekTo(double sec) {

View File

@@ -1,11 +1,11 @@
#pragma once
#include <algorithm>
#include <atomic>
#include <memory>
#include <thread>
#include <vector>
#include <QBasicTimer>
#include "tools/cabana/streams/abstractstream.h"
class LiveStream : public AbstractStream {
@@ -16,7 +16,7 @@ public:
virtual ~LiveStream();
void start() override;
void stop();
inline QDateTime beginDateTime() const { return begin_date_time; }
inline std::chrono::system_clock::time_point beginDateTime() const override { return begin_date_time; }
inline uint64_t beginMonoTime() const override { return begin_event_ts; }
double maxSeconds() const override { return std::max(1.0, (lastest_event_ts - begin_event_ts) / 1e9); }
void setSpeed(float speed) override { speed_ = speed; }
@@ -29,19 +29,20 @@ protected:
virtual void streamThread() = 0;
void handleEvent(kj::ArrayPtr<capnp::word> event);
std::atomic<bool> exit_ = false;
private:
void startUpdateTimer();
void timerEvent(QTimerEvent *event) override;
void updateThread();
void updateLastMessages() override;
void updateEvents();
std::mutex lock;
QThread *stream_thread;
std::thread stream_thread, update_thread;
std::atomic<bool> update_pending_ = false;
std::atomic<int> fps_ = 10;
std::vector<const CanEvent *> received_events_;
int timer_id;
QBasicTimer update_timer;
QDateTime begin_date_time;
std::chrono::system_clock::time_point begin_date_time;
uint64_t begin_event_ts = 0;
uint64_t lastest_event_ts = 0;
uint64_t current_event_ts = 0;

View File

@@ -1,11 +1,13 @@
#include "tools/cabana/streams/pandastream.h"
#include <QDebug>
#include <chrono>
#include <cstdio>
#include <thread>
#include <QCheckBox>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QThread>
#include <QTimer>
PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {
@@ -16,34 +18,48 @@ PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(co
bool PandaStream::connect() {
try {
qDebug() << "Connecting to panda " << config.serial;
panda.reset(new Panda(config.serial.toStdString(), 0, true));
fprintf(stderr, "Connecting to panda %s\n", config.serial.c_str());
panda.reset(new Panda(config.serial));
config.bus_config.resize(3);
qDebug() << "Connected";
fprintf(stderr, "Connected\n");
} catch (const std::exception& e) {
return false;
}
panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT);
for (int bus = 0; bus < config.bus_config.size(); bus++) {
panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps);
// CAN-FD
if (panda->hw_type == cereal::PandaState::PandaType::RED_PANDA || panda->hw_type == cereal::PandaState::PandaType::RED_PANDA_V2) {
if (config.bus_config[bus].can_fd) {
panda->set_data_speed_kbps(bus, config.bus_config[bus].data_speed_kbps);
} else {
// Hack to disable can-fd by setting data speed to a low value
panda->set_data_speed_kbps(bus, 10);
}
}
}
return true;
}
void PandaStream::streamThread() {
std::vector<can_frame> raw_can_data;
while (!QThread::currentThread()->isInterruptionRequested()) {
QThread::msleep(1);
while (!exit_) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
if (!panda->connected()) {
qDebug() << "Connection to panda lost. Attempting reconnect.";
fprintf(stderr, "Connection to panda lost. Attempting reconnect.\n");
if (!connect()){
QThread::msleep(1000);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
continue;
}
}
raw_can_data.clear();
if (!panda->can_receive(raw_can_data)) {
qDebug() << "failed to receive";
fprintf(stderr, "failed to receive\n");
continue;
}
@@ -58,6 +74,7 @@ void PandaStream::streamThread() {
handleEvent(capnp::messageToFlatArray(msg));
panda->send_heartbeat(false);
}
}
@@ -66,7 +83,7 @@ void PandaStream::streamThread() {
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(can->routeName())));
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;
@@ -105,16 +122,16 @@ void OpenPandaWidget::buildConfigForm() {
bool has_panda = !serial.isEmpty();
if (has_panda) {
try {
Panda panda(serial.toStdString(), 0, true);
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) {
qDebug() << "failed to open panda" << serial;
fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData());
has_panda = false;
}
}
if (has_panda) {
config.serial = serial;
config.serial = serial.toStdString();
config.bus_config.resize(3);
for (int i = 0; i < config.bus_config.size(); i++) {
QHBoxLayout *bus_layout = new QHBoxLayout;

View File

@@ -19,7 +19,7 @@ struct BusConfig {
};
struct PandaStreamConfig {
QString serial = "";
std::string serial = "";
std::vector<BusConfig> bus_config;
};
@@ -28,8 +28,8 @@ class PandaStream : public LiveStream {
public:
PandaStream(QObject *parent, PandaStreamConfig config_ = {});
~PandaStream() { stop(); }
inline QString routeName() const override {
return QString("Panda: %1").arg(config.serial);
inline std::string routeName() const override {
return "Panda: " + config.serial;
}
protected:

View File

@@ -1,5 +1,7 @@
#include "tools/cabana/streams/replaystream.h"
#include <filesystem>
#include <QLabel>
#include <QFileDialog>
#include <QGridLayout>
@@ -14,10 +16,7 @@ ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) {
unsetenv("ZMQ");
setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1);
// TODO: Remove when OpenpilotPrefix supports ZMQ
#ifndef __APPLE__
op_prefix = std::make_unique<OpenpilotPrefix>();
#endif
QObject::connect(&settings, &Settings::changed, this, [this]() {
if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes);
@@ -46,9 +45,9 @@ void ReplayStream::mergeSegments() {
}
}
bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags, bool auto_source) {
replay.reset(new Replay(route.toStdString(), {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
{}, nullptr, replay_flags, data_dir.toStdString(), auto_source));
bool ReplayStream::loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags, bool auto_source) {
replay.reset(new Replay(route, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
{}, nullptr, replay_flags, data_dir, auto_source));
replay->setSegmentCacheLimit(settings.max_cached_minutes);
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
@@ -68,21 +67,21 @@ bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint
QString message;
if (auth_content.empty()) {
message = "Authentication Required. Please run the following command to authenticate:\n\n"
"python3 tools/lib/auth.py\n\n"
"python3 openpilot/tools/lib/auth.py\n\n"
"This will grant access to routes from your comma account.";
} else {
message = tr("Access Denied. You do not have permission to access route:\n\n%1\n\n"
"This is likely a private route.").arg(route);
"This is likely a private route.").arg(QString::fromStdString(route));
}
QMessageBox::warning(nullptr, tr("Access Denied"), message);
} else if (replay->lastRouteError() == RouteLoadError::NetworkError) {
QMessageBox::warning(nullptr, tr("Network Error"),
tr("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(route));
tr("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route)));
} else if (replay->lastRouteError() == RouteLoadError::FileNotFound) {
QMessageBox::warning(nullptr, tr("Route Not Found"),
tr("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(route));
tr("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route)));
} else {
QMessageBox::warning(nullptr, tr("Route Load Failed"), tr("Failed to load route: '%1'").arg(route));
QMessageBox::warning(nullptr, tr("Route Load Failed"), tr("Failed to load route: '%1'").arg(QString::fromStdString(route)));
}
}
return success;
@@ -136,10 +135,10 @@ OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(p
setMinimumWidth(550);
QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() {
QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir);
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 = QFileInfo(dir).absolutePath();
settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string();
}
});
QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() {
@@ -168,7 +167,7 @@ AbstractStream *OpenReplayWidget::open() {
if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_ECAM;
if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC;
if (replay_stream->loadRoute(route, data_dir, flags)) {
if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) {
return replay_stream.release();
}
}

View File

@@ -18,15 +18,17 @@ class ReplayStream : public AbstractStream {
public:
ReplayStream(QObject *parent);
void start() override { replay->start(); }
bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false);
bool loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false);
bool eventFilter(const Event *event);
void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }
bool liveStreaming() const override { return false; }
inline QString routeName() const override { return QString::fromStdString(replay->route().name()); }
inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); }
inline std::string routeName() const override { return replay->route().name(); }
inline std::string carFingerprint() const override { return replay->carFingerprint(); }
double minSeconds() const override { return replay->minSeconds(); }
double maxSeconds() const { return replay->maxSeconds(); }
inline QDateTime beginDateTime() const { return QDateTime::fromSecsSinceEpoch(replay->routeDateTime()); }
inline std::chrono::system_clock::time_point beginDateTime() const override {
return std::chrono::system_clock::from_time_t(replay->routeDateTime());
}
inline uint64_t beginMonoTime() const override { return replay->routeStartNanos(); }
inline void setSpeed(float speed) override { replay->setSpeed(speed); }
inline float getSpeed() const { return replay->getSpeed(); }

View File

@@ -1,27 +1,77 @@
#include "tools/cabana/streams/routes.h"
#include <QDateTime>
#include <chrono>
#include <ctime>
#include <string>
#include <thread>
#include <utility>
#include <QApplication>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QListWidget>
#include <QMessageBox>
#include <QPainter>
class OneShotHttpRequest : public HttpRequest {
public:
OneShotHttpRequest(QObject *parent) : HttpRequest(parent, false) {}
void send(const QString &url) {
if (reply) {
reply->disconnect();
reply->abort();
reply->deleteLater();
reply = nullptr;
}
sendRequest(url);
#include "third_party/json11/json11.hpp"
// IQ.Pilot patch: iqpilot's tools/replay has no py_downloader — device and route
// listing come from the konn3kt API over libcurl. CommaApi2 returns the same JSON
// shapes and the same {"error": ...} envelope upstream's PyDownloader produces, so
// only the call sites change.
#include "tools/replay/api.h"
namespace {
// Parse a konn3kt API JSON response into (success, error_code).
std::pair<bool, int> checkApiResponse(const std::string &result) {
if (result.empty()) return {false, 500};
std::string err;
auto doc = json11::Json::parse(result, err);
if (!err.empty()) return {false, 500};
if (doc.is_object() && doc["error"].is_string()) {
return {false, doc["error"].string_value() == "unauthorized" ? 401 : 500};
}
};
return {true, 0};
}
int64_t nowUnixMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
}
// Parse ISO-8601 (with optional fractional seconds / Z) to unix ms. Returns 0 on failure.
int64_t parseIsoToUnixMs(const std::string &s) {
std::string bytes = s;
if (!bytes.empty() && (bytes.back() == 'Z' || bytes.back() == 'z')) bytes.pop_back();
int millis = 0;
auto dot = bytes.find('.');
if (dot != std::string::npos) {
std::string frac = bytes.substr(dot + 1);
bytes = bytes.substr(0, dot);
while (frac.size() < 3) frac.push_back('0');
millis = std::atoi(frac.substr(0, 3).c_str());
}
std::tm tm{};
const char *ret = strptime(bytes.c_str(), "%Y-%m-%dT%H:%M:%S", &tm);
if (!ret) ret = strptime(bytes.c_str(), "%Y-%m-%d %H:%M:%S", &tm);
if (!ret) return 0;
tm.tm_isdst = -1;
time_t secs = timegm(&tm);
if (secs == static_cast<time_t>(-1)) return 0;
return static_cast<int64_t>(secs) * 1000 + millis;
}
QString formatUnixMs(int64_t ms) {
time_t secs = static_cast<time_t>(ms / 1000);
std::tm tm{};
localtime_r(&secs, &tm);
char buf[64];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
return QString::fromUtf8(buf);
}
} // namespace
// The RouteListWidget class extends QListWidget to display a custom message when empty
class RouteListWidget : public QListWidget {
@@ -41,7 +91,7 @@ public:
QString empty_text_ = tr("No items");
};
RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent), route_requester_(new OneShotHttpRequest(this)) {
RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) {
setWindowTitle(tr("Remote routes"));
QFormLayout *layout = new QFormLayout(this);
@@ -52,41 +102,42 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent), route_requester_(
layout->addRow(button_box);
device_list_->addItem(tr("Loading..."));
// Populate period selector with predefined durations
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 signals and slots
QObject::connect(route_requester_, &HttpRequest::requestDone, this, &RoutesDialog::parseRouteList);
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);
QObject::connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
QObject::connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
// Send request to fetch devices
HttpRequest *http = new HttpRequest(this, false);
QObject::connect(http, &HttpRequest::requestDone, this, &RoutesDialog::parseDeviceList);
http->sendRequest(CommaApi::BASE_URL + "/v1/me/devices/");
// Fetch devices
std::thread([this, alive = std::weak_ptr<bool>(alive_)]() {
std::string result = CommaApi2::getDevices();
QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() {
if (!alive.expired()) parseDeviceList(r, response.first, response.second);
}, Qt::QueuedConnection);
}).detach();
}
void RoutesDialog::parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err) {
void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) {
if (success) {
device_list_->clear();
auto devices = QJsonDocument::fromJson(json.toUtf8()).array();
for (const QJsonValue &device : devices) {
QString dongle_id = device["dongle_id"].toString();
device_list_->addItem(dongle_id, dongle_id);
std::string err;
auto doc = json11::Json::parse(json.toStdString(), err);
if (err.empty() && doc.is_array()) {
for (const auto &device : doc.array_items()) {
QString dongle_id = QString::fromStdString(device["dongle_id"].string_value());
device_list_->addItem(dongle_id, dongle_id);
}
}
} else {
bool unauthorized = (err == QNetworkReply::ContentAccessDenied || err == QNetworkReply::AuthenticationRequiredError);
QMessageBox::warning(this, tr("Error"), unauthorized ? tr("Unauthorized, Authenticate with tools/lib/auth.py") : tr("Network error"));
QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with tools/lib/auth.py") : tr("Network error"));
reject();
}
sender()->deleteLater();
}
void RoutesDialog::fetchRoutes() {
@@ -95,34 +146,45 @@ void RoutesDialog::fetchRoutes() {
route_list_->clear();
route_list_->setEmptyText(tr("Loading..."));
// Construct URL with selected device and date range
QString url = QString("%1/v1/devices/%2").arg(CommaApi::BASE_URL, device_list_->currentText());
std::string did = device_list_->currentText().toStdString();
int period = period_selector_->currentData().toInt();
if (period == -1) {
url += "/routes/preserved";
} else {
QDateTime now = QDateTime::currentDateTime();
url += QString("/routes_segments?start=%1&end=%2")
.arg(now.addDays(-period).toMSecsSinceEpoch())
.arg(now.toMSecsSinceEpoch());
bool preserved = (period == -1);
int64_t start_ms = 0, end_ms = 0;
if (!preserved) {
end_ms = nowUnixMs();
start_ms = end_ms - static_cast<int64_t>(period) * 24LL * 60LL * 60LL * 1000LL;
}
route_requester_->send(url);
int request_id = ++fetch_id_;
std::thread([this, alive = std::weak_ptr<bool>(alive_), did, start_ms, end_ms, preserved, request_id]() {
std::string result = CommaApi2::getDeviceRoutes(did, start_ms, end_ms, preserved);
QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() {
if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second);
}, Qt::QueuedConnection);
}).detach();
}
void RoutesDialog::parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err) {
void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) {
if (success) {
for (const QJsonValue &route : QJsonDocument::fromJson(json.toUtf8()).array()) {
QDateTime from, to;
if (period_selector_->currentData().toInt() == -1) {
from = QDateTime::fromString(route["start_time"].toString(), Qt::ISODateWithMs);
to = QDateTime::fromString(route["end_time"].toString(), Qt::ISODateWithMs);
} else {
from = QDateTime::fromMSecsSinceEpoch(route["start_time_utc_millis"].toDouble());
to = QDateTime::fromMSecsSinceEpoch(route["end_time_utc_millis"].toDouble());
std::string err;
auto doc = json11::Json::parse(json.toStdString(), err);
if (err.empty() && doc.is_array()) {
for (const auto &route : doc.array_items()) {
int64_t from_ms = 0, to_ms = 0;
if (period_selector_->currentData().toInt() == -1) {
from_ms = parseIsoToUnixMs(route["start_time"].string_value());
to_ms = parseIsoToUnixMs(route["end_time"].string_value());
} else {
from_ms = static_cast<int64_t>(route["start_time_utc_millis"].number_value());
to_ms = static_cast<int64_t>(route["end_time_utc_millis"].number_value());
}
const int mins = static_cast<int>((to_ms - from_ms) / 60000);
auto item = new QListWidgetItem(QString("%1 %2min").arg(formatUnixMs(from_ms)).arg(mins));
item->setData(Qt::UserRole, QString::fromStdString(route["fullname"].string_value()));
route_list_->addItem(item);
}
auto item = new QListWidgetItem(QString("%1 %2min").arg(from.toString()).arg(from.secsTo(to) / 60));
item->setData(Qt::UserRole, route["fullname"].toString());
route_list_->addItem(item);
}
if (route_list_->count() > 0) route_list_->setCurrentRow(0);
} else {

View File

@@ -1,11 +1,12 @@
#pragma once
#include <atomic>
#include <memory>
#include <QComboBox>
#include <QDialog>
#include "tools/cabana/utils/api.h"
class RouteListWidget;
class OneShotHttpRequest;
class RoutesDialog : public QDialog {
Q_OBJECT
@@ -14,12 +15,14 @@ public:
QString route();
protected:
void parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err);
void parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err);
void parseDeviceList(const QString &json, bool success, int error_code);
void parseRouteList(const QString &json, bool success, int error_code);
void fetchRoutes();
QComboBox *device_list_;
QComboBox *period_selector_;
RouteListWidget *route_list_;
OneShotHttpRequest *route_requester_;
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);
};

View File

@@ -1,67 +1,99 @@
#include "tools/cabana/streams/socketcanstream.h"
#include <QDebug>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPushButton>
#include <QThread>
SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) {
if (!available()) {
throw std::runtime_error("SocketCAN plugin not available");
throw std::runtime_error("SocketCAN not available");
}
qDebug() << "Connecting to SocketCAN device" << config.device;
fprintf(stderr, "Connecting to SocketCAN device %s\n", config.device.c_str());
if (!connect()) {
throw std::runtime_error("Failed to connect to SocketCAN device");
}
}
SocketCanStream::~SocketCanStream() {
stop();
if (sock_fd >= 0) {
::close(sock_fd);
sock_fd = -1;
}
}
bool SocketCanStream::available() {
return QCanBus::instance()->plugins().contains("socketcan");
int fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (fd < 0) return false;
::close(fd);
return true;
}
bool SocketCanStream::connect() {
// Connecting might generate some warnings about missing socketcan/libsocketcan libraries
// These are expected and can be ignored, we don't need the advanced features of libsocketcan
QString errorString;
device.reset(QCanBus::instance()->createDevice("socketcan", config.device, &errorString));
device->setConfigurationParameter(QCanBusDevice::CanFdKey, true);
if (!device) {
qDebug() << "Failed to create SocketCAN device" << errorString;
sock_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (sock_fd < 0) {
fprintf(stderr, "Failed to create CAN socket\n");
return false;
}
if (!device->connectDevice()) {
qDebug() << "Failed to connect to device";
// Enable CAN-FD
int fd_enable = 1;
setsockopt(sock_fd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &fd_enable, sizeof(fd_enable));
struct ifreq ifr = {};
strncpy(ifr.ifr_name, config.device.c_str(), IFNAMSIZ - 1);
if (ioctl(sock_fd, SIOCGIFINDEX, &ifr) < 0) {
fprintf(stderr, "Failed to get interface index for %s\n", config.device.c_str());
::close(sock_fd);
sock_fd = -1;
return false;
}
struct sockaddr_can addr = {};
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
fprintf(stderr, "Failed to bind CAN socket\n");
::close(sock_fd);
sock_fd = -1;
return false;
}
// Set read timeout so the thread can check for interruption
struct timeval tv = {.tv_sec = 0, .tv_usec = 100000}; // 100ms
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
return true;
}
void SocketCanStream::streamThread() {
while (!QThread::currentThread()->isInterruptionRequested()) {
QThread::msleep(1);
struct canfd_frame frame;
auto frames = device->readAllFrames();
if (frames.size() == 0) continue;
while (!exit_) {
ssize_t nbytes = read(sock_fd, &frame, sizeof(frame));
if (nbytes <= 0) continue;
uint8_t len = (nbytes == CAN_MTU) ? frame.len : frame.len; // works for both CAN and CAN-FD
MessageBuilder msg;
auto evt = msg.initEvent();
auto canData = evt.initCan(frames.size());
for (uint i = 0; i < frames.size(); i++) {
if (!frames[i].isValid()) continue;
canData[i].setAddress(frames[i].frameId());
canData[i].setSrc(0);
auto payload = frames[i].payload();
canData[i].setDat(kj::arrayPtr((uint8_t*)payload.data(), payload.size()));
}
auto canData = evt.initCan(1);
canData[0].setAddress(frame.can_id & CAN_EFF_MASK);
canData[0].setSrc(0);
canData[0].setDat(kj::arrayPtr(frame.data, len));
handleEvent(capnp::messageToFlatArray(msg));
}
@@ -87,7 +119,7 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi
main_layout->addStretch(1);
QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices);
QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText(); });
QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); });
// Populate devices
refreshDevices();
@@ -95,12 +127,17 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi
void OpenSocketCanWidget::refreshDevices() {
device_edit->clear();
for (auto device : QCanBus::instance()->availableDevices(QStringLiteral("socketcan"))) {
device_edit->addItem(device.name());
// 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(qApp, config);

View File

@@ -1,27 +1,22 @@
#pragma once
#include <memory>
#include <QtSerialBus/QCanBus>
#include <QtSerialBus/QCanBusDevice>
#include <QtSerialBus/QCanBusDeviceInfo>
#include <QComboBox>
#include "tools/cabana/streams/livestream.h"
struct SocketCanStreamConfig {
QString device = ""; // TODO: support multiple devices/buses at once
std::string device = ""; // TODO: support multiple devices/buses at once
};
class SocketCanStream : public LiveStream {
Q_OBJECT
public:
SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {});
~SocketCanStream() { stop(); }
~SocketCanStream();
static bool available();
inline QString routeName() const override {
return QString("Live Streaming From Socket CAN %1").arg(config.device);
inline std::string routeName() const override {
return "Live Streaming From Socket CAN " + config.device;
}
protected:
@@ -29,7 +24,7 @@ protected:
bool connect();
SocketCanStreamConfig config = {};
std::unique_ptr<QCanBusDevice> device;
int sock_fd = -1;
};
class OpenSocketCanWidget : public AbstractOpenStreamWidget {

View File

@@ -1,14 +1,17 @@
#include "tools/cabana/streamselector.h"
#include <filesystem>
#include <QFileDialog>
#include <QLabel>
#include <QPushButton>
#include "streams/socketcanstream.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
StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) {
setWindowTitle(tr("Open stream"));
@@ -35,9 +38,11 @@ StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) {
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);
@@ -49,10 +54,10 @@ StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) {
setEnabled(true);
});
QObject::connect(file_btn, &QPushButton::clicked, [this]() {
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)");
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 = QFileInfo(fn).absolutePath();
settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string();
}
});
}

View File

@@ -0,0 +1,103 @@
import json
import subprocess
from pathlib import Path
import pytest
CABANA_DIR = Path(__file__).parent
CABANA_BIN = CABANA_DIR / "_cabana"
pytestmark = pytest.mark.skipif(not CABANA_BIN.exists(),
reason="cabana not built (scons -u tools/cabana/_cabana)")
def read(name):
return (CABANA_DIR / name).read_text()
class TestCabanaBinary:
def test_help(self):
result = subprocess.run([str(CABANA_BIN), "--help"], capture_output=True, text=True, timeout=60)
assert result.returncode == 0, result.stderr
assert "Usage:" in result.stderr
def test_help_documents_both_live_can_modes(self):
# --zmq attaches directly (konn3kt_canproxy); --bridge is upstream's local-bridge path
out = subprocess.run([str(CABANA_BIN), "--help"], capture_output=True, text=True, timeout=60).stderr
assert "--zmq" in out
assert "--bridge" in out
assert "konn3kt_canproxy.py" in out
def test_launcher_builds_the_right_targets(self):
launcher = read("cabana")
# iqpilot is not nested under openpilot/
assert "scons -u tools/cabana/_cabana cereal/messaging/bridge" in launcher
assert "openpilot/tools/cabana" not in launcher
class TestKonn3ktIntegration:
def test_routes_dialog_uses_konn3kt_api(self):
# upstream shells into tools/lib via PyDownloader; iqpilot goes through CommaApi2
routes = read("streams/routes.cc")
assert "PyDownloader::" not in routes
assert "py_downloader.h" not in routes
assert "CommaApi2::getDevices()" in routes
assert "CommaApi2::getDeviceRoutes(" in routes
def test_no_duplicate_qt_api_client(self):
# the old Qt HttpRequest/JWT client was folded into tools/replay/api.cc
assert not (CABANA_DIR / "utils" / "api.cc").exists()
assert not (CABANA_DIR / "utils" / "api.h").exists()
def test_no_comma_endpoints(self):
for name in ("streams/routes.cc", "cabana.cc", "README.md"):
body = read(name)
assert "connect.comma.ai" not in body, name
assert "api.comma.ai" not in body, name
def test_dbc_menu_points_at_iqdbc(self):
mainwin = read("mainwin.cc")
assert "commaai/iqdbc" in mainwin
assert "commaai/opendbc" not in mainwin
def test_dbc_json_generator_imports_iqdbc(self):
gen = read("dbc/generate_dbc_json.py")
assert "from iqdbc.car" in gen
assert "from opendbc.car" not in gen
def test_generated_dbc_json_covers_iqdbc_platforms(self):
# built by scons; cabana reads it to auto-select a DBC per car fingerprint
path = CABANA_DIR / "dbc" / "car_fingerprint_to_dbc.json"
assert path.is_file(), "run scons -u tools/cabana"
mapping = json.loads(path.read_text())
assert len(mapping) > 100
assert all(isinstance(v, str) and v for v in mapping.values())
def test_canproxy_targets_konn3kt(self):
proxy = read("konn3kt_canproxy.py")
assert "konn3kt" in proxy
# the proxy publishes on a local ZMQ 'can' socket that --zmq attaches to
assert 'os.environ["ZMQ"] = "1"' in proxy
class TestDeviceStreamModes:
"""The ZMQ attach is what konn3kt_canproxy feeds; upstream #38484 replaced it with a
bridge fork. Both must exist, and only the direct attach may set ZMQ=1."""
def test_three_modes_exist(self):
header = read("streams/devicestream.h")
assert "enum class Mode { Msgq, Zmq, Bridge };" in header
def test_only_zmq_mode_talks_zmq(self):
src = read("streams/devicestream.cc")
assert 'mode_ == Mode::Zmq ? setenv("ZMQ", "1", 1) : unsetenv("ZMQ")' in src
def test_zmq_mode_subscribes_to_the_given_address(self):
# regression guard: upstream hardcodes 127.0.0.1 and relies on the bridge fork,
# which never reads the canproxy publisher
src = read("streams/devicestream.cc")
assert 'const std::string address = mode_ == Mode::Zmq ? address_.toStdString() : "127.0.0.1";' in src
def test_only_bridge_mode_forks_the_bridge(self):
src = read("streams/devicestream.cc")
assert "if (mode_ == Mode::Bridge) {" in src

View File

@@ -1,14 +1,15 @@
#undef INFO
#include <QDir>
#include <filesystem>
#include <sstream>
#include "catch2/catch.hpp"
#include "common/tests/native_test.h"
#include "tools/cabana/dbc/dbcfile.h"
#include "tools/cabana/dbc/dbcmanager.h"
const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2";
TEST_CASE("DBCFile::generateDBC") {
QString fn = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, "tesla_can");
void test_generate_dbc() {
std::string fn = std::string(OPENDBC_FILE_PATH) + "/tesla_can.dbc";
DBCFile dbc_origin(fn);
DBCFile dbc_from_generated("", dbc_origin.generateDBC());
@@ -28,9 +29,9 @@ TEST_CASE("DBCFile::generateDBC") {
}
}
TEST_CASE("DBCFile::generateDBC - comment order") {
void test_comment_order() {
// Ensure that message comments are followed by signal comments and in the correct order
auto content = R"(BO_ 160 message_1: 8 EON
std::string content = R"(BO_ 160 message_1: 8 EON
SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX
BO_ 162 message_2: 8 EON
@@ -45,8 +46,8 @@ CM_ SG_ 162 signal_2 "signal comment";
REQUIRE(dbc.generateDBC() == content);
}
TEST_CASE("DBCFile::generateDBC -- preserve original header") {
QString content = R"(VERSION "1.0"
void test_preserve_original_header() {
std::string content = R"(VERSION "1.0"
NS_ :
CM_
@@ -65,8 +66,8 @@ CM_ SG_ 160 signal_1 "signal comment";
REQUIRE(dbc.generateDBC() == content);
}
TEST_CASE("DBCFile::generateDBC - escaped quotes") {
QString content = R"(BO_ 160 message_1: 8 EON
void test_escaped_quotes() {
std::string content = R"(BO_ 160 message_1: 8 EON
SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX
CM_ BO_ 160 "message comment with \"escaped quotes\"";
@@ -76,8 +77,8 @@ CM_ SG_ 160 signal_1 "signal comment with \"escaped quotes\"";
REQUIRE(dbc.generateDBC() == content);
}
TEST_CASE("parse_dbc") {
QString content = R"(
void test_parse_dbc() {
std::string content = R"(
BO_ 160 message_1: 8 EON
SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX
SG_ signal_2 : 12|1@1+ (1.0,0.0) [0.0|1] "" XXX
@@ -119,9 +120,9 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\"";
REQUIRE(sig_1->comment == "signal comment");
REQUIRE(sig_1->receiver_name == "XXX");
REQUIRE(sig_1->val_desc.size() == 3);
REQUIRE(sig_1->val_desc[0] == std::pair<double, QString>{0, "disabled"});
REQUIRE(sig_1->val_desc[1] == std::pair<double, QString>{1.2, "initializing"});
REQUIRE(sig_1->val_desc[2] == std::pair<double, QString>{2, "fault"});
REQUIRE(sig_1->val_desc[0] == std::pair<double, std::string>{0, "disabled"});
REQUIRE(sig_1->val_desc[1] == std::pair<double, std::string>{1.2, "initializing"});
REQUIRE(sig_1->val_desc[2] == std::pair<double, std::string>{2, "fault"});
auto &sig_2 = msg->sigs[1];
REQUIRE(sig_2->comment == "multiple line comment \n1\n2");
@@ -142,16 +143,66 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\"";
REQUIRE(msg->sigs[0]->comment == "signal comment with \"escaped quotes\"");
}
TEST_CASE("parse_iqdbc") {
QDir dir(OPENDBC_FILE_PATH);
QStringList errors;
for (auto fn : dir.entryList({"*.dbc"}, QDir::Files, QDir::Name)) {
// OPENDBC_FILE_PATH points at iqdbc/dbc here — this is the load-bearing check that
// cabana can still parse every DBC iqpilot ships.
void test_parse_iqdbc() {
std::vector<std::string> errors;
int parsed = 0;
for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH)) {
if (!entry.is_regular_file() || entry.path().extension() != ".dbc") continue;
try {
auto dbc = DBCFile(dir.filePath(fn));
auto dbc = DBCFile(entry.path().string());
++parsed;
} catch (std::exception &e) {
errors.push_back(e.what());
}
}
INFO(errors.join("\n").toStdString());
// guard against OPENDBC_FILE_PATH pointing somewhere empty, which would make the
// loop above pass vacuously
REQUIRE(parsed > 100);
std::ostringstream details;
for (const auto &error : errors) details << error << '\n';
if (!errors.empty()) std::cerr << details.str();
REQUIRE(errors.empty());
}
void test_dbc_manager() {
DBCManager manager;
int files_changed = 0;
int signals_added = 0;
int masks_updated = 0;
manager.setCallbacks({
.signal_added = [&](MessageId, const cabana::Signal *) { ++signals_added; },
.file_changed = [&]() { ++files_changed; },
.mask_updated = [&]() { ++masks_updated; },
});
std::string error;
REQUIRE(manager.open(SOURCE_ALL, "test", "BO_ 160 message: 8 XXX\n", &error));
REQUIRE(error.empty());
REQUIRE(files_changed == 1);
cabana::Signal signal{};
signal.name = "speed";
signal.start_bit = 0;
signal.size = 8;
signal.is_little_endian = true;
manager.addSignal({.source = 0, .address = 160}, signal);
REQUIRE(signals_added == 1);
REQUIRE(masks_updated == 1);
REQUIRE(manager.msg({.source = 0, .address = 160})->sig("speed") != nullptr);
}
void test_cabana_core() {
test_generate_dbc();
test_comment_order();
test_preserve_original_header();
test_escaped_quotes();
test_parse_dbc();
test_parse_iqdbc();
test_dbc_manager();
}
int main() {
return run_native_test(test_cabana_core);
}

View File

@@ -1,10 +0,0 @@
#define CATCH_CONFIG_RUNNER
#include "catch2/catch.hpp"
#include <QCoreApplication>
int main(int argc, char **argv) {
// unit tests for Qt
QCoreApplication app(argc, argv);
const int res = Catch::Session().run(argc, argv);
return (res < 0xff ? res : 0xff);
}

View File

@@ -1,10 +1,12 @@
#include "tools/cabana/tools/findsignal.h"
#include <set>
#include <thread>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMenu>
#include <QtConcurrent>
#include <QTimer>
#include <QVBoxLayout>
@@ -20,7 +22,7 @@ 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 s.id.toString();
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(" ");
}
@@ -32,36 +34,49 @@ void FindSignalModel::search(std::function<bool(double)> cmp) {
beginResetModel();
std::mutex lock;
const auto prev_sigs = !histories.isEmpty() ? histories.back() : initial_signals;
const auto prev_sigs = !histories.empty() ? histories.back() : initial_signals;
filtered_signals.clear();
filtered_signals.reserve(prev_sigs.size());
QtConcurrent::blockingMap(prev_sigs, [&](auto &s) {
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});
}
});
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.isEmpty()) {
if (!histories.empty()) {
beginResetModel();
histories.pop_back();
filtered_signals.clear();
if (!histories.isEmpty()) filtered_signals = histories.back();
if (!histories.empty()) filtered_signals = histories.back();
endResetModel();
}
}
@@ -84,9 +99,9 @@ FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags(
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-seperated values. Leave blank for all"));
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-seperated hex values. Leave blank for all"));
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("-"));
@@ -172,7 +187,7 @@ FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags(
}
void FindSignalDlg::search() {
if (model->histories.isEmpty()) {
if (model->histories.empty()) {
setInitialSignals();
}
auto v1 = value1->text().toDouble();
@@ -196,13 +211,13 @@ void FindSignalDlg::search() {
}
void FindSignalDlg::setInitialSignals() {
QSet<ushort> buses;
std::set<ushort> buses;
for (auto bus : bus_edit->text().trimmed().split(",")) {
bus = bus.trimmed();
if (!bus.isEmpty()) buses.insert(bus.toUShort());
}
QSet<uint32_t> addresses;
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));
@@ -225,7 +240,7 @@ void FindSignalDlg::setInitialSignals() {
model->initial_signals.clear();
for (const auto &[id, m] : can->lastMessages()) {
if ((buses.isEmpty() || buses.contains(id.source)) && (addresses.isEmpty() || addresses.contains(id.address))) {
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()) {
@@ -246,12 +261,12 @@ void FindSignalDlg::setInitialSignals() {
}
void FindSignalDlg::modelReset() {
properties_group->setEnabled(model->histories.isEmpty());
message_group->setEnabled(model->histories.isEmpty());
search_btn->setText(model->histories.isEmpty() ? tr("Find") : tr("Find Next"));
reset_btn->setEnabled(!model->histories.isEmpty());
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.isEmpty());
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()));
}
@@ -262,7 +277,7 @@ void FindSignalDlg::customMenuRequested(const QPoint &pos) {
menu.addAction(tr("Create Signal"));
if (menu.exec(view->mapToGlobal(pos))) {
auto &s = model->filtered_signals[index.row()];
UndoStack::push(new AddSigCommand(s.id, s.sig));
UndoStack::instance()->push(new AddSigCommand(s.id, s.sig));
emit openMessage(s.id);
}
}

View File

@@ -2,6 +2,8 @@
#include <algorithm>
#include <limits>
#include <string>
#include <vector>
#include <QAbstractTableModel>
#include <QCheckBox>
@@ -26,14 +28,14 @@ public:
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(filtered_signals.size(), 300); }
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();
QList<SearchSignal> filtered_signals;
QList<SearchSignal> initial_signals;
QList<QList<SearchSignal>> histories;
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();
};

View File

@@ -1,6 +1,7 @@
#include "tools/cabana/tools/findsimilarbits.h"
#include <algorithm>
#include <unordered_map>
#include <QGridLayout>
#include <QHeaderView>
@@ -31,7 +32,7 @@ FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::Wi
msg_cb = new QComboBox(this);
// TODO: update when src_bus_combo changes
for (auto &[address, msg] : dbc()->getMessages(-1)) {
msg_cb->addItem(msg.name, address);
msg_cb->addItem(QString::fromStdString(msg.name), address);
}
msg_cb->model()->sort(0);
msg_cb->setCurrentIndex(0);
@@ -114,10 +115,10 @@ void FindSimilarBitsDlg::find() {
search_btn->setEnabled(true);
}
QList<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) {
QHash<uint32_t, QVector<uint32_t>> mismatches;
QHash<uint32_t, uint32_t> msg_count;
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) {
@@ -143,14 +144,14 @@ QList<FindSimilarBitsDlg::mismatched_struct> FindSimilarBitsDlg::calcBits(uint8_
}
}
QList<mismatched_struct> result;
std::vector<mismatched_struct> result;
result.reserve(mismatches.size());
for (auto it = mismatches.begin(); it != mismatches.end(); ++it) {
if (auto cnt = msg_count[it.key()]; cnt > min_msgs_cnt) {
auto &mismatched = it.value();
for (int i = 0; i < mismatched.size(); ++i) {
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.key(), (uint32_t)i / 8, (uint32_t)i % 8, mismatched[i], cnt, perc});
result.push_back({it->first, (uint32_t)i / 8, (uint32_t)i % 8, mismatched[i], cnt, perc});
}
}
}

View File

@@ -1,5 +1,7 @@
#pragma once
#include <vector>
#include <QComboBox>
#include <QDialog>
#include <QLineEdit>
@@ -22,7 +24,7 @@ private:
uint32_t address, byte_idx, bit_idx, mismatches, total;
float perc;
};
QList<mismatched_struct> calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, uint8_t find_bus,
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();

View File

@@ -1,171 +0,0 @@
#include "tools/cabana/utils/api.h"
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <QApplication>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDebug>
#include <QJsonDocument>
#include <QNetworkRequest>
#include <memory>
#include <string>
#include "common/params.h"
#include "common/util.h"
#include "system/hardware/hw.h"
#include "tools/cabana/utils/util.h"
QString getVersion() {
static QString version = QString::fromStdString(Params().get("Version"));
return version;
}
QString getUserAgent() {
return "openpilot-" + getVersion();
}
std::optional<QString> getDongleId() {
std::string id = Params().get("DongleId");
if (!id.empty() && (id != "UnregisteredDevice")) {
return QString::fromStdString(id);
} else {
return {};
}
}
namespace CommaApi {
EVP_PKEY *get_private_key() {
static std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> pkey(nullptr, EVP_PKEY_free);
if (!pkey) {
FILE *fp = fopen(Path::rsa_file().c_str(), "rb");
if (!fp) {
qDebug() << "No private key found, please run manager.py or registration.py";
return nullptr;
}
pkey.reset(PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr));
fclose(fp);
}
return pkey.get();
}
QByteArray rsa_sign(const QByteArray &data) {
EVP_PKEY *pkey = get_private_key();
if (!pkey) return {};
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
if (!mdctx) return {};
QByteArray sig(EVP_PKEY_size(pkey), Qt::Uninitialized);
size_t sig_len = sig.size();
int ret = EVP_DigestSignInit(mdctx, nullptr, EVP_sha256(), nullptr, pkey);
ret &= EVP_DigestSignUpdate(mdctx, data.data(), data.size());
ret &= EVP_DigestSignFinal(mdctx, (unsigned char*)sig.data(), &sig_len);
EVP_MD_CTX_free(mdctx);
if (ret != 1) return {};
sig.resize(sig_len);
return sig;
}
QString create_jwt(const QJsonObject &payloads, int expiry) {
QJsonObject header = {{"alg", "RS256"}};
auto t = QDateTime::currentSecsSinceEpoch();
QJsonObject payload = {{"identity", getDongleId().value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}};
for (auto it = payloads.begin(); it != payloads.end(); ++it) {
payload.insert(it.key(), it.value());
}
auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals;
QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' +
QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts);
auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256);
return jwt + "." + rsa_sign(hash).toBase64(b64_opts);
}
} // namespace CommaApi
HttpRequest::HttpRequest(QObject *parent, bool create_jwt, int timeout) : create_jwt(create_jwt), QObject(parent) {
networkTimer = new QTimer(this);
networkTimer->setSingleShot(true);
networkTimer->setInterval(timeout);
connect(networkTimer, &QTimer::timeout, this, &HttpRequest::requestTimeout);
}
bool HttpRequest::active() const {
return reply != nullptr;
}
bool HttpRequest::timeout() const {
return reply && reply->error() == QNetworkReply::OperationCanceledError;
}
void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) {
if (active()) {
qDebug() << "HttpRequest is active";
return;
}
QString token;
if (create_jwt) {
token = CommaApi::create_jwt();
} else {
QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json"));
QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8());
token = json_d["access_token"].toString();
}
QNetworkRequest request;
request.setUrl(QUrl(requestURL));
request.setRawHeader("User-Agent", getUserAgent().toUtf8());
if (!token.isEmpty()) {
request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8());
}
if (method == HttpRequest::Method::GET) {
reply = nam()->get(request);
} else if (method == HttpRequest::Method::DELETE) {
reply = nam()->deleteResource(request);
}
networkTimer->start();
connect(reply, &QNetworkReply::finished, this, &HttpRequest::requestFinished);
}
void HttpRequest::requestTimeout() {
reply->abort();
}
void HttpRequest::requestFinished() {
networkTimer->stop();
if (reply->error() == QNetworkReply::NoError) {
emit requestDone(reply->readAll(), true, reply->error());
} else {
QString error;
if (reply->error() == QNetworkReply::OperationCanceledError) {
nam()->clearAccessCache();
nam()->clearConnectionCache();
error = "Request timed out";
} else {
error = reply->errorString();
}
emit requestDone(error, false, reply->error());
}
reply->deleteLater();
reply = nullptr;
}
QNetworkAccessManager *HttpRequest::nam() {
static QNetworkAccessManager *networkAccessManager = new QNetworkAccessManager(qApp);
return networkAccessManager;
}

View File

@@ -1,47 +0,0 @@
#pragma once
#include <QJsonObject>
#include <QNetworkReply>
#include <QString>
#include <QTimer>
#include "common/util.h"
namespace CommaApi {
const QString BASE_URL = util::getenv("API_HOST", "https://api-iqlabs.konn3kt.com").c_str();
QByteArray rsa_sign(const QByteArray &data);
QString create_jwt(const QJsonObject &payloads = {}, int expiry = 3600);
} // namespace CommaApi
/**
* Makes a request to the request endpoint.
*/
class HttpRequest : public QObject {
Q_OBJECT
public:
enum class Method {GET, DELETE};
explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000);
void sendRequest(const QString &requestURL, const Method method = Method::GET);
bool active() const;
bool timeout() const;
signals:
void requestDone(const QString &response, bool success, QNetworkReply::NetworkError error);
protected:
QNetworkReply *reply = nullptr;
private:
static QNetworkAccessManager *nam();
QTimer *networkTimer = nullptr;
bool create_jwt;
private slots:
void requestTimeout();
void requestFinished();
};

View File

@@ -1,41 +1,41 @@
#include "tools/cabana/utils/export.h"
#include <QFile>
#include <QTextStream>
#include <fstream>
#include <iomanip>
#include "tools/cabana/streams/abstractstream.h"
namespace utils {
void exportToCSV(const QString &file_name, std::optional<MessageId> msg_id) {
QFile file(file_name);
if (file.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
QTextStream stream(&file);
void exportToCSV(const std::string &file_name, std::optional<MessageId> msg_id) {
std::ofstream stream(file_name, std::ios::trunc);
if (stream) {
stream << "time,addr,bus,data\n";
for (auto e : msg_id ? can->events(*msg_id) : can->allEvents()) {
stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << ","
<< "0x" << QString::number(e->address, 16) << "," << e->src << ","
<< "0x" << QByteArray::fromRawData((const char *)e->dat, e->size).toHex().toUpper() << "\n";
stream << std::fixed << std::setprecision(3) << can->toSeconds(e->mono_time) << ","
<< "0x" << std::hex << e->address << std::dec << "," << static_cast<int>(e->src) << ",0x"
<< std::uppercase << std::hex << std::setfill('0');
for (int i = 0; i < e->size; ++i) stream << std::setw(2) << static_cast<int>(e->dat[i]);
stream << std::nouppercase << std::dec << "\n";
}
}
}
void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id) {
QFile file(file_name);
if (auto msg = dbc()->msg(msg_id); msg && msg->sigs.size() && file.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
QTextStream stream(&file);
void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id) {
std::ofstream stream(file_name, std::ios::trunc);
if (auto msg = dbc()->msg(msg_id); msg && !msg->sigs.empty() && stream) {
stream << "time,addr,bus";
for (auto s : msg->sigs)
stream << "," << s->name;
stream << "," << s->name.c_str();
stream << "\n";
for (auto e : can->events(msg_id)) {
stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << ","
<< "0x" << QString::number(e->address, 16) << "," << e->src;
stream << std::fixed << std::setprecision(3) << can->toSeconds(e->mono_time) << ","
<< "0x" << std::hex << e->address << std::dec << "," << static_cast<int>(e->src);
for (auto s : msg->sigs) {
double value = 0;
s->getValue(e->dat, e->size, &value);
stream << "," << QString::number(value, 'f', s->precision);
stream << "," << std::fixed << std::setprecision(s->precision) << value;
}
stream << "\n";
}

View File

@@ -1,10 +1,11 @@
#pragma once
#include <optional>
#include <string>
#include "tools/cabana/dbc/dbcmanager.h"
namespace utils {
void exportToCSV(const QString &file_name, std::optional<MessageId> msg_id = std::nullopt);
void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id);
void exportToCSV(const std::string &file_name, std::optional<MessageId> msg_id = std::nullopt);
void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id);
} // namespace utils

View File

@@ -1,24 +1,26 @@
#include "tools/cabana/utils/util.h"
#include <algorithm>
#include <cerrno>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <csignal>
#include <ctime>
#include <filesystem>
#include <limits>
#include <memory>
#include <string>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <QColor>
#include <QDateTime>
#include <QDir>
#include <QFontDatabase>
#include <QLocale>
#include <QPixmapCache>
#include <QSurfaceFormat>
#include <QFileInfo>
#include <QPainterPath>
#include <QTextStream>
#include <QtXml/QDomDocument>
#include <unordered_map>
#include "common/util.h"
// SegmentTree
@@ -101,7 +103,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
// Paint hex column
const auto &bytes = *static_cast<std::vector<uint8_t> *>(data.value<void *>());
const auto &colors = *static_cast<std::vector<QColor> *>(index.data(ColorsRole).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);
@@ -116,7 +118,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
painter->setPen(option.palette.color(QPalette::Text));
painter->fillRect(r, option.palette.color(QPalette::Window));
}
painter->fillRect(r, colors[i]);
painter->fillRect(r, toQColor(colors[i]));
} else {
painter->setPen(text_pen);
}
@@ -149,54 +151,211 @@ void TabBar::closeTabClicked() {
// UnixSignalHandler
UnixSignalHandler::UnixSignalHandler(QObject *parent) : QObject(nullptr) {
UnixSignalHandler::UnixSignalHandler() {
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd)) {
qFatal("Couldn't create TERM socketpair");
}
sn = new QSocketNotifier(sig_fd[1], QSocketNotifier::Read, this);
connect(sn, &QSocketNotifier::activated, this, &UnixSignalHandler::handleSigTerm);
waiter = std::thread([this]() {
int tmp = 0;
while (::read(sig_fd[1], &tmp, sizeof(tmp)) < 0) {
if (errno != EINTR) return;
}
if (shutting_down.load()) return;
// Marshal exit onto the GUI thread (qApp methods are not thread-safe).
QMetaObject::invokeMethod(qApp, []() {
printf("\nexiting...\n");
qApp->closeAllWindows();
qApp->exit();
}, Qt::QueuedConnection);
});
std::signal(SIGINT, signalHandler);
std::signal(SIGTERM, UnixSignalHandler::signalHandler);
}
UnixSignalHandler::~UnixSignalHandler() {
shutting_down.store(true);
int dummy = 0;
(void)!::write(sig_fd[0], &dummy, sizeof(dummy));
if (waiter.joinable()) waiter.join();
::close(sig_fd[0]);
::close(sig_fd[1]);
}
void UnixSignalHandler::signalHandler(int s) {
::write(sig_fd[0], &s, sizeof(s));
}
void UnixSignalHandler::handleSigTerm() {
sn->setEnabled(false);
int tmp;
::read(sig_fd[1], &tmp, sizeof(tmp));
printf("\nexiting...\n");
qApp->closeAllWindows();
qApp->exit();
(void)!::write(sig_fd[0], &s, sizeof(s));
}
// NameValidator
NameValidator::NameValidator(QObject *parent) : QRegExpValidator(QRegExp("^(\\w+)"), parent) {}
NameValidator::NameValidator(QObject *parent) : QValidator(parent) {}
QValidator::State NameValidator::validate(QString &input, int &pos) const {
Q_UNUSED(pos);
input.replace(' ', '_');
return QRegExpValidator::validate(input, pos);
if (input.isEmpty()) return QValidator::Intermediate;
for (const QChar &c : input) {
if (!c.isLetterOrNumber() && c != '_') return QValidator::Invalid;
}
return QValidator::Acceptable;
}
DoubleValidator::DoubleValidator(QObject *parent) : QDoubleValidator(parent) {
// Match locale of QString::toDouble() instead of system
QLocale locale(QLocale::C);
locale.setNumberOptions(QLocale::RejectGroupSeparator);
setLocale(locale);
// NodeValidator
NodeValidator::NodeValidator(QObject *parent) : QValidator(parent) {}
QValidator::State NodeValidator::validate(QString &input, int &pos) const {
Q_UNUSED(pos);
if (input.isEmpty()) return QValidator::Intermediate;
// Match ^\w+(,\w+)*$ ; a trailing comma is Intermediate (user still typing).
bool need_word = true;
for (const QChar &c : input) {
if (c.isLetterOrNumber() || c == '_') {
need_word = false;
} else if (c == ',' && !need_word) {
need_word = true;
} else {
return QValidator::Invalid;
}
}
return need_word ? QValidator::Intermediate : QValidator::Acceptable;
}
// NonWhitespaceValidator
NonWhitespaceValidator::NonWhitespaceValidator(QObject *parent) : QValidator(parent) {}
QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const {
Q_UNUSED(pos);
if (input.isEmpty()) return QValidator::Intermediate;
for (const QChar &c : input) {
if (c.isSpace()) return QValidator::Invalid;
}
return QValidator::Acceptable;
}
// IpAddressValidator
IpAddressValidator::IpAddressValidator(QObject *parent) : QValidator(parent) {}
QValidator::State IpAddressValidator::validate(QString &input, int &pos) const {
Q_UNUSED(pos);
if (input.isEmpty()) return QValidator::Intermediate;
int dots = 0;
int value = 0;
bool has_digit = false;
for (const QChar &c : input) {
if (c.isDigit()) {
value = has_digit ? value * 10 + c.digitValue() : c.digitValue();
if (value > 255) return QValidator::Invalid;
has_digit = true;
} else if (c == '.') {
if (!has_digit || dots >= 3) return QValidator::Invalid;
++dots;
has_digit = false;
value = 0;
} else {
return QValidator::Invalid;
}
}
return (dots == 3 && has_digit) ? QValidator::Acceptable : QValidator::Intermediate;
}
DoubleValidator::DoubleValidator(QObject *parent) : QValidator(parent) {}
QValidator::State DoubleValidator::validate(QString &input, int &pos) const {
Q_UNUSED(pos);
if (input.isEmpty()) return QValidator::Intermediate;
// Match QString::toDouble(): C locale, no hex floats / inf / nan.
const std::string bytes = input.toLatin1().toStdString();
// strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not.
if (bytes.find_first_of("xXpP") != std::string::npos) {
return QValidator::Invalid;
}
const char *start = bytes.c_str();
char *end = nullptr;
const double value = std::strtod(start, &end);
if (end == start) {
// Still typing a sign, decimal point, or exponent prefix.
if (input == "-" || input == "+" || input == "." || input == "-." || input == "+.") {
return QValidator::Intermediate;
}
return QValidator::Invalid;
}
if (*end == '\0') {
// Reject inf/nan (strtod accepts them; QDoubleValidator / toDouble path should not).
return std::isfinite(value) ? QValidator::Acceptable : QValidator::Invalid;
}
// Partial exponent / trailing sign while typing (e.g. "1e", "1e-", "1.").
for (const char *p = end; *p; ++p) {
const char c = *p;
if (!(c == 'e' || c == 'E' || c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9'))) {
return QValidator::Invalid;
}
}
return QValidator::Intermediate;
}
namespace utils {
std::string homePath() {
const char *home = ::getenv("HOME");
return home ? home : "";
}
std::filesystem::path configPath() {
#ifdef __APPLE__
return std::filesystem::path(homePath()) / "Library/Preferences";
#else
const char *xdg = ::getenv("XDG_CONFIG_HOME");
return (xdg && xdg[0]) ? std::filesystem::path(xdg) : std::filesystem::path(homePath()) / ".config";
#endif
}
#ifdef __APPLE__
static const char *clipboard_read_cmds[] = {"pbpaste"};
static const char *clipboard_write_cmds[] = {"pbcopy"};
#else
static const char *clipboard_read_cmds[] = {"wl-paste --no-newline 2>/dev/null", "xclip -selection clipboard -o 2>/dev/null", "xsel -ob 2>/dev/null"};
static const char *clipboard_write_cmds[] = {"wl-copy 2>/dev/null", "xclip -selection clipboard 2>/dev/null", "xsel -ib 2>/dev/null"};
#endif
bool getClipboardText(std::string *text) {
text->clear();
bool has_tool = false;
for (const char *cmd : clipboard_read_cmds) {
FILE *f = ::popen(cmd, "r");
if (!f) continue;
std::string out;
char buf[4096];
for (size_t n; (n = ::fread(buf, 1, sizeof(buf), f)) > 0;) out.append(buf, n);
int status = ::pclose(f);
if (status == 0) {
*text = std::move(out);
return true;
}
has_tool |= WIFEXITED(status) && WEXITSTATUS(status) != 127; // 127: command not found
}
return has_tool; // tool present but clipboard empty
}
bool setClipboardText(const std::string &text) {
std::signal(SIGPIPE, SIG_IGN);
for (const char *cmd : clipboard_write_cmds) {
FILE *f = ::popen(cmd, "w");
if (!f) continue;
size_t written = ::fwrite(text.data(), 1, text.size(), f);
if (::pclose(f) == 0 && written == text.size()) return true;
}
return false;
}
bool isDarkTheme() {
QColor windowColor = QApplication::palette().color(QPalette::Window);
return windowColor.lightness() < 128;
@@ -209,7 +368,10 @@ QPixmap icon(const QString &id) {
QString key = "bootstrap_" % id % (dark_theme ? "1" : "0");
if (!QPixmapCache::find(key, &pm)) {
pm = bootstrapPixmap(id);
if (dark_theme) {
// IQ.Pilot patch: ToolButton("") (chartswidget.cc) asks for an empty id, so pm can
// be null. Painting a null QPixmap is a no-op that logs two QPainter warnings on
// every dark-theme start. Upstream candidate.
if (dark_theme && !pm.isNull()) {
QPainter p(&pm);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.fillRect(pm.rect(), QColor("#bbbbbb"));
@@ -258,10 +420,34 @@ void setTheme(int theme) {
}
QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) {
QString format = absolute_time ? "yyyy-MM-dd hh:mm:ss"
: (sec > 60 * 60 ? "hh:mm:ss" : "mm:ss");
if (include_milliseconds) format += ".zzz";
return QDateTime::fromMSecsSinceEpoch(sec * 1000).toString(format);
if (absolute_time) {
const auto ms_total = static_cast<int64_t>(std::llround(sec * 1000.0));
const std::time_t secs = static_cast<std::time_t>(ms_total / 1000);
int millis = static_cast<int>(ms_total % 1000);
if (millis < 0) millis = -millis;
std::tm tm{};
localtime_r(&secs, &tm);
char buf[64];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
if (include_milliseconds) {
return QString::asprintf("%s.%03d", buf, millis);
}
return QString::fromUtf8(buf);
}
// Relative duration (not wall-clock).
const bool show_hours = sec > 60 * 60;
int total_ms = static_cast<int>(std::llround(std::max(0.0, sec) * 1000.0));
const int hours = total_ms / (3600 * 1000);
const int minutes = (total_ms / (60 * 1000)) % 60;
const int seconds = (total_ms / 1000) % 60;
const int millis = total_ms % 1000;
if (show_hours) {
return include_milliseconds ? QString::asprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis)
: QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds);
}
return include_milliseconds ? QString::asprintf("%02d:%02d.%03d", minutes, seconds, millis)
: QString::asprintf("%02d:%02d", minutes, seconds);
}
} // namespace utils
@@ -278,24 +464,10 @@ QString signalToolTip(const cabana::Signal *sig) {
Start Bit: %2 Size: %3<br />
MSB: %4 LSB: %5<br />
Little Endian: %6 Signed: %7</span>
)").arg(sig->name).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb)
)").arg(QString::fromStdString(sig->name)).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb)
.arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N");
}
void setSurfaceFormat() {
QSurfaceFormat fmt;
#ifdef __APPLE__
fmt.setVersion(3, 2);
fmt.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
fmt.setRenderableType(QSurfaceFormat::OpenGL);
#else
fmt.setRenderableType(QSurfaceFormat::OpenGLES);
#endif
fmt.setSamples(16);
fmt.setStencilBufferSize(1);
QSurfaceFormat::setDefaultFormat(fmt);
}
void sigTermHandler(int s) {
std::signal(s, SIG_DFL);
qApp->quit();
@@ -306,55 +478,68 @@ void initApp(int argc, char *argv[], bool disable_hidpi) {
std::signal(SIGINT, sigTermHandler);
std::signal(SIGTERM, sigTermHandler);
QString app_dir;
std::filesystem::path app_dir;
#ifdef __APPLE__
// Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering
QApplication tmp(argc, argv);
app_dir = QCoreApplication::applicationDirPath();
app_dir = QCoreApplication::applicationDirPath().toStdString();
if (disable_hidpi) {
qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit());
}
#else
app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path();
app_dir = std::filesystem::path(util::readlink("/proc/self/exe")).parent_path();
#endif
qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150));
qputenv("QT_DBL_CLICK_DIST", "150");
// ensure the current dir matches the exectuable's directory
QDir::setCurrent(app_dir);
setSurfaceFormat();
std::error_code ec;
std::filesystem::current_path(app_dir, ec);
}
static QHash<QString, QByteArray> load_bootstrap_icons() {
QHash<QString, QByteArray> icons;
// embedded at build time from the bootstrap_icons package (see SConscript)
extern const unsigned char bootstrap_icons_svg[];
extern const size_t bootstrap_icons_svg_len;
QFile f(":/bootstrap-icons.svg");
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
QDomDocument xml;
xml.setContent(&f);
QDomNode n = xml.documentElement().firstChild();
while (!n.isNull()) {
QDomElement e = n.toElement();
if (!e.isNull() && e.hasAttribute("id")) {
QString svg_str;
QTextStream stream(&svg_str);
n.save(stream, 0);
svg_str.replace("<symbol", "<svg");
svg_str.replace("</symbol>", "</svg>");
icons[e.attribute("id")] = svg_str.toUtf8();
static std::unordered_map<std::string, std::string> load_bootstrap_icons() {
std::unordered_map<std::string, std::string> icons;
const std::string content(reinterpret_cast<const char *>(bootstrap_icons_svg), bootstrap_icons_svg_len);
const std::string sym_open = "<symbol ";
const std::string sym_close = "</symbol>";
const std::string id_attr = "id=\"";
size_t pos = 0;
while ((pos = content.find(sym_open, pos)) != std::string::npos) {
size_t end = content.find(sym_close, pos);
if (end == std::string::npos) break;
end += sym_close.size();
// extract id
size_t id_start = content.find(id_attr, pos);
if (id_start != std::string::npos && id_start < end) {
id_start += id_attr.size();
size_t id_end = content.find('"', id_start);
if (id_end != std::string::npos && id_end < end) {
std::string id = content.substr(id_start, id_end - id_start);
std::string svg_str = content.substr(pos, end - pos);
// replace <symbol with <svg, </symbol> with </svg>
svg_str.replace(0, 7, "<svg"); // "<symbol" (7) -> "<svg" (4)
svg_str.replace(svg_str.size() - 9, 9, "</svg>"); // "</symbol>" (9) -> "</svg>" (6)
icons[id] = std::move(svg_str);
}
n = n.nextSibling();
}
pos = end;
}
return icons;
}
QPixmap bootstrapPixmap(const QString &id) {
static QHash<QString, QByteArray> icons = load_bootstrap_icons();
static auto icons = load_bootstrap_icons();
QPixmap pixmap;
if (auto it = icons.find(id); it != icons.end()) {
pixmap.loadFromData(it.value(), "svg");
auto it = icons.find(id.toStdString());
if (it != icons.end()) {
pixmap.loadFromData((const uchar *)it->second.data(), it->second.size(), "svg");
}
return pixmap;
}

View File

@@ -1,26 +1,32 @@
#pragma once
#include <array>
#include <atomic>
#include <cmath>
#include <filesystem>
#include <string>
#include <thread>
#include <vector>
#include <utility>
#include <QApplication>
#include <QByteArray>
#include <QDoubleValidator>
#include <QColor>
#include <QFont>
#include <QFontMetrics>
#include <QPainter>
#include <QRegExpValidator>
#include <QSocketNotifier>
#include <QStaticText>
#include <QStringBuilder>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QValidator>
#include "tools/cabana/dbc/dbc.h"
#include "tools/cabana/settings.h"
inline QColor toQColor(const CabanaColor &color) {
return QColor(color.r, color.g, color.b, color.a);
}
class LogSlider : public QSlider {
Q_OBJECT
@@ -84,22 +90,53 @@ private:
int h_margin, v_margin;
};
class NameValidator : public QRegExpValidator {
// Accepts a single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_'.
class NameValidator : public QValidator {
Q_OBJECT
public:
NameValidator(QObject *parent=nullptr);
QValidator::State validate(QString &input, int &pos) const override;
};
class DoubleValidator : public QDoubleValidator {
// Accepts comma-separated identifiers: \w+(,\w+)*
class NodeValidator : public QValidator {
Q_OBJECT
public:
NodeValidator(QObject *parent=nullptr);
QValidator::State validate(QString &input, int &pos) const override;
};
// Accepts one or more non-whitespace characters (\S+).
class NonWhitespaceValidator : public QValidator {
Q_OBJECT
public:
NonWhitespaceValidator(QObject *parent=nullptr);
QValidator::State validate(QString &input, int &pos) const override;
};
// Accepts a dotted IPv4 address (0-255 per octet).
class IpAddressValidator : public QValidator {
Q_OBJECT
public:
IpAddressValidator(QObject *parent=nullptr);
QValidator::State validate(QString &input, int &pos) const override;
};
// C-locale floating-point validator (matches QString::toDouble).
class DoubleValidator : public QValidator {
Q_OBJECT
public:
DoubleValidator(QObject *parent = nullptr);
QValidator::State validate(QString &input, int &pos) const override;
};
namespace utils {
QPixmap icon(const QString &id);
std::string homePath();
std::filesystem::path configPath();
bool getClipboardText(std::string *text); // false if no clipboard tool is available
bool setClipboardText(const std::string &text);
bool isDarkTheme();
void setTheme(int theme);
QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false);
@@ -108,7 +145,22 @@ inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text)
p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text);
}
inline QString toHex(const std::vector<uint8_t> &dat, char separator = '\0') {
return QByteArray::fromRawData((const char *)dat.data(), dat.size()).toHex(separator).toUpper();
static const char digits[] = "0123456789ABCDEF";
QString hex;
hex.reserve(dat.size() * (separator ? 3 : 2));
for (size_t i = 0; i < dat.size(); ++i) {
if (separator && i) hex += QLatin1Char(separator);
hex += QLatin1Char(digits[dat[i] >> 4]);
hex += QLatin1Char(digits[dat[i] & 0xf]);
}
return hex;
}
// boundary conversions for the remaining Qt byte-array based state APIs
template <typename T>
std::vector<uint8_t> toBytes(const T &dat) { return {dat.begin(), dat.end()}; }
inline auto qbytes(const std::vector<uint8_t> &dat) {
return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size());
}
}
@@ -147,20 +199,18 @@ private:
void closeTabClicked();
};
class UnixSignalHandler : public QObject {
Q_OBJECT
// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread
// (no Qt notifiers/timers). Exit is marshaled onto the GUI thread.
class UnixSignalHandler {
public:
UnixSignalHandler(QObject *parent = nullptr);
UnixSignalHandler();
~UnixSignalHandler();
static void signalHandler(int s);
public slots:
void handleSigTerm();
private:
inline static int sig_fd[2] = {};
QSocketNotifier *sn;
std::atomic<bool> shutting_down{false};
std::thread waiter;
};
int num_decimals(double num);

View File

@@ -1,6 +1,7 @@
#include "tools/cabana/videowidget.h"
#include <algorithm>
#include <thread>
#include <QAction>
#include <QActionGroup>
@@ -9,20 +10,20 @@
#include <QPainter>
#include <QStyleOptionSlider>
#include <QVBoxLayout>
#include <QtConcurrent>
#include "tools/cabana/tools/routeinfo.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[] = {
[(int)TimelineType::None] = QColor(111, 143, 175),
[(int)TimelineType::Engaged] = QColor(0, 163, 108),
[(int)TimelineType::UserBookmark] = Qt::magenta,
[(int)TimelineType::AlertInfo] = Qt::green,
[(int)TimelineType::AlertWarning] = QColor(255, 195, 0),
[(int)TimelineType::AlertCritical] = QColor(199, 0, 57),
QColor(111, 143, 175),
QColor(0, 163, 108),
Qt::green,
QColor(255, 195, 0),
QColor(199, 0, 57),
Qt::magenta,
};
static Replay *getReplay() {
@@ -50,7 +51,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) {
updatePlayBtnState();
setWhatsThis(tr(R"(
<b>Video</b><br />
<!-- TODO: add descprition here -->
<!-- TODO: add description here -->
<span style="color:gray">Timeline color</span>
<table>
<tr><td><span style="color:%1;"> </span>Disengaged </td>
@@ -156,7 +157,7 @@ QWidget *VideoWidget::createCameraWidget() {
slider->setTimeRange(can->minSeconds(), can->maxSeconds());
QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); });
QObject::connect(can, &AbstractStream::paused, cam_widget, [c = cam_widget]() { c->showPausedOverlay(); });
QObject::connect(can, &AbstractStream::paused, cam_widget, qOverload<>(&StreamCameraView::update));
QObject::connect(can, &AbstractStream::eventsMerged, this, [this]() { slider->update(); });
QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); });
QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated);
@@ -202,7 +203,7 @@ void VideoWidget::timeRangeChanged() {
QString VideoWidget::formatTime(double sec, bool include_milliseconds) {
if (settings.absolute_time)
sec = can->beginDateTime().addMSecs(sec * 1000).toMSecsSinceEpoch() / 1000.0;
sec += std::chrono::duration<double>(can->beginDateTime().time_since_epoch()).count();
return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time);
}
@@ -323,34 +324,40 @@ void Slider::mousePressEvent(QMouseEvent *e) {
// StreamCameraView
StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent)
: CameraWidget(stream_name, stream_type, parent) {
fade_animation = new QPropertyAnimation(this, "overlayOpacity");
fade_animation->setDuration(500);
fade_animation->setStartValue(0.2f);
fade_animation->setEndValue(0.7f);
fade_animation->setEasingCurve(QEasingCurve::InOutQuad);
connect(fade_animation, &QPropertyAnimation::valueChanged, this, QOverload<>::of(&StreamCameraView::update));
}
void StreamCameraView::parseQLog(std::shared_ptr<LogReader> qlog) {
std::mutex mutex;
QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [this, &mutex](const Event &e) {
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;
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::paintGL() {
CameraWidget::paintGL();
void StreamCameraView::paintEvent(QPaintEvent *event) {
CameraWidget::paintEvent(event);
QPainter p(this);
bool scrubbing = false;
@@ -363,7 +370,7 @@ void StreamCameraView::paintGL() {
}
if (can->isPaused()) {
p.setPen(QColor(200, 200, 200, static_cast<int>(255 * fade_animation->currentValue().toFloat())));
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"));
}
@@ -383,9 +390,9 @@ QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) {
void StreamCameraView::drawScrubThumbnail(QPainter &p) {
p.fillRect(rect(), Qt::black);
auto it = big_thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time));
auto it = big_thumbnails.lower_bound(can->toMonoTime(thumbnail_dispaly_time));
if (it != big_thumbnails.end()) {
QPixmap scaled_thumb = it.value().scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
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);
@@ -393,9 +400,9 @@ void StreamCameraView::drawScrubThumbnail(QPainter &p) {
}
void StreamCameraView::drawThumbnail(QPainter &p) {
auto it = thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time));
auto it = thumbnails.lower_bound(can->toMonoTime(thumbnail_dispaly_time));
if (it != thumbnails.end()) {
const QPixmap &thumb = it.value();
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);

View File

@@ -1,12 +1,12 @@
#pragma once
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <QFrame>
#include <QPropertyAnimation>
#include <QSlider>
#include <QToolBar>
#include <QTabBar>
@@ -35,8 +35,7 @@ class StreamCameraView : public CameraWidget {
public:
StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent = nullptr);
void paintGL() override;
void showPausedOverlay() { fade_animation->start(); }
void paintEvent(QPaintEvent *event) override;
void parseQLog(std::shared_ptr<LogReader> qlog);
private:
@@ -46,9 +45,8 @@ private:
void drawScrubThumbnail(QPainter &p);
void drawTime(QPainter &p, const QRect &rect, double seconds);
QPropertyAnimation *fade_animation;
QMap<uint64_t, QPixmap> big_thumbnails;
QMap<uint64_t, QPixmap> thumbnails;
std::map<uint64_t, QPixmap> big_thumbnails;
std::map<uint64_t, QPixmap> thumbnails;
double thumbnail_dispaly_time = -1;
friend class VideoWidget;
};

View File

@@ -0,0 +1,54 @@
# iqmacvisiond — IQ Vision offload server
Runs the iqvd perception model on an Apple-Silicon Mac and serves 2D detections
to one IQ device over wifi. The device (`iqvd`) ships the camera frame, the Mac
runs YOLO on the Metal GPU/NPU, and only boxes come back — the device does no
inference, so vision dots no longer contend with the driving model.
## Wire path
```
IQ device (auto-hotspot AP) ──wifi──▶ Mac (IQ Vision.app)
iqvd VisionClient iqmacvisiond server
read frame → downscale 640w cv2 decode → YOLOv8n (Metal)
JPEG encode → INFER ───────────────▶ detect
RESULT ◀─────────────────────────── {tracks: [2D boxes]}
publish iqVehicleTracks (dots)
publish iqEnvironment (3D via ground-plane + calibration)
```
- Discovery: the device UDP-broadcasts `IQVISION_DISCOVER_V1` on the subnet; the
Mac replies `IQVISION_HERE_V1:<tcp_port>`. No config, no pairing.
- Protocol: `iqvd_private_src/offload/protocol.py` (length-prefixed frames, JSON
header + optional binary blob). Shared verbatim by both sides — it is the ABI.
- Ports: tcp/51998 inference, udp/51999 discovery, tcp/51995 localhost status.
## The Mac app
- Menu-bar app (`◎` waiting, `◉` connected). Menu shows device, frames served,
inference p50/p99, and **Quit**.
- Keeps the Mac awake while running (`caffeinate`).
- Ships the model in the dmg — no download on first run.
- First launch creates a small venv (numpy, opencv-headless, rumps); tinygrad is
bundled.
## Build
```
tools/iqmacvisiond/macos/build_dmg.sh
```
Produces `IQ Vision.app` and `IQVision.dmg`. See `macos/SIGNING.md` for signing +
notarization.
## Run from source (dev)
```
DEV=METAL python3 tools/iqmacvisiond/server.py # server only
python3 tools/iqmacvisiond/menubar.py # menu-bar wrapper
python3 tools/iqmacvisiond/test_offload.py # protocol/geometry/loopback
```
Gating on the device: `VisionVehicleTracks` enables iqvd; when `maciqmodeld` (the
eMac driving offload) is running, iqvd is Mac-or-nothing — it never runs local
inference. On non-eMac setups iqvd falls back to on-device YOLO if no Mac is found.

View File

@@ -0,0 +1,23 @@
# Signing & notarization — IQ Vision.app
Unsigned, the app runs after a right-click ▸ Open (Gatekeeper first-run). For
distribution, sign + notarize:
```bash
APP="tools/iqmacvisiond/macos/dist/IQ Vision.app"
ENT="tools/iqmacvisiond/macos/entitlements.plist"
IDENTITY="Developer ID Application: <YOUR NAME> (<TEAMID>)"
codesign --force --deep --options runtime --entitlements "$ENT" \
--sign "$IDENTITY" "$APP"
hdiutil create -volname "IQ Vision" -srcfolder "$(dirname "$APP")" -ov -format UDZO \
tools/iqmacvisiond/macos/dist/IQVision.dmg
xcrun notarytool submit tools/iqmacvisiond/macos/dist/IQVision.dmg \
--apple-id "<APPLE_ID>" --team-id "<TEAMID>" --password "<APP_PW>" --wait
xcrun stapler staple tools/iqmacvisiond/macos/dist/IQVision.dmg
```
The entitlements cover: JIT + unsigned exec memory + library-validation off
(tinygrad Metal JIT) and network server/client (LAN discovery + inference).

View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
REPO="$(cd "$HERE/../../.." >/dev/null && pwd)"
OUT="${1:-$HERE/dist}"
APP="$OUT/IQ Vision.app"
rm -rf "$OUT"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cat > "$APP/Contents/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>IQ Vision</string>
<key>CFBundleIdentifier</key><string>com.iqpilot.iqvision</string>
<key>CFBundleVersion</key><string>1.0</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleExecutable</key><string>iqvision</string>
<key>LSMinimumSystemVersion</key><string>13.0</string>
<key>LSUIElement</key><true/>
</dict>
</plist>
PLIST
cat > "$APP/Contents/MacOS/iqvision" <<'LAUNCH'
#!/bin/bash
RES="$(cd "$(dirname "$0")/../Resources" && pwd)"
if [ ! -x "$HOME/Library/Application Support/IQVision/venv/bin/python" ]; then
osascript -e "tell application \"Terminal\"
activate
do script \"bash '$RES/macos/setup.sh'\"
end tell"
else
exec bash "$RES/macos/setup.sh" >/tmp/iqvision.log 2>&1
fi
LAUNCH
chmod +x "$APP/Contents/MacOS/iqvision"
RES="$APP/Contents/Resources"
SRC="$RES/openpilot/iqpilot/iqvd_private_src"
mkdir -p "$RES/tools/iqmacvisiond" "$RES/macos" "$SRC/offload" "$SRC/models"
cp "$REPO/tools/iqmacvisiond/server.py" "$REPO/tools/iqmacvisiond/menubar.py" "$RES/tools/iqmacvisiond/"
cp "$HERE/setup.sh" "$RES/macos/"
cp "$REPO/iqpilot/iqvd_private_src/__init__.py" "$REPO/iqpilot/iqvd_private_src/yolov8_net.py" "$SRC/"
cp "$REPO/iqpilot/iqvd_private_src/offload/"*.py "$SRC/offload/"
cp "$REPO/iqpilot/iqvd_private_src/models/yolov8n.safetensors" "$SRC/models/"
touch "$RES/openpilot/__init__.py" "$RES/openpilot/iqpilot/__init__.py" "$RES/tools/__init__.py" \
"$RES/tools/iqmacvisiond/__init__.py"
rsync -a --exclude=".git" --exclude="__pycache__" --exclude="extra" --exclude="test" \
--exclude="examples" --exclude="docs" "$REPO/tinygrad_repo/" "$RES/tinygrad_repo/"
hdiutil create -volname "IQ Vision" -srcfolder "$OUT" -ov -format UDZO "$OUT/IQVision.dmg" >/dev/null
echo "built: $OUT/IQVision.dmg ($(du -h "$OUT/IQVision.dmg" | cut -f1))"

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
<key>com.apple.security.cs.disable-library-validation</key><true/>
<key>com.apple.security.network.server</key><true/>
<key>com.apple.security.network.client</key><true/>
</dict>
</plist>

View File

@@ -0,0 +1,21 @@
#!/bin/bash
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
set -euo pipefail
RES="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." >/dev/null && pwd)"
SUPPORT="$HOME/Library/Application Support/IQVision"
VENV="$SUPPORT/venv"
PY="$VENV/bin/python"
mkdir -p "$SUPPORT"
if [ ! -x "$PY" ]; then
echo "Creating IQ Vision environment (one time)…"
/usr/bin/python3 -m venv "$VENV"
"$PY" -m pip install --upgrade --quiet pip
"$PY" -m pip install --quiet numpy "opencv-python-headless>=4.8" rumps
fi
export PYTHONPATH="$RES:$RES/tinygrad_repo"
export DEV=METAL
exec "$PY" "$RES/tools/iqmacvisiond/menubar.py"

80
tools/iqmacvisiond/menubar.py Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
import rumps
HERE = Path(__file__).resolve().parent
STATUS_URL = "http://127.0.0.1:51995/status.json"
DASHBOARD_URL = "http://127.0.0.1:51995"
class IQVisionApp(rumps.App):
def __init__(self):
super().__init__("IQ Vision", title="", quit_button=None)
self.item_state = rumps.MenuItem("Starting…")
self.item_device = rumps.MenuItem("Device: —")
self.item_frames = rumps.MenuItem("Frames: —")
self.item_exec = rumps.MenuItem("Inference: —")
self.item_awake = rumps.MenuItem("Keep awake: —")
self.menu = [
self.item_state, None,
self.item_device, self.item_frames, self.item_exec, self.item_awake, None,
rumps.MenuItem("Open Dashboard", callback=self.open_dashboard),
rumps.MenuItem("Quit IQ Vision", callback=self.quit_app),
]
self.proc: subprocess.Popen | None = None
self._start_server()
self.timer = rumps.Timer(self.refresh, 1)
self.timer.start()
def _start_server(self) -> None:
env = dict(os.environ)
self.proc = subprocess.Popen([sys.executable, str(HERE / "server.py")], env=env)
def refresh(self, _) -> None:
if self.proc is not None and self.proc.poll() is not None:
self.title = "◎!"
self.item_state.title = "Server stopped — reopen the app"
return
try:
with urllib.request.urlopen(STATUS_URL, timeout=0.8) as r:
s = json.load(r)
except Exception:
self.title = ""
self.item_state.title = "Warming up…"
return
live = s.get("connected") and s.get("fresh")
self.title = "" if live else ""
self.item_state.title = "Connected" if live else "Waiting for device"
self.item_device.title = f"Device: {s.get('peer') or ''}"
self.item_frames.title = f"Frames: {s.get('infer_count', 0):,}"
p50, p99 = s.get("exec_p50_ms", 0.0), s.get("exec_p99_ms", 0.0)
self.item_exec.title = f"Inference: {p50:.0f} / {p99:.0f} ms" if p50 else "Inference: —"
self.item_awake.title = f"Keep awake: {'on' if s.get('awake') else 'off'}"
def open_dashboard(self, _) -> None:
subprocess.Popen(["open", DASHBOARD_URL])
def quit_app(self, _) -> None:
if self.proc is not None:
self.proc.terminate()
try:
self.proc.wait(timeout=3)
except subprocess.TimeoutExpired:
self.proc.kill()
rumps.quit_application()
if __name__ == "__main__":
IQVisionApp().run()

290
tools/iqmacvisiond/server.py Executable file
View File

@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import signal
import socket
import subprocess
import sys
import threading
import time
from pathlib import Path
os.environ.setdefault("DEV", "METAL")
os.environ.setdefault("JIT_BATCH_SIZE", "0")
REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))
import cv2
import numpy as np
from openpilot.iqpilot.iqvd_private_src.offload.protocol import (
DISCOVERY_MAGIC, DISCOVERY_REPLY, DISCOVERY_PORT_DEFAULT, MSG_HELLO, MSG_HELLO_ACK, MSG_INFER,
MSG_PING, MSG_PONG, MSG_RESULT, ProtocolError, recv_msg, send_msg,
)
from openpilot.iqpilot.iqvd_private_src.offload.perception import Detector
log = logging.getLogger("iqmacvisiond")
DEFAULT_PORT = 51998
STATUS_PORT = 51995
MODEL_NAME = "yolov8n"
SESSION_IDLE_TIMEOUT_S = 8.0
STATUS: dict = {
"connected": False, "peer": "", "model": MODEL_NAME,
"exec_p50_ms": 0.0, "exec_p99_ms": 0.0, "infer_count": 0,
"last_seen": 0.0, "awake": False,
}
class KeepAwake:
def __init__(self):
self._proc: subprocess.Popen | None = None
def start(self) -> None:
if sys.platform != "darwin" or self._proc is not None:
return
try:
self._proc = subprocess.Popen(["caffeinate", "-dimsu"])
STATUS["awake"] = True
log.info("keep-awake active (caffeinate pid=%d)", self._proc.pid)
except OSError:
log.warning("caffeinate unavailable; display may sleep")
def stop(self) -> None:
if self._proc is not None:
self._proc.terminate()
self._proc = None
STATUS["awake"] = False
class DiscoveryResponder:
def __init__(self, tcp_port: int, disc_port: int = DISCOVERY_PORT_DEFAULT):
self.tcp_port = tcp_port
self.disc_port = disc_port
def start(self) -> None:
threading.Thread(target=self._serve, daemon=True).start()
def _serve(self) -> None:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind(("0.0.0.0", self.disc_port))
except OSError:
log.exception("discovery bind failed on udp/%d", self.disc_port)
return
reply = DISCOVERY_REPLY + f":{self.tcp_port}".encode()
log.info("discovery responder on udp/%d -> tcp/%d", self.disc_port, self.tcp_port)
while True:
try:
data, addr = sock.recvfrom(256)
except OSError:
continue
if data.startswith(DISCOVERY_MAGIC):
try:
sock.sendto(reply, addr)
except OSError:
pass
class StatusServer:
def __init__(self, port: int = STATUS_PORT):
self.port = port
def start(self) -> None:
threading.Thread(target=self._serve, daemon=True).start()
def _serve(self) -> None:
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def _send(self, code, ctype, body):
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path.startswith("/status.json"):
st = dict(STATUS)
st["fresh"] = (time.time() - st["last_seen"]) < 6 if st["last_seen"] else False
self._send(200, "application/json", json.dumps(st).encode())
else:
self._send(200, "text/html; charset=utf-8", _STATUS_HTML.encode())
try:
HTTPServer(("127.0.0.1", self.port), H).serve_forever()
except OSError:
log.exception("status server failed on %d", self.port)
_STATUS_HTML = """<!doctype html><html><head><meta charset=utf-8>
<title>IQ Vision</title><meta name=viewport content="width=device-width,initial-scale=1">
<style>
:root{color-scheme:dark}
body{margin:0;font:15px -apple-system,system-ui,sans-serif;background:#0b0d10;color:#e6e9ef}
.wrap{max-width:520px;margin:0 auto;padding:32px 20px}
h1{font-size:20px;margin:0 0 20px;display:flex;align-items:center;gap:10px}
.dot{width:12px;height:12px;border-radius:50%;background:#555}
.dot.green{background:#28d2c8;box-shadow:0 0 10px #28d2c8}
.dot.red{background:#e74c3c}
.row{display:flex;justify-content:space-between;padding:12px 0;border-bottom:1px solid #1c2027}
.k{color:#8a91a0}.v{font-variant-numeric:tabular-nums}
</style></head><body><div class=wrap>
<h1><span class=dot id=dot></span><span id=title>IQ Vision</span></h1>
<div class=row><span class=k>Device</span><span class="v" id=peer>—</span></div>
<div class=row><span class=k>Model</span><span class="v" id=model>—</span></div>
<div class=row><span class=k>Inference (p50 / p99)</span><span class="v" id=exec>—</span></div>
<div class=row><span class=k>Frames served</span><span class="v" id=count>—</span></div>
<div class=row><span class=k>Keep awake</span><span class="v" id=awake>—</span></div>
</div><script>
async function tick(){
try{
const s=await (await fetch('/status.json')).json();
const live=s.connected&&s.fresh;
document.getElementById('dot').className='dot '+(live?'green':'red');
document.getElementById('title').textContent=live?'IQ Vision — connected':'IQ Vision — waiting for device';
document.getElementById('peer').textContent=s.peer||'not connected';
document.getElementById('model').textContent=s.model||'';
document.getElementById('exec').textContent=s.exec_p50_ms?`${s.exec_p50_ms.toFixed(1)} / ${s.exec_p99_ms.toFixed(1)} ms`:'';
document.getElementById('count').textContent=s.infer_count?s.infer_count.toLocaleString():'';
document.getElementById('awake').textContent=s.awake?'on':'off';
}catch(e){document.getElementById('dot').className='dot red';}
}
tick();setInterval(tick,1000);
</script></body></html>"""
class Session:
def __init__(self, conn: socket.socket, detector: Detector):
self.conn = conn
self.detector = detector
try:
self.peer = conn.getpeername()[0]
except OSError:
self.peer = ""
self.infer_count = 0
self.exec_ms: list[float] = []
def handshake(self) -> bool:
msg_type, header, _ = recv_msg(self.conn)
if msg_type != MSG_HELLO:
raise ProtocolError(f"expected HELLO, got {msg_type}")
send_msg(self.conn, MSG_HELLO_ACK, {"ok": True, "model": MODEL_NAME, "hostname": socket.gethostname()})
STATUS.update(connected=True, peer=self.peer, last_seen=time.time())
log.info("device connected: %s dongle=%s", self.peer, header.get("dongle_id", ""))
return True
def serve(self) -> None:
while True:
msg_type, header, blob = recv_msg(self.conn)
if msg_type == MSG_INFER:
self._infer(header, blob)
elif msg_type == MSG_PING:
send_msg(self.conn, MSG_PONG, {})
else:
raise ProtocolError(f"unexpected message type {msg_type}")
def _infer(self, header: dict, jpeg: bytes) -> None:
st = time.perf_counter()
tracks = []
try:
rgb = cv2.imdecode(np.frombuffer(jpeg, np.uint8), cv2.IMREAD_COLOR)
if rgb is not None:
tracks = self.detector.detect(cv2.cvtColor(rgb, cv2.COLOR_BGR2RGB))
except Exception:
log.exception("inference failed for frame %s", header.get("frame_id"))
dt = (time.perf_counter() - st) * 1e3
send_msg(self.conn, MSG_RESULT, {"frame_id": header.get("frame_id", 0), "tracks": tracks,
"exec_ms": dt})
self.infer_count += 1
self.exec_ms.append(dt)
if len(self.exec_ms) > 400:
del self.exec_ms[:200]
if self.infer_count % 10 == 0 or self.infer_count == 1:
recent = self.exec_ms[-200:]
STATUS.update(connected=True, infer_count=self.infer_count, last_seen=time.time(),
exec_p50_ms=float(np.percentile(recent, 50)),
exec_p99_ms=float(np.percentile(recent, 99)))
def _weights_ok() -> bool:
from openpilot.iqpilot.iqvd_private_src.offload.perception import _weights_dir
return (_weights_dir() / "yolov8n.safetensors").exists()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--no-keep-awake", action="store_true")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
if not _weights_ok():
log.error("yolov8n.safetensors not found; the app ships the model with it")
sys.exit(1)
keep_awake = KeepAwake()
if not args.no_keep_awake:
keep_awake.start()
def _shutdown(*_):
keep_awake.stop()
sys.exit(0)
try:
signal.signal(signal.SIGTERM, _shutdown)
signal.signal(signal.SIGINT, _shutdown)
except ValueError:
pass
t0 = time.perf_counter()
detector = Detector(None)
for _ in range(3):
detector.detect(np.zeros((416, 640, 3), dtype=np.uint8))
log.info("model warm in %.1fs", time.perf_counter() - t0)
DiscoveryResponder(args.port).start()
StatusServer().start()
log.info("status dashboard on http://127.0.0.1:%d", STATUS_PORT)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((args.host, args.port))
server.listen(1)
log.info("READY listening on %s:%d", args.host, args.port)
try:
while True:
conn, addr = server.accept()
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
conn.settimeout(SESSION_IDLE_TIMEOUT_S)
try:
session = Session(conn, detector)
if session.handshake():
session.serve()
except (ConnectionError, ProtocolError, OSError) as e:
log.info("session ended: %s", e)
finally:
conn.close()
STATUS.update(connected=False, peer="")
finally:
keep_awake.stop()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
import socket
import sys
import threading
import time
from pathlib import Path
import numpy as np
REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT))
from openpilot.iqpilot.iqvd_private_src.offload import protocol
from openpilot.iqpilot.iqvd_private_src.offload.client import VisionClient, discover_server
from openpilot.iqpilot.iqvd_private_src.offload.geometry import pixel_to_ground, tracks_to_objects
def test_protocol_roundtrip():
a, b = socket.socketpair()
protocol.send_msg(a, protocol.MSG_INFER, {"frame_id": 7, "w": 640}, b"\x00\x01\x02payload")
mt, header, blob = protocol.recv_msg(b)
assert mt == protocol.MSG_INFER
assert header == {"frame_id": 7, "w": 640}
assert blob == b"\x00\x01\x02payload"
a.close()
b.close()
def test_protocol_empty_blob():
a, b = socket.socketpair()
protocol.send_msg(a, protocol.MSG_HELLO_ACK, {"ok": True, "model": "yolov8n"})
mt, header, blob = protocol.recv_msg(b)
assert mt == protocol.MSG_HELLO_ACK and header["model"] == "yolov8n" and blob == b""
a.close()
b.close()
def test_geometry_center_projects_forward():
intr = np.array([[900.0, 0.0, 960.0], [0.0, 900.0, 604.0], [0.0, 0.0, 1.0]])
device_from_calib = np.eye(3)
p = pixel_to_ground(960.0, 900.0, intr, device_from_calib, 1.22)
assert p is not None
assert p[0] > 0
assert abs(p[1]) < 1.0
def test_geometry_above_horizon_none():
intr = np.array([[900.0, 0.0, 960.0], [0.0, 900.0, 604.0], [0.0, 0.0, 1.0]])
assert pixel_to_ground(960.0, 100.0, intr, np.eye(3), 1.22) is None
def test_tracks_to_objects():
intr = np.array([[900.0, 0.0, 960.0], [0.0, 900.0, 604.0], [0.0, 0.0, 1.0]])
tracks = [{"x1": 0.45, "y1": 0.5, "x2": 0.55, "y2": 0.75, "prob": 0.9, "label": "car"}]
objs = tracks_to_objects(tracks, 1928, 1208, intr, [0.0, 0.0, 0.0])
assert len(objs) == 1
assert objs[0]["x"] > 0 and objs[0]["label"] == "car"
class _StubDetector:
def detect(self, rgb):
return [{"x1": 0.1, "y1": 0.2, "x2": 0.3, "y2": 0.5, "prob": 0.8, "label": "car"}]
def _run_server(port, ready):
from openpilot.iqpilot.iqvd_private_src.offload import protocol as p
import tools.iqmacvisiond.server as srv
disc = srv.DiscoveryResponder(port)
disc.start()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", port))
server.listen(1)
ready.set()
conn, _ = server.accept()
conn.settimeout(5)
session = srv.Session(conn, _StubDetector())
session.handshake()
try:
session.serve()
except (p.ProtocolError, OSError):
pass
def test_discovery_and_loopback():
port = 52050
ready = threading.Event()
threading.Thread(target=_run_server, args=(port, ready), daemon=True).start()
assert ready.wait(5)
time.sleep(0.2)
found = discover_server(timeout=2.0)
assert found is not None, "discovery failed"
assert found[1] == port
import cv2
ok, jpeg = cv2.imencode(".jpg", np.zeros((400, 640, 3), dtype=np.uint8))
assert ok
client = VisionClient("test-dongle")
assert client.connect(), "connect failed"
meta = {"frame_id": 42, "wide": False, "w": 640, "h": 400}
tracks = client.infer(jpeg.tobytes(), meta)
assert tracks is not None and len(tracks) == 1
assert tracks[0]["label"] == "car"
client.close()
if __name__ == "__main__":
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
for fn in fns:
fn()
print(f"ok {fn.__name__}")
print(f"\n{len(fns)} passed")

View File

@@ -17,12 +17,14 @@ from iqdbc.car.fingerprints import MIGRATION
from iqdbc.car.values import PLATFORMS
from openpilot.common.basedir import BASEDIR
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib')
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_libs')
jot_env = env.Clone()
# imgui.MESA_DIR only exists on the larch64 wheel; skip missing paths to avoid an ld search-path warning
jot_env["LIBPATH"] += [p for p in [imgui.MESA_DIR, libusb.LIB_DIR] if os.path.isdir(p)]
jot_env["CPPPATH"] += [imgui.INCLUDE_DIR, libusb.INCLUDE_DIR]
# IQ.Pilot patch: no comma-deps-bootstrap-icons wheel here, so BOOTSTRAP_ICONS_TTF is
# not defined; icons.cc reads the TTF vendored under tools/jotpluggler/assets instead.
jot_env["CXXFLAGS"] += [
"-DGLFW_INCLUDE_NONE",
'-DJOTP_REPO_ROOT=\'"%s"\'' % os.path.realpath(BASEDIR),
@@ -104,16 +106,15 @@ event_extractors = jot_env.Command("generated_event_extractors.h", [
jot_env.PrettyAction(generate_event_extractors, 'GEN'),
)
libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_DIR}/libimgui.a"), File(f"{imgui.LIB_DIR}/libglfw3.a"),
# IQ.Pilot patch: iqpilot's replay_lib resolves URL/api work via libcurl
# (vs upstream's Python downloader), so jotpluggler needs to link curl too.
# iqpilot's api.cc additionally uses OpenSSL primitives (PEM/RSA/SHA256) for
# JWT signing, and visionipc references OpenCL — both must also be linked.
"avformat", "avcodec", "avutil", "x264", "yuv", "z", "bz2", "zstd", "curl", "ssl", "crypto", "m", "pthread", "usb-1.0"]
# IQ.Pilot patch: iqpilot's replay_lib resolves route/API work via libcurl (vs upstream's
# Python downloader), so jotpluggler needs curl; api.cc signs konn3kt JWTs with OpenSSL
# primitives, and visionipc references OpenCL. Both must also be linked.
libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_DIR}/libimgui.a"), File(f"{imgui.LIB_DIR}/libglfw3.a")] + \
ffmpeg_libs + ["bz2", "zstd", "curl", "ssl", "crypto", "m", "pthread", "usb-1.0"]
if arch == "Darwin":
jot_env["FRAMEWORKS"] = ["OpenGL", "OpenCL", "Cocoa", "IOKit", "CoreFoundation", "CoreVideo", "CoreMedia", "VideoToolbox"]
else:
libs += ["GL", "OpenCL", "dl", "va", "va-drm", "drm"]
libs += ["GL", "OpenCL", "dl"]
program = jot_env.Program("jotpluggler", jot_env.Glob("*.cc"), LIBS=libs)
jot_env.Depends(program, generated_dbc_stamp)

View File

@@ -3,6 +3,7 @@
#include "tools/jotpluggler/common.h"
#include "tools/jotpluggler/internal.h"
#include "tools/jotpluggler/map.h"
#include "tools/jotpluggler/thumbnail.h"
#include "system/hardware/hw.h"
#include "imgui_impl_glfw.h"
@@ -1033,10 +1034,12 @@ bool apply_special_item_to_pane(WorkspaceTab *tab, TabUiState *tab_state, int pa
if (pane.title == UNTITLED_PANE_TITLE || previous_kind != PaneKind::Plot) {
pane.title = spec->label;
}
} else {
} else if (spec->kind == PaneKind::Camera) {
pane.title = spec->label;
resize_tab_pane_state(tab_state, tab->panes.size());
tab_state->camera_panes[static_cast<size_t>(pane_index)].fit_to_pane = true;
} else {
pane.title = spec->label;
}
tab_state->active_pane_index = pane_index;
return true;
@@ -1565,6 +1568,8 @@ void draw_pane_windows(AppSession *session, UiState *state) {
}
if (pane.kind == PaneKind::Map) {
draw_map_pane(session, state, &pane, static_cast<int>(i));
} else if (pane.kind == PaneKind::Thumbnail) {
draw_thumbnail_pane(session, state);
} else if (pane.kind == PaneKind::Camera) {
draw_camera_pane(session, state, tab_state, static_cast<int>(i), pane);
} else {
@@ -1847,6 +1852,7 @@ int run(const Options &options) {
for (std::unique_ptr<CameraFeedView> &feed : session.pane_camera_feeds) {
feed = std::make_unique<CameraFeedView>();
}
session.thumbnail_view = std::make_unique<ThumbnailView>();
sync_camera_feeds(&session);
if (session.async_route_loading) {
@@ -1892,6 +1898,7 @@ int run(const Options &options) {
for (std::unique_ptr<CameraFeedView> &feed : session.pane_camera_feeds) {
feed.reset();
}
session.thumbnail_view.reset();
return 0;
} catch (const std::exception &err) {
std::cerr << err.what() << "\n";

View File

@@ -81,6 +81,7 @@ struct Curve {
enum class PaneKind : uint8_t {
Plot,
Map,
Thumbnail,
Camera,
};
@@ -141,9 +142,15 @@ struct CameraFeedIndex {
std::vector<CameraFrameIndexEntry> entries;
};
struct ThumbnailFrame {
double timestamp = 0.0;
int segment = -1;
std::vector<uint8_t> jpeg;
};
enum class LogOrigin : uint8_t {
Log,
Android,
OperatingSystem,
Alert,
};
@@ -318,6 +325,7 @@ struct RouteData {
CameraFeedIndex driver_camera;
CameraFeedIndex wide_road_camera;
CameraFeedIndex qroad_camera;
std::vector<ThumbnailFrame> thumbnails;
GpsTrace gps_trace;
std::vector<LogEntry> logs;
std::vector<TimelineEntry> timeline;
@@ -445,6 +453,7 @@ bool icon_menu_item(const char *glyph,
class AsyncRouteLoader;
class CameraFeedView;
class ThumbnailView;
class StreamPoller;
class MapDataManager;
@@ -486,6 +495,7 @@ struct AppSession {
std::unique_ptr<AsyncRouteLoader> route_loader;
std::unique_ptr<StreamPoller> stream_poller;
std::array<std::unique_ptr<CameraFeedView>, 4> pane_camera_feeds;
std::unique_ptr<ThumbnailView> thumbnail_view;
std::unique_ptr<MapDataManager> map_data;
bool async_route_loading = false;
double next_stream_custom_refresh_time = 0.0;
@@ -885,3 +895,20 @@ private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
class ThumbnailView {
public:
ThumbnailView();
~ThumbnailView();
ThumbnailView(const ThumbnailView &) = delete;
ThumbnailView &operator=(const ThumbnailView &) = delete;
void setThumbnails(const std::vector<ThumbnailFrame> &thumbnails);
void update(double tracker_time);
void drawSized(ImVec2 size, bool loading);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};

View File

@@ -47,11 +47,11 @@ const char *special_item_label(std::string_view item_id) {
}
bool pane_kind_is_special(PaneKind kind) {
return kind == PaneKind::Map || kind == PaneKind::Camera;
return kind == PaneKind::Map || kind == PaneKind::Thumbnail || kind == PaneKind::Camera;
}
bool is_default_special_title(std::string_view title) {
if (title == "Map") return true;
if (title == "Map" || title == "Thumbnail") return true;
return std::any_of(kCameraViewSpecs.begin(), kCameraViewSpecs.end(), [&](const CameraViewSpec &spec) {
return title == spec.label;
});
@@ -162,14 +162,12 @@ void open_external_url(std::string_view url) {
}
}
std::string route_useradmin_url(const RouteIdentifier &route_id) {
return route_id.empty() ? std::string()
: "https://useradmin.comma.ai/?onebox=" + route_id.dongle_id + "%7C" + route_id.log_id;
}
std::string route_connect_url(const RouteIdentifier &route_id) {
return route_id.empty() ? std::string()
: "https://connect.comma.ai/" + route_id.canonical();
// IQ.Pilot patch: iqpilot routes live on konn3kt, not comma connect. konn3kt has no
// useradmin equivalent, so that link is gone; the share link keeps connect's
// <dongle_id>/<log_id> path shape (see tools/lib/logreader.py parse_indirect).
std::string route_konn3kt_url(const RouteIdentifier &route_id) {
static const std::string host = util::getenv("KONN3KT_APP_HOST", "https://konn3kt.com");
return route_id.empty() ? std::string() : host + "/" + route_id.canonical();
}
std::string route_google_maps_url(const GpsTrace &trace) {

View File

@@ -28,8 +28,9 @@ inline constexpr std::array<CameraViewSpec, 4> kCameraViewSpecs = {{
{CameraViewKind::QRoad, "qRoad Camera", "qroad", "qroad", "camera_qroad", &RouteData::qroad_camera},
}};
inline constexpr std::array<SpecialItemSpec, 5> kSpecialItemSpecs = {{
inline constexpr std::array<SpecialItemSpec, 6> kSpecialItemSpecs = {{
{"map", "Map", PaneKind::Map, CameraViewKind::Road},
{"thumbnail", "Thumbnail", PaneKind::Thumbnail, CameraViewKind::Road},
{kCameraViewSpecs[0].special_item_id, kCameraViewSpecs[0].label, PaneKind::Camera, kCameraViewSpecs[0].view},
{kCameraViewSpecs[1].special_item_id, kCameraViewSpecs[1].label, PaneKind::Camera, kCameraViewSpecs[1].view},
{kCameraViewSpecs[2].special_item_id, kCameraViewSpecs[2].label, PaneKind::Camera, kCameraViewSpecs[2].view},
@@ -62,6 +63,5 @@ bool app_begin_popup_modal(const char *name,
bool *p_open = nullptr,
ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize);
void open_external_url(std::string_view url);
std::string route_useradmin_url(const RouteIdentifier &route_id);
std::string route_connect_url(const RouteIdentifier &route_id);
std::string route_konn3kt_url(const RouteIdentifier &route_id);
std::string route_google_maps_url(const GpsTrace &trace);

View File

@@ -276,6 +276,7 @@ inline void Database::parseSg(const std::string &line, int line_number, Message
signal.type = Signal::Type::Multiplexor;
} else if (!indicator.empty() && indicator.front() == 'm') {
signal.type = Signal::Type::Multiplexed;
// IQ.Pilot patch: guard a bare "m" indicator; std::stoi("") throws.
const std::string mux = indicator.substr(1);
signal.multiplex_value = mux.empty() ? 0 : std::stoi(mux);
} else {

View File

@@ -62,6 +62,8 @@ class Generator:
def __init__(self, event_schema):
self.event_schema = event_schema
self.fixed_paths = []
self.event_base_slots = {}
self.static_enums = []
self.tmp_index = 0
self.lines = []
self.emits_memo = {}
@@ -103,9 +105,13 @@ class Generator:
self.emit(indent, f"append_dynamic_scalar_point({path_expr}, tm, {double_expr}, series);")
else:
slot = self.add_fixed_path(path)
if kind == "Enum":
self.emit_enum_capture(indent, cxx_string(path), enum_names(schema))
self.emit(indent, f"append_fixed_scalar_point(&series->fixed_series[{slot}], tm, {double_expr});")
names = enum_names(schema) if kind == "Enum" else []
if names:
enum_index = len(self.static_enums)
self.static_enums.append(names)
self.emit(indent, f"append_fixed_enum_point({slot}, {enum_index}, tm, {double_expr}, series);")
else:
self.emit(indent, f"append_fixed_scalar_point(&series->fixed_series[{slot}], tm, {double_expr});")
return
if type_kind == "struct":
@@ -144,9 +150,13 @@ class Generator:
self.emit(indent, f"if ({' && '.join(conditions)}) {{")
indent += 2
value_var = self.tmp("value")
self.emit(indent, f"const auto {value_var} = {get_call};")
self.emit_node(indent, type_kind, type_proto, value_schema, value_var, field_path, field_path_expr, dynamic_path)
# Scalar getters are only consumed once. Emitting them directly avoids
# thousands of single-use locals in the generated extractor.
value_expr = get_call
if kind is None:
value_expr = self.tmp("value")
self.emit(indent, f"const auto {value_expr} = {get_call};")
self.emit_node(indent, type_kind, type_proto, value_schema, value_expr, field_path, field_path_expr, dynamic_path)
if conditions:
indent -= 2
@@ -249,35 +259,47 @@ class Generator:
self.emit(indent + 2, "}")
self.emit(indent, "}")
self.emit(indent, "if (skip_raw_can) {")
self.emit(indent + 2, "return true;")
self.emit(indent + 2, "return;")
self.emit(indent, "}")
def emit_event_case(self, field_name):
def emit_event_reader(self, field_name):
field = self.event_schema.fields[field_name]
proto = field.proto
type_kind = field_type(field)
type_proto = field_type_proto(field)
kind = scalar_kind(type_proto)
schema = field.schema if kind == "Enum" or type_kind in NESTED_TYPE_KINDS else None
self.emit(4, f"case static_cast<cereal::Event::Which>({proto.discriminantValue}): {{")
valid_slot = self.add_fixed_path(f"/{field_name}/valid")
mono_slot = self.add_fixed_path(f"/{field_name}/logMonoTime")
seconds_slot = self.add_fixed_path(f"/{field_name}/t")
self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{valid_slot}], tm, event.getValid() ? 1.0 : 0.0);")
self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{mono_slot}], tm, static_cast<double>(event.getLogMonoTime()));")
self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{seconds_slot}], tm, tm);")
if field_name in {"can", "sendcan"}:
self.emit_can_special(6, field_name)
if self.node_emits(type_kind, type_proto, schema):
self.add_fixed_path(f"/{field_name}/logMonoTime")
self.add_fixed_path(f"/{field_name}/t")
self.event_base_slots[proto.discriminantValue] = valid_slot
emits_payload = self.node_emits(type_kind, type_proto, schema)
if field_name not in {"can", "sendcan"} and not emits_payload:
return None
reader_name = f"append_event_{proto.discriminantValue}"
needs_can = field_name in {"can", "sendcan"}
header_index = len(self.lines)
self.emit(0, "")
if needs_can:
self.emit_can_special(2, field_name)
if emits_payload:
payload = self.tmp("payload")
self.emit(6, f"const auto {payload} = event.{accessor('get', field_name)}();")
self.emit_node(6, type_kind, type_proto, schema, payload, f"/{field_name}", None, False)
self.emit(6, "return true;")
self.emit(4, "}")
self.emit(2, f"const auto {payload} = event.{accessor('get', field_name)}();")
self.emit_node(2, type_kind, type_proto, schema, payload, f"/{field_name}", None, False)
self.emit(0, "}")
self.emit(0, "")
if needs_can:
signature = "const cereal::Event::Reader &event, const dbc::Database *can_dbc, bool skip_raw_can, double tm, SeriesAccumulator *series"
else:
signature = "const cereal::Event::Reader &event, double tm, SeriesAccumulator *series"
self.lines[header_index] = f"__attribute__((noinline)) void {reader_name}({signature}) {{"
return reader_name, needs_can
def generate(self):
self.lines = []
self.emit(0, "// Generated by tools/jotpluggler/generate_event_extractors.py; do not edit.")
self.emit(0, "// Generated by openpilot/tools/jotpluggler/generate_event_extractors.py; do not edit.")
self.emit(0, "")
self.emit(0, "const std::vector<std::string> &static_event_fixed_paths() {")
self.emit(2, "static const std::vector<std::string> paths = {")
@@ -300,11 +322,66 @@ class Generator:
self.emit(2, "}")
self.emit(0, "}")
self.emit(0, "")
self.emit(0, "__attribute__((noinline)) void append_fixed_enum_point(size_t series_slot, size_t enum_index, double tm, double value, SeriesAccumulator *series);") # noqa: E501
self.emit(0, "")
self.emit(0, "// Keep each event payload behind its own optimizer boundary. Combining the")
self.emit(0, "// whole schema into one function creates much more code and runs slower.")
event_readers = {}
for field_name in self.event_schema.union_fields:
event_readers[field_name] = self.emit_event_reader(field_name)
self.emit(0, "static const std::initializer_list<std::string_view> static_event_enum_names[] = {")
for names in self.static_enums:
names_expr = "{" + ", ".join(cxx_string(name) for name in names) + "}"
self.emit(2, f"{names_expr},")
self.emit(0, "};")
self.emit(0, "")
self.emit(0, "__attribute__((noinline)) void append_fixed_enum_point(size_t series_slot, size_t enum_index, double tm, double value, SeriesAccumulator *series) {") # noqa: E501
self.emit(2, "RouteSeries *fixed_series = &series->fixed_series[series_slot];")
self.emit(2, "capture_static_enum_info(fixed_series->path, static_event_enum_names[enum_index], series);")
self.emit(2, "fixed_series->times.push_back(tm);")
self.emit(2, "fixed_series->values.push_back(value);")
self.emit(0, "}")
self.emit(0, "")
self.emit(0, "bool append_event_static_reader(cereal::Event::Which which, const cereal::Event::Reader &event, const dbc::Database *can_dbc, bool skip_raw_can, double time_offset, SeriesAccumulator *series) {") # noqa: E501
self.emit(2, "const double tm = static_cast<double>(event.getLogMonoTime()) / 1.0e9 - time_offset;")
self.emit(2, "const auto log_mono_time = event.getLogMonoTime();")
self.emit(2, "const double tm = static_cast<double>(log_mono_time) / 1.0e9 - time_offset;")
invalid_slot = "static_cast<size_t>(-1)"
max_discriminant = max(self.event_base_slots)
base_slots = [self.event_base_slots.get(i, invalid_slot) for i in range(max_discriminant + 1)]
self.emit(2, "static constexpr size_t event_base_slots[] = {")
for slot in base_slots:
self.emit(4, f"{slot},")
self.emit(2, "};")
self.emit(2, "const size_t event_index = static_cast<size_t>(which);")
self.emit(2, "if (event_index >= sizeof(event_base_slots) / sizeof(event_base_slots[0])) {")
self.emit(4, "return false;")
self.emit(2, "}")
self.emit(2, "const size_t base_slot = event_base_slots[event_index];")
self.emit(2, f"if (base_slot == {invalid_slot}) {{")
self.emit(4, "return false;")
self.emit(2, "}")
self.emit(2, "RouteSeries *base_series = &series->fixed_series[base_slot];")
self.emit(2, "base_series[0].times.push_back(tm);")
self.emit(2, "base_series[0].values.push_back(event.getValid() ? 1.0 : 0.0);")
self.emit(2, "base_series[1].times.push_back(tm);")
self.emit(2, "base_series[1].values.push_back(static_cast<double>(log_mono_time));")
self.emit(2, "base_series[2].times.push_back(tm);")
self.emit(2, "base_series[2].values.push_back(tm);")
self.emit(2, "switch (which) {")
for field_name in self.event_schema.union_fields:
self.emit_event_case(field_name)
field = self.event_schema.fields[field_name]
self.emit(4, f"case static_cast<cereal::Event::Which>({field.proto.discriminantValue}):")
reader = event_readers[field_name]
if reader is not None:
if reader[1]:
self.emit(6, f"{reader[0]}(event, can_dbc, skip_raw_can, tm, series);")
else:
self.emit(6, f"{reader[0]}(event, tm, series);")
self.emit(6, "return true;")
self.emit(4, "default:")
self.emit(6, "return false;")
self.emit(2, "}")
@@ -323,6 +400,8 @@ if __name__ == "__main__":
repo_root = Path(sys.argv[1]).resolve()
output = Path(sys.argv[2])
capnp.remove_import_hook()
# IQ.Pilot patch: iqpilot's tree is not nested under openpilot/ and car.capnp lives
# in cereal/ alongside log.capnp, so no extra import path is needed.
log = capnp.load(str(repo_root / "cereal" / "log.capnp"))
generated = Generator(log.Event.schema).generate()
output.parent.mkdir(parents=True, exist_ok=True)

View File

@@ -6,7 +6,7 @@
void icon_add_font(float size, bool merge, const ImFont *base_font) {
// IQ.Pilot patch: iqpilot's third_party/bootstrap is git-lfs and the TTF isn't
// checked in, so we vendor the font alongside jotpluggler instead of relying on
// third_party/bootstrap/bootstrap-icons.ttf.
// upstream's BOOTSTRAP_ICONS_TTF define from the comma-deps-bootstrap-icons wheel.
const std::filesystem::path ttf = repo_root() / "tools" / "jotpluggler" / "assets" / "bootstrap-icons.ttf";
ImGuiIO &io = ImGui::GetIO();
ImFontConfig config;

View File

@@ -62,6 +62,8 @@ json11::Json workspace_node_to_json(const WorkspaceNode &node, const WorkspaceTa
};
if (pane.kind == PaneKind::Map) {
obj["kind"] = "map";
} else if (pane.kind == PaneKind::Thumbnail) {
obj["kind"] = "thumbnail";
} else if (pane.kind == PaneKind::Camera) {
obj["kind"] = "camera";
obj["camera_view"] = camera_view_spec(pane.camera_view).layout_name;

View File

@@ -7,7 +7,7 @@
#include "imgui_impl_opengl3.h"
#include "imgui_impl_opengl3_loader.h"
#include "implot.h"
#include "libyuv.h"
#include "common/yuv.h"
#include "msgq_repo/msgq/ipc.h"
#include "tools/replay/framereader.h"
@@ -1173,18 +1173,14 @@ struct CameraFeedView::Impl {
result.width = reader->width;
result.height = reader->height;
result.rgba.resize(static_cast<size_t>(result.width) * static_cast<size_t>(result.height) * 4U, 0);
// IQ.Pilot patch: vendored libyuv lacks NV12ToABGR; use NV12ToARGB + in-place ARGBToABGR swap.
libyuv::NV12ToARGB(decode_buffer.y,
static_cast<int>(decode_buffer.stride),
decode_buffer.uv,
static_cast<int>(decode_buffer.stride),
result.rgba.data(),
result.width * 4,
result.width,
result.height);
libyuv::ARGBToABGR(result.rgba.data(), result.width * 4,
result.rgba.data(), result.width * 4,
result.width, result.height);
yuv::nv12_to_rgba(decode_buffer.y,
static_cast<int>(decode_buffer.stride),
decode_buffer.uv,
static_cast<int>(decode_buffer.stride),
result.rgba.data(),
result.width * 4,
result.width,
result.height);
result.success = true;
result.decode_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - decode_begin).count();
publish_result(*request, std::move(result));
@@ -1207,18 +1203,14 @@ struct CameraFeedView::Impl {
.height = reader->height,
};
prefetched.rgba.resize(static_cast<size_t>(prefetched.width) * static_cast<size_t>(prefetched.height) * 4U, 0);
// IQ.Pilot patch: vendored libyuv lacks NV12ToABGR; use NV12ToARGB + in-place ARGBToABGR swap.
libyuv::NV12ToARGB(decode_buffer.y,
static_cast<int>(decode_buffer.stride),
decode_buffer.uv,
static_cast<int>(decode_buffer.stride),
prefetched.rgba.data(),
prefetched.width * 4,
prefetched.width,
prefetched.height);
libyuv::ARGBToABGR(prefetched.rgba.data(), prefetched.width * 4,
prefetched.rgba.data(), prefetched.width * 4,
prefetched.width, prefetched.height);
yuv::nv12_to_rgba(decode_buffer.y,
static_cast<int>(decode_buffer.stride),
decode_buffer.uv,
static_cast<int>(decode_buffer.stride),
prefetched.rgba.data(),
prefetched.width * 4,
prefetched.width,
prefetched.height);
remember_cached_result(prefetched);
}
}

View File

@@ -19,6 +19,9 @@ void sync_camera_feeds(AppSession *session) {
session->pane_camera_feeds[i]->setCameraIndex(session->route_data.*(kCameraViewSpecs[i].route_member), kCameraViewSpecs[i].view);
}
}
if (session->thumbnail_view) {
session->thumbnail_view->setThumbnails(session->route_data.thumbnails);
}
}
void apply_route_data(AppSession *session, UiState *state, RouteData route_data) {
@@ -456,8 +459,7 @@ void draw_route_info_popup(AppSession *session, UiState *state, ImVec2 anchor) {
const char *copy_icon = icon::CLIPBOARD;
const char *link_icon = icon::BOX_ARROW_UP_RIGHT;
const std::string useradmin_label = std::string("Useradmin ") + link_icon;
const std::string connect_label = std::string("comma connect ") + link_icon;
const std::string konn3kt_label = std::string("konn3kt ") + link_icon;
if (ImGui::Button(copy_icon, ImVec2(34.0f, 26.0f))) {
ImGui::SetClipboardText(session->route_id.canonical().c_str());
state->status_text = "Copied route to clipboard";
@@ -470,14 +472,9 @@ void draw_route_info_popup(AppSession *session, UiState *state, ImVec2 anchor) {
ImGui::EndTooltip();
}
ImGui::SameLine();
if (ImGui::Button(useradmin_label.c_str(), ImVec2(132.0f, 26.0f))) {
open_external_url(route_useradmin_url(session->route_id));
state->status_text = "Opened useradmin";
}
ImGui::SameLine();
if (ImGui::Button(connect_label.c_str(), ImVec2(156.0f, 26.0f))) {
open_external_url(route_connect_url(session->route_id));
state->status_text = "Opened comma connect";
if (ImGui::Button(konn3kt_label.c_str(), ImVec2(120.0f, 26.0f))) {
open_external_url(route_konn3kt_url(session->route_id));
state->status_text = "Opened konn3kt";
}
ImGui::Spacing();

View File

@@ -24,10 +24,11 @@
#include "common/util.h"
#include "third_party/json11/json11.hpp"
#include "tools/replay/api.h"
#include "tools/replay/logreader.h"
// IQ.Pilot patch: iqpilot's tools/replay has no py_downloader; route file listing is
// served directly by CommaApi2::httpGet against the IQ.Pilot konn3kt API.
// served by the konn3kt CommaApi2 helper, which returns the same JSON shape (and the
// same {"error": ...} envelope) as upstream's PyDownloader.
#include "tools/replay/api.h"
namespace fs = std::filesystem;
@@ -100,6 +101,7 @@ struct LoadedRouteArtifacts {
std::vector<CanMessageData> can_messages;
std::vector<LogEntry> logs;
std::vector<TimelineEntry> timeline;
std::vector<ThumbnailFrame> thumbnails;
std::unordered_map<std::string, EnumInfo> enum_info;
};
@@ -263,7 +265,8 @@ RouteSelection parse_route_selection(std::string route_name) {
if (separator == "/") {
size_t pos = range_str.find(':');
int begin_segment = 0;
if (!parse_segment_number(range_str.substr(0, pos), &begin_segment)) {
const std::string begin_str = range_str.substr(0, pos);
if (!begin_str.empty() && !parse_segment_number(begin_str, &begin_segment)) {
return {};
}
route.begin_segment = begin_segment;
@@ -330,11 +333,7 @@ std::map<int, SegmentLogs> load_segments_from_json(const json11::Json &json) {
}
std::map<int, SegmentLogs> load_segments_from_server(const RouteSelection &route) {
// IQ.Pilot patch: replaces upstream PyDownloader::getRouteFiles with the existing
// iqpilot CommaApi2 helper (same JSON shape: /v1/route/<name>/files).
const std::string url = CommaApi2::BASE_URL + "/v1/route/" + route.canonical_name + "/files";
long response_code = 0;
const std::string result = CommaApi2::httpGet(url, &response_code);
const std::string result = CommaApi2::getRouteFiles(route.canonical_name);
if (result.empty()) throw std::runtime_error("Failed to fetch route files for " + route.canonical_name);
std::string parse_error;
@@ -438,7 +437,7 @@ std::array<uint8_t, 3> parse_color(std::string_view color) {
return out;
}
uint8_t android_priority_to_level(uint8_t priority) {
uint8_t operating_system_priority_to_level(uint8_t priority) {
switch (priority) {
case 2:
case 3:
@@ -497,7 +496,7 @@ void append_timeline_entry(std::vector<TimelineEntry> *timeline, double mono_tim
});
}
double android_wall_time_seconds(uint64_t timestamp) {
double operating_system_wall_time_seconds(uint64_t timestamp) {
if (timestamp == 0) return 0.0;
if (timestamp > 1000000000000ULL) return static_cast<double>(timestamp) / 1.0e9;
if (timestamp > 1000000000ULL) return static_cast<double>(timestamp) / 1.0e6;
@@ -615,13 +614,15 @@ void append_log_event(cereal::Event::Which which,
logs->push_back(std::move(entry));
break;
}
// IQ.Pilot patch: upstream renamed cereal's androidLog -> operatingSystemLog (#38209);
// iqpilot's cereal still carries the original field name. Only the accessor differs.
case cereal::Event::Which::ANDROID_LOG: {
const auto android = event.getAndroidLog();
auto entry = make_entry(LogOrigin::Android, android_priority_to_level(android.getPriority()));
entry.wall_time = android_wall_time_seconds(android.getTs());
entry.source = android.hasTag() ? android.getTag().cStr() : "android";
entry.message = android.hasMessage() ? android.getMessage().cStr() : std::string();
entry.context = "pid=" + std::to_string(android.getPid()) + ", tid=" + std::to_string(android.getTid());
const auto operating_system_log = event.getAndroidLog();
auto entry = make_entry(LogOrigin::OperatingSystem, operating_system_priority_to_level(operating_system_log.getPriority()));
entry.wall_time = operating_system_wall_time_seconds(operating_system_log.getTs());
entry.source = operating_system_log.hasTag() ? operating_system_log.getTag().cStr() : "operating_system";
entry.message = operating_system_log.hasMessage() ? operating_system_log.getMessage().cStr() : std::string();
entry.context = "pid=" + std::to_string(operating_system_log.getPid()) + ", tid=" + std::to_string(operating_system_log.getTid());
if (!entry.message.empty()) {
std::string err;
if (const auto p = json11::Json::parse(entry.message, err); err.empty() && p.is_object()) {
@@ -629,10 +630,10 @@ void append_log_event(cereal::Event::Which which,
if (p["SYSLOG_IDENTIFIER"].is_string() && !p["SYSLOG_IDENTIFIER"].string_value().empty())
entry.source = p["SYSLOG_IDENTIFIER"].string_value();
if (auto pri = json_int_value(p["PRIORITY"]); pri.has_value())
entry.level = android_priority_to_level(*pri);
entry.level = operating_system_priority_to_level(*pri);
if (auto ts = json_u64_value(p["__REALTIME_TIMESTAMP"]); ts.has_value())
entry.wall_time = android_wall_time_seconds(*ts);
entry.context = format_journal_context(p, android.getPid(), android.getTid());
entry.wall_time = operating_system_wall_time_seconds(*ts);
entry.context = format_journal_context(p, operating_system_log.getPid(), operating_system_log.getTid());
}
}
logs->push_back(std::move(entry));
@@ -690,6 +691,25 @@ std::vector<LogEntry> extract_segment_logs(const std::vector<Event> &events) {
return logs;
}
std::vector<ThumbnailFrame> extract_segment_thumbnails(const std::vector<Event> &events, int segment) {
std::vector<ThumbnailFrame> thumbnails;
for (const Event &event_record : events) {
if (event_record.which != cereal::Event::Which::THUMBNAIL) continue;
with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
const auto thumbnail = event.getThumbnail();
const auto jpeg = thumbnail.getThumbnail();
if (jpeg.size() == 0) return;
const uint64_t timestamp = thumbnail.getTimestampEof();
ThumbnailFrame frame;
frame.timestamp = static_cast<double>(timestamp != 0 ? timestamp : event.getLogMonoTime()) / 1.0e9;
frame.segment = segment;
frame.jpeg.assign(jpeg.begin(), jpeg.end());
thumbnails.push_back(std::move(frame));
});
}
return thumbnails;
}
RouteMetadata extract_segment_metadata(const std::vector<Event> &events) {
RouteMetadata metadata;
for (const Event &event_record : events) {
@@ -802,6 +822,8 @@ Pane parse_dock_area(const json11::Json &dock_area_node) {
const std::string kind = dock_area_node["kind"].string_value();
if (kind == "map") {
pane.kind = PaneKind::Map;
} else if (kind == "thumbnail") {
pane.kind = PaneKind::Thumbnail;
} else if (kind == "camera") {
pane.kind = PaneKind::Camera;
const std::string camera_view = dock_area_node["camera_view"].string_value();
@@ -910,7 +932,9 @@ void append_scalar_point(RouteSeries *series,
series->values.push_back(value);
}
void append_fixed_scalar_point(RouteSeries *series, double tm, double value) {
// This has thousands of generated call sites. Inlining it duplicates vector
// growth logic throughout the extractor and is slower both to compile and run.
__attribute__((noinline)) void append_fixed_scalar_point(RouteSeries *series, double tm, double value) {
series->times.push_back(tm);
series->values.push_back(value);
}
@@ -1173,6 +1197,7 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
std::vector<CanMessageData> &&can_messages,
std::vector<LogEntry> &&logs,
std::vector<TimelineEntry> &&timeline,
std::vector<ThumbnailFrame> &&thumbnails,
std::unordered_map<std::string, EnumInfo> &&enum_info,
std::string car_fingerprint,
std::string dbc_name) {
@@ -1239,6 +1264,14 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
route_data.x_min = timeline.front().start_time;
route_data.x_max = timeline.back().end_time;
}
std::sort(thumbnails.begin(), thumbnails.end(), [](const ThumbnailFrame &a, const ThumbnailFrame &b) {
return a.timestamp < b.timestamp;
});
if (!route_data.has_time_range && !thumbnails.empty()) {
route_data.has_time_range = true;
route_data.x_min = thumbnails.front().timestamp;
route_data.x_max = thumbnails.back().timestamp;
}
if (route_data.has_time_range) {
const double time_offset = route_data.x_min;
@@ -1260,6 +1293,9 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
entry.start_time -= time_offset;
entry.end_time -= time_offset;
}
for (ThumbnailFrame &thumbnail : thumbnails) {
thumbnail.timestamp -= time_offset;
}
route_data.x_max -= time_offset;
route_data.x_min = 0.0;
}
@@ -1277,6 +1313,7 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
merged_timeline.push_back(std::move(entry));
}
route_data.timeline = std::move(merged_timeline);
route_data.thumbnails = std::move(thumbnails);
std::sort(can_messages.begin(), can_messages.end(), [](const CanMessageData &a, const CanMessageData &b) {
return std::make_tuple(a.id.service, a.id.bus, a.id.address)
< std::make_tuple(b.id.service, b.id.bus, b.id.address);
@@ -1530,6 +1567,7 @@ LoadedRouteArtifacts load_route_series_parallel(
SeriesAccumulator series;
std::vector<LogEntry> logs;
std::vector<TimelineEntry> timeline;
std::vector<ThumbnailFrame> thumbnails;
};
const std::vector<std::pair<int, SegmentLogs>> segment_list(segments.begin(), segments.end());
@@ -1579,15 +1617,12 @@ LoadedRouteArtifacts load_route_series_parallel(
continue;
}
// IQ.Pilot patch: iqpilot's LogReader does not expose load-time telemetry
// (download/decompress/parse seconds, compressed/decompressed sizes). Stub
// these to 0 so the loader UI still renders without per-segment diagnostics.
segment_stats.download_seconds = 0.0;
segment_stats.decompress_seconds = 0.0;
segment_stats.parse_seconds = 0.0;
segment_stats.compressed_bytes = 0;
segment_stats.decompressed_bytes = 0;
stats->bytes_downloaded.fetch_add(0);
segment_stats.download_seconds = reader.download_seconds();
segment_stats.decompress_seconds = reader.decompress_seconds();
segment_stats.parse_seconds = reader.parse_seconds();
segment_stats.compressed_bytes = reader.compressed_size();
segment_stats.decompressed_bytes = reader.decompressed_size();
stats->bytes_downloaded.fetch_add(reader.compressed_size());
stats->segments_downloaded.fetch_add(1);
stats->publish(RouteLoadStage::DownloadingSegment, index, std::to_string(segment_number));
@@ -1595,6 +1630,7 @@ LoadedRouteArtifacts load_route_series_parallel(
results[index].series = extract_segment_series(reader.events, schema, can_dbc, skip_raw_can, worker_budget, segment_workers);
results[index].logs = extract_segment_logs(reader.events);
results[index].timeline = extract_segment_timeline(reader.events);
results[index].thumbnails = extract_segment_thumbnails(reader.events, segment_number);
segment_stats.extract_seconds = std::chrono::duration<double>(LoadStats::Clock::now() - extract_start).count();
segment_stats.event_count = reader.events.size();
segment_stats.series_count = populated_series_count(results[index].series);
@@ -1621,6 +1657,7 @@ LoadedRouteArtifacts load_route_series_parallel(
}
std::vector<LogEntry> logs;
std::vector<TimelineEntry> timeline;
std::vector<ThumbnailFrame> thumbnails;
for (SegmentResult &result : results) {
if (!result.logs.empty()) {
logs.insert(logs.end(),
@@ -1632,12 +1669,18 @@ LoadedRouteArtifacts load_route_series_parallel(
std::make_move_iterator(result.timeline.begin()),
std::make_move_iterator(result.timeline.end()));
}
if (!result.thumbnails.empty()) {
thumbnails.insert(thumbnails.end(),
std::make_move_iterator(result.thumbnails.begin()),
std::make_move_iterator(result.thumbnails.end()));
}
}
LoadedRouteArtifacts artifacts;
artifacts.series = collect_series(std::move(merged));
artifacts.can_messages = std::move(merged.can_messages);
artifacts.logs = std::move(logs);
artifacts.timeline = std::move(timeline);
artifacts.thumbnails = std::move(thumbnails);
artifacts.enum_info = std::move(merged.enum_info);
stats->merge_end = LoadStats::Clock::now();
return artifacts;
@@ -1843,6 +1886,7 @@ RouteData load_route_data(const std::string &route_name,
std::move(artifacts.can_messages),
std::move(artifacts.logs),
std::move(artifacts.timeline),
std::move(artifacts.thumbnails),
std::move(artifacts.enum_info),
metadata.car_fingerprint,
resolved_dbc);

View File

@@ -0,0 +1,53 @@
import subprocess
from pathlib import Path
import pytest
JOTPLUGGLER_DIR = Path(__file__).parent
JOTPLUGGLER_BIN = JOTPLUGGLER_DIR / "jotpluggler"
pytestmark = pytest.mark.skipif(not JOTPLUGGLER_BIN.exists(),
reason="jotpluggler not built (scons -u tools/jotpluggler)")
def run_jotpluggler(*args):
return subprocess.run([str(JOTPLUGGLER_BIN), *args], cwd=JOTPLUGGLER_DIR,
capture_output=True, text=True, timeout=60)
class TestJotpluggler:
def test_help(self):
result = run_jotpluggler("-h")
assert result.returncode == 0, result.stderr
assert "Usage:" in result.stderr
def test_generated_dbcs_materialized(self):
# the build materializes iqdbc's *_generated.dbc files; layout.cc and
# sketch_layout.cc both resolve DBC names against this directory
generated = JOTPLUGGLER_DIR / "generated_dbcs"
assert generated.is_dir()
assert list(generated.glob("*.dbc")), "no generated DBCs — iqdbc create_all did not run"
def test_car_fingerprint_header_uses_iqdbc_platforms(self):
header = (JOTPLUGGLER_DIR / "car_fingerprint_to_dbc.h").read_text()
assert "kCarFingerprintToDbc" in header
assert "dbc_for_car_fingerprint" in header
def test_bootstrap_icons_vendored(self):
# IQ.Pilot vendors the TTF because third_party/bootstrap is git-lfs; icons.cc
# reads this exact path instead of upstream's BOOTSTRAP_ICONS_TTF define
assert (JOTPLUGGLER_DIR / "assets" / "bootstrap-icons.ttf").is_file()
def test_no_comma_connect_links(self):
# iqpilot routes live on konn3kt; the comma connect / useradmin buttons are gone
common = (JOTPLUGGLER_DIR / "common.cc").read_text()
assert "connect.comma.ai" not in common
assert "useradmin.comma.ai" not in common
assert "route_konn3kt_url" in common
def test_route_files_go_through_konn3kt_api(self):
# upstream calls PyDownloader::getRouteFiles here; iqpilot has no py_downloader
sketch = (JOTPLUGGLER_DIR / "sketch_layout.cc").read_text()
assert "PyDownloader::" not in sketch
assert "py_downloader.h" not in sketch
assert "CommaApi2::getRouteFiles" in sketch

View File

@@ -0,0 +1,253 @@
#include "tools/jotpluggler/thumbnail.h"
#include "imgui_impl_opengl3_loader.h"
#include <algorithm>
#include <cmath>
#include <limits>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/pixfmt.h>
}
namespace {
bool decode_jpeg(const std::vector<uint8_t> &jpeg, int *width, int *height, std::vector<uint8_t> *rgba) {
if (jpeg.empty()) return false;
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG);
AVCodecContext *context = codec != nullptr ? avcodec_alloc_context3(codec) : nullptr;
AVFrame *frame = av_frame_alloc();
AVPacket *packet = av_packet_alloc();
if (context == nullptr || frame == nullptr || packet == nullptr) {
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&context);
return false;
}
const bool packet_ready = jpeg.size() <= static_cast<size_t>(std::numeric_limits<int>::max())
&& av_new_packet(packet, static_cast<int>(jpeg.size())) >= 0;
if (packet_ready) {
std::copy(jpeg.begin(), jpeg.end(), packet->data);
}
const bool decoded = packet_ready
&& avcodec_open2(context, codec, nullptr) >= 0
&& avcodec_send_packet(context, packet) >= 0
&& avcodec_receive_frame(context, frame) >= 0;
if (!decoded || frame->width <= 0 || frame->height <= 0) {
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&context);
return false;
}
int chroma_x_shift = 0;
int chroma_y_shift = 0;
switch (static_cast<AVPixelFormat>(frame->format)) {
case AV_PIX_FMT_YUV420P:
case AV_PIX_FMT_YUVJ420P:
chroma_x_shift = 1;
chroma_y_shift = 1;
break;
case AV_PIX_FMT_YUV422P:
case AV_PIX_FMT_YUVJ422P:
chroma_x_shift = 1;
break;
case AV_PIX_FMT_YUV444P:
case AV_PIX_FMT_YUVJ444P:
break;
default:
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&context);
return false;
}
*width = frame->width;
*height = frame->height;
rgba->resize(static_cast<size_t>(*width) * static_cast<size_t>(*height) * 4U);
const bool full_range = frame->color_range == AVCOL_RANGE_JPEG
|| frame->format == AV_PIX_FMT_YUVJ420P
|| frame->format == AV_PIX_FMT_YUVJ422P
|| frame->format == AV_PIX_FMT_YUVJ444P;
for (int y = 0; y < *height; ++y) {
const uint8_t *y_row = frame->data[0] + y * frame->linesize[0];
const uint8_t *u_row = frame->data[1] + (y >> chroma_y_shift) * frame->linesize[1];
const uint8_t *v_row = frame->data[2] + (y >> chroma_y_shift) * frame->linesize[2];
uint8_t *out = rgba->data() + static_cast<size_t>(y) * static_cast<size_t>(*width) * 4U;
for (int x = 0; x < *width; ++x) {
const double luma = full_range ? static_cast<double>(y_row[x])
: 1.164383 * (static_cast<double>(y_row[x]) - 16.0);
const double u = static_cast<double>(u_row[x >> chroma_x_shift]) - 128.0;
const double v = static_cast<double>(v_row[x >> chroma_x_shift]) - 128.0;
const double red = luma + (full_range ? 1.402 : 1.596027) * v;
const double green = luma - (full_range ? 0.344136 : 0.391762) * u
- (full_range ? 0.714136 : 0.812968) * v;
const double blue = luma + (full_range ? 1.772 : 2.017232) * u;
out[x * 4 + 0] = static_cast<uint8_t>(std::clamp(std::lround(red), 0L, 255L));
out[x * 4 + 1] = static_cast<uint8_t>(std::clamp(std::lround(green), 0L, 255L));
out[x * 4 + 2] = static_cast<uint8_t>(std::clamp(std::lround(blue), 0L, 255L));
out[x * 4 + 3] = 255;
}
}
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&context);
return true;
}
std::string format_thumbnail_time(double seconds) {
const int rounded = std::max(0, static_cast<int>(std::lround(seconds)));
const int hours = rounded / 3600;
const int minutes = (rounded % 3600) / 60;
const int secs = rounded % 60;
if (hours > 0) {
return util::string_format("%d:%02d:%02d", hours, minutes, secs);
}
return util::string_format("%02d:%02d", minutes, secs);
}
} // namespace
struct ThumbnailView::Impl {
~Impl() {
destroy_texture();
}
void setThumbnails(const std::vector<ThumbnailFrame> &next_thumbnails) {
destroy_texture();
thumbnails = &next_thumbnails;
displayed_index = -1;
failed_index = -1;
}
void update(double tracker_time) {
if (thumbnails == nullptr || thumbnails->empty()) return;
auto it = std::lower_bound(thumbnails->begin(), thumbnails->end(), tracker_time,
[](const ThumbnailFrame &frame, double time) {
return frame.timestamp < time;
});
if (it == thumbnails->end()) {
it = std::prev(thumbnails->end());
} else if (it != thumbnails->begin()) {
const auto previous = std::prev(it);
if (std::abs(previous->timestamp - tracker_time) <= std::abs(it->timestamp - tracker_time)) {
it = previous;
}
}
const int index = static_cast<int>(std::distance(thumbnails->begin(), it));
if (index == displayed_index || index == failed_index) return;
int width = 0;
int height = 0;
std::vector<uint8_t> rgba;
if (!decode_jpeg(it->jpeg, &width, &height, &rgba)) {
failed_index = index;
return;
}
if (texture == 0) {
glGenTextures(1, &texture);
}
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data());
glBindTexture(GL_TEXTURE_2D, 0);
texture_width = width;
texture_height = height;
displayed_index = index;
failed_index = -1;
}
void drawSized(ImVec2 size, bool loading) const {
size.x = std::max(1.0f, size.x);
size.y = std::max(1.0f, size.y);
ImGui::InvisibleButton("##thumbnail_sized", size);
const ImVec2 pane_min = ImGui::GetItemRectMin();
const ImVec2 pane_max = ImGui::GetItemRectMax();
ImDrawList *draw_list = ImGui::GetWindowDrawList();
draw_list->AddRectFilled(pane_min, pane_max, IM_COL32(24, 24, 24, 255));
if (texture != 0 && texture_width > 0 && texture_height > 0) {
const float scale = std::min(size.x / static_cast<float>(texture_width),
size.y / static_cast<float>(texture_height));
const ImVec2 image_size(static_cast<float>(texture_width) * scale,
static_cast<float>(texture_height) * scale);
const ImVec2 image_min(pane_min.x + (size.x - image_size.x) * 0.5f,
pane_min.y + (size.y - image_size.y) * 0.5f);
const ImVec2 image_max(image_min.x + image_size.x, image_min.y + image_size.y);
draw_list->AddImage(static_cast<ImTextureID>(texture), image_min, image_max);
if (thumbnails != nullptr && displayed_index >= 0
&& displayed_index < static_cast<int>(thumbnails->size())) {
const ThumbnailFrame &frame = (*thumbnails)[static_cast<size_t>(displayed_index)];
const std::string label = util::string_format("%s · segment %d · %d/%zu",
format_thumbnail_time(frame.timestamp).c_str(),
frame.segment,
displayed_index + 1,
thumbnails->size());
const ImVec2 text_size = ImGui::CalcTextSize(label.c_str());
const ImVec2 label_min(image_min.x, std::max(image_min.y, image_max.y - text_size.y - 14.0f));
draw_list->AddRectFilled(label_min, image_max, IM_COL32(0, 0, 0, 175));
draw_list->AddText(ImVec2(label_min.x + 7.0f, label_min.y + 7.0f), IM_COL32_WHITE, label.c_str());
}
return;
}
const bool has_thumbnails = thumbnails != nullptr && !thumbnails->empty();
const char *label = loading ? "loading" : (has_thumbnails ? "invalid thumbnail" : "no thumbnails");
const ImVec2 text_size = ImGui::CalcTextSize(label);
draw_list->AddText(ImVec2(pane_min.x + (size.x - text_size.x) * 0.5f,
pane_min.y + (size.y - text_size.y) * 0.5f),
IM_COL32(187, 187, 187, 255), label);
}
void destroy_texture() {
if (texture != 0) {
glDeleteTextures(1, &texture);
}
texture = 0;
texture_width = 0;
texture_height = 0;
}
const std::vector<ThumbnailFrame> *thumbnails = nullptr;
int displayed_index = -1;
int failed_index = -1;
GLuint texture = 0;
int texture_width = 0;
int texture_height = 0;
};
ThumbnailView::ThumbnailView() : impl_(std::make_unique<Impl>()) {}
ThumbnailView::~ThumbnailView() = default;
void ThumbnailView::setThumbnails(const std::vector<ThumbnailFrame> &thumbnails) {
impl_->setThumbnails(thumbnails);
}
void ThumbnailView::update(double tracker_time) {
impl_->update(tracker_time);
}
void ThumbnailView::drawSized(ImVec2 size, bool loading) {
impl_->drawSized(size, loading);
}
void draw_thumbnail_pane(AppSession *session, UiState *state) {
if (session->thumbnail_view == nullptr) {
ImGui::TextDisabled("Thumbnails unavailable");
return;
}
if (state->has_tracker_time) {
session->thumbnail_view->update(state->tracker_time);
}
session->thumbnail_view->drawSized(ImGui::GetContentRegionAvail(), session->async_route_loading);
}

View File

@@ -0,0 +1,5 @@
#pragma once
#include "tools/jotpluggler/app.h"
void draw_thumbnail_pane(AppSession *session, UiState *state);

Some files were not shown because too many files have changed in this diff Show More