IQ.Pilot Release Commit @ 661a2de
This commit is contained in:
7
tools/cabana/.gitignore
vendored
7
tools/cabana/.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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 <ipaddress> with your comma device's IP address.
|
||||
Replace <ipaddress> 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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> x </span>,
|
||||
<span style="background-color:lightGray;color:gray"> Backspace </span>,
|
||||
<span style="background-color:lightGray;color:gray"> Delete </span><br />
|
||||
Change endianness: <span style="background-color:lightGray;color:gray"> e </span><br />
|
||||
Change singedness: <span style="background-color:lightGray;color:gray"> s </span><br />
|
||||
Change signedness: <span style="background-color:lightGray;color:gray"> s </span><br />
|
||||
Open chart:
|
||||
<span style="background-color:lightGray;color:gray"> c </span>,
|
||||
<span style="background-color:lightGray;color:gray"> p </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());
|
||||
|
||||
@@ -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
38
tools/cabana/cabana
Executable 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" "$@"
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 *> ¤tCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; }
|
||||
inline std::vector<ChartView *> ¤tCharts() { 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); }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ¬ifier;
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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}
|
||||
};
|
||||
|
||||
46
tools/cabana/core/can_data.h
Normal file
46
tools/cabana/core/can_data.h
Normal 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
79
tools/cabana/core/color.h
Normal 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};
|
||||
}
|
||||
};
|
||||
27
tools/cabana/core/message_id.h
Normal file
27
tools/cabana/core/message_id.h
Normal 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); }
|
||||
};
|
||||
34
tools/cabana/core/settings.h
Normal file
34
tools/cabana/core/settings.h
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
18
tools/cabana/dbc/dbcqt.cc
Normal 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 ¬ifier;
|
||||
}
|
||||
27
tools/cabana/dbc/dbcqt.h
Normal file
27
tools/cabana/dbc/dbcqt.h
Normal 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();
|
||||
@@ -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
53
tools/cabana/deqt.md
Normal 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`
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
110
tools/cabana/konn3kt_canproxy.py
Executable 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
};
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
103
tools/cabana/test_cabana_konn3kt.py
Normal file
103
tools/cabana/test_cabana_konn3kt.py
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
54
tools/iqmacvisiond/README.md
Normal file
54
tools/iqmacvisiond/README.md
Normal 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.
|
||||
23
tools/iqmacvisiond/macos/SIGNING.md
Normal file
23
tools/iqmacvisiond/macos/SIGNING.md
Normal 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).
|
||||
59
tools/iqmacvisiond/macos/build_dmg.sh
Executable file
59
tools/iqmacvisiond/macos/build_dmg.sh
Executable 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))"
|
||||
11
tools/iqmacvisiond/macos/entitlements.plist
Normal file
11
tools/iqmacvisiond/macos/entitlements.plist
Normal 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>
|
||||
21
tools/iqmacvisiond/macos/setup.sh
Executable file
21
tools/iqmacvisiond/macos/setup.sh
Executable 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
80
tools/iqmacvisiond/menubar.py
Executable 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
290
tools/iqmacvisiond/server.py
Executable 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()
|
||||
117
tools/iqmacvisiond/test_offload.py
Executable file
117
tools/iqmacvisiond/test_offload.py
Executable 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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
53
tools/jotpluggler/test_jotpluggler.py
Normal file
53
tools/jotpluggler/test_jotpluggler.py
Normal 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
|
||||
253
tools/jotpluggler/thumbnail.cc
Normal file
253
tools/jotpluggler/thumbnail.cc
Normal 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);
|
||||
}
|
||||
5
tools/jotpluggler/thumbnail.h
Normal file
5
tools/jotpluggler/thumbnail.h
Normal 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
Reference in New Issue
Block a user