IQ.Pilot Release Commit @ 0b96bd5
This commit is contained in:
@@ -25,7 +25,7 @@ import site_init # noqa: F401
|
|||||||
# capnp's kj library warns when $PWD is stale (doesn't match the real cwd); keep them in sync
|
# capnp's kj library warns when $PWD is stale (doesn't match the real cwd); keep them in sync
|
||||||
os.environ.pop('PWD', None)
|
os.environ.pop('PWD', None)
|
||||||
|
|
||||||
Decider('MD5-timestamp')
|
Decider('MD5' if os.uname().machine == 'aarch64' else 'MD5-timestamp')
|
||||||
|
|
||||||
SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
|
SetOption('num_jobs', max(1, int(os.cpu_count()/2)))
|
||||||
|
|
||||||
|
|||||||
@@ -776,6 +776,10 @@ class CarState(CarStateBase):
|
|||||||
ret.steerFaultTemporary, ret.steerFaultPermanent = False, True
|
ret.steerFaultTemporary, ret.steerFaultPermanent = False, True
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.CP.flags & VolkswagenFlags.MLB:
|
||||||
|
# MLB LWS zero is vehicle-specific (measured 2.5 deg off centre on an 8R); the EPS angle is what the rack closes its own loop on
|
||||||
|
ret.steeringAngleDeg = pt_cp.vl["LH_EPS_03"]["EPS_Berechneter_LW"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_BLW"])]
|
||||||
|
|
||||||
ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])]
|
ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])]
|
||||||
ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE
|
ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE
|
||||||
|
|
||||||
|
|||||||
@@ -511,3 +511,42 @@ def test_mlb_carstate_subscriptions_settle_and_stay_alive(build):
|
|||||||
assert ret.canValid
|
assert ret.canValid
|
||||||
assert all(parser.can_valid for parser in car.can_parsers.values())
|
assert all(parser.can_valid for parser in car.can_parsers.values())
|
||||||
assert not any(parser.bus_timeout for parser in car.can_parsers.values())
|
assert not any(parser.bus_timeout for parser in car.can_parsers.values())
|
||||||
|
|
||||||
|
|
||||||
|
def _steering_state(flags, lwi=(0.0, 0), eps=(0.0, 0)):
|
||||||
|
state = object.__new__(CarState)
|
||||||
|
state.CP = SimpleNamespace(flags=flags)
|
||||||
|
state.CCP = SimpleNamespace(STEER_DRIVER_ALLOWANCE=100, hca_status_values={5: "ACTIVE"})
|
||||||
|
state.eps_init_complete = True
|
||||||
|
state.frame = 0
|
||||||
|
pt_cp = SimpleNamespace(vl={
|
||||||
|
"LWI_01": {"LWI_Lenkradwinkel": lwi[0], "LWI_VZ_Lenkradwinkel": lwi[1],
|
||||||
|
"LWI_Lenkradw_Geschw": 4.0, "LWI_VZ_Lenkradw_Geschw": 0},
|
||||||
|
"LH_EPS_03": {"EPS_Berechneter_LW": eps[0], "EPS_VZ_BLW": eps[1],
|
||||||
|
"EPS_Lenkmoment": 0.0, "EPS_VZ_Lenkmoment": 0, "EPS_HCA_Status": 5},
|
||||||
|
})
|
||||||
|
ret = structs.CarState()
|
||||||
|
state.parse_mlb_mqb_steering_state(ret, pt_cp)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def test_mlb_takes_the_steering_angle_from_the_eps():
|
||||||
|
ret = _steering_state(VolkswagenFlags.MLB, lwi=(12.0, 0), eps=(9.6, 0))
|
||||||
|
assert ret.steeringAngleDeg == pytest.approx(9.6)
|
||||||
|
assert ret.steeringRateDeg == pytest.approx(4.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mlb_eps_angle_honours_its_own_sign_bit():
|
||||||
|
ret = _steering_state(VolkswagenFlags.MLB, lwi=(12.0, 0), eps=(9.6, 1))
|
||||||
|
assert ret.steeringAngleDeg == pytest.approx(-9.6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mlb_without_hca_eps_keeps_the_steering_wheel_sensor():
|
||||||
|
flags = VolkswagenFlags.MLB | VolkswagenFlagsIQ.IQ_MLB_NO_HCA_EPS
|
||||||
|
ret = _steering_state(flags, lwi=(12.0, 1), eps=(9.6, 0))
|
||||||
|
assert ret.steeringAngleDeg == pytest.approx(-12.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mqb_keeps_the_steering_wheel_sensor():
|
||||||
|
ret = _steering_state(0, lwi=(12.0, 0), eps=(9.6, 0))
|
||||||
|
assert ret.steeringAngleDeg == pytest.approx(12.0)
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
#include "cereal/services.h"
|
#include "cereal/services.h"
|
||||||
#include "cereal/messaging/messaging.h"
|
#include "cereal/messaging/messaging.h"
|
||||||
|
|
||||||
|
#ifndef SERVICES_REGISTRY_STAMPED
|
||||||
|
static const char SERVICES_REGISTRY_TAG[] = "IQ_SERVICES_REGISTRY:unstamped";
|
||||||
|
#endif
|
||||||
|
|
||||||
const bool SIMULATION = (getenv("SIMULATION") != nullptr) && (std::string(getenv("SIMULATION")) == "1");
|
const bool SIMULATION = (getenv("SIMULATION") != nullptr) && (std::string(getenv("SIMULATION")) == "1");
|
||||||
|
|
||||||
static inline bool inList(const std::vector<const char *> &list, const char *value) {
|
static inline bool inList(const std::vector<const char *> &list, const char *value) {
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ def build_header():
|
|||||||
h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \
|
h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \
|
||||||
(k, k, should_log, v.frequency, decimation, v.queue_size)
|
(k, k, should_log, v.frequency, decimation, v.queue_size)
|
||||||
h += "};\n"
|
h += "};\n"
|
||||||
|
h += "#define SERVICES_REGISTRY_STAMPED 1\n"
|
||||||
h += f'static const char SERVICES_REGISTRY_TAG[] = "{registry_tag()}";\n'
|
h += f'static const char SERVICES_REGISTRY_TAG[] = "{registry_tag()}";\n'
|
||||||
|
|
||||||
h += "#endif\n"
|
h += "#endif\n"
|
||||||
|
|||||||
@@ -81,6 +81,19 @@ def stale_registry_artifacts(basedir: str = BASEDIR) -> list[str]:
|
|||||||
return stale
|
return stale
|
||||||
|
|
||||||
|
|
||||||
|
def purge_stale_registry_header(basedir: str = BASEDIR) -> bool:
|
||||||
|
from iqpilot.cereal.services import registry_tag
|
||||||
|
header = os.path.join(basedir, REGISTRY_ARTIFACTS[0])
|
||||||
|
if not os.path.isfile(header):
|
||||||
|
return False
|
||||||
|
with open(header, "rb") as f:
|
||||||
|
stamped = registry_tag().encode() in f.read()
|
||||||
|
if stamped:
|
||||||
|
return False
|
||||||
|
purge_registry_artifacts([], basedir)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def purge_registry_artifacts(stale: list[str], basedir: str = BASEDIR) -> None:
|
def purge_registry_artifacts(stale: list[str], basedir: str = BASEDIR) -> None:
|
||||||
for rel in set(stale) | set(REGISTRY_ARTIFACTS[:3]):
|
for rel in set(stale) | set(REGISTRY_ARTIFACTS[:3]):
|
||||||
path = os.path.join(basedir, rel)
|
path = os.path.join(basedir, rel)
|
||||||
@@ -101,6 +114,9 @@ def build(spinner, dirty: bool = False, minimal: bool = False, show_error_window
|
|||||||
|
|
||||||
# building with all cores can result in using too
|
# building with all cores can result in using too
|
||||||
# much memory, so retry with less parallelism
|
# much memory, so retry with less parallelism
|
||||||
|
if purge_stale_registry_header():
|
||||||
|
cloudlog.error("generated service registry header was stale, regenerating")
|
||||||
|
|
||||||
compile_output: list[bytes] = []
|
compile_output: list[bytes] = []
|
||||||
for n in get_job_sequence():
|
for n in get_job_sequence():
|
||||||
compile_output.clear()
|
compile_output.clear()
|
||||||
|
|||||||
@@ -43,3 +43,23 @@ def test_purge_removes_the_stale_binary_and_the_messaging_table(tmp_path):
|
|||||||
assert "iqpilot/cereal/messaging/socketmaster.o" not in remaining
|
assert "iqpilot/cereal/messaging/socketmaster.o" not in remaining
|
||||||
assert "iqpilot/cereal/libsocketmaster.a" not in remaining
|
assert "iqpilot/cereal/libsocketmaster.a" not in remaining
|
||||||
assert "iqpilot/system/loggerd/loggerd" in remaining
|
assert "iqpilot/system/loggerd/loggerd" in remaining
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_header_declares_the_stamp_for_the_compile_fallback():
|
||||||
|
assert "#define SERVICES_REGISTRY_STAMPED 1" in build_header()
|
||||||
|
|
||||||
|
|
||||||
|
def test_pre_build_purge_drops_an_unstamped_or_old_header(tmp_path):
|
||||||
|
from iqpilot.system.manager.build import purge_stale_registry_header
|
||||||
|
_write(tmp_path, "iqpilot/cereal/services.h", b"static std::map<std::string, service> services = {};\n")
|
||||||
|
_write(tmp_path, "iqpilot/cereal/messaging/socketmaster.o", b"o")
|
||||||
|
_write(tmp_path, "iqpilot/cereal/libsocketmaster.a", b"a")
|
||||||
|
assert purge_stale_registry_header(str(tmp_path))
|
||||||
|
assert not (tmp_path / "iqpilot/cereal/services.h").exists()
|
||||||
|
assert not (tmp_path / "iqpilot/cereal/messaging/socketmaster.o").exists()
|
||||||
|
assert not (tmp_path / "iqpilot/cereal/libsocketmaster.a").exists()
|
||||||
|
|
||||||
|
_write(tmp_path, "iqpilot/cereal/services.h", build_header().encode())
|
||||||
|
assert not purge_stale_registry_header(str(tmp_path))
|
||||||
|
assert (tmp_path / "iqpilot/cereal/services.h").exists()
|
||||||
|
assert not purge_stale_registry_header(str(tmp_path / "nowhere"))
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ brew "libusb"
|
|||||||
brew "libtool"
|
brew "libtool"
|
||||||
brew "llvm"
|
brew "llvm"
|
||||||
brew "openssl@3.0"
|
brew "openssl@3.0"
|
||||||
brew "qt@5"
|
|
||||||
brew "zeromq"
|
brew "zeromq"
|
||||||
cask "gcc-arm-embedded"
|
cask "gcc-arm-embedded"
|
||||||
brew "portaudio"
|
brew "portaudio"
|
||||||
|
|||||||
10
iqpilot/tools/cabana/.gitignore
vendored
10
iqpilot/tools/cabana/.gitignore
vendored
@@ -1,11 +1,5 @@
|
|||||||
moc_*
|
|
||||||
*.moc
|
|
||||||
*.generated.qrc
|
|
||||||
|
|
||||||
assets.cc
|
|
||||||
bootstrap_icons.cc
|
|
||||||
|
|
||||||
_cabana
|
_cabana
|
||||||
|
*.o
|
||||||
dbc/car_fingerprint_to_dbc.json
|
dbc/car_fingerprint_to_dbc.json
|
||||||
tests/test_cabana
|
|
||||||
tests/test_dbc_core
|
tests/test_dbc_core
|
||||||
|
ui/obj/
|
||||||
|
|||||||
@@ -1,117 +1,21 @@
|
|||||||
# Cabana
|
# 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 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).
|
Cabana is IQ.Pilot's desktop CAN analysis tool. It uses ImGui and GLFW on macOS and Linux without Qt.
|
||||||
|
|
||||||
## Usage Instructions
|
Run it through the project command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ./cabana -h
|
iq cabana
|
||||||
Usage: ./cabana [options] route
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help Displays help on commandline options.
|
|
||||||
--help-all Displays help including Qt specific options.
|
|
||||||
--demo use a demo route instead of providing your own
|
|
||||||
--auto Auto load the route from the best available source (no video):
|
|
||||||
internal, openpilotci, comma_api, car_segments, testing_closet
|
|
||||||
--qcam load qcamera
|
|
||||||
--ecam load wide road camera
|
|
||||||
--msgq read can messages from msgq
|
|
||||||
--panda read can messages from panda
|
|
||||||
--panda-serial <panda-serial> read can messages from panda with given serial
|
|
||||||
--socketcan <socketcan> read can messages from given SocketCAN device
|
|
||||||
--zmq <ip-address> read can messages from zmq at the specified ip-address
|
|
||||||
messages
|
|
||||||
--data_dir <data_dir> local directory with routes
|
|
||||||
--no-vipc do not output video
|
|
||||||
--dbc <dbc> dbc file to open
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
route the drive to replay. find your drives at
|
|
||||||
konn3kt.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
Cabana can open a local route, a Konn3kt route, a Panda, SocketCAN on Linux, local msgq, or a remote ZMQ stream.
|
||||||
|
|
||||||
### Running Cabana in Demo Mode
|
```bash
|
||||||
To run Cabana using a built-in demo route, use the following command:
|
iq cabana "dongle_id|2026-09-02--12-00-00"
|
||||||
|
iq cabana --panda
|
||||||
```shell
|
iq cabana --msgq
|
||||||
cabana --demo
|
iq cabana --zmq 192.168.1.10
|
||||||
|
iq cabana --bridge 192.168.1.10
|
||||||
```
|
```
|
||||||
|
|
||||||
### Loading a Specific Route
|
The executable is built on demand by `iqpilot/tools/cabana/cabana`. IQ.Pilot prebuilt device checkouts intentionally omit desktop analysis tools.
|
||||||
|
|
||||||
To load a specific route for replay, provide the route as an argument:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cabana "5beb9b58bd12b691/0000010a--a51155e496"
|
|
||||||
```
|
|
||||||
|
|
||||||
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 "5beb9b58bd12b691/0000010a--a51155e496" --dcam --ecam
|
|
||||||
```
|
|
||||||
|
|
||||||
### Streaming CAN Messages from a comma Device
|
|
||||||
|
|
||||||
[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 &
|
|
||||||
```
|
|
||||||
|
|
||||||
Then Run Cabana with the device's IP address:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cabana --zmq <ipaddress>
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
After disconnecting from the device, you can replay the logged CAN messages from the stream selector dialog -> browse local route.
|
|
||||||
|
|
||||||
### Streaming CAN Messages from Panda
|
|
||||||
|
|
||||||
To read CAN messages from a connected Panda, use the following command:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cabana --panda
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the Stream Selector Dialog
|
|
||||||
|
|
||||||
If you run Cabana without any arguments, a stream selector dialog will pop up, allowing you to choose the stream.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cabana
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional Information
|
|
||||||
|
|
||||||
For more information, see the [openpilot wiki](https://github.com/commaai/openpilot/wiki/Cabana)
|
|
||||||
|
|||||||
@@ -1,97 +1,19 @@
|
|||||||
import subprocess
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
|
|
||||||
import iqdbc
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
venv_site_packages = os.path.join(Dir("#").abspath, ".venv", "lib", "python3.12", "site-packages")
|
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:
|
if os.path.isdir(venv_site_packages) and venv_site_packages not in sys.path:
|
||||||
sys.path.insert(0, venv_site_packages)
|
sys.path.insert(0, venv_site_packages)
|
||||||
|
|
||||||
|
import imgui
|
||||||
|
import iqdbc
|
||||||
import libusb
|
import libusb
|
||||||
|
from iqpilot.common.basedir import BASEDIR
|
||||||
|
|
||||||
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_libs')
|
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"]
|
|
||||||
|
|
||||||
qt_libs = []
|
|
||||||
if arch == "Darwin":
|
|
||||||
qt_env['QTDIR'] = f"{brew_prefix}/opt/qt@5"
|
|
||||||
qt_dirs = [
|
|
||||||
os.path.join(qt_env['QTDIR'], "include"),
|
|
||||||
]
|
|
||||||
qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules]
|
|
||||||
qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")]
|
|
||||||
qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules]
|
|
||||||
qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin"))
|
|
||||||
else:
|
|
||||||
qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip()
|
|
||||||
qt_install_headers = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_HEADERS'], encoding='utf8').strip()
|
|
||||||
|
|
||||||
qt_env['QTDIR'] = qt_install_prefix
|
|
||||||
qt_dirs = [
|
|
||||||
f"{qt_install_headers}",
|
|
||||||
]
|
|
||||||
|
|
||||||
qt_gui_path = os.path.join(qt_install_headers, "QtGui")
|
|
||||||
qt_gui_dirs = [d for d in os.listdir(qt_gui_path) if os.path.isdir(os.path.join(qt_gui_path, d))]
|
|
||||||
qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else []
|
|
||||||
qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules]
|
|
||||||
|
|
||||||
qt_libs = [f"Qt5{m}" for m in qt_modules]
|
|
||||||
qt_env['QT3DIR'] = qt_env['QTDIR']
|
|
||||||
qt_env.Tool('qt3')
|
|
||||||
|
|
||||||
qt_env['CPPPATH'] += qt_dirs
|
|
||||||
qt_flags = [
|
|
||||||
"-D_REENTRANT",
|
|
||||||
"-DQT_NO_DEBUG",
|
|
||||||
"-DQT_WIDGETS_LIB",
|
|
||||||
"-DQT_GUI_LIB",
|
|
||||||
"-DQT_CORE_LIB",
|
|
||||||
"-DQT_MESSAGELOGCONTEXT",
|
|
||||||
]
|
|
||||||
qt_env['CXXFLAGS'] += qt_flags
|
|
||||||
qt_env['LIBPATH'] += ['#iqpilot/selfdrive/ui', ]
|
|
||||||
qt_env['LIBS'] = qt_libs
|
|
||||||
|
|
||||||
base_frameworks = qt_env['FRAMEWORKS']
|
|
||||||
base_libs = [common, messaging, cereal, visionipc, 'm', 'pthread'] + qt_env["LIBS"]
|
|
||||||
|
|
||||||
if arch == "Darwin":
|
|
||||||
base_frameworks += ['CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox']
|
|
||||||
|
|
||||||
cabana_env = qt_env.Clone()
|
|
||||||
cabana_env['CPPPATH'] += [libusb.INCLUDE_DIR]
|
|
||||||
cabana_env['LIBPATH'] += [libusb.LIB_DIR]
|
|
||||||
|
|
||||||
# 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"\'' % iqdbc.DBC_PATH
|
opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % iqdbc.DBC_PATH
|
||||||
cabana_env['CXXFLAGS'] += [opendbc_path]
|
|
||||||
|
|
||||||
# embed the bootstrap icons SVG into the binary
|
|
||||||
def build_bootstrap_icons_src(target, source, env):
|
def build_bootstrap_icons_src(target, source, env):
|
||||||
data = open(str(source[0]), 'rb').read()
|
data = open(str(source[0]), 'rb').read()
|
||||||
with open(str(target[0]), 'w') as f:
|
with open(str(target[0]), 'w') as f:
|
||||||
@@ -100,48 +22,92 @@ def build_bootstrap_icons_src(target, source, env):
|
|||||||
f.write('extern const size_t bootstrap_icons_svg_len;\n')
|
f.write('extern const size_t bootstrap_icons_svg_len;\n')
|
||||||
f.write('const unsigned char bootstrap_icons_svg[] = {\n')
|
f.write('const unsigned char bootstrap_icons_svg[] = {\n')
|
||||||
for i in range(0, len(data), 32):
|
for i in range(0, len(data), 32):
|
||||||
f.write(','.join(str(b) for b in data[i:i+32]) + ',\n')
|
f.write(','.join(str(b) for b in data[i:i + 32]) + ',\n')
|
||||||
f.write('};\n')
|
f.write('};\n')
|
||||||
f.write('const size_t bootstrap_icons_svg_len = sizeof(bootstrap_icons_svg);\n')
|
f.write('const size_t bootstrap_icons_svg_len = sizeof(bootstrap_icons_svg);\n')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# IQ.Pilot patch: no comma-deps-bootstrap-icons wheel here; the SVG is checked in
|
cabana_env = env.Clone()
|
||||||
# under third_party/bootstrap (see its pull.sh).
|
cabana_env['CPPPATH'] += [imgui.INCLUDE_DIR, libusb.INCLUDE_DIR]
|
||||||
bootstrap_icons_src = cabana_env.Command('assets/bootstrap_icons.cc', '#iqpilot/third_party/bootstrap/bootstrap-icons.svg',
|
cabana_env['LIBPATH'] += [p for p in [imgui.MESA_DIR, libusb.LIB_DIR] if os.path.isdir(p)]
|
||||||
cabana_env.PrettyAction(build_bootstrap_icons_src, 'GEN'))
|
cabana_env['CXXFLAGS'] += [
|
||||||
|
opendbc_path,
|
||||||
|
"-DGLFW_INCLUDE_NONE",
|
||||||
|
'-DCABANA_FONTS_DIR=\'"%s"\'' % os.path.join(os.path.realpath(BASEDIR), "iqpilot", "selfdrive", "assets", "fonts"),
|
||||||
|
'-DBOOTSTRAP_ICONS_TTF=\'"%s"\'' % os.path.join(os.path.realpath(BASEDIR), "iqpilot", "tools", "jotpluggler", "assets", "bootstrap-icons.ttf"),
|
||||||
|
]
|
||||||
|
|
||||||
# build assets
|
bootstrap_icons_src = cabana_env.Command(
|
||||||
assets = "assets/assets.cc"
|
'ui/obj/bootstrap_icons.cc',
|
||||||
cabana_env.Command(assets, "assets/assets.qrc", cabana_env.PrettyAction("rcc $SOURCES -o $TARGET", 'RCC'))
|
'#iqpilot/third_party/bootstrap/bootstrap-icons.svg',
|
||||||
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"]))
|
cabana_env.PrettyAction(build_bootstrap_icons_src, 'GEN'),
|
||||||
|
)
|
||||||
|
|
||||||
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',
|
core_srcs = [
|
||||||
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc',
|
'streams/pandastream.cc',
|
||||||
'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc',
|
'streams/devicestream.cc',
|
||||||
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
|
'streams/livestream.cc',
|
||||||
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc',
|
'streams/abstractstream.cc',
|
||||||
'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc']
|
'streams/replaystream.cc',
|
||||||
|
'dbc/dbc.cc',
|
||||||
|
'dbc/dbcfile.cc',
|
||||||
|
'dbc/dbcmanager.cc',
|
||||||
|
'utils/export.cc',
|
||||||
|
'utils/util.cc',
|
||||||
|
'utils/strings.cc',
|
||||||
|
'commands.cc',
|
||||||
|
'settings.cc',
|
||||||
|
'routes.cc',
|
||||||
|
'panda.cc',
|
||||||
|
]
|
||||||
if arch != "Darwin":
|
if arch != "Darwin":
|
||||||
cabana_srcs += ['streams/socketcanstream.cc']
|
core_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)
|
core_objects = [cabana_env.Object('ui/obj/' + src.replace('/', '_')[:-3], src) for src in core_srcs]
|
||||||
|
ui_objects = core_objects + [cabana_env.Object('ui/obj/bootstrap_icons', bootstrap_icons_src)]
|
||||||
|
ui_objects += cabana_env.Glob('ui/*.cc')
|
||||||
|
ui_objects += cabana_env.Glob('ui/widgets/*.cc')
|
||||||
|
ui_objects += cabana_env.Glob('ui/dialogs/*.cc')
|
||||||
|
ui_objects += cabana_env.Glob('ui/chart/*.cc')
|
||||||
|
ui_objects += cabana_env.Glob('ui/tools/*.cc')
|
||||||
|
|
||||||
|
cabana_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":
|
||||||
|
cabana_env['FRAMEWORKS'] = ['OpenGL', 'OpenCL', 'Cocoa', 'IOKit', 'CoreFoundation', 'CoreVideo', 'CoreMedia', 'Security', 'VideoToolbox']
|
||||||
|
else:
|
||||||
|
cabana_libs += ['GL', 'OpenCL', 'dl']
|
||||||
|
|
||||||
|
cabana = cabana_env.Program('_cabana', ui_objects, LIBS=cabana_libs)
|
||||||
|
|
||||||
if GetOption('extras'):
|
if GetOption('extras'):
|
||||||
# This target deliberately uses the base environment and links no Qt libraries.
|
test_env = env.Clone()
|
||||||
# It prevents Qt dependencies from creeping back into the DBC core.
|
test_env['CXXFLAGS'] += [opendbc_path]
|
||||||
dbc_core_test_env = env.Clone()
|
test_objects = [
|
||||||
dbc_core_test_env['CXXFLAGS'] += [opendbc_path]
|
test_env.Object('tests/dbc_core_tests', 'tests/test_cabana.cc'),
|
||||||
dbc_core_test_objects = [
|
test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'),
|
||||||
dbc_core_test_env.Object('tests/dbc_core_tests', 'tests/test_cabana.cc'),
|
test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'),
|
||||||
dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'),
|
test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'),
|
||||||
dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'),
|
test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'),
|
||||||
dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'),
|
test_env.Object('tests/dbc_core_util', 'utils/util.cc'),
|
||||||
|
test_env.Object('tests/dbc_core_icons', bootstrap_icons_src),
|
||||||
|
test_env.Object('tests/dbc_core_routes', 'routes.cc'),
|
||||||
]
|
]
|
||||||
dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects)
|
test_env.Program('tests/test_dbc_core', test_objects, LIBS=[replay_lib, common, 'curl', 'ssl', 'crypto'])
|
||||||
|
|
||||||
output_json_file = 'iqpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json'
|
output_json_file = 'iqpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json'
|
||||||
generate_dbc = cabana_env.Command('#' + output_json_file,
|
generate_dbc = cabana_env.Command(
|
||||||
['dbc/generate_dbc_json.py'],
|
'#' + output_json_file,
|
||||||
cabana_env.PrettyAction("python3 iqpilot/tools/cabana/dbc/generate_dbc_json.py --out " + output_json_file,
|
['dbc/generate_dbc_json.py'],
|
||||||
'GEN', logfile='${TARGET}.log'))
|
cabana_env.PrettyAction('"%s" iqpilot/tools/cabana/dbc/generate_dbc_json.py --out %s' % (sys.executable, output_json_file), 'GEN', logfile='${TARGET}.log'),
|
||||||
cabana_env.Depends(generate_dbc, ["#iqpilot/common", "#iqpilot/cereal"])
|
)
|
||||||
|
cabana_env.Depends(generate_dbc, ['#iqpilot/common', '#iqpilot/cereal', os.path.dirname(iqdbc.__file__)])
|
||||||
|
cabana_env.Depends(cabana, generate_dbc)
|
||||||
|
|||||||
1
iqpilot/tools/cabana/assets/.gitignore
vendored
1
iqpilot/tools/cabana/assets/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
*.cc
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<!DOCTYPE RCC><RCC version="1.0">
|
|
||||||
<qresource>
|
|
||||||
<file>cabana-icon.png</file>
|
|
||||||
</qresource>
|
|
||||||
</RCC>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,510 +0,0 @@
|
|||||||
#include "tools/cabana/binaryview.h"
|
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
#include <QFontDatabase>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QShortcut>
|
|
||||||
#include <QToolTip>
|
|
||||||
|
|
||||||
#include "tools/cabana/commands.h"
|
|
||||||
|
|
||||||
// BinaryView
|
|
||||||
|
|
||||||
const int CELL_HEIGHT = 36;
|
|
||||||
const int VERTICAL_HEADER_WIDTH = 30;
|
|
||||||
inline int get_bit_pos(const QModelIndex &index) { return flipBitPos(index.row() * 8 + index.column()); }
|
|
||||||
|
|
||||||
BinaryView::BinaryView(QWidget *parent) : QTableView(parent) {
|
|
||||||
model = new BinaryViewModel(this);
|
|
||||||
setModel(model);
|
|
||||||
delegate = new BinaryItemDelegate(this);
|
|
||||||
setItemDelegate(delegate);
|
|
||||||
horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
|
||||||
horizontalHeader()->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
|
||||||
verticalHeader()->setSectionsClickable(false);
|
|
||||||
verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
|
|
||||||
verticalHeader()->setDefaultSectionSize(CELL_HEIGHT);
|
|
||||||
horizontalHeader()->hide();
|
|
||||||
setShowGrid(false);
|
|
||||||
setMouseTracking(true);
|
|
||||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
|
|
||||||
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 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 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>,
|
|
||||||
<span style="background-color:lightGray;color:gray"> g </span>
|
|
||||||
)");
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::addShortcuts() {
|
|
||||||
// Delete (x, backspace, delete)
|
|
||||||
QShortcut *shortcut_delete_x = new QShortcut(QKeySequence(Qt::Key_X), this);
|
|
||||||
QShortcut *shortcut_delete_backspace = new QShortcut(QKeySequence(Qt::Key_Backspace), this);
|
|
||||||
QShortcut *shortcut_delete_delete = new QShortcut(QKeySequence(Qt::Key_Delete), this);
|
|
||||||
QObject::connect(shortcut_delete_delete, &QShortcut::activated, shortcut_delete_x, &QShortcut::activated);
|
|
||||||
QObject::connect(shortcut_delete_backspace, &QShortcut::activated, shortcut_delete_x, &QShortcut::activated);
|
|
||||||
QObject::connect(shortcut_delete_x, &QShortcut::activated, [=]{
|
|
||||||
if (hovered_sig != nullptr) {
|
|
||||||
UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, hovered_sig));
|
|
||||||
hovered_sig = nullptr;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change endianness (e)
|
|
||||||
QShortcut *shortcut_endian = new QShortcut(QKeySequence(Qt::Key_E), this);
|
|
||||||
QObject::connect(shortcut_endian, &QShortcut::activated, [=]{
|
|
||||||
if (hovered_sig != nullptr) {
|
|
||||||
cabana::Signal s = *hovered_sig;
|
|
||||||
s.is_little_endian = !s.is_little_endian;
|
|
||||||
emit editSignal(hovered_sig, s);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change signedness (s)
|
|
||||||
QShortcut *shortcut_sign = new QShortcut(QKeySequence(Qt::Key_S), this);
|
|
||||||
QObject::connect(shortcut_sign, &QShortcut::activated, [=]{
|
|
||||||
if (hovered_sig != nullptr) {
|
|
||||||
cabana::Signal s = *hovered_sig;
|
|
||||||
s.is_signed = !s.is_signed;
|
|
||||||
emit editSignal(hovered_sig, s);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open chart (c, p, g)
|
|
||||||
QShortcut *shortcut_plot = new QShortcut(QKeySequence(Qt::Key_P), this);
|
|
||||||
QShortcut *shortcut_plot_g = new QShortcut(QKeySequence(Qt::Key_G), this);
|
|
||||||
QShortcut *shortcut_plot_c = new QShortcut(QKeySequence(Qt::Key_C), this);
|
|
||||||
QObject::connect(shortcut_plot_g, &QShortcut::activated, shortcut_plot, &QShortcut::activated);
|
|
||||||
QObject::connect(shortcut_plot_c, &QShortcut::activated, shortcut_plot, &QShortcut::activated);
|
|
||||||
QObject::connect(shortcut_plot, &QShortcut::activated, [=]{
|
|
||||||
if (hovered_sig != nullptr) {
|
|
||||||
emit showChart(model->msg_id, hovered_sig, true, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize BinaryView::minimumSizeHint() const {
|
|
||||||
return {(horizontalHeader()->minimumSectionSize() + 1) * 9 + VERTICAL_HEADER_WIDTH + 2,
|
|
||||||
CELL_HEIGHT * std::min(model->rowCount(), 10) + 2};
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::highlight(const cabana::Signal *sig) {
|
|
||||||
if (sig != hovered_sig) {
|
|
||||||
for (int i = 0; i < model->items.size(); ++i) {
|
|
||||||
auto &item_sigs = model->items[i].sigs;
|
|
||||||
auto has = [](const auto &v, auto p) { return std::find(v.begin(), v.end(), p) != v.end(); };
|
|
||||||
if ((sig && has(item_sigs, sig)) || (hovered_sig && has(item_sigs, hovered_sig))) {
|
|
||||||
auto index = model->index(i / model->columnCount(), i % model->columnCount());
|
|
||||||
emit model->dataChanged(index, index, {Qt::DisplayRole});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hovered_sig = sig;
|
|
||||||
emit signalHovered(hovered_sig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) {
|
|
||||||
auto index = indexAt(last_mouse_pos);
|
|
||||||
if (!anchor_index.isValid() || !index.isValid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
QItemSelection selection;
|
|
||||||
auto [start, size, is_lb] = getSelection(index);
|
|
||||||
for (int i = 0; i < size; ++i) {
|
|
||||||
int pos = is_lb ? flipBitPos(start + i) : flipBitPos(start) + i;
|
|
||||||
selection << QItemSelectionRange{model->index(pos / 8, pos % 8)};
|
|
||||||
}
|
|
||||||
selectionModel()->select(selection, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::mousePressEvent(QMouseEvent *event) {
|
|
||||||
resize_sig = nullptr;
|
|
||||||
if (auto index = indexAt(last_mouse_pos = event->pos()); index.isValid() && index.column() != 8) {
|
|
||||||
anchor_index = index;
|
|
||||||
auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer();
|
|
||||||
int bit_pos = get_bit_pos(anchor_index);
|
|
||||||
for (auto s : item->sigs) {
|
|
||||||
if (bit_pos == s->lsb || bit_pos == s->msb) {
|
|
||||||
int idx = flipBitPos(bit_pos == s->lsb ? s->msb : s->lsb);
|
|
||||||
anchor_index = model->index(idx / 8, idx % 8);
|
|
||||||
resize_sig = s;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event->accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::highlightPosition(const QPoint &pos) {
|
|
||||||
if (auto index = indexAt(pos); index.isValid()) {
|
|
||||||
auto item = (BinaryViewModel::Item *)index.internalPointer();
|
|
||||||
const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back();
|
|
||||||
highlight(sig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::mouseMoveEvent(QMouseEvent *event) {
|
|
||||||
highlightPosition(last_mouse_pos = event->pos());
|
|
||||||
QTableView::mouseMoveEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::mouseReleaseEvent(QMouseEvent *event) {
|
|
||||||
QTableView::mouseReleaseEvent(event);
|
|
||||||
|
|
||||||
auto release_index = indexAt(event->pos());
|
|
||||||
if (release_index.isValid() && anchor_index.isValid()) {
|
|
||||||
if (selectionModel()->hasSelection()) {
|
|
||||||
auto sig = resize_sig ? *resize_sig : cabana::Signal{};
|
|
||||||
std::tie(sig.start_bit, sig.size, sig.is_little_endian) = getSelection(release_index);
|
|
||||||
resize_sig ? emit editSignal(resize_sig, sig)
|
|
||||||
: UndoStack::instance()->push(new AddSigCommand(model->msg_id, sig));
|
|
||||||
} else {
|
|
||||||
auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer();
|
|
||||||
if (item && item->sigs.size() > 0)
|
|
||||||
emit signalClicked(item->sigs.back());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clearSelection();
|
|
||||||
anchor_index = QModelIndex();
|
|
||||||
resize_sig = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::leaveEvent(QEvent *event) {
|
|
||||||
highlight(nullptr);
|
|
||||||
QTableView::leaveEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::setMessage(const MessageId &message_id) {
|
|
||||||
model->msg_id = message_id;
|
|
||||||
verticalScrollBar()->setValue(0);
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryView::refresh() {
|
|
||||||
clearSelection();
|
|
||||||
anchor_index = QModelIndex();
|
|
||||||
resize_sig = nullptr;
|
|
||||||
hovered_sig = nullptr;
|
|
||||||
model->refresh();
|
|
||||||
if (underMouse()) highlightPosition(last_mouse_pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
|
|
||||||
std::set<const cabana::Signal *> overlapping;
|
|
||||||
for (const auto &item : model->items) {
|
|
||||||
if (item.sigs.size() > 1) {
|
|
||||||
for (auto s : item.sigs) {
|
|
||||||
if (s->type == cabana::Signal::Type::Normal) overlapping.insert(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return overlapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::tuple<int, int, bool> BinaryView::getSelection(QModelIndex index) {
|
|
||||||
if (index.column() == 8) {
|
|
||||||
index = model->index(index.row(), 7);
|
|
||||||
}
|
|
||||||
bool is_lb = true;
|
|
||||||
if (resize_sig) {
|
|
||||||
is_lb = resize_sig->is_little_endian;
|
|
||||||
} else if (settings.drag_direction == Settings::DragDirection::MsbFirst) {
|
|
||||||
is_lb = index < anchor_index;
|
|
||||||
} else if (settings.drag_direction == Settings::DragDirection::LsbFirst) {
|
|
||||||
is_lb = !(index < anchor_index);
|
|
||||||
} else if (settings.drag_direction == Settings::DragDirection::AlwaysLE) {
|
|
||||||
is_lb = true;
|
|
||||||
} else if (settings.drag_direction == Settings::DragDirection::AlwaysBE) {
|
|
||||||
is_lb = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int cur_bit_pos = get_bit_pos(index);
|
|
||||||
int anchor_bit_pos = get_bit_pos(anchor_index);
|
|
||||||
int start_bit = is_lb ? std::min(cur_bit_pos, anchor_bit_pos) : get_bit_pos(std::min(index, anchor_index));
|
|
||||||
int size = is_lb ? std::abs(cur_bit_pos - anchor_bit_pos) + 1 : std::abs(flipBitPos(cur_bit_pos) - flipBitPos(anchor_bit_pos)) + 1;
|
|
||||||
return {start_bit, size, is_lb};
|
|
||||||
}
|
|
||||||
|
|
||||||
// BinaryViewModel
|
|
||||||
|
|
||||||
void BinaryViewModel::refresh() {
|
|
||||||
beginResetModel();
|
|
||||||
bit_flip_tracker = {};
|
|
||||||
items.clear();
|
|
||||||
if (auto dbc_msg = dbc()->msg(msg_id)) {
|
|
||||||
row_count = dbc_msg->size;
|
|
||||||
items.resize(row_count * column_count);
|
|
||||||
for (auto sig : dbc_msg->getSignals()) {
|
|
||||||
for (int j = 0; j < sig->size; ++j) {
|
|
||||||
int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j;
|
|
||||||
int idx = column_count * (pos / 8) + pos % 8;
|
|
||||||
if (idx >= items.size()) {
|
|
||||||
fprintf(stderr, "signal %s out of bounds.start_bit: %d size: %d\n",
|
|
||||||
sig->name.c_str(), sig->start_bit, sig->size);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (j == 0) sig->is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true;
|
|
||||||
if (j == sig->size - 1) sig->is_little_endian ? items[idx].is_msb = true : items[idx].is_lsb = true;
|
|
||||||
|
|
||||||
auto &sigs = items[idx].sigs;
|
|
||||||
sigs.push_back(sig);
|
|
||||||
if (sigs.size() > 1) {
|
|
||||||
std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) { return l->size > r->size; });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
row_count = can->lastMessage(msg_id).dat.size();
|
|
||||||
items.resize(row_count * column_count);
|
|
||||||
}
|
|
||||||
endResetModel();
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &color) {
|
|
||||||
auto &item = items[row * column_count + col];
|
|
||||||
item.valid = true;
|
|
||||||
if (item.val != val || item.bg_color != color) {
|
|
||||||
item.val = val;
|
|
||||||
item.bg_color = color;
|
|
||||||
auto idx = index(row, col);
|
|
||||||
emit dataChanged(idx, idx, {Qt::DisplayRole});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryViewModel::updateState() {
|
|
||||||
const auto &last_msg = can->lastMessage(msg_id);
|
|
||||||
const auto &binary = last_msg.dat;
|
|
||||||
// Handle size changes in binary data
|
|
||||||
if (binary.size() > row_count) {
|
|
||||||
beginInsertRows({}, row_count, binary.size() - 1);
|
|
||||||
row_count = binary.size();
|
|
||||||
items.resize(row_count * column_count);
|
|
||||||
endInsertRows();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &bit_flips = heatmap_live_mode ? last_msg.bit_flip_counts : getBitFlipChanges(binary.size());
|
|
||||||
// Find the maximum bit flip count across the message
|
|
||||||
uint32_t max_bit_flip_count = 1; // Default to 1 to avoid division by zero
|
|
||||||
for (const auto &row : bit_flips) {
|
|
||||||
for (uint32_t count : row) {
|
|
||||||
max_bit_flip_count = std::max(max_bit_flip_count, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const double max_alpha = 255.0;
|
|
||||||
const double min_alpha_with_signal = 25.0; // Base alpha for small flip counts
|
|
||||||
const double min_alpha_no_signal = 10.0; // Base alpha for small flip counts for no signal bits
|
|
||||||
const double log_factor = 1.0 + 0.2; // Factor for logarithmic scaling
|
|
||||||
const double log_scaler = max_alpha / log2(log_factor * max_bit_flip_count);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < binary.size(); ++i) {
|
|
||||||
for (int j = 0; j < 8; ++j) {
|
|
||||||
auto &item = items[i * column_count + j];
|
|
||||||
int bit_val = (binary[i] >> (7 - j)) & 1;
|
|
||||||
|
|
||||||
double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal;
|
|
||||||
uint32_t flip_count = bit_flips[i][j];
|
|
||||||
if (flip_count > 0) {
|
|
||||||
double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler;
|
|
||||||
double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal;
|
|
||||||
alpha = std::clamp(normalized_alpha, min_alpha, max_alpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto color = item.bg_color;
|
|
||||||
color.setAlpha(alpha);
|
|
||||||
updateItem(i, j, bit_val, color);
|
|
||||||
}
|
|
||||||
updateItem(i, 8, binary[i], toQColor(last_msg.colors[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<std::array<uint32_t, 8>> &BinaryViewModel::getBitFlipChanges(size_t msg_size) {
|
|
||||||
// Return cached results if time range and data are unchanged
|
|
||||||
auto time_range = can->timeRange();
|
|
||||||
if (bit_flip_tracker.time_range == time_range && !bit_flip_tracker.flip_counts.empty())
|
|
||||||
return bit_flip_tracker.flip_counts;
|
|
||||||
|
|
||||||
bit_flip_tracker.time_range = time_range;
|
|
||||||
bit_flip_tracker.flip_counts.assign(msg_size, std::array<uint32_t, 8>{});
|
|
||||||
|
|
||||||
// Iterate over events within the specified time range and calculate bit flips
|
|
||||||
auto [first, last] = can->eventsInRange(msg_id, time_range);
|
|
||||||
if (std::distance(first, last) <= 1) return bit_flip_tracker.flip_counts;
|
|
||||||
|
|
||||||
std::vector<uint8_t> prev_values((*first)->dat, (*first)->dat + (*first)->size);
|
|
||||||
for (auto it = std::next(first); it != last; ++it) {
|
|
||||||
const CanEvent *event = *it;
|
|
||||||
int size = std::min<int>(msg_size, event->size);
|
|
||||||
for (int i = 0; i < size; ++i) {
|
|
||||||
const uint8_t diff = event->dat[i] ^ prev_values[i];
|
|
||||||
if (!diff) continue;
|
|
||||||
|
|
||||||
auto &bit_flips = bit_flip_tracker.flip_counts[i];
|
|
||||||
for (int bit = 0; bit < 8; ++bit) {
|
|
||||||
if (diff & (1u << bit)) ++bit_flips[7 - bit];
|
|
||||||
}
|
|
||||||
prev_values[i] = event->dat[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bit_flip_tracker.flip_counts;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
||||||
if (orientation == Qt::Vertical) {
|
|
||||||
switch (role) {
|
|
||||||
case Qt::DisplayRole: return section;
|
|
||||||
case Qt::SizeHintRole: return QSize(VERTICAL_HEADER_WIDTH, 0);
|
|
||||||
case Qt::TextAlignmentRole: return Qt::AlignCenter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant BinaryViewModel::data(const QModelIndex &index, int role) const {
|
|
||||||
auto item = (const BinaryViewModel::Item *)index.internalPointer();
|
|
||||||
return role == Qt::ToolTipRole && item && !item->sigs.empty() ? signalToolTip(item->sigs.back()) : QVariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
// BinaryItemDelegate
|
|
||||||
|
|
||||||
BinaryItemDelegate::BinaryItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {
|
|
||||||
small_font.setPixelSize(8);
|
|
||||||
hex_font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
|
||||||
hex_font.setBold(true);
|
|
||||||
|
|
||||||
bin_text_table[0].setText("0");
|
|
||||||
bin_text_table[1].setText("1");
|
|
||||||
for (int i = 0; i < 256; ++i) {
|
|
||||||
hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper());
|
|
||||||
hex_text_table[i].prepare({}, hex_font);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BinaryItemDelegate::hasSignal(const QModelIndex &index, int dx, int dy, const cabana::Signal *sig) const {
|
|
||||||
if (!index.isValid()) return false;
|
|
||||||
auto model = (const BinaryViewModel*)(index.model());
|
|
||||||
int idx = (index.row() + dy) * model->columnCount() + index.column() + dx;
|
|
||||||
if (idx < 0 || idx >= (int)model->items.size()) return false;
|
|
||||||
auto &s = model->items[idx].sigs;
|
|
||||||
return std::find(s.begin(), s.end(), sig) != s.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
|
||||||
auto item = (const BinaryViewModel::Item *)index.internalPointer();
|
|
||||||
BinaryView *bin_view = (BinaryView *)parent();
|
|
||||||
painter->save();
|
|
||||||
|
|
||||||
if (index.column() == 8) {
|
|
||||||
if (item->valid) {
|
|
||||||
painter->setFont(hex_font);
|
|
||||||
painter->fillRect(option.rect, item->bg_color);
|
|
||||||
}
|
|
||||||
} else if (option.state & QStyle::State_Selected) {
|
|
||||||
auto color = bin_view->resize_sig ? toQColor(bin_view->resize_sig->color) : option.palette.color(QPalette::Active, QPalette::Highlight);
|
|
||||||
painter->fillRect(option.rect, color);
|
|
||||||
painter->setPen(option.palette.color(QPalette::BrightText));
|
|
||||||
} else if (!bin_view->selectionModel()->hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), bin_view->resize_sig) == item->sigs.end()) { // not resizing
|
|
||||||
if (item->sigs.size() > 0) {
|
|
||||||
for (auto &s : item->sigs) {
|
|
||||||
if (s == bin_view->hovered_sig) {
|
|
||||||
painter->fillRect(option.rect, toQColor(s->color.darker(125))); // 4/5x brightness
|
|
||||||
} else {
|
|
||||||
drawSignalCell(painter, option, index, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (item->valid && item->bg_color.alpha() > 0) {
|
|
||||||
painter->fillRect(option.rect, item->bg_color);
|
|
||||||
}
|
|
||||||
auto color_role = (std::find(item->sigs.begin(), item->sigs.end(), bin_view->hovered_sig) != item->sigs.end()) ? QPalette::BrightText : QPalette::Text;
|
|
||||||
painter->setPen(option.palette.color(bin_view->is_message_active ? QPalette::Normal : QPalette::Disabled, color_role));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item->sigs.size() > 1) {
|
|
||||||
painter->fillRect(option.rect, QBrush(Qt::darkGray, Qt::Dense7Pattern));
|
|
||||||
} else if (!item->valid) {
|
|
||||||
painter->fillRect(option.rect, QBrush(Qt::darkGray, Qt::BDiagPattern));
|
|
||||||
}
|
|
||||||
if (item->valid) {
|
|
||||||
utils::drawStaticText(painter, option.rect, index.column() == 8 ? hex_text_table[item->val] : bin_text_table[item->val]);
|
|
||||||
}
|
|
||||||
if (item->is_msb || item->is_lsb) {
|
|
||||||
painter->setFont(small_font);
|
|
||||||
painter->drawText(option.rect.adjusted(8, 0, -8, -3), Qt::AlignRight | Qt::AlignBottom, item->is_msb ? "M" : "L");
|
|
||||||
}
|
|
||||||
painter->restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw border on edge of signal
|
|
||||||
void BinaryItemDelegate::drawSignalCell(QPainter *painter, const QStyleOptionViewItem &option,
|
|
||||||
const QModelIndex &index, const cabana::Signal *sig) const {
|
|
||||||
bool draw_left = !hasSignal(index, -1, 0, sig);
|
|
||||||
bool draw_top = !hasSignal(index, 0, -1, sig);
|
|
||||||
bool draw_right = !hasSignal(index, 1, 0, sig);
|
|
||||||
bool draw_bottom = !hasSignal(index, 0, 1, sig);
|
|
||||||
|
|
||||||
const int spacing = 2;
|
|
||||||
QRect rc = option.rect.adjusted(draw_left * 3, draw_top * spacing, draw_right * -3, draw_bottom * -spacing);
|
|
||||||
QRegion subtract;
|
|
||||||
if (!draw_top) {
|
|
||||||
if (!draw_left && !hasSignal(index, -1, -1, sig)) {
|
|
||||||
subtract += QRect{rc.left(), rc.top(), 3, spacing};
|
|
||||||
} else if (!draw_right && !hasSignal(index, 1, -1, sig)) {
|
|
||||||
subtract += QRect{rc.right() - 2, rc.top(), 3, spacing};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!draw_bottom) {
|
|
||||||
if (!draw_left && !hasSignal(index, -1, 1, sig)) {
|
|
||||||
subtract += QRect{rc.left(), rc.bottom() - (spacing - 1), 3, spacing};
|
|
||||||
} else if (!draw_right && !hasSignal(index, 1, 1, sig)) {
|
|
||||||
subtract += QRect{rc.right() - 2, rc.bottom() - (spacing - 1), 3, spacing};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
painter->setClipRegion(QRegion(rc).subtracted(subtract));
|
|
||||||
|
|
||||||
auto item = (const BinaryViewModel::Item *)index.internalPointer();
|
|
||||||
QColor color = toQColor(sig->color);
|
|
||||||
color.setAlpha(item->bg_color.alpha());
|
|
||||||
// Mixing the signal color with the Base background color to fade it
|
|
||||||
painter->fillRect(rc, option.palette.color(QPalette::Base));
|
|
||||||
painter->fillRect(rc, color);
|
|
||||||
|
|
||||||
// Draw edges
|
|
||||||
color = toQColor(sig->color.darker(125));
|
|
||||||
painter->setPen(QPen(color, 1));
|
|
||||||
if (draw_left) painter->drawLine(rc.topLeft(), rc.bottomLeft());
|
|
||||||
if (draw_right) painter->drawLine(rc.topRight(), rc.bottomRight());
|
|
||||||
if (draw_bottom) painter->drawLine(rc.bottomLeft(), rc.bottomRight());
|
|
||||||
if (draw_top) painter->drawLine(rc.topLeft(), rc.topRight());
|
|
||||||
|
|
||||||
if (!subtract.isEmpty()) {
|
|
||||||
// fill gaps inside corners.
|
|
||||||
painter->setPen(QPen(color, 2, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin));
|
|
||||||
for (auto &r : subtract) {
|
|
||||||
painter->drawRect(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <set>
|
|
||||||
#include <tuple>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QStyledItemDelegate>
|
|
||||||
#include <QTableView>
|
|
||||||
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
class BinaryItemDelegate : public QStyledItemDelegate {
|
|
||||||
public:
|
|
||||||
BinaryItemDelegate(QObject *parent);
|
|
||||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
|
||||||
bool hasSignal(const QModelIndex &index, int dx, int dy, const cabana::Signal *sig) const;
|
|
||||||
void drawSignalCell(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index, const cabana::Signal *sig) const;
|
|
||||||
|
|
||||||
QFont small_font, hex_font;
|
|
||||||
std::array<QStaticText, 256> hex_text_table;
|
|
||||||
std::array<QStaticText, 2> bin_text_table;
|
|
||||||
};
|
|
||||||
|
|
||||||
class BinaryViewModel : public QAbstractTableModel {
|
|
||||||
public:
|
|
||||||
BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {}
|
|
||||||
void refresh();
|
|
||||||
void updateState();
|
|
||||||
void updateItem(int row, int col, uint8_t val, const QColor &color);
|
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; }
|
|
||||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; }
|
|
||||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override {
|
|
||||||
return createIndex(row, column, (void *)&items[row * column_count + column]);
|
|
||||||
}
|
|
||||||
Qt::ItemFlags flags(const QModelIndex &index) const override {
|
|
||||||
return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
|
||||||
}
|
|
||||||
const std::vector<std::array<uint32_t, 8>> &getBitFlipChanges(size_t msg_size);
|
|
||||||
|
|
||||||
struct BitFlipTracker {
|
|
||||||
std::optional<std::pair<double, double>> time_range;
|
|
||||||
std::vector<std::array<uint32_t, 8>> flip_counts;
|
|
||||||
} bit_flip_tracker;
|
|
||||||
|
|
||||||
struct Item {
|
|
||||||
QColor bg_color = QColor(102, 86, 169, 255);
|
|
||||||
bool is_msb = false;
|
|
||||||
bool is_lsb = false;
|
|
||||||
uint8_t val;
|
|
||||||
std::vector<const cabana::Signal *> sigs;
|
|
||||||
bool valid = false;
|
|
||||||
};
|
|
||||||
std::vector<Item> items;
|
|
||||||
bool heatmap_live_mode = true;
|
|
||||||
MessageId msg_id;
|
|
||||||
int row_count = 0;
|
|
||||||
const int column_count = 9;
|
|
||||||
};
|
|
||||||
|
|
||||||
class BinaryView : public QTableView {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
BinaryView(QWidget *parent = nullptr);
|
|
||||||
void setMessage(const MessageId &message_id);
|
|
||||||
void highlight(const cabana::Signal *sig);
|
|
||||||
std::set<const cabana::Signal*> getOverlappingSignals() const;
|
|
||||||
void updateState() { model->updateState(); }
|
|
||||||
void paintEvent(QPaintEvent *event) override {
|
|
||||||
is_message_active = can->isMessageActive(model->msg_id);
|
|
||||||
QTableView::paintEvent(event);
|
|
||||||
}
|
|
||||||
QSize minimumSizeHint() const override;
|
|
||||||
void setHeatmapLiveMode(bool live) { model->heatmap_live_mode = live; updateState(); }
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void signalClicked(const cabana::Signal *sig);
|
|
||||||
void signalHovered(const cabana::Signal *sig);
|
|
||||||
void editSignal(const cabana::Signal *origin_s, cabana::Signal &s);
|
|
||||||
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void addShortcuts();
|
|
||||||
void refresh();
|
|
||||||
std::tuple<int, int, bool> getSelection(QModelIndex index);
|
|
||||||
void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) override;
|
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
|
||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
|
||||||
void leaveEvent(QEvent *event) override;
|
|
||||||
void highlightPosition(const QPoint &pt);
|
|
||||||
|
|
||||||
QModelIndex anchor_index;
|
|
||||||
QPoint last_mouse_pos{-1, -1};
|
|
||||||
BinaryViewModel *model;
|
|
||||||
BinaryItemDelegate *delegate;
|
|
||||||
bool is_message_active = false;
|
|
||||||
const cabana::Signal *resize_sig = nullptr;
|
|
||||||
const cabana::Signal *hovered_sig = nullptr;
|
|
||||||
friend class BinaryItemDelegate;
|
|
||||||
};
|
|
||||||
@@ -1,38 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ROOT="$(cd "$DIR/../../../" && pwd)"
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||||
|
|
||||||
install_qt() {
|
cd "$REPO_ROOT"
|
||||||
if [[ "$(uname)" == "Darwin" ]]; then
|
uv run --extra tools scons -u iqpilot/tools/cabana/_cabana iqpilot/cereal/messaging/bridge
|
||||||
brew install qt@5
|
exec "$SCRIPT_DIR/_cabana" "$@"
|
||||||
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 iqpilot/tools/cabana/_cabana iqpilot/cereal/messaging/bridge
|
|
||||||
|
|
||||||
exec "$DIR/_cabana" "$@"
|
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
#include "tools/cabana/cameraview.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QPainter>
|
|
||||||
|
|
||||||
#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), QWidget(parent) {
|
|
||||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
|
||||||
qRegisterMetaType<std::set<VisionStreamType>>("availableStreams");
|
|
||||||
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() {
|
|
||||||
stopVipcThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CameraWidget::showEvent(QShowEvent *event) {
|
|
||||||
if (!vipc_thread.joinable()) {
|
|
||||||
clearFrames();
|
|
||||||
vipc_exit = false;
|
|
||||||
vipc_thread = std::thread(&CameraWidget::vipcThread, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CameraWidget::stopVipcThread() {
|
|
||||||
vipc_exit = true;
|
|
||||||
if (vipc_thread.joinable()) {
|
|
||||||
vipc_thread.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CameraWidget::availableStreamsUpdated(std::set<VisionStreamType> streams) {
|
|
||||||
available_streams = streams;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CameraWidget::paintEvent(QPaintEvent *event) {
|
|
||||||
QPainter p(this);
|
|
||||||
p.fillRect(rect(), bg);
|
|
||||||
|
|
||||||
std::lock_guard lk(frame_lock);
|
|
||||||
if (rgb_frame.isNull()) return;
|
|
||||||
|
|
||||||
// Scale for aspect ratio
|
|
||||||
float widget_ratio = (float)width() / height();
|
|
||||||
float frame_ratio = (float)rgb_frame.width() / rgb_frame.height();
|
|
||||||
int w = std::lround(width() * std::min(frame_ratio / widget_ratio, 1.0f));
|
|
||||||
int h = std::lround(height() * std::min(widget_ratio / frame_ratio, 1.0f));
|
|
||||||
QRect video_rect((width() - w) / 2, (height() - h) / 2, w, h);
|
|
||||||
|
|
||||||
p.setRenderHint(QPainter::SmoothPixmapTransform);
|
|
||||||
if (active_stream_type == VISION_STREAM_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() {
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CameraWidget::vipcThread() {
|
|
||||||
VisionStreamType cur_stream = requested_stream_type;
|
|
||||||
std::unique_ptr<VisionIpcClient> vipc_client;
|
|
||||||
VisionIpcBufExtra frame_meta = {};
|
|
||||||
|
|
||||||
while (!vipc_exit) {
|
|
||||||
if (!vipc_client || cur_stream != requested_stream_type) {
|
|
||||||
clearFrames();
|
|
||||||
fprintf(stderr, "connecting to stream %d, was connected to %d\n",
|
|
||||||
(int)requested_stream_type, (int)cur_stream);
|
|
||||||
cur_stream = requested_stream_type;
|
|
||||||
vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false));
|
|
||||||
}
|
|
||||||
active_stream_type = cur_stream;
|
|
||||||
|
|
||||||
if (!vipc_client->connected) {
|
|
||||||
clearFrames();
|
|
||||||
auto streams = VisionIpcClient::getAvailableStreams(stream_name, false);
|
|
||||||
if (streams.empty()) {
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
emit vipcAvailableStreamsUpdated(streams);
|
|
||||||
|
|
||||||
if (!vipc_client->connect(false)) {
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) {
|
|
||||||
// NV12 -> RGBA once per frame on the receive thread; paint just draws the image
|
|
||||||
if (rgb_back.width() != (int)buf->width || rgb_back.height() != (int)buf->height) {
|
|
||||||
rgb_back = QImage(buf->width, buf->height, QImage::Format_RGBA8888);
|
|
||||||
}
|
|
||||||
yuv::nv12_to_rgba(buf->y, buf->stride, buf->uv, buf->stride,
|
|
||||||
rgb_back.bits(), rgb_back.bytesPerLine(), buf->width, buf->height);
|
|
||||||
{
|
|
||||||
std::lock_guard lk(frame_lock);
|
|
||||||
rgb_frame.swap(rgb_back);
|
|
||||||
}
|
|
||||||
emit vipcThreadFrameReceived();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CameraWidget::clearFrames() {
|
|
||||||
std::lock_guard lk(frame_lock);
|
|
||||||
rgb_frame = QImage();
|
|
||||||
rgb_back = QImage();
|
|
||||||
available_streams.clear();
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <mutex>
|
|
||||||
#include <set>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QImage>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
#include "cereal/visionstream.h"
|
|
||||||
#include "msgq/visionipc/visionipc_client.h"
|
|
||||||
|
|
||||||
class CameraWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr);
|
|
||||||
~CameraWidget();
|
|
||||||
void setStreamType(VisionStreamType type) { requested_stream_type = type; }
|
|
||||||
VisionStreamType getStreamType() { return active_stream_type; }
|
|
||||||
void stopVipcThread();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void clicked();
|
|
||||||
void vipcThreadFrameReceived();
|
|
||||||
void vipcAvailableStreamsUpdated(std::set<VisionStreamType>);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
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();
|
|
||||||
|
|
||||||
QColor bg = Qt::black;
|
|
||||||
QImage rgb_frame; // written by vipc thread, drawn by GUI thread; guarded by frame_lock
|
|
||||||
QImage rgb_back; // vipc thread only
|
|
||||||
|
|
||||||
std::string stream_name;
|
|
||||||
std::atomic<VisionStreamType> active_stream_type;
|
|
||||||
std::atomic<VisionStreamType> requested_stream_type;
|
|
||||||
std::set<VisionStreamType> available_streams;
|
|
||||||
std::thread vipc_thread;
|
|
||||||
std::atomic<bool> vipc_exit = false;
|
|
||||||
std::mutex frame_lock;
|
|
||||||
|
|
||||||
protected slots:
|
|
||||||
void vipcFrameReceived();
|
|
||||||
void availableStreamsUpdated(std::set<VisionStreamType> streams);
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(std::set<VisionStreamType>);
|
|
||||||
@@ -1,770 +0,0 @@
|
|||||||
#include "tools/cabana/chart/chart.h"
|
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <limits>
|
|
||||||
#include <random>
|
|
||||||
|
|
||||||
#include <QActionGroup>
|
|
||||||
#include <QContextMenuEvent>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QPainterPath>
|
|
||||||
|
|
||||||
#include "tools/cabana/chart/chartswidget.h"
|
|
||||||
|
|
||||||
const int AXIS_X_TOP_MARGIN = 4;
|
|
||||||
const int X_TICK_COUNT = 5;
|
|
||||||
const double MIN_ZOOM_SECONDS = 0.01; // 10ms
|
|
||||||
// Define a small value of epsilon to compare double values
|
|
||||||
const float EPSILON = 0.000001;
|
|
||||||
static inline bool xLessThan(const QPointF &p, float x) { return p.x() < (x - EPSILON); }
|
|
||||||
|
|
||||||
static QMargins layoutMargins(const QStyle *style) {
|
|
||||||
return {
|
|
||||||
style->pixelMetric(QStyle::PM_LayoutLeftMargin),
|
|
||||||
style->pixelMetric(QStyle::PM_LayoutTopMargin),
|
|
||||||
style->pixelMetric(QStyle::PM_LayoutRightMargin),
|
|
||||||
style->pixelMetric(QStyle::PM_LayoutBottomMargin),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent)
|
|
||||||
: x_min(x_range.first), x_max(x_range.second), charts_widget(parent), QWidget(parent) {
|
|
||||||
series_type = (SeriesType)settings.chart_series_type;
|
|
||||||
align_to = 50;
|
|
||||||
setMouseTracking(true);
|
|
||||||
tip_label = new TipLabel(this);
|
|
||||||
createToolButtons();
|
|
||||||
signal_value_font.setPointSize(9);
|
|
||||||
|
|
||||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &ChartView::signalRemoved);
|
|
||||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &ChartView::signalUpdated);
|
|
||||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &ChartView::msgRemoved);
|
|
||||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &ChartView::msgUpdated);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::createToolButtons() {
|
|
||||||
close_btn = new ToolButton("x", tr("Remove Chart"), this);
|
|
||||||
|
|
||||||
menu = new QMenu(this);
|
|
||||||
// series types
|
|
||||||
auto change_series_group = new QActionGroup(menu);
|
|
||||||
change_series_group->setExclusive(true);
|
|
||||||
QStringList types{tr("Line"), tr("Step Line"), tr("Scatter")};
|
|
||||||
for (int i = 0; i < types.size(); ++i) {
|
|
||||||
QAction *act = new QAction(types[i], change_series_group);
|
|
||||||
act->setData(i);
|
|
||||||
act->setCheckable(true);
|
|
||||||
act->setChecked(i == (int)series_type);
|
|
||||||
menu->addAction(act);
|
|
||||||
}
|
|
||||||
menu->addSeparator();
|
|
||||||
menu->addAction(tr("Manage Signals"), this, &ChartView::manageSignals);
|
|
||||||
split_chart_act = menu->addAction(tr("Split Chart"), [this]() { charts_widget->splitChart(this); });
|
|
||||||
|
|
||||||
manage_btn = new ToolButton("list", "", this);
|
|
||||||
manage_btn->setMenu(menu);
|
|
||||||
manage_btn->setPopupMode(QToolButton::InstantPopup);
|
|
||||||
manage_btn->setStyleSheet("QToolButton::menu-indicator { image: none; }");
|
|
||||||
|
|
||||||
close_act = new QAction(tr("Close"), this);
|
|
||||||
QObject::connect(close_act, &QAction::triggered, [this] () { charts_widget->removeChart(this); });
|
|
||||||
QObject::connect(close_btn, &QToolButton::clicked, close_act, &QAction::triggered);
|
|
||||||
QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) {
|
|
||||||
setSeriesType((SeriesType)action->data().toInt());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize ChartView::sizeHint() const {
|
|
||||||
return {CHART_MIN_WIDTH, settings.chart_height};
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) {
|
|
||||||
if (hasSignal(msg_id, sig)) return;
|
|
||||||
|
|
||||||
sigs.push_back({.msg_id = msg_id, .sig = sig, .color = uniqueColor(toQColor(sig->color))});
|
|
||||||
updateSeries(sig);
|
|
||||||
updateTitle();
|
|
||||||
emit charts_widget->seriesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const {
|
|
||||||
return std::any_of(sigs.cbegin(), sigs.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::removeIf(std::function<bool(const SigItem &s)> predicate) {
|
|
||||||
int prev_size = sigs.size();
|
|
||||||
sigs.erase(std::remove_if(sigs.begin(), sigs.end(), predicate), sigs.end());
|
|
||||||
if (sigs.empty()) {
|
|
||||||
charts_widget->removeChart(this);
|
|
||||||
} else if (sigs.size() != prev_size) {
|
|
||||||
emit charts_widget->seriesChanged();
|
|
||||||
updateAxisY();
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::signalUpdated(const cabana::Signal *sig) {
|
|
||||||
auto it = std::find_if(sigs.begin(), sigs.end(), [sig](auto &s) { return s.sig == sig; });
|
|
||||||
if (it != sigs.end()) {
|
|
||||||
if (it->color != toQColor(sig->color)) {
|
|
||||||
it->color = uniqueColor(toQColor(sig->color), sig);
|
|
||||||
}
|
|
||||||
updateTitle();
|
|
||||||
updateSeries(sig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::msgUpdated(MessageId id) {
|
|
||||||
if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id.address == id.address; })) {
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::manageSignals() {
|
|
||||||
SignalSelector dlg(tr("Manage Chart"), this);
|
|
||||||
for (auto &s : sigs) {
|
|
||||||
dlg.addSelected(s.msg_id, s.sig);
|
|
||||||
}
|
|
||||||
if (dlg.exec() == QDialog::Accepted) {
|
|
||||||
auto items = dlg.seletedItems();
|
|
||||||
for (auto s : items) {
|
|
||||||
addSignal(s->msg_id, s->sig);
|
|
||||||
}
|
|
||||||
removeIf([&](auto &s) {
|
|
||||||
return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it->msg_id && s.sig == it->sig; });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::resizeEvent(QResizeEvent *event) {
|
|
||||||
QWidget::resizeEvent(event);
|
|
||||||
const auto margins = layoutMargins(style());
|
|
||||||
QPixmap grip = utils::icon("grip-horizontal");
|
|
||||||
move_icon_rect = QRect(QPoint(margins.left(), margins.top()), grip.size() / grip.devicePixelRatio());
|
|
||||||
close_btn->resize(close_btn->sizeHint());
|
|
||||||
manage_btn->resize(manage_btn->sizeHint());
|
|
||||||
close_btn->move(rect().right() - margins.right() - close_btn->width(), margins.top());
|
|
||||||
manage_btn->move(close_btn->x() - manage_btn->width() - style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), margins.top());
|
|
||||||
updatePlotArea(align_to, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::updatePlotArea(int left_pos, bool force) {
|
|
||||||
if (align_to != left_pos || force) {
|
|
||||||
align_to = left_pos;
|
|
||||||
|
|
||||||
const auto margins = layoutMargins(style());
|
|
||||||
QFont bold_font = font();
|
|
||||||
bold_font.setBold(true);
|
|
||||||
QFontMetrics fm(font()), bfm(bold_font);
|
|
||||||
const int marker_size = fm.height() - 4;
|
|
||||||
const int row_height = std::max(marker_size, fm.height()) + QFontMetrics(signal_value_font).height() + 3;
|
|
||||||
const int legend_left = move_icon_rect.right() + margins.left();
|
|
||||||
const int legend_right = std::max(manage_btn->x() - margins.right(), legend_left + 10);
|
|
||||||
|
|
||||||
// layout legend entries left-to-right, wrapping between the move icon and the buttons
|
|
||||||
legend_rects.clear();
|
|
||||||
int x = legend_left, y = margins.top();
|
|
||||||
for (auto &s : sigs) {
|
|
||||||
int w = marker_size + 5 + bfm.horizontalAdvance(QString::fromStdString(s.sig->name)) +
|
|
||||||
fm.horizontalAdvance(QString::fromStdString(" " + msgName(s.msg_id) + " " + s.msg_id.toString()));
|
|
||||||
w = std::min(w, legend_right - legend_left); // keep oversized entries clear of the header buttons
|
|
||||||
if (x + w > legend_right && x > legend_left) {
|
|
||||||
x = legend_left;
|
|
||||||
y += row_height;
|
|
||||||
}
|
|
||||||
legend_rects.emplace_back(x, y, w, std::max(marker_size, fm.height()));
|
|
||||||
x += w + 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add top space for the legend and signal values
|
|
||||||
int adjust_top = (y + row_height) - margins.top();
|
|
||||||
adjust_top = std::max(adjust_top, manage_btn->geometry().bottom() + style()->pixelMetric(QStyle::PM_LayoutTopMargin));
|
|
||||||
// add right space for x-axis label
|
|
||||||
QSizeF x_label_size = fm.size(Qt::TextSingleLine, QString::number(x_max, 'f', xAxisPrecision())) + QSizeF{5, 5};
|
|
||||||
plot_area = rect().adjusted(align_to + margins.left(), adjust_top + margins.top(),
|
|
||||||
-x_label_size.width() / 2 - margins.right(),
|
|
||||||
-x_label_size.height() - margins.bottom());
|
|
||||||
resetChartCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::updateTitle() {
|
|
||||||
split_chart_act->setEnabled(sigs.size() > 1);
|
|
||||||
updatePlotArea(align_to, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::updatePlot(double cur, double min, double max) {
|
|
||||||
cur_sec = cur;
|
|
||||||
if (min != x_min || max != x_max) {
|
|
||||||
x_min = min;
|
|
||||||
x_max = max;
|
|
||||||
updateAxisY();
|
|
||||||
// update tooltip
|
|
||||||
if (tooltip_x >= 0) {
|
|
||||||
showTip(secondsAtPoint({tooltip_x, 0}));
|
|
||||||
}
|
|
||||||
resetChartCache();
|
|
||||||
}
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
|
||||||
std::vector<QPointF> &vals, std::vector<QPointF> &step_vals) {
|
|
||||||
vals.reserve(vals.size() + events.capacity());
|
|
||||||
step_vals.reserve(step_vals.size() + events.capacity() * 2);
|
|
||||||
|
|
||||||
double value = 0;
|
|
||||||
for (const CanEvent *e : events) {
|
|
||||||
if (sig->getValue(e->dat, e->size, &value)) {
|
|
||||||
const double ts = can->toSeconds(e->mono_time);
|
|
||||||
vals.emplace_back(ts, value);
|
|
||||||
if (!step_vals.empty())
|
|
||||||
step_vals.emplace_back(ts, step_vals.back().y());
|
|
||||||
step_vals.emplace_back(ts, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *msg_new_events) {
|
|
||||||
for (auto &s : sigs) {
|
|
||||||
if (!sig || s.sig == sig) {
|
|
||||||
if (!msg_new_events) {
|
|
||||||
s.vals.clear();
|
|
||||||
s.step_vals.clear();
|
|
||||||
}
|
|
||||||
auto events = msg_new_events ? msg_new_events : &can->eventsMap();
|
|
||||||
auto it = events->find(s.msg_id);
|
|
||||||
if (it == events->end() || it->second.empty()) continue;
|
|
||||||
|
|
||||||
if (s.vals.empty() || can->toSeconds(it->second.back()->mono_time) > s.vals.back().x()) {
|
|
||||||
appendCanEvents(s.sig, it->second, s.vals, s.step_vals);
|
|
||||||
} else {
|
|
||||||
std::vector<QPointF> vals, step_vals;
|
|
||||||
appendCanEvents(s.sig, it->second, vals, step_vals);
|
|
||||||
s.vals.insert(std::lower_bound(s.vals.begin(), s.vals.end(), vals.front().x(), xLessThan),
|
|
||||||
vals.begin(), vals.end());
|
|
||||||
s.step_vals.insert(std::lower_bound(s.step_vals.begin(), s.step_vals.end(), step_vals.front().x(), xLessThan),
|
|
||||||
step_vals.begin(), step_vals.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!can->liveStreaming()) {
|
|
||||||
s.segment_tree.build(s.vals);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateAxisY();
|
|
||||||
// invoke resetChartCache in ui thread
|
|
||||||
QMetaObject::invokeMethod(this, &ChartView::resetChartCache, Qt::QueuedConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// auto zoom on yaxis
|
|
||||||
void ChartView::updateAxisY() {
|
|
||||||
if (sigs.empty()) return;
|
|
||||||
|
|
||||||
double min = std::numeric_limits<double>::max();
|
|
||||||
double max = std::numeric_limits<double>::lowest();
|
|
||||||
QString unit = QString::fromStdString(sigs[0].sig->unit);
|
|
||||||
|
|
||||||
for (auto &s : sigs) {
|
|
||||||
if (!s.visible) continue;
|
|
||||||
|
|
||||||
// Only show unit when all signals have the same unit
|
|
||||||
if (unit != QString::fromStdString(s.sig->unit)) {
|
|
||||||
unit.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), x_min, xLessThan);
|
|
||||||
auto last = std::lower_bound(first, s.vals.cend(), x_max, xLessThan);
|
|
||||||
s.min = std::numeric_limits<double>::max();
|
|
||||||
s.max = std::numeric_limits<double>::lowest();
|
|
||||||
if (can->liveStreaming()) {
|
|
||||||
for (auto it = first; it != last; ++it) {
|
|
||||||
if (it->y() < s.min) s.min = it->y();
|
|
||||||
if (it->y() > s.max) s.max = it->y();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last));
|
|
||||||
}
|
|
||||||
min = std::min(min, s.min);
|
|
||||||
max = std::max(max, s.max);
|
|
||||||
}
|
|
||||||
if (min == std::numeric_limits<double>::max()) min = 0;
|
|
||||||
if (max == std::numeric_limits<double>::lowest()) max = 0;
|
|
||||||
|
|
||||||
if (y_unit != unit) {
|
|
||||||
y_unit = unit;
|
|
||||||
y_label_width = 0; // recalc width
|
|
||||||
}
|
|
||||||
|
|
||||||
double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05;
|
|
||||||
auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3);
|
|
||||||
if (min_y != y_min || max_y != y_max || y_label_width == 0) {
|
|
||||||
y_min = min_y;
|
|
||||||
y_max = max_y;
|
|
||||||
y_tick_count = tick_count;
|
|
||||||
y_precision = std::max(int(-std::floor(std::log10((max_y - min_y) / (tick_count - 1)))), 0);
|
|
||||||
|
|
||||||
QFontMetrics fm(font());
|
|
||||||
int max_label_width = 0;
|
|
||||||
for (int i = 0; i < tick_count; i++) {
|
|
||||||
qreal value = min_y + (i * (max_y - min_y) / (tick_count - 1));
|
|
||||||
max_label_width = std::max(max_label_width, fm.horizontalAdvance(QString::number(value, 'f', y_precision)));
|
|
||||||
}
|
|
||||||
|
|
||||||
int title_spacing = y_unit.isEmpty() ? 0 : fm.size(Qt::TextSingleLine, y_unit).height();
|
|
||||||
y_label_width = title_spacing + max_label_width + 15;
|
|
||||||
emit axisYLabelWidthChanged(y_label_width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::tuple<double, double, int> ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) {
|
|
||||||
qreal range = niceNumber((max - min), true); // range with ceiling
|
|
||||||
qreal step = niceNumber(range / (tick_count - 1), false);
|
|
||||||
min = std::floor(min / step);
|
|
||||||
max = std::ceil(max / step);
|
|
||||||
tick_count = int(max - min) + 1;
|
|
||||||
return {min * step, max * step, tick_count};
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChartView::xAxisPrecision() const {
|
|
||||||
return std::max(int(-std::floor(std::log10((x_max - x_min) / (X_TICK_COUNT - 1)))), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n
|
|
||||||
qreal ChartView::niceNumber(qreal x, bool ceiling) {
|
|
||||||
qreal z = std::pow(10, std::floor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x
|
|
||||||
qreal q = x / z; //q<10 && q>=1;
|
|
||||||
if (ceiling) {
|
|
||||||
if (q <= 1.0) q = 1;
|
|
||||||
else if (q <= 2.0) q = 2;
|
|
||||||
else if (q <= 5.0) q = 5;
|
|
||||||
else q = 10;
|
|
||||||
} else {
|
|
||||||
if (q < 1.5) q = 1;
|
|
||||||
else if (q < 3.0) q = 2;
|
|
||||||
else if (q < 7.0) q = 5;
|
|
||||||
else q = 10;
|
|
||||||
}
|
|
||||||
return q * z;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::contextMenuEvent(QContextMenuEvent *event) {
|
|
||||||
QMenu context_menu(this);
|
|
||||||
context_menu.addActions(menu->actions());
|
|
||||||
context_menu.addSeparator();
|
|
||||||
context_menu.addAction(charts_widget->undo_zoom_action);
|
|
||||||
context_menu.addAction(charts_widget->redo_zoom_action);
|
|
||||||
context_menu.addSeparator();
|
|
||||||
context_menu.addAction(close_act);
|
|
||||||
context_menu.exec(event->globalPos());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::mousePressEvent(QMouseEvent *event) {
|
|
||||||
press_pos = event->pos();
|
|
||||||
if (event->button() == Qt::LeftButton && move_icon_rect.contains(event->pos())) {
|
|
||||||
charts_widget->startChartDrag(this, event->globalPos());
|
|
||||||
} else if (event->button() == Qt::LeftButton && event->modifiers().testFlag(Qt::ShiftModifier)) {
|
|
||||||
// Save current playback state when scrubbing
|
|
||||||
resume_after_scrub = !can->isPaused();
|
|
||||||
if (resume_after_scrub) {
|
|
||||||
can->pause(true);
|
|
||||||
}
|
|
||||||
mouse_mode = MouseMode::Scrub;
|
|
||||||
} else if (event->button() == Qt::LeftButton && plot_area.contains(event->pos())) {
|
|
||||||
mouse_mode = MouseMode::Rubber;
|
|
||||||
rubber_rect = QRect();
|
|
||||||
} else {
|
|
||||||
QWidget::mousePressEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::mouseMoveEvent(QMouseEvent *ev) {
|
|
||||||
// Scrubbing
|
|
||||||
if (mouse_mode == MouseMode::Scrub && ev->modifiers().testFlag(Qt::ShiftModifier)) {
|
|
||||||
if (plot_area.contains(ev->pos())) {
|
|
||||||
can->seekTo(std::clamp(secondsAtPoint(ev->pos()), can->minSeconds(), can->maxSeconds()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mouse_mode == MouseMode::Rubber) {
|
|
||||||
// horizontal selection, clamped to the plot area
|
|
||||||
int left = std::clamp(std::min(press_pos.x(), ev->pos().x()), plot_area.left(), plot_area.right());
|
|
||||||
int right = std::clamp(std::max(press_pos.x(), ev->pos().x()), plot_area.left(), plot_area.right());
|
|
||||||
rubber_rect = QRect(left, plot_area.top(), right - left, plot_area.height());
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTrackPoints();
|
|
||||||
if (mouse_mode != MouseMode::Rubber && plot_area.contains(ev->pos()) && isActiveWindow()) {
|
|
||||||
charts_widget->showValueTip(secondsAtPoint(ev->pos()));
|
|
||||||
} else if (tip_label->isVisible()) {
|
|
||||||
charts_widget->showValueTip(-1);
|
|
||||||
}
|
|
||||||
QWidget::mouseMoveEvent(ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::mouseReleaseEvent(QMouseEvent *event) {
|
|
||||||
if (event->button() == Qt::LeftButton && mouse_mode == MouseMode::Rubber) {
|
|
||||||
mouse_mode = MouseMode::None;
|
|
||||||
// Prevent zooming/seeking past the end of the route
|
|
||||||
double min = std::clamp(secondsAtPoint(rubber_rect.topLeft()), can->minSeconds(), can->maxSeconds());
|
|
||||||
double max = std::clamp(secondsAtPoint(rubber_rect.bottomRight()), can->minSeconds(), can->maxSeconds());
|
|
||||||
if (rubber_rect.width() <= 0) {
|
|
||||||
// no rubber dragged, seek to mouse position
|
|
||||||
can->seekTo(std::clamp(secondsAtPoint(press_pos), can->minSeconds(), can->maxSeconds()));
|
|
||||||
} else if (rubber_rect.width() > 10 && (max - min) > MIN_ZOOM_SECONDS) {
|
|
||||||
charts_widget->zoom_undo_stack.push(new ZoomCommand({min, max}));
|
|
||||||
}
|
|
||||||
rubber_rect = QRect();
|
|
||||||
update();
|
|
||||||
} else if (event->button() == Qt::LeftButton && mouse_mode == MouseMode::None && sigs.size() > 1) {
|
|
||||||
// toggle series visibility by clicking its legend entry
|
|
||||||
for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) {
|
|
||||||
if (legend_rects[i].contains(press_pos) && legend_rects[i].contains(event->pos())) {
|
|
||||||
sigs[i].visible = !sigs[i].visible;
|
|
||||||
updateAxisY();
|
|
||||||
updateTitle();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (event->button() == Qt::RightButton) {
|
|
||||||
charts_widget->zoom_undo_stack.undo();
|
|
||||||
} else {
|
|
||||||
QWidget::mouseReleaseEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resume playback if we were scrubbing
|
|
||||||
if (mouse_mode == MouseMode::Scrub) {
|
|
||||||
mouse_mode = MouseMode::None;
|
|
||||||
if (resume_after_scrub) {
|
|
||||||
can->pause(false);
|
|
||||||
resume_after_scrub = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::takeSignalsFrom(ChartView *source) {
|
|
||||||
for (auto &s : source->sigs) {
|
|
||||||
sigs.push_back(std::move(s));
|
|
||||||
sigs.back().color = uniqueColor(sigs.back().color, sigs.back().sig);
|
|
||||||
}
|
|
||||||
source->sigs.clear();
|
|
||||||
updateAxisY();
|
|
||||||
updateTitle();
|
|
||||||
charts_widget->removeChart(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::showTip(double sec) {
|
|
||||||
QRect tip_area(0, plot_area.top(), rect().width(), plot_area.height());
|
|
||||||
QRect visible_rect = charts_widget->chartVisibleRect(this).intersected(tip_area);
|
|
||||||
if (visible_rect.isEmpty()) {
|
|
||||||
tip_label->hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip_x = xPos(sec);
|
|
||||||
qreal x = -1;
|
|
||||||
QStringList text_list;
|
|
||||||
for (auto &s : sigs) {
|
|
||||||
if (s.visible) {
|
|
||||||
QString value = "--";
|
|
||||||
// use reverse iterator to find last item <= sec.
|
|
||||||
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double v) { return p.x() > v; });
|
|
||||||
if (it != s.vals.crend() && it->x() >= x_min) {
|
|
||||||
value = QString::fromStdString(s.sig->formatValue(it->y(), false));
|
|
||||||
s.track_pt = *it;
|
|
||||||
x = std::max(x, xPos(it->x()));
|
|
||||||
}
|
|
||||||
QString name = sigs.size() > 1 ? QString::fromStdString(s.sig->name) + ": " : "";
|
|
||||||
QString min = s.min == std::numeric_limits<double>::max() ? "--" : QString::number(s.min);
|
|
||||||
QString max = s.max == std::numeric_limits<double>::lowest() ? "--" : QString::number(s.max);
|
|
||||||
text_list << QString("<span style=\"color:%1;\">■ </span>%2<b>%3</b> (%4, %5)")
|
|
||||||
.arg(s.color.name(), name, value, min, max);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (x < 0) {
|
|
||||||
x = tooltip_x;
|
|
||||||
}
|
|
||||||
QPoint pt(x, plot_area.top());
|
|
||||||
text_list.push_front(QString::number(secondsAtPoint({x, 0}), 'f', 3));
|
|
||||||
QString text = "<p style='white-space:pre'>" % text_list.join("<br />") % "</p>";
|
|
||||||
tip_label->showText(pt, text, this, visible_rect);
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::hideTip() {
|
|
||||||
clearTrackPoints();
|
|
||||||
tooltip_x = -1;
|
|
||||||
tip_label->hide();
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::resetChartCache() {
|
|
||||||
chart_pixmap = QPixmap();
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::paintEvent(QPaintEvent *event) {
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHints(QPainter::Antialiasing);
|
|
||||||
|
|
||||||
// the static layer is invalidated on x-range change and data merge, so cache it in live mode too
|
|
||||||
const qreal dpr = devicePixelRatioF();
|
|
||||||
if (chart_pixmap.isNull() || chart_pixmap.size() != size() * dpr) {
|
|
||||||
chart_pixmap = QPixmap(size() * dpr);
|
|
||||||
chart_pixmap.setDevicePixelRatio(dpr);
|
|
||||||
QPainter p(&chart_pixmap);
|
|
||||||
p.setRenderHints(QPainter::Antialiasing);
|
|
||||||
p.setFont(font());
|
|
||||||
drawStaticLayer(&p);
|
|
||||||
}
|
|
||||||
painter.drawPixmap(QPoint(), chart_pixmap);
|
|
||||||
|
|
||||||
if (can_drop) {
|
|
||||||
painter.setPen(QPen(palette().color(QPalette::Highlight), 4));
|
|
||||||
painter.drawRect(rect());
|
|
||||||
}
|
|
||||||
drawForeground(&painter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::drawStaticLayer(QPainter *painter) {
|
|
||||||
painter->fillRect(rect(), palette().color(QPalette::Base));
|
|
||||||
painter->drawPixmap(move_icon_rect.topLeft(), utils::icon("grip-horizontal"));
|
|
||||||
drawAxes(painter);
|
|
||||||
drawLegend(painter);
|
|
||||||
drawSeries(painter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::drawAxes(QPainter *painter) {
|
|
||||||
const QColor text_color = palette().color(QPalette::Text);
|
|
||||||
QColor grid_color = text_color;
|
|
||||||
grid_color.setAlpha(50);
|
|
||||||
QFontMetrics fm(font());
|
|
||||||
painter->setFont(font());
|
|
||||||
|
|
||||||
// y grid lines and tick labels
|
|
||||||
for (int i = 0; i < y_tick_count; ++i) {
|
|
||||||
double value = y_min + i * (y_max - y_min) / (y_tick_count - 1);
|
|
||||||
qreal y = yPos(value);
|
|
||||||
painter->setPen(grid_color);
|
|
||||||
painter->drawLine(QPointF(plot_area.left(), y), QPointF(plot_area.right(), y));
|
|
||||||
painter->setPen(text_color);
|
|
||||||
QRectF label_rect(0, y - fm.height() / 2.0, plot_area.left() - 6, fm.height());
|
|
||||||
painter->drawText(label_rect, Qt::AlignRight | Qt::AlignVCenter, QString::number(value, 'f', y_precision));
|
|
||||||
}
|
|
||||||
|
|
||||||
// rotated y axis title (unit)
|
|
||||||
if (!y_unit.isEmpty()) {
|
|
||||||
painter->save();
|
|
||||||
painter->translate(plot_area.left() - y_label_width + fm.height() / 2.0, plot_area.center().y());
|
|
||||||
painter->rotate(-90);
|
|
||||||
painter->drawText(QRectF(-plot_area.height() / 2.0, -fm.height() / 2.0, plot_area.height(), fm.height()),
|
|
||||||
Qt::AlignCenter, y_unit);
|
|
||||||
painter->restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// x grid lines and tick labels
|
|
||||||
const int x_precision = xAxisPrecision();
|
|
||||||
for (int i = 0; i < X_TICK_COUNT; ++i) {
|
|
||||||
double sec = x_min + i * (x_max - x_min) / (X_TICK_COUNT - 1);
|
|
||||||
qreal x = xPos(sec);
|
|
||||||
painter->setPen(grid_color);
|
|
||||||
painter->drawLine(QPointF(x, plot_area.top()), QPointF(x, plot_area.bottom()));
|
|
||||||
painter->setPen(text_color);
|
|
||||||
QString label = QString::number(sec, 'f', x_precision);
|
|
||||||
QRectF label_rect(x - 100, plot_area.bottom() + AXIS_X_TOP_MARGIN, 200, fm.height());
|
|
||||||
painter->drawText(label_rect, Qt::AlignHCenter | Qt::AlignTop, label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::drawLegend(QPainter *painter) {
|
|
||||||
QColor title_color = palette().color(QPalette::WindowText);
|
|
||||||
// Draw message details in similar color, but slightly fade it to the background
|
|
||||||
QColor msg_color = title_color;
|
|
||||||
msg_color.setAlpha(180);
|
|
||||||
QFont bold_font = font();
|
|
||||||
bold_font.setBold(true);
|
|
||||||
const int marker_size = QFontMetrics(font()).height() - 4;
|
|
||||||
|
|
||||||
for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) {
|
|
||||||
const auto &s = sigs[i];
|
|
||||||
const QRect &r = legend_rects[i];
|
|
||||||
painter->setPen(Qt::NoPen);
|
|
||||||
painter->setBrush(s.color);
|
|
||||||
QRectF marker_rect(r.left(), r.center().y() - marker_size / 2.0, marker_size, marker_size);
|
|
||||||
series_type == SeriesType::Scatter ? painter->drawEllipse(marker_rect) : painter->drawRect(marker_rect);
|
|
||||||
|
|
||||||
bold_font.setStrikeOut(!s.visible);
|
|
||||||
QFont normal_font = font();
|
|
||||||
normal_font.setStrikeOut(!s.visible);
|
|
||||||
|
|
||||||
qreal x = r.left() + marker_size + 5;
|
|
||||||
painter->setFont(bold_font);
|
|
||||||
painter->setPen(title_color);
|
|
||||||
QString name = QFontMetrics(bold_font).elidedText(QString::fromStdString(s.sig->name), Qt::ElideRight, r.right() - x);
|
|
||||||
painter->drawText(QRectF(x, r.top(), r.right() - x, r.height()), Qt::AlignLeft | Qt::AlignVCenter, name);
|
|
||||||
x += QFontMetrics(bold_font).horizontalAdvance(name);
|
|
||||||
painter->setFont(normal_font);
|
|
||||||
painter->setPen(msg_color);
|
|
||||||
QString msg = QFontMetrics(normal_font).elidedText(QString::fromStdString(" " + msgName(s.msg_id) + " " + s.msg_id.toString()),
|
|
||||||
Qt::ElideRight, r.right() - x);
|
|
||||||
painter->drawText(QRectF(x, r.top(), r.right() - x, r.height()), Qt::AlignLeft | Qt::AlignVCenter, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::drawSeries(QPainter *painter) {
|
|
||||||
painter->save();
|
|
||||||
painter->setClipRect(plot_area);
|
|
||||||
for (auto &s : sigs) {
|
|
||||||
if (!s.visible) continue;
|
|
||||||
|
|
||||||
// visible points in vals to compute point density
|
|
||||||
auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), x_min, xLessThan);
|
|
||||||
auto last = std::lower_bound(first, s.vals.cend(), x_max, xLessThan);
|
|
||||||
int num_points = std::max<int>(last - first, 1);
|
|
||||||
double pixels_per_point = 0;
|
|
||||||
if (first != last) {
|
|
||||||
const QPointF &right_pt = last == s.vals.cend() ? s.vals.back() : *last;
|
|
||||||
pixels_per_point = (xPos(right_pt.x()) - xPos(first->x())) / num_points;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (series_type == SeriesType::Scatter) {
|
|
||||||
qreal radius = std::clamp(pixels_per_point / 2.0, 2.0, 8.0) / 2.0;
|
|
||||||
painter->setPen(Qt::NoPen);
|
|
||||||
painter->setBrush(s.color);
|
|
||||||
for (auto it = first; it != last; ++it) {
|
|
||||||
painter->drawEllipse(QPointF(xPos(it->x()), yPos(it->y())), radius, radius);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals;
|
|
||||||
auto begin = std::lower_bound(points.cbegin(), points.cend(), x_min, xLessThan);
|
|
||||||
if (begin != points.cbegin()) --begin;
|
|
||||||
auto end = std::lower_bound(begin, points.cend(), x_max, xLessThan);
|
|
||||||
if (end != points.cend()) ++end;
|
|
||||||
if (begin == end) continue;
|
|
||||||
|
|
||||||
std::vector<QPointF> polyline;
|
|
||||||
polyline.reserve(end - begin);
|
|
||||||
for (auto it = begin; it != end; ++it) {
|
|
||||||
polyline.emplace_back(xPos(it->x()), yPos(it->y()));
|
|
||||||
}
|
|
||||||
painter->setPen(QPen(s.color, 2));
|
|
||||||
painter->setBrush(Qt::NoBrush);
|
|
||||||
painter->drawPolyline(polyline.data(), polyline.size());
|
|
||||||
|
|
||||||
// show points when zoomed in enough
|
|
||||||
if (num_points == 1 || pixels_per_point > 20) {
|
|
||||||
painter->setPen(Qt::NoPen);
|
|
||||||
painter->setBrush(s.color);
|
|
||||||
for (auto it = first; it != last; ++it) {
|
|
||||||
painter->drawEllipse(QPointF(xPos(it->x()), yPos(it->y())), 4, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
painter->restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::drawForeground(QPainter *painter) {
|
|
||||||
drawTimeline(painter);
|
|
||||||
drawSignalValue(painter);
|
|
||||||
// draw track points
|
|
||||||
painter->setPen(Qt::NoPen);
|
|
||||||
qreal track_line_x = -1;
|
|
||||||
for (auto &s : sigs) {
|
|
||||||
if (!s.track_pt.isNull() && s.visible) {
|
|
||||||
painter->setBrush(s.color.darker(125));
|
|
||||||
QPointF pos(xPos(s.track_pt.x()), yPos(s.track_pt.y()));
|
|
||||||
painter->drawEllipse(pos, 5.5, 5.5);
|
|
||||||
track_line_x = std::max(track_line_x, pos.x());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (track_line_x > 0) {
|
|
||||||
painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine));
|
|
||||||
painter->drawLine(QPointF{track_line_x, (qreal)plot_area.top()}, QPointF{track_line_x, (qreal)plot_area.bottom()});
|
|
||||||
}
|
|
||||||
|
|
||||||
drawRubberBandTimeRange(painter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::drawRubberBandTimeRange(QPainter *painter) {
|
|
||||||
if (rubber_rect.width() <= 1) return;
|
|
||||||
|
|
||||||
// selection rect
|
|
||||||
QColor highlight = palette().color(QPalette::Highlight);
|
|
||||||
QColor fill = highlight;
|
|
||||||
fill.setAlpha(50);
|
|
||||||
painter->fillRect(rubber_rect, fill);
|
|
||||||
painter->setPen(highlight);
|
|
||||||
painter->setBrush(Qt::NoBrush);
|
|
||||||
painter->drawRect(rubber_rect);
|
|
||||||
|
|
||||||
// time labels at the bottom corners
|
|
||||||
painter->setPen(Qt::white);
|
|
||||||
painter->setFont(font());
|
|
||||||
for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) {
|
|
||||||
QString sec = QString::number(secondsAtPoint(pt), 'f', 2);
|
|
||||||
auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN);
|
|
||||||
pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2});
|
|
||||||
painter->fillRect(r, Qt::gray);
|
|
||||||
painter->drawText(r, Qt::AlignCenter, sec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::drawTimeline(QPainter *painter) {
|
|
||||||
// draw vertical time line
|
|
||||||
qreal x = std::clamp(xPos(cur_sec), (qreal)plot_area.left(), (qreal)plot_area.right());
|
|
||||||
painter->setPen(QPen(palette().color(QPalette::Text), 1));
|
|
||||||
painter->drawLine(QPointF{x, plot_area.top() - 1.0}, QPointF{x, plot_area.bottom() + 1.0});
|
|
||||||
|
|
||||||
// draw current time under the axis-x
|
|
||||||
QString time_str = QString::number(cur_sec, 'f', 2);
|
|
||||||
QSize time_str_size = QFontMetrics(font()).size(Qt::TextSingleLine, time_str) + QSize(8, 2);
|
|
||||||
QRectF time_str_rect(QPointF(x - time_str_size.width() / 2.0, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size);
|
|
||||||
QPainterPath path;
|
|
||||||
path.addRoundedRect(time_str_rect, 3, 3);
|
|
||||||
painter->fillPath(path, utils::isDarkTheme() ? Qt::darkGray : Qt::gray);
|
|
||||||
painter->setPen(palette().color(QPalette::BrightText));
|
|
||||||
painter->setFont(font());
|
|
||||||
painter->drawText(time_str_rect, Qt::AlignCenter, time_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::drawSignalValue(QPainter *painter) {
|
|
||||||
painter->setFont(signal_value_font);
|
|
||||||
painter->setPen(palette().color(QPalette::Text));
|
|
||||||
for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) {
|
|
||||||
const auto &s = sigs[i];
|
|
||||||
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec,
|
|
||||||
[](auto &p, double x) { return p.x() > x + EPSILON; });
|
|
||||||
QString value = (it != s.vals.crend() && it->x() >= x_min) ? QString::fromStdString(s.sig->formatValue(it->y())) : "--";
|
|
||||||
QRectF value_rect(legend_rects[i].bottomLeft() - QPoint(0, 1), legend_rects[i].size());
|
|
||||||
QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width());
|
|
||||||
painter->drawText(value_rect, Qt::AlignHCenter | Qt::AlignTop, elided_val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor ChartView::uniqueColor(QColor color, const cabana::Signal *exclude) const {
|
|
||||||
for (auto &s : sigs) {
|
|
||||||
if (s.sig != exclude && std::abs(color.hueF() - s.color.hueF()) < 0.1) {
|
|
||||||
// use different color to distinguish it from others.
|
|
||||||
auto last_color = sigs.back().color;
|
|
||||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
|
||||||
std::uniform_int_distribution<int> sat(35, 99);
|
|
||||||
std::uniform_int_distribution<int> val(85, 99);
|
|
||||||
color.setHsvF(std::fmod(last_color.hueF() + 60 / 360.0, 1.0),
|
|
||||||
sat(rng) / 100.0,
|
|
||||||
val(rng) / 100.0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartView::setSeriesType(SeriesType type) {
|
|
||||||
if (type != series_type) {
|
|
||||||
series_type = type;
|
|
||||||
menu->actions()[(int)type]->setChecked(true);
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <tuple>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QMenu>
|
|
||||||
|
|
||||||
#include "tools/cabana/chart/tiplabel.h"
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
enum class SeriesType {
|
|
||||||
Line = 0,
|
|
||||||
StepLine,
|
|
||||||
Scatter
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChartsWidget;
|
|
||||||
class ChartView : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent = nullptr);
|
|
||||||
void addSignal(const MessageId &msg_id, const cabana::Signal *sig);
|
|
||||||
bool hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const;
|
|
||||||
void updateSeries(const cabana::Signal *sig = nullptr, const MessageEventsMap *msg_new_events = nullptr);
|
|
||||||
void updatePlot(double cur, double min, double max);
|
|
||||||
void setSeriesType(SeriesType type);
|
|
||||||
void updatePlotArea(int left, bool force = false);
|
|
||||||
void showTip(double sec);
|
|
||||||
void hideTip();
|
|
||||||
double secondsAtPoint(const QPointF &pt) const {
|
|
||||||
return x_min + (pt.x() - plot_area.left()) * (x_max - x_min) / std::max(plot_area.width(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SigItem {
|
|
||||||
MessageId msg_id;
|
|
||||||
const cabana::Signal *sig = nullptr;
|
|
||||||
QColor color;
|
|
||||||
bool visible = true;
|
|
||||||
std::vector<QPointF> vals;
|
|
||||||
std::vector<QPointF> step_vals;
|
|
||||||
QPointF track_pt{};
|
|
||||||
SegmentTree segment_tree;
|
|
||||||
double min = 0;
|
|
||||||
double max = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void axisYLabelWidthChanged(int w);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void signalUpdated(const cabana::Signal *sig);
|
|
||||||
void manageSignals();
|
|
||||||
void msgUpdated(MessageId id);
|
|
||||||
void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); }
|
|
||||||
void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
|
||||||
std::vector<QPointF> &vals, std::vector<QPointF> &step_vals);
|
|
||||||
void createToolButtons();
|
|
||||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
|
||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
void updateAxisY();
|
|
||||||
void updateTitle();
|
|
||||||
void resetChartCache();
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
void drawStaticLayer(QPainter *painter);
|
|
||||||
void drawAxes(QPainter *painter);
|
|
||||||
void drawLegend(QPainter *painter);
|
|
||||||
void drawSeries(QPainter *painter);
|
|
||||||
void drawForeground(QPainter *painter);
|
|
||||||
void drawSignalValue(QPainter *painter);
|
|
||||||
void drawTimeline(QPainter *painter);
|
|
||||||
void drawRubberBandTimeRange(QPainter *painter);
|
|
||||||
int xAxisPrecision() const;
|
|
||||||
std::tuple<double, double, int> getNiceAxisNumbers(qreal min, qreal max, int tick_count);
|
|
||||||
qreal niceNumber(qreal x, bool ceiling);
|
|
||||||
QColor uniqueColor(QColor color, const cabana::Signal *exclude = nullptr) const;
|
|
||||||
void removeIf(std::function<bool(const SigItem &)> predicate);
|
|
||||||
void takeSignalsFrom(ChartView *source);
|
|
||||||
void setDropHighlight(bool highlight) { if (std::exchange(can_drop, highlight) != highlight) update(); }
|
|
||||||
inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; }
|
|
||||||
inline qreal xPos(double sec) const { return plot_area.left() + (sec - x_min) / (x_max - x_min) * plot_area.width(); }
|
|
||||||
inline qreal yPos(double val) const { return plot_area.bottom() - (val - y_min) / (y_max - y_min) * plot_area.height(); }
|
|
||||||
|
|
||||||
// layout
|
|
||||||
QRect plot_area;
|
|
||||||
QRect move_icon_rect;
|
|
||||||
std::vector<QRect> legend_rects;
|
|
||||||
// axes
|
|
||||||
double x_min;
|
|
||||||
double x_max;
|
|
||||||
double y_min = 0;
|
|
||||||
double y_max = 1;
|
|
||||||
int y_tick_count = 3;
|
|
||||||
int y_precision = 0;
|
|
||||||
QString y_unit;
|
|
||||||
int y_label_width = 0;
|
|
||||||
int align_to = 0;
|
|
||||||
// interaction
|
|
||||||
enum class MouseMode { None, Rubber, Scrub };
|
|
||||||
MouseMode mouse_mode = MouseMode::None;
|
|
||||||
QPoint press_pos;
|
|
||||||
QRect rubber_rect;
|
|
||||||
bool resume_after_scrub = false;
|
|
||||||
|
|
||||||
QMenu *menu;
|
|
||||||
QAction *split_chart_act;
|
|
||||||
QAction *close_act;
|
|
||||||
ToolButton *manage_btn;
|
|
||||||
ToolButton *close_btn;
|
|
||||||
TipLabel *tip_label;
|
|
||||||
std::vector<SigItem> sigs;
|
|
||||||
double cur_sec = 0;
|
|
||||||
SeriesType series_type = SeriesType::Line;
|
|
||||||
QPixmap chart_pixmap;
|
|
||||||
bool can_drop = false;
|
|
||||||
double tooltip_x = -1;
|
|
||||||
QFont signal_value_font;
|
|
||||||
ChartsWidget *charts_widget;
|
|
||||||
friend class ChartsWidget;
|
|
||||||
};
|
|
||||||
@@ -1,661 +0,0 @@
|
|||||||
#include "tools/cabana/chart/chartswidget.h"
|
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <future>
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QToolBar>
|
|
||||||
|
|
||||||
#include "tools/cabana/chart/chart.h"
|
|
||||||
|
|
||||||
const int MAX_COLUMN_COUNT = 4;
|
|
||||||
const int CHART_SPACING = 4;
|
|
||||||
|
|
||||||
ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
|
||||||
align_timer = new QTimer(this);
|
|
||||||
auto_scroll_timer = new QTimer(this);
|
|
||||||
setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
main_layout->setSpacing(0);
|
|
||||||
|
|
||||||
// toolbar
|
|
||||||
toolbar = new QToolBar(tr("Charts"), this);
|
|
||||||
int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize);
|
|
||||||
toolbar->setIconSize({icon_size, icon_size});
|
|
||||||
|
|
||||||
auto new_plot_btn = new ToolButton("file-plus", tr("New Chart"));
|
|
||||||
auto new_tab_btn = new ToolButton("window-stack", tr("New Tab"));
|
|
||||||
toolbar->addWidget(new_plot_btn);
|
|
||||||
toolbar->addWidget(new_tab_btn);
|
|
||||||
toolbar->addWidget(title_label = new QLabel());
|
|
||||||
title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0);
|
|
||||||
|
|
||||||
auto chart_type_action = toolbar->addAction("");
|
|
||||||
QMenu *chart_type_menu = new QMenu(this);
|
|
||||||
auto types = std::array{tr("Line"), tr("Step"), tr("Scatter")};
|
|
||||||
for (int i = 0; i < types.size(); ++i) {
|
|
||||||
QString type_text = types[i];
|
|
||||||
chart_type_menu->addAction(type_text, this, [=]() {
|
|
||||||
settings.chart_series_type = i;
|
|
||||||
chart_type_action->setText("Type: " + type_text);
|
|
||||||
settingChanged();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
chart_type_action->setText("Type: " + types[settings.chart_series_type]);
|
|
||||||
chart_type_action->setMenu(chart_type_menu);
|
|
||||||
qobject_cast<QToolButton *>(toolbar->widgetForAction(chart_type_action))->setPopupMode(QToolButton::InstantPopup);
|
|
||||||
|
|
||||||
QMenu *menu = new QMenu(this);
|
|
||||||
for (int i = 0; i < MAX_COLUMN_COUNT; ++i) {
|
|
||||||
menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); });
|
|
||||||
}
|
|
||||||
columns_action = toolbar->addAction("");
|
|
||||||
columns_action->setMenu(menu);
|
|
||||||
qobject_cast<QToolButton*>(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup);
|
|
||||||
|
|
||||||
QWidget *spacer = new QWidget(this);
|
|
||||||
spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
|
|
||||||
toolbar->addWidget(spacer);
|
|
||||||
|
|
||||||
range_lb_action = toolbar->addWidget(range_lb = new QLabel(this));
|
|
||||||
range_slider = new LogSlider(1000, Qt::Horizontal, this);
|
|
||||||
range_slider->setFixedWidth(150 * qApp->devicePixelRatio());
|
|
||||||
range_slider->setToolTip(tr("Set the chart range"));
|
|
||||||
range_slider->setRange(1, settings.max_cached_minutes * 60);
|
|
||||||
range_slider->setSingleStep(1);
|
|
||||||
range_slider->setPageStep(60); // 1 min
|
|
||||||
range_slider_action = toolbar->addWidget(range_slider);
|
|
||||||
|
|
||||||
// zoom controls
|
|
||||||
undo_zoom_action = toolbar->addAction(utils::icon("arrow-counterclockwise"), tr("Undo Zoom"), [this]() { zoom_undo_stack.undo(); });
|
|
||||||
redo_zoom_action = toolbar->addAction(utils::icon("arrow-clockwise"), tr("Redo Zoom"), [this]() { zoom_undo_stack.redo(); });
|
|
||||||
undo_zoom_action->setEnabled(false);
|
|
||||||
redo_zoom_action->setEnabled(false);
|
|
||||||
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);
|
|
||||||
|
|
||||||
toolbar->addWidget(remove_all_btn = new ToolButton("x-square", tr("Remove all charts")));
|
|
||||||
toolbar->addWidget(dock_btn = new ToolButton(""));
|
|
||||||
main_layout->addWidget(toolbar);
|
|
||||||
|
|
||||||
// tabbar
|
|
||||||
tabbar = new TabBar(this);
|
|
||||||
tabbar->setAutoHide(true);
|
|
||||||
tabbar->setExpanding(false);
|
|
||||||
tabbar->setDrawBase(true);
|
|
||||||
tabbar->setUsesScrollButtons(true);
|
|
||||||
main_layout->addWidget(tabbar);
|
|
||||||
|
|
||||||
// charts
|
|
||||||
charts_container = new ChartsContainer(this);
|
|
||||||
charts_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
|
||||||
charts_scroll = new QScrollArea(this);
|
|
||||||
charts_scroll->viewport()->setBackgroundRole(QPalette::Base);
|
|
||||||
charts_scroll->setFrameStyle(QFrame::NoFrame);
|
|
||||||
charts_scroll->setWidgetResizable(true);
|
|
||||||
charts_scroll->setWidget(charts_container);
|
|
||||||
charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
main_layout->addWidget(charts_scroll);
|
|
||||||
|
|
||||||
// chart drag preview
|
|
||||||
drag_preview = new QLabel(this);
|
|
||||||
drag_preview->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
drag_preview->hide();
|
|
||||||
|
|
||||||
// init settings
|
|
||||||
current_theme = settings.theme;
|
|
||||||
column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT);
|
|
||||||
max_chart_range = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60);
|
|
||||||
display_range = std::make_pair(can->minSeconds(), can->minSeconds() + max_chart_range);
|
|
||||||
range_slider->setValue(max_chart_range);
|
|
||||||
updateToolBar();
|
|
||||||
|
|
||||||
align_timer->setSingleShot(true);
|
|
||||||
QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts);
|
|
||||||
QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll);
|
|
||||||
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);
|
|
||||||
QObject::connect(can, &AbstractStream::timeRangeChanged, this, &ChartsWidget::timeRangeChanged);
|
|
||||||
QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange);
|
|
||||||
QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart);
|
|
||||||
QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll);
|
|
||||||
QObject::connect(reset_zoom_btn, &QToolButton::clicked, this, &ChartsWidget::zoomReset);
|
|
||||||
QObject::connect(&settings, &Settings::changed, this, &ChartsWidget::settingChanged);
|
|
||||||
QObject::connect(new_tab_btn, &QToolButton::clicked, this, &ChartsWidget::newTab);
|
|
||||||
QObject::connect(this, &ChartsWidget::seriesChanged, this, &ChartsWidget::updateTabBar);
|
|
||||||
QObject::connect(tabbar, &QTabBar::tabCloseRequested, this, &ChartsWidget::removeTab);
|
|
||||||
QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) {
|
|
||||||
if (index != -1) updateLayout(true);
|
|
||||||
});
|
|
||||||
QObject::connect(dock_btn, &QToolButton::clicked, this, &ChartsWidget::toggleChartsDocking);
|
|
||||||
|
|
||||||
setIsDocked(true);
|
|
||||||
newTab();
|
|
||||||
qApp->installEventFilter(this);
|
|
||||||
setWhatsThis(tr(R"(
|
|
||||||
<b>Chart View</b><br />
|
|
||||||
<b>Click</b>: Click to seek to a corresponding time.<br />
|
|
||||||
<b>Drag</b>: Zoom into the chart.<br />
|
|
||||||
<b>Shift + Drag</b>: Scrub through the chart to view values.<br />
|
|
||||||
<b>Right Mouse</b>: Open the context menu.<br />
|
|
||||||
)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::newTab() {
|
|
||||||
static int tab_unique_id = 0;
|
|
||||||
int idx = tabbar->addTab("");
|
|
||||||
tabbar->setTabData(idx, tab_unique_id++);
|
|
||||||
tabbar->setCurrentIndex(idx);
|
|
||||||
updateTabBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::removeTab(int index) {
|
|
||||||
int id = tabbar->tabData(index).toInt();
|
|
||||||
for (auto &c : tab_charts[id]) {
|
|
||||||
removeChart(c);
|
|
||||||
}
|
|
||||||
tab_charts.erase(id);
|
|
||||||
tabbar->removeTab(index);
|
|
||||||
updateTabBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::updateTabBar() {
|
|
||||||
for (int i = 0; i < tabbar->count(); ++i) {
|
|
||||||
const auto &charts_in_tab = tab_charts[tabbar->tabData(i).toInt()];
|
|
||||||
tabbar->setTabText(i, QString("Tab %1 (%2)").arg(i + 1).arg((int)charts_in_tab.size()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) {
|
|
||||||
std::vector<std::future<void>> futures;
|
|
||||||
for (auto c : charts) {
|
|
||||||
futures.push_back(std::async(std::launch::async, &ChartView::updateSeries, c, nullptr, &new_events));
|
|
||||||
}
|
|
||||||
for (auto &f : futures) f.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::timeRangeChanged(const std::optional<std::pair<double, double>> &time_range) {
|
|
||||||
updateToolBar();
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::zoomReset() {
|
|
||||||
can->setTimeRange(std::nullopt);
|
|
||||||
zoom_undo_stack.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect ChartsWidget::chartVisibleRect(ChartView *chart) {
|
|
||||||
const QRect visible_rect(-charts_container->pos(), charts_scroll->viewport()->size());
|
|
||||||
return chart->rect().intersected(QRect(chart->mapFrom(charts_container, visible_rect.topLeft()), visible_rect.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::showValueTip(double sec) {
|
|
||||||
emit showTip(sec);
|
|
||||||
if (sec < 0 && !value_tip_visible_) return;
|
|
||||||
|
|
||||||
value_tip_visible_ = sec >= 0;
|
|
||||||
for (auto c : currentCharts()) {
|
|
||||||
value_tip_visible_ ? c->showTip(sec) : c->hideTip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::updateState() {
|
|
||||||
if (charts.empty()) return;
|
|
||||||
|
|
||||||
const auto &time_range = can->timeRange();
|
|
||||||
const double cur_sec = can->currentSec();
|
|
||||||
if (!time_range.has_value()) {
|
|
||||||
double pos = (cur_sec - display_range.first) / std::max<float>(1.0, max_chart_range);
|
|
||||||
if (pos < 0 || pos > 0.8) {
|
|
||||||
display_range.first = std::max(can->minSeconds(), cur_sec - max_chart_range * 0.1);
|
|
||||||
}
|
|
||||||
double max_sec = std::min(display_range.first + max_chart_range, can->maxSeconds());
|
|
||||||
display_range.first = std::max(can->minSeconds(), max_sec - max_chart_range);
|
|
||||||
display_range.second = display_range.first + max_chart_range;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &range = time_range ? *time_range : display_range;
|
|
||||||
for (auto c : charts) {
|
|
||||||
c->updatePlot(cur_sec, range.first, range.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::setMaxChartRange(int value) {
|
|
||||||
max_chart_range = settings.chart_range = range_slider->value();
|
|
||||||
updateToolBar();
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::setIsDocked(bool docked) {
|
|
||||||
is_docked = docked;
|
|
||||||
dock_btn->setIcon(is_docked ? "arrow-up-right-square" : "arrow-down-left-square");
|
|
||||||
dock_btn->setToolTip(is_docked ? tr("Float the charts window") : tr("Dock the charts window"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::updateToolBar() {
|
|
||||||
title_label->setText(tr("Charts: %1").arg(charts.size()));
|
|
||||||
columns_action->setText(tr("Columns: %1").arg(column_count));
|
|
||||||
range_lb->setText(utils::formatSeconds(max_chart_range));
|
|
||||||
|
|
||||||
bool is_zoomed = can->timeRange().has_value();
|
|
||||||
range_lb_action->setVisible(!is_zoomed);
|
|
||||||
range_slider_action->setVisible(!is_zoomed);
|
|
||||||
undo_zoom_action->setVisible(is_zoomed);
|
|
||||||
redo_zoom_action->setVisible(is_zoomed);
|
|
||||||
reset_zoom_action->setVisible(is_zoomed);
|
|
||||||
reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(can->timeRange()->first, 0, 'f', 2).arg(can->timeRange()->second, 0, 'f', 2) : "");
|
|
||||||
remove_all_btn->setEnabled(!charts.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::settingChanged() {
|
|
||||||
if (std::exchange(current_theme, settings.theme) != current_theme) {
|
|
||||||
undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise"));
|
|
||||||
redo_zoom_action->setIcon(utils::icon("arrow-clockwise"));
|
|
||||||
}
|
|
||||||
if (range_slider->maximum() != settings.max_cached_minutes * 60) {
|
|
||||||
range_slider->setRange(1, settings.max_cached_minutes * 60);
|
|
||||||
}
|
|
||||||
for (auto c : charts) {
|
|
||||||
c->setFixedHeight(settings.chart_height);
|
|
||||||
c->setSeriesType((SeriesType)settings.chart_series_type);
|
|
||||||
c->resetChartCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) {
|
|
||||||
for (auto c : charts)
|
|
||||||
if (c->hasSignal(id, sig)) return c;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChartView *ChartsWidget::createChart(int pos) {
|
|
||||||
auto chart = new ChartView(can->timeRange().value_or(display_range), this);
|
|
||||||
chart->setFixedHeight(settings.chart_height);
|
|
||||||
chart->setMinimumWidth(CHART_MIN_WIDTH);
|
|
||||||
chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
|
|
||||||
QObject::connect(chart, &ChartView::axisYLabelWidthChanged, align_timer, qOverload<>(&QTimer::start));
|
|
||||||
pos = std::clamp(pos, 0, (int)charts.size());
|
|
||||||
charts.insert(charts.begin() + pos, chart);
|
|
||||||
currentCharts().insert(currentCharts().begin() + pos, chart);
|
|
||||||
updateLayout(true);
|
|
||||||
updateToolBar();
|
|
||||||
return chart;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) {
|
|
||||||
ChartView *chart = findChart(id, sig);
|
|
||||||
if (show && !chart) {
|
|
||||||
chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart();
|
|
||||||
chart->addSignal(id, sig);
|
|
||||||
updateState();
|
|
||||||
} else if (!show && chart) {
|
|
||||||
chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::splitChart(ChartView *src_chart) {
|
|
||||||
if (src_chart->sigs.size() > 1) {
|
|
||||||
int pos = std::find(charts.begin(), charts.end(), src_chart) - charts.begin() + 1;
|
|
||||||
for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) {
|
|
||||||
auto c = createChart(pos);
|
|
||||||
// Restore to the original color
|
|
||||||
it->color = toQColor(it->sig->color);
|
|
||||||
c->sigs.emplace_back(std::move(*it));
|
|
||||||
c->updateAxisY();
|
|
||||||
c->updateTitle();
|
|
||||||
it = src_chart->sigs.erase(it);
|
|
||||||
}
|
|
||||||
src_chart->updateAxisY();
|
|
||||||
src_chart->updateTitle();
|
|
||||||
updateState();
|
|
||||||
QTimer::singleShot(0, src_chart, &ChartView::resetChartCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList ChartsWidget::serializeChartIds() const {
|
|
||||||
QStringList chart_ids;
|
|
||||||
for (auto c : charts) {
|
|
||||||
QStringList ids;
|
|
||||||
for (const auto& s : c->sigs)
|
|
||||||
ids += QString("%1|%2").arg(QString::fromStdString(s.msg_id.toString()), QString::fromStdString(s.sig->name));
|
|
||||||
chart_ids += ids.join(',');
|
|
||||||
}
|
|
||||||
std::reverse(chart_ids.begin(), chart_ids.end());
|
|
||||||
return chart_ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::restoreChartsFromIds(const QStringList& chart_ids) {
|
|
||||||
for (const auto& chart_id : chart_ids) {
|
|
||||||
int index = 0;
|
|
||||||
for (const auto& part : chart_id.split(',')) {
|
|
||||||
const auto sig_parts = part.split('|');
|
|
||||||
if (sig_parts.size() != 2) continue;
|
|
||||||
MessageId msg_id = MessageId::fromString(sig_parts[0].toStdString());
|
|
||||||
if (auto* msg = dbc()->msg(msg_id))
|
|
||||||
if (auto* sig = msg->sig(sig_parts[1].toStdString()))
|
|
||||||
showChart(msg_id, sig, true, index++ > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::setColumnCount(int n) {
|
|
||||||
n = std::clamp(n, 1, MAX_COLUMN_COUNT);
|
|
||||||
if (column_count != n) {
|
|
||||||
column_count = settings.chart_column_count = n;
|
|
||||||
updateToolBar();
|
|
||||||
updateLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::updateLayout(bool force) {
|
|
||||||
auto charts_layout = charts_container->charts_layout;
|
|
||||||
int n = MAX_COLUMN_COUNT;
|
|
||||||
for (; n > 1; --n) {
|
|
||||||
if ((n * CHART_MIN_WIDTH + (n - 1) * charts_layout->horizontalSpacing()) < charts_layout->geometry().width()) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool show_column_cb = n > 1;
|
|
||||||
columns_action->setVisible(show_column_cb);
|
|
||||||
|
|
||||||
n = std::min(column_count, n);
|
|
||||||
auto ¤t_charts = currentCharts();
|
|
||||||
if ((current_charts.size() != charts_layout->count() || n != current_column_count) || force) {
|
|
||||||
current_column_count = n;
|
|
||||||
charts_container->setUpdatesEnabled(false);
|
|
||||||
for (auto c : charts) {
|
|
||||||
c->setVisible(false);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < current_charts.size(); ++i) {
|
|
||||||
charts_layout->addWidget(current_charts[i], i / n, i % n);
|
|
||||||
if (current_charts[i]->sigs.empty()) {
|
|
||||||
// the chart will be resized after add signal. delay setVisible to reduce flicker.
|
|
||||||
QTimer::singleShot(0, current_charts[i], [c = current_charts[i]]() { c->setVisible(true); });
|
|
||||||
} else {
|
|
||||||
current_charts[i]->setVisible(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
charts_container->setUpdatesEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::startChartDrag(ChartView *chart, const QPoint &global_pos) {
|
|
||||||
stopAutoScroll();
|
|
||||||
drag = {.source = chart, .press_pos = global_pos};
|
|
||||||
QPixmap px = chart->grab().scaledToWidth(CHART_MIN_WIDTH * chart->devicePixelRatio(), Qt::SmoothTransformation);
|
|
||||||
drag_preview->setPixmap(px);
|
|
||||||
drag_preview->resize(px.size() / px.devicePixelRatio());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::dragChartMove(const QPoint &global_pos) {
|
|
||||||
if (!drag.active) {
|
|
||||||
if ((global_pos - drag.press_pos).manhattanLength() < QApplication::startDragDistance()) return;
|
|
||||||
drag.active = true;
|
|
||||||
drag_preview->show();
|
|
||||||
drag_preview->raise();
|
|
||||||
}
|
|
||||||
drag_preview->move(mapFromGlobal(global_pos) + QPoint(5, 5));
|
|
||||||
|
|
||||||
// hovering a tab switches to it so the chart can be dropped into another tab
|
|
||||||
int tab = tabbar->tabAt(tabbar->mapFromGlobal(global_pos));
|
|
||||||
if (tab >= 0 && tab != tabbar->currentIndex()) {
|
|
||||||
tabbar->setCurrentIndex(tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QPoint container_pos = charts_container->mapFromGlobal(global_pos);
|
|
||||||
ChartView *target = nullptr;
|
|
||||||
for (auto c : currentCharts()) {
|
|
||||||
if (c != drag.source && c->isVisible() && c->geometry().contains(container_pos)) {
|
|
||||||
target = c;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (std::exchange(drop_target, target) != target) {
|
|
||||||
for (auto c : charts) c->setDropHighlight(c == target);
|
|
||||||
}
|
|
||||||
bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos));
|
|
||||||
bool on_background = !target && in_viewport && !charts_container->childAt(container_pos);
|
|
||||||
charts_container->drawDropIndicator(on_background ? container_pos : QPoint());
|
|
||||||
|
|
||||||
if (in_viewport) {
|
|
||||||
startAutoScroll(global_pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::cancelChartDrag() {
|
|
||||||
drag = {};
|
|
||||||
stopAutoScroll();
|
|
||||||
drag_preview->hide();
|
|
||||||
charts_container->drawDropIndicator({});
|
|
||||||
if (auto target = std::exchange(drop_target, nullptr)) target->setDropHighlight(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::dragChartRelease(const QPoint &global_pos) {
|
|
||||||
ChartView *source = drag.source;
|
|
||||||
bool active = drag.active;
|
|
||||||
ChartView *target = drop_target;
|
|
||||||
cancelChartDrag();
|
|
||||||
if (!active) return;
|
|
||||||
|
|
||||||
const QPoint container_pos = charts_container->mapFromGlobal(global_pos);
|
|
||||||
bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos));
|
|
||||||
if (target) {
|
|
||||||
// merge source into target
|
|
||||||
target->takeSignalsFrom(source);
|
|
||||||
} else if (in_viewport && !charts_container->childAt(container_pos)) {
|
|
||||||
// reorder within the current tab
|
|
||||||
auto w = charts_container->getDropAfter(container_pos);
|
|
||||||
if (w != source) {
|
|
||||||
for (auto &[_, list] : tab_charts) {
|
|
||||||
list.erase(std::remove(list.begin(), list.end(), source), list.end());
|
|
||||||
}
|
|
||||||
auto &cur = currentCharts();
|
|
||||||
int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0;
|
|
||||||
cur.insert(cur.begin() + to, source);
|
|
||||||
updateLayout(true);
|
|
||||||
updateTabBar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::startAutoScroll(const QPoint &global_pos) {
|
|
||||||
auto_scroll_pos = global_pos;
|
|
||||||
auto_scroll_timer->start(50);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::stopAutoScroll() {
|
|
||||||
auto_scroll_timer->stop();
|
|
||||||
auto_scroll_count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::doAutoScroll() {
|
|
||||||
QScrollBar *scroll = charts_scroll->verticalScrollBar();
|
|
||||||
if (auto_scroll_count < scroll->pageStep()) {
|
|
||||||
++auto_scroll_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
int value = scroll->value();
|
|
||||||
QPoint pos = charts_scroll->viewport()->mapFromGlobal(auto_scroll_pos);
|
|
||||||
QRect area = charts_scroll->viewport()->rect();
|
|
||||||
|
|
||||||
if (pos.y() - area.top() < settings.chart_height / 2) {
|
|
||||||
scroll->setValue(value - auto_scroll_count);
|
|
||||||
} else if (area.bottom() - pos.y() < settings.chart_height / 2) {
|
|
||||||
scroll->setValue(value + auto_scroll_count);
|
|
||||||
}
|
|
||||||
if (value == scroll->value()) {
|
|
||||||
stopAutoScroll();
|
|
||||||
} else if (chartDragActive()) {
|
|
||||||
// refresh the drop indicator/target at the new scroll position
|
|
||||||
dragChartMove(auto_scroll_pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize ChartsWidget::minimumSizeHint() const {
|
|
||||||
return QSize(CHART_MIN_WIDTH * 1.5, QWidget::minimumSizeHint().height());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::newChart() {
|
|
||||||
SignalSelector dlg(tr("New Chart"), this);
|
|
||||||
if (dlg.exec() == QDialog::Accepted) {
|
|
||||||
auto items = dlg.seletedItems();
|
|
||||||
if (!items.empty()) {
|
|
||||||
auto c = createChart();
|
|
||||||
for (auto it : items) {
|
|
||||||
c->addSignal(it->msg_id, it->sig);
|
|
||||||
}
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::removeChart(ChartView *chart) {
|
|
||||||
if (drag.source == chart) cancelChartDrag();
|
|
||||||
if (drop_target == chart) drop_target = nullptr;
|
|
||||||
charts.erase(std::remove(charts.begin(), charts.end(), chart), charts.end());
|
|
||||||
chart->deleteLater();
|
|
||||||
for (auto &[_, list] : tab_charts) {
|
|
||||||
list.erase(std::remove(list.begin(), list.end(), chart), list.end());
|
|
||||||
}
|
|
||||||
updateToolBar();
|
|
||||||
updateLayout(true);
|
|
||||||
alignCharts();
|
|
||||||
emit seriesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::removeAll() {
|
|
||||||
while (tabbar->count() > 1) {
|
|
||||||
tabbar->removeTab(1);
|
|
||||||
}
|
|
||||||
tab_charts.clear();
|
|
||||||
|
|
||||||
if (!charts.empty()) {
|
|
||||||
for (auto c : charts) {
|
|
||||||
delete c;
|
|
||||||
}
|
|
||||||
charts.clear();
|
|
||||||
emit seriesChanged();
|
|
||||||
}
|
|
||||||
zoomReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsWidget::alignCharts() {
|
|
||||||
int plot_left = 0;
|
|
||||||
for (auto c : charts) {
|
|
||||||
plot_left = std::max(plot_left, c->y_label_width);
|
|
||||||
}
|
|
||||||
plot_left = std::max((plot_left / 10) * 10 + 10, 50);
|
|
||||||
for (auto c : charts) {
|
|
||||||
c->updatePlotArea(plot_left);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChartsWidget::eventFilter(QObject *o, QEvent *e) {
|
|
||||||
// route all mouse events to the chart drag, even when the source chart is hidden by a tab switch
|
|
||||||
if (chartDragActive()) {
|
|
||||||
if (e->type() == QEvent::MouseMove) {
|
|
||||||
dragChartMove(static_cast<QMouseEvent *>(e)->globalPos());
|
|
||||||
return true;
|
|
||||||
} else if (e->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(e)->button() == Qt::LeftButton) {
|
|
||||||
dragChartRelease(static_cast<QMouseEvent *>(e)->globalPos());
|
|
||||||
return false; // let the release through so Qt clears the implicit mouse grab
|
|
||||||
} else if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonRelease) {
|
|
||||||
return true; // swallow other buttons during the drag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value_tip_visible_) return false;
|
|
||||||
|
|
||||||
if (e->type() == QEvent::MouseMove) {
|
|
||||||
bool on_tip = qobject_cast<TipLabel *>(o) != nullptr;
|
|
||||||
auto global_pos = static_cast<QMouseEvent *>(e)->globalPos();
|
|
||||||
|
|
||||||
for (const auto &c : charts) {
|
|
||||||
auto local_pos = c->mapFromGlobal(global_pos);
|
|
||||||
if (c->plot_area.contains(local_pos)) {
|
|
||||||
if (on_tip) {
|
|
||||||
showValueTip(c->secondsAtPoint(local_pos));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showValueTip(-1);
|
|
||||||
} else if (e->type() == QEvent::Wheel) {
|
|
||||||
if (auto tip = qobject_cast<TipLabel *>(o)) {
|
|
||||||
// Forward the event to the parent widget
|
|
||||||
QCoreApplication::sendEvent(tip->parentWidget(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChartsWidget::event(QEvent *event) {
|
|
||||||
bool back_button = false;
|
|
||||||
switch (event->type()) {
|
|
||||||
case QEvent::Resize:
|
|
||||||
updateLayout();
|
|
||||||
break;
|
|
||||||
case QEvent::MouseButtonPress:
|
|
||||||
back_button = static_cast<QMouseEvent *>(event)->button() == Qt::BackButton;
|
|
||||||
break;
|
|
||||||
case QEvent::NativeGesture:
|
|
||||||
back_button = (static_cast<QNativeGestureEvent *>(event)->value() == 180);
|
|
||||||
break;
|
|
||||||
case QEvent::WindowDeactivate:
|
|
||||||
case QEvent::FocusOut:
|
|
||||||
if (chartDragActive()) cancelChartDrag();
|
|
||||||
showValueTip(-1);
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (back_button) {
|
|
||||||
zoom_undo_stack.undo();
|
|
||||||
return true; // Return true since the event has been handled
|
|
||||||
}
|
|
||||||
return QFrame::event(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChartsContainer
|
|
||||||
|
|
||||||
ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) {
|
|
||||||
setBackgroundRole(QPalette::Window);
|
|
||||||
QVBoxLayout *charts_main_layout = new QVBoxLayout(this);
|
|
||||||
charts_main_layout->setContentsMargins(0, CHART_SPACING, 0, CHART_SPACING);
|
|
||||||
charts_layout = new QGridLayout();
|
|
||||||
charts_layout->setSpacing(CHART_SPACING);
|
|
||||||
charts_main_layout->addLayout(charts_layout);
|
|
||||||
charts_main_layout->addStretch(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChartsContainer::paintEvent(QPaintEvent *ev) {
|
|
||||||
if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) {
|
|
||||||
QRect r = geometry();
|
|
||||||
r.setHeight(CHART_SPACING);
|
|
||||||
if (auto insert_after = getDropAfter(drop_indictor_pos)) {
|
|
||||||
r.moveTop(insert_after->geometry().bottom());
|
|
||||||
}
|
|
||||||
|
|
||||||
QPainter p(this);
|
|
||||||
p.fillRect(r, palette().highlight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ChartView *ChartsContainer::getDropAfter(const QPoint &pos) const {
|
|
||||||
auto it = std::find_if(charts_widget->currentCharts().crbegin(), charts_widget->currentCharts().crend(), [&pos](auto c) {
|
|
||||||
auto area = c->geometry();
|
|
||||||
return pos.x() >= area.left() && pos.x() <= area.right() && pos.y() >= area.bottom();
|
|
||||||
});
|
|
||||||
return it == charts_widget->currentCharts().crend() ? nullptr : *it;
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QGridLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QToolBar>
|
|
||||||
|
|
||||||
#include "tools/cabana/chart/signalselector.h"
|
|
||||||
#include "tools/cabana/commands.h"
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
const int CHART_MIN_WIDTH = 300;
|
|
||||||
|
|
||||||
class ChartView;
|
|
||||||
class ChartsWidget;
|
|
||||||
|
|
||||||
class ChartsContainer : public QWidget {
|
|
||||||
public:
|
|
||||||
ChartsContainer(ChartsWidget *parent);
|
|
||||||
void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); }
|
|
||||||
void paintEvent(QPaintEvent *ev) override;
|
|
||||||
ChartView *getDropAfter(const QPoint &pos) const;
|
|
||||||
|
|
||||||
QGridLayout *charts_layout;
|
|
||||||
ChartsWidget *charts_widget;
|
|
||||||
QPoint drop_indictor_pos;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ChartsWidget : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
ChartsWidget(QWidget *parent = nullptr);
|
|
||||||
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
|
|
||||||
inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; }
|
|
||||||
QStringList serializeChartIds() const;
|
|
||||||
void restoreChartsFromIds(const QStringList &chart_ids);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void setColumnCount(int n);
|
|
||||||
void removeAll();
|
|
||||||
void timeRangeChanged(const std::optional<std::pair<double, double>> &time_range);
|
|
||||||
void setIsDocked(bool dock);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void toggleChartsDocking();
|
|
||||||
void seriesChanged();
|
|
||||||
void showTip(double seconds);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QSize minimumSizeHint() const override;
|
|
||||||
bool event(QEvent *event) override;
|
|
||||||
void alignCharts();
|
|
||||||
void newChart();
|
|
||||||
ChartView *createChart(int pos = 0);
|
|
||||||
void removeChart(ChartView *chart);
|
|
||||||
void splitChart(ChartView *chart);
|
|
||||||
QRect chartVisibleRect(ChartView *chart);
|
|
||||||
void eventsMerged(const MessageEventsMap &new_events);
|
|
||||||
void updateState();
|
|
||||||
void zoomReset();
|
|
||||||
void startChartDrag(ChartView *chart, const QPoint &global_pos);
|
|
||||||
void dragChartMove(const QPoint &global_pos);
|
|
||||||
void dragChartRelease(const QPoint &global_pos);
|
|
||||||
void cancelChartDrag();
|
|
||||||
bool chartDragActive() const { return drag.source != nullptr; }
|
|
||||||
void startAutoScroll(const QPoint &global_pos);
|
|
||||||
void stopAutoScroll();
|
|
||||||
void doAutoScroll();
|
|
||||||
void updateToolBar();
|
|
||||||
void updateTabBar();
|
|
||||||
void setMaxChartRange(int value);
|
|
||||||
void updateLayout(bool force = false);
|
|
||||||
void settingChanged();
|
|
||||||
void showValueTip(double sec);
|
|
||||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
|
||||||
void newTab();
|
|
||||||
void removeTab(int index);
|
|
||||||
inline std::vector<ChartView *> ¤tCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; }
|
|
||||||
ChartView *findChart(const MessageId &id, const cabana::Signal *sig);
|
|
||||||
|
|
||||||
QLabel *title_label;
|
|
||||||
QLabel *range_lb;
|
|
||||||
LogSlider *range_slider;
|
|
||||||
QAction *range_lb_action;
|
|
||||||
QAction *range_slider_action;
|
|
||||||
bool is_docked = true;
|
|
||||||
ToolButton *dock_btn;
|
|
||||||
|
|
||||||
QToolBar *toolbar;
|
|
||||||
QAction *undo_zoom_action;
|
|
||||||
QAction *redo_zoom_action;
|
|
||||||
QAction *reset_zoom_action;
|
|
||||||
ToolButton *reset_zoom_btn;
|
|
||||||
UndoStack zoom_undo_stack;
|
|
||||||
|
|
||||||
ToolButton *remove_all_btn;
|
|
||||||
std::vector<ChartView *> charts;
|
|
||||||
std::unordered_map<int, std::vector<ChartView *>> tab_charts;
|
|
||||||
TabBar *tabbar;
|
|
||||||
ChartsContainer *charts_container;
|
|
||||||
QScrollArea *charts_scroll;
|
|
||||||
uint32_t max_chart_range = 0;
|
|
||||||
std::pair<double, double> display_range;
|
|
||||||
QAction *columns_action;
|
|
||||||
int column_count = 1;
|
|
||||||
int current_column_count = 0;
|
|
||||||
struct ChartDrag {
|
|
||||||
ChartView *source = nullptr;
|
|
||||||
QPoint press_pos; // global
|
|
||||||
bool active = false;
|
|
||||||
} drag;
|
|
||||||
QLabel *drag_preview;
|
|
||||||
ChartView *drop_target = nullptr;
|
|
||||||
int auto_scroll_count = 0;
|
|
||||||
QPoint auto_scroll_pos;
|
|
||||||
QTimer *auto_scroll_timer;
|
|
||||||
QTimer *align_timer;
|
|
||||||
int current_theme = 0;
|
|
||||||
bool value_tip_visible_ = false;
|
|
||||||
friend class ChartView;
|
|
||||||
friend class ChartsContainer;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ZoomCommand : public UndoCommand {
|
|
||||||
public:
|
|
||||||
ZoomCommand(std::pair<double, double> range) : range(range) {
|
|
||||||
prev_range = can->timeRange();
|
|
||||||
}
|
|
||||||
void undo() override { can->setTimeRange(prev_range); }
|
|
||||||
void redo() override { can->setTimeRange(range); }
|
|
||||||
std::optional<std::pair<double, double>> prev_range, range;
|
|
||||||
};
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
#include "tools/cabana/chart/signalselector.h"
|
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
#include <QDialogButtonBox>
|
|
||||||
#include <QGridLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) {
|
|
||||||
setWindowTitle(title);
|
|
||||||
QGridLayout *main_layout = new QGridLayout(this);
|
|
||||||
|
|
||||||
// left column
|
|
||||||
main_layout->addWidget(new QLabel(tr("Available Signals")), 0, 0);
|
|
||||||
main_layout->addWidget(msgs_combo = new QComboBox(this), 1, 0);
|
|
||||||
msgs_combo->setEditable(true);
|
|
||||||
msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg..."));
|
|
||||||
msgs_combo->setInsertPolicy(QComboBox::NoInsert);
|
|
||||||
|
|
||||||
main_layout->addWidget(available_list = new QListWidget(this), 2, 0);
|
|
||||||
|
|
||||||
// buttons
|
|
||||||
QVBoxLayout *btn_layout = new QVBoxLayout();
|
|
||||||
QPushButton *add_btn = new QPushButton(utils::icon("chevron-right"), "", this);
|
|
||||||
add_btn->setEnabled(false);
|
|
||||||
QPushButton *remove_btn = new QPushButton(utils::icon("chevron-left"), "", this);
|
|
||||||
remove_btn->setEnabled(false);
|
|
||||||
btn_layout->addStretch(0);
|
|
||||||
btn_layout->addWidget(add_btn);
|
|
||||||
btn_layout->addWidget(remove_btn);
|
|
||||||
btn_layout->addStretch(0);
|
|
||||||
main_layout->addLayout(btn_layout, 0, 1, 3, 1);
|
|
||||||
|
|
||||||
// right column
|
|
||||||
main_layout->addWidget(new QLabel(tr("Selected Signals")), 0, 2);
|
|
||||||
main_layout->addWidget(selected_list = new QListWidget(this), 1, 2, 2, 1);
|
|
||||||
|
|
||||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
|
||||||
main_layout->addWidget(buttonBox, 3, 2);
|
|
||||||
|
|
||||||
for (const auto &[id, _] : can->lastMessages()) {
|
|
||||||
if (auto m = dbc()->msg(id)) {
|
|
||||||
msgs_combo->addItem(QString("%1 (%2)").arg(QString::fromStdString(m->name)).arg(QString::fromStdString(id.toString())), QVariant::fromValue(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msgs_combo->model()->sort(0);
|
|
||||||
msgs_combo->setCurrentIndex(-1);
|
|
||||||
|
|
||||||
QObject::connect(msgs_combo, qOverload<int>(&QComboBox::currentIndexChanged), this, &SignalSelector::updateAvailableList);
|
|
||||||
QObject::connect(available_list, &QListWidget::currentRowChanged, [=](int row) { add_btn->setEnabled(row != -1); });
|
|
||||||
QObject::connect(selected_list, &QListWidget::currentRowChanged, [=](int row) { remove_btn->setEnabled(row != -1); });
|
|
||||||
QObject::connect(available_list, &QListWidget::itemDoubleClicked, this, &SignalSelector::add);
|
|
||||||
QObject::connect(selected_list, &QListWidget::itemDoubleClicked, this, &SignalSelector::remove);
|
|
||||||
QObject::connect(add_btn, &QPushButton::clicked, [this]() { if (auto item = available_list->currentItem()) add(item); });
|
|
||||||
QObject::connect(remove_btn, &QPushButton::clicked, [this]() { if (auto item = selected_list->currentItem()) remove(item); });
|
|
||||||
QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
|
||||||
QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSelector::add(QListWidgetItem *item) {
|
|
||||||
auto it = (ListItem *)item;
|
|
||||||
addItemToList(selected_list, it->msg_id, it->sig, true);
|
|
||||||
delete item;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSelector::remove(QListWidgetItem *item) {
|
|
||||||
auto it = (ListItem *)item;
|
|
||||||
if (it->msg_id == msgs_combo->currentData().value<MessageId>()) {
|
|
||||||
addItemToList(available_list, it->msg_id, it->sig);
|
|
||||||
}
|
|
||||||
delete item;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSelector::updateAvailableList(int index) {
|
|
||||||
if (index == -1) return;
|
|
||||||
available_list->clear();
|
|
||||||
MessageId msg_id = msgs_combo->itemData(index).value<MessageId>();
|
|
||||||
auto selected_items = seletedItems();
|
|
||||||
for (auto s : dbc()->msg(msg_id)->getSignals()) {
|
|
||||||
bool is_selected = std::any_of(selected_items.begin(), selected_items.end(),
|
|
||||||
[sig = s, &msg_id](auto it) { return it->msg_id == msg_id && it->sig == sig; });
|
|
||||||
if (!is_selected) {
|
|
||||||
addItemToList(available_list, msg_id, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) {
|
|
||||||
QString text = QString("<span style=\"color:%0;\">■ </span> %1").arg(toQColor(sig->color).name(), QString::fromStdString(sig->name));
|
|
||||||
if (show_msg_name) text += QString(" <font color=\"gray\">%0 %1</font>").arg(QString::fromStdString(msgName(id)), QString::fromStdString(id.toString()));
|
|
||||||
|
|
||||||
QLabel *label = new QLabel(text);
|
|
||||||
label->setContentsMargins(5, 0, 5, 0);
|
|
||||||
auto new_item = new ListItem(id, sig, parent);
|
|
||||||
new_item->setSizeHint(label->sizeHint());
|
|
||||||
parent->setItemWidget(new_item, label);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<SignalSelector::ListItem *> SignalSelector::seletedItems() {
|
|
||||||
std::vector<SignalSelector::ListItem *> ret;
|
|
||||||
for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i));
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QDialog>
|
|
||||||
#include <QListWidget>
|
|
||||||
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
|
|
||||||
class SignalSelector : public QDialog {
|
|
||||||
public:
|
|
||||||
struct ListItem : public QListWidgetItem {
|
|
||||||
ListItem(const MessageId &msg_id, const cabana::Signal *sig, QListWidget *parent) : msg_id(msg_id), sig(sig), QListWidgetItem(parent) {}
|
|
||||||
MessageId msg_id;
|
|
||||||
const cabana::Signal *sig;
|
|
||||||
};
|
|
||||||
|
|
||||||
SignalSelector(QString title, QWidget *parent);
|
|
||||||
std::vector<ListItem *> seletedItems();
|
|
||||||
inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateAvailableList(int index);
|
|
||||||
void addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name = false);
|
|
||||||
void add(QListWidgetItem *item);
|
|
||||||
void remove(QListWidgetItem *item);
|
|
||||||
|
|
||||||
QComboBox *msgs_combo;
|
|
||||||
QListWidget *available_list;
|
|
||||||
QListWidget *selected_list;
|
|
||||||
};
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#include "tools/cabana/chart/sparkline.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <limits>
|
|
||||||
#include <QPainter>
|
|
||||||
|
|
||||||
void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size) {
|
|
||||||
if (first == last || size.isEmpty()) {
|
|
||||||
pixmap = QPixmap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
points_.clear();
|
|
||||||
min_val = std::numeric_limits<double>::max();
|
|
||||||
max_val = std::numeric_limits<double>::lowest();
|
|
||||||
points_.reserve(std::distance(first, last));
|
|
||||||
|
|
||||||
uint64_t start_time = (*first)->mono_time;
|
|
||||||
double value = 0.0;
|
|
||||||
for (auto it = first; it != last; ++it) {
|
|
||||||
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
|
|
||||||
min_val = std::min(min_val, value);
|
|
||||||
max_val = std::max(max_val, value);
|
|
||||||
points_.emplace_back(((*it)->mono_time - start_time) / 1e9, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (points_.empty()) {
|
|
||||||
pixmap = QPixmap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
freq_ = points_.size() / std::max(points_.back().x() - points_.front().x(), 1.0);
|
|
||||||
render(toQColor(sig->color), range, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Sparkline::render(const QColor &color, int range, QSize size) {
|
|
||||||
// Adjust for flat lines
|
|
||||||
bool is_flat_line = min_val == max_val;
|
|
||||||
if (is_flat_line) {
|
|
||||||
min_val -= 1.0;
|
|
||||||
max_val += 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate scaling
|
|
||||||
const double xscale = (size.width() - 1) / (double)range;
|
|
||||||
const double yscale = (size.height() - 3) / (max_val - min_val);
|
|
||||||
bool draw_individual_points = (points_.back().x() * xscale / points_.size()) > 8.0;
|
|
||||||
|
|
||||||
// Transform or downsample points
|
|
||||||
render_points_.reserve(points_.size());
|
|
||||||
render_points_.clear();
|
|
||||||
if (draw_individual_points) {
|
|
||||||
for (const auto &p : points_) {
|
|
||||||
render_points_.emplace_back(p.x() * xscale, 1.0 + (max_val - p.y()) * yscale);
|
|
||||||
}
|
|
||||||
} else if (is_flat_line) {
|
|
||||||
double y = size.height() / 2.0;
|
|
||||||
render_points_.emplace_back(0.0, y);
|
|
||||||
render_points_.emplace_back(points_.back().x() * xscale, y);
|
|
||||||
} else {
|
|
||||||
double prev_y = points_.front().y();
|
|
||||||
render_points_.emplace_back(points_.front().x() * xscale, 1.0 + (max_val - prev_y) * yscale);
|
|
||||||
bool in_flat = false;
|
|
||||||
|
|
||||||
for (size_t i = 1; i < points_.size(); ++i) {
|
|
||||||
const auto &p = points_[i];
|
|
||||||
double y = p.y();
|
|
||||||
if (std::abs(y - prev_y) < 1e-6) {
|
|
||||||
in_flat = true;
|
|
||||||
} else {
|
|
||||||
if (in_flat) render_points_.emplace_back(points_[i - 1].x() * xscale, 1.0 + (max_val - prev_y) * yscale);
|
|
||||||
render_points_.emplace_back(p.x() * xscale, 1.0 + (max_val - y) * yscale);
|
|
||||||
in_flat = false;
|
|
||||||
}
|
|
||||||
prev_y = y;
|
|
||||||
}
|
|
||||||
if (in_flat) render_points_.emplace_back(points_.back().x() * xscale, 1.0 + (max_val - prev_y) * yscale);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render to pixmap
|
|
||||||
qreal dpr = qApp->devicePixelRatio();
|
|
||||||
const QSize pixmap_size = size * dpr;
|
|
||||||
if (pixmap.size() != pixmap_size) {
|
|
||||||
pixmap = QPixmap(pixmap_size);
|
|
||||||
}
|
|
||||||
pixmap.setDevicePixelRatio(dpr);
|
|
||||||
pixmap.fill(Qt::transparent);
|
|
||||||
QPainter painter(&pixmap);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing, render_points_.size() <= 500);
|
|
||||||
painter.setPen(color);
|
|
||||||
painter.drawPolyline(render_points_.data(), render_points_.size());
|
|
||||||
|
|
||||||
painter.setPen(QPen(color, 3));
|
|
||||||
if (draw_individual_points) {
|
|
||||||
painter.drawPoints(render_points_.data(), render_points_.size());
|
|
||||||
} else {
|
|
||||||
painter.drawPoint(render_points_.back());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPointF>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "tools/cabana/dbc/dbc.h"
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
class Sparkline {
|
|
||||||
public:
|
|
||||||
void update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size);
|
|
||||||
inline double freq() const { return freq_; }
|
|
||||||
bool isEmpty() const { return pixmap.isNull(); }
|
|
||||||
|
|
||||||
QPixmap pixmap;
|
|
||||||
double min_val = 0;
|
|
||||||
double max_val = 0;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void render(const QColor &color, int range, QSize size);
|
|
||||||
|
|
||||||
std::vector<QPointF> points_;
|
|
||||||
std::vector<QPointF> render_points_;
|
|
||||||
double freq_ = 0;
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#include "tools/cabana/chart/tiplabel.h"
|
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QStylePainter>
|
|
||||||
#include <QToolTip>
|
|
||||||
|
|
||||||
#include "tools/cabana/settings.h"
|
|
||||||
#include "tools/cabana/utils/util.h"
|
|
||||||
|
|
||||||
TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) {
|
|
||||||
setAttribute(Qt::WA_ShowWithoutActivating);
|
|
||||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
|
|
||||||
setForegroundRole(QPalette::ToolTipText);
|
|
||||||
setBackgroundRole(QPalette::ToolTipBase);
|
|
||||||
|
|
||||||
QFont font;
|
|
||||||
font.setPointSizeF(8.34563465);
|
|
||||||
setFont(font);
|
|
||||||
auto palette = QToolTip::palette();
|
|
||||||
if (!utils::isDarkTheme()) {
|
|
||||||
palette.setColor(QPalette::ToolTipBase, QApplication::palette().color(QPalette::Base));
|
|
||||||
palette.setColor(QPalette::ToolTipText, QRgb(0x404044)); // same color as chart label brush
|
|
||||||
}
|
|
||||||
setPalette(palette);
|
|
||||||
ensurePolished();
|
|
||||||
setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this));
|
|
||||||
setTextFormat(Qt::RichText);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TipLabel::showText(const QPoint &pt, const QString &text, QWidget *w, const QRect &rect) {
|
|
||||||
setText(text);
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
QSize extra(1, 1);
|
|
||||||
resize(sizeHint() + extra);
|
|
||||||
QPoint tip_pos(pt.x() + 8, rect.top() + 2);
|
|
||||||
if (tip_pos.x() + size().width() >= rect.right()) {
|
|
||||||
tip_pos.rx() = pt.x() - size().width() - 8;
|
|
||||||
}
|
|
||||||
if (rect.contains({tip_pos, size()})) {
|
|
||||||
move(w->mapToGlobal(tip_pos));
|
|
||||||
setVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TipLabel::paintEvent(QPaintEvent *ev) {
|
|
||||||
QStylePainter p(this);
|
|
||||||
QStyleOptionFrame opt;
|
|
||||||
opt.init(this);
|
|
||||||
p.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
|
|
||||||
p.end();
|
|
||||||
QLabel::paintEvent(ev);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
|
|
||||||
class TipLabel : public QLabel {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
TipLabel(QWidget *parent = nullptr);
|
|
||||||
void showText(const QPoint &pt, const QString &sec, QWidget *w, const QRect &rect);
|
|
||||||
void paintEvent(QPaintEvent *ev) override;
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
#include "tools/cabana/commands.h"
|
#include "tools/cabana/commands.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
// UndoStack
|
|
||||||
|
|
||||||
void UndoStack::push(UndoCommand *cmd) {
|
void UndoStack::push(UndoCommand *cmd) {
|
||||||
commands_.resize(index_); // drop any redoable commands
|
commands_.resize(index_);
|
||||||
if (clean_index_ > index_) clean_index_ = -1;
|
if (clean_index_ > index_) clean_index_ = -1;
|
||||||
commands_.emplace_back(cmd);
|
commands_.emplace_back(cmd);
|
||||||
cmd->redo();
|
cmd->redo();
|
||||||
@@ -28,22 +29,22 @@ void UndoStack::clear() {
|
|||||||
bool was_clean = isClean();
|
bool was_clean = isClean();
|
||||||
commands_.clear();
|
commands_.clear();
|
||||||
index_ = clean_index_ = 0;
|
index_ = clean_index_ = 0;
|
||||||
if (callbacks_.index_changed) callbacks_.index_changed();
|
indexChanged();
|
||||||
if (!was_clean && callbacks_.clean_changed) callbacks_.clean_changed(true);
|
if (!was_clean) cleanChanged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UndoStack::setClean() {
|
void UndoStack::setClean() {
|
||||||
if (!isClean()) {
|
if (!isClean()) {
|
||||||
clean_index_ = index_;
|
clean_index_ = index_;
|
||||||
if (callbacks_.clean_changed) callbacks_.clean_changed(true);
|
cleanChanged(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UndoStack::setIndex(int index) {
|
void UndoStack::setIndex(int index) {
|
||||||
bool was_clean = isClean();
|
bool was_clean = isClean();
|
||||||
index_ = index;
|
index_ = index;
|
||||||
if (callbacks_.index_changed) callbacks_.index_changed();
|
indexChanged();
|
||||||
if (isClean() != was_clean && callbacks_.clean_changed) callbacks_.clean_changed(isClean());
|
if (isClean() != was_clean) cleanChanged(isClean());
|
||||||
}
|
}
|
||||||
|
|
||||||
UndoStack *UndoStack::instance() {
|
UndoStack *UndoStack::instance() {
|
||||||
@@ -51,19 +52,7 @@ UndoStack *UndoStack::instance() {
|
|||||||
return &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 std::string &name, int size,
|
EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size,
|
||||||
const std::string &node, const std::string &comment)
|
const std::string &node, const std::string &comment)
|
||||||
@@ -90,7 +79,7 @@ void EditMsgCommand::redo() {
|
|||||||
dbc()->updateMsg(id, new_name, new_size, new_node, new_comment);
|
dbc()->updateMsg(id, new_name, new_size, new_node, new_comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveMsgCommand
|
|
||||||
|
|
||||||
RemoveMsgCommand::RemoveMsgCommand(const MessageId &id) : id(id) {
|
RemoveMsgCommand::RemoveMsgCommand(const MessageId &id) : id(id) {
|
||||||
if (auto msg = dbc()->msg(id)) {
|
if (auto msg = dbc()->msg(id)) {
|
||||||
@@ -112,7 +101,7 @@ void RemoveMsgCommand::redo() {
|
|||||||
dbc()->removeMsg(id);
|
dbc()->removeMsg(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSigCommand
|
|
||||||
|
|
||||||
AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig)
|
AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig)
|
||||||
: id(id), signal(sig) {
|
: id(id), signal(sig) {
|
||||||
@@ -134,7 +123,7 @@ void AddSigCommand::redo() {
|
|||||||
dbc()->addSignal(id, signal);
|
dbc()->addSignal(id, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveSigCommand
|
|
||||||
|
|
||||||
RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig) : id(id) {
|
RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig) : id(id) {
|
||||||
sigs.push_back(*sig);
|
sigs.push_back(*sig);
|
||||||
@@ -151,13 +140,13 @@ RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *si
|
|||||||
void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); }
|
void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); }
|
||||||
void RemoveSigCommand::redo() { for (const auto &s : sigs) dbc()->removeSignal(id, s.name); }
|
void RemoveSigCommand::redo() { for (const auto &s : sigs) dbc()->removeSignal(id, s.name); }
|
||||||
|
|
||||||
// EditSignalCommand
|
|
||||||
|
|
||||||
EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig)
|
EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig)
|
||||||
: id(id) {
|
: id(id) {
|
||||||
sigs.push_back({*sig, new_sig});
|
sigs.push_back({*sig, new_sig});
|
||||||
if (sig->type == cabana::Signal::Type::Multiplexor && new_sig.type == cabana::Signal::Type::Normal) {
|
if (sig->type == cabana::Signal::Type::Multiplexor && new_sig.type == cabana::Signal::Type::Normal) {
|
||||||
// convert all multiplexed signals to normal signals
|
|
||||||
auto msg = dbc()->msg(id);
|
auto msg = dbc()->msg(id);
|
||||||
assert(msg);
|
assert(msg);
|
||||||
for (const auto &s : msg->sigs) {
|
for (const auto &s : msg->sigs) {
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QObject>
|
#include "tools/cabana/core/observable.h"
|
||||||
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
#include "tools/cabana/dbc/dbcmanager.h"
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
|
|
||||||
@@ -21,12 +19,7 @@ public:
|
|||||||
|
|
||||||
class UndoStack {
|
class UndoStack {
|
||||||
public:
|
public:
|
||||||
struct Callbacks {
|
void push(UndoCommand *cmd);
|
||||||
std::function<void()> index_changed;
|
|
||||||
std::function<void(bool)> clean_changed;
|
|
||||||
};
|
|
||||||
|
|
||||||
void push(UndoCommand *cmd); // takes ownership and calls redo()
|
|
||||||
void undo();
|
void undo();
|
||||||
void redo();
|
void redo();
|
||||||
void clear();
|
void clear();
|
||||||
@@ -36,31 +29,18 @@ public:
|
|||||||
bool canRedo() const { return index_ < (int)commands_.size(); }
|
bool canRedo() const { return index_ < (int)commands_.size(); }
|
||||||
std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; }
|
std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; }
|
||||||
std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; }
|
std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; }
|
||||||
void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); }
|
|
||||||
static UndoStack *instance();
|
static UndoStack *instance();
|
||||||
|
|
||||||
|
Observable<> indexChanged;
|
||||||
|
Observable<bool> cleanChanged;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setIndex(int index);
|
void setIndex(int index);
|
||||||
std::vector<std::unique_ptr<UndoCommand>> commands_;
|
std::vector<std::unique_ptr<UndoCommand>> commands_;
|
||||||
int index_ = 0;
|
int index_ = 0;
|
||||||
int clean_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 {
|
class EditMsgCommand : public UndoCommand {
|
||||||
public:
|
public:
|
||||||
EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node,
|
EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node,
|
||||||
@@ -116,5 +96,5 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
const MessageId id;
|
const MessageId id;
|
||||||
std::vector<std::pair<cabana::Signal, cabana::Signal>> sigs; // {old_sig, new_sig}
|
std::vector<std::pair<cabana::Signal, cabana::Signal>> sigs;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ struct CabanaColor {
|
|||||||
return r == other.r && g == other.g && b == other.b && a == other.a;
|
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
|
||||||
struct Hsv { float hue; float saturation; float value; };
|
struct Hsv { float hue; float saturation; float value; };
|
||||||
Hsv hsv() const {
|
Hsv hsv() const {
|
||||||
const float red = r / 255.0f, green = g / 255.0f, blue = b / 255.0f;
|
const float red = r / 255.0f, green = g / 255.0f, blue = b / 255.0f;
|
||||||
|
|||||||
83
iqpilot/tools/cabana/core/observable.h
Normal file
83
iqpilot/tools/cabana/core/observable.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace observable_detail {
|
||||||
|
struct HandlerTable {
|
||||||
|
virtual ~HandlerTable() = default;
|
||||||
|
virtual void erase(int id) = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Connection {
|
||||||
|
public:
|
||||||
|
Connection() = default;
|
||||||
|
Connection(std::weak_ptr<observable_detail::HandlerTable> table, int id) : table_(std::move(table)), id_(id) {}
|
||||||
|
Connection(Connection &&other) noexcept { *this = std::move(other); }
|
||||||
|
Connection &operator=(Connection &&other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
disconnect();
|
||||||
|
table_ = std::move(other.table_);
|
||||||
|
id_ = std::exchange(other.id_, -1);
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
Connection(const Connection &) = delete;
|
||||||
|
Connection &operator=(const Connection &) = delete;
|
||||||
|
~Connection() { disconnect(); }
|
||||||
|
|
||||||
|
void disconnect() {
|
||||||
|
if (auto table = table_.lock()) table->erase(id_);
|
||||||
|
table_.reset();
|
||||||
|
id_ = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::weak_ptr<observable_detail::HandlerTable> table_;
|
||||||
|
int id_ = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Connections = std::vector<Connection>;
|
||||||
|
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
class Observable {
|
||||||
|
public:
|
||||||
|
using Handler = std::function<void(Args...)>;
|
||||||
|
|
||||||
|
Observable() = default;
|
||||||
|
Observable(const Observable &) = delete;
|
||||||
|
Observable &operator=(const Observable &) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] Connection connect(Handler handler) {
|
||||||
|
int id = table_->next_id++;
|
||||||
|
table_->handlers.emplace(id, std::make_shared<Handler>(std::move(handler)));
|
||||||
|
return Connection(table_, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void operator()(Args... args) const {
|
||||||
|
auto table = table_;
|
||||||
|
std::vector<int> ids;
|
||||||
|
ids.reserve(table->handlers.size());
|
||||||
|
for (const auto &[id, _] : table->handlers) ids.push_back(id);
|
||||||
|
for (int id : ids) {
|
||||||
|
auto it = table->handlers.find(id);
|
||||||
|
if (it == table->handlers.end()) continue;
|
||||||
|
auto handler = it->second;
|
||||||
|
(*handler)(args...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Table : observable_detail::HandlerTable {
|
||||||
|
std::map<int, std::shared_ptr<Handler>> handlers;
|
||||||
|
int next_id = 0;
|
||||||
|
void erase(int id) override { handlers.erase(id); }
|
||||||
|
};
|
||||||
|
std::shared_ptr<Table> table_ = std::make_shared<Table>();
|
||||||
|
};
|
||||||
@@ -5,18 +5,18 @@
|
|||||||
|
|
||||||
constexpr int LIGHT_THEME = 1;
|
constexpr int LIGHT_THEME = 1;
|
||||||
constexpr int DARK_THEME = 2;
|
constexpr int DARK_THEME = 2;
|
||||||
|
constexpr int STREAM_UPDATE_FPS = 30;
|
||||||
|
|
||||||
struct CabanaSettingsState {
|
struct CabanaSettingsState {
|
||||||
enum DragDirection { MsbFirst, LsbFirst, AlwaysLE, AlwaysBE };
|
enum DragDirection { MsbFirst, LsbFirst, AlwaysLE, AlwaysBE };
|
||||||
|
|
||||||
bool absolute_time = false;
|
bool absolute_time = false;
|
||||||
int fps = 10;
|
|
||||||
int max_cached_minutes = 30;
|
int max_cached_minutes = 30;
|
||||||
int chart_height = 200;
|
int chart_height = 200;
|
||||||
int chart_column_count = 1;
|
int chart_column_count = 1;
|
||||||
int chart_range = 3 * 60;
|
int chart_range = 3 * 60;
|
||||||
int chart_series_type = 0;
|
int chart_series_type = 0;
|
||||||
int theme = 0;
|
int theme = LIGHT_THEME;
|
||||||
int sparkline_range = 15;
|
int sparkline_range = 15;
|
||||||
bool multiple_lines_hex = false;
|
bool multiple_lines_hex = false;
|
||||||
bool log_livestream = true;
|
bool log_livestream = true;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ int numDecimals(double value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cabana::Msg
|
|
||||||
|
|
||||||
cabana::Msg::~Msg() {
|
cabana::Msg::~Msg() {
|
||||||
for (auto s : sigs) {
|
for (auto s : sigs) {
|
||||||
@@ -77,7 +77,7 @@ int cabana::Msg::indexOf(const cabana::Signal *sig) const {
|
|||||||
|
|
||||||
std::string cabana::Msg::newSignalName() {
|
std::string cabana::Msg::newSignalName() {
|
||||||
std::string new_name;
|
std::string new_name;
|
||||||
for (int i = 1; /**/; ++i) {
|
for (int i = 1; ; ++i) {
|
||||||
new_name = "NEW_SIGNAL_" + std::to_string(i);
|
new_name = "NEW_SIGNAL_" + std::to_string(i);
|
||||||
if (sig(new_name) == nullptr) break;
|
if (sig(new_name) == nullptr) break;
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ void cabana::Msg::update() {
|
|||||||
mask.assign(size, 0x00);
|
mask.assign(size, 0x00);
|
||||||
multiplexor = nullptr;
|
multiplexor = nullptr;
|
||||||
|
|
||||||
// sort signals
|
|
||||||
std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) {
|
std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) {
|
||||||
return std::tie(r->type, l->multiplex_value, l->start_bit, l->name) <
|
return std::tie(r->type, l->multiplex_value, l->start_bit, l->name) <
|
||||||
std::tie(l->type, r->multiplex_value, r->start_bit, r->name);
|
std::tie(l->type, r->multiplex_value, r->start_bit, r->name);
|
||||||
@@ -103,7 +103,7 @@ void cabana::Msg::update() {
|
|||||||
}
|
}
|
||||||
sig->update();
|
sig->update();
|
||||||
|
|
||||||
// update mask
|
|
||||||
int i = sig->msb / 8;
|
int i = sig->msb / 8;
|
||||||
int bits = sig->size;
|
int bits = sig->size;
|
||||||
while (i >= 0 && i < size && bits > 0) {
|
while (i >= 0 && i < size && bits > 0) {
|
||||||
@@ -131,7 +131,7 @@ void cabana::Msg::update() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cabana::Signal
|
|
||||||
|
|
||||||
void cabana::Signal::update() {
|
void cabana::Signal::update() {
|
||||||
updateMsbLsb(*this);
|
updateMsbLsb(*this);
|
||||||
@@ -150,7 +150,7 @@ void cabana::Signal::update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string 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);
|
int64_t raw_value = round((value - offset) / factor);
|
||||||
for (const auto &[val, desc] : val_desc) {
|
for (const auto &[val, desc] : val_desc) {
|
||||||
if (std::abs(raw_value - val) < 1e-6) {
|
if (std::abs(raw_value - val) < 1e-6) {
|
||||||
@@ -185,7 +185,7 @@ bool cabana::Signal::operator==(const cabana::Signal &other) const {
|
|||||||
multiplex_value == other.multiplex_value && type == other.type && receiver_name == other.receiver_name;
|
multiplex_value == other.multiplex_value && type == other.type && receiver_name == other.receiver_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
|
||||||
|
|
||||||
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig) {
|
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig) {
|
||||||
const int msb_byte = sig.msb / 8;
|
const int msb_byte = sig.msb / 8;
|
||||||
@@ -194,11 +194,11 @@ double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal
|
|||||||
const int lsb_byte = sig.lsb / 8;
|
const int lsb_byte = sig.lsb / 8;
|
||||||
uint64_t val = 0;
|
uint64_t val = 0;
|
||||||
|
|
||||||
// Fast path: signal fits in a single byte
|
|
||||||
if (msb_byte == lsb_byte) {
|
if (msb_byte == lsb_byte) {
|
||||||
val = (data[msb_byte] >> (sig.lsb & 7)) & ((1ULL << sig.size) - 1);
|
val = (data[msb_byte] >> (sig.lsb & 7)) & ((1ULL << sig.size) - 1);
|
||||||
} else {
|
} else {
|
||||||
// Multi-byte case: signal spans across multiple bytes
|
|
||||||
int bits = sig.size;
|
int bits = sig.size;
|
||||||
int i = msb_byte;
|
int i = msb_byte;
|
||||||
const int step = sig.is_little_endian ? -1 : 1;
|
const int step = sig.is_little_endian ? -1 : 1;
|
||||||
@@ -212,7 +212,7 @@ double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign extension (if needed)
|
|
||||||
if (sig.is_signed && (val & (1ULL << (sig.size - 1)))) {
|
if (sig.is_signed && (val & (1ULL << (sig.size - 1)))) {
|
||||||
val |= ~((1ULL << sig.size) - 1);
|
val |= ~((1ULL << sig.size) - 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public:
|
|||||||
int precision = 0;
|
int precision = 0;
|
||||||
CabanaColor color;
|
CabanaColor color;
|
||||||
|
|
||||||
// Multiplexed
|
|
||||||
int multiplex_value = 0;
|
int multiplex_value = 0;
|
||||||
Signal *multiplexor = nullptr;
|
Signal *multiplexor = nullptr;
|
||||||
};
|
};
|
||||||
@@ -81,9 +81,9 @@ public:
|
|||||||
cabana::Signal *multiplexor = nullptr;
|
cabana::Signal *multiplexor = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace cabana
|
}
|
||||||
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig);
|
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig);
|
||||||
void updateMsbLsb(cabana::Signal &s);
|
void updateMsbLsb(cabana::Signal &s);
|
||||||
inline int flipBitPos(int start_bit) { return 8 * (start_bit / 8) + 7 - start_bit % 8; }
|
inline int flipBitPos(int start_bit) { return 8 * (start_bit / 8) + 7 - start_bit % 8; }
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ bool commentComplete(const std::string &line) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
}
|
||||||
|
|
||||||
DBCFile::DBCFile(const std::string &dbc_file_name) {
|
DBCFile::DBCFile(const std::string &dbc_file_name) {
|
||||||
std::ifstream file(dbc_file_name, std::ios::binary);
|
std::ifstream file(dbc_file_name, std::ios::binary);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
fileChanged();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &name, const s
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
fileChanged();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,26 +40,26 @@ void DBCManager::close(const SourceSet &sources) {
|
|||||||
for (auto s : sources) {
|
for (auto s : sources) {
|
||||||
dbc_files[s] = nullptr;
|
dbc_files[s] = nullptr;
|
||||||
}
|
}
|
||||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
fileChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DBCManager::close(DBCFile *dbc_file) {
|
void DBCManager::close(DBCFile *dbc_file) {
|
||||||
for (auto &[_, f] : dbc_files) {
|
for (auto &[_, f] : dbc_files) {
|
||||||
if (f.get() == dbc_file) f = nullptr;
|
if (f.get() == dbc_file) f = nullptr;
|
||||||
}
|
}
|
||||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
fileChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DBCManager::closeAll() {
|
void DBCManager::closeAll() {
|
||||||
dbc_files.clear();
|
dbc_files.clear();
|
||||||
if (callbacks_.file_changed) callbacks_.file_changed();
|
fileChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) {
|
void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) {
|
||||||
if (auto m = msg(id)) {
|
if (auto m = msg(id)) {
|
||||||
if (auto s = m->addSignal(sig)) {
|
if (auto s = m->addSignal(sig)) {
|
||||||
if (callbacks_.signal_added) callbacks_.signal_added(id, s);
|
signalAdded(id, s);
|
||||||
if (callbacks_.mask_updated) callbacks_.mask_updated();
|
maskUpdated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,8 +67,8 @@ void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) {
|
|||||||
void DBCManager::updateSignal(const MessageId &id, const std::string &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 m = msg(id)) {
|
||||||
if (auto s = m->updateSignal(sig_name, sig)) {
|
if (auto s = m->updateSignal(sig_name, sig)) {
|
||||||
if (callbacks_.signal_updated) callbacks_.signal_updated(s);
|
signalUpdated(s);
|
||||||
if (callbacks_.mask_updated) callbacks_.mask_updated();
|
maskUpdated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,26 +76,26 @@ void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name,
|
|||||||
void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) {
|
void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) {
|
||||||
if (auto m = msg(id)) {
|
if (auto m = msg(id)) {
|
||||||
if (auto s = m->sig(sig_name)) {
|
if (auto s = m->sig(sig_name)) {
|
||||||
if (callbacks_.signal_removed) callbacks_.signal_removed(s);
|
signalRemoved(s);
|
||||||
m->removeSignal(sig_name);
|
m->removeSignal(sig_name);
|
||||||
if (callbacks_.mask_updated) callbacks_.mask_updated();
|
maskUpdated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DBCManager::updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &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);
|
auto dbc_file = findDBCFile(id);
|
||||||
assert(dbc_file); // This should be impossible
|
assert(dbc_file);
|
||||||
dbc_file->updateMsg(id, name, size, node, comment);
|
dbc_file->updateMsg(id, name, size, node, comment);
|
||||||
if (callbacks_.msg_updated) callbacks_.msg_updated(id);
|
msgUpdated(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DBCManager::removeMsg(const MessageId &id) {
|
void DBCManager::removeMsg(const MessageId &id) {
|
||||||
auto dbc_file = findDBCFile(id);
|
auto dbc_file = findDBCFile(id);
|
||||||
assert(dbc_file); // This should be impossible
|
assert(dbc_file);
|
||||||
dbc_file->removeMsg(id);
|
dbc_file->removeMsg(id);
|
||||||
if (callbacks_.msg_removed) callbacks_.msg_removed(id);
|
msgRemoved(id);
|
||||||
if (callbacks_.mask_updated) callbacks_.mask_updated();
|
maskUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string DBCManager::newMsgName(const MessageId &id) {
|
std::string DBCManager::newMsgName(const MessageId &id) {
|
||||||
@@ -126,7 +126,7 @@ cabana::Msg *DBCManager::msg(uint8_t source, const std::string &name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> DBCManager::signalNames() {
|
std::vector<std::string> DBCManager::signalNames() {
|
||||||
// Used for autocompletion
|
|
||||||
std::set<std::string> names;
|
std::set<std::string> names;
|
||||||
for (auto &f : allDBCFiles()) {
|
for (auto &f : allDBCFiles()) {
|
||||||
for (auto &[_, m] : f->getMessages()) {
|
for (auto &[_, m] : f->getMessages()) {
|
||||||
@@ -141,12 +141,19 @@ std::vector<std::string> DBCManager::signalNames() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int DBCManager::nonEmptyDBCCount() {
|
int DBCManager::nonEmptyDBCCount() {
|
||||||
auto files = allDBCFiles();
|
return nonEmptyDBCFiles().size();
|
||||||
return std::count_if(files.cbegin(), files.cend(), [](auto &f) { return !f->isEmpty(); });
|
}
|
||||||
|
|
||||||
|
std::vector<DBCFile *> DBCManager::nonEmptyDBCFiles() {
|
||||||
|
std::vector<DBCFile *> files;
|
||||||
|
for (auto f : allDBCFiles()) {
|
||||||
|
if (!f->isEmpty()) files.push_back(f);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
DBCFile *DBCManager::findDBCFile(const uint8_t source) {
|
DBCFile *DBCManager::findDBCFile(const uint8_t source) {
|
||||||
// Find DBC file that matches id.source, fall back to SOURCE_ALL if no specific DBC is found
|
|
||||||
auto it = dbc_files.count(source) ? dbc_files.find(source) : dbc_files.find(-1);
|
auto it = dbc_files.count(source) ? dbc_files.find(source) : dbc_files.find(-1);
|
||||||
return it != dbc_files.end() ? it->second.get() : nullptr;
|
return it != dbc_files.end() ? it->second.get() : nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "tools/cabana/core/observable.h"
|
||||||
#include "tools/cabana/dbc/dbcfile.h"
|
#include "tools/cabana/dbc/dbcfile.h"
|
||||||
|
|
||||||
typedef std::set<int> SourceSet;
|
typedef std::set<int> SourceSet;
|
||||||
@@ -15,16 +15,6 @@ inline bool operator<(const std::shared_ptr<DBCFile> &l, const std::shared_ptr<D
|
|||||||
|
|
||||||
class DBCManager {
|
class DBCManager {
|
||||||
public:
|
public:
|
||||||
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;
|
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 &dbc_file_name, std::string *error = nullptr);
|
||||||
bool open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error = nullptr);
|
bool open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error = nullptr);
|
||||||
@@ -49,16 +39,23 @@ public:
|
|||||||
std::vector<std::string> signalNames();
|
std::vector<std::string> signalNames();
|
||||||
inline int dbcCount() { return allDBCFiles().size(); }
|
inline int dbcCount() { return allDBCFiles().size(); }
|
||||||
int nonEmptyDBCCount();
|
int nonEmptyDBCCount();
|
||||||
|
std::vector<DBCFile *> nonEmptyDBCFiles();
|
||||||
|
|
||||||
const SourceSet sources(const DBCFile *dbc_file) const;
|
const SourceSet sources(const DBCFile *dbc_file) const;
|
||||||
DBCFile *findDBCFile(const uint8_t source);
|
DBCFile *findDBCFile(const uint8_t source);
|
||||||
inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); }
|
inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); }
|
||||||
std::set<DBCFile *> allDBCFiles();
|
std::set<DBCFile *> allDBCFiles();
|
||||||
void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); }
|
|
||||||
|
Observable<MessageId, const cabana::Signal *> signalAdded;
|
||||||
|
Observable<const cabana::Signal *> signalRemoved;
|
||||||
|
Observable<const cabana::Signal *> signalUpdated;
|
||||||
|
Observable<MessageId> msgUpdated;
|
||||||
|
Observable<MessageId> msgRemoved;
|
||||||
|
Observable<> fileChanged;
|
||||||
|
Observable<> maskUpdated;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::map<int, std::shared_ptr<DBCFile>> dbc_files;
|
std::map<int, std::shared_ptr<DBCFile>> dbc_files;
|
||||||
Callbacks callbacks_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
DBCManager *dbc();
|
DBCManager *dbc();
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#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,53 +0,0 @@
|
|||||||
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,324 +0,0 @@
|
|||||||
#include "tools/cabana/detailwidget.h"
|
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QRadioButton>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QToolBar>
|
|
||||||
|
|
||||||
#include "tools/cabana/commands.h"
|
|
||||||
#include "tools/cabana/mainwin.h"
|
|
||||||
|
|
||||||
// DetailWidget
|
|
||||||
|
|
||||||
DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) {
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
|
|
||||||
// tabbar
|
|
||||||
tabbar = new TabBar(this);
|
|
||||||
tabbar->setUsesScrollButtons(true);
|
|
||||||
tabbar->setAutoHide(true);
|
|
||||||
tabbar->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
||||||
main_layout->addWidget(tabbar);
|
|
||||||
|
|
||||||
createToolBar();
|
|
||||||
|
|
||||||
// warning
|
|
||||||
warning_widget = new QWidget(this);
|
|
||||||
QHBoxLayout *warning_hlayout = new QHBoxLayout(warning_widget);
|
|
||||||
warning_hlayout->addWidget(warning_icon = new QLabel(this), 0, Qt::AlignTop);
|
|
||||||
warning_hlayout->addWidget(warning_label = new QLabel(this), 1, Qt::AlignLeft);
|
|
||||||
warning_widget->hide();
|
|
||||||
main_layout->addWidget(warning_widget);
|
|
||||||
|
|
||||||
// msg widget
|
|
||||||
splitter = new QSplitter(Qt::Vertical, this);
|
|
||||||
splitter->addWidget(binary_view = new BinaryView(this));
|
|
||||||
splitter->addWidget(signal_view = new SignalView(charts, this));
|
|
||||||
binary_view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
|
|
||||||
signal_view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
|
||||||
splitter->setStretchFactor(0, 0);
|
|
||||||
splitter->setStretchFactor(1, 1);
|
|
||||||
|
|
||||||
tab_widget = new QTabWidget(this);
|
|
||||||
tab_widget->setStyleSheet("QTabWidget::pane {border: none; margin-bottom: -2px;}");
|
|
||||||
tab_widget->setTabPosition(QTabWidget::South);
|
|
||||||
tab_widget->addTab(splitter, utils::icon("file-earmark-ruled"), "&Msg");
|
|
||||||
tab_widget->addTab(history_log = new LogsWidget(this), utils::icon("stopwatch"), "&Logs");
|
|
||||||
main_layout->addWidget(tab_widget);
|
|
||||||
|
|
||||||
QObject::connect(binary_view, &BinaryView::signalHovered, signal_view, &SignalView::signalHovered);
|
|
||||||
QObject::connect(binary_view, &BinaryView::signalClicked, [this](const cabana::Signal *s) { signal_view->selectSignal(s, true); });
|
|
||||||
QObject::connect(binary_view, &BinaryView::editSignal, signal_view->model, &SignalModel::saveSignal);
|
|
||||||
QObject::connect(binary_view, &BinaryView::showChart, charts, &ChartsWidget::showChart);
|
|
||||||
QObject::connect(signal_view, &SignalView::showChart, charts, &ChartsWidget::showChart);
|
|
||||||
QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight);
|
|
||||||
QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); });
|
|
||||||
QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState);
|
|
||||||
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) {
|
|
||||||
setMessage(tabbar->tabData(index).value<MessageId>());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
QObject::connect(tabbar, &QTabBar::tabCloseRequested, tabbar, &QTabBar::removeTab);
|
|
||||||
QObject::connect(charts, &ChartsWidget::seriesChanged, signal_view, &SignalView::updateChartState);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailWidget::createToolBar() {
|
|
||||||
QToolBar *toolbar = new QToolBar(this);
|
|
||||||
int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize);
|
|
||||||
toolbar->setIconSize({icon_size, icon_size});
|
|
||||||
toolbar->addWidget(name_label = new ElidedLabel(this));
|
|
||||||
name_label->setStyleSheet("QLabel{font-weight:bold;}");
|
|
||||||
|
|
||||||
QWidget *spacer = new QWidget();
|
|
||||||
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
|
||||||
toolbar->addWidget(spacer);
|
|
||||||
|
|
||||||
// Heatmap label and radio buttons
|
|
||||||
toolbar->addWidget(new QLabel(tr("Heatmap:"), this));
|
|
||||||
auto *heatmap_live = new QRadioButton(tr("Live"), this);
|
|
||||||
auto *heatmap_all = new QRadioButton(tr("All"), this);
|
|
||||||
heatmap_live->setChecked(true);
|
|
||||||
|
|
||||||
toolbar->addWidget(heatmap_live);
|
|
||||||
toolbar->addWidget(heatmap_all);
|
|
||||||
|
|
||||||
// Edit and remove buttons
|
|
||||||
toolbar->addSeparator();
|
|
||||||
toolbar->addAction(utils::icon("pencil"), tr("Edit Message"), this, &DetailWidget::editMsg);
|
|
||||||
action_remove_msg = toolbar->addAction(utils::icon("x-lg"), tr("Remove Message"), this, &DetailWidget::removeMsg);
|
|
||||||
|
|
||||||
layout()->addWidget(toolbar);
|
|
||||||
|
|
||||||
connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); });
|
|
||||||
connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional<std::pair<double, double>> &range) {
|
|
||||||
auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All";
|
|
||||||
heatmap_all->setText(text);
|
|
||||||
(range ? heatmap_all : heatmap_live)->setChecked(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailWidget::showTabBarContextMenu(const QPoint &pt) {
|
|
||||||
int index = tabbar->tabAt(pt);
|
|
||||||
if (index >= 0) {
|
|
||||||
QMenu menu(this);
|
|
||||||
menu.addAction(tr("Close Other Tabs"));
|
|
||||||
if (menu.exec(tabbar->mapToGlobal(pt))) {
|
|
||||||
tabbar->moveTab(index, 0);
|
|
||||||
tabbar->setCurrentIndex(0);
|
|
||||||
while (tabbar->count() > 1) {
|
|
||||||
tabbar->removeTab(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int DetailWidget::findOrAddTab(const MessageId& message_id) {
|
|
||||||
int index = tabbar->count() - 1;
|
|
||||||
for (/**/; index >= 0; --index) {
|
|
||||||
if (tabbar->tabData(index).value<MessageId>() == message_id) break;
|
|
||||||
}
|
|
||||||
if (index == -1) {
|
|
||||||
index = tabbar->addTab(QString::fromStdString(message_id.toString()));
|
|
||||||
tabbar->setTabData(index, QVariant::fromValue(message_id));
|
|
||||||
tabbar->setTabToolTip(index, QString::fromStdString(msgName(message_id)));
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailWidget::setMessage(const MessageId &message_id) {
|
|
||||||
if (std::exchange(msg_id, message_id) == message_id) return;
|
|
||||||
|
|
||||||
tabbar->blockSignals(true);
|
|
||||||
int index = findOrAddTab(message_id);
|
|
||||||
tabbar->setCurrentIndex(index);
|
|
||||||
tabbar->blockSignals(false);
|
|
||||||
|
|
||||||
setUpdatesEnabled(false);
|
|
||||||
signal_view->setMessage(msg_id);
|
|
||||||
binary_view->setMessage(msg_id);
|
|
||||||
history_log->setMessage(msg_id);
|
|
||||||
refresh();
|
|
||||||
setUpdatesEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::pair<QString, QStringList> DetailWidget::serializeMessageIds() const {
|
|
||||||
QStringList msgs;
|
|
||||||
for (int i = 0; i < tabbar->count(); ++i) {
|
|
||||||
MessageId id = tabbar->tabData(i).value<MessageId>();
|
|
||||||
msgs.append(QString::fromStdString(id.toString()));
|
|
||||||
}
|
|
||||||
return std::make_pair(QString::fromStdString(msg_id.toString()), msgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailWidget::restoreTabs(const QString active_msg_id, const QStringList& msg_ids) {
|
|
||||||
tabbar->blockSignals(true);
|
|
||||||
for (const auto& str_id : msg_ids) {
|
|
||||||
MessageId id = MessageId::fromString(str_id.toStdString());
|
|
||||||
if (dbc()->msg(id) != nullptr)
|
|
||||||
findOrAddTab(id);
|
|
||||||
}
|
|
||||||
tabbar->blockSignals(false);
|
|
||||||
|
|
||||||
auto active_id = MessageId::fromString(active_msg_id.toStdString());
|
|
||||||
if (dbc()->msg(active_id) != nullptr)
|
|
||||||
setMessage(active_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailWidget::refresh() {
|
|
||||||
QStringList warnings;
|
|
||||||
auto msg = dbc()->msg(msg_id);
|
|
||||||
if (msg) {
|
|
||||||
if (msg_id.source == INVALID_SOURCE) {
|
|
||||||
warnings.push_back(tr("No messages received."));
|
|
||||||
} else if (msg->size != can->lastMessage(msg_id).dat.size()) {
|
|
||||||
warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size));
|
|
||||||
}
|
|
||||||
for (auto s : binary_view->getOverlappingSignals()) {
|
|
||||||
warnings.push_back(tr("%1 has overlapping bits.").arg(QString::fromStdString(s->name)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QString msg_name = msg ? QString("%1 (%2)").arg(QString::fromStdString(msg->name), QString::fromStdString(msg->transmitter)) : QString::fromStdString(msgName(msg_id));
|
|
||||||
name_label->setText(msg_name);
|
|
||||||
name_label->setToolTip(msg_name);
|
|
||||||
action_remove_msg->setEnabled(msg != nullptr);
|
|
||||||
|
|
||||||
if (!warnings.isEmpty()) {
|
|
||||||
warning_label->setText(warnings.join('\n'));
|
|
||||||
warning_icon->setPixmap(utils::icon(msg ? "exclamation-triangle" : "info-circle"));
|
|
||||||
}
|
|
||||||
warning_widget->setVisible(!warnings.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailWidget::updateState(const std::set<MessageId> *msgs) {
|
|
||||||
if ((msgs && !msgs->count(msg_id)))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (tab_widget->currentIndex() == 0)
|
|
||||||
binary_view->updateState();
|
|
||||||
else
|
|
||||||
history_log->updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailWidget::editMsg() {
|
|
||||||
auto msg = dbc()->msg(msg_id);
|
|
||||||
int size = msg ? msg->size : can->lastMessage(msg_id).dat.size();
|
|
||||||
EditMessageDialog dlg(msg_id, QString::fromStdString(msgName(msg_id)), size, this);
|
|
||||||
if (dlg.exec()) {
|
|
||||||
UndoStack::instance()->push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(),
|
|
||||||
dlg.node->text().trimmed().toStdString(), dlg.comment_edit->toPlainText().trimmed().toStdString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DetailWidget::removeMsg() {
|
|
||||||
UndoStack::instance()->push(new RemoveMsgCommand(msg_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditMessageDialog
|
|
||||||
|
|
||||||
EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent)
|
|
||||||
: original_name(title), msg_id(msg_id), QDialog(parent) {
|
|
||||||
setWindowTitle(tr("Edit message: %1").arg(QString::fromStdString(msg_id.toString())));
|
|
||||||
QFormLayout *form_layout = new QFormLayout(this);
|
|
||||||
|
|
||||||
form_layout->addRow("", error_label = new QLabel);
|
|
||||||
error_label->setVisible(false);
|
|
||||||
form_layout->addRow(tr("Name"), name_edit = new QLineEdit(title, this));
|
|
||||||
name_edit->setValidator(new NameValidator(name_edit));
|
|
||||||
|
|
||||||
form_layout->addRow(tr("Size"), size_spin = new QSpinBox(this));
|
|
||||||
size_spin->setRange(1, CAN_MAX_DATA_BYTES);
|
|
||||||
size_spin->setValue(size);
|
|
||||||
|
|
||||||
form_layout->addRow(tr("Node"), node = new QLineEdit(this));
|
|
||||||
node->setValidator(new NameValidator(name_edit));
|
|
||||||
form_layout->addRow(tr("Comment"), comment_edit = new QTextEdit(this));
|
|
||||||
form_layout->addRow(btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel));
|
|
||||||
|
|
||||||
if (auto msg = dbc()->msg(msg_id)) {
|
|
||||||
node->setText(QString::fromStdString(msg->transmitter));
|
|
||||||
comment_edit->setText(QString::fromStdString(msg->comment));
|
|
||||||
}
|
|
||||||
validateName(name_edit->text());
|
|
||||||
setFixedWidth(parent->width() * 0.9);
|
|
||||||
connect(name_edit, &QLineEdit::textEdited, this, &EditMessageDialog::validateName);
|
|
||||||
connect(btn_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
|
||||||
connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditMessageDialog::validateName(const QString &text) {
|
|
||||||
bool valid = text.compare(QString::fromStdString(UNTITLED), Qt::CaseInsensitive) != 0;
|
|
||||||
error_label->setVisible(false);
|
|
||||||
if (!text.isEmpty() && valid && text != original_name) {
|
|
||||||
valid = dbc()->msg(msg_id.source, text.toStdString()) == nullptr;
|
|
||||||
if (!valid) {
|
|
||||||
error_label->setText(tr("Name already exists"));
|
|
||||||
error_label->setVisible(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
btn_box->button(QDialogButtonBox::Ok)->setEnabled(valid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CenterWidget
|
|
||||||
|
|
||||||
CenterWidget::CenterWidget(QWidget *parent) : QWidget(parent) {
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
main_layout->addWidget(welcome_widget = createWelcomeWidget());
|
|
||||||
}
|
|
||||||
|
|
||||||
DetailWidget* CenterWidget::ensureDetailWidget() {
|
|
||||||
if (!detail_widget) {
|
|
||||||
delete welcome_widget;
|
|
||||||
welcome_widget = nullptr;
|
|
||||||
layout()->addWidget(detail_widget = new DetailWidget(((MainWindow*)parentWidget())->charts_widget, this));
|
|
||||||
}
|
|
||||||
return detail_widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CenterWidget::clear() {
|
|
||||||
delete detail_widget;
|
|
||||||
detail_widget = nullptr;
|
|
||||||
if (!welcome_widget) {
|
|
||||||
layout()->addWidget(welcome_widget = createWelcomeWidget());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget *CenterWidget::createWelcomeWidget() {
|
|
||||||
QWidget *w = new QWidget(this);
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(w);
|
|
||||||
main_layout->addStretch(0);
|
|
||||||
QLabel *logo = new QLabel("CABANA");
|
|
||||||
logo->setAlignment(Qt::AlignCenter);
|
|
||||||
logo->setStyleSheet("font-size:50px;font-weight:bold;");
|
|
||||||
main_layout->addWidget(logo);
|
|
||||||
|
|
||||||
auto newShortcutRow = [](const QString &title, const QString &key) {
|
|
||||||
QHBoxLayout *hlayout = new QHBoxLayout();
|
|
||||||
auto btn = new QToolButton();
|
|
||||||
btn->setText(key);
|
|
||||||
btn->setEnabled(false);
|
|
||||||
hlayout->addWidget(new QLabel(title), 0, Qt::AlignRight);
|
|
||||||
hlayout->addWidget(btn, 0, Qt::AlignLeft);
|
|
||||||
return hlayout;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto lb = new QLabel(tr("<-Select a message to view details"));
|
|
||||||
lb->setAlignment(Qt::AlignHCenter);
|
|
||||||
main_layout->addWidget(lb);
|
|
||||||
main_layout->addLayout(newShortcutRow("Pause", "Space"));
|
|
||||||
main_layout->addLayout(newShortcutRow("Help", "F1"));
|
|
||||||
main_layout->addLayout(newShortcutRow("WhatsThis", "Shift+F1"));
|
|
||||||
main_layout->addStretch(0);
|
|
||||||
|
|
||||||
w->setStyleSheet("QLabel{color:darkGray;}");
|
|
||||||
w->setBackgroundRole(QPalette::Base);
|
|
||||||
w->setAutoFillBackground(true);
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QDialogButtonBox>
|
|
||||||
#include <QSplitter>
|
|
||||||
#include <QTabWidget>
|
|
||||||
#include <QTextEdit>
|
|
||||||
#include <set>
|
|
||||||
|
|
||||||
#include "tools/cabana/binaryview.h"
|
|
||||||
#include "tools/cabana/chart/chartswidget.h"
|
|
||||||
#include "tools/cabana/historylog.h"
|
|
||||||
#include "tools/cabana/signalview.h"
|
|
||||||
#include "tools/cabana/utils/elidedlabel.h"
|
|
||||||
|
|
||||||
class EditMessageDialog : public QDialog {
|
|
||||||
public:
|
|
||||||
EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent);
|
|
||||||
void validateName(const QString &text);
|
|
||||||
|
|
||||||
MessageId msg_id;
|
|
||||||
QString original_name;
|
|
||||||
QDialogButtonBox *btn_box;
|
|
||||||
QLineEdit *name_edit;
|
|
||||||
QLineEdit *node;
|
|
||||||
QTextEdit *comment_edit;
|
|
||||||
QLabel *error_label;
|
|
||||||
QSpinBox *size_spin;
|
|
||||||
};
|
|
||||||
|
|
||||||
class DetailWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
DetailWidget(ChartsWidget *charts, QWidget *parent);
|
|
||||||
void setMessage(const MessageId &message_id);
|
|
||||||
void refresh();
|
|
||||||
std::pair<QString, QStringList> serializeMessageIds() const;
|
|
||||||
void restoreTabs(const QString active_msg_id, const QStringList &msg_ids);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void createToolBar();
|
|
||||||
int findOrAddTab(const MessageId& message_id);
|
|
||||||
void showTabBarContextMenu(const QPoint &pt);
|
|
||||||
void editMsg();
|
|
||||||
void removeMsg();
|
|
||||||
void updateState(const std::set<MessageId> *msgs = nullptr);
|
|
||||||
|
|
||||||
MessageId msg_id;
|
|
||||||
QLabel *warning_icon, *warning_label;
|
|
||||||
ElidedLabel *name_label;
|
|
||||||
QWidget *warning_widget;
|
|
||||||
TabBar *tabbar;
|
|
||||||
QTabWidget *tab_widget;
|
|
||||||
QAction *action_remove_msg;
|
|
||||||
LogsWidget *history_log;
|
|
||||||
BinaryView *binary_view;
|
|
||||||
SignalView *signal_view;
|
|
||||||
ChartsWidget *charts;
|
|
||||||
QSplitter *splitter;
|
|
||||||
};
|
|
||||||
|
|
||||||
class CenterWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
CenterWidget(QWidget *parent);
|
|
||||||
void setMessage(const MessageId &message_id) { ensureDetailWidget()->setMessage(message_id); }
|
|
||||||
DetailWidget* getDetailWidget() { return detail_widget; }
|
|
||||||
DetailWidget* ensureDetailWidget();
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QWidget *createWelcomeWidget();
|
|
||||||
DetailWidget *detail_widget = nullptr;
|
|
||||||
QWidget *welcome_widget = nullptr;
|
|
||||||
};
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
#include "tools/cabana/historylog.h"
|
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "tools/cabana/commands.h"
|
|
||||||
#include "tools/cabana/utils/export.h"
|
|
||||||
|
|
||||||
QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
|
|
||||||
const auto &m = messages[index.row()];
|
|
||||||
const int col = index.column();
|
|
||||||
if (role == Qt::DisplayRole) {
|
|
||||||
if (col == 0) return QString::number(can->toSeconds(m.mono_time), 'f', 3);
|
|
||||||
if (!isHexMode()) return QString::fromStdString(sigs[col - 1]->formatValue(m.sig_values[col - 1], false));
|
|
||||||
} else if (role == Qt::TextAlignmentRole) {
|
|
||||||
return (uint32_t)(Qt::AlignRight | Qt::AlignVCenter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHexMode() && col == 1) {
|
|
||||||
if (role == ColorsRole) return QVariant::fromValue((void *)(&m.colors));
|
|
||||||
if (role == BytesRole) return QVariant::fromValue((void *)(&m.data));
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void HistoryLogModel::setMessage(const MessageId &message_id) {
|
|
||||||
msg_id = message_id;
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HistoryLogModel::reset() {
|
|
||||||
beginResetModel();
|
|
||||||
sigs.clear();
|
|
||||||
if (auto dbc_msg = dbc()->msg(msg_id)) {
|
|
||||||
sigs = dbc_msg->getSignals();
|
|
||||||
}
|
|
||||||
messages.clear();
|
|
||||||
hex_colors = {};
|
|
||||||
endResetModel();
|
|
||||||
setFilter(0, "", nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
||||||
if (orientation == Qt::Horizontal) {
|
|
||||||
if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
|
|
||||||
if (section == 0) return "Time";
|
|
||||||
if (isHexMode()) return "Data";
|
|
||||||
|
|
||||||
QString name = QString::fromStdString(sigs[section - 1]->name);
|
|
||||||
QString unit = QString::fromStdString(sigs[section - 1]->unit);
|
|
||||||
return unit.isEmpty() ? name : QString("%1 (%2)").arg(name, unit);
|
|
||||||
} else if (role == Qt::BackgroundRole && section > 0 && !isHexMode()) {
|
|
||||||
// Alpha-blend the signal color with the background to ensure contrast
|
|
||||||
QColor sigColor = toQColor(sigs[section - 1]->color);
|
|
||||||
sigColor.setAlpha(128);
|
|
||||||
return QBrush(sigColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void HistoryLogModel::setHexMode(bool hex) {
|
|
||||||
hex_mode = hex;
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HistoryLogModel::setFilter(int sig_idx, const QString &value, std::function<bool(double, double)> cmp) {
|
|
||||||
filter_sig_idx = sig_idx;
|
|
||||||
filter_value = value.toDouble();
|
|
||||||
filter_cmp = value.isEmpty() ? nullptr : cmp;
|
|
||||||
updateState(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HistoryLogModel::updateState(bool clear) {
|
|
||||||
if (clear && !messages.empty()) {
|
|
||||||
beginRemoveRows({}, 0, messages.size() - 1);
|
|
||||||
messages.clear();
|
|
||||||
endRemoveRows();
|
|
||||||
}
|
|
||||||
uint64_t current_time = can->toMonoTime(can->lastMessage(msg_id).ts) + 1;
|
|
||||||
fetchData(messages.begin(), current_time, messages.empty() ? 0 : messages.front().mono_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HistoryLogModel::canFetchMore(const QModelIndex &parent) const {
|
|
||||||
const auto &events = can->events(msg_id);
|
|
||||||
return !events.empty() && !messages.empty() && messages.back().mono_time > events.front()->mono_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HistoryLogModel::fetchMore(const QModelIndex &parent) {
|
|
||||||
if (!messages.empty())
|
|
||||||
fetchData(messages.end(), messages.back().mono_time, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HistoryLogModel::fetchData(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time) {
|
|
||||||
const auto &events = can->events(msg_id);
|
|
||||||
auto first = std::upper_bound(events.rbegin(), events.rend(), from_time, [](uint64_t ts, auto e) {
|
|
||||||
return ts > e->mono_time;
|
|
||||||
});
|
|
||||||
|
|
||||||
std::vector<HistoryLogModel::Message> msgs;
|
|
||||||
std::vector<double> values(sigs.size());
|
|
||||||
msgs.reserve(batch_size);
|
|
||||||
for (; first != events.rend() && (*first)->mono_time > min_time; ++first) {
|
|
||||||
const CanEvent *e = *first;
|
|
||||||
for (int i = 0; i < sigs.size(); ++i) {
|
|
||||||
sigs[i]->getValue(e->dat, e->size, &values[i]);
|
|
||||||
}
|
|
||||||
if (!filter_cmp || filter_cmp(values[filter_sig_idx], filter_value)) {
|
|
||||||
msgs.emplace_back(Message{e->mono_time, values, {e->dat, e->dat + e->size}});
|
|
||||||
if (msgs.size() >= batch_size && min_time == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!msgs.empty()) {
|
|
||||||
if (isHexMode() && (min_time > 0 || messages.empty())) {
|
|
||||||
const auto freq = can->lastMessage(msg_id).freq;
|
|
||||||
const std::vector<uint8_t> no_mask;
|
|
||||||
for (auto &m : msgs) {
|
|
||||||
hex_colors.compute(msg_id, m.data.data(), m.data.size(), m.mono_time / (double)1e9, can->getSpeed(), no_mask, freq);
|
|
||||||
m.colors = hex_colors.colors;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int pos = std::distance(messages.begin(), insert_pos);
|
|
||||||
beginInsertRows({}, pos , pos + msgs.size() - 1);
|
|
||||||
messages.insert(insert_pos, std::move_iterator(msgs.begin()), std::move_iterator(msgs.end()));
|
|
||||||
endInsertRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeaderView
|
|
||||||
|
|
||||||
QSize HeaderView::sectionSizeFromContents(int logicalIndex) const {
|
|
||||||
static const QSize time_col_size = fontMetrics().size(Qt::TextSingleLine, "000000.000") + QSize(10, 6);
|
|
||||||
if (logicalIndex == 0) {
|
|
||||||
return time_col_size;
|
|
||||||
} else {
|
|
||||||
int default_size = qMax(100, (rect().width() - time_col_size.width()) / (model()->columnCount() - 1));
|
|
||||||
QString text = model()->headerData(logicalIndex, this->orientation(), Qt::DisplayRole).toString();
|
|
||||||
const QRect rect = fontMetrics().boundingRect({0, 0, default_size, 2000}, defaultAlignment(), text.replace(QChar('_'), ' '));
|
|
||||||
QSize size = rect.size() + QSize{10, 6};
|
|
||||||
return QSize{qMax(size.width(), default_size), size.height()};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const {
|
|
||||||
auto bg_role = model()->headerData(logicalIndex, Qt::Horizontal, Qt::BackgroundRole);
|
|
||||||
if (bg_role.isValid()) {
|
|
||||||
painter->fillRect(rect, bg_role.value<QBrush>());
|
|
||||||
}
|
|
||||||
QString text = model()->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
|
|
||||||
painter->setPen(palette().color(utils::isDarkTheme() ? QPalette::BrightText : QPalette::Text));
|
|
||||||
painter->drawText(rect.adjusted(5, 3, -5, -3), defaultAlignment(), text.replace(QChar('_'), ' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogsWidget
|
|
||||||
|
|
||||||
LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) {
|
|
||||||
setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
main_layout->setSpacing(0);
|
|
||||||
|
|
||||||
QWidget *toolbar = new QWidget(this);
|
|
||||||
toolbar->setAutoFillBackground(true);
|
|
||||||
QHBoxLayout *h = new QHBoxLayout(toolbar);
|
|
||||||
|
|
||||||
filters_widget = new QWidget(this);
|
|
||||||
QHBoxLayout *filter_layout = new QHBoxLayout(filters_widget);
|
|
||||||
filter_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
filter_layout->addWidget(display_type_cb = new QComboBox(this));
|
|
||||||
filter_layout->addWidget(signals_cb = new QComboBox(this));
|
|
||||||
filter_layout->addWidget(comp_box = new QComboBox(this));
|
|
||||||
filter_layout->addWidget(value_edit = new QLineEdit(this));
|
|
||||||
h->addWidget(filters_widget);
|
|
||||||
h->addStretch(0);
|
|
||||||
export_btn = new ToolButton("filetype-csv", tr("Export to CSV file..."));
|
|
||||||
h->addWidget(export_btn, 0, Qt::AlignRight);
|
|
||||||
|
|
||||||
display_type_cb->addItems({"Signal", "Hex"});
|
|
||||||
display_type_cb->setToolTip(tr("Display signal value or raw hex value"));
|
|
||||||
comp_box->addItems({">", "=", "!=", "<"});
|
|
||||||
value_edit->setClearButtonEnabled(true);
|
|
||||||
value_edit->setValidator(new DoubleValidator(this));
|
|
||||||
|
|
||||||
main_layout->addWidget(toolbar);
|
|
||||||
QFrame *line = new QFrame(this);
|
|
||||||
line->setFrameStyle(QFrame::HLine | QFrame::Sunken);
|
|
||||||
main_layout->addWidget(line);
|
|
||||||
main_layout->addWidget(logs = new QTableView(this));
|
|
||||||
logs->setModel(model = new HistoryLogModel(this));
|
|
||||||
logs->setItemDelegate(delegate = new MessageBytesDelegate(this));
|
|
||||||
logs->setHorizontalHeader(new HeaderView(Qt::Horizontal, this));
|
|
||||||
logs->horizontalHeader()->setDefaultAlignment(Qt::AlignRight | (Qt::Alignment)Qt::TextWordWrap);
|
|
||||||
logs->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
|
||||||
logs->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
|
|
||||||
logs->verticalHeader()->setDefaultSectionSize(delegate->sizeForBytes(8).height());
|
|
||||||
logs->setFrameShape(QFrame::NoFrame);
|
|
||||||
|
|
||||||
QObject::connect(display_type_cb, qOverload<int>(&QComboBox::activated), model, &HistoryLogModel::setHexMode);
|
|
||||||
QObject::connect(signals_cb, SIGNAL(activated(int)), this, SLOT(filterChanged()));
|
|
||||||
QObject::connect(comp_box, SIGNAL(activated(int)), this, SLOT(filterChanged()));
|
|
||||||
QObject::connect(value_edit, &QLineEdit::textEdited, this, &LogsWidget::filterChanged);
|
|
||||||
QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV);
|
|
||||||
QObject::connect(can, &AbstractStream::seekedTo, 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); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogsWidget::modelReset() {
|
|
||||||
signals_cb->clear();
|
|
||||||
for (auto s : model->sigs) {
|
|
||||||
signals_cb->addItem(QString::fromStdString(s->name));
|
|
||||||
}
|
|
||||||
export_btn->setEnabled(false);
|
|
||||||
value_edit->clear();
|
|
||||||
comp_box->setCurrentIndex(0);
|
|
||||||
filters_widget->setVisible(!model->sigs.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogsWidget::filterChanged() {
|
|
||||||
if (value_edit->text().isEmpty() && !value_edit->isModified()) return;
|
|
||||||
|
|
||||||
std::function<bool(double, double)> cmp = nullptr;
|
|
||||||
switch (comp_box->currentIndex()) {
|
|
||||||
case 0: cmp = std::greater<double>{}; break;
|
|
||||||
case 1: cmp = std::equal_to<double>{}; break;
|
|
||||||
case 2: cmp = [](double l, double r) { return l != r; }; break; // not equal
|
|
||||||
case 3: cmp = std::less<double>{}; break;
|
|
||||||
}
|
|
||||||
model->setFilter(signals_cb->currentIndex(), value_edit->text(), cmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LogsWidget::exportToCSV() {
|
|
||||||
QString dir = QString("%1/%2_%3.csv").arg(QString::fromStdString(settings.last_dir)).arg(QString::fromStdString(can->routeName())).arg(QString::fromStdString(msgName(model->msg_id)));
|
|
||||||
QString fn = QFileDialog::getSaveFileName(this, QString("Export %1 to CSV file").arg(QString::fromStdString(msgName(model->msg_id))),
|
|
||||||
dir, tr("csv (*.csv)"));
|
|
||||||
if (!fn.isEmpty()) {
|
|
||||||
model->isHexMode() ? utils::exportToCSV(fn.toStdString(), model->msg_id)
|
|
||||||
: utils::exportSignalsToCSV(fn.toStdString(), model->msg_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <deque>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QTableView>
|
|
||||||
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
class HeaderView : public QHeaderView {
|
|
||||||
public:
|
|
||||||
HeaderView(Qt::Orientation orientation, QWidget *parent = nullptr) : QHeaderView(orientation, parent) {}
|
|
||||||
QSize sectionSizeFromContents(int logicalIndex) const override;
|
|
||||||
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
class HistoryLogModel : public QAbstractTableModel {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {}
|
|
||||||
void setMessage(const MessageId &message_id);
|
|
||||||
void updateState(bool clear = false);
|
|
||||||
void setFilter(int sig_idx, const QString &value, std::function<bool(double, double)> cmp);
|
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
void fetchMore(const QModelIndex &parent) override;
|
|
||||||
bool canFetchMore(const QModelIndex &parent) const override;
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return messages.size(); }
|
|
||||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return !isHexMode() ? sigs.size() + 1 : 2; }
|
|
||||||
inline bool isHexMode() const { return sigs.empty() || hex_mode; }
|
|
||||||
void reset();
|
|
||||||
void setHexMode(bool hex_mode);
|
|
||||||
|
|
||||||
struct Message {
|
|
||||||
uint64_t mono_time = 0;
|
|
||||||
std::vector<double> sig_values;
|
|
||||||
std::vector<uint8_t> data;
|
|
||||||
std::vector<CabanaColor> colors;
|
|
||||||
};
|
|
||||||
|
|
||||||
void fetchData(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time);
|
|
||||||
|
|
||||||
MessageId msg_id;
|
|
||||||
CanData hex_colors;
|
|
||||||
const int batch_size = 50;
|
|
||||||
int filter_sig_idx = -1;
|
|
||||||
double filter_value = 0;
|
|
||||||
std::function<bool(double, double)> filter_cmp = nullptr;
|
|
||||||
std::deque<Message> messages;
|
|
||||||
std::vector<cabana::Signal *> sigs;
|
|
||||||
bool hex_mode = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class LogsWidget : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
LogsWidget(QWidget *parent);
|
|
||||||
void setMessage(const MessageId &message_id) { model->setMessage(message_id); }
|
|
||||||
void updateState() { model->updateState(); }
|
|
||||||
void showEvent(QShowEvent *event) override { model->updateState(true); }
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void filterChanged();
|
|
||||||
void exportToCSV();
|
|
||||||
void modelReset();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QTableView *logs;
|
|
||||||
HistoryLogModel *model;
|
|
||||||
QComboBox *signals_cb, *comp_box, *display_type_cb;
|
|
||||||
QLineEdit *value_edit;
|
|
||||||
QWidget *filters_widget;
|
|
||||||
ToolButton *export_btn;
|
|
||||||
MessageBytesDelegate *delegate;
|
|
||||||
};
|
|
||||||
@@ -1,741 +0,0 @@
|
|||||||
#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 <QFileDialog>
|
|
||||||
#include <QMenuBar>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QProgressDialog>
|
|
||||||
#include <QResizeEvent>
|
|
||||||
#include <QShortcut>
|
|
||||||
#include <QTextDocument>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#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();
|
|
||||||
createDockWindows();
|
|
||||||
setCentralWidget(center_widget = new CenterWidget(this));
|
|
||||||
createActions();
|
|
||||||
createStatusBar();
|
|
||||||
createShortcuts();
|
|
||||||
|
|
||||||
// save default window state to allow resetting it
|
|
||||||
default_state = utils::toBytes(saveState());
|
|
||||||
|
|
||||||
// restore states; restoreGeometry() itself corrects stale off-screen geometry
|
|
||||||
restoreGeometry(utils::qbytes(settings.geometry));
|
|
||||||
restoreState(utils::qbytes(settings.window_state));
|
|
||||||
|
|
||||||
// install handlers
|
|
||||||
static auto static_main_win = this;
|
|
||||||
qRegisterMetaType<uint64_t>("uint64_t");
|
|
||||||
qRegisterMetaType<SourceSet>("SourceSet");
|
|
||||||
installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) {
|
|
||||||
emit static_main_win->updateProgressBar(cur, total, success);
|
|
||||||
});
|
|
||||||
installMessageHandler([](ReplyMsgType type, const std::string msg) {
|
|
||||||
emit static_main_win->showMessage(QString::fromStdString(msg), 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
setStyleSheet(QString(R"(QMainWindow::separator {
|
|
||||||
width: %1px; /* when vertical */
|
|
||||||
height: %1px; /* when horizontal */
|
|
||||||
})").arg(style()->pixelMetric(QStyle::PM_SplitterWidth)));
|
|
||||||
|
|
||||||
QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage);
|
|
||||||
QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress);
|
|
||||||
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(); });
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::loadFingerprints() {
|
|
||||||
std::ifstream json_file((QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json").toStdString());
|
|
||||||
if (!json_file) return;
|
|
||||||
const std::string contents{std::istreambuf_iterator<char>(json_file), std::istreambuf_iterator<char>()};
|
|
||||||
std::string err;
|
|
||||||
auto doc = json11::Json::parse(contents, err);
|
|
||||||
if (!err.empty() || !doc.is_object()) return;
|
|
||||||
fingerprint_to_dbc.clear();
|
|
||||||
for (const auto &kv : doc.object_items()) {
|
|
||||||
if (kv.second.is_string()) {
|
|
||||||
fingerprint_to_dbc.emplace(kv.first, kv.second.string_value());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::createActions() {
|
|
||||||
// File menu
|
|
||||||
QMenu *file_menu = menuBar()->addMenu(tr("&File"));
|
|
||||||
file_menu->addAction(tr("Open Stream..."), this, &MainWindow::selectAndOpenStream);
|
|
||||||
close_stream_act = file_menu->addAction(tr("Close stream"), this, &MainWindow::closeStream);
|
|
||||||
export_to_csv_act = file_menu->addAction(tr("Export to CSV..."), this, &MainWindow::exportToCSV);
|
|
||||||
close_stream_act->setEnabled(false);
|
|
||||||
export_to_csv_act->setEnabled(false);
|
|
||||||
file_menu->addSeparator();
|
|
||||||
|
|
||||||
file_menu->addAction(tr("New DBC File"), [this]() { newFile(); }, QKeySequence::New);
|
|
||||||
file_menu->addAction(tr("Open DBC File..."), [this]() { openFile(); }, QKeySequence::Open);
|
|
||||||
|
|
||||||
manage_dbcs_menu = file_menu->addMenu(tr("Manage &DBC Files"));
|
|
||||||
QObject::connect(manage_dbcs_menu, &QMenu::aboutToShow, this, &MainWindow::updateLoadSaveMenus);
|
|
||||||
|
|
||||||
open_recent_menu = file_menu->addMenu(tr("Open &Recent"));
|
|
||||||
QObject::connect(open_recent_menu, &QMenu::aboutToShow, this, &MainWindow::updateRecentFileMenu);
|
|
||||||
|
|
||||||
file_menu->addSeparator();
|
|
||||||
QMenu *load_iqdbc_menu = file_menu->addMenu(tr("Load DBC from commaai/iqdbc"));
|
|
||||||
// load_iqdbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }");
|
|
||||||
std::vector<std::string> dbc_names;
|
|
||||||
std::error_code ec;
|
|
||||||
for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH, ec)) {
|
|
||||||
if (entry.is_regular_file() && entry.path().extension() == ".dbc") {
|
|
||||||
dbc_names.push_back(entry.path().filename().string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::sort(dbc_names.begin(), dbc_names.end());
|
|
||||||
for (const auto &dbc_name : dbc_names) {
|
|
||||||
QString name = QString::fromStdString(dbc_name);
|
|
||||||
load_iqdbc_menu->addAction(name, [this, name]() { loadDBCFromOpendbc(name); });
|
|
||||||
}
|
|
||||||
|
|
||||||
file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); });
|
|
||||||
|
|
||||||
file_menu->addSeparator();
|
|
||||||
save_dbc = file_menu->addAction(tr("Save DBC..."), this, &MainWindow::save, QKeySequence::Save);
|
|
||||||
save_dbc_as = file_menu->addAction(tr("Save DBC As..."), this, &MainWindow::saveAs, QKeySequence::SaveAs);
|
|
||||||
copy_dbc_to_clipboard = file_menu->addAction(tr("Copy DBC To Clipboard"), this, &MainWindow::saveToClipboard);
|
|
||||||
|
|
||||||
file_menu->addSeparator();
|
|
||||||
file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption, QKeySequence::Preferences);
|
|
||||||
|
|
||||||
file_menu->addSeparator();
|
|
||||||
file_menu->addAction(tr("E&xit"), qApp, &QApplication::closeAllWindows, QKeySequence::Quit);
|
|
||||||
|
|
||||||
// Edit Menu
|
|
||||||
QMenu *edit_menu = menuBar()->addMenu(tr("&Edit"));
|
|
||||||
undo_act = edit_menu->addAction(tr("&Undo"), []() { UndoStack::instance()->undo(); });
|
|
||||||
undo_act->setShortcuts(QKeySequence::Undo);
|
|
||||||
redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); });
|
|
||||||
redo_act->setShortcuts(QKeySequence::Redo);
|
|
||||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &MainWindow::updateUndoRedoActions);
|
|
||||||
updateUndoRedoActions();
|
|
||||||
|
|
||||||
// View Menu
|
|
||||||
QMenu *view_menu = menuBar()->addMenu(tr("&View"));
|
|
||||||
auto act = view_menu->addAction(tr("Full Screen"), this, &MainWindow::toggleFullScreen, QKeySequence::FullScreen);
|
|
||||||
addAction(act);
|
|
||||||
view_menu->addSeparator();
|
|
||||||
view_menu->addAction(messages_dock->toggleViewAction());
|
|
||||||
view_menu->addAction(video_dock->toggleViewAction());
|
|
||||||
view_menu->addSeparator();
|
|
||||||
view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(utils::qbytes(default_state)); });
|
|
||||||
|
|
||||||
// Tools Menu
|
|
||||||
tools_menu = menuBar()->addMenu(tr("&Tools"));
|
|
||||||
tools_menu->addAction(tr("Find &Similar Bits"), this, &MainWindow::findSimilarBits);
|
|
||||||
tools_menu->addAction(tr("&Find Signal"), this, &MainWindow::findSignal);
|
|
||||||
|
|
||||||
// Help Menu
|
|
||||||
QMenu *help_menu = menuBar()->addMenu(tr("&Help"));
|
|
||||||
help_menu->addAction(tr("Help"), this, &MainWindow::onlineHelp, QKeySequence::HelpContents);
|
|
||||||
help_menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::createDockWindows() {
|
|
||||||
messages_dock = new QDockWidget(tr("MESSAGES"), this);
|
|
||||||
messages_dock->setObjectName("MessagesPanel");
|
|
||||||
messages_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea);
|
|
||||||
messages_dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
|
|
||||||
addDockWidget(Qt::LeftDockWidgetArea, messages_dock);
|
|
||||||
|
|
||||||
video_dock = new QDockWidget("", this);
|
|
||||||
video_dock->setObjectName(tr("VideoPanel"));
|
|
||||||
video_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
|
||||||
video_dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
|
|
||||||
addDockWidget(Qt::RightDockWidgetArea, video_dock);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::createDockWidgets() {
|
|
||||||
messages_widget = new MessagesWidget(this);
|
|
||||||
messages_dock->setWidget(messages_widget);
|
|
||||||
QObject::connect(messages_widget, &MessagesWidget::titleChanged, messages_dock, &QDockWidget::setWindowTitle);
|
|
||||||
QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, center_widget, &CenterWidget::setMessage);
|
|
||||||
|
|
||||||
// right panel
|
|
||||||
charts_widget = new ChartsWidget(this);
|
|
||||||
QWidget *charts_container = new QWidget(this);
|
|
||||||
charts_layout = new QVBoxLayout(charts_container);
|
|
||||||
charts_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
charts_layout->addWidget(charts_widget);
|
|
||||||
|
|
||||||
// splitter between video and charts
|
|
||||||
video_splitter = new QSplitter(Qt::Vertical, this);
|
|
||||||
video_widget = new VideoWidget(this);
|
|
||||||
video_splitter->addWidget(video_widget);
|
|
||||||
|
|
||||||
video_splitter->addWidget(charts_container);
|
|
||||||
video_splitter->setStretchFactor(1, 1);
|
|
||||||
video_splitter->restoreState(utils::qbytes(settings.video_splitter_state));
|
|
||||||
video_splitter->handle(1)->setEnabled(!can->liveStreaming());
|
|
||||||
video_dock->setWidget(video_splitter);
|
|
||||||
QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking);
|
|
||||||
QObject::connect(charts_widget, &ChartsWidget::showTip, video_widget, &VideoWidget::showThumbnail);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::createStatusBar() {
|
|
||||||
progress_bar = new QProgressBar();
|
|
||||||
progress_bar->setRange(0, 100);
|
|
||||||
progress_bar->setTextVisible(true);
|
|
||||||
progress_bar->setFixedSize({300, 16});
|
|
||||||
progress_bar->setVisible(false);
|
|
||||||
statusBar()->addWidget(new QLabel(tr("For Help, Press F1")));
|
|
||||||
statusBar()->addPermanentWidget(progress_bar);
|
|
||||||
statusBar()->addPermanentWidget(status_label = new QLabel(this));
|
|
||||||
updateStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::createShortcuts() {
|
|
||||||
auto shortcut = new QShortcut(QKeySequence(Qt::Key_Space), this, nullptr, nullptr, Qt::ApplicationShortcut);
|
|
||||||
QObject::connect(shortcut, &QShortcut::activated, this, []() {
|
|
||||||
if (can) can->pause(!can->isPaused());
|
|
||||||
});
|
|
||||||
// TODO: add more shortcuts here.
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::undoStackCleanChanged(bool clean) {
|
|
||||||
setWindowModified(!clean);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::updateUndoRedoActions() {
|
|
||||||
auto stack = UndoStack::instance();
|
|
||||||
undo_act->setEnabled(stack->canUndo());
|
|
||||||
undo_act->setText(stack->canUndo() ? tr("&Undo %1").arg(QString::fromStdString(stack->undoText())) : tr("&Undo"));
|
|
||||||
redo_act->setEnabled(stack->canRedo());
|
|
||||||
redo_act->setText(stack->canRedo() ? tr("&Redo %1").arg(QString::fromStdString(stack->redoText())) : tr("&Redo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::DBCFileChanged() {
|
|
||||||
UndoStack::instance()->clear();
|
|
||||||
|
|
||||||
// Update file menu
|
|
||||||
int cnt = dbc()->nonEmptyDBCCount();
|
|
||||||
save_dbc->setText(cnt > 1 ? tr("Save %1 DBCs...").arg(cnt) : tr("Save DBC..."));
|
|
||||||
save_dbc->setEnabled(cnt > 0);
|
|
||||||
save_dbc_as->setEnabled(cnt == 1);
|
|
||||||
// TODO: Support clipboard for multiple files
|
|
||||||
copy_dbc_to_clipboard->setEnabled(cnt == 1);
|
|
||||||
manage_dbcs_menu->setEnabled(dynamic_cast<DummyStream *>(can) == nullptr);
|
|
||||||
|
|
||||||
QStringList title;
|
|
||||||
for (auto f : dbc()->allDBCFiles()) {
|
|
||||||
title.push_back(tr("(%1) %2").arg(QString::fromStdString(toString(dbc()->sources(f))), QString::fromStdString(f->name())));
|
|
||||||
}
|
|
||||||
setWindowFilePath(title.join(" | "));
|
|
||||||
|
|
||||||
QTimer::singleShot(0, this, &::MainWindow::restoreSessionState);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::selectAndOpenStream() {
|
|
||||||
StreamSelector dlg(this);
|
|
||||||
if (dlg.exec()) {
|
|
||||||
openStream(dlg.stream(), dlg.dbcFile());
|
|
||||||
} else if (!can) {
|
|
||||||
openStream(new DummyStream(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::closeStream() {
|
|
||||||
openStream(new DummyStream(this));
|
|
||||||
if (dbc()->nonEmptyDBCCount() > 0) {
|
|
||||||
emit dbcNotifier()->DBCFileChanged();
|
|
||||||
}
|
|
||||||
statusBar()->showMessage(tr("stream closed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::exportToCSV() {
|
|
||||||
QString dir = QString("%1/%2.csv").arg(QString::fromStdString(settings.last_dir)).arg(QString::fromStdString(can->routeName()));
|
|
||||||
QString fn = QFileDialog::getSaveFileName(this, "Export stream to CSV file", dir, tr("csv (*.csv)"));
|
|
||||||
if (!fn.isEmpty()) {
|
|
||||||
utils::exportToCSV(fn.toStdString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::newFile(SourceSet s) {
|
|
||||||
closeFile(s);
|
|
||||||
dbc()->open(s, std::string(""), std::string(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::openFile(SourceSet s) {
|
|
||||||
remindSaveChanges();
|
|
||||||
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)");
|
|
||||||
if (!fn.isEmpty()) {
|
|
||||||
loadFile(fn, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::loadFile(const QString &fn, SourceSet s) {
|
|
||||||
if (!fn.isEmpty()) {
|
|
||||||
closeFile(s);
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
if (dbc()->open(s, fn.toStdString(), &error)) {
|
|
||||||
updateRecentFiles(fn);
|
|
||||||
statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000);
|
|
||||||
} else {
|
|
||||||
QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC file"), tr("Failed to parse DBC file %1").arg(fn));
|
|
||||||
msg_box.setDetailedText(QString::fromStdString(error));
|
|
||||||
msg_box.exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::loadDBCFromOpendbc(const QString &name) {
|
|
||||||
loadFile(QString("%1/%2").arg(OPENDBC_FILE_PATH, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::loadFromClipboard(SourceSet s, bool close_all) {
|
|
||||||
std::string text;
|
|
||||||
if (!utils::getClipboardText(&text)) {
|
|
||||||
QMessageBox::warning(this, tr("Load From Clipboard"), tr("No clipboard tool found. Install xclip (X11) or wl-clipboard (Wayland)."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (text.empty()) {
|
|
||||||
QMessageBox::warning(this, tr("Load From Clipboard"), tr("Clipboard is empty."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeFile(s);
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
bool ret = dbc()->open(s, std::string(""), text, &error);
|
|
||||||
if (ret && dbc()->nonEmptyDBCCount() > 0) {
|
|
||||||
QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!"));
|
|
||||||
} else {
|
|
||||||
QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC from clipboard"), tr("Make sure that you paste the text with correct format."));
|
|
||||||
msg_box.setDetailedText(QString::fromStdString(error));
|
|
||||||
msg_box.exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::openStream(AbstractStream *stream, const QString &dbc_file) {
|
|
||||||
if (can) {
|
|
||||||
QObject::connect(can, &QObject::destroyed, this, [=]() { startStream(stream, dbc_file); });
|
|
||||||
can->deleteLater();
|
|
||||||
} else {
|
|
||||||
startStream(stream, dbc_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::startStream(AbstractStream *stream, QString dbc_file) {
|
|
||||||
center_widget->clear();
|
|
||||||
delete messages_widget;
|
|
||||||
delete video_splitter;
|
|
||||||
|
|
||||||
can = stream;
|
|
||||||
can->setParent(this); // take ownership
|
|
||||||
can->start();
|
|
||||||
|
|
||||||
loadFile(dbc_file);
|
|
||||||
statusBar()->showMessage(tr("Stream [%1] started").arg(QString::fromStdString(can->routeName())), 2000);
|
|
||||||
|
|
||||||
bool has_stream = dynamic_cast<DummyStream *>(can) == nullptr;
|
|
||||||
close_stream_act->setEnabled(has_stream);
|
|
||||||
export_to_csv_act->setEnabled(has_stream);
|
|
||||||
tools_menu->setEnabled(has_stream);
|
|
||||||
createDockWidgets();
|
|
||||||
|
|
||||||
video_dock->setWindowTitle(QString::fromStdString(can->routeName()));
|
|
||||||
if (can->liveStreaming() || video_splitter->sizes()[0] == 0) {
|
|
||||||
// display video at minimum size.
|
|
||||||
video_splitter->setSizes({1, 1});
|
|
||||||
}
|
|
||||||
// Don't overwrite already loaded DBC
|
|
||||||
if (!dbc()->nonEmptyDBCCount()) {
|
|
||||||
newFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
QObject::connect(can, &AbstractStream::eventsMerged, this, &MainWindow::eventsMerged);
|
|
||||||
|
|
||||||
if (has_stream) {
|
|
||||||
auto wait_dlg = new QProgressDialog(
|
|
||||||
can->liveStreaming() ? tr("Waiting for the live stream to start...") : tr("Loading segment data..."),
|
|
||||||
tr("&Abort"), 0, 100, this);
|
|
||||||
wait_dlg->setWindowModality(Qt::WindowModal);
|
|
||||||
wait_dlg->setFixedSize(400, wait_dlg->sizeHint().height());
|
|
||||||
QObject::connect(wait_dlg, &QProgressDialog::canceled, this, &MainWindow::close);
|
|
||||||
QObject::connect(can, &AbstractStream::eventsMerged, wait_dlg, &QProgressDialog::deleteLater);
|
|
||||||
QObject::connect(this, &MainWindow::updateProgressBar, wait_dlg, [=](uint64_t cur, uint64_t total, bool success) {
|
|
||||||
wait_dlg->setValue((int)((cur / (double)total) * 100));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::eventsMerged() {
|
|
||||||
if (!can->liveStreaming() && std::exchange(car_fingerprint, QString::fromStdString(can->carFingerprint())) != car_fingerprint) {
|
|
||||||
video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPRINT: %2")
|
|
||||||
.arg(QString::fromStdString(can->routeName()))
|
|
||||||
.arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint));
|
|
||||||
// Don't overwrite already loaded DBC
|
|
||||||
auto it = fingerprint_to_dbc.find(car_fingerprint.toStdString());
|
|
||||||
if (!dbc()->nonEmptyDBCCount() && it != fingerprint_to_dbc.end()) {
|
|
||||||
QTimer::singleShot(0, this, [this, dbc_name = QString::fromStdString(it->second)]() {
|
|
||||||
loadDBCFromOpendbc(dbc_name + ".dbc");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::save() {
|
|
||||||
// Save all open DBC files
|
|
||||||
for (auto dbc_file : dbc()->allDBCFiles()) {
|
|
||||||
if (dbc_file->isEmpty()) continue;
|
|
||||||
saveFile(dbc_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::saveAs() {
|
|
||||||
// Save as all open DBC files. Should not be called with more than 1 file open
|
|
||||||
for (auto dbc_file : dbc()->allDBCFiles()) {
|
|
||||||
if (dbc_file->isEmpty()) continue;
|
|
||||||
saveFileAs(dbc_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::closeFile(SourceSet s) {
|
|
||||||
remindSaveChanges();
|
|
||||||
if (s == SOURCE_ALL) {
|
|
||||||
dbc()->closeAll();
|
|
||||||
} else {
|
|
||||||
dbc()->close(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::closeFile(DBCFile *dbc_file) {
|
|
||||||
assert(dbc_file != nullptr);
|
|
||||||
remindSaveChanges();
|
|
||||||
dbc()->close(dbc_file);
|
|
||||||
// Ensure we always have at least one file open
|
|
||||||
if (dbc()->dbcCount() == 0) {
|
|
||||||
newFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::saveFile(DBCFile *dbc_file) {
|
|
||||||
assert(dbc_file != nullptr);
|
|
||||||
if (!dbc_file->filename.empty()) {
|
|
||||||
dbc_file->save();
|
|
||||||
UndoStack::instance()->setClean();
|
|
||||||
statusBar()->showMessage(tr("File saved"), 2000);
|
|
||||||
} else if (!dbc_file->isEmpty()) {
|
|
||||||
saveFileAs(dbc_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::saveFileAs(DBCFile *dbc_file) {
|
|
||||||
QString title = tr("Save File (bus: %1)").arg(QString::fromStdString(toString(dbc()->sources(dbc_file))));
|
|
||||||
QString fn = QFileDialog::getSaveFileName(this, title, QString::fromStdString((std::filesystem::path(settings.last_dir) / "untitled.dbc").string()), tr("DBC (*.dbc)"));
|
|
||||||
if (!fn.isEmpty()) {
|
|
||||||
dbc_file->saveAs(fn.toStdString());
|
|
||||||
UndoStack::instance()->setClean();
|
|
||||||
statusBar()->showMessage(tr("File saved as %1").arg(fn), 2000);
|
|
||||||
updateRecentFiles(fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::saveToClipboard() {
|
|
||||||
// Copy all open DBC files to clipboard. Should not be called with more than 1 file open
|
|
||||||
for (auto dbc_file : dbc()->allDBCFiles()) {
|
|
||||||
if (dbc_file->isEmpty()) continue;
|
|
||||||
saveFileToClipboard(dbc_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::saveFileToClipboard(DBCFile *dbc_file) {
|
|
||||||
assert(dbc_file != nullptr);
|
|
||||||
if (utils::setClipboardText(dbc_file->generateDBC())) {
|
|
||||||
QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!"));
|
|
||||||
} else {
|
|
||||||
QMessageBox::warning(this, tr("Copy To Clipboard"), tr("Failed to copy DBC to clipboard. Install xclip (X11) or wl-clipboard (Wayland)."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::updateLoadSaveMenus() {
|
|
||||||
manage_dbcs_menu->clear();
|
|
||||||
|
|
||||||
for (int source : can->sources) {
|
|
||||||
if (source >= 64) continue; // Sent and blocked buses are handled implicitly
|
|
||||||
|
|
||||||
SourceSet ss = {source, uint8_t(source + 128), uint8_t(source + 192)};
|
|
||||||
|
|
||||||
QMenu *bus_menu = new QMenu(this);
|
|
||||||
bus_menu->addAction(tr("New DBC File..."), [=]() { newFile(ss); });
|
|
||||||
bus_menu->addAction(tr("Open DBC File..."), [=]() { openFile(ss); });
|
|
||||||
bus_menu->addAction(tr("Load DBC From Clipboard..."), [=]() { loadFromClipboard(ss, false); });
|
|
||||||
|
|
||||||
// Show sub-menu for each dbc for this source.
|
|
||||||
auto dbc_file = dbc()->findDBCFile(source);
|
|
||||||
if (dbc_file) {
|
|
||||||
bus_menu->addSeparator();
|
|
||||||
bus_menu->addAction(QString::fromStdString(dbc_file->name()) + " (" + QString::fromStdString(toString(dbc()->sources(dbc_file))) + ")")->setEnabled(false);
|
|
||||||
bus_menu->addAction(tr("Save..."), [=]() { saveFile(dbc_file); });
|
|
||||||
bus_menu->addAction(tr("Save As..."), [=]() { saveFileAs(dbc_file); });
|
|
||||||
bus_menu->addAction(tr("Copy to Clipboard..."), [=]() { saveFileToClipboard(dbc_file); });
|
|
||||||
bus_menu->addAction(tr("Remove from this bus..."), [=]() { closeFile(ss); });
|
|
||||||
bus_menu->addAction(tr("Remove from all buses..."), [=]() { closeFile(dbc_file); });
|
|
||||||
}
|
|
||||||
bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(dbc_file ? QString::fromStdString(dbc_file->name()) : "No DBCs loaded"));
|
|
||||||
|
|
||||||
manage_dbcs_menu->addMenu(bus_menu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::updateRecentFiles(const QString &fn) {
|
|
||||||
const std::string filename = fn.toStdString();
|
|
||||||
settings.recent_files.erase(std::remove(settings.recent_files.begin(), settings.recent_files.end(), filename), settings.recent_files.end());
|
|
||||||
settings.recent_files.insert(settings.recent_files.begin(), filename);
|
|
||||||
while (settings.recent_files.size() > MAX_RECENT_FILES) {
|
|
||||||
settings.recent_files.pop_back();
|
|
||||||
}
|
|
||||||
settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::updateRecentFileMenu() {
|
|
||||||
open_recent_menu->clear();
|
|
||||||
|
|
||||||
int num_recent_files = std::min<int>(settings.recent_files.size(), MAX_RECENT_FILES);
|
|
||||||
if (!num_recent_files) {
|
|
||||||
open_recent_menu->addAction(tr("No Recent Files"))->setEnabled(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < num_recent_files; ++i) {
|
|
||||||
QString text = tr("&%1 %2").arg(i + 1).arg(QString::fromStdString(std::filesystem::path(settings.recent_files[i]).filename().string()));
|
|
||||||
open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(QString::fromStdString(file)); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::remindSaveChanges() {
|
|
||||||
while (!UndoStack::instance()->isClean()) {
|
|
||||||
QString text = tr("You have unsaved changes. Press ok to save them, cancel to discard.");
|
|
||||||
int ret = QMessageBox::question(this, tr("Unsaved Changes"), text, QMessageBox::Ok | QMessageBox::Cancel);
|
|
||||||
if (ret != QMessageBox::Ok) break;
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
UndoStack::instance()->clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) {
|
|
||||||
if (success && cur < total) {
|
|
||||||
progress_bar->setValue((cur / (double)total) * 100);
|
|
||||||
progress_bar->setFormat(tr("Downloading %p% (%1)").arg(formattedDataSize(total).c_str()));
|
|
||||||
progress_bar->show();
|
|
||||||
} else {
|
|
||||||
progress_bar->hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::updateStatus() {
|
|
||||||
status_label->setText(tr("Cached Minutes:%1 FPS:%2").arg(settings.max_cached_minutes).arg(settings.fps));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
|
|
||||||
if (obj == floating_window && event->type() == QEvent::Close) {
|
|
||||||
toggleChartsDocking();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return QMainWindow::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::toggleChartsDocking() {
|
|
||||||
if (floating_window) {
|
|
||||||
// Dock the charts widget back to the main window
|
|
||||||
floating_window->removeEventFilter(this);
|
|
||||||
charts_layout->insertWidget(0, charts_widget, 1);
|
|
||||||
floating_window->deleteLater();
|
|
||||||
floating_window = nullptr;
|
|
||||||
charts_widget->setIsDocked(true);
|
|
||||||
} else {
|
|
||||||
// Float the charts widget in a separate window
|
|
||||||
floating_window = new QWidget(this, Qt::Window);
|
|
||||||
floating_window->setWindowTitle("Charts");
|
|
||||||
floating_window->setLayout(new QVBoxLayout());
|
|
||||||
floating_window->layout()->addWidget(charts_widget);
|
|
||||||
floating_window->installEventFilter(this);
|
|
||||||
floating_window->showMaximized();
|
|
||||||
charts_widget->setIsDocked(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
|
||||||
remindSaveChanges();
|
|
||||||
|
|
||||||
installDownloadProgressHandler(nullptr);
|
|
||||||
installMessageHandler(nullptr);
|
|
||||||
|
|
||||||
if (floating_window)
|
|
||||||
floating_window->deleteLater();
|
|
||||||
|
|
||||||
// save states
|
|
||||||
settings.geometry = utils::toBytes(saveGeometry());
|
|
||||||
settings.window_state = utils::toBytes(saveState());
|
|
||||||
if (can && !can->liveStreaming()) {
|
|
||||||
settings.video_splitter_state = utils::toBytes(video_splitter->saveState());
|
|
||||||
}
|
|
||||||
if (messages_widget) {
|
|
||||||
settings.message_header_state = messages_widget->saveHeaderState();
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSessionState();
|
|
||||||
settings.save();
|
|
||||||
QWidget::closeEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::setOption() {
|
|
||||||
SettingsDlg dlg(this);
|
|
||||||
dlg.exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::findSimilarBits() {
|
|
||||||
FindSimilarBitsDlg *dlg = new FindSimilarBitsDlg(this);
|
|
||||||
QObject::connect(dlg, &FindSimilarBitsDlg::openMessage, messages_widget, &MessagesWidget::selectMessage);
|
|
||||||
dlg->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::findSignal() {
|
|
||||||
FindSignalDlg *dlg = new FindSignalDlg(this);
|
|
||||||
QObject::connect(dlg, &FindSignalDlg::openMessage, messages_widget, &MessagesWidget::selectMessage);
|
|
||||||
dlg->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::onlineHelp() {
|
|
||||||
if (auto help = findChild<HelpOverlay*>()) {
|
|
||||||
help->close();
|
|
||||||
} else {
|
|
||||||
help = new HelpOverlay(this);
|
|
||||||
help->setGeometry(rect());
|
|
||||||
help->show();
|
|
||||||
help->raise();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::toggleFullScreen() {
|
|
||||||
if (isFullScreen()) {
|
|
||||||
menuBar()->show();
|
|
||||||
statusBar()->show();
|
|
||||||
showNormal();
|
|
||||||
showMaximized();
|
|
||||||
} else {
|
|
||||||
menuBar()->hide();
|
|
||||||
statusBar()->hide();
|
|
||||||
showFullScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::saveSessionState() {
|
|
||||||
settings.recent_dbc_file = "";
|
|
||||||
settings.active_msg_id = "";
|
|
||||||
settings.selected_msg_ids.clear();
|
|
||||||
settings.active_charts.clear();
|
|
||||||
|
|
||||||
for (auto &f : dbc()->allDBCFiles())
|
|
||||||
if (!f->isEmpty()) { settings.recent_dbc_file = f->filename; break; }
|
|
||||||
|
|
||||||
if (auto *detail = center_widget->getDetailWidget()) {
|
|
||||||
auto [active_id, ids] = detail->serializeMessageIds();
|
|
||||||
settings.active_msg_id = active_id.toStdString();
|
|
||||||
settings.selected_msg_ids.clear();
|
|
||||||
for (const auto &id : ids) settings.selected_msg_ids.push_back(id.toStdString());
|
|
||||||
}
|
|
||||||
if (charts_widget) {
|
|
||||||
settings.active_charts.clear();
|
|
||||||
for (const auto &id : charts_widget->serializeChartIds()) settings.active_charts.push_back(id.toStdString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::restoreSessionState() {
|
|
||||||
if (settings.recent_dbc_file.empty() || dbc()->nonEmptyDBCCount() == 0) return;
|
|
||||||
|
|
||||||
QString dbc_file;
|
|
||||||
for (auto& f : dbc()->allDBCFiles())
|
|
||||||
if (!f->isEmpty()) { dbc_file = QString::fromStdString(f->filename); break; }
|
|
||||||
if (dbc_file.toStdString() != settings.recent_dbc_file) return;
|
|
||||||
|
|
||||||
if (!settings.selected_msg_ids.empty()) {
|
|
||||||
QStringList ids;
|
|
||||||
for (const auto &id : settings.selected_msg_ids) ids.push_back(QString::fromStdString(id));
|
|
||||||
center_widget->ensureDetailWidget()->restoreTabs(QString::fromStdString(settings.active_msg_id), ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (charts_widget != nullptr && !settings.active_charts.empty()) {
|
|
||||||
QStringList ids;
|
|
||||||
for (const auto &id : settings.active_charts) ids.push_back(QString::fromStdString(id));
|
|
||||||
charts_widget->restoreChartsFromIds(ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HelpOverlay
|
|
||||||
HelpOverlay::HelpOverlay(MainWindow *parent) : QWidget(parent) {
|
|
||||||
setAttribute(Qt::WA_NoSystemBackground, true);
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground, true);
|
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
parent->installEventFilter(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HelpOverlay::paintEvent(QPaintEvent *event) {
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.fillRect(rect(), QColor(0, 0, 0, 50));
|
|
||||||
auto parent = parentWidget();
|
|
||||||
drawHelpForWidget(painter, parent->findChild<MessagesWidget *>());
|
|
||||||
drawHelpForWidget(painter, parent->findChild<BinaryView *>());
|
|
||||||
drawHelpForWidget(painter, parent->findChild<SignalView *>());
|
|
||||||
drawHelpForWidget(painter, parent->findChild<ChartsWidget *>());
|
|
||||||
drawHelpForWidget(painter, parent->findChild<VideoWidget *>());
|
|
||||||
}
|
|
||||||
|
|
||||||
void HelpOverlay::drawHelpForWidget(QPainter &painter, QWidget *w) {
|
|
||||||
if (w && w->isVisible() && !w->whatsThis().isEmpty()) {
|
|
||||||
QPoint pt = mapFromGlobal(w->mapToGlobal(w->rect().center()));
|
|
||||||
if (rect().contains(pt)) {
|
|
||||||
QTextDocument document;
|
|
||||||
document.setHtml(w->whatsThis());
|
|
||||||
QSize doc_size = document.size().toSize();
|
|
||||||
QPoint topleft = {pt.x() - doc_size.width() / 2, pt.y() - doc_size.height() / 2};
|
|
||||||
painter.translate(topleft);
|
|
||||||
painter.fillRect(QRect{{0, 0}, doc_size}, palette().toolTipBase());
|
|
||||||
document.drawContents(&painter);
|
|
||||||
painter.translate(-topleft);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HelpOverlay::eventFilter(QObject *obj, QEvent *event) {
|
|
||||||
if (obj == parentWidget() && event->type() == QEvent::Resize) {
|
|
||||||
QResizeEvent *resize_event = (QResizeEvent *)(event);
|
|
||||||
setGeometry(QRect{QPoint(0, 0), resize_event->size()});
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HelpOverlay::mouseReleaseEvent(QMouseEvent *event) {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QDockWidget>
|
|
||||||
#include <QMainWindow>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QProgressBar>
|
|
||||||
#include <QSplitter>
|
|
||||||
#include <QStatusBar>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <set>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "tools/cabana/chart/chartswidget.h"
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
#include "tools/cabana/detailwidget.h"
|
|
||||||
#include "tools/cabana/messageswidget.h"
|
|
||||||
#include "tools/cabana/videowidget.h"
|
|
||||||
#include "tools/cabana/tools/findsimilarbits.h"
|
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
MainWindow(AbstractStream *stream, const QString &dbc_file);
|
|
||||||
void toggleChartsDocking();
|
|
||||||
void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); }
|
|
||||||
void loadFile(const QString &fn, SourceSet s = SOURCE_ALL);
|
|
||||||
ChartsWidget *charts_widget = nullptr;
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void selectAndOpenStream();
|
|
||||||
void openStream(AbstractStream *stream, const QString &dbc_file = {});
|
|
||||||
void closeStream();
|
|
||||||
void exportToCSV();
|
|
||||||
|
|
||||||
void newFile(SourceSet s = SOURCE_ALL);
|
|
||||||
void openFile(SourceSet s = SOURCE_ALL);
|
|
||||||
void loadDBCFromOpendbc(const QString &name);
|
|
||||||
void save();
|
|
||||||
void saveAs();
|
|
||||||
void saveToClipboard();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void showMessage(const QString &msg, int timeout);
|
|
||||||
void updateProgressBar(uint64_t cur, uint64_t total, bool success);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void startStream(AbstractStream *stream, QString dbc_file);
|
|
||||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
|
||||||
void remindSaveChanges();
|
|
||||||
void closeFile(SourceSet s = SOURCE_ALL);
|
|
||||||
void closeFile(DBCFile *dbc_file);
|
|
||||||
void saveFile(DBCFile *dbc_file);
|
|
||||||
void saveFileAs(DBCFile *dbc_file);
|
|
||||||
void saveFileToClipboard(DBCFile *dbc_file);
|
|
||||||
void loadFingerprints();
|
|
||||||
void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true);
|
|
||||||
void updateRecentFiles(const QString &fn);
|
|
||||||
void updateRecentFileMenu();
|
|
||||||
void createActions();
|
|
||||||
void createDockWindows();
|
|
||||||
void createStatusBar();
|
|
||||||
void createShortcuts();
|
|
||||||
void closeEvent(QCloseEvent *event) override;
|
|
||||||
void DBCFileChanged();
|
|
||||||
void updateDownloadProgress(uint64_t cur, uint64_t total, bool success);
|
|
||||||
void setOption();
|
|
||||||
void findSimilarBits();
|
|
||||||
void findSignal();
|
|
||||||
void undoStackCleanChanged(bool clean);
|
|
||||||
void updateUndoRedoActions();
|
|
||||||
void onlineHelp();
|
|
||||||
void toggleFullScreen();
|
|
||||||
void updateStatus();
|
|
||||||
void updateLoadSaveMenus();
|
|
||||||
void createDockWidgets();
|
|
||||||
void eventsMerged();
|
|
||||||
void saveSessionState();
|
|
||||||
void restoreSessionState();
|
|
||||||
|
|
||||||
VideoWidget *video_widget = nullptr;
|
|
||||||
QDockWidget *video_dock;
|
|
||||||
QDockWidget *messages_dock;
|
|
||||||
MessagesWidget *messages_widget = nullptr;
|
|
||||||
CenterWidget *center_widget;
|
|
||||||
QWidget *floating_window = nullptr;
|
|
||||||
QVBoxLayout *charts_layout;
|
|
||||||
QProgressBar *progress_bar;
|
|
||||||
QLabel *status_label;
|
|
||||||
std::unordered_map<std::string, std::string> fingerprint_to_dbc;
|
|
||||||
QSplitter *video_splitter = nullptr;
|
|
||||||
enum { MAX_RECENT_FILES = 15 };
|
|
||||||
QMenu *open_recent_menu = nullptr;
|
|
||||||
QMenu *manage_dbcs_menu = nullptr;
|
|
||||||
QMenu *tools_menu = nullptr;
|
|
||||||
QAction *close_stream_act = nullptr;
|
|
||||||
QAction *export_to_csv_act = nullptr;
|
|
||||||
QAction *save_dbc = nullptr;
|
|
||||||
QAction *save_dbc_as = nullptr;
|
|
||||||
QAction *copy_dbc_to_clipboard = nullptr;
|
|
||||||
QAction *undo_act = nullptr;
|
|
||||||
QAction *redo_act = nullptr;
|
|
||||||
QString car_fingerprint;
|
|
||||||
std::vector<uint8_t> default_state;
|
|
||||||
};
|
|
||||||
|
|
||||||
class HelpOverlay : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
HelpOverlay(MainWindow *parent);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void drawHelpForWidget(QPainter &painter, QWidget *w);
|
|
||||||
void paintEvent(QPaintEvent *event) override;
|
|
||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
|
||||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
|
||||||
};
|
|
||||||
@@ -1,465 +0,0 @@
|
|||||||
#include "tools/cabana/messageswidget.h"
|
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
#include <limits>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QCheckBox>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPalette>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "tools/cabana/commands.h"
|
|
||||||
|
|
||||||
MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) {
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
// toolbar
|
|
||||||
main_layout->addWidget(createToolBar());
|
|
||||||
// message table
|
|
||||||
main_layout->addWidget(view = new MessageView(this));
|
|
||||||
view->setItemDelegate(delegate = new MessageBytesDelegate(view, settings.multiple_lines_hex));
|
|
||||||
view->setModel(model = new MessageListModel(this));
|
|
||||||
view->setHeader(header = new MessageViewHeader(this));
|
|
||||||
view->setSortingEnabled(true);
|
|
||||||
view->sortByColumn(MessageListModel::Column::NAME, Qt::AscendingOrder);
|
|
||||||
view->setAllColumnsShowFocus(true);
|
|
||||||
view->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
||||||
view->setItemsExpandable(false);
|
|
||||||
view->setIndentation(0);
|
|
||||||
view->setRootIsDecorated(false);
|
|
||||||
|
|
||||||
// Must be called before setting any header parameters to avoid overriding
|
|
||||||
restoreHeaderState(settings.message_header_state);
|
|
||||||
header->setSectionsMovable(true);
|
|
||||||
header->setSectionResizeMode(MessageListModel::Column::DATA, QHeaderView::Fixed);
|
|
||||||
header->setStretchLastSection(true);
|
|
||||||
header->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
||||||
|
|
||||||
// signals/slots
|
|
||||||
QObject::connect(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow);
|
|
||||||
QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent);
|
|
||||||
QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions);
|
|
||||||
QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
view->updateBytesSectionSize();
|
|
||||||
updateTitle();
|
|
||||||
});
|
|
||||||
QObject::connect(view->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) {
|
|
||||||
if (current.isValid() && current.row() < model->items_.size()) {
|
|
||||||
const auto &id = model->items_[current.row()].id;
|
|
||||||
if (!current_msg_id || id != *current_msg_id) {
|
|
||||||
current_msg_id = id;
|
|
||||||
emit msgSelectionChanged(*current_msg_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setWhatsThis(tr(R"(
|
|
||||||
<b>Message View</b><br/>
|
|
||||||
<!-- TODO: add description here -->
|
|
||||||
<span style="color:gray">Byte color</span><br />
|
|
||||||
<span style="color:gray;">■ </span> constant changing<br />
|
|
||||||
<span style="color:blue;">■ </span> increasing<br />
|
|
||||||
<span style="color:red;">■ </span> decreasing<br />
|
|
||||||
<span style="color:gray">Shortcuts</span><br />
|
|
||||||
Horizontal Scrolling: <span style="background-color:lightGray;color:gray"> shift+wheel </span>
|
|
||||||
)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget *MessagesWidget::createToolBar() {
|
|
||||||
QWidget *toolbar = new QWidget(this);
|
|
||||||
QHBoxLayout *layout = new QHBoxLayout(toolbar);
|
|
||||||
layout->setContentsMargins(0, 9, 0, 0);
|
|
||||||
layout->addWidget(suppress_add = new QPushButton("Suppress Highlighted"));
|
|
||||||
layout->addWidget(suppress_clear = new QPushButton());
|
|
||||||
suppress_clear->setToolTip(tr("Clear suppressed"));
|
|
||||||
layout->addStretch(1);
|
|
||||||
QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Signals"), this);
|
|
||||||
suppress_defined_signals->setToolTip(tr("Suppress defined signals"));
|
|
||||||
suppress_defined_signals->setChecked(settings.suppress_defined_signals);
|
|
||||||
layout->addWidget(suppress_defined_signals);
|
|
||||||
|
|
||||||
auto view_button = new ToolButton("three-dots", tr("View..."));
|
|
||||||
view_button->setMenu(menu);
|
|
||||||
view_button->setPopupMode(QToolButton::InstantPopup);
|
|
||||||
view_button->setStyleSheet("QToolButton::menu-indicator { image: none; }");
|
|
||||||
layout->addWidget(view_button);
|
|
||||||
|
|
||||||
QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted);
|
|
||||||
QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted);
|
|
||||||
QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals);
|
|
||||||
|
|
||||||
suppressHighlighted();
|
|
||||||
return toolbar;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessagesWidget::updateTitle() {
|
|
||||||
auto stats = std::accumulate(
|
|
||||||
model->items_.begin(), model->items_.end(), std::pair<size_t, size_t>(),
|
|
||||||
[](const auto &pair, const auto &item) {
|
|
||||||
auto m = dbc()->msg(item.id);
|
|
||||||
return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair;
|
|
||||||
});
|
|
||||||
emit titleChanged(tr("%1 Messages (%2 DBC Messages, %3 Signals)")
|
|
||||||
.arg(model->items_.size()).arg(stats.first).arg(stats.second));
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessagesWidget::selectMessage(const MessageId &msg_id) {
|
|
||||||
auto it = std::find_if(model->items_.cbegin(), model->items_.cend(),
|
|
||||||
[&msg_id](auto &item) { return item.id == msg_id; });
|
|
||||||
if (it != model->items_.cend()) {
|
|
||||||
view->setCurrentIndex(model->index(std::distance(model->items_.cbegin(), it), 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessagesWidget::suppressHighlighted() {
|
|
||||||
int n = sender() == suppress_add ? can->suppressHighlighted() : (can->clearSuppressed(), 0);
|
|
||||||
suppress_clear->setText(n > 0 ? tr("Clear (%1)").arg(n) : tr("Clear"));
|
|
||||||
suppress_clear->setEnabled(n > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessagesWidget::headerContextMenuEvent(const QPoint &pos) {
|
|
||||||
menu->exec(header->mapToGlobal(pos));
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessagesWidget::menuAboutToShow() {
|
|
||||||
menu->clear();
|
|
||||||
for (int i = 0; i < header->count(); ++i) {
|
|
||||||
int logical_index = header->logicalIndex(i);
|
|
||||||
auto action = menu->addAction(model->headerData(logical_index, Qt::Horizontal).toString(),
|
|
||||||
[=](bool checked) { header->setSectionHidden(logical_index, !checked); });
|
|
||||||
action->setCheckable(true);
|
|
||||||
action->setChecked(!header->isSectionHidden(logical_index));
|
|
||||||
// Can't hide the name column
|
|
||||||
action->setEnabled(logical_index > 0);
|
|
||||||
}
|
|
||||||
menu->addSeparator();
|
|
||||||
auto action = menu->addAction(tr("Multi-Line bytes"), this, &MessagesWidget::setMultiLineBytes);
|
|
||||||
action->setCheckable(true);
|
|
||||||
action->setChecked(settings.multiple_lines_hex);
|
|
||||||
|
|
||||||
action = menu->addAction(tr("Show inactive messages"), model, &MessageListModel::showInactiveMessages);
|
|
||||||
action->setCheckable(true);
|
|
||||||
action->setChecked(model->show_inactive_messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessagesWidget::setMultiLineBytes(bool multi) {
|
|
||||||
settings.multiple_lines_hex = multi;
|
|
||||||
delegate->setMultipleLines(multi);
|
|
||||||
view->updateBytesSectionSize();
|
|
||||||
view->doItemsLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageListModel
|
|
||||||
|
|
||||||
QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
||||||
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
|
|
||||||
switch (section) {
|
|
||||||
case Column::NAME: return tr("Name");
|
|
||||||
case Column::SOURCE: return tr("Bus");
|
|
||||||
case Column::ADDRESS: return tr("ID");
|
|
||||||
case Column::NODE: return tr("Node");
|
|
||||||
case Column::FREQ: return tr("Freq");
|
|
||||||
case Column::COUNT: return tr("Count");
|
|
||||||
case Column::DATA: return tr("Bytes");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
|
||||||
if (!index.isValid() || index.row() >= items_.size()) return {};
|
|
||||||
|
|
||||||
auto getFreq = [](float freq) {
|
|
||||||
if (freq > 0) {
|
|
||||||
return freq >= 0.95 ? QString::number(std::nearbyint(freq)) : QString::number(freq, 'f', 2);
|
|
||||||
} else {
|
|
||||||
return QStringLiteral("--");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const static QString NA = QStringLiteral("N/A");
|
|
||||||
const auto &item = items_[index.row()];
|
|
||||||
if (role == Qt::DisplayRole) {
|
|
||||||
switch (index.column()) {
|
|
||||||
case Column::NAME: return item.name;
|
|
||||||
case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA;
|
|
||||||
case Column::ADDRESS: return toHexString(item.id.address);
|
|
||||||
case Column::NODE: return item.node;
|
|
||||||
case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA;
|
|
||||||
case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA;
|
|
||||||
case Column::DATA: return item.id.source != INVALID_SOURCE ? "" : NA;
|
|
||||||
}
|
|
||||||
} else if (role == ColorsRole) {
|
|
||||||
return QVariant::fromValue((void*)(&can->lastMessage(item.id).colors));
|
|
||||||
} else if (role == BytesRole && index.column() == Column::DATA && item.id.source != INVALID_SOURCE) {
|
|
||||||
return QVariant::fromValue((void*)(&can->lastMessage(item.id).dat));
|
|
||||||
} else if (role == Qt::ToolTipRole && index.column() == Column::NAME) {
|
|
||||||
auto msg = dbc()->msg(item.id);
|
|
||||||
auto tooltip = item.name;
|
|
||||||
if (msg && !msg->comment.empty()) tooltip += "<br /><span style=\"color:gray;\">" + QString::fromStdString(msg->comment) + "</span>";
|
|
||||||
return tooltip;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageListModel::setFilterStrings(const std::map<int, QString> &filters) {
|
|
||||||
filters_ = filters;
|
|
||||||
filterAndSort();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageListModel::showInactiveMessages(bool show) {
|
|
||||||
show_inactive_messages = show;
|
|
||||||
filterAndSort();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageListModel::dbcModified() {
|
|
||||||
dbc_messages_.clear();
|
|
||||||
for (const auto &[_, m] : dbc()->getMessages(-1)) {
|
|
||||||
dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address});
|
|
||||||
}
|
|
||||||
filterAndSort();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageListModel::sortItems(std::vector<MessageListModel::Item> &items) {
|
|
||||||
auto compare = [this](const auto &l, const auto &r) {
|
|
||||||
switch (sort_column) {
|
|
||||||
case Column::NAME: return std::tie(l.name, l.id) < std::tie(r.name, r.id);
|
|
||||||
case Column::SOURCE: return std::tie(l.id.source, l.id.address) < std::tie(r.id.source, r.id.address);
|
|
||||||
case Column::ADDRESS: return std::tie(l.id.address, l.id.source) < std::tie(r.id.address, r.id.source);
|
|
||||||
case Column::NODE: return std::tie(l.node, l.id) < std::tie(r.node, r.id);
|
|
||||||
case Column::FREQ: return std::tie(can->lastMessage(l.id).freq, l.id) < std::tie(can->lastMessage(r.id).freq, r.id);
|
|
||||||
case Column::COUNT: return std::tie(can->lastMessage(l.id).count, l.id) < std::tie(can->lastMessage(r.id).count, r.id);
|
|
||||||
default: return false; // Default case to suppress compiler warning
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sort_order == Qt::DescendingOrder)
|
|
||||||
std::stable_sort(items.rbegin(), items.rend(), compare);
|
|
||||||
else
|
|
||||||
std::stable_sort(items.begin(), items.end(), compare);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool parseRange(const QString &filter, uint32_t value, int base = 10) {
|
|
||||||
// Parse out filter string into a range (e.g. "1" -> {1, 1}, "1-3" -> {1, 3}, "1-" -> {1, inf})
|
|
||||||
unsigned int min = std::numeric_limits<unsigned int>::min();
|
|
||||||
unsigned int max = std::numeric_limits<unsigned int>::max();
|
|
||||||
auto s = filter.split('-');
|
|
||||||
bool ok = s.size() >= 1 && s.size() <= 2;
|
|
||||||
if (ok && !s[0].isEmpty()) min = s[0].toUInt(&ok, base);
|
|
||||||
if (ok && s.size() == 1) {
|
|
||||||
max = min;
|
|
||||||
} else if (ok && s.size() == 2 && !s[1].isEmpty()) {
|
|
||||||
max = s[1].toUInt(&ok, base);
|
|
||||||
}
|
|
||||||
return ok && value >= min && value <= max;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MessageListModel::match(const MessageListModel::Item &item) {
|
|
||||||
if (filters_.empty())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
bool match = true;
|
|
||||||
const auto &data = can->lastMessage(item.id);
|
|
||||||
for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) {
|
|
||||||
const QString &txt = it->second;
|
|
||||||
switch (it->first) {
|
|
||||||
case Column::NAME: {
|
|
||||||
match = item.name.contains(txt, Qt::CaseInsensitive);
|
|
||||||
if (!match) {
|
|
||||||
const auto m = dbc()->msg(item.id);
|
|
||||||
match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(),
|
|
||||||
[&txt](const auto &s) { return QString::fromStdString(s->name).contains(txt, Qt::CaseInsensitive); });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Column::SOURCE:
|
|
||||||
match = parseRange(txt, item.id.source);
|
|
||||||
break;
|
|
||||||
case Column::ADDRESS:
|
|
||||||
match = toHexString(item.id.address).contains(txt, Qt::CaseInsensitive);
|
|
||||||
match = match || parseRange(txt, item.id.address, 16);
|
|
||||||
break;
|
|
||||||
case Column::NODE:
|
|
||||||
match = item.node.contains(txt, Qt::CaseInsensitive);
|
|
||||||
break;
|
|
||||||
case Column::FREQ:
|
|
||||||
match = parseRange(txt, data.freq);
|
|
||||||
break;
|
|
||||||
case Column::COUNT:
|
|
||||||
match = parseRange(txt, data.count);
|
|
||||||
break;
|
|
||||||
case Column::DATA:
|
|
||||||
match = utils::toHex(data.dat).contains(txt, Qt::CaseInsensitive);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MessageListModel::filterAndSort() {
|
|
||||||
// merge CAN and DBC messages
|
|
||||||
std::vector<MessageId> all_messages;
|
|
||||||
all_messages.reserve(can->lastMessages().size() + dbc_messages_.size());
|
|
||||||
auto dbc_msgs = dbc_messages_;
|
|
||||||
for (const auto &[id, m] : can->lastMessages()) {
|
|
||||||
all_messages.push_back(id);
|
|
||||||
dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address});
|
|
||||||
}
|
|
||||||
all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end());
|
|
||||||
|
|
||||||
// filter and sort
|
|
||||||
std::vector<Item> items;
|
|
||||||
items.reserve(all_messages.size());
|
|
||||||
for (const auto &id : all_messages) {
|
|
||||||
if (show_inactive_messages || can->isMessageActive(id)) {
|
|
||||||
auto msg = dbc()->msg(id);
|
|
||||||
Item item = {.id = id,
|
|
||||||
.name = msg ? QString::fromStdString(msg->name) : QString::fromStdString(UNTITLED),
|
|
||||||
.node = msg ? QString::fromStdString(msg->transmitter) : QString()};
|
|
||||||
if (match(item))
|
|
||||||
items.emplace_back(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortItems(items);
|
|
||||||
|
|
||||||
if (items_ != items) {
|
|
||||||
beginResetModel();
|
|
||||||
items_ = std::move(items);
|
|
||||||
endResetModel();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageListModel::msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids) {
|
|
||||||
if (has_new_ids || ((filters_.count(Column::FREQ) || filters_.count(Column::COUNT) || filters_.count(Column::DATA)) &&
|
|
||||||
++sort_threshold_ == settings.fps)) {
|
|
||||||
sort_threshold_ = 0;
|
|
||||||
if (filterAndSort()) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update viewport
|
|
||||||
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageListModel::sort(int column, Qt::SortOrder order) {
|
|
||||||
if (column != Column::DATA) {
|
|
||||||
sort_column = column;
|
|
||||||
sort_order = order;
|
|
||||||
filterAndSort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageView
|
|
||||||
|
|
||||||
void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
|
||||||
const auto &item = ((MessageListModel*)model())->items_[index.row()];
|
|
||||||
if (!can->isMessageActive(item.id)) {
|
|
||||||
QStyleOptionViewItem custom_option = option;
|
|
||||||
custom_option.palette.setBrush(QPalette::Text, custom_option.palette.color(QPalette::Disabled, QPalette::Text));
|
|
||||||
auto color = QApplication::palette().color(QPalette::HighlightedText);
|
|
||||||
color.setAlpha(100);
|
|
||||||
custom_option.palette.setBrush(QPalette::HighlightedText, color);
|
|
||||||
QTreeView::drawRow(painter, custom_option, index);
|
|
||||||
} else {
|
|
||||||
QTreeView::drawRow(painter, option, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
QPen oldPen = painter->pen();
|
|
||||||
const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this);
|
|
||||||
painter->setPen(QColor::fromRgba(static_cast<QRgb>(gridHint)));
|
|
||||||
// Draw bottom border for the row
|
|
||||||
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
|
|
||||||
// Draw vertical borders for each column
|
|
||||||
for (int i = 0; i < header()->count(); ++i) {
|
|
||||||
int sectionX = header()->sectionViewportPosition(i);
|
|
||||||
painter->drawLine(sectionX, option.rect.top(), sectionX, option.rect.bottom());
|
|
||||||
}
|
|
||||||
painter->setPen(oldPen);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageView::setModel(QAbstractItemModel *model) {
|
|
||||||
QTreeView::setModel(model);
|
|
||||||
// Bypass the slow call to QTreeView::dataChanged.
|
|
||||||
// QTreeView::dataChanged will invalidate the height cache and that's what we don't need in MessageView.
|
|
||||||
QObject::disconnect(model, &QAbstractItemModel::dataChanged, this, nullptr);
|
|
||||||
QObject::connect(model, &QAbstractItemModel::dataChanged, this,
|
|
||||||
[this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageView::updateBytesSectionSize() {
|
|
||||||
auto delegate = ((MessageBytesDelegate *)itemDelegate());
|
|
||||||
int max_bytes = 8;
|
|
||||||
if (!delegate->multipleLines()) {
|
|
||||||
for (const auto &[_, m] : can->lastMessages()) {
|
|
||||||
max_bytes = std::max<int>(max_bytes, m.dat.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setUniformRowHeights(!delegate->multipleLines());
|
|
||||||
header()->resizeSection(MessageListModel::Column::DATA, delegate->sizeForBytes(max_bytes).width());
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageView::wheelEvent(QWheelEvent *event) {
|
|
||||||
if (event->modifiers() == Qt::ShiftModifier) {
|
|
||||||
QApplication::sendEvent(horizontalScrollBar(), event);
|
|
||||||
} else {
|
|
||||||
QTreeView::wheelEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageViewHeader
|
|
||||||
|
|
||||||
MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizontal, parent) {
|
|
||||||
QObject::connect(this, &QHeaderView::sectionResized, this, &MessageViewHeader::updateHeaderPositions);
|
|
||||||
QObject::connect(this, &QHeaderView::sectionMoved, this, &MessageViewHeader::updateHeaderPositions);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageViewHeader::updateFilters() {
|
|
||||||
std::map<int, QString> filters;
|
|
||||||
for (int i = 0; i < (int)editors.size(); i++) {
|
|
||||||
if (!editors[i]->text().isEmpty()) {
|
|
||||||
filters[i] = editors[i]->text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
qobject_cast<MessageListModel*>(model())->setFilterStrings(filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageViewHeader::updateHeaderPositions() {
|
|
||||||
QSize sz = QHeaderView::sizeHint();
|
|
||||||
for (int i = 0; i < (int)editors.size(); i++) {
|
|
||||||
int h = editors[i]->sizeHint().height();
|
|
||||||
editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h);
|
|
||||||
editors[i]->setHidden(isSectionHidden(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageViewHeader::updateGeometries() {
|
|
||||||
for (int i = (int)editors.size(); i < count(); i++) {
|
|
||||||
QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
|
|
||||||
auto edit = new QLineEdit(this);
|
|
||||||
edit->setClearButtonEnabled(true);
|
|
||||||
edit->setPlaceholderText(tr("Filter %1").arg(column_name));
|
|
||||||
|
|
||||||
QObject::connect(edit, &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters);
|
|
||||||
editors.push_back(edit);
|
|
||||||
}
|
|
||||||
setViewportMargins(0, 0, 0, !editors.empty() ? editors[0]->sizeHint().height() : 0);
|
|
||||||
|
|
||||||
QHeaderView::updateGeometries();
|
|
||||||
updateHeaderPositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize MessageViewHeader::sizeHint() const {
|
|
||||||
QSize sz = QHeaderView::sizeHint();
|
|
||||||
return !editors.empty() ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz;
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <map>
|
|
||||||
#include <optional>
|
|
||||||
#include <set>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QAbstractTableModel>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QTreeView>
|
|
||||||
#include <QWheelEvent>
|
|
||||||
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
class MessageListModel : public QAbstractTableModel {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum Column {
|
|
||||||
NAME = 0,
|
|
||||||
SOURCE,
|
|
||||||
ADDRESS,
|
|
||||||
NODE,
|
|
||||||
FREQ,
|
|
||||||
COUNT,
|
|
||||||
DATA,
|
|
||||||
};
|
|
||||||
|
|
||||||
MessageListModel(QObject *parent) : QAbstractTableModel(parent) {}
|
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
|
||||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column::DATA + 1; }
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); }
|
|
||||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
|
|
||||||
void setFilterStrings(const std::map<int, QString> &filters);
|
|
||||||
void showInactiveMessages(bool show);
|
|
||||||
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
|
|
||||||
bool filterAndSort();
|
|
||||||
void dbcModified();
|
|
||||||
|
|
||||||
struct Item {
|
|
||||||
MessageId id;
|
|
||||||
QString name;
|
|
||||||
QString node;
|
|
||||||
bool operator==(const Item &other) const {
|
|
||||||
return id == other.id && name == other.name && node == other.node;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
std::vector<Item> items_;
|
|
||||||
bool show_inactive_messages = true;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void sortItems(std::vector<MessageListModel::Item> &items);
|
|
||||||
bool match(const MessageListModel::Item &id);
|
|
||||||
|
|
||||||
std::map<int, QString> filters_;
|
|
||||||
std::set<MessageId> dbc_messages_;
|
|
||||||
int sort_column = 0;
|
|
||||||
Qt::SortOrder sort_order = Qt::AscendingOrder;
|
|
||||||
int sort_threshold_ = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class MessageView : public QTreeView {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
MessageView(QWidget *parent) : QTreeView(parent) {}
|
|
||||||
void updateBytesSectionSize();
|
|
||||||
void setModel(QAbstractItemModel *model) override;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
|
||||||
void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override {}
|
|
||||||
void wheelEvent(QWheelEvent *event) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
class MessageViewHeader : public QHeaderView {
|
|
||||||
// https://stackoverflow.com/a/44346317
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
MessageViewHeader(QWidget *parent);
|
|
||||||
void updateHeaderPositions();
|
|
||||||
void updateGeometries() override;
|
|
||||||
QSize sizeHint() const override;
|
|
||||||
void updateFilters();
|
|
||||||
|
|
||||||
std::vector<QLineEdit *> editors;
|
|
||||||
};
|
|
||||||
|
|
||||||
class MessagesWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
MessagesWidget(QWidget *parent);
|
|
||||||
void selectMessage(const MessageId &message_id);
|
|
||||||
std::vector<uint8_t> saveHeaderState() const {
|
|
||||||
const auto state = view->header()->saveState();
|
|
||||||
return {state.begin(), state.end()};
|
|
||||||
}
|
|
||||||
bool restoreHeaderState(const std::vector<uint8_t> &state) const {
|
|
||||||
return view->header()->restoreState({(const char *)state.data(), (int)state.size()});
|
|
||||||
}
|
|
||||||
void suppressHighlighted();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void msgSelectionChanged(const MessageId &message_id);
|
|
||||||
void titleChanged(const QString &title);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
QWidget *createToolBar();
|
|
||||||
void headerContextMenuEvent(const QPoint &pos);
|
|
||||||
void menuAboutToShow();
|
|
||||||
void setMultiLineBytes(bool multi);
|
|
||||||
void updateTitle();
|
|
||||||
|
|
||||||
MessageView *view;
|
|
||||||
MessageViewHeader *header;
|
|
||||||
MessageBytesDelegate *delegate;
|
|
||||||
std::optional<MessageId> current_msg_id;
|
|
||||||
MessageListModel *model;
|
|
||||||
QPushButton *suppress_add;
|
|
||||||
QPushButton *suppress_clear;
|
|
||||||
QMenu *menu;
|
|
||||||
};
|
|
||||||
@@ -122,7 +122,7 @@ void Panda::set_data_speed_kbps(uint16_t bus, uint16_t speed) {
|
|||||||
|
|
||||||
|
|
||||||
bool Panda::can_receive(std::vector<can_frame>& out_vec) {
|
bool Panda::can_receive(std::vector<can_frame>& out_vec) {
|
||||||
// Check if enough space left in buffer to store RECV_SIZE data
|
|
||||||
assert(receive_buffer_size + RECV_SIZE <= sizeof(receive_buffer));
|
assert(receive_buffer_size + RECV_SIZE <= sizeof(receive_buffer));
|
||||||
|
|
||||||
int recv = bulk_read(0x81, &receive_buffer[receive_buffer_size], RECV_SIZE);
|
int recv = bulk_read(0x81, &receive_buffer[receive_buffer_size], RECV_SIZE);
|
||||||
@@ -151,7 +151,7 @@ bool Panda::unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector<can_fra
|
|||||||
|
|
||||||
const uint8_t data_len = dlc_to_len[header.data_len_code];
|
const uint8_t data_len = dlc_to_len[header.data_len_code];
|
||||||
if (pos + sizeof(can_header) + data_len > size) {
|
if (pos + sizeof(can_header) + data_len > size) {
|
||||||
// we don't have all the data for this message yet
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ bool Panda::unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector<can_fra
|
|||||||
pos += sizeof(can_header) + data_len;
|
pos += sizeof(can_header) + data_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
// move the overflowing data to the beginning of the buffer for the next round
|
|
||||||
memmove(data, &data[pos], size - pos);
|
memmove(data, &data[pos], size - pos);
|
||||||
size -= pos;
|
size -= pos;
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ uint8_t Panda::calculate_checksum(uint8_t *data, uint32_t len) {
|
|||||||
return checksum;
|
return checksum;
|
||||||
}
|
}
|
||||||
|
|
||||||
// USB implementation methods
|
|
||||||
bool Panda::init_usb_connection(const std::string& serial) {
|
bool Panda::init_usb_connection(const std::string& serial) {
|
||||||
ssize_t num_devices;
|
ssize_t num_devices;
|
||||||
libusb_device **dev_list = NULL;
|
libusb_device **dev_list = NULL;
|
||||||
@@ -201,7 +201,7 @@ bool Panda::init_usb_connection(const std::string& serial) {
|
|||||||
ctx = init_usb_ctx();
|
ctx = init_usb_ctx();
|
||||||
if (!ctx) { goto fail; }
|
if (!ctx) { goto fail; }
|
||||||
|
|
||||||
// connect by serial
|
|
||||||
num_devices = libusb_get_device_list(ctx, &dev_list);
|
num_devices = libusb_get_device_list(ctx, &dev_list);
|
||||||
if (num_devices < 0) { goto fail; }
|
if (num_devices < 0) { goto fail; }
|
||||||
|
|
||||||
@@ -333,7 +333,7 @@ int Panda::bulk_read(unsigned char endpoint, unsigned char* data, int length, un
|
|||||||
do {
|
do {
|
||||||
err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout);
|
err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout);
|
||||||
if (err == LIBUSB_ERROR_TIMEOUT) {
|
if (err == LIBUSB_ERROR_TIMEOUT) {
|
||||||
break; // timeout is okay to exit, recv still happened
|
break;
|
||||||
} else if (err == LIBUSB_ERROR_OVERFLOW) {
|
} else if (err == LIBUSB_ERROR_OVERFLOW) {
|
||||||
comms_healthy_flag = false;
|
comms_healthy_flag = false;
|
||||||
LOGE_100("overflow got 0x%x", transferred);
|
LOGE_100("overflow got 0x%x", transferred);
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ public:
|
|||||||
bool comms_healthy();
|
bool comms_healthy();
|
||||||
std::string hw_serial();
|
std::string hw_serial();
|
||||||
|
|
||||||
// Static functions
|
|
||||||
static std::vector<std::string> list(bool usb_only=false);
|
static std::vector<std::string> list(bool usb_only=false);
|
||||||
|
|
||||||
// Panda functionality
|
|
||||||
cereal::PandaState::PandaType get_hw_type();
|
cereal::PandaState::PandaType get_hw_type();
|
||||||
void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U);
|
void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U);
|
||||||
void send_heartbeat(bool engaged);
|
void send_heartbeat(bool engaged);
|
||||||
@@ -71,18 +71,18 @@ public:
|
|||||||
void can_reset_communications();
|
void can_reset_communications();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// USB connection members
|
|
||||||
libusb_context *ctx = nullptr;
|
libusb_context *ctx = nullptr;
|
||||||
libusb_device_handle *dev_handle = nullptr;
|
libusb_device_handle *dev_handle = nullptr;
|
||||||
std::string hw_serial_str;
|
std::string hw_serial_str;
|
||||||
std::atomic<bool> connected_flag = true;
|
std::atomic<bool> connected_flag = true;
|
||||||
std::atomic<bool> comms_healthy_flag = true;
|
std::atomic<bool> comms_healthy_flag = true;
|
||||||
|
|
||||||
// CAN buffer members
|
|
||||||
uint8_t receive_buffer[RECV_SIZE + sizeof(can_header) + 64];
|
uint8_t receive_buffer[RECV_SIZE + sizeof(can_header) + 64];
|
||||||
uint32_t receive_buffer_size = 0;
|
uint32_t receive_buffer_size = 0;
|
||||||
|
|
||||||
// Internal methods
|
|
||||||
bool init_usb_connection(const std::string& serial);
|
bool init_usb_connection(const std::string& serial);
|
||||||
void cleanup_usb();
|
void cleanup_usb();
|
||||||
void handle_usb_issue(int err, const char func[]);
|
void handle_usb_issue(int err, const char func[]);
|
||||||
|
|||||||
115
iqpilot/tools/cabana/routes.cc
Normal file
115
iqpilot/tools/cabana/routes.cc
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#include "tools/cabana/routes.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <ctime>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "json11/json11.hpp"
|
||||||
|
#include "tools/replay/api.h"
|
||||||
|
|
||||||
|
namespace routes {
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
time_t secs = timegm(&tm);
|
||||||
|
if (secs == static_cast<time_t>(-1)) return 0;
|
||||||
|
return static_cast<int64_t>(secs) * 1000 + millis;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string 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 buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DeviceInfo> parseDevices(const std::string &json) {
|
||||||
|
std::vector<DeviceInfo> devices;
|
||||||
|
std::string err;
|
||||||
|
auto doc = json11::Json::parse(json, err);
|
||||||
|
if (err.empty() && doc.is_array()) {
|
||||||
|
for (const auto &device : doc.array_items()) {
|
||||||
|
devices.push_back({device["dongle_id"].string_value()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<RouteInfo> parseRoutes(const std::string &json, bool preserved) {
|
||||||
|
std::vector<RouteInfo> items;
|
||||||
|
std::string err;
|
||||||
|
auto doc = json11::Json::parse(json, err);
|
||||||
|
if (err.empty() && doc.is_array()) {
|
||||||
|
for (const auto &route : doc.array_items()) {
|
||||||
|
RouteInfo info;
|
||||||
|
info.name = route["fullname"].string_value();
|
||||||
|
if (preserved) {
|
||||||
|
info.start_ms = parseIsoToUnixMs(route["start_time"].string_value());
|
||||||
|
info.end_ms = parseIsoToUnixMs(route["end_time"].string_value());
|
||||||
|
} else {
|
||||||
|
info.start_ms = static_cast<int64_t>(route["start_time_utc_millis"].number_value());
|
||||||
|
info.end_ms = static_cast<int64_t>(route["end_time_utc_millis"].number_value());
|
||||||
|
}
|
||||||
|
items.push_back(std::move(info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchDevices(DevicesCallback callback) {
|
||||||
|
std::thread([callback = std::move(callback)]() {
|
||||||
|
std::string result = CommaApi2::getDevices();
|
||||||
|
auto [success, error_code] = checkApiResponse(result);
|
||||||
|
callback(success ? parseDevices(result) : std::vector<DeviceInfo>{}, success, error_code);
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchRoutes(const std::string &dongle_id, int period_days, RoutesCallback callback) {
|
||||||
|
const bool preserved = period_days == -1;
|
||||||
|
int64_t start_ms = 0, end_ms = 0;
|
||||||
|
if (!preserved) {
|
||||||
|
end_ms = nowUnixMs();
|
||||||
|
start_ms = end_ms - static_cast<int64_t>(period_days) * 24LL * 60LL * 60LL * 1000LL;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread([dongle_id, start_ms, end_ms, preserved, callback = std::move(callback)]() {
|
||||||
|
std::string result = CommaApi2::getDeviceRoutes(dongle_id, start_ms, end_ms, preserved);
|
||||||
|
auto [success, error_code] = checkApiResponse(result);
|
||||||
|
callback(success ? parseRoutes(result, preserved) : std::vector<RouteInfo>{}, success, error_code);
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
36
iqpilot/tools/cabana/routes.h
Normal file
36
iqpilot/tools/cabana/routes.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace routes {
|
||||||
|
|
||||||
|
struct DeviceInfo {
|
||||||
|
std::string dongle_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RouteInfo {
|
||||||
|
std::string name;
|
||||||
|
int64_t start_ms = 0;
|
||||||
|
int64_t end_ms = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
using DevicesCallback = std::function<void(std::vector<DeviceInfo> devices, bool success, int error_code)>;
|
||||||
|
using RoutesCallback = std::function<void(std::vector<RouteInfo> routes, bool success, int error_code)>;
|
||||||
|
|
||||||
|
std::pair<bool, int> checkApiResponse(const std::string &result);
|
||||||
|
|
||||||
|
int64_t nowUnixMs();
|
||||||
|
int64_t parseIsoToUnixMs(const std::string &s);
|
||||||
|
std::string formatUnixMs(int64_t ms);
|
||||||
|
|
||||||
|
std::vector<DeviceInfo> parseDevices(const std::string &json);
|
||||||
|
std::vector<RouteInfo> parseRoutes(const std::string &json, bool preserved);
|
||||||
|
|
||||||
|
void fetchDevices(DevicesCallback callback);
|
||||||
|
void fetchRoutes(const std::string &dongle_id, int period_days, RoutesCallback callback);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,41 +2,23 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cctype>
|
|
||||||
#include <charconv>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <map>
|
|
||||||
#include <string_view>
|
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#ifdef __APPLE__
|
|
||||||
#include <CoreFoundation/CoreFoundation.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <QAbstractButton>
|
|
||||||
#include <QDialogButtonBox>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
#include "third_party/json11/json11.hpp"
|
#include "json11/json11.hpp"
|
||||||
#include "tools/cabana/utils/util.h"
|
#include "tools/cabana/utils/util.h"
|
||||||
|
|
||||||
const int MIN_CACHE_MINIUTES = 30;
|
|
||||||
const int MAX_CACHE_MINIUTES = 120;
|
|
||||||
|
|
||||||
Settings settings;
|
Settings settings;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -150,256 +132,6 @@ bool preserveCorruptSettings() {
|
|||||||
return true;
|
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>
|
template <typename T>
|
||||||
void readSetting(const json11::Json::object &settings_json, const char *key, T &value) {
|
void readSetting(const json11::Json::object &settings_json, const char *key, T &value) {
|
||||||
auto it = settings_json.find(key);
|
auto it = settings_json.find(key);
|
||||||
@@ -431,20 +163,6 @@ void readSetting(const json11::Json::object &settings_json, const char *key, std
|
|||||||
value = std::move(stored);
|
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>
|
template <typename T>
|
||||||
void writeSetting(json11::Json::object &settings_json, const char *key, const T &value) {
|
void writeSetting(json11::Json::object &settings_json, const char *key, const T &value) {
|
||||||
if constexpr (std::is_same_v<T, bool>) {
|
if constexpr (std::is_same_v<T, bool>) {
|
||||||
@@ -462,32 +180,17 @@ void writeSetting(json11::Json::object &settings_json, const char *key, const st
|
|||||||
settings_json[key] = 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>
|
template <class Store, class SettingOperation>
|
||||||
void settingsOp(Store &s, SettingOperation op) {
|
void settingsOp(Store &s, SettingOperation op) {
|
||||||
op(s, "absolute_time", settings.absolute_time);
|
op(s, "absolute_time", settings.absolute_time);
|
||||||
op(s, "fps", settings.fps);
|
|
||||||
op(s, "max_cached_minutes", settings.max_cached_minutes);
|
op(s, "max_cached_minutes", settings.max_cached_minutes);
|
||||||
op(s, "chart_height", settings.chart_height);
|
op(s, "chart_height", settings.chart_height);
|
||||||
op(s, "chart_range", settings.chart_range);
|
op(s, "chart_range", settings.chart_range);
|
||||||
op(s, "chart_column_count", settings.chart_column_count);
|
op(s, "chart_column_count", settings.chart_column_count);
|
||||||
op(s, "last_dir", settings.last_dir);
|
op(s, "last_dir", settings.last_dir);
|
||||||
op(s, "last_route_dir", settings.last_route_dir);
|
op(s, "last_route_dir", settings.last_route_dir);
|
||||||
op(s, "window_state", settings.window_state);
|
|
||||||
op(s, "geometry", settings.geometry);
|
|
||||||
op(s, "video_splitter_state", settings.video_splitter_state);
|
|
||||||
op(s, "recent_files", settings.recent_files);
|
op(s, "recent_files", settings.recent_files);
|
||||||
op(s, "message_header_state", settings.message_header_state);
|
op(s, "ui_state", settings.ui_state);
|
||||||
op(s, "chart_series_type", settings.chart_series_type);
|
op(s, "chart_series_type", settings.chart_series_type);
|
||||||
op(s, "theme", settings.theme);
|
op(s, "theme", settings.theme);
|
||||||
op(s, "sparkline_range", settings.sparkline_range);
|
op(s, "sparkline_range", settings.sparkline_range);
|
||||||
@@ -502,7 +205,7 @@ void settingsOp(Store &s, SettingOperation op) {
|
|||||||
op(s, "active_charts", settings.active_charts);
|
op(s, "active_charts", settings.active_charts);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
}
|
||||||
|
|
||||||
Settings::Settings() {
|
Settings::Settings() {
|
||||||
last_dir = last_route_dir = utils::homePath();
|
last_dir = last_route_dir = utils::homePath();
|
||||||
@@ -511,17 +214,10 @@ Settings::Settings() {
|
|||||||
if (stored_settings.valid) {
|
if (stored_settings.valid) {
|
||||||
if (stored_settings.exists) {
|
if (stored_settings.exists) {
|
||||||
settingsOp(stored_settings.values, [](const auto &s, const char *key, auto &value) { readSetting(s, key, value); });
|
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);
|
if (theme != LIGHT_THEME && theme != DARK_THEME) theme = LIGHT_THEME;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
void Settings::save() {
|
||||||
if (!ensureSettingsDirectory()) return;
|
if (!ensureSettingsDirectory()) return;
|
||||||
|
|
||||||
@@ -535,84 +231,3 @@ void Settings::save() {
|
|||||||
settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); });
|
settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); });
|
||||||
saveSettings(stored_settings.values);
|
saveSettings(stored_settings.values);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SettingsDlg
|
|
||||||
|
|
||||||
SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
|
|
||||||
setWindowTitle(tr("Settings"));
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
QGroupBox *groupbox = new QGroupBox("General");
|
|
||||||
QFormLayout *form_layout = new QFormLayout(groupbox);
|
|
||||||
|
|
||||||
form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this));
|
|
||||||
theme->setToolTip(tr("You may need to restart cabana after changes theme"));
|
|
||||||
theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")});
|
|
||||||
theme->setCurrentIndex(settings.theme);
|
|
||||||
|
|
||||||
form_layout->addRow("FPS", fps = new QSpinBox(this));
|
|
||||||
fps->setRange(10, 100);
|
|
||||||
fps->setSingleStep(10);
|
|
||||||
fps->setValue(settings.fps);
|
|
||||||
|
|
||||||
form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this));
|
|
||||||
cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES);
|
|
||||||
cached_minutes->setSingleStep(1);
|
|
||||||
cached_minutes->setValue(settings.max_cached_minutes);
|
|
||||||
main_layout->addWidget(groupbox);
|
|
||||||
|
|
||||||
groupbox = new QGroupBox("New Signal Settings");
|
|
||||||
form_layout = new QFormLayout(groupbox);
|
|
||||||
form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this));
|
|
||||||
drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")});
|
|
||||||
drag_direction->setCurrentIndex(settings.drag_direction);
|
|
||||||
main_layout->addWidget(groupbox);
|
|
||||||
|
|
||||||
groupbox = new QGroupBox("Chart");
|
|
||||||
form_layout = new QFormLayout(groupbox);
|
|
||||||
form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this));
|
|
||||||
chart_height->setRange(100, 500);
|
|
||||||
chart_height->setSingleStep(10);
|
|
||||||
chart_height->setValue(settings.chart_height);
|
|
||||||
main_layout->addWidget(groupbox);
|
|
||||||
|
|
||||||
log_livestream = new QGroupBox(tr("Enable live stream logging"), this);
|
|
||||||
log_livestream->setCheckable(true);
|
|
||||||
log_livestream->setChecked(settings.log_livestream);
|
|
||||||
QHBoxLayout *path_layout = new QHBoxLayout(log_livestream);
|
|
||||||
path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this));
|
|
||||||
log_path->setReadOnly(true);
|
|
||||||
auto browse_btn = new QPushButton(tr("B&rowse..."));
|
|
||||||
path_layout->addWidget(browse_btn);
|
|
||||||
main_layout->addWidget(log_livestream);
|
|
||||||
|
|
||||||
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
|
||||||
main_layout->addWidget(buttonBox);
|
|
||||||
setFixedSize(400, sizeHint().height());
|
|
||||||
|
|
||||||
QObject::connect(browse_btn, &QPushButton::clicked, [this]() {
|
|
||||||
QString fn = QFileDialog::getExistingDirectory(
|
|
||||||
this, tr("Log File Location"),
|
|
||||||
QString::fromStdString(utils::homePath()),
|
|
||||||
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
|
|
||||||
if (!fn.isEmpty()) {
|
|
||||||
log_path->setText(fn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
|
||||||
QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SettingsDlg::save() {
|
|
||||||
if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) {
|
|
||||||
// set theme before emit changed
|
|
||||||
utils::setTheme(settings.theme);
|
|
||||||
}
|
|
||||||
settings.fps = fps->value();
|
|
||||||
settings.max_cached_minutes = cached_minutes->value();
|
|
||||||
settings.chart_height = chart_height->value();
|
|
||||||
settings.log_livestream = log_livestream->isChecked();
|
|
||||||
settings.log_path = log_path->text().toStdString();
|
|
||||||
settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex();
|
|
||||||
emit settings.changed();
|
|
||||||
QDialog::accept();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,45 +1,18 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QDialog>
|
|
||||||
#include <QGroupBox>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QSpinBox>
|
|
||||||
|
|
||||||
|
#include "tools/cabana/core/observable.h"
|
||||||
#include "tools/cabana/core/settings.h"
|
#include "tools/cabana/core/settings.h"
|
||||||
|
|
||||||
class Settings : public QObject, public CabanaSettingsState {
|
class Settings : public CabanaSettingsState {
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Settings();
|
Settings();
|
||||||
void save();
|
void save();
|
||||||
|
|
||||||
// Qt frontend layout state. This intentionally stays outside CabanaSettingsState.
|
std::string ui_state;
|
||||||
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:
|
Observable<> changed;
|
||||||
void changed();
|
|
||||||
};
|
|
||||||
|
|
||||||
class SettingsDlg : public QDialog {
|
|
||||||
public:
|
|
||||||
SettingsDlg(QWidget *parent);
|
|
||||||
void save();
|
|
||||||
QSpinBox *fps;
|
|
||||||
QSpinBox *cached_minutes;
|
|
||||||
QSpinBox *chart_height;
|
|
||||||
QComboBox *chart_series_type;
|
|
||||||
QComboBox *theme;
|
|
||||||
QGroupBox *log_livestream;
|
|
||||||
QLineEdit *log_path;
|
|
||||||
QComboBox *drag_direction;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Settings settings;
|
extern Settings settings;
|
||||||
|
|||||||
@@ -1,719 +0,0 @@
|
|||||||
#include "tools/cabana/signalview.h"
|
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <future>
|
|
||||||
|
|
||||||
#include <QDialogButtonBox>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPainterPath>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "tools/cabana/commands.h"
|
|
||||||
#include "tools/cabana/utils/util.h"
|
|
||||||
|
|
||||||
// SignalModel
|
|
||||||
|
|
||||||
static QString signalTypeToString(cabana::Signal::Type type) {
|
|
||||||
if (type == cabana::Signal::Type::Multiplexor) return "Multiplexor Signal";
|
|
||||||
else if (type == cabana::Signal::Type::Multiplexed) return "Multiplexed Signal";
|
|
||||||
else return "Normal Signal";
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) {
|
|
||||||
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{.type = Item::Sig, .parent = root_item, .sig = sig, .title = QString::fromStdString(sig->name)};
|
|
||||||
root_item->children.insert(root_item->children.begin() + pos, parent_item);
|
|
||||||
QString titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type",
|
|
||||||
"Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"};
|
|
||||||
for (int i = 0; i < std::size(titles); ++i) {
|
|
||||||
auto item = new Item{.type = (Item::Type)(i + Item::Name), .parent = parent_item, .sig = sig, .title = titles[i]};
|
|
||||||
parent_item->children.push_back(item);
|
|
||||||
if (item->type == Item::ExtraInfo) {
|
|
||||||
parent_item = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalModel::setMessage(const MessageId &id) {
|
|
||||||
msg_id = id;
|
|
||||||
filter_str = "";
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalModel::setFilter(const QString &txt) {
|
|
||||||
filter_str = txt;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalModel::refresh() {
|
|
||||||
beginResetModel();
|
|
||||||
root.reset(new SignalModel::Item);
|
|
||||||
if (auto msg = dbc()->msg(msg_id)) {
|
|
||||||
for (auto s : msg->getSignals()) {
|
|
||||||
if (filter_str.isEmpty() || QString::fromStdString(s->name).contains(filter_str, Qt::CaseInsensitive)) {
|
|
||||||
insertItem(root.get(), root->children.size(), s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalModel::Item *SignalModel::getItem(const QModelIndex &index) const {
|
|
||||||
auto item = index.isValid() ? (SignalModel::Item *)index.internalPointer() : nullptr;
|
|
||||||
return item ? item : root.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
int SignalModel::rowCount(const QModelIndex &parent) const {
|
|
||||||
if (parent.isValid() && parent.column() > 0) return 0;
|
|
||||||
|
|
||||||
return getItem(parent)->children.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
Qt::ItemFlags SignalModel::flags(const QModelIndex &index) const {
|
|
||||||
if (!index.isValid()) return Qt::NoItemFlags;
|
|
||||||
|
|
||||||
auto item = getItem(index);
|
|
||||||
Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
|
|
||||||
if (index.column() == 1 && item->children.empty()) {
|
|
||||||
flags |= (item->type == Item::Endian || item->type == Item::Signed) ? Qt::ItemIsUserCheckable : Qt::ItemIsEditable;
|
|
||||||
}
|
|
||||||
if (item->type == Item::MultiplexValue && item->sig->type != cabana::Signal::Type::Multiplexed) {
|
|
||||||
flags &= ~Qt::ItemIsEnabled;
|
|
||||||
}
|
|
||||||
return flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
int SignalModel::signalRow(const cabana::Signal *sig) const {
|
|
||||||
for (int i = 0; i < root->children.size(); ++i) {
|
|
||||||
if (root->children[i]->sig == sig) return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex SignalModel::index(int row, int column, const QModelIndex &parent) const {
|
|
||||||
if (parent.isValid() && parent.column() != 0) return {};
|
|
||||||
|
|
||||||
auto parent_item = getItem(parent);
|
|
||||||
if (parent_item && row < parent_item->children.size()) {
|
|
||||||
return createIndex(row, column, parent_item->children[row]);
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex SignalModel::parent(const QModelIndex &index) const {
|
|
||||||
if (!index.isValid()) return {};
|
|
||||||
Item *parent_item = getItem(index)->parent;
|
|
||||||
return !parent_item || parent_item == root.get() ? QModelIndex() : createIndex(parent_item->row(), 0, parent_item);
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant SignalModel::data(const QModelIndex &index, int role) const {
|
|
||||||
if (index.isValid()) {
|
|
||||||
const Item *item = getItem(index);
|
|
||||||
if (role == Qt::DisplayRole || role == Qt::EditRole) {
|
|
||||||
if (index.column() == 0) {
|
|
||||||
return item->type == Item::Sig ? QString::fromStdString(item->sig->name) : item->title;
|
|
||||||
} else {
|
|
||||||
switch (item->type) {
|
|
||||||
case Item::Sig: return item->sig_val;
|
|
||||||
case Item::Name: return QString::fromStdString(item->sig->name);
|
|
||||||
case Item::Size: return item->sig->size;
|
|
||||||
case Item::Node: return QString::fromStdString(item->sig->receiver_name);
|
|
||||||
case Item::SignalType: return signalTypeToString(item->sig->type);
|
|
||||||
case Item::MultiplexValue: return item->sig->multiplex_value;
|
|
||||||
case Item::Offset: return QString::fromStdString(doubleToString(item->sig->offset));
|
|
||||||
case Item::Factor: return QString::fromStdString(doubleToString(item->sig->factor));
|
|
||||||
case Item::Unit: return QString::fromStdString(item->sig->unit);
|
|
||||||
case Item::Comment: return QString::fromStdString(item->sig->comment);
|
|
||||||
case Item::Min: return QString::fromStdString(doubleToString(item->sig->min));
|
|
||||||
case Item::Max: return QString::fromStdString(doubleToString(item->sig->max));
|
|
||||||
case Item::Desc: {
|
|
||||||
QStringList val_desc;
|
|
||||||
for (auto &[val, desc] : item->sig->val_desc) {
|
|
||||||
val_desc << QString("%1 \"%2\"").arg(val).arg(QString::fromStdString(desc));
|
|
||||||
}
|
|
||||||
return val_desc.join(" ");
|
|
||||||
}
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (role == Qt::CheckStateRole && index.column() == 1) {
|
|
||||||
if (item->type == Item::Endian) return item->sig->is_little_endian ? Qt::Checked : Qt::Unchecked;
|
|
||||||
if (item->type == Item::Signed) return item->sig->is_signed ? Qt::Checked : Qt::Unchecked;
|
|
||||||
} else if (role == Qt::ToolTipRole && item->type == Item::Sig) {
|
|
||||||
return (index.column() == 0) ? signalToolTip(item->sig) : QString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int role) {
|
|
||||||
if (role != Qt::EditRole && role != Qt::CheckStateRole) return false;
|
|
||||||
|
|
||||||
Item *item = getItem(index);
|
|
||||||
cabana::Signal s = *item->sig;
|
|
||||||
switch (item->type) {
|
|
||||||
case Item::Name: s.name = value.toString().toStdString(); break;
|
|
||||||
case Item::Size: s.size = value.toInt(); break;
|
|
||||||
case Item::Node: s.receiver_name = value.toString().trimmed().toStdString(); break;
|
|
||||||
case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break;
|
|
||||||
case Item::MultiplexValue: s.multiplex_value = value.toInt(); break;
|
|
||||||
case Item::Endian: s.is_little_endian = value.toBool(); break;
|
|
||||||
case Item::Signed: s.is_signed = value.toBool(); break;
|
|
||||||
case Item::Offset: s.offset = value.toDouble(); break;
|
|
||||||
case Item::Factor: s.factor = value.toDouble(); break;
|
|
||||||
case Item::Unit: s.unit = value.toString().toStdString(); break;
|
|
||||||
case Item::Comment: s.comment = value.toString().toStdString(); break;
|
|
||||||
case Item::Min: s.min = value.toDouble(); break;
|
|
||||||
case Item::Max: s.max = value.toDouble(); break;
|
|
||||||
case Item::Desc: s.val_desc = value.value<ValueDescription>(); break;
|
|
||||||
default: return false;
|
|
||||||
}
|
|
||||||
bool ret = saveSignal(item->sig, s);
|
|
||||||
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole});
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) {
|
|
||||||
auto msg = dbc()->msg(msg_id);
|
|
||||||
if (s.name != origin_s->name && msg->sig(s.name) != nullptr) {
|
|
||||||
QString text = tr("There is already a signal with the same name '%1'").arg(QString::fromStdString(s.name));
|
|
||||||
QMessageBox::warning(nullptr, tr("Failed to save signal"), text);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.is_little_endian != origin_s->is_little_endian) {
|
|
||||||
s.start_bit = flipBitPos(s.start_bit);
|
|
||||||
}
|
|
||||||
UndoStack::instance()->push(new EditSignalCommand(msg_id, origin_s, s));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalModel::handleMsgChanged(MessageId id) {
|
|
||||||
if (id.address == msg_id.address) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
|
|
||||||
if (id == msg_id) {
|
|
||||||
if (filter_str.isEmpty()) {
|
|
||||||
int i = dbc()->msg(msg_id)->indexOf(sig);
|
|
||||||
beginInsertRows({}, i, i);
|
|
||||||
insertItem(root.get(), i, sig);
|
|
||||||
endInsertRows();
|
|
||||||
} else if (QString::fromStdString(sig->name).contains(filter_str, Qt::CaseInsensitive)) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
|
|
||||||
if (int row = signalRow(sig); row != -1) {
|
|
||||||
emit dataChanged(index(row, 0), index(row, 1), {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole});
|
|
||||||
|
|
||||||
if (filter_str.isEmpty()) {
|
|
||||||
// move row when the order changes.
|
|
||||||
int to = dbc()->msg(msg_id)->indexOf(sig);
|
|
||||||
if (to != row) {
|
|
||||||
beginMoveRows({}, row, row, {}, to > row ? to + 1 : to);
|
|
||||||
auto item = root->children[row];
|
|
||||||
root->children.erase(root->children.begin() + row);
|
|
||||||
root->children.insert(root->children.begin() + to, item);
|
|
||||||
endMoveRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalModel::handleSignalRemoved(const cabana::Signal *sig) {
|
|
||||||
if (int row = signalRow(sig); row != -1) {
|
|
||||||
beginRemoveRows({}, row, row);
|
|
||||||
delete root->children[row];
|
|
||||||
root->children.erase(root->children.begin() + row);
|
|
||||||
endRemoveRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignalItemDelegate
|
|
||||||
|
|
||||||
SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {
|
|
||||||
name_validator = new NameValidator(this);
|
|
||||||
node_validator = new NodeValidator(this);
|
|
||||||
double_validator = new DoubleValidator(this);
|
|
||||||
|
|
||||||
label_font.setPointSize(8);
|
|
||||||
minmax_font.setPixelSize(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize SignalItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
|
||||||
int width = option.widget->size().width() / 2;
|
|
||||||
if (index.column() == 0) {
|
|
||||||
int spacing = option.widget->style()->pixelMetric(QStyle::PM_TreeViewIndentation) + color_label_width + 8;
|
|
||||||
auto text = index.data(Qt::DisplayRole).toString();
|
|
||||||
auto item = (SignalModel::Item *)index.internalPointer();
|
|
||||||
if (item->type == SignalModel::Item::Sig && item->sig->type != cabana::Signal::Type::Normal) {
|
|
||||||
text += item->sig->type == cabana::Signal::Type::Multiplexor ? QString(" M ") : QString(" m%1 ").arg(item->sig->multiplex_value);
|
|
||||||
spacing += (option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1) * 2;
|
|
||||||
}
|
|
||||||
width = std::min<int>(option.widget->size().width() / 3.0, option.fontMetrics.horizontalAdvance(text) + spacing);
|
|
||||||
}
|
|
||||||
return {width, option.fontMetrics.height() + option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2};
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
|
||||||
auto item = (SignalModel::Item *)index.internalPointer();
|
|
||||||
if (editor && item->type == SignalModel::Item::Sig && index.column() == 1) {
|
|
||||||
QRect geom = option.rect;
|
|
||||||
geom.setLeft(geom.right() - editor->sizeHint().width());
|
|
||||||
editor->setGeometry(geom);
|
|
||||||
button_size = geom.size();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QStyledItemDelegate::updateEditorGeometry(editor, option, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
|
||||||
const int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
|
|
||||||
const int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin);
|
|
||||||
auto item = static_cast<SignalModel::Item*>(index.internalPointer());
|
|
||||||
|
|
||||||
QRect rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin);
|
|
||||||
painter->setRenderHint(QPainter::Antialiasing);
|
|
||||||
if (option.state & QStyle::State_Selected) {
|
|
||||||
painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index.column() == 0) {
|
|
||||||
if (item->type == SignalModel::Item::Sig) {
|
|
||||||
// color label
|
|
||||||
QPainterPath path;
|
|
||||||
QRect icon_rect{rect.x(), rect.y(), color_label_width, rect.height()};
|
|
||||||
path.addRoundedRect(icon_rect, 3, 3);
|
|
||||||
painter->setPen(item->highlight ? Qt::white : Qt::black);
|
|
||||||
painter->setFont(label_font);
|
|
||||||
painter->fillPath(path, toQColor(item->sig->color.darker(item->highlight ? 125 : 0)));
|
|
||||||
painter->drawText(icon_rect, Qt::AlignCenter, QString::number(item->row() + 1));
|
|
||||||
|
|
||||||
rect.setLeft(icon_rect.right() + h_margin * 2);
|
|
||||||
// multiplexer indicator
|
|
||||||
if (item->sig->type != cabana::Signal::Type::Normal) {
|
|
||||||
QString indicator = item->sig->type == cabana::Signal::Type::Multiplexor ? QString(" M ") : QString(" m%1 ").arg(item->sig->multiplex_value);
|
|
||||||
QRect indicator_rect{rect.x(), rect.y(), option.fontMetrics.horizontalAdvance(indicator), rect.height()};
|
|
||||||
painter->setBrush(Qt::gray);
|
|
||||||
painter->setPen(Qt::NoPen);
|
|
||||||
painter->drawRoundedRect(indicator_rect, 3, 3);
|
|
||||||
painter->setPen(Qt::white);
|
|
||||||
painter->drawText(indicator_rect, Qt::AlignCenter, indicator);
|
|
||||||
rect.setLeft(indicator_rect.right() + h_margin * 2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rect.setLeft(option.widget->style()->pixelMetric(QStyle::PM_TreeViewIndentation) + color_label_width + h_margin * 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// name
|
|
||||||
auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, rect.width());
|
|
||||||
painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text));
|
|
||||||
painter->setFont(option.font);
|
|
||||||
painter->drawText(rect, option.displayAlignment, text);
|
|
||||||
} else if (index.column() == 1) {
|
|
||||||
if (!item->sparkline.pixmap.isNull()) {
|
|
||||||
QSize sparkline_size = item->sparkline.pixmap.size() / item->sparkline.pixmap.devicePixelRatio();
|
|
||||||
painter->drawPixmap(QRect(rect.topLeft(), sparkline_size), item->sparkline.pixmap);
|
|
||||||
// min-max value
|
|
||||||
painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text));
|
|
||||||
rect.adjust(sparkline_size.width() + 1, 0, 0, 0);
|
|
||||||
int value_adjust = 10;
|
|
||||||
if (!item->sparkline.isEmpty() && (item->highlight || option.state & QStyle::State_Selected)) {
|
|
||||||
painter->drawLine(rect.topLeft(), rect.bottomLeft());
|
|
||||||
rect.adjust(5, -v_margin, 0, v_margin);
|
|
||||||
painter->setFont(minmax_font);
|
|
||||||
QString min = QString::number(item->sparkline.min_val);
|
|
||||||
QString max = QString::number(item->sparkline.max_val);
|
|
||||||
painter->drawText(rect, Qt::AlignLeft | Qt::AlignTop, max);
|
|
||||||
painter->drawText(rect, Qt::AlignLeft | Qt::AlignBottom, min);
|
|
||||||
QFontMetrics fm(minmax_font);
|
|
||||||
value_adjust = std::max(fm.horizontalAdvance(min), fm.horizontalAdvance(max)) + 5;
|
|
||||||
} else if (!item->sparkline.isEmpty() && item->sig->type == cabana::Signal::Type::Multiplexed) {
|
|
||||||
// display freq of multiplexed signal
|
|
||||||
painter->setFont(label_font);
|
|
||||||
QString freq = QString("%1 hz").arg(item->sparkline.freq(), 0, 'g', 2);
|
|
||||||
painter->drawText(rect.adjusted(5, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, freq);
|
|
||||||
value_adjust = QFontMetrics(label_font).horizontalAdvance(freq) + 10;
|
|
||||||
}
|
|
||||||
// signal value
|
|
||||||
painter->setFont(option.font);
|
|
||||||
rect.adjust(value_adjust, 0, -button_size.width(), 0);
|
|
||||||
auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, rect.width());
|
|
||||||
painter->drawText(rect, Qt::AlignRight | Qt::AlignVCenter, text);
|
|
||||||
} else {
|
|
||||||
QStyledItemDelegate::paint(painter, option, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
|
||||||
auto item = (SignalModel::Item *)index.internalPointer();
|
|
||||||
if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Node || item->type == SignalModel::Item::Offset ||
|
|
||||||
item->type == SignalModel::Item::Factor || item->type == SignalModel::Item::MultiplexValue ||
|
|
||||||
item->type == SignalModel::Item::Min || item->type == SignalModel::Item::Max) {
|
|
||||||
QLineEdit *e = new QLineEdit(parent);
|
|
||||||
e->setFrame(false);
|
|
||||||
if (item->type == SignalModel::Item::Name) e->setValidator(name_validator);
|
|
||||||
else if (item->type == SignalModel::Item::Node) e->setValidator(node_validator);
|
|
||||||
else e->setValidator(double_validator);
|
|
||||||
|
|
||||||
return e;
|
|
||||||
} else if (item->type == SignalModel::Item::Size) {
|
|
||||||
QSpinBox *spin = new QSpinBox(parent);
|
|
||||||
spin->setFrame(false);
|
|
||||||
spin->setRange(1, CAN_MAX_DATA_BYTES);
|
|
||||||
return spin;
|
|
||||||
} else if (item->type == SignalModel::Item::SignalType) {
|
|
||||||
QComboBox *c = new QComboBox(parent);
|
|
||||||
c->addItem(signalTypeToString(cabana::Signal::Type::Normal), (int)cabana::Signal::Type::Normal);
|
|
||||||
if (!dbc()->msg(((SignalModel *)index.model())->msg_id)->multiplexor) {
|
|
||||||
c->addItem(signalTypeToString(cabana::Signal::Type::Multiplexor), (int)cabana::Signal::Type::Multiplexor);
|
|
||||||
} else if (item->sig->type != cabana::Signal::Type::Multiplexor) {
|
|
||||||
c->addItem(signalTypeToString(cabana::Signal::Type::Multiplexed), (int)cabana::Signal::Type::Multiplexed);
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
} else if (item->type == SignalModel::Item::Desc) {
|
|
||||||
ValueDescriptionDlg dlg(item->sig->val_desc, parent);
|
|
||||||
dlg.setWindowTitle(QString::fromStdString(item->sig->name));
|
|
||||||
if (dlg.exec()) {
|
|
||||||
((QAbstractItemModel *)index.model())->setData(index, QVariant::fromValue(dlg.val_desc));
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return QStyledItemDelegate::createEditor(parent, option, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const {
|
|
||||||
auto item = (SignalModel::Item *)index.internalPointer();
|
|
||||||
if (item->type == SignalModel::Item::SignalType) {
|
|
||||||
model->setData(index, ((QComboBox*)editor)->currentData().toInt());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QStyledItemDelegate::setModelData(editor, model, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignalView
|
|
||||||
|
|
||||||
SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QFrame(parent) {
|
|
||||||
setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
|
|
||||||
// title bar
|
|
||||||
QWidget *title_bar = new QWidget(this);
|
|
||||||
QHBoxLayout *hl = new QHBoxLayout(title_bar);
|
|
||||||
hl->addWidget(signal_count_lb = new QLabel());
|
|
||||||
filter_edit = new QLineEdit(this);
|
|
||||||
filter_edit->setValidator(new NonWhitespaceValidator(this));
|
|
||||||
filter_edit->setClearButtonEnabled(true);
|
|
||||||
filter_edit->setPlaceholderText(tr("Filter Signal"));
|
|
||||||
hl->addWidget(filter_edit);
|
|
||||||
hl->addStretch(1);
|
|
||||||
|
|
||||||
// WARNING: increasing the maximum range can result in severe performance degradation.
|
|
||||||
// 30s is a reasonable value at present.
|
|
||||||
const int max_range = 30; // 30s
|
|
||||||
settings.sparkline_range = std::clamp(settings.sparkline_range, 1, max_range);
|
|
||||||
hl->addWidget(sparkline_label = new QLabel());
|
|
||||||
hl->addWidget(sparkline_range_slider = new QSlider(Qt::Horizontal, this));
|
|
||||||
sparkline_range_slider->setRange(1, max_range);
|
|
||||||
sparkline_range_slider->setValue(settings.sparkline_range);
|
|
||||||
sparkline_range_slider->setToolTip(tr("Sparkline time range"));
|
|
||||||
|
|
||||||
auto collapse_btn = new ToolButton("dash-square", tr("Collapse All"));
|
|
||||||
collapse_btn->setIconSize({12, 12});
|
|
||||||
hl->addWidget(collapse_btn);
|
|
||||||
|
|
||||||
// tree view
|
|
||||||
tree = new TreeView(this);
|
|
||||||
tree->setModel(model = new SignalModel(this));
|
|
||||||
tree->setItemDelegate(delegate = new SignalItemDelegate(this));
|
|
||||||
tree->setFrameShape(QFrame::NoFrame);
|
|
||||||
tree->setHeaderHidden(true);
|
|
||||||
tree->setMouseTracking(true);
|
|
||||||
tree->setExpandsOnDoubleClick(false);
|
|
||||||
tree->setEditTriggers(QAbstractItemView::AllEditTriggers);
|
|
||||||
tree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
|
||||||
tree->header()->setStretchLastSection(true);
|
|
||||||
tree->setMinimumHeight(300);
|
|
||||||
|
|
||||||
// Use a distinctive background for the whole row containing a QSpinBox or QLineEdit
|
|
||||||
QString nodeBgColor = palette().color(QPalette::AlternateBase).name(QColor::HexArgb);
|
|
||||||
tree->setStyleSheet(QString("QSpinBox{background-color:%1;border:none;} QLineEdit{background-color:%1;}").arg(nodeBgColor));
|
|
||||||
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
main_layout->setSpacing(0);
|
|
||||||
main_layout->addWidget(title_bar);
|
|
||||||
main_layout->addWidget(tree);
|
|
||||||
updateToolBar();
|
|
||||||
|
|
||||||
QObject::connect(filter_edit, &QLineEdit::textEdited, model, &SignalModel::setFilter);
|
|
||||||
QObject::connect(sparkline_range_slider, &QSlider::valueChanged, this, &SignalView::setSparklineRange);
|
|
||||||
QObject::connect(collapse_btn, &QPushButton::clicked, tree, &QTreeView::collapseAll);
|
|
||||||
QObject::connect(tree, &QAbstractItemView::clicked, this, &SignalView::rowClicked);
|
|
||||||
QObject::connect(tree, &QTreeView::viewportEntered, [this]() { emit highlight(nullptr); });
|
|
||||||
QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); });
|
|
||||||
QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged);
|
|
||||||
QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged);
|
|
||||||
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);
|
|
||||||
QObject::connect(tree->header(), &QHeaderView::sectionResized, [this](int logicalIndex, int oldSize, int newSize) {
|
|
||||||
if (logicalIndex == 1) {
|
|
||||||
value_column_width = newSize;
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setWhatsThis(tr(R"(
|
|
||||||
<b>Signal view</b><br />
|
|
||||||
<!-- TODO: add description here -->
|
|
||||||
)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::setMessage(const MessageId &id) {
|
|
||||||
max_value_width = 0;
|
|
||||||
filter_edit->clear();
|
|
||||||
model->setMessage(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::rowsChanged() {
|
|
||||||
for (int i = 0; i < model->rowCount(); ++i) {
|
|
||||||
auto index = model->index(i, 1);
|
|
||||||
if (!tree->indexWidget(index)) {
|
|
||||||
QWidget *w = new QWidget(this);
|
|
||||||
QHBoxLayout *h = new QHBoxLayout(w);
|
|
||||||
int v_margin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin);
|
|
||||||
int h_margin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin);
|
|
||||||
h->setContentsMargins(0, v_margin, -h_margin, v_margin);
|
|
||||||
h->setSpacing(style()->pixelMetric(QStyle::PM_ToolBarItemSpacing));
|
|
||||||
|
|
||||||
auto remove_btn = new ToolButton("x", tr("Remove signal"));
|
|
||||||
auto plot_btn = new ToolButton("graph-up", "");
|
|
||||||
plot_btn->setCheckable(true);
|
|
||||||
h->addWidget(plot_btn);
|
|
||||||
h->addWidget(remove_btn);
|
|
||||||
|
|
||||||
tree->setIndexWidget(index, w);
|
|
||||||
auto sig = model->getItem(index)->sig;
|
|
||||||
QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, sig)); });
|
|
||||||
QObject::connect(plot_btn, &QToolButton::clicked, [=](bool checked) {
|
|
||||||
emit showChart(model->msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateToolBar();
|
|
||||||
updateChartState();
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::rowClicked(const QModelIndex &index) {
|
|
||||||
auto item = model->getItem(index);
|
|
||||||
if (item->type == SignalModel::Item::Sig || item->type == SignalModel::Item::ExtraInfo) {
|
|
||||||
auto expand_index = model->index(index.row(), 0, index.parent());
|
|
||||||
tree->setExpanded(expand_index, !tree->isExpanded(expand_index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::selectSignal(const cabana::Signal *sig, bool expand) {
|
|
||||||
if (int row = model->signalRow(sig); row != -1) {
|
|
||||||
auto idx = model->index(row, 0);
|
|
||||||
if (expand) {
|
|
||||||
tree->setExpanded(idx, !tree->isExpanded(idx));
|
|
||||||
}
|
|
||||||
tree->scrollTo(idx, QAbstractItemView::PositionAtTop);
|
|
||||||
tree->setCurrentIndex(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::updateChartState() {
|
|
||||||
int i = 0;
|
|
||||||
for (auto item : model->root->children) {
|
|
||||||
bool chart_opened = charts->hasSignal(model->msg_id, item->sig);
|
|
||||||
auto buttons = tree->indexWidget(model->index(i, 1))->findChildren<QToolButton *>();
|
|
||||||
if (buttons.size() > 0) {
|
|
||||||
buttons[0]->setChecked(chart_opened);
|
|
||||||
buttons[0]->setToolTip(chart_opened ? tr("Close Plot") : tr("Show Plot\nSHIFT click to add to previous opened plot"));
|
|
||||||
}
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::signalHovered(const cabana::Signal *sig) {
|
|
||||||
auto &children = model->root->children;
|
|
||||||
for (int i = 0; i < children.size(); ++i) {
|
|
||||||
bool highlight = children[i]->sig == sig;
|
|
||||||
if (std::exchange(children[i]->highlight, highlight) != highlight) {
|
|
||||||
emit model->dataChanged(model->index(i, 0), model->index(i, 0), {Qt::DecorationRole});
|
|
||||||
emit model->dataChanged(model->index(i, 1), model->index(i, 1), {Qt::DisplayRole});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::updateToolBar() {
|
|
||||||
signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount()));
|
|
||||||
sparkline_label->setText(utils::formatSeconds(settings.sparkline_range));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::setSparklineRange(int value) {
|
|
||||||
settings.sparkline_range = value;
|
|
||||||
updateToolBar();
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
|
|
||||||
if (id.address == model->msg_id.address) {
|
|
||||||
selectSignal(sig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::handleSignalUpdated(const cabana::Signal *sig) {
|
|
||||||
if (int row = model->signalRow(sig); row != -1)
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::pair<QModelIndex, QModelIndex> SignalView::visibleSignalRange() {
|
|
||||||
auto topLevelIndex = [](QModelIndex index) {
|
|
||||||
while (index.isValid() && index.parent().isValid()) index = index.parent();
|
|
||||||
return index;
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto viewport_rect = tree->viewport()->rect();
|
|
||||||
QModelIndex first_visible = tree->indexAt(viewport_rect.topLeft());
|
|
||||||
if (first_visible.parent().isValid()) {
|
|
||||||
first_visible = topLevelIndex(first_visible);
|
|
||||||
first_visible = first_visible.siblingAtRow(first_visible.row() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
QModelIndex last_visible = topLevelIndex(tree->indexAt(viewport_rect.bottomRight()));
|
|
||||||
if (!last_visible.isValid()) {
|
|
||||||
last_visible = model->index(model->rowCount() - 1, 0);
|
|
||||||
}
|
|
||||||
return {first_visible, last_visible};
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::updateState(const std::set<MessageId> *msgs) {
|
|
||||||
const auto &last_msg = can->lastMessage(model->msg_id);
|
|
||||||
if (model->rowCount() == 0 || (msgs && !msgs->count(model->msg_id)) || last_msg.dat.size() == 0) return;
|
|
||||||
|
|
||||||
for (auto item : model->root->children) {
|
|
||||||
double value = 0;
|
|
||||||
if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) {
|
|
||||||
item->sig_val = QString::fromStdString(item->sig->formatValue(value));
|
|
||||||
max_value_width = std::max(max_value_width, fontMetrics().horizontalAdvance(item->sig_val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto [first_visible, last_visible] = visibleSignalRange();
|
|
||||||
if (first_visible.isValid() && last_visible.isValid()) {
|
|
||||||
const static int min_max_width = QFontMetrics(delegate->minmax_font).horizontalAdvance("-000.00") + 5;
|
|
||||||
int available_width = value_column_width - delegate->button_size.width();
|
|
||||||
int value_width = std::min<int>(max_value_width + min_max_width, available_width / 2);
|
|
||||||
QSize size(available_width - value_width,
|
|
||||||
delegate->button_size.height() - style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2);
|
|
||||||
|
|
||||||
auto [first, last] = can->eventsInRange(model->msg_id, std::make_pair(last_msg.ts -settings.sparkline_range, last_msg.ts));
|
|
||||||
std::vector<std::future<void>> futures;
|
|
||||||
for (int i = first_visible.row(); i <= last_visible.row(); ++i) {
|
|
||||||
auto item = model->getItem(model->index(i, 1));
|
|
||||||
futures.push_back(std::async(std::launch::async,
|
|
||||||
&Sparkline::update, &item->sparkline, item->sig, first, last, settings.sparkline_range, size));
|
|
||||||
}
|
|
||||||
for (auto &f : futures) f.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < model->rowCount(); ++i) {
|
|
||||||
emit model->dataChanged(model->index(i, 1), model->index(i, 1), {Qt::DisplayRole});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SignalView::resizeEvent(QResizeEvent* event) {
|
|
||||||
updateState();
|
|
||||||
QFrame::resizeEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValueDescriptionDlg
|
|
||||||
|
|
||||||
ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions, QWidget *parent) : QDialog(parent) {
|
|
||||||
QHBoxLayout *toolbar_layout = new QHBoxLayout();
|
|
||||||
QPushButton *add = new QPushButton(utils::icon("plus"), "");
|
|
||||||
QPushButton *remove = new QPushButton(utils::icon("dash"), "");
|
|
||||||
remove->setEnabled(false);
|
|
||||||
toolbar_layout->addWidget(add);
|
|
||||||
toolbar_layout->addWidget(remove);
|
|
||||||
toolbar_layout->addStretch(0);
|
|
||||||
|
|
||||||
table = new QTableWidget(descriptions.size(), 2, this);
|
|
||||||
table->setItemDelegate(new Delegate(this));
|
|
||||||
table->setHorizontalHeaderLabels({"Value", "Description"});
|
|
||||||
table->horizontalHeader()->setStretchLastSection(true);
|
|
||||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
||||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
||||||
table->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
|
|
||||||
table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
||||||
|
|
||||||
int row = 0;
|
|
||||||
for (auto &[val, desc] : descriptions) {
|
|
||||||
table->setItem(row, 0, new QTableWidgetItem(QString::number(val)));
|
|
||||||
table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(desc)));
|
|
||||||
++row;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
main_layout->addLayout(toolbar_layout);
|
|
||||||
main_layout->addWidget(table);
|
|
||||||
main_layout->addWidget(btn_box);
|
|
||||||
setMinimumWidth(500);
|
|
||||||
|
|
||||||
QObject::connect(btn_box, &QDialogButtonBox::accepted, this, &ValueDescriptionDlg::save);
|
|
||||||
QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
|
||||||
QObject::connect(add, &QPushButton::clicked, [this]() {
|
|
||||||
table->setRowCount(table->rowCount() + 1);
|
|
||||||
table->setItem(table->rowCount() - 1, 0, new QTableWidgetItem);
|
|
||||||
table->setItem(table->rowCount() - 1, 1, new QTableWidgetItem);
|
|
||||||
});
|
|
||||||
QObject::connect(remove, &QPushButton::clicked, [this]() { table->removeRow(table->currentRow()); });
|
|
||||||
QObject::connect(table, &QTableWidget::itemSelectionChanged, [=]() {
|
|
||||||
remove->setEnabled(table->currentRow() != -1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ValueDescriptionDlg::save() {
|
|
||||||
for (int i = 0; i < table->rowCount(); ++i) {
|
|
||||||
QString val = table->item(i, 0)->text().trimmed();
|
|
||||||
QString desc = table->item(i, 1)->text().trimmed();
|
|
||||||
if (!val.isEmpty() && !desc.isEmpty()) {
|
|
||||||
val_desc.push_back({val.toDouble(), desc.toStdString()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QDialog::accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
QWidget *ValueDescriptionDlg::Delegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
|
||||||
QLineEdit *edit = new QLineEdit(parent);
|
|
||||||
edit->setFrame(false);
|
|
||||||
if (index.column() == 0) {
|
|
||||||
edit->setValidator(new DoubleValidator(parent));
|
|
||||||
}
|
|
||||||
return edit;
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <set>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QAbstractItemModel>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QSlider>
|
|
||||||
#include <QStyledItemDelegate>
|
|
||||||
#include <QTableWidget>
|
|
||||||
#include <QTreeView>
|
|
||||||
|
|
||||||
#include "tools/cabana/chart/chartswidget.h"
|
|
||||||
#include "tools/cabana/chart/sparkline.h"
|
|
||||||
|
|
||||||
class SignalModel : public QAbstractItemModel {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
struct Item {
|
|
||||||
enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc };
|
|
||||||
~Item() { for (auto c : children) delete c; }
|
|
||||||
inline int row() {
|
|
||||||
auto it = std::find(parent->children.begin(), parent->children.end(), this);
|
|
||||||
return it != parent->children.end() ? std::distance(parent->children.begin(), it) : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Type type = Type::Root;
|
|
||||||
Item *parent = nullptr;
|
|
||||||
std::vector<Item *> children;
|
|
||||||
|
|
||||||
const cabana::Signal *sig = nullptr;
|
|
||||||
QString title;
|
|
||||||
bool highlight = false;
|
|
||||||
QString sig_val = "-";
|
|
||||||
Sparkline sparkline;
|
|
||||||
};
|
|
||||||
|
|
||||||
SignalModel(QObject *parent);
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 2; }
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
QModelIndex parent(const QModelIndex &index) const override;
|
|
||||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
|
||||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
|
||||||
void setMessage(const MessageId &id);
|
|
||||||
void setFilter(const QString &txt);
|
|
||||||
bool saveSignal(const cabana::Signal *origin_s, cabana::Signal &s);
|
|
||||||
Item *getItem(const QModelIndex &index) const;
|
|
||||||
int signalRow(const cabana::Signal *sig) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig);
|
|
||||||
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
|
|
||||||
void handleSignalUpdated(const cabana::Signal *sig);
|
|
||||||
void handleSignalRemoved(const cabana::Signal *sig);
|
|
||||||
void handleMsgChanged(MessageId id);
|
|
||||||
void refresh();
|
|
||||||
|
|
||||||
MessageId msg_id;
|
|
||||||
QString filter_str;
|
|
||||||
std::unique_ptr<Item> root;
|
|
||||||
friend class SignalView;
|
|
||||||
friend class SignalItemDelegate;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ValueDescriptionDlg : public QDialog {
|
|
||||||
public:
|
|
||||||
ValueDescriptionDlg(const ValueDescription &descriptions, QWidget *parent);
|
|
||||||
ValueDescription val_desc;
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct Delegate : public QStyledItemDelegate {
|
|
||||||
Delegate(QWidget *parent) : QStyledItemDelegate(parent) {}
|
|
||||||
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
|
||||||
};
|
|
||||||
|
|
||||||
void save();
|
|
||||||
QTableWidget *table;
|
|
||||||
};
|
|
||||||
|
|
||||||
class SignalItemDelegate : public QStyledItemDelegate {
|
|
||||||
public:
|
|
||||||
SignalItemDelegate(QObject *parent);
|
|
||||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
|
||||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
|
||||||
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
|
||||||
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
|
||||||
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
|
|
||||||
|
|
||||||
QValidator *name_validator, *double_validator, *node_validator;
|
|
||||||
QFont label_font, minmax_font;
|
|
||||||
const int color_label_width = 18;
|
|
||||||
mutable QSize button_size;
|
|
||||||
};
|
|
||||||
|
|
||||||
class SignalView : public QFrame {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
SignalView(ChartsWidget *charts, QWidget *parent);
|
|
||||||
void setMessage(const MessageId &id);
|
|
||||||
void signalHovered(const cabana::Signal *sig);
|
|
||||||
void updateChartState();
|
|
||||||
void selectSignal(const cabana::Signal *sig, bool expand = false);
|
|
||||||
void rowClicked(const QModelIndex &index);
|
|
||||||
SignalModel *model = nullptr;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void highlight(const cabana::Signal *sig);
|
|
||||||
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void rowsChanged();
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
void updateToolBar();
|
|
||||||
void setSparklineRange(int value);
|
|
||||||
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
|
|
||||||
void handleSignalUpdated(const cabana::Signal *sig);
|
|
||||||
void updateState(const std::set<MessageId> *msgs = nullptr);
|
|
||||||
std::pair<QModelIndex, QModelIndex> visibleSignalRange();
|
|
||||||
|
|
||||||
struct TreeView : public QTreeView {
|
|
||||||
TreeView(QWidget *parent) : QTreeView(parent) {}
|
|
||||||
void rowsInserted(const QModelIndex &parent, int start, int end) override {
|
|
||||||
((SignalView *)parentWidget())->rowsChanged();
|
|
||||||
// update widget geometries in QTreeView::rowsInserted
|
|
||||||
QTreeView::rowsInserted(parent, start, end);
|
|
||||||
}
|
|
||||||
void setModel(QAbstractItemModel *m) override {
|
|
||||||
QTreeView::setModel(m);
|
|
||||||
// Bypass the slow call to QTreeView::dataChanged.
|
|
||||||
QObject::disconnect(m, &QAbstractItemModel::dataChanged, this, nullptr);
|
|
||||||
QObject::connect(m, &QAbstractItemModel::dataChanged, this,
|
|
||||||
[this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); });
|
|
||||||
}
|
|
||||||
void leaveEvent(QEvent *event) override {
|
|
||||||
emit static_cast<SignalView *>(parentWidget())->highlight(nullptr);
|
|
||||||
QTreeView::leaveEvent(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
int max_value_width = 0;
|
|
||||||
int value_column_width = 0;
|
|
||||||
TreeView *tree;
|
|
||||||
QLabel *sparkline_label;
|
|
||||||
QSlider *sparkline_range_slider;
|
|
||||||
QLineEdit *filter_edit;
|
|
||||||
ChartsWidget *charts;
|
|
||||||
QLabel *signal_count_lb;
|
|
||||||
SignalItemDelegate *delegate;
|
|
||||||
};
|
|
||||||
@@ -1,26 +1,51 @@
|
|||||||
#include "tools/cabana/streams/abstractstream.h"
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
#include "tools/cabana/dbc/dbcqt.h"
|
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include "common/timing.h"
|
#include "common/timing.h"
|
||||||
#include "tools/cabana/settings.h"
|
#include "tools/cabana/settings.h"
|
||||||
|
|
||||||
static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024; // 6MB
|
static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024;
|
||||||
|
|
||||||
AbstractStream *can = nullptr;
|
AbstractStream *can = nullptr;
|
||||||
|
|
||||||
AbstractStream::AbstractStream(QObject *parent) : QObject(parent) {
|
AbstractStream::AbstractStream() {
|
||||||
assert(parent != nullptr);
|
|
||||||
event_buffer_ = std::make_unique<MonotonicBuffer>(EVENT_NEXT_BUFFER_SIZE);
|
event_buffer_ = std::make_unique<MonotonicBuffer>(EVENT_NEXT_BUFFER_SIZE);
|
||||||
|
|
||||||
QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection);
|
|
||||||
QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo);
|
connections_.push_back(seekedTo.connect([this](double sec) { updateLastMsgsTo(sec); }));
|
||||||
QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; });
|
connections_.push_back(seeking.connect([this](double sec) { current_sec_ = sec; }));
|
||||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &AbstractStream::updateMasks);
|
connections_.push_back(dbc()->fileChanged.connect([this]() { updateMasks(); }));
|
||||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::maskUpdated, this, &AbstractStream::updateMasks);
|
connections_.push_back(dbc()->maskUpdated.connect([this]() { updateMasks(); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AbstractStream::postToMainThread(std::function<void()> fn) {
|
||||||
|
utils::runOnMainThread([alive = std::weak_ptr<bool>(alive_), fn = std::move(fn)]() {
|
||||||
|
if (!alive.expired()) fn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AbstractStream::postToMainThreadAndWait(std::function<void()> fn) {
|
||||||
|
assert(!utils::isMainThread());
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
if (exiting_) return;
|
||||||
|
auto done = std::make_shared<bool>(false);
|
||||||
|
postToMainThread([this, alive = std::weak_ptr<bool>(alive_), done, fn = std::move(fn)]() {
|
||||||
|
fn();
|
||||||
|
if (alive.expired()) return;
|
||||||
|
std::lock_guard lk(mutex_);
|
||||||
|
*done = true;
|
||||||
|
wait_cv_.notify_all();
|
||||||
|
});
|
||||||
|
wait_cv_.wait(lock, [&]() { return *done || exiting_; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void AbstractStream::cancelWaits() {
|
||||||
|
std::lock_guard lk(mutex_);
|
||||||
|
exiting_ = true;
|
||||||
|
wait_cv_.notify_all();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractStream::updateMasks() {
|
void AbstractStream::updateMasks() {
|
||||||
@@ -34,7 +59,7 @@ void AbstractStream::updateMasks() {
|
|||||||
masks_[{.source = (uint8_t)s, .address = address}] = m.mask;
|
masks_[{.source = (uint8_t)s, .address = address}] = m.mask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// clear bit change counts
|
|
||||||
for (auto &[id, m] : messages_) {
|
for (auto &[id, m] : messages_) {
|
||||||
auto &mask = masks_[id];
|
auto &mask = masks_[id];
|
||||||
const int size = std::min(mask.size(), m.last_changes.size());
|
const int size = std::min(mask.size(), m.last_changes.size());
|
||||||
@@ -97,9 +122,8 @@ void AbstractStream::updateLastMessages() {
|
|||||||
|
|
||||||
if (sources.size() != prev_src_size) {
|
if (sources.size() != prev_src_size) {
|
||||||
updateMasks();
|
updateMasks();
|
||||||
emit sourcesUpdated(sources);
|
|
||||||
}
|
}
|
||||||
emit msgsReceived(&msgs, prev_msg_size != last_msgs.size());
|
msgsReceived(&msgs, prev_msg_size != last_msgs.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractStream::setTimeRange(const std::optional<std::pair<double, double>> &range) {
|
void AbstractStream::setTimeRange(const std::optional<std::pair<double, double>> &range) {
|
||||||
@@ -107,7 +131,7 @@ void AbstractStream::setTimeRange(const std::optional<std::pair<double, double>>
|
|||||||
if (time_range_ && (current_sec_ < time_range_->first || current_sec_ >= time_range_->second)) {
|
if (time_range_ && (current_sec_ < time_range_->first || current_sec_ >= time_range_->second)) {
|
||||||
seekTo(time_range_->first);
|
seekTo(time_range_->first);
|
||||||
}
|
}
|
||||||
emit timeRangeChanged(time_range_);
|
timeRangeChanged(time_range_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size) {
|
void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size) {
|
||||||
@@ -132,7 +156,7 @@ bool AbstractStream::isMessageActive(const MessageId &id) const {
|
|||||||
if (id.source == INVALID_SOURCE) {
|
if (id.source == INVALID_SOURCE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Check if the message is active based on time difference and frequency
|
|
||||||
const auto &m = lastMessage(id);
|
const auto &m = lastMessage(id);
|
||||||
float delta = currentSec() - m.ts;
|
float delta = currentSec() - m.ts;
|
||||||
|
|
||||||
@@ -140,7 +164,7 @@ bool AbstractStream::isMessageActive(const MessageId &id) const {
|
|||||||
return delta < 1.5;
|
return delta < 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
return delta < (5.0 / m.freq) + (1.0 / settings.fps);
|
return delta < (5.0 / m.freq) + (1.0 / STREAM_UPDATE_FPS);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractStream::updateLastMsgsTo(double sec) {
|
void AbstractStream::updateLastMsgsTo(double sec) {
|
||||||
@@ -154,7 +178,7 @@ void AbstractStream::updateLastMsgsTo(double sec) {
|
|||||||
if (it != ev.begin()) {
|
if (it != ev.begin()) {
|
||||||
auto &m = msgs[id];
|
auto &m = msgs[id];
|
||||||
double freq = 0;
|
double freq = 0;
|
||||||
// Keep suppressed bits.
|
|
||||||
if (auto old_m = messages_.find(id); old_m != messages_.end()) {
|
if (auto old_m = messages_.find(id); old_m != messages_.end()) {
|
||||||
freq = old_m->second.freq;
|
freq = old_m->second.freq;
|
||||||
m.last_changes.reserve(old_m->second.last_changes.size());
|
m.last_changes.reserve(old_m->second.last_changes.size());
|
||||||
@@ -175,16 +199,16 @@ void AbstractStream::updateLastMsgsTo(double sec) {
|
|||||||
std::any_of(messages_.cbegin(), messages_.cend(),
|
std::any_of(messages_.cbegin(), messages_.cend(),
|
||||||
[this](const auto &m) { return !last_msgs.count(m.first); });
|
[this](const auto &m) { return !last_msgs.count(m.first); });
|
||||||
last_msgs = messages_;
|
last_msgs = messages_;
|
||||||
emit msgsReceived(nullptr, id_changed);
|
msgsReceived(nullptr, id_changed);
|
||||||
|
|
||||||
std::lock_guard lk(mutex_);
|
std::lock_guard lk(mutex_);
|
||||||
seek_finished_ = true;
|
seek_finished_ = true;
|
||||||
seek_finished_cv_.notify_one();
|
wait_cv_.notify_all();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractStream::waitForSeekFinshed() {
|
void AbstractStream::waitForSeekFinshed() {
|
||||||
std::unique_lock lock(mutex_);
|
std::unique_lock lock(mutex_);
|
||||||
seek_finished_cv_.wait(lock, [this]() { return seek_finished_; });
|
wait_cv_.wait(lock, [this]() { return seek_finished_ || exiting_; });
|
||||||
seek_finished_ = false;
|
seek_finished_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,11 +227,15 @@ void AbstractStream::mergeEvents(const std::vector<const CanEvent *> &events) {
|
|||||||
static MessageEventsMap msg_events;
|
static MessageEventsMap msg_events;
|
||||||
std::for_each(msg_events.begin(), msg_events.end(), [](auto &e) { e.second.clear(); });
|
std::for_each(msg_events.begin(), msg_events.end(), [](auto &e) { e.second.clear(); });
|
||||||
|
|
||||||
// Group events by message ID
|
|
||||||
for (auto e : events) {
|
for (auto e : events) {
|
||||||
msg_events[{.source = e->src, .address = e->address}].push_back(e);
|
msg_events[{.source = e->src, .address = e->address}].push_back(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertEvents(events, msg_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AbstractStream::insertEvents(const std::vector<const CanEvent *> &events, const MessageEventsMap &msg_events) {
|
||||||
if (!events.empty()) {
|
if (!events.empty()) {
|
||||||
for (const auto &[id, new_e] : msg_events) {
|
for (const auto &[id, new_e] : msg_events) {
|
||||||
if (!new_e.empty()) {
|
if (!new_e.empty()) {
|
||||||
@@ -218,16 +246,16 @@ void AbstractStream::mergeEvents(const std::vector<const CanEvent *> &events) {
|
|||||||
}
|
}
|
||||||
auto pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), events.front()->mono_time, CompareCanEvent());
|
auto pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), events.front()->mono_time, CompareCanEvent());
|
||||||
all_events_.insert(pos, events.cbegin(), events.cend());
|
all_events_.insert(pos, events.cbegin(), events.cend());
|
||||||
emit eventsMerged(msg_events);
|
eventsMerged(msg_events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<CanEventIter, CanEventIter> AbstractStream::eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> time_range) const {
|
std::pair<CanEventIter, CanEventIter> AbstractStream::eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> time_range) const {
|
||||||
const auto &events = can->events(id);
|
const auto &events = this->events(id);
|
||||||
if (!time_range) return {events.begin(), events.end()};
|
if (!time_range) return {events.begin(), events.end()};
|
||||||
|
|
||||||
auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent());
|
auto first = std::lower_bound(events.begin(), events.end(), toMonoTime(time_range->first), CompareCanEvent());
|
||||||
auto last = std::upper_bound(first, events.end(), can->toMonoTime(time_range->second), CompareCanEvent());
|
auto last = std::upper_bound(first, events.end(), toMonoTime(time_range->second), CompareCanEvent());
|
||||||
return {first, last};
|
return {first, last};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +276,7 @@ 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);
|
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
|
|
||||||
double calc_freq(const MessageId &msg_id, double current_sec) {
|
double calc_freq(const MessageId &msg_id, double current_sec) {
|
||||||
auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec));
|
auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec));
|
||||||
int count = std::distance(first, last);
|
int count = std::distance(first, last);
|
||||||
@@ -258,7 +286,7 @@ double calc_freq(const MessageId &msg_id, double current_sec) {
|
|||||||
return duration > std::numeric_limits<double>::epsilon() ? (count - 1) / duration : 0.0;
|
return duration > std::numeric_limits<double>::epsilon() ? (count - 1) / duration : 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
}
|
||||||
|
|
||||||
void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const int size, double current_sec,
|
void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const int size, double current_sec,
|
||||||
double playback_speed, const std::vector<uint8_t> &mask, double in_freq) {
|
double playback_speed, const std::vector<uint8_t> &mask, double in_freq) {
|
||||||
@@ -291,21 +319,21 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
|
|||||||
const uint8_t cur = can_data[i] & mask_byte;
|
const uint8_t cur = can_data[i] & mask_byte;
|
||||||
if (last != cur) {
|
if (last != cur) {
|
||||||
const int delta = cur - last;
|
const int delta = cur - last;
|
||||||
// Keep track if signal is changing randomly, or mostly moving in the same direction
|
|
||||||
last_change.same_delta_counter += std::signbit(delta) == std::signbit(last_change.delta) ? 1 : -4;
|
last_change.same_delta_counter += std::signbit(delta) == std::signbit(last_change.delta) ? 1 : -4;
|
||||||
last_change.same_delta_counter = std::clamp(last_change.same_delta_counter, 0, 16);
|
last_change.same_delta_counter = std::clamp(last_change.same_delta_counter, 0, 16);
|
||||||
|
|
||||||
const double delta_t = ts - last_change.ts;
|
const double delta_t = ts - last_change.ts;
|
||||||
// Mostly moves in the same direction, color based on delta up/down
|
|
||||||
if (delta_t * freq > periodic_threshold || last_change.same_delta_counter > 8) {
|
if (delta_t * freq > periodic_threshold || last_change.same_delta_counter > 8) {
|
||||||
// Last change was while ago, choose color based on delta up or down
|
|
||||||
colors[i] = getColor(cur > last ? CYAN : RED);
|
colors[i] = getColor(cur > last ? CYAN : RED);
|
||||||
} else {
|
} else {
|
||||||
// Periodic changes
|
|
||||||
colors[i] = blend(colors[i], getColor(GREYISH_BLUE));
|
colors[i] = blend(colors[i], getColor(GREYISH_BLUE));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track bit level changes
|
|
||||||
auto &row_bit_flips = bit_flip_counts[i];
|
auto &row_bit_flips = bit_flip_counts[i];
|
||||||
const uint8_t diff = (cur ^ last);
|
const uint8_t diff = (cur ^ last);
|
||||||
for (int bit = 0; bit < 8; bit++) {
|
for (int bit = 0; bit < 8; bit++) {
|
||||||
@@ -317,7 +345,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
|
|||||||
last_change.ts = ts;
|
last_change.ts = ts;
|
||||||
last_change.delta = delta;
|
last_change.delta = delta;
|
||||||
} else {
|
} else {
|
||||||
// Fade out
|
|
||||||
colors[i].setAlphaF(std::max(0.0f, colors[i].alphaF() - alpha_delta));
|
colors[i].setAlphaF(std::max(0.0f, colors[i].alphaF() - alpha_delta));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -14,16 +15,15 @@
|
|||||||
|
|
||||||
#include "cereal/messaging/messaging.h"
|
#include "cereal/messaging/messaging.h"
|
||||||
#include "tools/cabana/core/can_data.h"
|
#include "tools/cabana/core/can_data.h"
|
||||||
|
#include "tools/cabana/core/observable.h"
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
#include "tools/cabana/dbc/dbcmanager.h"
|
||||||
#include "tools/cabana/utils/util.h"
|
#include "tools/cabana/utils/util.h"
|
||||||
#include "tools/replay/util.h"
|
#include "tools/replay/util.h"
|
||||||
|
|
||||||
class AbstractStream : public QObject {
|
class AbstractStream {
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AbstractStream(QObject *parent);
|
AbstractStream();
|
||||||
virtual ~AbstractStream() {}
|
virtual ~AbstractStream() = default;
|
||||||
virtual void start() = 0;
|
virtual void start() = 0;
|
||||||
virtual bool liveStreaming() const { return true; }
|
virtual bool liveStreaming() const { return true; }
|
||||||
virtual void seekTo(double ts) {}
|
virtual void seekTo(double ts) {}
|
||||||
@@ -56,22 +56,25 @@ public:
|
|||||||
void clearSuppressed();
|
void clearSuppressed();
|
||||||
void suppressDefinedSignals(bool suppress);
|
void suppressDefinedSignals(bool suppress);
|
||||||
|
|
||||||
signals:
|
|
||||||
void paused();
|
|
||||||
void resume();
|
|
||||||
void seeking(double sec);
|
|
||||||
void seekedTo(double sec);
|
|
||||||
void timeRangeChanged(const std::optional<std::pair<double, double>> &range);
|
|
||||||
void eventsMerged(const MessageEventsMap &events_map);
|
|
||||||
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
|
|
||||||
void sourcesUpdated(const SourceSet &s);
|
|
||||||
void privateUpdateLastMsgsSignal();
|
|
||||||
|
|
||||||
public:
|
Observable<> paused;
|
||||||
|
Observable<> resume;
|
||||||
|
Observable<double> seeking;
|
||||||
|
Observable<double> seekedTo;
|
||||||
|
Observable<const std::optional<std::pair<double, double>> &> timeRangeChanged;
|
||||||
|
Observable<const MessageEventsMap &> eventsMerged;
|
||||||
|
Observable<const std::set<MessageId> *, bool> msgsReceived;
|
||||||
|
Observable<const std::string &> error;
|
||||||
|
|
||||||
SourceSet sources;
|
SourceSet sources;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void postToMainThread(std::function<void()> fn);
|
||||||
|
void postToMainThreadAndWait(std::function<void()> fn);
|
||||||
|
void cancelWaits();
|
||||||
|
void requestUpdateLastMessages() { postToMainThread([this]() { updateLastMessages(); }); }
|
||||||
void mergeEvents(const std::vector<const CanEvent *> &events);
|
void mergeEvents(const std::vector<const CanEvent *> &events);
|
||||||
|
void insertEvents(const std::vector<const CanEvent *> &events, const MessageEventsMap &msg_events);
|
||||||
const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c);
|
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 updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size);
|
||||||
void waitForSeekFinshed();
|
void waitForSeekFinshed();
|
||||||
@@ -87,33 +90,24 @@ private:
|
|||||||
MessageEventsMap events_;
|
MessageEventsMap events_;
|
||||||
std::unordered_map<MessageId, CanData> last_msgs;
|
std::unordered_map<MessageId, CanData> last_msgs;
|
||||||
std::unique_ptr<MonotonicBuffer> event_buffer_;
|
std::unique_ptr<MonotonicBuffer> event_buffer_;
|
||||||
|
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
||||||
|
Connections connections_;
|
||||||
|
|
||||||
|
|
||||||
// Members accessed in multiple threads. (mutex protected)
|
|
||||||
std::mutex mutex_;
|
std::mutex mutex_;
|
||||||
std::condition_variable seek_finished_cv_;
|
std::condition_variable wait_cv_;
|
||||||
bool seek_finished_ = false;
|
bool seek_finished_ = false;
|
||||||
|
bool exiting_ = false;
|
||||||
std::set<MessageId> new_msgs_;
|
std::set<MessageId> new_msgs_;
|
||||||
std::unordered_map<MessageId, CanData> messages_;
|
std::unordered_map<MessageId, CanData> messages_;
|
||||||
std::unordered_map<MessageId, std::vector<uint8_t>> masks_;
|
std::unordered_map<MessageId, std::vector<uint8_t>> masks_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AbstractOpenStreamWidget : public QWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {}
|
|
||||||
virtual AbstractStream *open() = 0;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void enableOpenButton(bool);
|
|
||||||
};
|
|
||||||
|
|
||||||
class DummyStream : public AbstractStream {
|
class DummyStream : public AbstractStream {
|
||||||
Q_OBJECT
|
|
||||||
public:
|
public:
|
||||||
DummyStream(QObject *parent) : AbstractStream(parent) {}
|
|
||||||
std::string routeName() const override { return "No Stream"; }
|
std::string routeName() const override { return "No Stream"; }
|
||||||
void start() override {}
|
void start() override {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// A global pointer referring to the unique AbstractStream object
|
|
||||||
extern AbstractStream *can;
|
extern AbstractStream *can;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "tools/cabana/streams/devicestream.h"
|
#include "tools/cabana/streams/devicestream.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
@@ -9,22 +10,17 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
|
||||||
#include "cereal/services.h"
|
#include "cereal/services.h"
|
||||||
|
|
||||||
#include <QButtonGroup>
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QRadioButton>
|
|
||||||
|
|
||||||
#include "tools/cabana/utils/util.h"
|
#include "tools/cabana/utils/util.h"
|
||||||
|
|
||||||
// DeviceStream
|
|
||||||
|
|
||||||
DeviceStream::DeviceStream(QObject *parent, Mode mode, QString address)
|
|
||||||
: mode_(mode), address_(address.isEmpty() ? "127.0.0.1" : address), LiveStream(parent) {
|
DeviceStream::DeviceStream(Mode mode, std::string address)
|
||||||
|
: mode_(mode), address_(address.empty() ? "127.0.0.1" : std::move(address)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceStream::~DeviceStream() {
|
DeviceStream::~DeviceStream() {
|
||||||
@@ -43,7 +39,7 @@ void DeviceStream::stopBridge() {
|
|||||||
bridge_pid = -1;
|
bridge_pid = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
usleep(100000); // 100ms, up to ~3s
|
usleep(100000);
|
||||||
}
|
}
|
||||||
::kill(bridge_pid, SIGKILL);
|
::kill(bridge_pid, SIGKILL);
|
||||||
::waitpid(bridge_pid, nullptr, 0);
|
::waitpid(bridge_pid, nullptr, 0);
|
||||||
@@ -53,17 +49,14 @@ void DeviceStream::stopBridge() {
|
|||||||
void DeviceStream::start() {
|
void DeviceStream::start() {
|
||||||
if (mode_ == Mode::Bridge) {
|
if (mode_ == Mode::Bridge) {
|
||||||
stopBridge();
|
stopBridge();
|
||||||
const std::string path = (std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()) /
|
const std::string path = (executableDir() / "../../cereal/messaging/bridge").lexically_normal().string();
|
||||||
"../../cereal/messaging/bridge").lexically_normal().string();
|
|
||||||
const std::string addr = address_.toStdString();
|
|
||||||
const char *can_filter = "/\"can/\"";
|
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};
|
int err_pipe[2] = {-1, -1};
|
||||||
if (::pipe(err_pipe) != 0) {
|
if (::pipe(err_pipe) != 0) {
|
||||||
QMessageBox::warning(nullptr, tr("Error"),
|
error(std::string("Failed to start bridge: ") + strerror(errno));
|
||||||
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno))));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +64,7 @@ void DeviceStream::start() {
|
|||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
::close(err_pipe[0]);
|
::close(err_pipe[0]);
|
||||||
::fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC);
|
::fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC);
|
||||||
execl(path.c_str(), path.c_str(), addr.c_str(), can_filter, static_cast<char *>(nullptr));
|
execl(path.c_str(), path.c_str(), address_.c_str(), can_filter, static_cast<char *>(nullptr));
|
||||||
const int err = errno;
|
const int err = errno;
|
||||||
(void)!::write(err_pipe[1], &err, sizeof(err));
|
(void)!::write(err_pipe[1], &err, sizeof(err));
|
||||||
_exit(127);
|
_exit(127);
|
||||||
@@ -80,8 +73,7 @@ void DeviceStream::start() {
|
|||||||
::close(err_pipe[1]);
|
::close(err_pipe[1]);
|
||||||
if (pid < 0) {
|
if (pid < 0) {
|
||||||
::close(err_pipe[0]);
|
::close(err_pipe[0]);
|
||||||
QMessageBox::warning(nullptr, tr("Error"),
|
error(std::string("Failed to start bridge: ") + strerror(errno));
|
||||||
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno))));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +81,10 @@ void DeviceStream::start() {
|
|||||||
const ssize_t n = ::read(err_pipe[0], &exec_errno, sizeof(exec_errno));
|
const ssize_t n = ::read(err_pipe[0], &exec_errno, sizeof(exec_errno));
|
||||||
::close(err_pipe[0]);
|
::close(err_pipe[0]);
|
||||||
if (n == static_cast<ssize_t>(sizeof(exec_errno))) {
|
if (n == static_cast<ssize_t>(sizeof(exec_errno))) {
|
||||||
// Child failed to exec; reap and surface the error.
|
|
||||||
int status = 0;
|
int status = 0;
|
||||||
::waitpid(pid, &status, 0);
|
::waitpid(pid, &status, 0);
|
||||||
QMessageBox::warning(nullptr, tr("Error"),
|
error(std::string("Failed to start bridge: ") + strerror(exec_errno));
|
||||||
tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno))));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,16 +95,13 @@ void DeviceStream::start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DeviceStream::streamThread() {
|
void DeviceStream::streamThread() {
|
||||||
// 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");
|
mode_ == Mode::Zmq ? setenv("ZMQ", "1", 1) : unsetenv("ZMQ");
|
||||||
const std::string address = mode_ == Mode::Zmq ? address_.toStdString() : "127.0.0.1";
|
const std::string socket_address = mode_ == Mode::Zmq ? address_ : "127.0.0.1";
|
||||||
|
|
||||||
std::unique_ptr<Context> context(Context::create());
|
std::unique_ptr<Context> context(Context::create());
|
||||||
std::unique_ptr<SubSocket> sock(SubSocket::create(context.get(), "can", address, false, true, services.at("can").queue_size));
|
std::unique_ptr<SubSocket> sock(SubSocket::create(context.get(), "can", socket_address, false, true, services.at("can").queue_size));
|
||||||
assert(sock != NULL);
|
assert(sock != NULL);
|
||||||
// run as fast as messages come in
|
|
||||||
while (!exit_) {
|
while (!exit_) {
|
||||||
std::unique_ptr<Message> msg(sock->receive(true));
|
std::unique_ptr<Message> msg(sock->receive(true));
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
@@ -123,36 +111,3 @@ void DeviceStream::streamThread() {
|
|||||||
handleEvent(kj::ArrayPtr<capnp::word>((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word)));
|
handleEvent(kj::ArrayPtr<capnp::word>((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenDeviceWidget
|
|
||||||
|
|
||||||
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"));
|
|
||||||
ip_address->setValidator(new IpAddressValidator(this));
|
|
||||||
|
|
||||||
group = new QButtonGroup(this);
|
|
||||||
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) {
|
|
||||||
if (checked) ip_address->setEnabled(button != msgq);
|
|
||||||
});
|
|
||||||
zmq->setChecked(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
AbstractStream *OpenDeviceWidget::open() {
|
|
||||||
auto mode = static_cast<DeviceStream::Mode>(group->checkedId());
|
|
||||||
return new DeviceStream(qApp, mode, mode == DeviceStream::Mode::Msgq ? "" : ip_address->text());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,28 +2,17 @@
|
|||||||
|
|
||||||
#include "tools/cabana/streams/livestream.h"
|
#include "tools/cabana/streams/livestream.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
#include <sys/types.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 {
|
class DeviceStream : public LiveStream {
|
||||||
Q_OBJECT
|
|
||||||
public:
|
public:
|
||||||
enum class Mode { Msgq, Zmq, Bridge };
|
enum class Mode { Msgq, Zmq, Bridge };
|
||||||
|
|
||||||
DeviceStream(QObject *parent, Mode mode = Mode::Msgq, QString address = {});
|
DeviceStream(Mode mode = Mode::Msgq, std::string address = {});
|
||||||
~DeviceStream();
|
~DeviceStream();
|
||||||
inline std::string routeName() const override {
|
inline std::string routeName() const override {
|
||||||
return "Live Streaming From " + address_.toStdString();
|
return "Live Streaming From " + address_;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -32,17 +21,5 @@ protected:
|
|||||||
void stopBridge();
|
void stopBridge();
|
||||||
pid_t bridge_pid = -1;
|
pid_t bridge_pid = -1;
|
||||||
const Mode mode_;
|
const Mode mode_;
|
||||||
const QString address_;
|
const std::string address_;
|
||||||
};
|
|
||||||
|
|
||||||
class OpenDeviceWidget : public AbstractOpenStreamWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
OpenDeviceWidget(QWidget *parent = nullptr);
|
|
||||||
AbstractStream *open() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QLineEdit *ip_address;
|
|
||||||
QButtonGroup *group;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "common/timing.h"
|
#include "common/timing.h"
|
||||||
#include "common/util.h"
|
#include "common/util.h"
|
||||||
|
#include "tools/cabana/settings.h"
|
||||||
|
|
||||||
struct LiveStream::Logger {
|
struct LiveStream::Logger {
|
||||||
Logger() : start_ts(seconds_since_epoch()), segment_num(-1) {}
|
Logger() : start_ts(seconds_since_epoch()), segment_num(-1) {}
|
||||||
@@ -21,12 +22,9 @@ struct LiveStream::Logger {
|
|||||||
localtime_r(&start_time, &local_time);
|
localtime_r(&start_time, &local_time);
|
||||||
std::ostringstream date;
|
std::ostringstream date;
|
||||||
date << std::put_time(&local_time, "%Y-%m-%d--%H-%M-%S");
|
date << std::put_time(&local_time, "%Y-%m-%d--%H-%M-%S");
|
||||||
QString dir = QString("%1/%2--%3")
|
std::string dir = settings.log_path + "/" + date.str() + "--" + std::to_string(n);
|
||||||
.arg(QString::fromStdString(settings.log_path))
|
util::create_directories(dir, 0755);
|
||||||
.arg(QString::fromStdString(date.str()))
|
fs.reset(new std::ofstream(dir + "/rlog", std::ios::binary | std::ios::out));
|
||||||
.arg(n);
|
|
||||||
util::create_directories(dir.toStdString(), 0755);
|
|
||||||
fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto bytes = data.asBytes();
|
auto bytes = data.asBytes();
|
||||||
@@ -38,7 +36,7 @@ struct LiveStream::Logger {
|
|||||||
uint64_t start_ts;
|
uint64_t start_ts;
|
||||||
};
|
};
|
||||||
|
|
||||||
LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) {
|
LiveStream::LiveStream() {
|
||||||
if (settings.log_livestream) {
|
if (settings.log_livestream) {
|
||||||
logger = std::make_unique<Logger>();
|
logger = std::make_unique<Logger>();
|
||||||
}
|
}
|
||||||
@@ -50,7 +48,6 @@ LiveStream::~LiveStream() {
|
|||||||
|
|
||||||
void LiveStream::start() {
|
void LiveStream::start() {
|
||||||
begin_date_time = std::chrono::system_clock::now();
|
begin_date_time = std::chrono::system_clock::now();
|
||||||
fps_ = settings.fps;
|
|
||||||
exit_ = false;
|
exit_ = false;
|
||||||
stream_thread = std::thread(&LiveStream::streamThread, this);
|
stream_thread = std::thread(&LiveStream::streamThread, this);
|
||||||
update_thread = std::thread(&LiveStream::updateThread, this);
|
update_thread = std::thread(&LiveStream::updateThread, this);
|
||||||
@@ -64,15 +61,15 @@ void LiveStream::stop() {
|
|||||||
|
|
||||||
void LiveStream::updateThread() {
|
void LiveStream::updateThread() {
|
||||||
while (!exit_) {
|
while (!exit_) {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / fps_));
|
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / STREAM_UPDATE_FPS));
|
||||||
// coalesce: skip the emit if the main thread hasn't processed the previous one yet.
|
|
||||||
if (!update_pending_.exchange(true)) {
|
if (!update_pending_.exchange(true)) {
|
||||||
emit privateUpdateLastMsgsSignal();
|
requestUpdateLastMessages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// called in streamThread
|
|
||||||
void LiveStream::handleEvent(kj::ArrayPtr<capnp::word> data) {
|
void LiveStream::handleEvent(kj::ArrayPtr<capnp::word> data) {
|
||||||
if (logger) {
|
if (logger) {
|
||||||
logger->write(data);
|
logger->write(data);
|
||||||
@@ -89,12 +86,11 @@ void LiveStream::handleEvent(kj::ArrayPtr<capnp::word> data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// called on the main thread by the queued privateUpdateLastMsgsSignal connection
|
|
||||||
void LiveStream::updateLastMessages() {
|
void LiveStream::updateLastMessages() {
|
||||||
update_pending_ = false;
|
update_pending_ = false;
|
||||||
fps_ = settings.fps;
|
|
||||||
{
|
{
|
||||||
// merge events received from live stream thread.
|
|
||||||
std::lock_guard lk(lock);
|
std::lock_guard lk(lock);
|
||||||
mergeEvents(received_events_);
|
mergeEvents(received_events_);
|
||||||
uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0;
|
uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0;
|
||||||
@@ -142,10 +138,10 @@ void LiveStream::seekTo(double sec) {
|
|||||||
first_update_ts = nanos_since_boot();
|
first_update_ts = nanos_since_boot();
|
||||||
current_event_ts = first_event_ts = std::min<uint64_t>(sec * 1e9 + begin_event_ts, lastest_event_ts);
|
current_event_ts = first_event_ts = std::min<uint64_t>(sec * 1e9 + begin_event_ts, lastest_event_ts);
|
||||||
post_last_event = (first_event_ts == lastest_event_ts);
|
post_last_event = (first_event_ts == lastest_event_ts);
|
||||||
emit seekedTo((current_event_ts - begin_event_ts) / 1e9);
|
seekedTo((current_event_ts - begin_event_ts) / 1e9);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LiveStream::pause(bool pause) {
|
void LiveStream::pause(bool pause) {
|
||||||
paused_ = pause;
|
paused_ = pause;
|
||||||
emit(pause ? paused() : resume());
|
pause ? paused() : resume();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,8 @@
|
|||||||
#include "tools/cabana/streams/abstractstream.h"
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
|
|
||||||
class LiveStream : public AbstractStream {
|
class LiveStream : public AbstractStream {
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LiveStream(QObject *parent);
|
LiveStream();
|
||||||
virtual ~LiveStream();
|
virtual ~LiveStream();
|
||||||
void start() override;
|
void start() override;
|
||||||
void stop();
|
void stop();
|
||||||
@@ -39,7 +37,6 @@ private:
|
|||||||
std::mutex lock;
|
std::mutex lock;
|
||||||
std::thread stream_thread, update_thread;
|
std::thread stream_thread, update_thread;
|
||||||
std::atomic<bool> update_pending_ = false;
|
std::atomic<bool> update_pending_ = false;
|
||||||
std::atomic<int> fps_ = 10;
|
|
||||||
std::vector<const CanEvent *> received_events_;
|
std::vector<const CanEvent *> received_events_;
|
||||||
|
|
||||||
std::chrono::system_clock::time_point begin_date_time;
|
std::chrono::system_clock::time_point begin_date_time;
|
||||||
|
|||||||
@@ -4,13 +4,7 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include <QCheckBox>
|
PandaStream::PandaStream(PandaStreamConfig config_) : config(config_) {
|
||||||
#include <QLabel>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {
|
|
||||||
if (!connect()) {
|
if (!connect()) {
|
||||||
throw std::runtime_error("Failed to connect to panda");
|
throw std::runtime_error("Failed to connect to panda");
|
||||||
}
|
}
|
||||||
@@ -30,12 +24,12 @@ bool PandaStream::connect() {
|
|||||||
for (int bus = 0; bus < config.bus_config.size(); bus++) {
|
for (int bus = 0; bus < config.bus_config.size(); bus++) {
|
||||||
panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps);
|
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 (panda->hw_type == cereal::PandaState::PandaType::RED_PANDA || panda->hw_type == cereal::PandaState::PandaType::RED_PANDA_V2) {
|
||||||
if (config.bus_config[bus].can_fd) {
|
if (config.bus_config[bus].can_fd) {
|
||||||
panda->set_data_speed_kbps(bus, config.bus_config[bus].data_speed_kbps);
|
panda->set_data_speed_kbps(bus, config.bus_config[bus].data_speed_kbps);
|
||||||
} else {
|
} else {
|
||||||
// Hack to disable can-fd by setting data speed to a low value
|
|
||||||
panda->set_data_speed_kbps(bus, 10);
|
panda->set_data_speed_kbps(bus, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,113 +71,3 @@ void PandaStream::streamThread() {
|
|||||||
panda->send_heartbeat(false);
|
panda->send_heartbeat(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenPandaWidget
|
|
||||||
|
|
||||||
OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
|
|
||||||
form_layout = new QFormLayout(this);
|
|
||||||
if (can && dynamic_cast<PandaStream *>(can) != nullptr) {
|
|
||||||
form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(QString::fromStdString(can->routeName()))));
|
|
||||||
form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda."));
|
|
||||||
QTimer::singleShot(0, [this]() { emit enableOpenButton(false); });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QHBoxLayout *serial_layout = new QHBoxLayout();
|
|
||||||
serial_layout->addWidget(serial_edit = new QComboBox());
|
|
||||||
|
|
||||||
QPushButton *refresh = new QPushButton(tr("Refresh"));
|
|
||||||
refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
|
||||||
serial_layout->addWidget(refresh);
|
|
||||||
form_layout->addRow(tr("Serial"), serial_layout);
|
|
||||||
|
|
||||||
QObject::connect(refresh, &QPushButton::clicked, this, &OpenPandaWidget::refreshSerials);
|
|
||||||
QObject::connect(serial_edit, &QComboBox::currentTextChanged, this, &OpenPandaWidget::buildConfigForm);
|
|
||||||
|
|
||||||
// Populate serials
|
|
||||||
refreshSerials();
|
|
||||||
buildConfigForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenPandaWidget::refreshSerials() {
|
|
||||||
serial_edit->clear();
|
|
||||||
for (auto serial : Panda::list()) {
|
|
||||||
serial_edit->addItem(QString::fromStdString(serial));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenPandaWidget::buildConfigForm() {
|
|
||||||
for (int i = form_layout->rowCount() - 1; i > 0; --i) {
|
|
||||||
form_layout->removeRow(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString serial = serial_edit->currentText();
|
|
||||||
bool has_fd = false;
|
|
||||||
bool has_panda = !serial.isEmpty();
|
|
||||||
if (has_panda) {
|
|
||||||
try {
|
|
||||||
Panda panda(serial.toStdString());
|
|
||||||
has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2);
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData());
|
|
||||||
has_panda = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (has_panda) {
|
|
||||||
config.serial = serial.toStdString();
|
|
||||||
config.bus_config.resize(3);
|
|
||||||
for (int i = 0; i < config.bus_config.size(); i++) {
|
|
||||||
QHBoxLayout *bus_layout = new QHBoxLayout;
|
|
||||||
|
|
||||||
// CAN Speed
|
|
||||||
bus_layout->addWidget(new QLabel(tr("CAN Speed (kbps):")));
|
|
||||||
QComboBox *can_speed = new QComboBox;
|
|
||||||
for (int j = 0; j < std::size(speeds); j++) {
|
|
||||||
can_speed->addItem(QString::number(speeds[j]));
|
|
||||||
|
|
||||||
if (data_speeds[j] == config.bus_config[i].can_speed_kbps) {
|
|
||||||
can_speed->setCurrentIndex(j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QObject::connect(can_speed, qOverload<int>(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].can_speed_kbps = speeds[index];});
|
|
||||||
bus_layout->addWidget(can_speed);
|
|
||||||
|
|
||||||
// CAN-FD Speed
|
|
||||||
if (has_fd) {
|
|
||||||
QCheckBox *enable_fd = new QCheckBox("CAN-FD");
|
|
||||||
bus_layout->addWidget(enable_fd);
|
|
||||||
bus_layout->addWidget(new QLabel(tr("Data Speed (kbps):")));
|
|
||||||
QComboBox *data_speed = new QComboBox;
|
|
||||||
for (int j = 0; j < std::size(data_speeds); j++) {
|
|
||||||
data_speed->addItem(QString::number(data_speeds[j]));
|
|
||||||
|
|
||||||
if (data_speeds[j] == config.bus_config[i].data_speed_kbps) {
|
|
||||||
data_speed->setCurrentIndex(j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data_speed->setEnabled(false);
|
|
||||||
bus_layout->addWidget(data_speed);
|
|
||||||
|
|
||||||
QObject::connect(data_speed, qOverload<int>(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].data_speed_kbps = data_speeds[index];});
|
|
||||||
QObject::connect(enable_fd, &QCheckBox::stateChanged, data_speed, &QComboBox::setEnabled);
|
|
||||||
QObject::connect(enable_fd, &QCheckBox::stateChanged, [=](int state) {config.bus_config[i].can_fd = (bool)state;});
|
|
||||||
}
|
|
||||||
|
|
||||||
form_layout->addRow(tr("Bus %1:").arg(i), bus_layout);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.serial = "";
|
|
||||||
form_layout->addWidget(new QLabel(tr("No panda found")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AbstractStream *OpenPandaWidget::open() {
|
|
||||||
try {
|
|
||||||
return new PandaStream(qApp, config);
|
|
||||||
} catch (std::exception &e) {
|
|
||||||
QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what()));
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,15 +3,9 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QFormLayout>
|
|
||||||
|
|
||||||
#include "tools/cabana/streams/livestream.h"
|
#include "tools/cabana/streams/livestream.h"
|
||||||
#include "tools/cabana/panda.h"
|
#include "tools/cabana/panda.h"
|
||||||
|
|
||||||
const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U};
|
|
||||||
const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U};
|
|
||||||
|
|
||||||
struct BusConfig {
|
struct BusConfig {
|
||||||
int can_speed_kbps = 500;
|
int can_speed_kbps = 500;
|
||||||
int data_speed_kbps = 2000;
|
int data_speed_kbps = 2000;
|
||||||
@@ -24,9 +18,8 @@ struct PandaStreamConfig {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class PandaStream : public LiveStream {
|
class PandaStream : public LiveStream {
|
||||||
Q_OBJECT
|
|
||||||
public:
|
public:
|
||||||
PandaStream(QObject *parent, PandaStreamConfig config_ = {});
|
PandaStream(PandaStreamConfig config_ = {});
|
||||||
~PandaStream() { stop(); }
|
~PandaStream() { stop(); }
|
||||||
inline std::string routeName() const override {
|
inline std::string routeName() const override {
|
||||||
return "Panda: " + config.serial;
|
return "Panda: " + config.serial;
|
||||||
@@ -39,19 +32,3 @@ protected:
|
|||||||
std::unique_ptr<Panda> panda;
|
std::unique_ptr<Panda> panda;
|
||||||
PandaStreamConfig config = {};
|
PandaStreamConfig config = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
class OpenPandaWidget : public AbstractOpenStreamWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
OpenPandaWidget(QWidget *parent = nullptr);
|
|
||||||
AbstractStream *open() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void refreshSerials();
|
|
||||||
void buildConfigForm();
|
|
||||||
|
|
||||||
QComboBox *serial_edit;
|
|
||||||
QFormLayout *form_layout;
|
|
||||||
PandaStreamConfig config = {};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
#include "tools/cabana/streams/replaystream.h"
|
#include "tools/cabana/streams/replaystream.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <string>
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QGridLayout>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QPushButton>
|
|
||||||
|
|
||||||
#include "common/timing.h"
|
#include "common/timing.h"
|
||||||
#include "common/util.h"
|
#include "common/util.h"
|
||||||
#include "tools/cabana/streams/routes.h"
|
#include "tools/cabana/settings.h"
|
||||||
|
|
||||||
ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) {
|
ReplayStream::ReplayStream() {
|
||||||
unsetenv("ZMQ");
|
unsetenv("ZMQ");
|
||||||
setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1);
|
setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1);
|
||||||
|
|
||||||
op_prefix = std::make_unique<OpenpilotPrefix>();
|
op_prefix = std::make_unique<OpenpilotPrefix>();
|
||||||
|
|
||||||
QObject::connect(&settings, &Settings::changed, this, [this]() {
|
settings_connection_ = settings.changed.connect([this]() {
|
||||||
if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReplayStream::~ReplayStream() {
|
||||||
|
cancelWaits();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void ReplayStream::mergeSegments() {
|
void ReplayStream::mergeSegments() {
|
||||||
auto event_data = replay->getEventData();
|
auto event_data = replay->getEventData();
|
||||||
for (const auto &[n, seg] : event_data->segments) {
|
for (const auto &[n, seg] : event_data->segments) {
|
||||||
@@ -31,58 +31,59 @@ void ReplayStream::mergeSegments() {
|
|||||||
|
|
||||||
std::vector<const CanEvent *> new_events;
|
std::vector<const CanEvent *> new_events;
|
||||||
new_events.reserve(seg->log->events.size());
|
new_events.reserve(seg->log->events.size());
|
||||||
|
MessageEventsMap msg_events;
|
||||||
for (const Event &e : seg->log->events) {
|
for (const Event &e : seg->log->events) {
|
||||||
if (e.which == cereal::Event::Which::CAN) {
|
if (e.which == cereal::Event::Which::CAN) {
|
||||||
capnp::FlatArrayMessageReader reader(e.data);
|
capnp::FlatArrayMessageReader reader(e.data);
|
||||||
auto event = reader.getRoot<cereal::Event>();
|
auto event = reader.getRoot<cereal::Event>();
|
||||||
for (const auto &c : event.getCan()) {
|
for (const auto &c : event.getCan()) {
|
||||||
new_events.push_back(newEvent(e.mono_time, c));
|
const CanEvent *ce = newEvent(e.mono_time, c);
|
||||||
|
new_events.push_back(ce);
|
||||||
|
msg_events[{.source = ce->src, .address = ce->address}].push_back(ce);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mergeEvents(new_events);
|
postToMainThreadAndWait([&]() { insertEvents(new_events, msg_events); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ReplayStream::loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags, bool 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"},
|
replay.reset(new Replay(route, {"can", "narrowRoadEncodeIdx", "cabinEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||||
{}, nullptr, replay_flags, data_dir, auto_source));
|
{}, nullptr, replay_flags, data_dir, auto_source));
|
||||||
replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
||||||
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
|
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
|
||||||
|
|
||||||
// Forward replay callbacks to corresponding Qt signals.
|
|
||||||
replay->onSeeking = [this](double sec) { emit seeking(sec); };
|
replay->onSeeking = [this](double sec) { postToMainThread([this, sec]() { seeking(sec); }); };
|
||||||
replay->onSeekedTo = [this](double sec) {
|
replay->onSeekedTo = [this](double sec) {
|
||||||
emit seekedTo(sec);
|
postToMainThread([this, sec]() { seekedTo(sec); });
|
||||||
waitForSeekFinshed();
|
waitForSeekFinshed();
|
||||||
};
|
};
|
||||||
replay->onQLogLoaded = [this](std::shared_ptr<LogReader> qlog) { emit qLogLoaded(qlog); };
|
replay->onQLogLoaded = [this](std::shared_ptr<LogReader> qlog) { postToMainThread([this, qlog]() { qLogLoaded(qlog); }); };
|
||||||
replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::BlockingQueuedConnection); };
|
replay->onSegmentsMerged = [this]() { mergeSegments(); };
|
||||||
|
|
||||||
bool success = replay->load();
|
bool success = replay->load();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
std::string message;
|
||||||
if (replay->lastRouteError() == RouteLoadError::Unauthorized) {
|
if (replay->lastRouteError() == RouteLoadError::Unauthorized) {
|
||||||
auto auth_content = util::read_file(util::getenv("HOME") + "/.comma/auth.json");
|
auto auth_content = util::read_file(util::getenv("HOME") + "/.comma/auth.json");
|
||||||
QString message;
|
|
||||||
if (auth_content.empty()) {
|
if (auth_content.empty()) {
|
||||||
message = "Authentication Required. Please run the following command to authenticate:\n\n"
|
message = "Authentication Required. Please run the following command to authenticate:\n\n"
|
||||||
"python3 openpilot/tools/lib/auth.py\n\n"
|
"python3 iqpilot/tools/lib/auth.py\n\n"
|
||||||
"This will grant access to routes from your comma account.";
|
"This will grant access to routes from your comma account.";
|
||||||
} else {
|
} else {
|
||||||
message = tr("Access Denied. You do not have permission to access route:\n\n%1\n\n"
|
message = "Access Denied. You do not have permission to access route:\n\n" + route + "\n\n"
|
||||||
"This is likely a private route.").arg(QString::fromStdString(route));
|
"This is likely a private route.";
|
||||||
}
|
}
|
||||||
QMessageBox::warning(nullptr, tr("Access Denied"), message);
|
|
||||||
} else if (replay->lastRouteError() == RouteLoadError::NetworkError) {
|
} else if (replay->lastRouteError() == RouteLoadError::NetworkError) {
|
||||||
QMessageBox::warning(nullptr, tr("Network Error"),
|
message = "Unable to load the route:\n\n " + route + ".\n\nPlease check your network connection and try again.";
|
||||||
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) {
|
} else if (replay->lastRouteError() == RouteLoadError::FileNotFound) {
|
||||||
QMessageBox::warning(nullptr, tr("Route Not Found"),
|
message = "The specified route could not be found:\n\n " + route + ".\n\nPlease check the route name and try again.";
|
||||||
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 {
|
} else {
|
||||||
QMessageBox::warning(nullptr, tr("Route Load Failed"), tr("Failed to load route: '%1'").arg(QString::fromStdString(route)));
|
message = "Failed to load route: '" + route + "'";
|
||||||
}
|
}
|
||||||
|
error(message);
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
@@ -101,8 +102,8 @@ bool ReplayStream::eventFilter(const Event *event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double ts = millis_since_boot();
|
double ts = millis_since_boot();
|
||||||
if ((ts - prev_update_ts) > (1000.0 / settings.fps)) {
|
if ((ts - prev_update_ts) > (1000.0 / STREAM_UPDATE_FPS)) {
|
||||||
emit privateUpdateLastMsgsSignal();
|
requestUpdateLastMessages();
|
||||||
prev_update_ts = ts;
|
prev_update_ts = ts;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -110,66 +111,5 @@ bool ReplayStream::eventFilter(const Event *event) {
|
|||||||
|
|
||||||
void ReplayStream::pause(bool pause) {
|
void ReplayStream::pause(bool pause) {
|
||||||
replay->pause(pause);
|
replay->pause(pause);
|
||||||
emit(pause ? paused() : resume());
|
pause ? paused() : resume();
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// OpenReplayWidget
|
|
||||||
|
|
||||||
OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
|
|
||||||
QGridLayout *grid_layout = new QGridLayout(this);
|
|
||||||
grid_layout->addWidget(new QLabel(tr("Route")), 0, 0);
|
|
||||||
grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1);
|
|
||||||
route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route"));
|
|
||||||
auto browse_remote_btn = new QPushButton(tr("Remote route..."), this);
|
|
||||||
grid_layout->addWidget(browse_remote_btn, 0, 2);
|
|
||||||
auto browse_local_btn = new QPushButton(tr("Local route..."), this);
|
|
||||||
grid_layout->addWidget(browse_local_btn, 0, 3);
|
|
||||||
|
|
||||||
QHBoxLayout *camera_layout = new QHBoxLayout();
|
|
||||||
for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")})
|
|
||||||
camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this)));
|
|
||||||
cameras[0]->setChecked(true);
|
|
||||||
camera_layout->addStretch(1);
|
|
||||||
grid_layout->addItem(camera_layout, 1, 1);
|
|
||||||
|
|
||||||
setMinimumWidth(550);
|
|
||||||
QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() {
|
|
||||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir));
|
|
||||||
if (!dir.isEmpty()) {
|
|
||||||
route_edit->setText(dir);
|
|
||||||
settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() {
|
|
||||||
RoutesDialog route_dlg(this);
|
|
||||||
if (route_dlg.exec()) {
|
|
||||||
route_edit->setText(route_dlg.route());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
AbstractStream *OpenReplayWidget::open() {
|
|
||||||
QString route = route_edit->text();
|
|
||||||
QString data_dir;
|
|
||||||
if (int idx = route.lastIndexOf('/'); idx != -1 && util::file_exists(route.toStdString())) {
|
|
||||||
data_dir = route.mid(0, idx + 1);
|
|
||||||
route = route.mid(idx + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_valid_format = Route::parseRoute(route.toStdString()).str.size() > 0;
|
|
||||||
if (!is_valid_format) {
|
|
||||||
QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route));
|
|
||||||
} else {
|
|
||||||
auto replay_stream = std::make_unique<ReplayStream>(qApp);
|
|
||||||
uint32_t flags = REPLAY_FLAG_NONE;
|
|
||||||
if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_DCAM;
|
|
||||||
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.toStdString(), data_dir.toStdString(), flags)) {
|
|
||||||
return replay_stream.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QCheckBox>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "common/prefix.h"
|
#include "common/prefix.h"
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
#include "tools/replay/replay.h"
|
#include "tools/replay/replay.h"
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(std::shared_ptr<LogReader>);
|
|
||||||
|
|
||||||
class ReplayStream : public AbstractStream {
|
class ReplayStream : public AbstractStream {
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ReplayStream(QObject *parent);
|
ReplayStream();
|
||||||
|
~ReplayStream();
|
||||||
void start() override { replay->start(); }
|
void start() override { replay->start(); }
|
||||||
bool loadRoute(const std::string &route, const std::string &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);
|
bool eventFilter(const Event *event);
|
||||||
@@ -36,24 +31,13 @@ public:
|
|||||||
inline bool isPaused() const override { return replay->isPaused(); }
|
inline bool isPaused() const override { return replay->isPaused(); }
|
||||||
void pause(bool pause) override;
|
void pause(bool pause) override;
|
||||||
|
|
||||||
signals:
|
|
||||||
void qLogLoaded(std::shared_ptr<LogReader> qlog);
|
Observable<std::shared_ptr<LogReader>> qLogLoaded;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void mergeSegments();
|
void mergeSegments();
|
||||||
std::unique_ptr<Replay> replay = nullptr;
|
std::unique_ptr<Replay> replay = nullptr;
|
||||||
|
Connection settings_connection_;
|
||||||
std::set<int> processed_segments;
|
std::set<int> processed_segments;
|
||||||
std::unique_ptr<OpenpilotPrefix> op_prefix;
|
std::unique_ptr<OpenpilotPrefix> op_prefix;
|
||||||
};
|
};
|
||||||
|
|
||||||
class OpenReplayWidget : public AbstractOpenStreamWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
OpenReplayWidget(QWidget *parent = nullptr);
|
|
||||||
AbstractStream *open() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QLineEdit *route_edit;
|
|
||||||
std::vector<QCheckBox *> cameras;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
#include "tools/cabana/streams/routes.h"
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <ctime>
|
|
||||||
#include <string>
|
|
||||||
#include <thread>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QDialogButtonBox>
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QListWidget>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QPainter>
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
public:
|
|
||||||
RouteListWidget(QWidget *parent = nullptr) : QListWidget(parent) {}
|
|
||||||
void setEmptyText(const QString &text) {
|
|
||||||
empty_text_ = text;
|
|
||||||
viewport()->update();
|
|
||||||
}
|
|
||||||
void paintEvent(QPaintEvent *event) override {
|
|
||||||
QListWidget::paintEvent(event);
|
|
||||||
if (count() == 0) {
|
|
||||||
QPainter painter(viewport());
|
|
||||||
painter.drawText(viewport()->rect(), Qt::AlignCenter, empty_text_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QString empty_text_ = tr("No items");
|
|
||||||
};
|
|
||||||
|
|
||||||
RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) {
|
|
||||||
setWindowTitle(tr("Remote routes"));
|
|
||||||
|
|
||||||
QFormLayout *layout = new QFormLayout(this);
|
|
||||||
layout->addRow(tr("Device"), device_list_ = new QComboBox(this));
|
|
||||||
layout->addRow(period_selector_ = new QComboBox(this));
|
|
||||||
layout->addRow(route_list_ = new RouteListWidget(this));
|
|
||||||
auto button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
|
||||||
layout->addRow(button_box);
|
|
||||||
|
|
||||||
device_list_->addItem(tr("Loading..."));
|
|
||||||
period_selector_->addItem(tr("Last week"), 7);
|
|
||||||
period_selector_->addItem(tr("Last 2 weeks"), 14);
|
|
||||||
period_selector_->addItem(tr("Last month"), 30);
|
|
||||||
period_selector_->addItem(tr("Last 6 months"), 180);
|
|
||||||
period_selector_->addItem(tr("Preserved"), -1);
|
|
||||||
|
|
||||||
connect(device_list_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
|
|
||||||
connect(period_selector_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
|
|
||||||
connect(route_list_, &QListWidget::itemDoubleClicked, this, &QDialog::accept);
|
|
||||||
connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
|
||||||
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
|
||||||
|
|
||||||
// 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, int error_code) {
|
|
||||||
if (success) {
|
|
||||||
device_list_->clear();
|
|
||||||
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 {
|
|
||||||
QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with tools/lib/auth.py") : tr("Network error"));
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RoutesDialog::fetchRoutes() {
|
|
||||||
if (device_list_->currentIndex() == -1 || device_list_->currentData().isNull())
|
|
||||||
return;
|
|
||||||
|
|
||||||
route_list_->clear();
|
|
||||||
route_list_->setEmptyText(tr("Loading..."));
|
|
||||||
|
|
||||||
std::string did = device_list_->currentText().toStdString();
|
|
||||||
int period = period_selector_->currentData().toInt();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, int error_code) {
|
|
||||||
if (success) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (route_list_->count() > 0) route_list_->setCurrentRow(0);
|
|
||||||
} else {
|
|
||||||
QMessageBox::warning(this, tr("Error"), tr("Failed to fetch routes. Check your network connection."));
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
route_list_->setEmptyText(tr("No items"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString RoutesDialog::route() {
|
|
||||||
auto current_item = route_list_->currentItem();
|
|
||||||
return current_item ? current_item->data(Qt::UserRole).toString() : "";
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QDialog>
|
|
||||||
|
|
||||||
class RouteListWidget;
|
|
||||||
|
|
||||||
class RoutesDialog : public QDialog {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
RoutesDialog(QWidget *parent);
|
|
||||||
QString route();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
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_;
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
@@ -8,15 +8,8 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
#include <QFormLayout>
|
SocketCanStream::SocketCanStream(SocketCanStreamConfig config_) : config(config_) {
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QPushButton>
|
|
||||||
|
|
||||||
SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) {
|
|
||||||
if (!available()) {
|
if (!available()) {
|
||||||
throw std::runtime_error("SocketCAN not available");
|
throw std::runtime_error("SocketCAN not available");
|
||||||
}
|
}
|
||||||
@@ -49,7 +42,7 @@ bool SocketCanStream::connect() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable CAN-FD
|
|
||||||
int fd_enable = 1;
|
int fd_enable = 1;
|
||||||
setsockopt(sock_fd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &fd_enable, sizeof(fd_enable));
|
setsockopt(sock_fd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &fd_enable, sizeof(fd_enable));
|
||||||
|
|
||||||
@@ -72,8 +65,8 @@ bool SocketCanStream::connect() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set read timeout so the thread can check for interruption
|
|
||||||
struct timeval tv = {.tv_sec = 0, .tv_usec = 100000}; // 100ms
|
struct timeval tv = {.tv_sec = 0, .tv_usec = 100000};
|
||||||
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -86,7 +79,7 @@ void SocketCanStream::streamThread() {
|
|||||||
ssize_t nbytes = read(sock_fd, &frame, sizeof(frame));
|
ssize_t nbytes = read(sock_fd, &frame, sizeof(frame));
|
||||||
if (nbytes <= 0) continue;
|
if (nbytes <= 0) continue;
|
||||||
|
|
||||||
uint8_t len = (nbytes == CAN_MTU) ? frame.len : frame.len; // works for both CAN and CAN-FD
|
uint8_t len = (nbytes == CAN_MTU) ? frame.len : frame.len;
|
||||||
|
|
||||||
MessageBuilder msg;
|
MessageBuilder msg;
|
||||||
auto evt = msg.initEvent();
|
auto evt = msg.initEvent();
|
||||||
@@ -98,51 +91,3 @@ void SocketCanStream::streamThread() {
|
|||||||
handleEvent(capnp::messageToFlatArray(msg));
|
handleEvent(capnp::messageToFlatArray(msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) {
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
main_layout->addStretch(1);
|
|
||||||
|
|
||||||
QFormLayout *form_layout = new QFormLayout();
|
|
||||||
|
|
||||||
QHBoxLayout *device_layout = new QHBoxLayout();
|
|
||||||
device_edit = new QComboBox();
|
|
||||||
device_edit->setFixedWidth(300);
|
|
||||||
device_layout->addWidget(device_edit);
|
|
||||||
|
|
||||||
QPushButton *refresh = new QPushButton(tr("Refresh"));
|
|
||||||
refresh->setFixedWidth(100);
|
|
||||||
device_layout->addWidget(refresh);
|
|
||||||
form_layout->addRow(tr("Device"), device_layout);
|
|
||||||
main_layout->addLayout(form_layout);
|
|
||||||
|
|
||||||
main_layout->addStretch(1);
|
|
||||||
|
|
||||||
QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices);
|
|
||||||
QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); });
|
|
||||||
|
|
||||||
// Populate devices
|
|
||||||
refreshDevices();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenSocketCanWidget::refreshDevices() {
|
|
||||||
device_edit->clear();
|
|
||||||
// Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN)
|
|
||||||
std::error_code ec;
|
|
||||||
for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) {
|
|
||||||
std::ifstream type_file(entry.path() / "type");
|
|
||||||
int type = 0;
|
|
||||||
if (type_file >> type && type == 280) {
|
|
||||||
device_edit->addItem(QString::fromStdString(entry.path().filename().string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AbstractStream *OpenSocketCanWidget::open() {
|
|
||||||
try {
|
|
||||||
return new SocketCanStream(qApp, config);
|
|
||||||
} catch (std::exception &e) {
|
|
||||||
QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what()));
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
|
|
||||||
#include "tools/cabana/streams/livestream.h"
|
#include "tools/cabana/streams/livestream.h"
|
||||||
|
|
||||||
struct SocketCanStreamConfig {
|
struct SocketCanStreamConfig {
|
||||||
std::string device = ""; // TODO: support multiple devices/buses at once
|
std::string device = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
class SocketCanStream : public LiveStream {
|
class SocketCanStream : public LiveStream {
|
||||||
Q_OBJECT
|
|
||||||
public:
|
public:
|
||||||
SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {});
|
SocketCanStream(SocketCanStreamConfig config_ = {});
|
||||||
~SocketCanStream();
|
~SocketCanStream();
|
||||||
static bool available();
|
static bool available();
|
||||||
|
|
||||||
@@ -26,17 +23,3 @@ protected:
|
|||||||
SocketCanStreamConfig config = {};
|
SocketCanStreamConfig config = {};
|
||||||
int sock_fd = -1;
|
int sock_fd = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
class OpenSocketCanWidget : public AbstractOpenStreamWidget {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
OpenSocketCanWidget(QWidget *parent = nullptr);
|
|
||||||
AbstractStream *open() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void refreshDevices();
|
|
||||||
|
|
||||||
QComboBox *device_edit;
|
|
||||||
SocketCanStreamConfig config = {};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
#include "tools/cabana/streamselector.h"
|
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPushButton>
|
|
||||||
|
|
||||||
#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"));
|
|
||||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
|
||||||
tab = new QTabWidget(this);
|
|
||||||
layout->addWidget(tab);
|
|
||||||
|
|
||||||
QHBoxLayout *dbc_layout = new QHBoxLayout();
|
|
||||||
dbc_file = new QLineEdit(this);
|
|
||||||
dbc_file->setReadOnly(true);
|
|
||||||
dbc_file->setPlaceholderText(tr("Choose a dbc file to open"));
|
|
||||||
QPushButton *file_btn = new QPushButton(tr("Browse..."));
|
|
||||||
dbc_layout->addWidget(new QLabel(tr("dbc File")));
|
|
||||||
dbc_layout->addWidget(dbc_file);
|
|
||||||
dbc_layout->addWidget(file_btn);
|
|
||||||
layout->addLayout(dbc_layout);
|
|
||||||
|
|
||||||
QFrame *line = new QFrame(this);
|
|
||||||
line->setFrameStyle(QFrame::HLine | QFrame::Sunken);
|
|
||||||
layout->addWidget(line);
|
|
||||||
|
|
||||||
btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel);
|
|
||||||
layout->addWidget(btn_box);
|
|
||||||
|
|
||||||
addStreamWidget(new OpenReplayWidget, tr("&Replay"));
|
|
||||||
addStreamWidget(new OpenPandaWidget, tr("&Panda"));
|
|
||||||
#ifdef __linux__
|
|
||||||
if (SocketCanStream::available()) {
|
|
||||||
addStreamWidget(new OpenSocketCanWidget, tr("&SocketCAN"));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
addStreamWidget(new OpenDeviceWidget, tr("&Device"));
|
|
||||||
|
|
||||||
QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
|
||||||
QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() {
|
|
||||||
setEnabled(false);
|
|
||||||
if (stream_ = ((AbstractOpenStreamWidget *)tab->currentWidget())->open(); stream_) {
|
|
||||||
accept();
|
|
||||||
}
|
|
||||||
setEnabled(true);
|
|
||||||
});
|
|
||||||
QObject::connect(file_btn, &QPushButton::clicked, [this]() {
|
|
||||||
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)");
|
|
||||||
if (!fn.isEmpty()) {
|
|
||||||
dbc_file->setText(fn);
|
|
||||||
settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void StreamSelector::addStreamWidget(AbstractOpenStreamWidget *w, const QString &title) {
|
|
||||||
tab->addTab(w, title);
|
|
||||||
auto open_btn = btn_box->button(QDialogButtonBox::Open);
|
|
||||||
QObject::connect(w, &AbstractOpenStreamWidget::enableOpenButton, open_btn, &QPushButton::setEnabled);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QDialogButtonBox>
|
|
||||||
#include <QDialog>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QTabWidget>
|
|
||||||
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
class StreamSelector : public QDialog {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
StreamSelector(QWidget *parent = nullptr);
|
|
||||||
void addStreamWidget(AbstractOpenStreamWidget *w, const QString &title);
|
|
||||||
QString dbcFile() const { return dbc_file->text(); }
|
|
||||||
AbstractStream *stream() const { return stream_; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
AbstractStream *stream_ = nullptr;
|
|
||||||
QLineEdit *dbc_file;
|
|
||||||
QTabWidget *tab;
|
|
||||||
QDialogButtonBox *btn_box;
|
|
||||||
};
|
|
||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
CABANA_DIR = Path(__file__).parent
|
CABANA_DIR = Path(__file__).parent
|
||||||
CABANA_BIN = CABANA_DIR / "_cabana"
|
CABANA_BIN = CABANA_DIR / "_cabana"
|
||||||
|
|
||||||
@@ -16,85 +17,76 @@ class TestCabanaBinary:
|
|||||||
result = subprocess.run([str(CABANA_BIN), "--help"], capture_output=True, text=True, timeout=60)
|
result = subprocess.run([str(CABANA_BIN), "--help"], capture_output=True, text=True, timeout=60)
|
||||||
assert result.returncode == 0, result.stderr
|
assert result.returncode == 0, result.stderr
|
||||||
assert "Usage:" in result.stderr
|
assert "Usage:" in result.stderr
|
||||||
|
assert "--zmq" in result.stderr
|
||||||
|
assert "--bridge" in result.stderr
|
||||||
|
|
||||||
def test_help_documents_both_live_can_modes(self):
|
def test_launcher_builds_current_targets(self):
|
||||||
assert CABANA_BIN.exists(), "cabana not built (scons -u iqpilot/tools/cabana/_cabana)"
|
|
||||||
# --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")
|
launcher = read("cabana")
|
||||||
# iqpilot is not nested under openpilot/
|
assert "iqpilot/tools/cabana/_cabana" in launcher
|
||||||
assert "scons -u iqpilot/tools/cabana/_cabana iqpilot/cereal/messaging/bridge" in launcher
|
assert "iqpilot/cereal/messaging/bridge" in launcher
|
||||||
assert "openpilot/tools/cabana" not in launcher
|
assert "openpilot/tools/cabana" not in launcher
|
||||||
|
|
||||||
|
|
||||||
class TestKonn3ktIntegration:
|
class TestKonn3ktIntegration:
|
||||||
def test_routes_dialog_uses_konn3kt_api(self):
|
def test_routes_use_konn3kt_api(self):
|
||||||
# upstream shells into tools/lib via PyDownloader; iqpilot goes through CommaApi2
|
routes = read("routes.cc")
|
||||||
routes = read("streams/routes.cc")
|
|
||||||
assert "PyDownloader::" not in routes
|
assert "PyDownloader::" not in routes
|
||||||
assert "py_downloader.h" not in routes
|
assert "py_downloader.h" not in routes
|
||||||
assert "CommaApi2::getDevices()" in routes
|
assert "CommaApi2::getDevices()" in routes
|
||||||
assert "CommaApi2::getDeviceRoutes(" 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):
|
def test_no_comma_endpoints(self):
|
||||||
for name in ("streams/routes.cc", "cabana.cc", "README.md"):
|
for name in ("routes.cc", "ui/main.cc", "README.md"):
|
||||||
body = read(name)
|
body = read(name)
|
||||||
assert "connect.comma.ai" not in body, name
|
assert "connect.comma.ai" not in body, name
|
||||||
assert "api.comma.ai" not in body, name
|
assert "api.comma.ai" not in body, name
|
||||||
|
|
||||||
def test_dbc_menu_points_at_iqdbc(self):
|
def test_dbc_menu_uses_iqdbc(self):
|
||||||
mainwin = read("mainwin.cc")
|
mainwin = read("ui/mainwin.cc")
|
||||||
assert "commaai/iqdbc" in mainwin
|
assert "IQ.Pilot iqdbc" in mainwin
|
||||||
assert "commaai/opendbc" not in mainwin
|
assert "commaai/opendbc" not in mainwin
|
||||||
|
|
||||||
def test_dbc_json_generator_imports_iqdbc(self):
|
def test_dbc_json_generator_imports_iqdbc(self):
|
||||||
gen = read("dbc/generate_dbc_json.py")
|
generator = read("dbc/generate_dbc_json.py")
|
||||||
assert "from iqdbc.car" in gen
|
assert "from iqdbc.car" in generator
|
||||||
assert "from opendbc.car" not in gen
|
assert "from opendbc.car" not in generator
|
||||||
|
|
||||||
def test_generated_dbc_json_covers_iqdbc_platforms(self):
|
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"
|
path = CABANA_DIR / "dbc" / "car_fingerprint_to_dbc.json"
|
||||||
assert path.is_file(), "run scons -u iqpilot/tools/cabana"
|
assert path.is_file(), "run scons -u iqpilot/tools/cabana/_cabana"
|
||||||
mapping = json.loads(path.read_text())
|
mapping = json.loads(path.read_text())
|
||||||
assert len(mapping) > 100
|
assert len(mapping) > 100
|
||||||
assert all(isinstance(v, str) and v for v in mapping.values())
|
assert all(isinstance(value, str) and value for value in mapping.values())
|
||||||
|
|
||||||
def test_canproxy_targets_konn3kt(self):
|
def test_canproxy_targets_konn3kt(self):
|
||||||
proxy = read("konn3kt_canproxy.py")
|
proxy = read("konn3kt_canproxy.py")
|
||||||
assert "konn3kt" in proxy
|
assert "konn3kt" in proxy
|
||||||
# the proxy publishes on a local ZMQ 'can' socket that --zmq attaches to
|
|
||||||
assert 'os.environ["ZMQ"] = "1"' in proxy
|
assert 'os.environ["ZMQ"] = "1"' in proxy
|
||||||
|
|
||||||
|
|
||||||
class TestDeviceStreamModes:
|
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):
|
def test_three_modes_exist(self):
|
||||||
header = read("streams/devicestream.h")
|
header = read("streams/devicestream.h")
|
||||||
assert "enum class Mode { Msgq, Zmq, Bridge };" in header
|
assert "enum class Mode { Msgq, Zmq, Bridge };" in header
|
||||||
|
|
||||||
def test_only_zmq_mode_talks_zmq(self):
|
def test_only_zmq_mode_talks_zmq(self):
|
||||||
src = read("streams/devicestream.cc")
|
source = read("streams/devicestream.cc")
|
||||||
assert 'mode_ == Mode::Zmq ? setenv("ZMQ", "1", 1) : unsetenv("ZMQ")' in src
|
assert 'mode_ == Mode::Zmq ? setenv("ZMQ", "1", 1) : unsetenv("ZMQ")' in source
|
||||||
|
|
||||||
def test_zmq_mode_subscribes_to_the_given_address(self):
|
def test_zmq_mode_uses_requested_address(self):
|
||||||
# regression guard: upstream hardcodes 127.0.0.1 and relies on the bridge fork,
|
source = read("streams/devicestream.cc")
|
||||||
# which never reads the canproxy publisher
|
assert 'const std::string socket_address = mode_ == Mode::Zmq ? address_ : "127.0.0.1";' in source
|
||||||
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):
|
def test_only_bridge_mode_forks_bridge(self):
|
||||||
src = read("streams/devicestream.cc")
|
source = read("streams/devicestream.cc")
|
||||||
assert "if (mode_ == Mode::Bridge) {" in src
|
assert "if (mode_ == Mode::Bridge) {" in source
|
||||||
|
|
||||||
|
|
||||||
|
class TestFrontendRemoval:
|
||||||
|
def test_qt_frontend_is_absent(self):
|
||||||
|
assert not (CABANA_DIR / "cabana.cc").exists()
|
||||||
|
assert not (CABANA_DIR / "mainwin.cc").exists()
|
||||||
|
|
||||||
|
def test_imgui_frontend_is_present(self):
|
||||||
|
assert (CABANA_DIR / "ui" / "app.cc").is_file()
|
||||||
|
assert (CABANA_DIR / "ui" / "main.cc").is_file()
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <ctime>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include "common/tests/native_test.h"
|
#include "common/tests/native_test.h"
|
||||||
#include "tools/cabana/dbc/dbcfile.h"
|
#include "tools/cabana/dbc/dbcfile.h"
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
#include "tools/cabana/dbc/dbcmanager.h"
|
||||||
|
#include "tools/cabana/routes.h"
|
||||||
|
#include "tools/cabana/utils/strings.h"
|
||||||
|
|
||||||
const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2";
|
const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2";
|
||||||
|
|
||||||
@@ -30,7 +35,7 @@ void test_generate_dbc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void test_comment_order() {
|
void test_comment_order() {
|
||||||
// Ensure that message comments are followed by signal comments and in the correct order
|
|
||||||
std::string 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
|
SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX
|
||||||
|
|
||||||
@@ -91,7 +96,7 @@ VAL_ 160 signal_1 0 "disabled" 1.2 "initializing" 2 "fault";
|
|||||||
|
|
||||||
CM_ BO_ 160 "message comment" ;
|
CM_ BO_ 160 "message comment" ;
|
||||||
CM_ SG_ 160 signal_1 "signal comment";
|
CM_ SG_ 160 signal_1 "signal comment";
|
||||||
CM_ SG_ 160 signal_2 "multiple line comment
|
CM_ SG_ 160 signal_2 "multiple line comment
|
||||||
1
|
1
|
||||||
2
|
2
|
||||||
";
|
";
|
||||||
@@ -125,9 +130,9 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\"";
|
|||||||
REQUIRE(sig_1->val_desc[2] == std::pair<double, std::string>{2, "fault"});
|
REQUIRE(sig_1->val_desc[2] == std::pair<double, std::string>{2, "fault"});
|
||||||
|
|
||||||
auto &sig_2 = msg->sigs[1];
|
auto &sig_2 = msg->sigs[1];
|
||||||
REQUIRE(sig_2->comment == "multiple line comment \n1\n2");
|
REQUIRE(sig_2->comment == "multiple line comment\n1\n2");
|
||||||
|
|
||||||
|
|
||||||
// multiplexed signals
|
|
||||||
msg = file.msg(162);
|
msg = file.msg(162);
|
||||||
REQUIRE(msg != nullptr);
|
REQUIRE(msg != nullptr);
|
||||||
REQUIRE(msg->sigs.size() == 2);
|
REQUIRE(msg->sigs.size() == 2);
|
||||||
@@ -138,42 +143,20 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\"";
|
|||||||
REQUIRE(msg->sigs[1]->size == 1);
|
REQUIRE(msg->sigs[1]->size == 1);
|
||||||
REQUIRE(msg->sigs[1]->receiver_name == "XXX");
|
REQUIRE(msg->sigs[1]->receiver_name == "XXX");
|
||||||
|
|
||||||
// escaped quotes
|
|
||||||
REQUIRE(msg->comment == "message comment with \"escaped quotes\"");
|
REQUIRE(msg->comment == "message comment with \"escaped quotes\"");
|
||||||
REQUIRE(msg->sigs[0]->comment == "signal comment with \"escaped quotes\"");
|
REQUIRE(msg->sigs[0]->comment == "signal comment with \"escaped quotes\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_parse_iqdbc() {
|
|
||||||
std::vector<std::string> errors;
|
|
||||||
int parsed = 0;
|
|
||||||
for (const auto &entry : std::filesystem::recursive_directory_iterator(OPENDBC_FILE_PATH)) {
|
|
||||||
if (!entry.is_regular_file() || entry.path().extension() != ".dbc") continue;
|
|
||||||
try {
|
|
||||||
auto dbc = DBCFile(entry.path().string());
|
|
||||||
++parsed;
|
|
||||||
} catch (std::exception &e) {
|
|
||||||
errors.push_back(e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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() {
|
void test_dbc_manager() {
|
||||||
DBCManager manager;
|
DBCManager manager;
|
||||||
int files_changed = 0;
|
int files_changed = 0;
|
||||||
int signals_added = 0;
|
int signals_added = 0;
|
||||||
int masks_updated = 0;
|
int masks_updated = 0;
|
||||||
manager.setCallbacks({
|
Connections connections;
|
||||||
.signal_added = [&](MessageId, const cabana::Signal *) { ++signals_added; },
|
connections.push_back(manager.signalAdded.connect([&](MessageId, const cabana::Signal *) { ++signals_added; }));
|
||||||
.file_changed = [&]() { ++files_changed; },
|
connections.push_back(manager.fileChanged.connect([&]() { ++files_changed; }));
|
||||||
.mask_updated = [&]() { ++masks_updated; },
|
connections.push_back(manager.maskUpdated.connect([&]() { ++masks_updated; }));
|
||||||
});
|
|
||||||
|
|
||||||
std::string error;
|
std::string error;
|
||||||
REQUIRE(manager.open(SOURCE_ALL, "test", "BO_ 160 message: 8 XXX\n", &error));
|
REQUIRE(manager.open(SOURCE_ALL, "test", "BO_ 160 message: 8 XXX\n", &error));
|
||||||
@@ -191,14 +174,129 @@ void test_dbc_manager() {
|
|||||||
REQUIRE(manager.msg({.source = 0, .address = 160})->sig("speed") != nullptr);
|
REQUIRE(manager.msg({.source = 0, .address = 160})->sig("speed") != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_format_seconds() {
|
||||||
|
REQUIRE(utils::formatSeconds(0) == "00:00");
|
||||||
|
REQUIRE(utils::formatSeconds(59.4) == "00:59");
|
||||||
|
REQUIRE(utils::formatSeconds(-1) == "00:00");
|
||||||
|
REQUIRE(utils::formatSeconds(61.234, true) == "01:01.234");
|
||||||
|
REQUIRE(utils::formatSeconds(3599.9) == "59:59");
|
||||||
|
REQUIRE(utils::formatSeconds(3601) == "01:00:01");
|
||||||
|
REQUIRE(utils::formatSeconds(3601.5, true) == "01:00:01.500");
|
||||||
|
|
||||||
|
const char *tz = getenv("TZ");
|
||||||
|
const bool had_tz = tz != nullptr;
|
||||||
|
const std::string saved_tz = had_tz ? tz : "";
|
||||||
|
setenv("TZ", "UTC", 1);
|
||||||
|
tzset();
|
||||||
|
REQUIRE(utils::formatSeconds(0, false, true) == "1970-01-01 00:00:00");
|
||||||
|
REQUIRE(utils::formatSeconds(1700000000.123, true, true) == "2023-11-14 22:13:20.123");
|
||||||
|
if (had_tz) {
|
||||||
|
setenv("TZ", saved_tz.c_str(), 1);
|
||||||
|
} else {
|
||||||
|
unsetenv("TZ");
|
||||||
|
}
|
||||||
|
tzset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_to_hex() {
|
||||||
|
REQUIRE(utils::toHex({}) == "");
|
||||||
|
REQUIRE(utils::toHex({0x00, 0x0f, 0xab, 0xff}) == "000FABFF");
|
||||||
|
REQUIRE(utils::toHex({0x01, 0x02, 0x03}, ' ') == "01 02 03");
|
||||||
|
|
||||||
|
REQUIRE(utils::toHexString(0) == "0x00");
|
||||||
|
REQUIRE(utils::toHexString(0xf) == "0x0F");
|
||||||
|
REQUIRE(utils::toHexString(0x1ab) == "0x1AB");
|
||||||
|
REQUIRE(utils::toHexString(0x1fffffff) == "0x1FFFFFFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_signal_tooltip() {
|
||||||
|
cabana::Signal sig{};
|
||||||
|
sig.name = "speed";
|
||||||
|
sig.start_bit = 3;
|
||||||
|
sig.size = 12;
|
||||||
|
sig.msb = 14;
|
||||||
|
sig.lsb = 3;
|
||||||
|
sig.is_little_endian = true;
|
||||||
|
sig.is_signed = false;
|
||||||
|
REQUIRE(utils::signalToolTip(&sig) == R"(
|
||||||
|
speed<br /><span font-size:small">
|
||||||
|
Start Bit: 3 Size: 12<br />
|
||||||
|
MSB: 14 LSB: 3<br />
|
||||||
|
Little Endian: Y Signed: N</span>
|
||||||
|
)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_route_timestamps() {
|
||||||
|
REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05Z") == 1704164645000);
|
||||||
|
REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05") == 1704164645000);
|
||||||
|
REQUIRE(routes::parseIsoToUnixMs("2024-01-02 03:04:05") == 1704164645000);
|
||||||
|
REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.123Z") == 1704164645123);
|
||||||
|
REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.4Z") == 1704164645400);
|
||||||
|
REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.123456Z") == 1704164645123);
|
||||||
|
REQUIRE(routes::parseIsoToUnixMs("") == 0);
|
||||||
|
REQUIRE(routes::parseIsoToUnixMs("not a timestamp") == 0);
|
||||||
|
|
||||||
|
|
||||||
|
const char *tz = getenv("TZ");
|
||||||
|
const std::string prev_tz = tz ? tz : "";
|
||||||
|
setenv("TZ", "UTC", 1);
|
||||||
|
tzset();
|
||||||
|
REQUIRE(routes::formatUnixMs(1704164645123) == "2024-01-02 03:04:05");
|
||||||
|
if (tz) {
|
||||||
|
setenv("TZ", prev_tz.c_str(), 1);
|
||||||
|
} else {
|
||||||
|
unsetenv("TZ");
|
||||||
|
}
|
||||||
|
tzset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_route_api_response() {
|
||||||
|
REQUIRE(routes::checkApiResponse("") == std::make_pair(false, 500));
|
||||||
|
REQUIRE(routes::checkApiResponse("not json") == std::make_pair(false, 500));
|
||||||
|
REQUIRE(routes::checkApiResponse(R"({"error": "unauthorized"})") == std::make_pair(false, 401));
|
||||||
|
REQUIRE(routes::checkApiResponse(R"({"error": "server error"})") == std::make_pair(false, 500));
|
||||||
|
REQUIRE(routes::checkApiResponse("[]") == std::make_pair(true, 0));
|
||||||
|
REQUIRE(routes::checkApiResponse(R"({"dongle_id": "aaaa"})") == std::make_pair(true, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_route_json() {
|
||||||
|
auto devices = routes::parseDevices(R"([{"dongle_id": "aaaa"}, {"dongle_id": "bbbb"}])");
|
||||||
|
REQUIRE(devices.size() == 2);
|
||||||
|
REQUIRE(devices[0].dongle_id == "aaaa");
|
||||||
|
REQUIRE(devices[1].dongle_id == "bbbb");
|
||||||
|
REQUIRE(routes::parseDevices("not json").empty());
|
||||||
|
REQUIRE(routes::parseDevices(R"({"error": "unauthorized"})").empty());
|
||||||
|
|
||||||
|
auto list = routes::parseRoutes(
|
||||||
|
R"([{"fullname": "aaaa|2024-01-02--03-04-05", "start_time_utc_millis": 1704164645000, "end_time_utc_millis": 1704165245000}])", false);
|
||||||
|
REQUIRE(list.size() == 1);
|
||||||
|
REQUIRE(list[0].name == "aaaa|2024-01-02--03-04-05");
|
||||||
|
REQUIRE(list[0].start_ms == 1704164645000);
|
||||||
|
REQUIRE(list[0].end_ms == 1704165245000);
|
||||||
|
|
||||||
|
|
||||||
|
auto preserved = routes::parseRoutes(
|
||||||
|
R"([{"fullname": "aaaa|2024-01-02--03-04-05", "start_time": "2024-01-02T03:04:05Z", "end_time": "2024-01-02T03:14:05Z"}])", true);
|
||||||
|
REQUIRE(preserved.size() == 1);
|
||||||
|
REQUIRE(preserved[0].start_ms == 1704164645000);
|
||||||
|
REQUIRE(preserved[0].end_ms == 1704165245000);
|
||||||
|
|
||||||
|
REQUIRE(routes::parseRoutes("not json", false).empty());
|
||||||
|
}
|
||||||
|
|
||||||
void test_cabana_core() {
|
void test_cabana_core() {
|
||||||
|
test_format_seconds();
|
||||||
|
test_to_hex();
|
||||||
|
test_signal_tooltip();
|
||||||
test_generate_dbc();
|
test_generate_dbc();
|
||||||
test_comment_order();
|
test_comment_order();
|
||||||
test_preserve_original_header();
|
test_preserve_original_header();
|
||||||
test_escaped_quotes();
|
test_escaped_quotes();
|
||||||
test_parse_dbc();
|
test_parse_dbc();
|
||||||
test_parse_iqdbc();
|
|
||||||
test_dbc_manager();
|
test_dbc_manager();
|
||||||
|
test_route_timestamps();
|
||||||
|
test_route_api_response();
|
||||||
|
test_route_json();
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
|
|||||||
11
iqpilot/tools/cabana/tests/test_cabana_ui.py
Normal file
11
iqpilot/tools/cabana/tests/test_cabana_ui.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CABANA_DIR = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
class TestCabanaUi:
|
||||||
|
def test_help(self):
|
||||||
|
result = subprocess.run(["./_cabana", "-h"], cwd=CABANA_DIR, capture_output=True, text=True)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert "Usage:" in result.stderr
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
#include "tools/cabana/tools/findsignal.h"
|
|
||||||
|
|
||||||
#include <set>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QMenu>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
// FindSignalModel
|
|
||||||
|
|
||||||
QVariant FindSignalModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
|
||||||
static QString titles[] = {"Id", "Start Bit, size", "(time, value)"};
|
|
||||||
if (role != Qt::DisplayRole) return {};
|
|
||||||
return orientation == Qt::Horizontal ? titles[section] : QString::number(section + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant FindSignalModel::data(const QModelIndex &index, int role) const {
|
|
||||||
if (role == Qt::DisplayRole) {
|
|
||||||
const auto &s = filtered_signals[index.row()];
|
|
||||||
switch (index.column()) {
|
|
||||||
case 0: return QString::fromStdString(s.id.toString());
|
|
||||||
case 1: return QString("%1, %2").arg(s.sig.start_bit).arg(s.sig.size);
|
|
||||||
case 2: return s.values.join(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindSignalModel::search(std::function<bool(double)> cmp) {
|
|
||||||
beginResetModel();
|
|
||||||
|
|
||||||
std::mutex lock;
|
|
||||||
const auto prev_sigs = !histories.empty() ? histories.back() : initial_signals;
|
|
||||||
filtered_signals.clear();
|
|
||||||
filtered_signals.reserve(prev_sigs.size());
|
|
||||||
|
|
||||||
unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
|
|
||||||
size_t chunk = (prev_sigs.size() + num_threads - 1) / num_threads;
|
|
||||||
std::vector<std::thread> threads;
|
|
||||||
for (unsigned int t = 0; t < num_threads && t * chunk < (size_t)prev_sigs.size(); ++t) {
|
|
||||||
size_t start = t * chunk;
|
|
||||||
size_t end = std::min(start + chunk, (size_t)prev_sigs.size());
|
|
||||||
threads.emplace_back([&, start, end]() {
|
|
||||||
for (size_t i = start; i < end; ++i) {
|
|
||||||
const auto &s = prev_sigs[i];
|
|
||||||
const auto &events = can->events(s.id);
|
|
||||||
auto first = std::upper_bound(events.cbegin(), events.cend(), s.mono_time, CompareCanEvent());
|
|
||||||
auto last = events.cend();
|
|
||||||
if (last_time < std::numeric_limits<uint64_t>::max()) {
|
|
||||||
last = std::upper_bound(events.cbegin(), events.cend(), last_time, CompareCanEvent());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto it = std::find_if(first, last, [&](const CanEvent *e) { return cmp(get_raw_value(e->dat, e->size, s.sig)); });
|
|
||||||
if (it != last) {
|
|
||||||
auto values = s.values;
|
|
||||||
values += QString("(%1, %2)").arg(can->toSeconds((*it)->mono_time), 0, 'f', 3).arg(get_raw_value((*it)->dat, (*it)->size, s.sig));
|
|
||||||
std::lock_guard lk(lock);
|
|
||||||
filtered_signals.push_back({.id = s.id, .mono_time = (*it)->mono_time, .sig = s.sig, .values = values});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (auto &th : threads) th.join();
|
|
||||||
|
|
||||||
histories.push_back(filtered_signals);
|
|
||||||
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindSignalModel::undo() {
|
|
||||||
if (!histories.empty()) {
|
|
||||||
beginResetModel();
|
|
||||||
histories.pop_back();
|
|
||||||
filtered_signals.clear();
|
|
||||||
if (!histories.empty()) filtered_signals = histories.back();
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindSignalModel::reset() {
|
|
||||||
beginResetModel();
|
|
||||||
histories.clear();
|
|
||||||
filtered_signals.clear();
|
|
||||||
initial_signals.clear();
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindSignalDlg
|
|
||||||
FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags() | Qt::Window) {
|
|
||||||
setWindowTitle(tr("Find Signal"));
|
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
|
|
||||||
// Messages group
|
|
||||||
message_group = new QGroupBox(tr("Messages"), this);
|
|
||||||
QFormLayout *message_layout = new QFormLayout(message_group);
|
|
||||||
message_layout->addRow(tr("Bus"), bus_edit = new QLineEdit());
|
|
||||||
bus_edit->setPlaceholderText(tr("comma-separated values. Leave blank for all"));
|
|
||||||
message_layout->addRow(tr("Address"), address_edit = new QLineEdit());
|
|
||||||
address_edit->setPlaceholderText(tr("comma-separated hex values. Leave blank for all"));
|
|
||||||
QHBoxLayout *hlayout = new QHBoxLayout();
|
|
||||||
hlayout->addWidget(first_time_edit = new QLineEdit("0"));
|
|
||||||
hlayout->addWidget(new QLabel("-"));
|
|
||||||
hlayout->addWidget(last_time_edit = new QLineEdit("MAX"));
|
|
||||||
hlayout->addWidget(new QLabel("seconds"));
|
|
||||||
hlayout->addStretch(0);
|
|
||||||
message_layout->addRow(tr("Time"), hlayout);
|
|
||||||
|
|
||||||
// Signal group
|
|
||||||
properties_group = new QGroupBox(tr("Signal"));
|
|
||||||
QFormLayout *property_layout = new QFormLayout(properties_group);
|
|
||||||
property_layout->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
|
|
||||||
|
|
||||||
hlayout = new QHBoxLayout();
|
|
||||||
hlayout->addWidget(min_size = new QSpinBox);
|
|
||||||
hlayout->addWidget(new QLabel("-"));
|
|
||||||
hlayout->addWidget(max_size = new QSpinBox);
|
|
||||||
hlayout->addWidget(litter_endian = new QCheckBox(tr("Little endian")));
|
|
||||||
hlayout->addWidget(is_signed = new QCheckBox(tr("Signed")));
|
|
||||||
hlayout->addStretch(0);
|
|
||||||
min_size->setRange(1, 64);
|
|
||||||
max_size->setRange(1, 64);
|
|
||||||
min_size->setValue(8);
|
|
||||||
max_size->setValue(8);
|
|
||||||
litter_endian->setChecked(true);
|
|
||||||
property_layout->addRow(tr("Size"), hlayout);
|
|
||||||
property_layout->addRow(tr("Factor"), factor_edit = new QLineEdit("1.0"));
|
|
||||||
property_layout->addRow(tr("Offset"), offset_edit = new QLineEdit("0.0"));
|
|
||||||
|
|
||||||
// find group
|
|
||||||
QGroupBox *find_group = new QGroupBox(tr("Find signal"), this);
|
|
||||||
QVBoxLayout *vlayout = new QVBoxLayout(find_group);
|
|
||||||
hlayout = new QHBoxLayout();
|
|
||||||
hlayout->addWidget(new QLabel(tr("Value")));
|
|
||||||
hlayout->addWidget(compare_cb = new QComboBox(this));
|
|
||||||
hlayout->addWidget(value1 = new QLineEdit);
|
|
||||||
hlayout->addWidget(to_label = new QLabel("-"));
|
|
||||||
hlayout->addWidget(value2 = new QLineEdit);
|
|
||||||
hlayout->addWidget(undo_btn = new QPushButton(tr("Undo prev find"), this));
|
|
||||||
hlayout->addWidget(search_btn = new QPushButton(tr("Find")));
|
|
||||||
hlayout->addWidget(reset_btn = new QPushButton(tr("Reset"), this));
|
|
||||||
vlayout->addLayout(hlayout);
|
|
||||||
|
|
||||||
compare_cb->addItems({"=", ">", ">=", "!=", "<", "<=", "between"});
|
|
||||||
value1->setFocus(Qt::OtherFocusReason);
|
|
||||||
value2->setVisible(false);
|
|
||||||
to_label->setVisible(false);
|
|
||||||
undo_btn->setEnabled(false);
|
|
||||||
reset_btn->setEnabled(false);
|
|
||||||
|
|
||||||
auto double_validator = new DoubleValidator(this);
|
|
||||||
for (auto edit : {value1, value2, factor_edit, offset_edit, first_time_edit, last_time_edit}) {
|
|
||||||
edit->setValidator(double_validator);
|
|
||||||
}
|
|
||||||
|
|
||||||
vlayout->addWidget(view = new QTableView(this));
|
|
||||||
view->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
||||||
view->horizontalHeader()->setStretchLastSection(true);
|
|
||||||
view->horizontalHeader()->setSelectionMode(QAbstractItemView::NoSelection);
|
|
||||||
view->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
||||||
view->setModel(model = new FindSignalModel(this));
|
|
||||||
|
|
||||||
hlayout = new QHBoxLayout();
|
|
||||||
hlayout->addWidget(message_group);
|
|
||||||
hlayout->addWidget(properties_group);
|
|
||||||
main_layout->addLayout(hlayout);
|
|
||||||
main_layout->addWidget(find_group);
|
|
||||||
main_layout->addWidget(stats_label = new QLabel());
|
|
||||||
|
|
||||||
setMinimumSize({700, 650});
|
|
||||||
QObject::connect(search_btn, &QPushButton::clicked, this, &FindSignalDlg::search);
|
|
||||||
QObject::connect(undo_btn, &QPushButton::clicked, model, &FindSignalModel::undo);
|
|
||||||
QObject::connect(model, &QAbstractItemModel::modelReset, this, &FindSignalDlg::modelReset);
|
|
||||||
QObject::connect(reset_btn, &QPushButton::clicked, model, &FindSignalModel::reset);
|
|
||||||
QObject::connect(view, &QTableView::customContextMenuRequested, this, &FindSignalDlg::customMenuRequested);
|
|
||||||
QObject::connect(view, &QTableView::doubleClicked, [this](const QModelIndex &index) {
|
|
||||||
if (index.isValid()) emit openMessage(model->filtered_signals[index.row()].id);
|
|
||||||
});
|
|
||||||
QObject::connect(compare_cb, qOverload<int>(&QComboBox::currentIndexChanged), [=](int index) {
|
|
||||||
to_label->setVisible(index == compare_cb->count() - 1);
|
|
||||||
value2->setVisible(index == compare_cb->count() - 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindSignalDlg::search() {
|
|
||||||
if (model->histories.empty()) {
|
|
||||||
setInitialSignals();
|
|
||||||
}
|
|
||||||
auto v1 = value1->text().toDouble();
|
|
||||||
auto v2 = value2->text().toDouble();
|
|
||||||
std::function<bool(double)> cmp = nullptr;
|
|
||||||
switch (compare_cb->currentIndex()) {
|
|
||||||
case 0: cmp = [v1](double v) { return v == v1;}; break;
|
|
||||||
case 1: cmp = [v1](double v) { return v > v1;}; break;
|
|
||||||
case 2: cmp = [v1](double v) { return v >= v1;}; break;
|
|
||||||
case 3: cmp = [v1](double v) { return v != v1;}; break;
|
|
||||||
case 4: cmp = [v1](double v) { return v < v1;}; break;
|
|
||||||
case 5: cmp = [v1](double v) { return v <= v1;}; break;
|
|
||||||
case 6: cmp = [v1, v2](double v) { return v >= v1 && v <= v2;}; break;
|
|
||||||
}
|
|
||||||
properties_group->setEnabled(false);
|
|
||||||
message_group->setEnabled(false);
|
|
||||||
search_btn->setEnabled(false);
|
|
||||||
stats_label->setVisible(false);
|
|
||||||
search_btn->setText("Finding ....");
|
|
||||||
QTimer::singleShot(0, this, [=]() { model->search(cmp); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindSignalDlg::setInitialSignals() {
|
|
||||||
std::set<ushort> buses;
|
|
||||||
for (auto bus : bus_edit->text().trimmed().split(",")) {
|
|
||||||
bus = bus.trimmed();
|
|
||||||
if (!bus.isEmpty()) buses.insert(bus.toUShort());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<uint32_t> addresses;
|
|
||||||
for (auto addr : address_edit->text().trimmed().split(",")) {
|
|
||||||
addr = addr.trimmed();
|
|
||||||
if (!addr.isEmpty()) addresses.insert(addr.toULong(nullptr, 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
cabana::Signal sig{};
|
|
||||||
sig.is_little_endian = litter_endian->isChecked();
|
|
||||||
sig.is_signed = is_signed->isChecked();
|
|
||||||
sig.factor = factor_edit->text().toDouble();
|
|
||||||
sig.offset = offset_edit->text().toDouble();
|
|
||||||
|
|
||||||
double first_time_val = first_time_edit->text().toDouble();
|
|
||||||
double last_time_val = last_time_edit->text().toDouble();
|
|
||||||
auto [first_sec, last_sec] = std::minmax(first_time_val, last_time_val);
|
|
||||||
uint64_t first_time = can->toMonoTime(first_sec);
|
|
||||||
model->last_time = std::numeric_limits<uint64_t>::max();
|
|
||||||
if (last_sec > 0) {
|
|
||||||
model->last_time = can->toMonoTime(last_sec);
|
|
||||||
}
|
|
||||||
model->initial_signals.clear();
|
|
||||||
|
|
||||||
for (const auto &[id, m] : can->lastMessages()) {
|
|
||||||
if ((buses.empty() || buses.count(id.source)) && (addresses.empty() || addresses.count(id.address))) {
|
|
||||||
const auto &events = can->events(id);
|
|
||||||
auto e = std::lower_bound(events.cbegin(), events.cend(), first_time, CompareCanEvent());
|
|
||||||
if (e != events.cend()) {
|
|
||||||
const int total_size = m.dat.size() * 8;
|
|
||||||
for (int size = min_size->value(); size <= max_size->value(); ++size) {
|
|
||||||
for (int start = 0; start <= total_size - size; ++start) {
|
|
||||||
FindSignalModel::SearchSignal s{.id = id, .mono_time = first_time, .sig = sig};
|
|
||||||
s.sig.start_bit = start;
|
|
||||||
s.sig.size = size;
|
|
||||||
updateMsbLsb(s.sig);
|
|
||||||
s.value = get_raw_value((*e)->dat, (*e)->size, s.sig);
|
|
||||||
model->initial_signals.push_back(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindSignalDlg::modelReset() {
|
|
||||||
properties_group->setEnabled(model->histories.empty());
|
|
||||||
message_group->setEnabled(model->histories.empty());
|
|
||||||
search_btn->setText(model->histories.empty() ? tr("Find") : tr("Find Next"));
|
|
||||||
reset_btn->setEnabled(!model->histories.empty());
|
|
||||||
undo_btn->setEnabled(model->histories.size() > 1);
|
|
||||||
search_btn->setEnabled(model->rowCount() > 0 || model->histories.empty());
|
|
||||||
stats_label->setVisible(true);
|
|
||||||
stats_label->setText(tr("%1 matches. right click on an item to create signal. double click to open message").arg(model->filtered_signals.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindSignalDlg::customMenuRequested(const QPoint &pos) {
|
|
||||||
if (auto index = view->indexAt(pos); index.isValid()) {
|
|
||||||
QMenu menu(this);
|
|
||||||
menu.addAction(tr("Create Signal"));
|
|
||||||
if (menu.exec(view->mapToGlobal(pos))) {
|
|
||||||
auto &s = model->filtered_signals[index.row()];
|
|
||||||
UndoStack::instance()->push(new AddSigCommand(s.id, s.sig));
|
|
||||||
emit openMessage(s.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <limits>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QAbstractTableModel>
|
|
||||||
#include <QCheckBox>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QTableView>
|
|
||||||
|
|
||||||
#include "tools/cabana/commands.h"
|
|
||||||
#include "tools/cabana/settings.h"
|
|
||||||
|
|
||||||
class FindSignalModel : public QAbstractTableModel {
|
|
||||||
public:
|
|
||||||
struct SearchSignal {
|
|
||||||
MessageId id = {};
|
|
||||||
uint64_t mono_time = 0;
|
|
||||||
cabana::Signal sig = {};
|
|
||||||
double value = 0.;
|
|
||||||
QStringList values;
|
|
||||||
};
|
|
||||||
|
|
||||||
FindSignalModel(QObject *parent) : QAbstractTableModel(parent) {}
|
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 3; }
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return std::min((int)filtered_signals.size(), 300); }
|
|
||||||
void search(std::function<bool(double)> cmp);
|
|
||||||
void reset();
|
|
||||||
void undo();
|
|
||||||
|
|
||||||
std::vector<SearchSignal> filtered_signals;
|
|
||||||
std::vector<SearchSignal> initial_signals;
|
|
||||||
std::vector<std::vector<SearchSignal>> histories;
|
|
||||||
uint64_t last_time = std::numeric_limits<uint64_t>::max();
|
|
||||||
};
|
|
||||||
|
|
||||||
class FindSignalDlg : public QDialog {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
FindSignalDlg(QWidget *parent);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void openMessage(const MessageId &id);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void search();
|
|
||||||
void modelReset();
|
|
||||||
void setInitialSignals();
|
|
||||||
void customMenuRequested(const QPoint &pos);
|
|
||||||
|
|
||||||
QLineEdit *value1, *value2, *factor_edit, *offset_edit;
|
|
||||||
QLineEdit *bus_edit, *address_edit, *first_time_edit, *last_time_edit;
|
|
||||||
QComboBox *compare_cb;
|
|
||||||
QSpinBox *min_size, *max_size;
|
|
||||||
QCheckBox *litter_endian, *is_signed;
|
|
||||||
QPushButton *search_btn, *reset_btn, *undo_btn;
|
|
||||||
QGroupBox *properties_group, *message_group;
|
|
||||||
QTableView *view;
|
|
||||||
QLabel *to_label, *stats_label;
|
|
||||||
FindSignalModel *model;
|
|
||||||
};
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
#include "tools/cabana/tools/findsimilarbits.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include <QGridLayout>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QIntValidator>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QRadioButton>
|
|
||||||
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
#include "tools/cabana/streams/abstractstream.h"
|
|
||||||
|
|
||||||
FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags() | Qt::Window) {
|
|
||||||
setWindowTitle(tr("Find similar bits"));
|
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
|
|
||||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
|
||||||
|
|
||||||
QHBoxLayout *src_layout = new QHBoxLayout();
|
|
||||||
src_bus_combo = new QComboBox(this);
|
|
||||||
find_bus_combo = new QComboBox(this);
|
|
||||||
for (auto cb : {src_bus_combo, find_bus_combo}) {
|
|
||||||
for (uint8_t bus : can->sources) {
|
|
||||||
cb->addItem(QString::number(bus), bus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg_cb = new QComboBox(this);
|
|
||||||
// TODO: update when src_bus_combo changes
|
|
||||||
for (auto &[address, msg] : dbc()->getMessages(-1)) {
|
|
||||||
msg_cb->addItem(QString::fromStdString(msg.name), address);
|
|
||||||
}
|
|
||||||
msg_cb->model()->sort(0);
|
|
||||||
msg_cb->setCurrentIndex(0);
|
|
||||||
|
|
||||||
byte_idx_sb = new QSpinBox(this);
|
|
||||||
byte_idx_sb->setFixedWidth(50);
|
|
||||||
byte_idx_sb->setRange(0, 63);
|
|
||||||
|
|
||||||
bit_idx_sb = new QSpinBox(this);
|
|
||||||
bit_idx_sb->setFixedWidth(50);
|
|
||||||
bit_idx_sb->setRange(0, 7);
|
|
||||||
|
|
||||||
src_layout->addWidget(new QLabel(tr("Bus")));
|
|
||||||
src_layout->addWidget(src_bus_combo);
|
|
||||||
src_layout->addWidget(msg_cb);
|
|
||||||
src_layout->addWidget(new QLabel(tr("Byte Index")));
|
|
||||||
src_layout->addWidget(byte_idx_sb);
|
|
||||||
src_layout->addWidget(new QLabel(tr("Bit Index")));
|
|
||||||
src_layout->addWidget(bit_idx_sb);
|
|
||||||
src_layout->addStretch(0);
|
|
||||||
|
|
||||||
QHBoxLayout *find_layout = new QHBoxLayout();
|
|
||||||
find_layout->addWidget(new QLabel(tr("Bus")));
|
|
||||||
find_layout->addWidget(find_bus_combo);
|
|
||||||
find_layout->addWidget(new QLabel(tr("Equal")));
|
|
||||||
equal_combo = new QComboBox(this);
|
|
||||||
equal_combo->addItems({"Yes", "No"});
|
|
||||||
find_layout->addWidget(equal_combo);
|
|
||||||
min_msgs = new QLineEdit(this);
|
|
||||||
min_msgs->setValidator(new QIntValidator(this));
|
|
||||||
min_msgs->setText("100");
|
|
||||||
find_layout->addWidget(new QLabel(tr("Min msg count")));
|
|
||||||
find_layout->addWidget(min_msgs);
|
|
||||||
search_btn = new QPushButton(tr("&Find"), this);
|
|
||||||
find_layout->addWidget(search_btn);
|
|
||||||
find_layout->addStretch(0);
|
|
||||||
|
|
||||||
QGridLayout *grid_layout = new QGridLayout();
|
|
||||||
grid_layout->addWidget(new QLabel("Find From:"), 0, 0);
|
|
||||||
grid_layout->addLayout(src_layout, 0, 1);
|
|
||||||
grid_layout->addWidget(new QLabel("Find In:"), 1, 0);
|
|
||||||
grid_layout->addLayout(find_layout, 1, 1);
|
|
||||||
main_layout->addLayout(grid_layout);
|
|
||||||
|
|
||||||
table = new QTableWidget(this);
|
|
||||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
||||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
||||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
||||||
table->horizontalHeader()->setStretchLastSection(true);
|
|
||||||
main_layout->addWidget(table);
|
|
||||||
|
|
||||||
setMinimumSize({700, 500});
|
|
||||||
QObject::connect(search_btn, &QPushButton::clicked, this, &FindSimilarBitsDlg::find);
|
|
||||||
QObject::connect(table, &QTableWidget::doubleClicked, [this](const QModelIndex &index) {
|
|
||||||
if (index.isValid()) {
|
|
||||||
MessageId msg_id = {.source = (uint8_t)find_bus_combo->currentData().toUInt(), .address = table->item(index.row(), 0)->text().toUInt(0, 16)};
|
|
||||||
emit openMessage(msg_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindSimilarBitsDlg::find() {
|
|
||||||
search_btn->setEnabled(false);
|
|
||||||
table->clear();
|
|
||||||
uint32_t selected_address = msg_cb->currentData().toUInt();
|
|
||||||
auto msg_mismatched = calcBits(src_bus_combo->currentText().toUInt(), selected_address, byte_idx_sb->value(), bit_idx_sb->value(),
|
|
||||||
find_bus_combo->currentText().toUInt(), equal_combo->currentIndex() == 0, min_msgs->text().toInt());
|
|
||||||
table->setRowCount(msg_mismatched.size());
|
|
||||||
table->setColumnCount(6);
|
|
||||||
table->setHorizontalHeaderLabels({"address", "byte idx", "bit idx", "mismatches", "total msgs", "% mismatched"});
|
|
||||||
for (int i = 0; i < msg_mismatched.size(); ++i) {
|
|
||||||
auto &m = msg_mismatched[i];
|
|
||||||
table->setItem(i, 0, new QTableWidgetItem(QString("%1").arg(m.address, 1, 16)));
|
|
||||||
table->setItem(i, 1, new QTableWidgetItem(QString::number(m.byte_idx)));
|
|
||||||
table->setItem(i, 2, new QTableWidgetItem(QString::number(m.bit_idx)));
|
|
||||||
table->setItem(i, 3, new QTableWidgetItem(QString::number(m.mismatches)));
|
|
||||||
table->setItem(i, 4, new QTableWidgetItem(QString::number(m.total)));
|
|
||||||
table->setItem(i, 5, new QTableWidgetItem(QString::number(m.perc, 'f', 2)));
|
|
||||||
}
|
|
||||||
search_btn->setEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<FindSimilarBitsDlg::mismatched_struct> FindSimilarBitsDlg::calcBits(uint8_t bus, uint32_t selected_address, int byte_idx,
|
|
||||||
int bit_idx, uint8_t find_bus, bool equal, int min_msgs_cnt) {
|
|
||||||
std::unordered_map<uint32_t, std::vector<uint32_t>> mismatches;
|
|
||||||
std::unordered_map<uint32_t, uint32_t> msg_count;
|
|
||||||
const auto &events = can->allEvents();
|
|
||||||
int bit_to_find = -1;
|
|
||||||
for (const CanEvent *e : events) {
|
|
||||||
if (e->src == bus) {
|
|
||||||
if (e->address == selected_address && e->size > byte_idx) {
|
|
||||||
bit_to_find = ((e->dat[byte_idx] >> (7 - bit_idx)) & 1) != 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e->src == find_bus) {
|
|
||||||
++msg_count[e->address];
|
|
||||||
if (bit_to_find == -1) continue;
|
|
||||||
|
|
||||||
auto &mismatched = mismatches[e->address];
|
|
||||||
if (mismatched.size() < e->size * 8) {
|
|
||||||
mismatched.resize(e->size * 8);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < e->size; ++i) {
|
|
||||||
for (int j = 0; j < 8; ++j) {
|
|
||||||
int bit = ((e->dat[i] >> (7 - j)) & 1) != 0;
|
|
||||||
mismatched[i * 8 + j] += equal ? (bit != bit_to_find) : (bit == bit_to_find);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<mismatched_struct> result;
|
|
||||||
result.reserve(mismatches.size());
|
|
||||||
for (auto it = mismatches.begin(); it != mismatches.end(); ++it) {
|
|
||||||
if (auto cnt = msg_count[it->first]; cnt > (uint32_t)min_msgs_cnt) {
|
|
||||||
auto &mismatched = it->second;
|
|
||||||
for (int i = 0; i < (int)mismatched.size(); ++i) {
|
|
||||||
if (float perc = (mismatched[i] / (double)cnt) * 100; perc < 50) {
|
|
||||||
result.push_back({it->first, (uint32_t)i / 8, (uint32_t)i % 8, mismatched[i], cnt, perc});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::sort(result.begin(), result.end(), [](auto &l, auto &r) { return l.perc < r.perc; });
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QComboBox>
|
|
||||||
#include <QDialog>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QSpinBox>
|
|
||||||
#include <QTableWidget>
|
|
||||||
|
|
||||||
#include "tools/cabana/dbc/dbcmanager.h"
|
|
||||||
|
|
||||||
class FindSimilarBitsDlg : public QDialog {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
FindSimilarBitsDlg(QWidget *parent);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void openMessage(const MessageId &msg_id);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct mismatched_struct {
|
|
||||||
uint32_t address, byte_idx, bit_idx, mismatches, total;
|
|
||||||
float perc;
|
|
||||||
};
|
|
||||||
std::vector<mismatched_struct> calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, uint8_t find_bus,
|
|
||||||
bool equal, int min_msgs_cnt);
|
|
||||||
void find();
|
|
||||||
|
|
||||||
QTableWidget *table;
|
|
||||||
QComboBox *src_bus_combo, *find_bus_combo, *msg_cb, *equal_combo;
|
|
||||||
QSpinBox *byte_idx_sb, *bit_idx_sb;
|
|
||||||
QPushButton *search_btn;
|
|
||||||
QLineEdit *min_msgs;
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#include "tools/cabana/tools/routeinfo.h"
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QScrollBar>
|
|
||||||
#include <QTableWidget>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include "tools/cabana/streams/replaystream.h"
|
|
||||||
|
|
||||||
RouteInfoDlg::RouteInfoDlg(QWidget *parent) : QDialog(parent) {
|
|
||||||
auto *replay = qobject_cast<ReplayStream *>(can)->getReplay();
|
|
||||||
setWindowTitle(tr("Route: %1").arg(QString::fromStdString(replay->route().name())));
|
|
||||||
|
|
||||||
auto *table = new QTableWidget(replay->route().segments().size(), 7, this);
|
|
||||||
table->setToolTip(tr("Click on a row to seek to the corresponding segment."));
|
|
||||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
||||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
||||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
||||||
table->setHorizontalHeaderLabels({"", "rlog", "fcam", "ecam", "dcam", "qlog", "qcam"});
|
|
||||||
table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
|
||||||
table->verticalHeader()->setVisible(false);
|
|
||||||
table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
|
|
||||||
int row = 0;
|
|
||||||
for (const auto &[seg_num, seg] : replay->route().segments()) {
|
|
||||||
table->setItem(row, 0, new QTableWidgetItem(QString::number(seg_num)));
|
|
||||||
table->setItem(row, 1, new QTableWidgetItem(seg.rlog.empty() ? "--" : "Yes"));
|
|
||||||
table->setItem(row, 2, new QTableWidgetItem(seg.road_cam.empty() ? "--" : "Yes"));
|
|
||||||
table->setItem(row, 3, new QTableWidgetItem(seg.wide_road_cam.empty() ? "--" : "Yes"));
|
|
||||||
table->setItem(row, 4, new QTableWidgetItem(seg.driver_cam.empty() ? "--" : "Yes"));
|
|
||||||
table->setItem(row, 5, new QTableWidgetItem(seg.qlog.empty() ? "--" : "Yes"));
|
|
||||||
table->setItem(row, 6, new QTableWidgetItem(seg.qcamera.empty() ? "--" : "Yes"));
|
|
||||||
++row;
|
|
||||||
}
|
|
||||||
table->setMinimumWidth(table->horizontalHeader()->length() + table->verticalScrollBar()->sizeHint().width());
|
|
||||||
table->setMinimumHeight(table->rowHeight(0) * std::min(table->rowCount(), 13) + table->horizontalHeader()->height() + table->frameWidth() * 2);
|
|
||||||
|
|
||||||
connect(table, &QTableWidget::itemClicked, [](QTableWidgetItem *item) { can->seekTo(item->row() * 60.0); });
|
|
||||||
|
|
||||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
|
||||||
layout->addWidget(table);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <QDialog>
|
|
||||||
|
|
||||||
class RouteInfoDlg : public QDialog {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
RouteInfoDlg(QWidget *parent = nullptr);
|
|
||||||
};
|
|
||||||
212
iqpilot/tools/cabana/ui/app.cc
Normal file
212
iqpilot/tools/cabana/ui/app.cc
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
#include "tools/cabana/ui/app.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_impl_glfw.h"
|
||||||
|
#include "imgui_impl_opengl3.h"
|
||||||
|
#include "imgui_impl_opengl3_loader.h"
|
||||||
|
#include "implot.h"
|
||||||
|
#include <GLFW/glfw3.h>
|
||||||
|
|
||||||
|
#include "tools/cabana/settings.h"
|
||||||
|
#include "tools/cabana/ui/inistate.h"
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
#include "tools/cabana/ui/mainwin.h"
|
||||||
|
#include "tools/cabana/utils/util.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::atomic<bool> g_signal_exit{false};
|
||||||
|
std::vector<KeyEvent> g_key_events;
|
||||||
|
void keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) {
|
||||||
|
ImGui_ImplGlfw_KeyCallback(window, key, scancode, action, mods);
|
||||||
|
if (action == GLFW_PRESS) g_key_events.push_back({key, mods});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GLFWwindow *g_focus_lost_window = nullptr;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void windowFocusCallback(GLFWwindow *w, int f) {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
ImGui_ImplGlfw_WindowFocusCallback(w, f);
|
||||||
|
#else
|
||||||
|
if (f) {
|
||||||
|
g_focus_lost_window = nullptr;
|
||||||
|
ImGui_ImplGlfw_WindowFocusCallback(w, f);
|
||||||
|
} else {
|
||||||
|
g_focus_lost_window = w;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
bool anyMouseButtonDown(GLFWwindow *w) {
|
||||||
|
for (int b = GLFW_MOUSE_BUTTON_1; b <= GLFW_MOUSE_BUTTON_LAST; ++b) {
|
||||||
|
if (glfwGetMouseButton(w, b) == GLFW_PRESS) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void deliverPendingFocusLoss() {
|
||||||
|
if (g_focus_lost_window == nullptr || anyMouseButtonDown(g_focus_lost_window)) return;
|
||||||
|
ImGui_ImplGlfw_WindowFocusCallback(g_focus_lost_window, GLFW_FALSE);
|
||||||
|
g_focus_lost_window = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void hookViewportCallbacks() {
|
||||||
|
for (ImGuiViewport *viewport : ImGui::GetPlatformIO().Viewports) {
|
||||||
|
if (viewport->PlatformHandle == nullptr || viewport == ImGui::GetMainViewport()) continue;
|
||||||
|
glfwSetKeyCallback((GLFWwindow *)viewport->PlatformHandle, keyCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void glfwErrorCallback(int error, const char *description) {
|
||||||
|
fprintf(stderr, "GLFW error %d: %s\n", error, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void renderFrame(GLFWwindow *window, MainWindow *win) {
|
||||||
|
glfwPollEvents();
|
||||||
|
deliverPendingFocusLoss();
|
||||||
|
utils::drainMainThreadQueue();
|
||||||
|
|
||||||
|
int fb_w = 0, fb_h = 0;
|
||||||
|
glfwGetFramebufferSize(window, &fb_w, &fb_h);
|
||||||
|
|
||||||
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
|
ImGui_ImplGlfw_NewFrame();
|
||||||
|
ImGui::NewFrame();
|
||||||
|
win->draw();
|
||||||
|
ImGui::Render();
|
||||||
|
|
||||||
|
const ImVec4 &bg = ImGui::GetStyle().Colors[ImGuiCol_WindowBg];
|
||||||
|
glViewport(0, 0, fb_w, fb_h);
|
||||||
|
glClearColor(bg.x, bg.y, bg.z, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||||
|
|
||||||
|
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||||
|
GLFWwindow *backup_context = glfwGetCurrentContext();
|
||||||
|
ImGui::UpdatePlatformWindows();
|
||||||
|
hookViewportCallbacks();
|
||||||
|
ImGui::RenderPlatformWindowsDefault();
|
||||||
|
glfwMakeContextCurrent(backup_context);
|
||||||
|
}
|
||||||
|
glfwSwapBuffers(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlfwRuntime {
|
||||||
|
public:
|
||||||
|
GlfwRuntime() {
|
||||||
|
glfwSetErrorCallback(glfwErrorCallback);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
setMacAppName("Cabana");
|
||||||
|
#endif
|
||||||
|
if (!glfwInit()) throw std::runtime_error("glfwInit failed");
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||||
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
|
||||||
|
#endif
|
||||||
|
window_ = glfwCreateWindow(1600, 900, "Cabana", nullptr, nullptr);
|
||||||
|
if (window_ == nullptr) {
|
||||||
|
glfwTerminate();
|
||||||
|
throw std::runtime_error("glfwCreateWindow failed");
|
||||||
|
}
|
||||||
|
glfwMakeContextCurrent(window_);
|
||||||
|
glfwSwapInterval(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
~GlfwRuntime() {
|
||||||
|
if (window_ != nullptr) glfwDestroyWindow(window_);
|
||||||
|
glfwTerminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
GlfwRuntime(const GlfwRuntime &) = delete;
|
||||||
|
GlfwRuntime &operator=(const GlfwRuntime &) = delete;
|
||||||
|
GLFWwindow *window() const { return window_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
GLFWwindow *window_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImGuiRuntime {
|
||||||
|
public:
|
||||||
|
explicit ImGuiRuntime(GLFWwindow *window) {
|
||||||
|
IMGUI_CHECKVERSION();
|
||||||
|
ImGui::CreateContext();
|
||||||
|
ImPlot::CreateContext();
|
||||||
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
||||||
|
io.ConfigViewportsNoDecoration = false;
|
||||||
|
io.IniFilename = nullptr;
|
||||||
|
io.LogFilename = nullptr;
|
||||||
|
if (!ImGui_ImplGlfw_InitForOpenGL(window, true)) {
|
||||||
|
ImPlot::DestroyContext();
|
||||||
|
ImGui::DestroyContext();
|
||||||
|
throw std::runtime_error("ImGui_ImplGlfw_InitForOpenGL failed");
|
||||||
|
}
|
||||||
|
glfwSetKeyCallback(window, keyCallback);
|
||||||
|
glfwSetWindowFocusCallback(window, windowFocusCallback);
|
||||||
|
if (!ImGui_ImplOpenGL3_Init("#version 330")) {
|
||||||
|
ImGui_ImplGlfw_Shutdown();
|
||||||
|
ImPlot::DestroyContext();
|
||||||
|
ImGui::DestroyContext();
|
||||||
|
throw std::runtime_error("ImGui_ImplOpenGL3_Init failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~ImGuiRuntime() {
|
||||||
|
ImGui_ImplOpenGL3_Shutdown();
|
||||||
|
ImGui_ImplGlfw_Shutdown();
|
||||||
|
ImPlot::DestroyContext();
|
||||||
|
ImGui::DestroyContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiRuntime(const ImGuiRuntime &) = delete;
|
||||||
|
ImGuiRuntime &operator=(const ImGuiRuntime &) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<KeyEvent> takeKeyEvents() {
|
||||||
|
return std::exchange(g_key_events, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
int run(std::unique_ptr<AbstractStream> stream, StreamLoader stream_loader, const std::string &dbc_file) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
UnixSignalHandler signal_handler([]() { g_signal_exit = true; });
|
||||||
|
|
||||||
|
GlfwRuntime glfw;
|
||||||
|
ImGuiRuntime imgui(glfw.window());
|
||||||
|
loadFonts();
|
||||||
|
applyTheme(settings.theme);
|
||||||
|
inistate::addSettingsHandler();
|
||||||
|
inistate::load();
|
||||||
|
inistate::applyWindowGeometry(glfw.window());
|
||||||
|
|
||||||
|
MainWindow win(glfw.window(), std::move(stream), std::move(stream_loader), dbc_file);
|
||||||
|
while (!win.exited()) {
|
||||||
|
if (g_signal_exit.exchange(false)) {
|
||||||
|
printf("\nexiting...\n");
|
||||||
|
win.close();
|
||||||
|
} else if (glfwWindowShouldClose(glfw.window())) {
|
||||||
|
glfwSetWindowShouldClose(glfw.window(), GLFW_FALSE);
|
||||||
|
win.close();
|
||||||
|
}
|
||||||
|
renderFrame(glfw.window(), &win);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
fprintf(stderr, "%s\n", e.what());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
iqpilot/tools/cabana/ui/app.h
Normal file
23
iqpilot/tools/cabana/ui/app.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
|
|
||||||
|
|
||||||
|
using StreamLoader = std::function<std::unique_ptr<AbstractStream>()>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int run(std::unique_ptr<AbstractStream> stream, StreamLoader stream_loader, const std::string &dbc_file);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct KeyEvent {
|
||||||
|
int key;
|
||||||
|
int mods;
|
||||||
|
};
|
||||||
|
std::vector<KeyEvent> takeKeyEvents();
|
||||||
790
iqpilot/tools/cabana/ui/chart/chart.cc
Normal file
790
iqpilot/tools/cabana/ui/chart/chart.cc
Normal file
@@ -0,0 +1,790 @@
|
|||||||
|
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||||
|
#include "tools/cabana/ui/chart/chart.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cfloat>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <limits>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include "tools/cabana/core/settings.h"
|
||||||
|
#include "tools/cabana/settings.h"
|
||||||
|
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||||
|
#include "tools/cabana/ui/icons.h"
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
#include "tools/cabana/utils/strings.h"
|
||||||
|
|
||||||
|
const int AXIS_X_TOP_MARGIN = 4;
|
||||||
|
const int X_TICK_COUNT = 5;
|
||||||
|
const double MIN_ZOOM_SECONDS = 0.01;
|
||||||
|
const double EPSILON = 1e-6;
|
||||||
|
constexpr ImVec4 LAYOUT_MARGINS{8, 6, 8, 6};
|
||||||
|
static inline bool xLessThan(const ImPlotPoint &p, double x) { return p.x < (x - EPSILON); }
|
||||||
|
static inline bool isNull(const ImPlotPoint &p) { return p.x == 0 && p.y == 0; }
|
||||||
|
|
||||||
|
static std::string formatNumber(double value, int precision) {
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), "%.*f", precision, value);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int axisPrecision(double range, int tick_count, int min_precision) {
|
||||||
|
return std::max(int(-std::floor(std::log10(range / (tick_count - 1)))), min_precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addTextEllipsis(ImDrawList *dl, ImFont *font, ImU32 col, const ImVec2 &pos, float max_x, const std::string &text) {
|
||||||
|
const float size = ImGui::GetFontSize();
|
||||||
|
ImGui::PushFont(font, 0.0f);
|
||||||
|
ImGui::RenderTextEllipsis(dl, pos, ImVec2(max_x, pos.y + size), max_x, text.c_str(), nullptr, nullptr);
|
||||||
|
ImGui::PopFont();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent)
|
||||||
|
: x_min_(x_range.first), x_max_(x_range.second), charts_widget_(parent) {
|
||||||
|
series_type_ = (SeriesType)settings.chart_series_type;
|
||||||
|
|
||||||
|
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { signalRemoved(sig); }));
|
||||||
|
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { signalUpdated(sig); }));
|
||||||
|
connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { msgRemoved(id); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawMenuActions() {
|
||||||
|
|
||||||
|
const float indent = ImGui::GetFontSize();
|
||||||
|
float label_width = ImGui::CalcTextSize("Manage Signals").x;
|
||||||
|
for (const char *type : SERIES_TYPE_NAMES) label_width = std::max(label_width, ImGui::CalcTextSize(type).x);
|
||||||
|
for (int i = 0; i < (int)std::size(SERIES_TYPE_NAMES); ++i) {
|
||||||
|
if (radioMenuItem(SERIES_TYPE_NAMES[i], i == (int)series_type_, indent + label_width + indent)) {
|
||||||
|
setSeriesType((SeriesType)i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Indent(indent);
|
||||||
|
if (ImGui::MenuItem("Manage Signals")) manageSignals();
|
||||||
|
if (ImGui::MenuItem("Split Chart", nullptr, false, sigs_.size() > 1)) charts_widget_->splitChart(this);
|
||||||
|
ImGui::Unindent(indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ChartView::createToolButtons() {
|
||||||
|
ImGui::SetCursorScreenPos(layout_.close_btn_rect.Min);
|
||||||
|
bool close_clicked = toolButton("close_btn", icon::X, "Remove Chart");
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(layout_.manage_btn_rect.Min);
|
||||||
|
if (toolButton("manage_btn", icon::LIST, "")) ImGui::OpenPopup("manage_menu");
|
||||||
|
if (ImGui::BeginPopup("manage_menu")) {
|
||||||
|
drawMenuActions();
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (close_clicked) charts_widget_->removeChart(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) {
|
||||||
|
if (hasSignal(msg_id, sig)) return;
|
||||||
|
|
||||||
|
sigs_.push_back({.msg_id = msg_id, .sig = sig, .color = uniqueColor(sig->color)});
|
||||||
|
updateSeries(sig);
|
||||||
|
charts_widget_->seriesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const {
|
||||||
|
return std::any_of(sigs_.cbegin(), sigs_.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::removeIf(std::function<bool(const SigItem &s)> predicate) {
|
||||||
|
int prev_size = sigs_.size();
|
||||||
|
sigs_.erase(std::remove_if(sigs_.begin(), sigs_.end(), predicate), sigs_.end());
|
||||||
|
if (sigs_.empty()) {
|
||||||
|
charts_widget_->removeChart(this);
|
||||||
|
} else if (sigs_.size() != prev_size) {
|
||||||
|
charts_widget_->seriesChanged();
|
||||||
|
updateAxisY();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::signalUpdated(const cabana::Signal *sig) {
|
||||||
|
auto it = std::find_if(sigs_.begin(), sigs_.end(), [sig](auto &s) { return s.sig == sig; });
|
||||||
|
if (it != sigs_.end()) {
|
||||||
|
if (!(it->color == sig->color)) {
|
||||||
|
it->color = uniqueColor(sig->color, sig);
|
||||||
|
}
|
||||||
|
updateSeries(sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::manageSignals() {
|
||||||
|
auto dlg = std::make_unique<SignalSelector>("Manage Chart");
|
||||||
|
for (auto &s : sigs_) {
|
||||||
|
dlg->addSelected(s.msg_id, s.sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
charts_widget_->execSignalSelector(std::move(dlg), this, [this](SignalSelector &selector) {
|
||||||
|
const auto &items = selector.selectedItems();
|
||||||
|
for (const auto &s : items) {
|
||||||
|
addSignal(s.msg_id, s.sig);
|
||||||
|
}
|
||||||
|
removeIf([&](auto &s) {
|
||||||
|
return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it.msg_id && s.sig == it.sig; });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::updateLayout() {
|
||||||
|
const ImVec2 grip = ImGui::CalcTextSize(icon::GRIP_HORIZONTAL);
|
||||||
|
const ImVec2 top_left = layout_.rect.Min + ImVec2(LAYOUT_MARGINS.x, LAYOUT_MARGINS.y);
|
||||||
|
layout_.move_icon_rect = ImRect(top_left, top_left + grip);
|
||||||
|
const ImVec2 pad = ImGui::GetStyle().FramePadding * 2;
|
||||||
|
const ImVec2 close_size = ImGui::CalcTextSize(icon::X) + pad;
|
||||||
|
const ImVec2 manage_size = ImGui::CalcTextSize(icon::LIST) + pad;
|
||||||
|
const ImVec2 close_min(layout_.rect.Max.x - LAYOUT_MARGINS.z - close_size.x, top_left.y);
|
||||||
|
layout_.close_btn_rect = ImRect(close_min, close_min + close_size);
|
||||||
|
const ImVec2 manage_min(close_min.x - manage_size.x - ImGui::GetStyle().ItemSpacing.x, top_left.y);
|
||||||
|
layout_.manage_btn_rect = ImRect(manage_min, manage_min + manage_size);
|
||||||
|
|
||||||
|
ImFont *bold = boldFont();
|
||||||
|
const float font_size = ImGui::GetFontSize();
|
||||||
|
const float fm_height = ImGui::GetTextLineHeight();
|
||||||
|
const int marker_size = markerSize();
|
||||||
|
const int row_height = std::max<int>(marker_size, fm_height) + fm_height + 3;
|
||||||
|
const int legend_left = layout_.move_icon_rect.Max.x + LAYOUT_MARGINS.x;
|
||||||
|
const int legend_right = std::max<int>(layout_.manage_btn_rect.Min.x - LAYOUT_MARGINS.z, legend_left + 10);
|
||||||
|
|
||||||
|
|
||||||
|
layout_.legend_rects.clear();
|
||||||
|
int x = legend_left, y = top_left.y;
|
||||||
|
for (auto &s : sigs_) {
|
||||||
|
int w = marker_size + 5 + bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x +
|
||||||
|
ImGui::CalcTextSize(msgLabel(s.msg_id).c_str()).x;
|
||||||
|
w = std::min(w, legend_right - legend_left);
|
||||||
|
if (x + w > legend_right && x > legend_left) {
|
||||||
|
x = legend_left;
|
||||||
|
y += row_height;
|
||||||
|
}
|
||||||
|
layout_.legend_rects.emplace_back(ImVec2(x, y), ImVec2(x + w, y + std::max<int>(marker_size, fm_height)));
|
||||||
|
x += w + 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int adjust_top = (y + row_height) - top_left.y;
|
||||||
|
adjust_top = std::max<int>(adjust_top, layout_.manage_btn_rect.Max.y - layout_.rect.Min.y + LAYOUT_MARGINS.y);
|
||||||
|
layout_.header_bottom = layout_.rect.Min.y + adjust_top + LAYOUT_MARGINS.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::updatePlot(double cur, double min, double max) {
|
||||||
|
cur_sec_ = cur;
|
||||||
|
if (min != x_min_ || max != x_max_) {
|
||||||
|
x_min_ = min;
|
||||||
|
x_max_ = max;
|
||||||
|
updateAxisY();
|
||||||
|
if (tooltip_x_ >= 0) {
|
||||||
|
showTip(secondsAtPoint({(float)tooltip_x_, 0}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
||||||
|
std::vector<ImPlotPoint> &vals, std::vector<ImPlotPoint> &step_vals) {
|
||||||
|
vals.reserve(vals.size() + events.size());
|
||||||
|
step_vals.reserve(step_vals.size() + events.size() * 2);
|
||||||
|
|
||||||
|
double value = 0;
|
||||||
|
for (const CanEvent *e : events) {
|
||||||
|
if (sig->getValue(e->dat, e->size, &value)) {
|
||||||
|
const double ts = can->toSeconds(e->mono_time);
|
||||||
|
vals.emplace_back(ts, value);
|
||||||
|
if (!step_vals.empty())
|
||||||
|
step_vals.emplace_back(ts, step_vals.back().y);
|
||||||
|
step_vals.emplace_back(ts, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *msg_new_events) {
|
||||||
|
for (auto &s : sigs_) {
|
||||||
|
if (!sig || s.sig == sig) {
|
||||||
|
if (!msg_new_events) {
|
||||||
|
s.vals.clear();
|
||||||
|
s.step_vals.clear();
|
||||||
|
}
|
||||||
|
auto events = msg_new_events ? msg_new_events : &can->eventsMap();
|
||||||
|
auto it = events->find(s.msg_id);
|
||||||
|
if (it == events->end() || it->second.empty()) continue;
|
||||||
|
|
||||||
|
if (s.vals.empty() || can->toSeconds(it->second.back()->mono_time) > s.vals.back().x) {
|
||||||
|
appendCanEvents(s.sig, it->second, s.vals, s.step_vals);
|
||||||
|
} else {
|
||||||
|
std::vector<ImPlotPoint> vals, step_vals;
|
||||||
|
appendCanEvents(s.sig, it->second, vals, step_vals);
|
||||||
|
if (vals.empty()) continue;
|
||||||
|
s.vals.insert(std::lower_bound(s.vals.begin(), s.vals.end(), vals.front().x, xLessThan),
|
||||||
|
vals.begin(), vals.end());
|
||||||
|
s.step_vals.insert(std::lower_bound(s.step_vals.begin(), s.step_vals.end(), step_vals.front().x, xLessThan),
|
||||||
|
step_vals.begin(), step_vals.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!can->liveStreaming()) {
|
||||||
|
s.segment_tree.build(s.vals.size(), [&vals = s.vals](int i) { return vals[i].y; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateAxisY();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<ChartView::PointIter, ChartView::PointIter> ChartView::visibleRange(const std::vector<ImPlotPoint> &points) const {
|
||||||
|
auto first = std::lower_bound(points.cbegin(), points.cend(), x_min_, xLessThan);
|
||||||
|
auto last = std::lower_bound(first, points.cend(), x_max_, xLessThan);
|
||||||
|
return {first, last};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImPlotPoint *ChartView::lastPointBefore(const SigItem &s, double sec) const {
|
||||||
|
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double x) { return p.x > x + EPSILON; });
|
||||||
|
return it != s.vals.crend() && it->x >= x_min_ ? &*it : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::updateAxisY() {
|
||||||
|
if (sigs_.empty()) return;
|
||||||
|
|
||||||
|
double min = std::numeric_limits<double>::max();
|
||||||
|
double max = std::numeric_limits<double>::lowest();
|
||||||
|
std::string unit = sigs_[0].sig->unit;
|
||||||
|
|
||||||
|
for (auto &s : sigs_) {
|
||||||
|
if (!s.visible) continue;
|
||||||
|
|
||||||
|
|
||||||
|
if (unit != s.sig->unit) {
|
||||||
|
unit.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [first, last] = visibleRange(s.vals);
|
||||||
|
s.min = std::numeric_limits<double>::max();
|
||||||
|
s.max = std::numeric_limits<double>::lowest();
|
||||||
|
if (can->liveStreaming()) {
|
||||||
|
for (auto it = first; it != last; ++it) {
|
||||||
|
if (it->y < s.min) s.min = it->y;
|
||||||
|
if (it->y > s.max) s.max = it->y;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last));
|
||||||
|
}
|
||||||
|
min = std::min(min, s.min);
|
||||||
|
max = std::max(max, s.max);
|
||||||
|
}
|
||||||
|
if (min == std::numeric_limits<double>::max()) min = 0;
|
||||||
|
if (max == std::numeric_limits<double>::lowest()) max = 0;
|
||||||
|
|
||||||
|
y_unit_ = unit;
|
||||||
|
|
||||||
|
double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05;
|
||||||
|
auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3);
|
||||||
|
if (min_y != y_min_ || max_y != y_max_) {
|
||||||
|
y_min_ = min_y;
|
||||||
|
y_max_ = max_y;
|
||||||
|
y_tick_count_ = tick_count;
|
||||||
|
y_precision_ = axisPrecision(max_y - min_y, tick_count, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<double, double, int> ChartView::getNiceAxisNumbers(double min, double max, int tick_count) {
|
||||||
|
double range = niceNumber((max - min), true);
|
||||||
|
double step = niceNumber(range / (tick_count - 1), false);
|
||||||
|
min = std::floor(min / step);
|
||||||
|
max = std::ceil(max / step);
|
||||||
|
tick_count = int(max - min) + 1;
|
||||||
|
return {min * step, max * step, tick_count};
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChartView::xAxisPrecision() const {
|
||||||
|
return axisPrecision(x_max_ - x_min_, X_TICK_COUNT, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
double ChartView::niceNumber(double x, bool ceiling) {
|
||||||
|
double z = std::pow(10, std::floor(std::log10(x)));
|
||||||
|
double q = x / z;
|
||||||
|
if (ceiling) {
|
||||||
|
if (q <= 1.0) q = 1;
|
||||||
|
else if (q <= 2.0) q = 2;
|
||||||
|
else if (q <= 5.0) q = 5;
|
||||||
|
else q = 10;
|
||||||
|
} else {
|
||||||
|
if (q < 1.5) q = 1;
|
||||||
|
else if (q < 3.0) q = 2;
|
||||||
|
else if (q < 7.0) q = 5;
|
||||||
|
else q = 10;
|
||||||
|
}
|
||||||
|
return q * z;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawContextMenu() {
|
||||||
|
if (drawing_ghost_) return;
|
||||||
|
|
||||||
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) &&
|
||||||
|
!ImGui::IsAnyItemActive()) {
|
||||||
|
ImGui::OpenPopup("context_menu");
|
||||||
|
}
|
||||||
|
context_menu_id_ = ImGui::GetID("context_menu");
|
||||||
|
if (ImGui::BeginPopup("context_menu")) {
|
||||||
|
drawMenuActions();
|
||||||
|
|
||||||
|
const float indent = ImGui::GetFontSize();
|
||||||
|
ImGui::Indent(indent);
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (can->timeRange().has_value()) {
|
||||||
|
const std::string undo_text = std::string(icon::ARROW_COUNTERCLOCKWISE) + " Undo Zoom";
|
||||||
|
const std::string redo_text = std::string(icon::ARROW_CLOCKWISE) + " Redo Zoom";
|
||||||
|
if (ImGui::MenuItem(undo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canUndo())) charts_widget_->zoom_undo_stack_.undo();
|
||||||
|
if (ImGui::MenuItem(redo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canRedo())) charts_widget_->zoom_undo_stack_.redo();
|
||||||
|
ImGui::Separator();
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Close")) charts_widget_->removeChart(this);
|
||||||
|
ImGui::Unindent(indent);
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::handleMousePress() {
|
||||||
|
if (drawing_ghost_) return;
|
||||||
|
const ImVec2 pos = ImGui::GetMousePos();
|
||||||
|
|
||||||
|
const bool widget_pressed = ImGui::IsMouseClicked(ImGuiMouseButton_Left) && layout_.rect.Contains(pos) &&
|
||||||
|
ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
|
||||||
|
!layout_.close_btn_rect.Contains(pos) && !layout_.manage_btn_rect.Contains(pos);
|
||||||
|
if (!widget_pressed) return;
|
||||||
|
press_pos_ = pos;
|
||||||
|
if (layout_.move_icon_rect.Contains(pos)) return;
|
||||||
|
|
||||||
|
if (ImGui::GetIO().KeyShift) {
|
||||||
|
|
||||||
|
resume_after_scrub_ = !can->isPaused();
|
||||||
|
if (resume_after_scrub_) {
|
||||||
|
can->pause(true);
|
||||||
|
}
|
||||||
|
mouse_mode_ = MouseMode::Scrub;
|
||||||
|
} else if (layout_.plot_area.Contains(pos)) {
|
||||||
|
mouse_mode_ = MouseMode::Rubber;
|
||||||
|
rubber_rect_ = ImRect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::handleMouseMove() {
|
||||||
|
if (drawing_ghost_) return;
|
||||||
|
const ImVec2 pos = ImGui::GetMousePos();
|
||||||
|
const ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||||
|
|
||||||
|
if (delta.x == 0 && delta.y == 0) return;
|
||||||
|
|
||||||
|
if (mouse_mode_ == MouseMode::None && !layout_.rect.Contains(pos)) return;
|
||||||
|
|
||||||
|
if (mouse_mode_ == MouseMode::Scrub && ImGui::GetIO().KeyShift) {
|
||||||
|
if (layout_.plot_area.Contains(pos)) {
|
||||||
|
can->seekTo(std::clamp(secondsAtPoint(pos), can->minSeconds(), can->maxSeconds()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse_mode_ == MouseMode::Rubber) {
|
||||||
|
|
||||||
|
float left = std::clamp(std::min(press_pos_.x, pos.x), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||||
|
float right = std::clamp(std::max(press_pos_.x, pos.x), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||||
|
rubber_rect_ = ImRect(ImVec2(left, layout_.plot_area.Min.y), ImVec2(right, layout_.plot_area.Max.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTrackPoints();
|
||||||
|
if (mouse_mode_ != MouseMode::Rubber && layout_.plot_area.Contains(pos) && (layout_.plot_hovered || mouse_mode_ != MouseMode::None) &&
|
||||||
|
ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) {
|
||||||
|
charts_widget_->showValueTip(secondsAtPoint(pos));
|
||||||
|
} else if (tip_label_.isVisible()) {
|
||||||
|
charts_widget_->showValueTip(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::handleMouseRelease() {
|
||||||
|
if (drawing_ghost_) return;
|
||||||
|
const bool left_released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||||
|
const bool right_released = ImGui::IsMouseReleased(ImGuiMouseButton_Right) && layout_.rect.Contains(ImGui::GetMousePos());
|
||||||
|
if (!left_released && !right_released) return;
|
||||||
|
if (left_released && mouse_mode_ == MouseMode::Rubber) {
|
||||||
|
mouse_mode_ = MouseMode::None;
|
||||||
|
|
||||||
|
double min = std::clamp(secondsAtPoint(rubber_rect_.Min), can->minSeconds(), can->maxSeconds());
|
||||||
|
double max = std::clamp(secondsAtPoint(rubber_rect_.Max), can->minSeconds(), can->maxSeconds());
|
||||||
|
if (rubber_rect_.GetWidth() <= 0) {
|
||||||
|
|
||||||
|
can->seekTo(std::clamp(secondsAtPoint(press_pos_), can->minSeconds(), can->maxSeconds()));
|
||||||
|
} else if (rubber_rect_.GetWidth() > 10 && (max - min) > MIN_ZOOM_SECONDS) {
|
||||||
|
charts_widget_->zoom_undo_stack_.push(new ZoomCommand({min, max}));
|
||||||
|
}
|
||||||
|
rubber_rect_ = ImRect();
|
||||||
|
} else if (right_released && !ImGui::IsPopupOpen(context_menu_id_, ImGuiPopupFlags_None)) {
|
||||||
|
charts_widget_->zoom_undo_stack_.undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse_mode_ == MouseMode::Scrub) {
|
||||||
|
mouse_mode_ = MouseMode::None;
|
||||||
|
if (resume_after_scrub_) {
|
||||||
|
can->pause(false);
|
||||||
|
resume_after_scrub_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::takeSignalsFrom(ChartView *source) {
|
||||||
|
for (auto &s : source->sigs_) {
|
||||||
|
sigs_.push_back(std::move(s));
|
||||||
|
sigs_.back().color = uniqueColor(sigs_.back().color, sigs_.back().sig);
|
||||||
|
}
|
||||||
|
source->sigs_.clear();
|
||||||
|
updateAxisY();
|
||||||
|
charts_widget_->removeChart(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ChartView::SigItem> ChartView::takeExtraSignals() {
|
||||||
|
std::vector<SigItem> extra;
|
||||||
|
for (auto it = sigs_.begin() + 1; it != sigs_.end(); ++it) {
|
||||||
|
it->color = it->sig->color;
|
||||||
|
extra.push_back(std::move(*it));
|
||||||
|
}
|
||||||
|
sigs_.resize(1);
|
||||||
|
updateAxisY();
|
||||||
|
return extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::adoptSignal(SigItem s) {
|
||||||
|
sigs_.push_back(std::move(s));
|
||||||
|
updateAxisY();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::showTip(double sec) {
|
||||||
|
ImRect tip_area(ImVec2(layout_.rect.Min.x, layout_.plot_area.Min.y), ImVec2(layout_.rect.Max.x, layout_.plot_area.Max.y));
|
||||||
|
ImRect visible_rect = charts_widget_->chartVisibleRect(this);
|
||||||
|
visible_rect.ClipWith(tip_area);
|
||||||
|
if (visible_rect.GetWidth() <= 0 || visible_rect.GetHeight() <= 0) {
|
||||||
|
tip_label_.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip_x_ = xPos(sec);
|
||||||
|
float x = -1;
|
||||||
|
std::vector<TipLine> text_list;
|
||||||
|
for (auto &s : sigs_) {
|
||||||
|
if (s.visible) {
|
||||||
|
std::string value = "--";
|
||||||
|
if (const ImPlotPoint *pt = lastPointBefore(s, sec)) {
|
||||||
|
value = s.sig->formatValue(pt->y, false);
|
||||||
|
s.track_pt = *pt;
|
||||||
|
x = std::max(x, xPos(pt->x));
|
||||||
|
}
|
||||||
|
std::string name = sigs_.size() > 1 ? s.sig->name + ": " : "";
|
||||||
|
std::string min = s.min == std::numeric_limits<double>::max() ? "--" : utils::toString(s.min);
|
||||||
|
std::string max = s.max == std::numeric_limits<double>::lowest() ? "--" : utils::toString(s.max);
|
||||||
|
text_list.push_back({.has_marker = true, .marker = toImU32(s.color), .name = name, .bold = value, .rest = " (" + min + ", " + max + ")"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x < 0) {
|
||||||
|
x = tooltip_x_;
|
||||||
|
}
|
||||||
|
ImVec2 pt(x, layout_.plot_area.Min.y);
|
||||||
|
text_list.insert(text_list.begin(), TipLine{.name = formatNumber(secondsAtPoint({x, 0}), 3)});
|
||||||
|
tip_label_.showText(pt, text_list, visible_rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::hideTip() {
|
||||||
|
clearTrackPoints();
|
||||||
|
tooltip_x_ = -1;
|
||||||
|
tip_label_.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::draw(float width) {
|
||||||
|
ImGui::PushID(this);
|
||||||
|
width = std::max(width, (float)CHART_MIN_WIDTH);
|
||||||
|
layout_.plot_hovered = false;
|
||||||
|
|
||||||
|
const ImVec2 tile_pos = ImGui::GetCursorScreenPos();
|
||||||
|
const ImVec2 tile_size(width, (float)settings.chart_height);
|
||||||
|
layout_.rect = ImRect(tile_pos, tile_pos + tile_size);
|
||||||
|
if (ImGui::BeginChild("chart", tile_size, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
|
||||||
|
updateLayout();
|
||||||
|
paint();
|
||||||
|
drawContextMenu();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
const ImRect visible_rect = charts_widget_->chartVisibleRect(this);
|
||||||
|
if (!drawing_ghost_ && visible_rect.GetWidth() > 0 && visible_rect.GetHeight() > 0) tip_label_.draw();
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawGhost(float width) {
|
||||||
|
|
||||||
|
drawing_ghost_ = true;
|
||||||
|
const Layout saved = layout_;
|
||||||
|
draw(width);
|
||||||
|
layout_ = saved;
|
||||||
|
drawing_ghost_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::paint() {
|
||||||
|
drawStaticLayer();
|
||||||
|
|
||||||
|
if (can_drop_) {
|
||||||
|
ImGui::GetWindowDrawList()->AddRect(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_Header), 0.0f, 0, 4.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawStaticLayer() {
|
||||||
|
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||||
|
painter->AddRectFilled(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_ChildBg));
|
||||||
|
ImGui::SetCursorScreenPos(layout_.move_icon_rect.Min);
|
||||||
|
ImGui::InvisibleButton("grip", layout_.move_icon_rect.GetSize());
|
||||||
|
if (ImGui::IsItemActivated()) charts_widget_->startChartDrag(this, ImGui::GetMousePos());
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
painter->AddText(layout_.move_icon_rect.Min, ImGui::GetColorU32(ImGuiCol_Text), icon::GRIP_HORIZONTAL);
|
||||||
|
createToolButtons();
|
||||||
|
drawLegend();
|
||||||
|
drawSignalValue();
|
||||||
|
drawAxes();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawAxes() {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(layout_.rect.Min.x, layout_.header_bottom));
|
||||||
|
const float plot_h = std::max(layout_.rect.Max.y - layout_.header_bottom - LAYOUT_MARGINS.w, 10.0f);
|
||||||
|
ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(LAYOUT_MARGINS.x, AXIS_X_TOP_MARGIN));
|
||||||
|
ImPlot::PushStyleColor(ImPlotCol_PlotBg, ImVec4(0, 0, 0, 0));
|
||||||
|
ImPlot::PushStyleColor(ImPlotCol_FrameBg, ImVec4(0, 0, 0, 0));
|
||||||
|
|
||||||
|
|
||||||
|
const bool dark = isDarkTheme();
|
||||||
|
ImVec4 grid_color;
|
||||||
|
if (dark) {
|
||||||
|
grid_color = colorRgb(DarkTheme::light.r, DarkTheme::light.g, DarkTheme::light.b);
|
||||||
|
} else {
|
||||||
|
grid_color = ImGui::GetStyleColorVec4(ImGuiCol_Text);
|
||||||
|
grid_color.w = 50.0f / 255.0f;
|
||||||
|
}
|
||||||
|
ImPlot::PushStyleColor(ImPlotCol_AxisGrid, grid_color);
|
||||||
|
ImPlot::PushStyleColor(ImPlotCol_PlotBorder, grid_color);
|
||||||
|
ImPlot::PushStyleColor(ImPlotCol_AxisTick, ImVec4(0, 0, 0, 0));
|
||||||
|
ImPlot::PushStyleColor(ImPlotCol_AxisText, ImGui::GetStyleColorVec4(ImGuiCol_Text));
|
||||||
|
ImPlot::PushStyleVar(ImPlotStyleVar_MajorTickLen, ImVec2(0, 0));
|
||||||
|
|
||||||
|
ImPlot::PushStyleVar(ImPlotStyleVar_MajorGridSize, dark ? ImVec2(2.0f, 2.0f) : ImVec2(1.0f, 1.0f));
|
||||||
|
const ImPlotFlags flags = ImPlotFlags_NoTitle | ImPlotFlags_NoLegend | ImPlotFlags_NoMenus | ImPlotFlags_NoMouseText |
|
||||||
|
ImPlotFlags_NoBoxSelect | ImPlotFlags_NoInputs | ImPlotFlags_NoFrame;
|
||||||
|
const ImPlotAxisFlags axis_flags = ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_NoSideSwitch | ImPlotAxisFlags_Lock;
|
||||||
|
|
||||||
|
const float x_label_width = ImGui::CalcTextSize(formatNumber(x_max_, xAxisPrecision()).c_str()).x + 5;
|
||||||
|
if (ImPlot::BeginPlot("##plot", ImVec2(layout_.rect.GetWidth() - x_label_width / 2, plot_h), flags)) {
|
||||||
|
ImPlot::SetupAxis(ImAxis_X1, nullptr, axis_flags);
|
||||||
|
ImPlot::SetupAxis(ImAxis_Y1, y_unit_.empty() ? nullptr : y_unit_.c_str(), axis_flags);
|
||||||
|
ImPlot::SetupAxisLimits(ImAxis_X1, x_min_, x_max_, ImPlotCond_Always);
|
||||||
|
ImPlot::SetupAxisLimits(ImAxis_Y1, y_min_, y_max_, ImPlotCond_Always);
|
||||||
|
|
||||||
|
ImPlot::SetupAxisFormat(ImAxis_Y1, ("%." + std::to_string(y_precision_) + "f").c_str());
|
||||||
|
ImPlot::SetupAxisTicks(ImAxis_Y1, y_min_, y_max_, y_tick_count_);
|
||||||
|
ImPlot::SetupAxisFormat(ImAxis_X1, ("%." + std::to_string(xAxisPrecision()) + "f").c_str());
|
||||||
|
ImPlot::SetupAxisTicks(ImAxis_X1, x_min_, x_max_, X_TICK_COUNT);
|
||||||
|
ImPlot::SetupFinish();
|
||||||
|
|
||||||
|
layout_.plot_area = ImRect(ImPlot::GetPlotPos(), ImPlot::GetPlotPos() + ImPlot::GetPlotSize());
|
||||||
|
|
||||||
|
layout_.plot_hovered = layout_.plot_area.Contains(ImGui::GetMousePos()) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
||||||
|
drawSeries();
|
||||||
|
handleMousePress();
|
||||||
|
handleMouseMove();
|
||||||
|
handleMouseRelease();
|
||||||
|
drawForeground();
|
||||||
|
ImPlot::EndPlot();
|
||||||
|
}
|
||||||
|
ImPlot::PopStyleColor(6);
|
||||||
|
ImPlot::PopStyleVar(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawLegend() {
|
||||||
|
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||||
|
const ImU32 title_color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||||
|
|
||||||
|
const ImU32 msg_color = withAlpha(title_color, 180);
|
||||||
|
ImFont *bold = boldFont();
|
||||||
|
ImFont *normal = ImGui::GetFont();
|
||||||
|
const float font_size = ImGui::GetFontSize();
|
||||||
|
const float marker_size = markerSize();
|
||||||
|
|
||||||
|
for (int i = 0; i < sigs_.size() && i < layout_.legend_rects.size(); ++i) {
|
||||||
|
const auto &s = sigs_[i];
|
||||||
|
const ImRect &r = layout_.legend_rects[i];
|
||||||
|
|
||||||
|
ImGui::PushID(i);
|
||||||
|
ImGui::SetCursorScreenPos(r.Min);
|
||||||
|
if (ImGui::InvisibleButton("legend", ImVec2(std::max(r.GetWidth(), 1.0f), std::max(r.GetHeight(), 1.0f))) &&
|
||||||
|
mouse_mode_ == MouseMode::None && sigs_.size() > 1) {
|
||||||
|
sigs_[i].visible = !sigs_[i].visible;
|
||||||
|
updateAxisY();
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
|
||||||
|
if (series_type_ == SeriesType::Scatter) {
|
||||||
|
painter->AddCircleFilled(r.Min + ImVec2(marker_size / 2.0f, 2.0f + marker_size / 2.0f), marker_size / 2.0f, toImU32(s.color));
|
||||||
|
} else {
|
||||||
|
drawColorMarker(painter, r.Min, toImU32(s.color));
|
||||||
|
}
|
||||||
|
|
||||||
|
float x = r.Min.x + marker_size + 5;
|
||||||
|
const float text_y = r.GetCenter().y - font_size / 2.0f;
|
||||||
|
addTextEllipsis(painter, bold, title_color, ImVec2(x, text_y), r.Max.x, s.sig->name);
|
||||||
|
float name_w = std::min(bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x, r.Max.x - x);
|
||||||
|
x += name_w;
|
||||||
|
const std::string msg = msgLabel(s.msg_id);
|
||||||
|
addTextEllipsis(painter, normal, msg_color, ImVec2(x, text_y), r.Max.x, msg);
|
||||||
|
if (!s.visible) {
|
||||||
|
const float y = r.GetCenter().y;
|
||||||
|
painter->AddLine(ImVec2(r.Min.x + marker_size + 5, y), ImVec2(std::min(x + ImGui::CalcTextSize(msg.c_str()).x, r.Max.x), y), title_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawSeries() {
|
||||||
|
for (int i = 0; i < sigs_.size(); ++i) {
|
||||||
|
auto &s = sigs_[i];
|
||||||
|
if (!s.visible) continue;
|
||||||
|
|
||||||
|
|
||||||
|
auto [first, last] = visibleRange(s.vals);
|
||||||
|
int num_points = std::max<int>(last - first, 1);
|
||||||
|
double pixels_per_point = 0;
|
||||||
|
if (first != last) {
|
||||||
|
const ImPlotPoint &right_pt = last == s.vals.cend() ? s.vals.back() : *last;
|
||||||
|
pixels_per_point = (xPos(right_pt.x) - xPos(first->x)) / num_points;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string label = "##sig" + std::to_string(i);
|
||||||
|
ImPlotSpec spec;
|
||||||
|
spec.LineColor = toImVec4(s.color);
|
||||||
|
spec.Stride = sizeof(ImPlotPoint);
|
||||||
|
if (series_type_ == SeriesType::Scatter) {
|
||||||
|
float radius = std::clamp(pixels_per_point / 2.0, 2.0, 8.0) / 2.0;
|
||||||
|
spec.Marker = ImPlotMarker_Circle;
|
||||||
|
spec.MarkerSize = radius;
|
||||||
|
if (first != last) ImPlot::PlotScatter(label.c_str(), &first->x, &first->y, last - first, spec);
|
||||||
|
} else {
|
||||||
|
const auto &points = series_type_ == SeriesType::StepLine ? s.step_vals : s.vals;
|
||||||
|
|
||||||
|
auto [begin, end] = visibleRange(points);
|
||||||
|
if (begin != points.cbegin()) --begin;
|
||||||
|
if (end != points.cend()) ++end;
|
||||||
|
if (begin == end) continue;
|
||||||
|
|
||||||
|
spec.LineWeight = 2;
|
||||||
|
ImPlot::PlotLine(label.c_str(), &begin->x, &begin->y, end - begin, spec);
|
||||||
|
|
||||||
|
|
||||||
|
if ((num_points == 1 || pixels_per_point > 20) && first != last) {
|
||||||
|
ImPlotSpec dots;
|
||||||
|
dots.LineColor = toImVec4(s.color);
|
||||||
|
dots.Stride = sizeof(ImPlotPoint);
|
||||||
|
dots.Marker = ImPlotMarker_Circle;
|
||||||
|
dots.MarkerSize = 4;
|
||||||
|
ImPlot::PlotScatter((label + "_pts").c_str(), &first->x, &first->y, last - first, dots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawForeground() {
|
||||||
|
drawTimeline();
|
||||||
|
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||||
|
ImPlot::PushPlotClipRect();
|
||||||
|
float track_line_x = -1;
|
||||||
|
for (auto &s : sigs_) {
|
||||||
|
if (!isNull(s.track_pt) && s.visible) {
|
||||||
|
ImVec2 pos(xPos(s.track_pt.x), yPos(s.track_pt.y));
|
||||||
|
painter->AddCircleFilled(pos, 5.5f, toImU32(s.color.darker(125)));
|
||||||
|
track_line_x = std::max(track_line_x, pos.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (track_line_x > 0) {
|
||||||
|
const ImU32 dark_gray = IM_COL32(0x80, 0x80, 0x80, 0xff);
|
||||||
|
for (float y = layout_.plot_area.Min.y; y < layout_.plot_area.Max.y; y += 8) {
|
||||||
|
painter->AddLine(ImVec2(track_line_x, y), ImVec2(track_line_x, std::min(y + 4, layout_.plot_area.Max.y)), dark_gray, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImPlot::PopPlotClipRect();
|
||||||
|
|
||||||
|
drawRubberBandTimeRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawRubberBandTimeRange() {
|
||||||
|
if (rubber_rect_.GetWidth() <= 1) return;
|
||||||
|
|
||||||
|
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||||
|
|
||||||
|
const ImU32 highlight = withAlpha(ImGui::GetColorU32(ImGuiCol_Header), 255);
|
||||||
|
painter->AddRectFilled(rubber_rect_.Min, rubber_rect_.Max, withAlpha(highlight, 50));
|
||||||
|
painter->AddRect(rubber_rect_.Min, rubber_rect_.Max, highlight);
|
||||||
|
|
||||||
|
|
||||||
|
const ImU32 white = IM_COL32_WHITE;
|
||||||
|
const ImU32 gray = IM_COL32(0xa0, 0xa0, 0xa4, 0xff);
|
||||||
|
painter = ImGui::GetWindowDrawList();
|
||||||
|
painter->PushClipRect(layout_.rect.Min, layout_.rect.Max);
|
||||||
|
for (const auto &pt : {rubber_rect_.GetBL(), rubber_rect_.GetBR()}) {
|
||||||
|
std::string sec = formatNumber(secondsAtPoint(pt), 2);
|
||||||
|
ImVec2 size = ImGui::CalcTextSize(sec.c_str()) + ImVec2(12, AXIS_X_TOP_MARGIN * 2);
|
||||||
|
ImVec2 top_left = pt.x == rubber_rect_.Min.x ? ImVec2(pt.x - size.x, pt.y + 2) : ImVec2(pt.x, pt.y + 2);
|
||||||
|
painter->AddRectFilled(top_left, top_left + size, gray);
|
||||||
|
painter->AddText(top_left + ImVec2(6, AXIS_X_TOP_MARGIN), white, sec.c_str());
|
||||||
|
}
|
||||||
|
painter->PopClipRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawTimeline() {
|
||||||
|
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||||
|
float x = std::clamp(xPos(cur_sec_), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||||
|
painter->AddLine(ImVec2(x, layout_.plot_area.Min.y - 1.0f), ImVec2(x, layout_.plot_area.Max.y + 1.0f), ImGui::GetColorU32(ImGuiCol_Text), 1.0f);
|
||||||
|
|
||||||
|
std::string time_str = formatNumber(cur_sec_, 2);
|
||||||
|
ImVec2 time_str_size = ImGui::CalcTextSize(time_str.c_str()) + ImVec2(8, 2);
|
||||||
|
ImVec2 time_str_pos(x - time_str_size.x / 2.0f, layout_.plot_area.Max.y + AXIS_X_TOP_MARGIN);
|
||||||
|
const bool dark = isDarkTheme();
|
||||||
|
painter->AddRectFilled(time_str_pos, time_str_pos + time_str_size, dark ? IM_COL32(0x80, 0x80, 0x80, 0xff) : IM_COL32(0xa0, 0xa0, 0xa4, 0xff), 3.0f);
|
||||||
|
painter->AddText(time_str_pos + ImVec2(4, 1), IM_COL32_WHITE, time_str.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartView::drawSignalValue() {
|
||||||
|
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||||
|
const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||||
|
for (int i = 0; i < sigs_.size() && i < layout_.legend_rects.size(); ++i) {
|
||||||
|
const auto &s = sigs_[i];
|
||||||
|
const ImPlotPoint *pt = lastPointBefore(s, cur_sec_);
|
||||||
|
std::string value = pt ? s.sig->formatValue(pt->y) : "--";
|
||||||
|
const ImVec2 value_min = layout_.legend_rects[i].GetBL() - ImVec2(0, 1);
|
||||||
|
ImRect value_rect(value_min, value_min + layout_.legend_rects[i].GetSize());
|
||||||
|
float w = ImGui::CalcTextSize(value.c_str()).x;
|
||||||
|
if (w <= value_rect.GetWidth()) {
|
||||||
|
painter->AddText(ImVec2(value_rect.GetCenter().x - w / 2, value_rect.Min.y), color, value.c_str());
|
||||||
|
} else {
|
||||||
|
addTextEllipsis(painter, ImGui::GetFont(), color, value_rect.Min, value_rect.Max.x, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CabanaColor ChartView::uniqueColor(CabanaColor color, const cabana::Signal *exclude) const {
|
||||||
|
for (auto &s : sigs_) {
|
||||||
|
if (s.sig != exclude && std::abs(color.hsv().hue - s.color.hsv().hue) < 0.1) {
|
||||||
|
|
||||||
|
auto last_color = sigs_.back().color;
|
||||||
|
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||||
|
std::uniform_int_distribution<int> sat(35, 99);
|
||||||
|
std::uniform_int_distribution<int> val(85, 99);
|
||||||
|
color = CabanaColor::fromHsv(std::fmod(last_color.hsv().hue + 60 / 360.0, 1.0),
|
||||||
|
sat(rng) / 100.0,
|
||||||
|
val(rng) / 100.0,
|
||||||
|
color.a / 255.0f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
142
iqpilot/tools/cabana/ui/chart/chart.h
Normal file
142
iqpilot/tools/cabana/ui/chart/chart.h
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
#include "implot.h"
|
||||||
|
|
||||||
|
#include "tools/cabana/ui/chart/tiplabel.h"
|
||||||
|
#include "tools/cabana/dbc/dbcmanager.h"
|
||||||
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
|
#include "tools/cabana/utils/util.h"
|
||||||
|
|
||||||
|
enum class SeriesType {
|
||||||
|
Line = 0,
|
||||||
|
StepLine,
|
||||||
|
Scatter
|
||||||
|
};
|
||||||
|
inline constexpr const char *SERIES_TYPE_NAMES[] = {"Line", "Step Line", "Scatter"};
|
||||||
|
|
||||||
|
|
||||||
|
inline std::string msgLabel(const MessageId &id) { return " " + msgName(id) + " " + id.toString(); }
|
||||||
|
|
||||||
|
class ChartsWidget;
|
||||||
|
class ChartView {
|
||||||
|
public:
|
||||||
|
struct SigItem {
|
||||||
|
MessageId msg_id;
|
||||||
|
const cabana::Signal *sig = nullptr;
|
||||||
|
CabanaColor color;
|
||||||
|
bool visible = true;
|
||||||
|
std::vector<ImPlotPoint> vals;
|
||||||
|
std::vector<ImPlotPoint> step_vals;
|
||||||
|
ImPlotPoint track_pt{};
|
||||||
|
SegmentTree segment_tree;
|
||||||
|
double min = 0;
|
||||||
|
double max = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent);
|
||||||
|
void addSignal(const MessageId &msg_id, const cabana::Signal *sig);
|
||||||
|
bool hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const;
|
||||||
|
void updateSeries(const cabana::Signal *sig = nullptr, const MessageEventsMap *msg_new_events = nullptr);
|
||||||
|
void updatePlot(double cur, double min, double max);
|
||||||
|
void setSeriesType(SeriesType type) { series_type_ = type; }
|
||||||
|
void showTip(double sec);
|
||||||
|
void hideTip();
|
||||||
|
void draw(float width);
|
||||||
|
void drawGhost(float width);
|
||||||
|
void removeIf(std::function<bool(const SigItem &)> predicate);
|
||||||
|
void takeSignalsFrom(ChartView *source);
|
||||||
|
|
||||||
|
std::vector<SigItem> takeExtraSignals();
|
||||||
|
void adoptSignal(SigItem s);
|
||||||
|
void setDropHighlight(bool highlight) { can_drop_ = highlight; }
|
||||||
|
const std::vector<SigItem> &signals() const { return sigs_; }
|
||||||
|
const ImRect &rect() const { return layout_.rect; }
|
||||||
|
bool plotHovered() const { return layout_.plot_hovered; }
|
||||||
|
double secondsAtPoint(const ImVec2 &pt) const {
|
||||||
|
return x_min_ + (pt.x - layout_.plot_area.Min.x) * (x_max_ - x_min_) / std::max(layout_.plot_area.GetWidth(), 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using PointIter = std::vector<ImPlotPoint>::const_iterator;
|
||||||
|
|
||||||
|
void signalUpdated(const cabana::Signal *sig);
|
||||||
|
void manageSignals();
|
||||||
|
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; }); }
|
||||||
|
|
||||||
|
void appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
||||||
|
std::vector<ImPlotPoint> &vals, std::vector<ImPlotPoint> &step_vals);
|
||||||
|
void createToolButtons();
|
||||||
|
void drawContextMenu();
|
||||||
|
void handleMousePress();
|
||||||
|
void handleMouseMove();
|
||||||
|
void handleMouseRelease();
|
||||||
|
void updateLayout();
|
||||||
|
void updateAxisY();
|
||||||
|
void paint();
|
||||||
|
void drawStaticLayer();
|
||||||
|
void drawAxes();
|
||||||
|
void drawLegend();
|
||||||
|
void drawSeries();
|
||||||
|
void drawForeground();
|
||||||
|
void drawSignalValue();
|
||||||
|
void drawTimeline();
|
||||||
|
void drawRubberBandTimeRange();
|
||||||
|
void drawMenuActions();
|
||||||
|
int xAxisPrecision() const;
|
||||||
|
std::tuple<double, double, int> getNiceAxisNumbers(double min, double max, int tick_count);
|
||||||
|
double niceNumber(double x, bool ceiling);
|
||||||
|
CabanaColor uniqueColor(CabanaColor color, const cabana::Signal *exclude = nullptr) const;
|
||||||
|
|
||||||
|
const ImPlotPoint *lastPointBefore(const SigItem &s, double sec) const;
|
||||||
|
|
||||||
|
std::pair<PointIter, PointIter> visibleRange(const std::vector<ImPlotPoint> &points) const;
|
||||||
|
inline void clearTrackPoints() { for (auto &s : sigs_) s.track_pt = {}; }
|
||||||
|
inline float xPos(double sec) const { return layout_.plot_area.Min.x + (sec - x_min_) / (x_max_ - x_min_) * layout_.plot_area.GetWidth(); }
|
||||||
|
inline float yPos(double val) const { return layout_.plot_area.Max.y - (val - y_min_) / (y_max_ - y_min_) * layout_.plot_area.GetHeight(); }
|
||||||
|
|
||||||
|
|
||||||
|
struct Layout {
|
||||||
|
ImRect rect;
|
||||||
|
ImRect plot_area;
|
||||||
|
ImRect move_icon_rect;
|
||||||
|
ImRect close_btn_rect;
|
||||||
|
ImRect manage_btn_rect;
|
||||||
|
std::vector<ImRect> legend_rects;
|
||||||
|
float header_bottom = 0;
|
||||||
|
bool plot_hovered = false;
|
||||||
|
} layout_;
|
||||||
|
|
||||||
|
double x_min_;
|
||||||
|
double x_max_;
|
||||||
|
double y_min_ = 0;
|
||||||
|
double y_max_ = 1;
|
||||||
|
int y_tick_count_ = 3;
|
||||||
|
int y_precision_ = 0;
|
||||||
|
std::string y_unit_;
|
||||||
|
|
||||||
|
enum class MouseMode { None, Rubber, Scrub };
|
||||||
|
MouseMode mouse_mode_ = MouseMode::None;
|
||||||
|
ImVec2 press_pos_;
|
||||||
|
ImRect rubber_rect_;
|
||||||
|
bool resume_after_scrub_ = false;
|
||||||
|
bool drawing_ghost_ = false;
|
||||||
|
ImGuiID context_menu_id_ = 0;
|
||||||
|
|
||||||
|
TipLabel tip_label_;
|
||||||
|
std::vector<SigItem> sigs_;
|
||||||
|
double cur_sec_ = 0;
|
||||||
|
SeriesType series_type_ = SeriesType::Line;
|
||||||
|
bool can_drop_ = false;
|
||||||
|
double tooltip_x_ = -1;
|
||||||
|
ChartsWidget *charts_widget_;
|
||||||
|
Connections connections_;
|
||||||
|
};
|
||||||
674
iqpilot/tools/cabana/ui/chart/chartswidget.cc
Normal file
674
iqpilot/tools/cabana/ui/chart/chartswidget.cc
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||||
|
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||||
|
|
||||||
|
#include "tools/cabana/ui/threadpool.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
#include "tools/cabana/settings.h"
|
||||||
|
#include "tools/cabana/ui/chart/chart.h"
|
||||||
|
#include "tools/cabana/ui/icons.h"
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
#include "tools/cabana/utils/strings.h"
|
||||||
|
|
||||||
|
const int MAX_COLUMN_COUNT = 4;
|
||||||
|
const int CHART_SPACING = 4;
|
||||||
|
const int START_DRAG_DISTANCE = 10;
|
||||||
|
const float LAYOUT_HORIZONTAL_SPACING = 6.0f;
|
||||||
|
const float MIN_RANGE_SLIDER_WIDTH = 40.0f;
|
||||||
|
|
||||||
|
bool LogSlider::draw(const char *label, float width) {
|
||||||
|
return fusionSliderInt(label, &pos_, min_, max_, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartsWidget::ChartsWidget() {
|
||||||
|
range_slider_.setRange(1, settings.max_cached_minutes * 60);
|
||||||
|
|
||||||
|
tabbar_.setAutoHide(true);
|
||||||
|
tabbar_.setUsesScrollButtons(true);
|
||||||
|
tabbar_.setTabsClosable(true);
|
||||||
|
|
||||||
|
column_count_ = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT);
|
||||||
|
max_chart_range_ = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60);
|
||||||
|
display_range_ = std::make_pair(can->minSeconds(), can->minSeconds() + max_chart_range_);
|
||||||
|
range_slider_.setValue(max_chart_range_);
|
||||||
|
|
||||||
|
connections_.push_back(dbc()->fileChanged.connect([this]() { removeAll(); }));
|
||||||
|
connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { eventsMerged(events); }));
|
||||||
|
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *, bool) { updateState(); }));
|
||||||
|
connections_.push_back(can->seeking.connect([this](double) { updateState(); }));
|
||||||
|
connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { updateState(); }));
|
||||||
|
connections_.push_back(settings.changed.connect([this]() { settingChanged(); }));
|
||||||
|
connections_.push_back(seriesChanged.connect([this]() { updateTabBar(); }));
|
||||||
|
connections_.push_back(tabbar_.tabCloseRequested.connect([this](int index) { removeTab(index); }));
|
||||||
|
connections_.push_back(tabbar_.tabContextMenu.connect([this](int index) {
|
||||||
|
if (ImGui::BeginPopupContextItem()) {
|
||||||
|
if (ImGui::MenuItem("Close Other Tabs")) {
|
||||||
|
tabbar_.moveTab(index, 0);
|
||||||
|
tabbar_.setCurrentIndex(0);
|
||||||
|
while (tabbar_.count() > 1) removeTab(1);
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
connections_.push_back(tabbar_.currentChanged.connect([this](int index) {
|
||||||
|
if (index != -1) updateLayout();
|
||||||
|
}));
|
||||||
|
|
||||||
|
setIsDocked(true);
|
||||||
|
newTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartsWidget::~ChartsWidget() = default;
|
||||||
|
|
||||||
|
std::string ChartsWidget::whatsThis() const {
|
||||||
|
return R"(
|
||||||
|
<b>Chart View</b><br />
|
||||||
|
<b>Click</b>: Click to seek to a corresponding time.<br />
|
||||||
|
<b>Drag</b>: Zoom into the chart.<br />
|
||||||
|
<b>Shift + Drag</b>: Scrub through the chart to view values.<br />
|
||||||
|
<b>Right Mouse</b>: Open the context menu.<br />
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::newTab() {
|
||||||
|
static int tab_unique_id = 0;
|
||||||
|
int idx = tabbar_.addTab("");
|
||||||
|
tabbar_.setTabData(idx, tab_unique_id++);
|
||||||
|
tabbar_.setCurrentIndex(idx);
|
||||||
|
updateTabBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::removeTab(int index) {
|
||||||
|
int id = tabbar_.tabData(index);
|
||||||
|
for (auto &c : std::vector<ChartView *>(tab_charts_[id])) {
|
||||||
|
removeChart(c);
|
||||||
|
}
|
||||||
|
tab_charts_.erase(id);
|
||||||
|
tabbar_.removeTab(index);
|
||||||
|
updateTabBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::updateTabBar() {
|
||||||
|
for (int i = 0; i < tabbar_.count(); ++i) {
|
||||||
|
const auto &charts_in_tab = tab_charts_[tabbar_.tabData(i)];
|
||||||
|
tabbar_.setTabText(i, "Tab " + std::to_string(i + 1) + " (" + std::to_string((int)charts_in_tab.size()) + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) {
|
||||||
|
std::vector<std::future<void>> futures;
|
||||||
|
for (auto &c : charts_) {
|
||||||
|
futures.push_back(ThreadPool::instance().run([c = c.get(), &new_events]() { c->updateSeries(nullptr, &new_events); }));
|
||||||
|
}
|
||||||
|
for (auto &f : futures) f.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::zoomReset() {
|
||||||
|
can->setTimeRange(std::nullopt);
|
||||||
|
zoom_undo_stack_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImRect ChartsWidget::chartVisibleRect(ChartView *chart) {
|
||||||
|
ImRect r = chart->rect();
|
||||||
|
r.ClipWith(charts_scroll_viewport_);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::showValueTip(double sec) {
|
||||||
|
if (chartDragActive()) sec = -1;
|
||||||
|
showTip(sec);
|
||||||
|
if (sec < 0 && !value_tip_visible_) return;
|
||||||
|
|
||||||
|
value_tip_visible_ = sec >= 0;
|
||||||
|
for (auto c : currentCharts()) {
|
||||||
|
value_tip_visible_ ? c->showTip(sec) : c->hideTip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::updateState() {
|
||||||
|
if (charts_.empty()) return;
|
||||||
|
|
||||||
|
const auto &time_range = can->timeRange();
|
||||||
|
const double cur_sec = can->currentSec();
|
||||||
|
if (!time_range.has_value()) {
|
||||||
|
double pos = (cur_sec - display_range_.first) / std::max<float>(1.0, max_chart_range_);
|
||||||
|
if (pos < 0 || pos > 0.8) {
|
||||||
|
display_range_.first = std::max(can->minSeconds(), cur_sec - max_chart_range_ * 0.1);
|
||||||
|
}
|
||||||
|
double max_sec = std::min(display_range_.first + max_chart_range_, can->maxSeconds());
|
||||||
|
display_range_.first = std::max(can->minSeconds(), max_sec - max_chart_range_);
|
||||||
|
display_range_.second = display_range_.first + max_chart_range_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &range = time_range ? *time_range : display_range_;
|
||||||
|
for (auto &c : charts_) {
|
||||||
|
c->updatePlot(cur_sec, range.first, range.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::setMaxChartRange(int value) {
|
||||||
|
max_chart_range_ = settings.chart_range = value;
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::setIsDocked(bool docked) {
|
||||||
|
is_docked_ = docked;
|
||||||
|
if (!docked) float_window_init_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::drawToolBar() {
|
||||||
|
beginToolbar();
|
||||||
|
float slider_width = 150.0f;
|
||||||
|
const bool is_zoomed = can->timeRange().has_value();
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<ToolbarItem> items;
|
||||||
|
items.push_back({toolbarButtonWidth(icon::PLUS_SQUARE), [this]() {
|
||||||
|
if (toolButton("new_plot_btn", icon::PLUS_SQUARE, "New Chart")) newChart();
|
||||||
|
}});
|
||||||
|
items.push_back({toolbarButtonWidth(icon::WINDOW_STACK), [this]() {
|
||||||
|
if (toolButton("new_tab_btn", icon::WINDOW_STACK, "New Tab")) newTab();
|
||||||
|
}});
|
||||||
|
const std::string title_label = "Charts: " + std::to_string(charts_.size());
|
||||||
|
items.push_back({ImGui::CalcTextSize(title_label.c_str()).x + LAYOUT_HORIZONTAL_SPACING, [&title_label]() {
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted(title_label.c_str());
|
||||||
|
ImGui::SameLine(0.0f, LAYOUT_HORIZONTAL_SPACING);
|
||||||
|
ImGui::Dummy(ImVec2(0.0f, 0.0f));
|
||||||
|
}});
|
||||||
|
|
||||||
|
const int type_count = (int)std::size(SERIES_TYPE_NAMES);
|
||||||
|
const std::string chart_type_text = std::string("Type: ") + SERIES_TYPE_NAMES[std::clamp(settings.chart_series_type, 0, type_count - 1)];
|
||||||
|
items.push_back({menuButtonWidth(chart_type_text), [this, &chart_type_text]() {
|
||||||
|
menuButton("chart_type", chart_type_text, "chart_type_menu");
|
||||||
|
if (ImGui::BeginPopup("chart_type_menu")) {
|
||||||
|
for (int i = 0; i < type_count; ++i) {
|
||||||
|
if (ImGui::MenuItem(SERIES_TYPE_NAMES[i])) {
|
||||||
|
settings.chart_series_type = i;
|
||||||
|
settingChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
const std::string columns_action_text = "Columns: " + std::to_string(column_count_);
|
||||||
|
if (columns_action_visible_) {
|
||||||
|
items.push_back({menuButtonWidth(columns_action_text), [this, &columns_action_text]() {
|
||||||
|
menuButton("columns", columns_action_text, "columns_menu");
|
||||||
|
if (ImGui::BeginPopup("columns_menu")) {
|
||||||
|
for (int i = 0; i < MAX_COLUMN_COUNT; ++i) {
|
||||||
|
if (ImGui::MenuItem(std::to_string(i + 1).c_str())) setColumnCount(i + 1);
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const size_t spacer_index = items.size();
|
||||||
|
size_t slider_index = (size_t)-1;
|
||||||
|
const std::string range_lb = is_zoomed ? std::string() : utils::formatSeconds(max_chart_range_);
|
||||||
|
std::string reset_zoom_text;
|
||||||
|
if (!is_zoomed) {
|
||||||
|
items.push_back({ImGui::CalcTextSize(range_lb.c_str()).x, [&range_lb]() {
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted(range_lb.c_str());
|
||||||
|
}});
|
||||||
|
slider_index = items.size();
|
||||||
|
items.push_back({slider_width, [this, &slider_width]() {
|
||||||
|
if (range_slider_.draw("##range_slider", slider_width)) setMaxChartRange(range_slider_.value());
|
||||||
|
ImGui::SetItemTooltip("Set the chart range");
|
||||||
|
}});
|
||||||
|
} else {
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), "%.2f-%.2f", can->timeRange()->first, can->timeRange()->second);
|
||||||
|
reset_zoom_text = buf;
|
||||||
|
items.push_back({toolbarButtonWidth(icon::ARROW_COUNTERCLOCKWISE), [this]() {
|
||||||
|
ImGui::BeginDisabled(!zoom_undo_stack_.canUndo());
|
||||||
|
if (toolButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo();
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}});
|
||||||
|
items.push_back({toolbarButtonWidth(icon::ARROW_CLOCKWISE), [this]() {
|
||||||
|
ImGui::BeginDisabled(!zoom_undo_stack_.canRedo());
|
||||||
|
if (toolButton("redo_zoom", icon::ARROW_CLOCKWISE, "Redo Zoom")) zoom_undo_stack_.redo();
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}});
|
||||||
|
items.push_back({toolbarButtonWidth(std::string(icon::ZOOM_OUT) + " " + reset_zoom_text), [this, &reset_zoom_text]() {
|
||||||
|
if (toolButton("reset_zoom_btn", icon::ZOOM_OUT, "Reset Zoom", reset_zoom_text.c_str())) zoomReset();
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
items.push_back({toolbarButtonWidth(icon::X_SQUARE), [this]() {
|
||||||
|
ImGui::BeginDisabled(charts_.empty());
|
||||||
|
if (toolButton("remove_all_btn", icon::X_SQUARE, "Remove all charts")) removeAll();
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}});
|
||||||
|
const char *dock_btn_icon = is_docked_ ? icon::ARROW_UP_RIGHT_SQUARE : icon::ARROW_DOWN_LEFT_SQUARE;
|
||||||
|
items.push_back({toolbarButtonWidth(dock_btn_icon), [this, dock_btn_icon]() {
|
||||||
|
if (toolButton("dock_btn", dock_btn_icon, is_docked_ ? "Float the charts window" : "Dock the charts window")) toggleChartsDocking();
|
||||||
|
}});
|
||||||
|
|
||||||
|
|
||||||
|
if (slider_index != (size_t)-1) {
|
||||||
|
const float shrink = std::min(slider_width - MIN_RANGE_SLIDER_WIDTH, toolbarWidth(items, spacer_index) - ImGui::GetContentRegionAvail().x);
|
||||||
|
if (shrink > 0.0f) {
|
||||||
|
slider_width -= shrink;
|
||||||
|
items[slider_index].width = slider_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawToolbar(items, spacer_index);
|
||||||
|
endToolbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::settingChanged() {
|
||||||
|
if (range_slider_.maximum() != settings.max_cached_minutes * 60) {
|
||||||
|
range_slider_.setRange(1, settings.max_cached_minutes * 60);
|
||||||
|
}
|
||||||
|
for (auto &c : charts_) {
|
||||||
|
c->setSeriesType((SeriesType)settings.chart_series_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) {
|
||||||
|
for (auto &c : charts_)
|
||||||
|
if (c->hasSignal(id, sig)) return c.get();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartView *ChartsWidget::createChart(int pos) {
|
||||||
|
auto chart = std::make_unique<ChartView>(can->timeRange().value_or(display_range_), this);
|
||||||
|
ChartView *ptr = chart.get();
|
||||||
|
pos = std::clamp(pos, 0, (int)charts_.size());
|
||||||
|
charts_.insert(charts_.begin() + pos, std::move(chart));
|
||||||
|
currentCharts().insert(currentCharts().begin() + pos, ptr);
|
||||||
|
updateLayout();
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) {
|
||||||
|
ChartView *chart = findChart(id, sig);
|
||||||
|
if (show && !chart) {
|
||||||
|
chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart();
|
||||||
|
chart->addSignal(id, sig);
|
||||||
|
updateState();
|
||||||
|
} else if (!show && chart) {
|
||||||
|
chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::splitChart(ChartView *src_chart) {
|
||||||
|
if (src_chart->signals().size() > 1) {
|
||||||
|
auto it = std::find_if(charts_.begin(), charts_.end(), [src_chart](auto &c) { return c.get() == src_chart; });
|
||||||
|
const int pos = it - charts_.begin() + 1;
|
||||||
|
for (auto &s : src_chart->takeExtraSignals()) {
|
||||||
|
createChart(pos)->adoptSignal(std::move(s));
|
||||||
|
}
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> ChartsWidget::serializeChartIds() const {
|
||||||
|
std::vector<std::string> chart_ids;
|
||||||
|
for (auto &c : charts_) {
|
||||||
|
std::string ids;
|
||||||
|
for (const auto &s : c->signals()) {
|
||||||
|
if (!ids.empty()) ids += ',';
|
||||||
|
ids += s.msg_id.toString() + "|" + s.sig->name;
|
||||||
|
}
|
||||||
|
chart_ids.push_back(ids);
|
||||||
|
}
|
||||||
|
std::reverse(chart_ids.begin(), chart_ids.end());
|
||||||
|
return chart_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::restoreChartsFromIds(const std::vector<std::string> &chart_ids) {
|
||||||
|
for (const auto &chart_id : chart_ids) {
|
||||||
|
int index = 0;
|
||||||
|
for (const auto &part : utils::split(chart_id, ',')) {
|
||||||
|
const size_t sep = part.find('|');
|
||||||
|
if (sep == std::string::npos) continue;
|
||||||
|
MessageId msg_id = MessageId::fromString(part.substr(0, sep));
|
||||||
|
if (auto *msg = dbc()->msg(msg_id))
|
||||||
|
if (auto *sig = msg->sig(part.substr(sep + 1)))
|
||||||
|
showChart(msg_id, sig, true, index++ > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::setColumnCount(int n) {
|
||||||
|
n = std::clamp(n, 1, MAX_COLUMN_COUNT);
|
||||||
|
if (column_count_ != n) {
|
||||||
|
column_count_ = settings.chart_column_count = n;
|
||||||
|
updateLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::updateLayout() {
|
||||||
|
|
||||||
|
const float container_width = charts_container_.geometry().GetWidth();
|
||||||
|
if (container_width <= 0) return;
|
||||||
|
|
||||||
|
int n = MAX_COLUMN_COUNT;
|
||||||
|
for (; n > 1; --n) {
|
||||||
|
if ((n * CHART_MIN_WIDTH + (n - 1) * CHART_SPACING) < container_width) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
columns_action_visible_ = n > 1;
|
||||||
|
current_column_count_ = std::min(column_count_, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::startChartDrag(ChartView *chart, const ImVec2 &global_pos) {
|
||||||
|
stopAutoScroll();
|
||||||
|
drag_ = {.source = chart, .press_pos = global_pos};
|
||||||
|
showValueTip(-1);
|
||||||
|
|
||||||
|
drag_preview_size_ = ImVec2(CHART_MIN_WIDTH, (float)settings.chart_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::dragChartMove(const ImVec2 &global_pos) {
|
||||||
|
if (!drag_.active) {
|
||||||
|
ImVec2 d = global_pos - drag_.press_pos;
|
||||||
|
if (std::abs(d.x) + std::abs(d.y) < START_DRAG_DISTANCE) return;
|
||||||
|
drag_.active = true;
|
||||||
|
drag_preview_visible_ = true;
|
||||||
|
}
|
||||||
|
drag_preview_pos_ = global_pos + ImVec2(5, 5);
|
||||||
|
|
||||||
|
|
||||||
|
int tab = tabbar_.tabAt(global_pos);
|
||||||
|
if (tab >= 0 && tab != tabbar_.currentIndex()) {
|
||||||
|
tabbar_.setCurrentIndex(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartView *target = nullptr;
|
||||||
|
for (auto c : currentCharts()) {
|
||||||
|
if (c != drag_.source && c->rect().Contains(global_pos)) {
|
||||||
|
target = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (std::exchange(drop_target_, target) != target) {
|
||||||
|
for (auto &c : charts_) c->setDropHighlight(c.get() == target);
|
||||||
|
}
|
||||||
|
bool in_viewport = charts_scroll_viewport_.Contains(global_pos);
|
||||||
|
bool on_background = !target && in_viewport && !charts_container_.childAt(global_pos);
|
||||||
|
charts_container_.setDropIndicator(on_background ? global_pos : ImVec2());
|
||||||
|
|
||||||
|
if (in_viewport) {
|
||||||
|
startAutoScroll(global_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::cancelChartDrag() {
|
||||||
|
drag_ = {};
|
||||||
|
stopAutoScroll();
|
||||||
|
drag_preview_visible_ = false;
|
||||||
|
charts_container_.setDropIndicator({});
|
||||||
|
if (auto target = std::exchange(drop_target_, nullptr)) target->setDropHighlight(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::dragChartRelease(const ImVec2 &global_pos) {
|
||||||
|
ChartView *source = drag_.source;
|
||||||
|
bool active = drag_.active;
|
||||||
|
ChartView *target = drop_target_;
|
||||||
|
cancelChartDrag();
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
bool in_viewport = charts_scroll_viewport_.Contains(global_pos);
|
||||||
|
if (target) {
|
||||||
|
|
||||||
|
target->takeSignalsFrom(source);
|
||||||
|
} else if (in_viewport && !charts_container_.childAt(global_pos)) {
|
||||||
|
|
||||||
|
auto w = charts_container_.getDropAfter(global_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();
|
||||||
|
updateTabBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::drawDragPreview() {
|
||||||
|
if (!drag_preview_visible_ || !drag_.source) return;
|
||||||
|
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(drag_preview_pos_);
|
||||||
|
ImGui::SetNextWindowSize(drag_preview_size_);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||||
|
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoDecoration |
|
||||||
|
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
|
||||||
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking;
|
||||||
|
if (ImGui::Begin("##chart_drag_ghost", nullptr, flags)) {
|
||||||
|
drag_.source->drawGhost(drag_preview_size_.x);
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
ImGui::PopStyleVar(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::startAutoScroll(const ImVec2 &global_pos) {
|
||||||
|
auto_scroll_pos_ = global_pos;
|
||||||
|
if (!auto_scroll_timer_active_) auto_scroll_timer_next_ = ImGui::GetTime() + 0.05;
|
||||||
|
auto_scroll_timer_active_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::stopAutoScroll() {
|
||||||
|
auto_scroll_timer_active_ = false;
|
||||||
|
auto_scroll_count_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::doAutoScroll() {
|
||||||
|
if (!charts_scroll_) return;
|
||||||
|
const int page_step = charts_scroll_viewport_.GetHeight();
|
||||||
|
if (auto_scroll_count_ < page_step) {
|
||||||
|
++auto_scroll_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
int value = charts_scroll_->Scroll.y;
|
||||||
|
ImVec2 pos = auto_scroll_pos_;
|
||||||
|
ImRect area = charts_scroll_viewport_;
|
||||||
|
|
||||||
|
int new_value = value;
|
||||||
|
if (pos.y - area.Min.y < settings.chart_height / 2) {
|
||||||
|
new_value = value - auto_scroll_count_;
|
||||||
|
} else if (area.Max.y - pos.y < settings.chart_height / 2) {
|
||||||
|
new_value = value + auto_scroll_count_;
|
||||||
|
}
|
||||||
|
new_value = std::clamp<int>(new_value, 0, charts_scroll_->ScrollMax.y);
|
||||||
|
if (new_value != value) ImGui::SetScrollY(charts_scroll_, new_value);
|
||||||
|
if (value == new_value) {
|
||||||
|
stopAutoScroll();
|
||||||
|
} else if (chartDragActive()) {
|
||||||
|
|
||||||
|
dragChartMove(auto_scroll_pos_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::newChart() {
|
||||||
|
execSignalSelector(std::make_unique<SignalSelector>("New Chart"), nullptr, [this](SignalSelector &dlg) {
|
||||||
|
const auto &items = dlg.selectedItems();
|
||||||
|
if (!items.empty()) {
|
||||||
|
auto c = createChart();
|
||||||
|
for (const auto &it : items) {
|
||||||
|
c->addSignal(it.msg_id, it.sig);
|
||||||
|
}
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::execSignalSelector(std::unique_ptr<SignalSelector> dlg, ChartView *owner, std::function<void(SignalSelector &)> accepted) {
|
||||||
|
signal_selector_ = std::move(dlg);
|
||||||
|
signal_selector_owner_ = owner;
|
||||||
|
signal_selector_accepted_ = std::move(accepted);
|
||||||
|
signal_selector_->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::removeChart(ChartView *chart) {
|
||||||
|
if (drag_.source == chart) cancelChartDrag();
|
||||||
|
if (drop_target_ == chart) drop_target_ = nullptr;
|
||||||
|
if (signal_selector_owner_ == chart) {
|
||||||
|
signal_selector_owner_ = nullptr;
|
||||||
|
signal_selector_accepted_ = nullptr;
|
||||||
|
}
|
||||||
|
auto it = std::find_if(charts_.begin(), charts_.end(), [chart](auto &c) { return c.get() == chart; });
|
||||||
|
if (it != charts_.end()) {
|
||||||
|
deleted_charts_.push_back(std::move(*it));
|
||||||
|
charts_.erase(it);
|
||||||
|
}
|
||||||
|
for (auto &[_, list] : tab_charts_) {
|
||||||
|
list.erase(std::remove(list.begin(), list.end(), chart), list.end());
|
||||||
|
}
|
||||||
|
updateLayout();
|
||||||
|
seriesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::removeAll() {
|
||||||
|
while (tabbar_.count() > 1) {
|
||||||
|
tabbar_.removeTab(1);
|
||||||
|
}
|
||||||
|
std::vector<ChartView *> all;
|
||||||
|
for (auto &c : charts_) all.push_back(c.get());
|
||||||
|
for (auto c : all) removeChart(c);
|
||||||
|
tab_charts_.clear();
|
||||||
|
zoomReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::handleEvents() {
|
||||||
|
|
||||||
|
if (ImGui::IsMouseClicked(3) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
|
||||||
|
zoom_undo_stack_.undo();
|
||||||
|
}
|
||||||
|
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) {
|
||||||
|
if (chartDragActive()) cancelChartDrag();
|
||||||
|
showValueTip(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (chartDragActive()) {
|
||||||
|
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||||
|
dragChartMove(ImGui::GetMousePos());
|
||||||
|
} else {
|
||||||
|
dragChartRelease(ImGui::GetMousePos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value_tip_visible_) return;
|
||||||
|
|
||||||
|
|
||||||
|
const ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||||
|
if (!any_plot_hovered_ &&
|
||||||
|
(delta.x != 0 || delta.y != 0 || !ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))) {
|
||||||
|
showValueTip(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsWidget::draw() {
|
||||||
|
deleted_charts_.clear();
|
||||||
|
|
||||||
|
|
||||||
|
if (float_window_init_ && !is_docked_) {
|
||||||
|
float_window_init_ = false;
|
||||||
|
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||||
|
const ImVec2 size(viewport->WorkSize.x * 0.6f, viewport->WorkSize.y * 0.6f);
|
||||||
|
ImGui::SetWindowSize(size);
|
||||||
|
ImGui::SetWindowPos(viewport->WorkPos + (viewport->WorkSize - size) * 0.5f);
|
||||||
|
}
|
||||||
|
ImGui::PushID(this);
|
||||||
|
if (auto_scroll_timer_active_ && ImGui::GetTime() >= auto_scroll_timer_next_) {
|
||||||
|
auto_scroll_timer_next_ = ImGui::GetTime() + 0.05;
|
||||||
|
doAutoScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleEvents();
|
||||||
|
|
||||||
|
drawToolBar();
|
||||||
|
tabbar_.draw();
|
||||||
|
|
||||||
|
any_plot_hovered_ = false;
|
||||||
|
if (ImGui::BeginChild("charts_scroll", ImVec2(0, 0), ImGuiChildFlags_None, 0)) {
|
||||||
|
charts_scroll_ = ImGui::GetCurrentWindow();
|
||||||
|
charts_scroll_viewport_ = charts_scroll_->InnerRect;
|
||||||
|
charts_container_.draw();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
drawDragPreview();
|
||||||
|
|
||||||
|
if (signal_selector_ && !signal_selector_->draw()) {
|
||||||
|
auto dlg = std::move(signal_selector_);
|
||||||
|
auto accepted = std::move(signal_selector_accepted_);
|
||||||
|
signal_selector_owner_ = nullptr;
|
||||||
|
if (dlg->accepted() && accepted) accepted(*dlg);
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsContainer::draw() {
|
||||||
|
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||||
|
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||||
|
geometry_ = ImRect(start, start + ImVec2(window->InnerRect.GetWidth(), 0));
|
||||||
|
charts_widget_->updateLayout();
|
||||||
|
|
||||||
|
const int n = std::max(charts_widget_->current_column_count_, 1);
|
||||||
|
const float spacing = CHART_SPACING;
|
||||||
|
const float width = (geometry_.GetWidth() - (n - 1) * spacing) / n;
|
||||||
|
const ImVec2 origin = ImGui::GetCursorScreenPos() + ImVec2(0, CHART_SPACING);
|
||||||
|
auto current_charts = charts_widget_->currentCharts();
|
||||||
|
float bottom = origin.y;
|
||||||
|
const bool aligned = ImPlot::BeginAlignedPlots("charts_align", true);
|
||||||
|
for (int i = 0; i < current_charts.size(); ++i) {
|
||||||
|
ImVec2 pos = origin + ImVec2((i % n) * (width + spacing), (i / n) * (settings.chart_height + spacing));
|
||||||
|
ImGui::SetCursorScreenPos(pos);
|
||||||
|
current_charts[i]->draw(width);
|
||||||
|
bottom = std::max(bottom, pos.y + settings.chart_height);
|
||||||
|
if (current_charts[i]->plotHovered()) charts_widget_->any_plot_hovered_ = true;
|
||||||
|
}
|
||||||
|
if (aligned) ImPlot::EndAlignedPlots();
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(origin.x, bottom));
|
||||||
|
ImGui::Dummy(ImVec2(geometry_.GetWidth(), CHART_SPACING));
|
||||||
|
geometry_.Max.y = bottom + CHART_SPACING;
|
||||||
|
drawDropIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChartsContainer::drawDropIndicator() {
|
||||||
|
if (!(drop_indicator_pos_.x == 0 && drop_indicator_pos_.y == 0) && !childAt(drop_indicator_pos_)) {
|
||||||
|
ImRect r = geometry_;
|
||||||
|
r.Max.y = r.Min.y + CHART_SPACING;
|
||||||
|
if (auto insert_after = getDropAfter(drop_indicator_pos_)) {
|
||||||
|
float h = r.GetHeight();
|
||||||
|
r.Min.y = insert_after->rect().Max.y;
|
||||||
|
r.Max.y = r.Min.y + h;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::GetWindowDrawList()->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_Header));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartView *ChartsContainer::getDropAfter(const ImVec2 &pos) const {
|
||||||
|
const auto &charts = charts_widget_->currentCharts();
|
||||||
|
auto it = std::find_if(charts.crbegin(), charts.crend(), [&pos](auto c) {
|
||||||
|
const ImRect &area = c->rect();
|
||||||
|
return pos.x >= area.Min.x && pos.x <= area.Max.x && pos.y >= area.Max.y;
|
||||||
|
});
|
||||||
|
return it == charts.crend() ? nullptr : *it;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartView *ChartsContainer::childAt(const ImVec2 &pos) const {
|
||||||
|
for (auto c : charts_widget_->currentCharts()) {
|
||||||
|
if (c->rect().Contains(pos)) return c;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
168
iqpilot/tools/cabana/ui/chart/chartswidget.h
Normal file
168
iqpilot/tools/cabana/ui/chart/chartswidget.h
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
|
||||||
|
#include "tools/cabana/ui/chart/signalselector.h"
|
||||||
|
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||||
|
#include "tools/cabana/commands.h"
|
||||||
|
#include "tools/cabana/dbc/dbcmanager.h"
|
||||||
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
|
#include "tools/cabana/utils/util.h"
|
||||||
|
|
||||||
|
const int CHART_MIN_WIDTH = 300;
|
||||||
|
|
||||||
|
|
||||||
|
class LogSlider {
|
||||||
|
public:
|
||||||
|
LogSlider(double factor) : scale_(factor) {}
|
||||||
|
|
||||||
|
void setRange(double min, double max) {
|
||||||
|
scale_.setRange(min, max);
|
||||||
|
min_ = min;
|
||||||
|
max_ = max;
|
||||||
|
setValue(pos_);
|
||||||
|
}
|
||||||
|
int value() const { return scale_.value(pos_, minimum(), maximum()); }
|
||||||
|
void setValue(int v) { pos_ = scale_.position(v, minimum(), maximum()); }
|
||||||
|
int minimum() const { return min_; }
|
||||||
|
int maximum() const { return max_; }
|
||||||
|
bool draw(const char *label, float width);
|
||||||
|
|
||||||
|
private:
|
||||||
|
LogScale scale_;
|
||||||
|
int min_ = 0;
|
||||||
|
int max_ = 1;
|
||||||
|
int pos_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChartView;
|
||||||
|
class ChartsWidget;
|
||||||
|
|
||||||
|
class ChartsContainer {
|
||||||
|
public:
|
||||||
|
ChartsContainer(ChartsWidget *parent) : charts_widget_(parent) {}
|
||||||
|
void setDropIndicator(const ImVec2 &pt) { drop_indicator_pos_ = pt; }
|
||||||
|
void draw();
|
||||||
|
ChartView *getDropAfter(const ImVec2 &pos) const;
|
||||||
|
ChartView *childAt(const ImVec2 &pos) const;
|
||||||
|
const ImRect &geometry() const { return geometry_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void drawDropIndicator();
|
||||||
|
|
||||||
|
ImRect geometry_;
|
||||||
|
ChartsWidget *charts_widget_;
|
||||||
|
ImVec2 drop_indicator_pos_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChartsWidget {
|
||||||
|
public:
|
||||||
|
ChartsWidget();
|
||||||
|
~ChartsWidget();
|
||||||
|
void draw();
|
||||||
|
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
|
||||||
|
inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; }
|
||||||
|
std::vector<std::string> serializeChartIds() const;
|
||||||
|
void restoreChartsFromIds(const std::vector<std::string> &chart_ids);
|
||||||
|
std::string whatsThis() const;
|
||||||
|
|
||||||
|
void setColumnCount(int n);
|
||||||
|
void removeAll();
|
||||||
|
void setIsDocked(bool dock);
|
||||||
|
|
||||||
|
Observable<> toggleChartsDocking;
|
||||||
|
Observable<> seriesChanged;
|
||||||
|
Observable<double> showTip;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleEvents();
|
||||||
|
void newChart();
|
||||||
|
ChartView *createChart(int pos = 0);
|
||||||
|
void removeChart(ChartView *chart);
|
||||||
|
void splitChart(ChartView *chart);
|
||||||
|
ImRect chartVisibleRect(ChartView *chart);
|
||||||
|
void eventsMerged(const MessageEventsMap &new_events);
|
||||||
|
void updateState();
|
||||||
|
void zoomReset();
|
||||||
|
void startChartDrag(ChartView *chart, const ImVec2 &global_pos);
|
||||||
|
void dragChartMove(const ImVec2 &global_pos);
|
||||||
|
void dragChartRelease(const ImVec2 &global_pos);
|
||||||
|
void cancelChartDrag();
|
||||||
|
bool chartDragActive() const { return drag_.source != nullptr; }
|
||||||
|
void startAutoScroll(const ImVec2 &global_pos);
|
||||||
|
void stopAutoScroll();
|
||||||
|
void doAutoScroll();
|
||||||
|
void drawToolBar();
|
||||||
|
void updateTabBar();
|
||||||
|
void setMaxChartRange(int value);
|
||||||
|
void updateLayout();
|
||||||
|
void settingChanged();
|
||||||
|
void showValueTip(double sec);
|
||||||
|
void newTab();
|
||||||
|
void removeTab(int index);
|
||||||
|
inline std::vector<ChartView *> ¤tCharts() { return tab_charts_[tabbar_.tabData(tabbar_.currentIndex())]; }
|
||||||
|
ChartView *findChart(const MessageId &id, const cabana::Signal *sig);
|
||||||
|
|
||||||
|
void execSignalSelector(std::unique_ptr<SignalSelector> dlg, ChartView *owner, std::function<void(SignalSelector &)> accepted);
|
||||||
|
void drawDragPreview();
|
||||||
|
|
||||||
|
LogSlider range_slider_{1000};
|
||||||
|
bool is_docked_ = true;
|
||||||
|
bool float_window_init_ = false;
|
||||||
|
|
||||||
|
UndoStack zoom_undo_stack_;
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<ChartView>> charts_;
|
||||||
|
std::unordered_map<int, std::vector<ChartView *>> tab_charts_;
|
||||||
|
TabBar tabbar_;
|
||||||
|
ChartsContainer charts_container_{this};
|
||||||
|
ImGuiWindow *charts_scroll_ = nullptr;
|
||||||
|
ImRect charts_scroll_viewport_;
|
||||||
|
int max_chart_range_ = 0;
|
||||||
|
std::pair<double, double> display_range_;
|
||||||
|
bool columns_action_visible_ = false;
|
||||||
|
int column_count_ = 1;
|
||||||
|
int current_column_count_ = 0;
|
||||||
|
struct ChartDrag {
|
||||||
|
ChartView *source = nullptr;
|
||||||
|
ImVec2 press_pos;
|
||||||
|
bool active = false;
|
||||||
|
} drag_;
|
||||||
|
|
||||||
|
ImVec2 drag_preview_pos_;
|
||||||
|
ImVec2 drag_preview_size_;
|
||||||
|
bool drag_preview_visible_ = false;
|
||||||
|
ChartView *drop_target_ = nullptr;
|
||||||
|
int auto_scroll_count_ = 0;
|
||||||
|
ImVec2 auto_scroll_pos_;
|
||||||
|
bool auto_scroll_timer_active_ = false;
|
||||||
|
double auto_scroll_timer_next_ = 0;
|
||||||
|
bool value_tip_visible_ = false;
|
||||||
|
bool any_plot_hovered_ = false;
|
||||||
|
std::vector<std::unique_ptr<ChartView>> deleted_charts_;
|
||||||
|
std::unique_ptr<SignalSelector> signal_selector_;
|
||||||
|
ChartView *signal_selector_owner_ = nullptr;
|
||||||
|
std::function<void(SignalSelector &)> signal_selector_accepted_;
|
||||||
|
Connections connections_;
|
||||||
|
friend class ChartView;
|
||||||
|
friend class ChartsContainer;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ZoomCommand : public UndoCommand {
|
||||||
|
public:
|
||||||
|
ZoomCommand(std::pair<double, double> range) : range(range) {
|
||||||
|
prev_range = can->timeRange();
|
||||||
|
}
|
||||||
|
void undo() override { can->setTimeRange(prev_range); }
|
||||||
|
void redo() override { can->setTimeRange(range); }
|
||||||
|
std::optional<std::pair<double, double>> prev_range, range;
|
||||||
|
};
|
||||||
155
iqpilot/tools/cabana/ui/chart/signalselector.cc
Normal file
155
iqpilot/tools/cabana/ui/chart/signalselector.cc
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#include "tools/cabana/ui/chart/signalselector.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cfloat>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
|
#include "tools/cabana/ui/chart/chart.h"
|
||||||
|
#include "tools/cabana/ui/icons.h"
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
#include "tools/cabana/utils/strings.h"
|
||||||
|
|
||||||
|
SignalSelector::SignalSelector(std::string title) : title_(std::move(title)) {
|
||||||
|
for (const auto &[id, _] : can->lastMessages()) {
|
||||||
|
if (auto m = dbc()->msg(id)) {
|
||||||
|
msgs_combo_.push_back({m->name + " (" + id.toString() + ")", id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(msgs_combo_.begin(), msgs_combo_.end(), [](auto &a, auto &b) { return a.text < b.text; });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SignalSelector::draw() {
|
||||||
|
if (!open_) return false;
|
||||||
|
const std::string popup_id = title_ + "###SignalSelector";
|
||||||
|
if (!show_) {
|
||||||
|
ImGui::OpenPopup(popup_id.c_str());
|
||||||
|
show_ = true;
|
||||||
|
}
|
||||||
|
setNextDialogWindow(ImVec2(700.0f, 450.0f));
|
||||||
|
if (!ImGui::BeginPopupModal(popup_id.c_str(), nullptr, ImGuiWindowFlags_NoSavedSettings)) {
|
||||||
|
open_ = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float btn_w = ImGui::GetFrameHeight() + 8.0f;
|
||||||
|
const float column_w = (ImGui::GetContentRegionAvail().x - btn_w - ImGui::GetStyle().ItemSpacing.x * 2) / 2;
|
||||||
|
|
||||||
|
const float lists_h = ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing() * 3;
|
||||||
|
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
ImGui::TextUnformatted("Available Signals");
|
||||||
|
|
||||||
|
const char *preview = msgs_combo_index_ >= 0 ? msgs_combo_[msgs_combo_index_].text.c_str() : "Select a msg...";
|
||||||
|
ImGui::SetNextItemWidth(column_w);
|
||||||
|
if (ImGui::BeginCombo("##msgs_combo", preview)) {
|
||||||
|
if (ImGui::IsWindowAppearing()) {
|
||||||
|
msgs_combo_filter_.clear();
|
||||||
|
ImGui::SetKeyboardFocusHere();
|
||||||
|
}
|
||||||
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||||
|
inputText("##msgs_filter", &msgs_combo_filter_, "Select a msg...");
|
||||||
|
for (int i = 0; i < (int)msgs_combo_.size(); ++i) {
|
||||||
|
if (!msgs_combo_filter_.empty() && !utils::containsCI(msgs_combo_[i].text, msgs_combo_filter_)) continue;
|
||||||
|
if (ImGui::Selectable(msgs_combo_[i].text.c_str(), i == msgs_combo_index_)) {
|
||||||
|
msgs_combo_index_ = i;
|
||||||
|
updateAvailableList(i);
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
bool add_dbl = false;
|
||||||
|
drawList("##available_list", available_list_, &available_row_, false, &add_dbl, ImVec2(column_w, lists_h));
|
||||||
|
ImGui::EndGroup();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
ImGui::Dummy(ImVec2(btn_w, (lists_h + ImGui::GetFrameHeightWithSpacing() * 2) / 2 - ImGui::GetFrameHeight()));
|
||||||
|
ImGui::BeginDisabled(available_row_ == -1);
|
||||||
|
bool add_clicked = ImGui::Button(icon::CHEVRON_RIGHT, ImVec2(btn_w, 0));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::BeginDisabled(selected_row_ == -1);
|
||||||
|
bool remove_clicked = ImGui::Button(icon::CHEVRON_LEFT, ImVec2(btn_w, 0));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::EndGroup();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
ImGui::TextUnformatted("Selected Signals");
|
||||||
|
bool remove_dbl = false;
|
||||||
|
drawList("##selected_list", selected_list_, &selected_row_, true, &remove_dbl, ImVec2(column_w, lists_h + ImGui::GetFrameHeightWithSpacing()));
|
||||||
|
bool rejected = false;
|
||||||
|
dialogButtons("OK", &accepted_, &rejected);
|
||||||
|
const bool done = accepted_ || rejected;
|
||||||
|
ImGui::EndGroup();
|
||||||
|
|
||||||
|
if ((add_dbl || add_clicked) && available_row_ >= 0 && available_row_ < (int)available_list_.size()) {
|
||||||
|
add(available_row_);
|
||||||
|
} else if ((remove_dbl || remove_clicked) && selected_row_ >= 0 && selected_row_ < (int)selected_list_.size()) {
|
||||||
|
remove(selected_row_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
open_ = false;
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
return open_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalSelector::drawList(const char *id, std::vector<ListItem> &list, int *current_row, bool show_msg_name, bool *double_clicked, const ImVec2 &size) {
|
||||||
|
if (!ImGui::BeginListBox(id, size)) return;
|
||||||
|
for (int i = 0; i < (int)list.size(); ++i) {
|
||||||
|
const auto &item = list[i];
|
||||||
|
ImGui::PushID(i);
|
||||||
|
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||||
|
if (ImGui::Selectable("##item", i == *current_row)) *current_row = i;
|
||||||
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||||
|
*current_row = i;
|
||||||
|
*double_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||||
|
float x = pos.x + 5;
|
||||||
|
drawColorMarker(dl, ImVec2(x, pos.y), toImU32(item.sig->color));
|
||||||
|
x += markerSize() + 4;
|
||||||
|
dl->AddText(ImVec2(x, pos.y), ImGui::GetColorU32(ImGuiCol_Text), item.sig->name.c_str());
|
||||||
|
if (show_msg_name) {
|
||||||
|
x += ImGui::CalcTextSize(item.sig->name.c_str()).x;
|
||||||
|
dl->AddText(ImVec2(x, pos.y), ImGui::GetColorU32(ImGuiCol_TextDisabled), msgLabel(item.msg_id).c_str());
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndListBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalSelector::add(int row) {
|
||||||
|
const auto &item = available_list_[row];
|
||||||
|
selected_list_.emplace_back(item.msg_id, item.sig);
|
||||||
|
available_list_.erase(available_list_.begin() + row);
|
||||||
|
available_row_ = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalSelector::remove(int row) {
|
||||||
|
const auto &item = selected_list_[row];
|
||||||
|
if (msgs_combo_index_ >= 0 && item.msg_id == msgs_combo_[msgs_combo_index_].id) {
|
||||||
|
available_list_.emplace_back(item.msg_id, item.sig);
|
||||||
|
}
|
||||||
|
selected_list_.erase(selected_list_.begin() + row);
|
||||||
|
selected_row_ = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalSelector::updateAvailableList(int index) {
|
||||||
|
if (index == -1) return;
|
||||||
|
available_list_.clear();
|
||||||
|
available_row_ = -1;
|
||||||
|
MessageId msg_id = msgs_combo_[index].id;
|
||||||
|
for (auto s : dbc()->msg(msg_id)->getSignals()) {
|
||||||
|
bool is_selected = std::any_of(selected_list_.begin(), selected_list_.end(),
|
||||||
|
[sig = s, &msg_id](auto &it) { return it.msg_id == msg_id && it.sig == sig; });
|
||||||
|
if (!is_selected) {
|
||||||
|
available_list_.emplace_back(msg_id, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
iqpilot/tools/cabana/ui/chart/signalselector.h
Normal file
46
iqpilot/tools/cabana/ui/chart/signalselector.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "tools/cabana/dbc/dbcmanager.h"
|
||||||
|
|
||||||
|
|
||||||
|
class SignalSelector {
|
||||||
|
public:
|
||||||
|
struct ListItem {
|
||||||
|
ListItem(const MessageId &msg_id, const cabana::Signal *sig) : msg_id(msg_id), sig(sig) {}
|
||||||
|
MessageId msg_id;
|
||||||
|
const cabana::Signal *sig;
|
||||||
|
};
|
||||||
|
|
||||||
|
SignalSelector(std::string title);
|
||||||
|
const std::vector<ListItem> &selectedItems() const { return selected_list_; }
|
||||||
|
inline void addSelected(const MessageId &id, const cabana::Signal *sig) { selected_list_.emplace_back(id, sig); }
|
||||||
|
void open() { open_ = true; show_ = false; accepted_ = false; }
|
||||||
|
bool draw();
|
||||||
|
bool accepted() const { return accepted_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateAvailableList(int index);
|
||||||
|
void add(int row);
|
||||||
|
void remove(int row);
|
||||||
|
void drawList(const char *id, std::vector<ListItem> &list, int *current_row, bool show_msg_name, bool *double_clicked, const ImVec2 &size);
|
||||||
|
|
||||||
|
struct ComboItem {
|
||||||
|
std::string text;
|
||||||
|
MessageId id;
|
||||||
|
};
|
||||||
|
std::string title_;
|
||||||
|
std::vector<ComboItem> msgs_combo_;
|
||||||
|
int msgs_combo_index_ = -1;
|
||||||
|
std::string msgs_combo_filter_;
|
||||||
|
std::vector<ListItem> available_list_;
|
||||||
|
std::vector<ListItem> selected_list_;
|
||||||
|
int available_row_ = -1;
|
||||||
|
int selected_row_ = -1;
|
||||||
|
bool accepted_ = false;
|
||||||
|
bool open_ = false;
|
||||||
|
bool show_ = false;
|
||||||
|
};
|
||||||
177
iqpilot/tools/cabana/ui/chart/sparkline.cc
Normal file
177
iqpilot/tools/cabana/ui/chart/sparkline.cc
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#include "tools/cabana/ui/chart/sparkline.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
|
||||||
|
void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, ImVec2 sz,
|
||||||
|
double window_end) {
|
||||||
|
if (first == last || sz.x <= 0 || sz.y <= 0) {
|
||||||
|
render_points_.clear();
|
||||||
|
size = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
points_.clear();
|
||||||
|
min_val = std::numeric_limits<double>::max();
|
||||||
|
max_val = std::numeric_limits<double>::lowest();
|
||||||
|
points_.reserve(std::distance(first, last));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const double window_start = window_end - range;
|
||||||
|
double value = 0.0;
|
||||||
|
for (auto it = first; it != last; ++it) {
|
||||||
|
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
|
||||||
|
double x = can->toSeconds((*it)->mono_time) - window_start;
|
||||||
|
|
||||||
|
|
||||||
|
if (x >= 0.0) {
|
||||||
|
min_val = std::min(min_val, value);
|
||||||
|
max_val = std::max(max_val, value);
|
||||||
|
}
|
||||||
|
points_.push_back({x, value});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (min_val > max_val) {
|
||||||
|
for (const auto &p : points_) {
|
||||||
|
min_val = std::min(min_val, p.y);
|
||||||
|
max_val = std::max(max_val, p.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points_.empty()) {
|
||||||
|
render_points_.clear();
|
||||||
|
size = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
freq_ = points_.size() / std::max(points_.back().x - points_.front().x, 1.0);
|
||||||
|
render(sig->color, range, sz, window_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sparkline::render(const CabanaColor &color, int range, ImVec2 sz, double window_end) {
|
||||||
|
bool is_flat_line = min_val == max_val;
|
||||||
|
if (is_flat_line) {
|
||||||
|
min_val -= 1.0;
|
||||||
|
max_val += 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double xscale = (sz.x - 1) / (double)range;
|
||||||
|
const double yscale = (sz.y - 3) / (max_val - min_val);
|
||||||
|
const double span = points_.back().x - points_.front().x;
|
||||||
|
bool draw_individual_points = (span * xscale / points_.size()) > 8.0;
|
||||||
|
|
||||||
|
|
||||||
|
render_points_.reserve(points_.size());
|
||||||
|
render_points_.clear();
|
||||||
|
if (draw_individual_points) {
|
||||||
|
for (const auto &p : points_) {
|
||||||
|
render_points_.emplace_back(p.x * xscale, 1.0 + (max_val - p.y) * yscale);
|
||||||
|
}
|
||||||
|
} else if (is_flat_line) {
|
||||||
|
double y = sz.y / 2.0;
|
||||||
|
render_points_.emplace_back(points_.front().x * xscale, y);
|
||||||
|
render_points_.emplace_back(points_.back().x * xscale, y);
|
||||||
|
} else {
|
||||||
|
double prev_y = points_.front().y;
|
||||||
|
render_points_.emplace_back(points_.front().x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||||
|
bool in_flat = false;
|
||||||
|
|
||||||
|
for (size_t i = 1; i < points_.size(); ++i) {
|
||||||
|
const auto &p = points_[i];
|
||||||
|
double y = p.y;
|
||||||
|
if (std::abs(y - prev_y) < 1e-6) {
|
||||||
|
in_flat = true;
|
||||||
|
} else {
|
||||||
|
if (in_flat) render_points_.emplace_back(points_[i - 1].x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||||
|
render_points_.emplace_back(p.x * xscale, 1.0 + (max_val - y) * yscale);
|
||||||
|
in_flat = false;
|
||||||
|
}
|
||||||
|
prev_y = y;
|
||||||
|
}
|
||||||
|
if (in_flat) render_points_.emplace_back(points_.back().x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||||
|
}
|
||||||
|
|
||||||
|
size = sz;
|
||||||
|
CabanaColor line_color = color;
|
||||||
|
if (!isDarkTheme()) {
|
||||||
|
auto [h, s, v] = color.hsv();
|
||||||
|
line_color = CabanaColor::fromHsv(h, std::min(1.0f, s * 2.0f), v * 0.7f, color.a / 255.0f);
|
||||||
|
}
|
||||||
|
color_ = toImU32(line_color);
|
||||||
|
draw_individual_points_ = draw_individual_points;
|
||||||
|
window_end_ = window_end;
|
||||||
|
xscale_ = xscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sparkline::draw(ImDrawList *draw_list, ImVec2 pos) const {
|
||||||
|
if (render_points_.empty()) return;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const float shift = std::clamp((float)((can->currentSec() - window_end_) * xscale_), 0.0f, size.x);
|
||||||
|
|
||||||
|
|
||||||
|
const float px = 1.0f / std::max(1.0f, ImGui::GetIO().DisplayFramebufferScale.x);
|
||||||
|
auto snap = [&](float x) { return std::floor(x / px) * px; };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ImVec2 offset(pos.x - shift, pos.y);
|
||||||
|
const double k = offset.x - (window_end_ * xscale_ - (size.x - 1));
|
||||||
|
offset.x += snap(k) - k;
|
||||||
|
auto point_at = [&](const ImVec2 &p) { return ImVec2(offset.x + p.x, offset.y + p.y); };
|
||||||
|
|
||||||
|
draw_list->PushClipRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), true);
|
||||||
|
|
||||||
|
|
||||||
|
auto draw_point = [&](const ImVec2 &p) { draw_list->AddRectFilled(ImVec2(p.x - 1.5f, p.y - 1.5f), ImVec2(p.x + 1.5f, p.y + 1.5f), color_); };
|
||||||
|
|
||||||
|
if (draw_individual_points_) {
|
||||||
|
for (const auto &p : render_points_) {
|
||||||
|
draw_list->PathLineTo(point_at(p));
|
||||||
|
draw_point(point_at(p));
|
||||||
|
}
|
||||||
|
draw_list->PathStroke(color_, ImDrawFlags_None, 1.5f);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<ImVec2> pts;
|
||||||
|
pts.reserve(render_points_.size());
|
||||||
|
float col = -1e9f;
|
||||||
|
for (const auto &p : render_points_) {
|
||||||
|
ImVec2 sp = point_at(p);
|
||||||
|
float c = snap(sp.x);
|
||||||
|
if (c != col) {
|
||||||
|
pts.push_back(sp);
|
||||||
|
col = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
auto steep = [&](size_t i) { return std::abs(pts[i + 1].y - pts[i].y) > 2.0f * std::abs(pts[i + 1].x - pts[i].x) + px; };
|
||||||
|
const ImDrawListFlags saved = draw_list->Flags;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i + 1 < pts.size()) {
|
||||||
|
const bool is_steep = steep(i);
|
||||||
|
size_t j = i + 1;
|
||||||
|
while (j + 1 < pts.size() && steep(j) == is_steep) ++j;
|
||||||
|
draw_list->Flags = is_steep ? (saved & ~ImDrawListFlags_AntiAliasedLines) : saved;
|
||||||
|
for (size_t n = i; n <= j; ++n) draw_list->PathLineTo(pts[n]);
|
||||||
|
draw_list->PathStroke(color_, ImDrawFlags_None, 1.0f);
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
draw_list->Flags = saved;
|
||||||
|
draw_point(point_at(render_points_.back()));
|
||||||
|
}
|
||||||
|
draw_list->PopClipRect();
|
||||||
|
}
|
||||||
34
iqpilot/tools/cabana/ui/chart/sparkline.h
Normal file
34
iqpilot/tools/cabana/ui/chart/sparkline.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "tools/cabana/dbc/dbc.h"
|
||||||
|
#include "tools/cabana/streams/abstractstream.h"
|
||||||
|
|
||||||
|
class Sparkline {
|
||||||
|
public:
|
||||||
|
void update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, ImVec2 sz, double window_end);
|
||||||
|
inline double freq() const { return freq_; }
|
||||||
|
bool isEmpty() const { return render_points_.empty(); }
|
||||||
|
|
||||||
|
void draw(ImDrawList *draw_list, ImVec2 pos) const;
|
||||||
|
|
||||||
|
ImVec2 size = {};
|
||||||
|
double min_val = 0;
|
||||||
|
double max_val = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Point {
|
||||||
|
double x, y;
|
||||||
|
};
|
||||||
|
void render(const CabanaColor &color, int range, ImVec2 sz, double window_end);
|
||||||
|
|
||||||
|
std::vector<Point> points_;
|
||||||
|
std::vector<ImVec2> render_points_;
|
||||||
|
ImU32 color_ = 0;
|
||||||
|
double window_end_ = 0;
|
||||||
|
double xscale_ = 0;
|
||||||
|
bool draw_individual_points_ = false;
|
||||||
|
double freq_ = 0;
|
||||||
|
};
|
||||||
69
iqpilot/tools/cabana/ui/chart/tiplabel.cc
Normal file
69
iqpilot/tools/cabana/ui/chart/tiplabel.cc
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||||
|
#include "tools/cabana/ui/chart/tiplabel.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cfloat>
|
||||||
|
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
|
||||||
|
ImVec2 TipLabel::layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const {
|
||||||
|
ImFont *bold = boldFont();
|
||||||
|
const float font_size = ImGui::GetFontSize();
|
||||||
|
const float line_height = ImGui::GetTextLineHeight();
|
||||||
|
ImVec2 size(0, 0);
|
||||||
|
float y = origin.y;
|
||||||
|
for (const auto &line : text_) {
|
||||||
|
float x = origin.x;
|
||||||
|
if (line.has_marker) {
|
||||||
|
if (p) drawColorMarker(p, ImVec2(x, y), line.marker);
|
||||||
|
x += markerSize() + 4;
|
||||||
|
}
|
||||||
|
if (p) p->AddText(ImVec2(x, y), fg, line.name.c_str());
|
||||||
|
x += ImGui::CalcTextSize(line.name.c_str()).x;
|
||||||
|
if (!line.bold.empty()) {
|
||||||
|
if (p) p->AddText(bold, font_size, ImVec2(x, y), fg, line.bold.c_str());
|
||||||
|
x += bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, line.bold.c_str()).x;
|
||||||
|
}
|
||||||
|
if (p) p->AddText(ImVec2(x, y), fg, line.rest.c_str());
|
||||||
|
x += ImGui::CalcTextSize(line.rest.c_str()).x;
|
||||||
|
size.x = std::max(size.x, x - origin.x);
|
||||||
|
y += line_height;
|
||||||
|
}
|
||||||
|
size.y = y - origin.y;
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 TipLabel::sizeHint() const {
|
||||||
|
return layoutLines(nullptr, ImVec2(0, 0), 0) + ImVec2(MARGIN * 2, MARGIN * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TipLabel::showText(const ImVec2 &pt, const std::vector<TipLine> &text, const ImRect &rect) {
|
||||||
|
text_ = text;
|
||||||
|
if (!text_.empty()) {
|
||||||
|
ImVec2 extra(1, 1);
|
||||||
|
size_ = sizeHint() + extra;
|
||||||
|
ImVec2 tip_pos(pt.x + 8, rect.Min.y + 2);
|
||||||
|
if (tip_pos.x + size_.x >= rect.Max.x) {
|
||||||
|
tip_pos.x = pt.x - size_.x - 8;
|
||||||
|
}
|
||||||
|
if (rect.Contains(ImRect(tip_pos, tip_pos + size_))) {
|
||||||
|
pos_ = tip_pos;
|
||||||
|
visible_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visible_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TipLabel::draw() {
|
||||||
|
if (!visible_) return;
|
||||||
|
|
||||||
|
ImDrawList *p = ImGui::GetForegroundDrawList();
|
||||||
|
const bool dark = isDarkTheme();
|
||||||
|
const ImU32 bg = dark ? ImGui::GetColorU32(ImGuiCol_PopupBg) : ImGui::GetColorU32(ImGuiCol_ChildBg);
|
||||||
|
const ImU32 fg = dark ? ImGui::GetColorU32(ImGuiCol_Text) : IM_COL32(0x40, 0x40, 0x44, 0xff);
|
||||||
|
|
||||||
|
p->AddRectFilled(pos_, pos_ + size_, bg);
|
||||||
|
p->AddRect(pos_, pos_ + size_, ImGui::GetColorU32(ImGuiCol_Border));
|
||||||
|
layoutLines(p, pos_ + ImVec2(MARGIN, MARGIN), fg);
|
||||||
|
}
|
||||||
35
iqpilot/tools/cabana/ui/chart/tiplabel.h
Normal file
35
iqpilot/tools/cabana/ui/chart/tiplabel.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
|
||||||
|
|
||||||
|
struct TipLine {
|
||||||
|
bool has_marker = false;
|
||||||
|
ImU32 marker = 0;
|
||||||
|
std::string name;
|
||||||
|
std::string bold;
|
||||||
|
std::string rest;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TipLabel {
|
||||||
|
public:
|
||||||
|
void showText(const ImVec2 &pt, const std::vector<TipLine> &text, const ImRect &rect);
|
||||||
|
void hide() { visible_ = false; }
|
||||||
|
bool isVisible() const { return visible_; }
|
||||||
|
void draw();
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
ImVec2 layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const;
|
||||||
|
ImVec2 sizeHint() const;
|
||||||
|
|
||||||
|
static constexpr float MARGIN = 2.0f;
|
||||||
|
std::vector<TipLine> text_;
|
||||||
|
ImVec2 pos_;
|
||||||
|
ImVec2 size_;
|
||||||
|
bool visible_ = false;
|
||||||
|
};
|
||||||
222
iqpilot/tools/cabana/ui/dialogs/filedialog.cc
Normal file
222
iqpilot/tools/cabana/ui/dialogs/filedialog.cc
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
#include "tools/cabana/ui/dialogs/filedialog.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||||
|
#include "tools/cabana/ui/icons.h"
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace FileDialog {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bool naturalLess(const std::string &a, const std::string &b) {
|
||||||
|
auto skip = [](const std::string &s, size_t &i) {
|
||||||
|
while (i < s.size() && !isalnum(static_cast<unsigned char>(s[i]))) ++i;
|
||||||
|
};
|
||||||
|
size_t i = 0, j = 0;
|
||||||
|
for (;;) {
|
||||||
|
skip(a, i);
|
||||||
|
skip(b, j);
|
||||||
|
if (i >= a.size() || j >= b.size()) break;
|
||||||
|
if (isdigit(static_cast<unsigned char>(a[i])) && isdigit(static_cast<unsigned char>(b[j]))) {
|
||||||
|
size_t ie = i, je = j;
|
||||||
|
while (ie < a.size() && isdigit(static_cast<unsigned char>(a[ie]))) ++ie;
|
||||||
|
while (je < b.size() && isdigit(static_cast<unsigned char>(b[je]))) ++je;
|
||||||
|
const unsigned long long na = std::stoull(a.substr(i, ie - i)), nb = std::stoull(b.substr(j, je - j));
|
||||||
|
if (na != nb) return na < nb;
|
||||||
|
i = ie;
|
||||||
|
j = je;
|
||||||
|
} else {
|
||||||
|
const int ca = tolower(static_cast<unsigned char>(a[i])), cb = tolower(static_cast<unsigned char>(b[j]));
|
||||||
|
if (ca != cb) return ca < cb;
|
||||||
|
++i;
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool a_done = i >= a.size(), b_done = j >= b.size();
|
||||||
|
if (a_done != b_done) return a_done;
|
||||||
|
return a < b;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Mode { OpenFile, SaveFile, Directory };
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
bool active = false;
|
||||||
|
Mode mode = Mode::OpenFile;
|
||||||
|
std::string title;
|
||||||
|
std::string extension;
|
||||||
|
fs::path dir;
|
||||||
|
std::string dir_input;
|
||||||
|
std::string filename;
|
||||||
|
std::vector<fs::directory_entry> entries;
|
||||||
|
Callback callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
State g_state;
|
||||||
|
PopupOwner g_owner;
|
||||||
|
|
||||||
|
void listDir() {
|
||||||
|
State &s = g_state;
|
||||||
|
s.entries.clear();
|
||||||
|
std::error_code ec;
|
||||||
|
for (const auto &entry : fs::directory_iterator(s.dir, ec)) {
|
||||||
|
const std::string name = entry.path().filename().string();
|
||||||
|
if (name.empty() || name[0] == '.') continue;
|
||||||
|
const bool is_dir = entry.is_directory(ec);
|
||||||
|
if (!is_dir && s.mode == Mode::Directory) continue;
|
||||||
|
if (!is_dir && !s.extension.empty() && entry.path().extension() != s.extension) continue;
|
||||||
|
s.entries.push_back(entry);
|
||||||
|
}
|
||||||
|
std::sort(s.entries.begin(), s.entries.end(), [](const auto &a, const auto &b) {
|
||||||
|
std::error_code sort_ec;
|
||||||
|
const bool da = a.is_directory(sort_ec), db = b.is_directory(sort_ec);
|
||||||
|
return da != db ? da : naturalLess(a.path().filename().string(), b.path().filename().string());
|
||||||
|
});
|
||||||
|
s.dir_input = s.dir.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDir(const fs::path &dir) {
|
||||||
|
std::error_code ec;
|
||||||
|
fs::path d = fs::is_directory(dir, ec) ? fs::absolute(dir, ec) : fs::current_path(ec);
|
||||||
|
g_state.dir = d.lexically_normal();
|
||||||
|
listDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
void start(Mode mode, const std::string &title, const fs::path &dir, const std::string &filename,
|
||||||
|
const std::string &extension, Callback cb) {
|
||||||
|
State &s = g_state;
|
||||||
|
s = State{};
|
||||||
|
s.active = true;
|
||||||
|
s.mode = mode;
|
||||||
|
s.title = title;
|
||||||
|
s.extension = extension;
|
||||||
|
s.filename = filename;
|
||||||
|
s.callback = std::move(cb);
|
||||||
|
g_owner.reset();
|
||||||
|
setDir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish(const std::string &path) {
|
||||||
|
Callback cb = std::move(g_state.callback);
|
||||||
|
g_state = State{};
|
||||||
|
g_owner.reset();
|
||||||
|
if (cb) cb(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void accept(const fs::path &path) {
|
||||||
|
if (g_state.mode == Mode::SaveFile) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (fs::exists(path, ec)) {
|
||||||
|
const std::string name = path.filename().string();
|
||||||
|
MessageBox::question(g_state.title, name + " already exists.\nDo you want to replace it?", [path](bool ok) {
|
||||||
|
if (ok) finish(path.string());
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish(path.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void getOpenFileName(const std::string &title, const std::string &dir, const std::string &extension, Callback cb) {
|
||||||
|
start(Mode::OpenFile, title, dir, "", extension, std::move(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
void getSaveFileName(const std::string &title, const std::string &default_path, const std::string &extension, Callback cb) {
|
||||||
|
const fs::path p(default_path);
|
||||||
|
start(Mode::SaveFile, title, p.parent_path(), p.filename().string(), extension, std::move(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
void getExistingDirectory(const std::string &title, const std::string &dir, Callback cb) {
|
||||||
|
start(Mode::Directory, title, dir, "", "", std::move(cb));
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() {
|
||||||
|
State &s = g_state;
|
||||||
|
if (!s.active) return;
|
||||||
|
const std::string popup_id = s.title + "###FileDialog";
|
||||||
|
if (!beginDialog(popup_id.c_str(), &g_owner, ImVec2(640.0f, 480.0f), 0)) return;
|
||||||
|
|
||||||
|
if (ImGui::Button("Up")) setDir(s.dir.parent_path());
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
|
if (inputText("##dir", &s.dir_input, "", ImGuiInputTextFlags_EnterReturnsTrue)) setDir(s.dir_input);
|
||||||
|
|
||||||
|
const float footer = ImGui::GetFrameHeightWithSpacing() * (s.mode == Mode::Directory ? 1.0f : 2.0f) + ImGui::GetStyle().ItemSpacing.y;
|
||||||
|
bool ok = false, cancel = false;
|
||||||
|
fs::path result, pending_dir;
|
||||||
|
ImGui::BeginChild("entries", ImVec2(0, -footer), ImGuiChildFlags_Borders);
|
||||||
|
std::error_code dir_ec;
|
||||||
|
for (size_t i = 0; i < s.entries.size(); ++i) {
|
||||||
|
const auto &entry = s.entries[i];
|
||||||
|
const bool is_dir = entry.is_directory(dir_ec);
|
||||||
|
const std::string name = entry.path().filename().string();
|
||||||
|
const std::string label = (is_dir ? std::string(icon::FOLDER) : std::string(icon::FILE_EARMARK)) + " " + name;
|
||||||
|
ImGui::PushID(static_cast<int>(i));
|
||||||
|
const bool selected = !is_dir && name == s.filename;
|
||||||
|
if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||||
|
const bool double_clicked = ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
|
||||||
|
if (is_dir) {
|
||||||
|
if (double_clicked) {
|
||||||
|
pending_dir = entry.path();
|
||||||
|
} else if (s.mode == Mode::Directory) {
|
||||||
|
s.filename = name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.filename = name;
|
||||||
|
if (double_clicked && s.mode == Mode::OpenFile) {
|
||||||
|
result = entry.path();
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
if (ok || !pending_dir.empty()) break;
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
if (!pending_dir.empty()) setDir(pending_dir);
|
||||||
|
|
||||||
|
if (s.mode != Mode::Directory) {
|
||||||
|
ImGui::SetNextItemWidth(-90.0f);
|
||||||
|
if (inputText("##name", &s.filename, "File name", ImGuiInputTextFlags_EnterReturnsTrue)) ok = true;
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextDisabled("%s", s.extension.empty() ? "*" : ("*" + s.extension).c_str());
|
||||||
|
}
|
||||||
|
const char *accept_label = s.mode == Mode::SaveFile ? "Save" : (s.mode == Mode::Directory ? "Choose" : "Open");
|
||||||
|
dialogButtons(accept_label, &ok, &cancel);
|
||||||
|
|
||||||
|
if (ok && result.empty()) {
|
||||||
|
if (s.mode == Mode::Directory) {
|
||||||
|
result = s.filename.empty() ? s.dir : s.dir / s.filename;
|
||||||
|
} else if (!s.filename.empty()) {
|
||||||
|
result = fs::path(s.filename).is_absolute() ? fs::path(s.filename) : s.dir / s.filename;
|
||||||
|
if (s.mode == Mode::SaveFile && !s.extension.empty() && result.extension().empty()) result += s.extension;
|
||||||
|
if (s.mode == Mode::OpenFile && !fs::is_regular_file(result, dir_ec)) ok = false;
|
||||||
|
} else {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok || cancel) ImGui::CloseCurrentPopup();
|
||||||
|
|
||||||
|
MessageBox::draw();
|
||||||
|
if (!s.active) ImGui::CloseCurrentPopup();
|
||||||
|
ImGui::EndPopup();
|
||||||
|
if (cancel) {
|
||||||
|
finish("");
|
||||||
|
} else if (ok) {
|
||||||
|
accept(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
iqpilot/tools/cabana/ui/dialogs/filedialog.h
Normal file
19
iqpilot/tools/cabana/ui/dialogs/filedialog.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
|
||||||
|
namespace FileDialog {
|
||||||
|
|
||||||
|
using Callback = std::function<void(const std::string &path)>;
|
||||||
|
|
||||||
|
void getOpenFileName(const std::string &title, const std::string &dir, const std::string &extension, Callback cb);
|
||||||
|
void getSaveFileName(const std::string &title, const std::string &default_path, const std::string &extension, Callback cb);
|
||||||
|
void getExistingDirectory(const std::string &title, const std::string &dir, Callback cb);
|
||||||
|
|
||||||
|
void draw();
|
||||||
|
|
||||||
|
}
|
||||||
87
iqpilot/tools/cabana/ui/dialogs/messagebox.cc
Normal file
87
iqpilot/tools/cabana/ui/dialogs/messagebox.cc
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
|
||||||
|
namespace MessageBox {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Box {
|
||||||
|
std::string title;
|
||||||
|
std::string text;
|
||||||
|
std::string detailed_text;
|
||||||
|
bool has_cancel = false;
|
||||||
|
std::function<void(bool)> on_result;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::deque<Box> g_queue;
|
||||||
|
bool g_show_details = false;
|
||||||
|
PopupOwner g_owner;
|
||||||
|
|
||||||
|
void push(Box box) { g_queue.push_back(std::move(box)); }
|
||||||
|
|
||||||
|
std::function<void(bool)> wrap(std::function<void()> on_close) {
|
||||||
|
if (!on_close) return nullptr;
|
||||||
|
return [on_close = std::move(on_close)](bool) { on_close(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void information(const std::string &title, const std::string &text, std::function<void()> on_close) {
|
||||||
|
push({.title = title, .text = text, .on_result = wrap(std::move(on_close))});
|
||||||
|
}
|
||||||
|
|
||||||
|
void warning(const std::string &title, const std::string &text, const std::string &detailed_text,
|
||||||
|
std::function<void()> on_close) {
|
||||||
|
push({.title = title, .text = text, .detailed_text = detailed_text, .on_result = wrap(std::move(on_close))});
|
||||||
|
}
|
||||||
|
|
||||||
|
void question(const std::string &title, const std::string &text, std::function<void(bool)> on_result) {
|
||||||
|
push({.title = title, .text = text, .has_cancel = true, .on_result = std::move(on_result)});
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() {
|
||||||
|
if (g_queue.empty()) return;
|
||||||
|
Box &box = g_queue.front();
|
||||||
|
const std::string popup_id = box.title + "###MessageBox";
|
||||||
|
const bool first = g_owner.popup_id == 0;
|
||||||
|
if (!g_owner.begin(popup_id.c_str())) return;
|
||||||
|
|
||||||
|
const ImGuiStyle &style = ImGui::GetStyle();
|
||||||
|
const float min_width = ImGui::CalcTextSize(box.title.c_str()).x + style.FramePadding.x * 2 + style.WindowPadding.x * 2;
|
||||||
|
ImGui::SetNextWindowSizeConstraints(ImVec2(min_width, 0.0f), ImVec2(FLT_MAX, FLT_MAX));
|
||||||
|
setNextDialogWindow(ImVec2(0.0f, 0.0f));
|
||||||
|
if (!ImGui::BeginPopupModal(popup_id.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) return;
|
||||||
|
if (first) g_show_details = false;
|
||||||
|
bool result = false, done = false;
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 480.0f);
|
||||||
|
ImGui::TextUnformatted(box.text.c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
if (g_show_details) {
|
||||||
|
ImGui::InputTextMultiline("##details", box.detailed_text.data(), box.detailed_text.size() + 1,
|
||||||
|
ImVec2(480.0f, 160.0f), ImGuiInputTextFlags_ReadOnly);
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
if (!box.detailed_text.empty()) {
|
||||||
|
|
||||||
|
if (ImGui::Button(g_show_details ? "Hide Details..." : "Show Details...")) g_show_details = !g_show_details;
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
|
dialogButtons("OK", &result, &done, true, box.has_cancel ? "Cancel" : nullptr);
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false)) result = true;
|
||||||
|
if (result) done = true;
|
||||||
|
if (done) ImGui::CloseCurrentPopup();
|
||||||
|
ImGui::EndPopup();
|
||||||
|
if (done) {
|
||||||
|
g_owner.reset();
|
||||||
|
Box finished = std::move(g_queue.front());
|
||||||
|
g_queue.pop_front();
|
||||||
|
if (finished.on_result) finished.on_result(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
iqpilot/tools/cabana/ui/dialogs/messagebox.h
Normal file
19
iqpilot/tools/cabana/ui/dialogs/messagebox.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
namespace MessageBox {
|
||||||
|
|
||||||
|
|
||||||
|
void information(const std::string &title, const std::string &text, std::function<void()> on_close = nullptr);
|
||||||
|
void warning(const std::string &title, const std::string &text, const std::string &detailed_text = "",
|
||||||
|
std::function<void()> on_close = nullptr);
|
||||||
|
|
||||||
|
void question(const std::string &title, const std::string &text, std::function<void(bool ok)> on_result);
|
||||||
|
|
||||||
|
void draw();
|
||||||
|
|
||||||
|
}
|
||||||
124
iqpilot/tools/cabana/ui/dialogs/routesdialog.cc
Normal file
124
iqpilot/tools/cabana/ui/dialogs/routesdialog.cc
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#include "tools/cabana/ui/dialogs/routesdialog.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||||
|
#include "tools/cabana/ui/util.h"
|
||||||
|
#include "tools/cabana/utils/util.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char *PERIOD_NAMES[] = {"Last week", "Last 2 weeks", "Last month", "Last 6 months", "Preserved"};
|
||||||
|
const int PERIOD_DAYS[] = {7, 14, 30, 180, -1};
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoutesDialog::open(std::function<void(bool, const std::string &)> on_done) {
|
||||||
|
on_done_ = std::move(on_done);
|
||||||
|
open_ = true;
|
||||||
|
popup_.reset();
|
||||||
|
s_ = State{};
|
||||||
|
alive_ = std::make_shared<bool>(true);
|
||||||
|
|
||||||
|
routes::fetchDevices([this, alive = std::weak_ptr<bool>(alive_)](std::vector<routes::DeviceInfo> devices, bool success, int error_code) {
|
||||||
|
utils::runOnMainThread(utils::guarded(alive.lock(), [this, devices = std::move(devices), success, error_code]() {
|
||||||
|
setDeviceList(devices, success, error_code);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoutesDialog::setDeviceList(const std::vector<routes::DeviceInfo> &devices, bool success, int error_code) {
|
||||||
|
if (success) {
|
||||||
|
s_.devices.clear();
|
||||||
|
for (const auto &device : devices) s_.devices.push_back(device.dongle_id);
|
||||||
|
s_.devices_loaded = true;
|
||||||
|
s_.device_index = 0;
|
||||||
|
fetchRoutes();
|
||||||
|
} else {
|
||||||
|
|
||||||
|
MessageBox::warning("Error", error_code == 401 ? "Unauthorized. Authenticate with iqpilot/tools/lib/auth.py" : "Network error", "",
|
||||||
|
utils::guarded(alive_, [this]() { finish(false); }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoutesDialog::fetchRoutes() {
|
||||||
|
if (!s_.devices_loaded || s_.devices.empty()) return;
|
||||||
|
|
||||||
|
s_.routes.clear();
|
||||||
|
s_.route_index = -1;
|
||||||
|
s_.empty_text = "Loading...";
|
||||||
|
|
||||||
|
const int request_id = ++s_.fetch_id;
|
||||||
|
auto on_routes = [this, alive = std::weak_ptr<bool>(alive_), request_id](std::vector<routes::RouteInfo> list, bool success, int) {
|
||||||
|
utils::runOnMainThread(utils::guarded(alive.lock(), [this, list = std::move(list), success, request_id]() {
|
||||||
|
if (s_.fetch_id == request_id) setRouteList(list, success);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
routes::fetchRoutes(s_.devices[s_.device_index], PERIOD_DAYS[s_.period_index], std::move(on_routes));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoutesDialog::setRouteList(const std::vector<routes::RouteInfo> &list, bool success) {
|
||||||
|
if (success) {
|
||||||
|
for (const auto &route : list) {
|
||||||
|
const int mins = static_cast<int>((route.end_ms - route.start_ms) / 60000);
|
||||||
|
s_.routes.push_back({routes::formatUnixMs(route.start_ms) + " " + std::to_string(mins) + "min", route.name});
|
||||||
|
}
|
||||||
|
if (!s_.routes.empty()) s_.route_index = 0;
|
||||||
|
} else {
|
||||||
|
MessageBox::warning("Error", "Failed to fetch routes. Check your network connection.", "",
|
||||||
|
utils::guarded(alive_, [this]() { finish(false); }));
|
||||||
|
}
|
||||||
|
s_.empty_text = "No items";
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoutesDialog::finish(bool accepted) {
|
||||||
|
alive_.reset();
|
||||||
|
open_ = false;
|
||||||
|
auto on_done = std::move(on_done_);
|
||||||
|
if (on_done) on_done(accepted, accepted && s_.route_index >= 0 ? s_.routes[s_.route_index].name : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RoutesDialog::draw() {
|
||||||
|
if (!open_) return;
|
||||||
|
if (!beginDialog("Remote routes", &popup_, ImVec2(480.0f, 420.0f))) return;
|
||||||
|
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted("Device");
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
|
if (s_.devices_loaded) {
|
||||||
|
if (comboBox("##device", &s_.device_index, s_.devices)) fetchRoutes();
|
||||||
|
} else {
|
||||||
|
int idx = 0;
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
comboBox("##device", &idx, {"Loading..."});
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
|
if (ImGui::Combo("##period", &s_.period_index, PERIOD_NAMES, IM_ARRAYSIZE(PERIOD_NAMES))) fetchRoutes();
|
||||||
|
|
||||||
|
bool accepted = false, rejected = false;
|
||||||
|
const float footer = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
||||||
|
ImGui::BeginChild("routes", ImVec2(0, -footer), ImGuiChildFlags_Borders);
|
||||||
|
if (s_.routes.empty()) {
|
||||||
|
const ImVec2 size = ImGui::CalcTextSize(s_.empty_text.c_str());
|
||||||
|
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||||
|
ImGui::SetCursorPos(ImVec2((avail.x - size.x) * 0.5f, (avail.y - size.y) * 0.5f));
|
||||||
|
ImGui::TextUnformatted(s_.empty_text.c_str());
|
||||||
|
}
|
||||||
|
for (int i = 0; i < static_cast<int>(s_.routes.size()); ++i) {
|
||||||
|
ImGui::PushID(i);
|
||||||
|
if (ImGui::Selectable(s_.routes[i].label.c_str(), s_.route_index == i, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||||
|
s_.route_index = i;
|
||||||
|
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) accepted = true;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
dialogButtons("OK", &accepted, &rejected);
|
||||||
|
MessageBox::draw();
|
||||||
|
if (accepted || rejected || !open_) ImGui::CloseCurrentPopup();
|
||||||
|
ImGui::EndPopup();
|
||||||
|
if (accepted || rejected) finish(accepted);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user