diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 598f2c592b..f2cc51285d 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -7,7 +7,7 @@ on: pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.ref != 'refs/heads/master' && github.ref || github.run_id }}-${{ github.event_name }} + group: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: diff --git a/.github/workflows/tools_tests.yaml b/.github/workflows/tools_tests.yaml index 549a2f4195..94cc3c2580 100644 --- a/.github/workflows/tools_tests.yaml +++ b/.github/workflows/tools_tests.yaml @@ -7,7 +7,7 @@ on: pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.ref != 'refs/heads/master' && github.ref || github.run_id }}-${{ github.event_name }} + group: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: diff --git a/Jenkinsfile b/Jenkinsfile index ea62ff3c49..c34d253585 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -59,7 +59,7 @@ pipeline { branch 'devel-staging' } steps { - phone_steps("tici", [ + phone_steps("tici-needs-can", [ ["build release3-staging & dashcam3-staging", "PUSH=1 $SOURCE_DIR/release/build_release.sh"], ]) } @@ -111,7 +111,7 @@ pipeline { R3_PUSH = "${env.BRANCH_NAME == 'master' ? '1' : ' '}" } steps { - phone_steps("tici", [ + phone_steps("tici-needs-can", [ ["build master-ci", "cd $SOURCE_DIR/release && TARGET_DIR=$TEST_DIR EXTRA_FILES='tools/' ./build_devel.sh"], ["build openpilot", "cd selfdrive/manager && ./build.py"], ["check dirty", "release/check-dirty.sh"], @@ -122,25 +122,44 @@ pipeline { } } + stage('loopback-tests') { + agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } + steps { + phone_steps("tici-loopback", [ + ["build openpilot", "cd selfdrive/manager && ./build.py"], + ["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"], + ]) + } + } + stage('HW + Unit Tests') { agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } steps { - phone_steps("tici2", [ + phone_steps("tici-common", [ ["build", "cd selfdrive/manager && ./build.py"], ["test power draw", "python system/hardware/tici/test_power_draw.py"], - ["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"], ["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"], ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python selfdrive/loggerd/tests/test_encoder.py"], - ["test sensord", "python selfdrive/sensord/tests/test_sensord.py"], ["test pigeond", "python selfdrive/sensord/tests/test_pigeond.py"], ]) } } - stage('camerad') { + stage('camerad-ar') { + agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } + steps { + phone_steps("tici-ar0321", [ + ["build", "cd selfdrive/manager && ./build.py"], + ["test camerad", "python system/camerad/test/test_camerad.py"], + ["test exposure", "python system/camerad/test/test_exposure.py"], + ]) + } + } + + stage('camerad-ox') { agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } steps { - phone_steps("tici-party", [ + phone_steps("tici-ox03c10", [ ["build", "cd selfdrive/manager && ./build.py"], ["test camerad", "python system/camerad/test/test_camerad.py"], ["test exposure", "python system/camerad/test/test_exposure.py"], @@ -148,27 +167,32 @@ pipeline { } } - stage('sensord (LSM-C)') { + stage('sensord') { agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } steps { phone_steps("tici-lsmc", [ ["build", "cd selfdrive/manager && ./build.py"], ["test sensord", "cd selfdrive/sensord/tests && python -m unittest test_sensord.py"], ]) + phone_steps("tici-bmx-lsm", [ + ["build", "cd selfdrive/manager && ./build.py"], + ["test sensord", "cd selfdrive/sensord/tests && python -m unittest test_sensord.py"], + ]) } } stage('replay') { agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } steps { - phone_steps("tici3", [ + phone_steps("tici-common", [ ["build", "cd selfdrive/manager && ./build.py"], ["model replay", "cd selfdrive/test/process_replay && ./model_replay.py"], ]) } } - } + } } + } } diff --git a/RELEASES.md b/RELEASES.md index 744168c8fb..97c92dfa47 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,21 +1,40 @@ -Version 0.8.17 (2022-XX-XX) +Version 0.9.1 (2022-12-XX) ======================== * New driving model - * Internal feature space accuracy increased tenfold during training, this makes the model dramatically more accurate. -* New driver monitoring model - * New end-to-end distracted trigger -* Self-tuning torque lateral controller parameters - * Parameters learned live for each car + + +Version 0.9.0 (2022-11-21) +======================== +* New driving model + * Internal feature space information content increased tenfold during training to ~700 bits, which makes the model dramatically more accurate + * Less reliance on previous frames makes model more reactive and snappy + * Trained in new reprojective simulator + * Trained in 36 hours from scratch, compared to one week for previous releases + * Training now simulates both lateral and longitudinal behavior, which allows openpilot to slow down for turns, stop at traffic lights, and more in experimental mode +* Experimental driving mode + * End-to-end longitudinal control + * Stops for traffic lights and stop signs + * Slows down for turns + * openpilot defaults to chill mode, enable experimental mode in settings +* Driver monitoring updates + * New bigger model with added end-to-end distracted trigger + * Reduced false positives during driver calibration +* Self-tuning torque controller: learns parameters live for each car * Torque controller used on all Toyota, Lexus, Hyundai, Kia, and Genesis models * UI updates - * Multi-language in navigation * Matched speeds shown on car's dash + * Multi-language in navigation * Improved update experience * Border turns grey while overriding steering - * Added button to bookmark events while driving; view them later in comma connect -* AGNOS 6 + * Bookmark events while driving; view them in comma connect + * New onroad visualization for experimental mode * tools: new and improved cabana thanks to deanlee! +* Experimental longitudinal support for Volkswagen, CAN-FD Hyundai, and new GM models +* Genesis GV70 2022-23 support thanks to zunichky and sunnyhaibin! +* Hyundai Santa Cruz 2021-22 support thanks to sunnyhaibin! +* Kia Sportage 2023 support thanks to sunnyhaibin! * Kia Sportage Hybrid 2023 support thanks to sunnyhaibin! +* Kia Stinger 2022 support thanks to sunnyhaibin! Version 0.8.16 (2022-08-26) ======================== diff --git a/SConstruct b/SConstruct index 74680c0598..033e10a1f0 100644 --- a/SConstruct +++ b/SConstruct @@ -298,12 +298,15 @@ if arch == "Darwin": qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"] qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin")) else: - qt_env['QTDIR'] = "/usr" + 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"/usr/include/{real_arch}-linux-gnu/qt5", - f"/usr/include/{real_arch}-linux-gnu/qt5/QtGui/5.12.8/QtGui", + f"{qt_install_headers}", + f"{qt_install_headers}/QtGui/5.12.8/QtGui", ] - qt_dirs += [f"/usr/include/{real_arch}-linux-gnu/qt5/Qt{m}" for m in qt_modules] + qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules] qt_libs = [f"Qt5{m}" for m in qt_modules] if arch == "larch64": diff --git a/body b/body index 04aeb30ce0..dc780f858c 160000 --- a/body +++ b/body @@ -1 +1 @@ -Subproject commit 04aeb30ce0bb14759989cd374158233877e1e151 +Subproject commit dc780f858c1ef641471d09b72569e199e3e10acb diff --git a/cereal b/cereal index 1d25fc3f20..3bae09cf65 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 1d25fc3f202d5ddeee97848480323e9b14f9bdfa +Subproject commit 3bae09cf6527674d7eda3a9956242aad94a8f3d2 diff --git a/common/params.cc b/common/params.cc index 155bc88487..9e3e32d584 100644 --- a/common/params.cc +++ b/common/params.cc @@ -102,7 +102,8 @@ std::unordered_map keys = { {"DashcamOverride", PERSISTENT}, {"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"DisablePowerDown", PERSISTENT}, - {"EndToEndLong", PERSISTENT}, + {"ExperimentalMode", PERSISTENT}, + {"ExperimentalModeConfirmed", PERSISTENT}, {"ExperimentalLongitudinalEnabled", PERSISTENT}, // WARNING: THIS MAY DISABLE AEB {"DisableUpdates", PERSISTENT}, {"DisengageOnAccelerator", PERSISTENT}, diff --git a/common/util.cc b/common/util.cc index b6a8322a27..010fe8a11a 100644 --- a/common/util.cc +++ b/common/util.cc @@ -1,5 +1,6 @@ #include "common/util.h" +#include #include #include @@ -149,6 +150,14 @@ int safe_fflush(FILE *stream) { return ret; } +int safe_ioctl(int fd, unsigned long request, void *argp) { + int ret; + do { + ret = ioctl(fd, request, argp); + } while ((ret == -1) && (errno == EINTR)); + return ret; +} + std::string readlink(const std::string &path) { char buff[4096]; ssize_t len = ::readlink(path.c_str(), buff, sizeof(buff)-1); diff --git a/common/util.h b/common/util.h index e13f4dc130..b46f7bde4a 100644 --- a/common/util.h +++ b/common/util.h @@ -86,6 +86,7 @@ int write_file(const char* path, const void* data, size_t size, int flags = O_WR FILE* safe_fopen(const char* filename, const char* mode); size_t safe_fwrite(const void * ptr, size_t size, size_t count, FILE * stream); int safe_fflush(FILE *stream); +int safe_ioctl(int fd, unsigned long request, void *argp); std::string readlink(const std::string& path); bool file_exists(const std::string& fn); diff --git a/common/version.h b/common/version.h index 0a109c1faa..7b5764785a 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.8.17" +#define COMMA_VERSION "0.9.1" diff --git a/docs/CARS.md b/docs/CARS.md index ad288a1cc6..a0d22a3d27 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,19 +4,19 @@ A supported vehicle is one that just works when you install a comma three. All supported cars provide a better experience than any stock system. -# 211 Supported Cars +# 215 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Harness| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:| |Acura|ILX 2016-19|AcuraWatch Plus|openpilot|25 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec| |Acura|RDX 2016-18|AcuraWatch Plus|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Nidec| |Acura|RDX 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A| -|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Audi|Q3 2020-21|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| +|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Audi|Q3 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| |Cadillac|Escalade ESV 2016[3](#footnotes)|Adaptive Cruise Control (ACC) & LKAS|openpilot|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|OBD-II| |Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|GM| |Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|GM| @@ -31,6 +31,7 @@ A supported vehicle is one that just works when you install a comma three. All s |Genesis|G70 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai F| |Genesis|G80 2017-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| |Genesis|G90 2017-18|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C| +|Genesis|GV70 2022-23|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L| |GMC|Acadia 2018[3](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|OBD-II| |GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|GM| |Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Honda Bosch A| @@ -59,8 +60,8 @@ A supported vehicle is one that just works when you install a comma three. All s |Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K| |Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai J| |Hyundai|i30 2019|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai E| -|Hyundai|Ioniq 5 (with HDA II) 2022|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai Q| -|Hyundai|Ioniq 5 (without HDA II) 2022|Highway Driving Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K| +|Hyundai|Ioniq 5 (with HDA II) 2022-23|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai Q| +|Hyundai|Ioniq 5 (without HDA II) 2022-23|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K| |Hyundai|Ioniq Electric 2019|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C| |Hyundai|Ioniq Electric 2020|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| |Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C| @@ -72,6 +73,7 @@ A supported vehicle is one that just works when you install a comma three. All s |Hyundai|Kona Electric 2022|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai O| |Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai I| |Hyundai|Palisade 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| +|Hyundai|Santa Cruz 2021-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N| |Hyundai|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai D| |Hyundai|Santa Fe 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L| |Hyundai|Santa Fe Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L| @@ -81,13 +83,13 @@ A supported vehicle is one that just works when you install a comma three. All s |Hyundai|Sonata Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai A| |Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L| |Hyundai|Tucson Diesel 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L| -|Hyundai|Tucson Hybrid 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N| +|Hyundai|Tucson Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N| |Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai E| |Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|FCA| |Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|FCA| |Kia|Ceed 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai E| -|Kia|EV6 (with HDA II) 2022|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai P| -|Kia|EV6 (without HDA II) 2022|Highway Driving Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L| +|Kia|EV6 (with HDA II) 2022|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai P| +|Kia|EV6 (without HDA II) 2022|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai L| |Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai G| |Kia|K5 2021-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai A| |Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| @@ -102,9 +104,11 @@ A supported vehicle is one that just works when you install a comma three. All s |Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai A| |Kia|Sorento 2018|Advanced Smart Cruise Control|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C| |Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai E| -|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N| +|Kia|Sportage 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N| +|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N| |Kia|Stinger 2018-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C| -|Kia|Telluride 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| +|Kia|Stinger 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai K| +|Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| |Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| |Lexus|ES 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Lexus|ES Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| @@ -129,8 +133,8 @@ A supported vehicle is one that just works when you install a comma three. All s |Nissan|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Nissan A| |Nissan|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Nissan A| |Ram|1500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Ram| -|SEAT|Ateca 2018|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| +|SEAT|Ateca 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| |Subaru|Ascent 2019-21|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Subaru A| |Subaru|Crosstrek 2018-19|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Subaru A| |Subaru|Crosstrek 2020-21|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Subaru A| @@ -141,13 +145,13 @@ A supported vehicle is one that just works when you install a comma three. All s |Subaru|Outback 2020-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Subaru B| |Subaru|XV 2018-19|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Subaru A| |Subaru|XV 2020-21|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Subaru A| -|Škoda|Kamiq 2021[6](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Škoda|Karoq 2019-21[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Škoda|Kodiaq 2018-19|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Škoda|Octavia 2015, 2018-19|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Škoda|Octavia RS 2016|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Škoda|Scala 2020|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Škoda|Superb 2015-18|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| +|Škoda|Kamiq 2021[6](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[9](#footnotes)| +|Škoda|Karoq 2019-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Škoda|Kodiaq 2018-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Škoda|Octavia 2015, 2018-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Škoda|Octavia RS 2016|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Škoda|Scala 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[9](#footnotes)| +|Škoda|Superb 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| |Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| @@ -164,7 +168,7 @@ A supported vehicle is one that just works when you install a comma three. All s |Toyota|Camry Hybrid 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|Corolla 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| |Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| -|Toyota|Corolla Cross (Non-US only) 2020-21|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| +|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| @@ -188,48 +192,48 @@ A supported vehicle is one that just works when you install a comma three. All s |Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|RAV4 Hybrid 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| |Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| -|Volkswagen|Arteon 2018-22[8,9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Arteon eHybrid 2020-22[8,9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Arteon R 2020-22[8,9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Atlas 2018-23[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Atlas Cross Sport 2021-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|California 2021[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Caravelle 2020[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|CC 2018-22[8,9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Volkswagen|Golf 2015-20[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Volkswagen|Golf R 2015-19[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| -|Volkswagen|Jetta 2018-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Jetta GLI 2021-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Passat 2015-22[7,8,9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Passat Alltrack 2015-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Passat GTE 2015-22[8,9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Polo 2020-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Polo GTI 2020-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|T-Cross 2021[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|T-Roc 2021[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Taos 2022[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Teramont 2018-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Teramont Cross Sport 2021-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Teramont X 2021-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Tiguan 2019-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -|Volkswagen|Touran 2017|Adaptive Cruise Control (ACC) & Lane Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|VW| +|Volkswagen|Arteon 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Arteon eHybrid 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Arteon R 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Atlas Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|California 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Jetta 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Jetta GLI 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Passat 2015-22[7](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Polo 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[9](#footnotes)| +|Volkswagen|Polo GTI 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[9](#footnotes)| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[9](#footnotes)| +|Volkswagen|T-Roc 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[9](#footnotes)| +|Volkswagen|Taos 2022|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Tiguan 2019-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| +|Volkswagen|Touran 2017|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| -1Experimental openpilot longitudinal control is available behind a toggle; the toggle is only available in non-release branches such as `master-ci`. Using openpilot longitudinal may disable Automatic Emergency Braking (AEB).
-2When the Driver Support Unit (DSU) is disconnected, openpilot Adaptive Cruise Control (ACC) will replace stock Adaptive Cruise Control (ACC). NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).
+1Experimental openpilot longitudinal control is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `master-ci`.
+2By default, this car will use the stock Adaptive Cruise Control (ACC) for longitudinal control. If the Driver Support Unit (DSU) is disconnected, openpilot ACC will replace stock ACC. NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).
3Requires a community built ASCM harness. NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).
42019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
5openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
6Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
7Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
-8Model-years 2021 and beyond may have a new camera harness design, which isn't yet available from the comma store. Before ordering, remove the Lane Assist camera cover and check to see if the connector is black (older design) or light brown (newer design). In the interim, if your car has a J533 connector CAN gateway inside the dashboard, choose "VW J533 Development" from the vehicle drop-down for a suitable harness. (Some newer models are also observed to not have a J533 connector.)
-9Includes versions with extra rear cargo space (may be called Variant, Estate, SportWagen, Shooting Brake, etc.)
+8Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
+9Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store.
## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). diff --git a/launch_env.sh b/launch_env.sh index 88e1f2a9c5..3059ec268e 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="6.1" + export AGNOS_VERSION="6.2" fi if [ -z "$PASSIVE" ]; then diff --git a/panda b/panda index 9147cba1af..2e90b6f308 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 9147cba1af0a8d72379242eb1ce0bfd42ab8075e +Subproject commit 2e90b6f308dc09bf1734f6cb5cc990cb8149486d diff --git a/poetry.lock b/poetry.lock index 2dff4fecf1..d79a3f035f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -230,7 +230,7 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "av" @@ -531,7 +531,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "cleo" @@ -1410,7 +1410,7 @@ notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test_extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] [[package]] name = "ipython-genutils" @@ -1459,9 +1459,9 @@ python-versions = ">=3.6.1,<4.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "itsdangerous" @@ -1892,7 +1892,7 @@ mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] -code_style = ["pre-commit (==2.6)"] +code-style = ["pre-commit (==2.6)"] compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] linkify = ["linkify-it-py (>=1.0,<2.0)"] plugins = ["mdit-py-plugins"] @@ -1959,7 +1959,7 @@ python-versions = ">=3.7" markdown-it-py = ">=1.0.0,<3.0.0" [package.extras] -code_style = ["pre-commit"] +code-style = ["pre-commit"] rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] @@ -2169,7 +2169,7 @@ sphinx = ">=4,<6" typing-extensions = "*" [package.extras] -code_style = ["pre-commit (>=2.12,<3.0)"] +code-style = ["pre-commit (>=2.12,<3.0)"] linkify = ["linkify-it-py (>=1.0,<2.0)"] rtd = ["ipython", "sphinx-book-theme", "sphinx-design", "sphinxcontrib.mermaid (>=0.7.1,<0.8.0)", "sphinxext-opengraph (>=0.6.3,<0.7.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx (<5.2)", "sphinx-pytest"] @@ -3193,7 +3193,7 @@ optional = false python-versions = ">=3.6" [package.extras] -asyncio_client = ["aiohttp (>=3.4)"] +asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] [[package]] @@ -3217,7 +3217,7 @@ bidict = ">=0.21.0" python-engineio = ">=4.3.0" [package.extras] -asyncio_client = ["aiohttp (>=3.4)"] +asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] [[package]] @@ -3393,7 +3393,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-oauthlib" @@ -3584,7 +3584,7 @@ falcon = ["falcon (>=1.4)"] fastapi = ["fastapi (>=0.79.0)"] flask = ["blinker (>=1.1)", "flask (>=0.11)"] httpx = ["httpx (>=0.16.0)"] -pure_eval = ["asttokens", "executing", "pure-eval"] +pure-eval = ["asttokens", "executing", "pure-eval"] pyspark = ["pyspark (>=2.4.4)"] quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] rq = ["rq (>=0.6)"] @@ -3834,6 +3834,14 @@ python-versions = ">=3.5" lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "spidev" +version = "3.6" +description = "Python bindings for Linux SPI access through spidev" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "sqlalchemy" version = "1.4.42" @@ -3850,19 +3858,19 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] -mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] -mssql_pymssql = ["pymssql"] -mssql_pyodbc = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] -mysql_connector = ["mysql-connector-python"] +mysql-connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] -postgresql_psycopg2binary = ["psycopg2-binary"] -postgresql_psycopg2cffi = ["psycopg2cffi"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql", "pymysql (<1)"] sqlcipher = ["sqlcipher3_binary"] @@ -4378,7 +4386,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = "~3.8" -content-hash = "76f06d46b1c308e59968994e42193599d8ba3cab6d552460c9c48f282313b64d" +content-hash = "d2854112975a9d83a9540175b2d430487e40e0292d48a1ba6c591db60a08c136" [metadata.files] adal = [ @@ -5384,6 +5392,7 @@ gevent = [ {file = "gevent-22.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:d2ea4ce36c09355379bc038be2bd50118f97d2eb6381b7096de4d05aa4c3e241"}, {file = "gevent-22.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e73c9f71aa2a6795ecbec9b57282b002375e863e283558feb87b62840c8c1ac"}, {file = "gevent-22.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc3758f0dc95007c1780d28a9fd2150416a79c50f308f62a674d78a845ea1b9"}, + {file = "gevent-22.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03c10ca0beeab0c6be516030471ea630447ddd1f649d3335e5b162097cd4130a"}, {file = "gevent-22.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fe2c0ff095171c49f78f1d4e6dc89fa58253783c7b6dccab9f1d76e2ee391f10"}, {file = "gevent-22.10.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d18fcc324f39a3b21795022eb47c7752d6e4f4ed89d8cca41f1cc604553265b3"}, {file = "gevent-22.10.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ea39c70ce166c4a1d4386c7fae96cb8d84ad799527b3378406051104d15443"}, @@ -6554,6 +6563,11 @@ pillow-avif-plugin = [ {file = "pillow_avif_plugin-1.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017e5e52cb4320414e8ce3e2089eae2cb87c22c73ff6012b17ae326fc5753b20"}, {file = "pillow_avif_plugin-1.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2a57136d4866de5dc80cfb24d66655955fbdd87acf1d11d88c8dc2ab41023e46"}, {file = "pillow_avif_plugin-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:f339511d0fccb69e3a5e3af39f8fe6700b0a07279015006ea56f8f49e7fecff4"}, + {file = "pillow_avif_plugin-1.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05e821ecd90bb0b8d2dc7610625372cc47de9cb893d09662528bad572f669d1c"}, + {file = "pillow_avif_plugin-1.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33886a5f9796fe9a8a3bc25ccfdeba7db119adb50b7004f1928a14b07d0213a"}, + {file = "pillow_avif_plugin-1.2.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75b7ed186c2f740dd26e556f6a966c59a170b70263e429a2c81920fe444da8a7"}, + {file = "pillow_avif_plugin-1.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11aef6b79078b8dad25c928e5871c146ab94424472851d5bf539ba62abde9ac"}, + {file = "pillow_avif_plugin-1.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:10696c536d68a14cefea3b98edb8d5a7ae29e8e07458f1d59c5d1cd780a8bf2a"}, {file = "pillow_avif_plugin-1.2.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1a7291d6a5fb7336e72685a31d193e0b3a6bee9986c9ac4d8bd4b68dbe6d4f7f"}, {file = "pillow_avif_plugin-1.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:14b9c5dbf237e7dc12f69819ea181a457b3bd4f59f8cd71d028d3635fd3bcab4"}, {file = "pillow_avif_plugin-1.2.2-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:799cbfbeee831332d280c80df9ce16b5c3b1224c318264e97e89df8da32e870e"}, @@ -6942,11 +6956,13 @@ pyprof2calltree = [ ] pyproj = [ {file = "pyproj-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f343725566267a296b09ee7e591894f1fdc90f84f8ad5ec476aeb53bd4479c07"}, + {file = "pyproj-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5816807ca0bdc7256558770c6206a6783a3f02bcf844f94ee245f197bb5f7285"}, {file = "pyproj-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e609903572a56cca758bbaee5c1663c3e829ddce5eec4f368e68277e37022b"}, {file = "pyproj-3.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4fd425ee8b6781c249c7adb7daa2e6c41ce573afabe4f380f5eecd913b56a3be"}, {file = "pyproj-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:954b068136518b3174d0a99448056e97af62b63392a95c420894f7de2229dae6"}, {file = "pyproj-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:4a23d84c5ffc383c7d9f0bde3a06fc1f6697b1b96725597f8f01e7b4bef0a2b5"}, {file = "pyproj-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1f9c100fd0fd80edbc7e4daa303600a8cbef6f0de43d005617acb38276b88dc0"}, + {file = "pyproj-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa5171f700f174777a9e9ed8f4655583243967c0f9cf2c90e3f54e54ff740134"}, {file = "pyproj-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a496d9057b2128db9d733e66b206f2d5954bbae6b800d412f562d780561478c"}, {file = "pyproj-3.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e54796e2d9554a5eb8f11df4748af1fbbc47f76aa234d6faf09216a84554c5"}, {file = "pyproj-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a454a7c4423faa2a14e939d08ef293ee347fa529c9df79022b0585a6e1d8310c"}, @@ -6957,6 +6973,7 @@ pyproj = [ {file = "pyproj-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f80adda8c54b84271a93829477a01aa57bc178c834362e9f74e1de1b5033c74c"}, {file = "pyproj-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:221d8939685e0c43ee594c9f04b6a73a10e8e1cc0e85f28be0b4eb2f1bc8777d"}, {file = "pyproj-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d94afed99f31673d3d19fe750283621e193e2a53ca9e0443bf9d092c3905833b"}, + {file = "pyproj-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0fff9c3a991508f16027be27d153f6c5583d03799443639d13c681e60f49e2d7"}, {file = "pyproj-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b85acf09e5a9e35cd9ee72989793adb7089b4e611be02a43d3d0bda50ad116b"}, {file = "pyproj-3.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45554f47d1a12a84b0620e4abc08a2a1b5d9f273a4759eaef75e74788ec7162a"}, {file = "pyproj-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12f62c20656ac9b6076ebb213e9a635d52f4f01fef95310121d337e62e910cb6"}, @@ -7537,6 +7554,10 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] +spidev = [ + {file = "spidev-3.6-cp39-cp39-linux_armv7l.whl", hash = "sha256:280abc00a1ef7780ef62c3f294f52a2527b6c47d8c269fea98664970bcaf6da5"}, + {file = "spidev-3.6.tar.gz", hash = "sha256:14dbc37594a4aaef85403ab617985d3c3ef464d62bc9b769ef552db53701115b"}, +] sqlalchemy = [ {file = "SQLAlchemy-1.4.42-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:28e881266a172a4d3c5929182fde6bb6fba22ac93f137d5380cc78a11a9dd124"}, {file = "SQLAlchemy-1.4.42-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca9389a00f639383c93ed00333ed763812f80b5ae9e772ea32f627043f8c9c88"}, diff --git a/pyproject.toml b/pyproject.toml index ebb7954536..7e76b9cdfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ tqdm = "^4.64.0" urllib3 = "^1.26.10" utm = "^0.7.0" websocket_client = "^1.3.3" +spidev = "^3.6" [tool.poetry.group.dev.dependencies] diff --git a/release/files_common b/release/files_common index e3c417040c..a294e1e5b5 100644 --- a/release/files_common +++ b/release/files_common @@ -90,8 +90,12 @@ selfdrive/boardd/boardd_api_impl.pyx selfdrive/boardd/can_list_to_can_capnp.cc selfdrive/boardd/panda.cc selfdrive/boardd/panda.h +selfdrive/boardd/spi.cc +selfdrive/boardd/panda_comms.h +selfdrive/boardd/panda_comms.cc selfdrive/boardd/set_time.py selfdrive/boardd/pandad.py +selfdrive/boardd/tests/test_boardd_loopback.py selfdrive/car/__init__.py selfdrive/car/docs_definitions.py diff --git a/selfdrive/assets/fonts/JetBrainsMono-Medium.ttf b/selfdrive/assets/fonts/JetBrainsMono-Medium.ttf new file mode 100644 index 0000000000..a6ba5529af Binary files /dev/null and b/selfdrive/assets/fonts/JetBrainsMono-Medium.ttf differ diff --git a/selfdrive/assets/img_couch.svg b/selfdrive/assets/img_couch.svg new file mode 100644 index 0000000000..2e86012809 --- /dev/null +++ b/selfdrive/assets/img_couch.svg @@ -0,0 +1,4 @@ + + + + diff --git a/selfdrive/assets/img_experimental.svg b/selfdrive/assets/img_experimental.svg new file mode 100644 index 0000000000..0eaec3b3cd --- /dev/null +++ b/selfdrive/assets/img_experimental.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/selfdrive/assets/img_experimental_grey.svg b/selfdrive/assets/img_experimental_grey.svg new file mode 100644 index 0000000000..dc87105ac5 --- /dev/null +++ b/selfdrive/assets/img_experimental_grey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/selfdrive/assets/img_experimental_white.svg b/selfdrive/assets/img_experimental_white.svg new file mode 100644 index 0000000000..ae4f18fde2 --- /dev/null +++ b/selfdrive/assets/img_experimental_white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/selfdrive/boardd/SConscript b/selfdrive/boardd/SConscript index dcbea03d3c..d99e67a9f0 100644 --- a/selfdrive/boardd/SConscript +++ b/selfdrive/boardd/SConscript @@ -1,9 +1,9 @@ Import('env', 'envCython', 'common', 'cereal', 'messaging') libs = ['usb-1.0', common, cereal, messaging, 'pthread', 'zmq', 'capnp', 'kj'] -env.Program('boardd', ['main.cc', 'boardd.cc', 'panda.cc'], LIBS=libs) +env.Program('boardd', ['main.cc', 'boardd.cc', 'panda.cc', 'panda_comms.cc', 'spi.cc'], LIBS=libs) env.Library('libcan_list_to_can_capnp', ['can_list_to_can_capnp.cc']) envCython.Program('boardd_api_impl.so', 'boardd_api_impl.pyx', LIBS=["can_list_to_can_capnp", 'capnp', 'kj'] + envCython["LIBS"]) if GetOption('test'): - env.Program('tests/test_boardd_usbprotocol', ['tests/test_boardd_usbprotocol.cc', 'panda.cc'], LIBS=libs) + env.Program('tests/test_boardd_usbprotocol', ['tests/test_boardd_usbprotocol.cc', 'panda.cc', 'panda_comms.cc', 'spi.cc'], LIBS=libs) diff --git a/selfdrive/boardd/boardd.cc b/selfdrive/boardd/boardd.cc index 2d613b68ce..5496902252 100644 --- a/selfdrive/boardd/boardd.cc +++ b/selfdrive/boardd/boardd.cc @@ -19,8 +19,6 @@ #include #include -#include - #include "cereal/gen/cpp/car.capnp.h" #include "cereal/messaging/messaging.h" #include "common/params.h" @@ -67,7 +65,7 @@ static std::string get_time_str(const struct tm &time) { bool check_all_connected(const std::vector &pandas) { for (const auto& panda : pandas) { - if (!panda->connected) { + if (!panda->connected()) { do_exit = true; return false; } @@ -184,7 +182,7 @@ bool safety_setter_thread(std::vector pandas) { return true; } -Panda *usb_connect(std::string serial="", uint32_t index=0) { +Panda *connect(std::string serial="", uint32_t index=0) { std::unique_ptr panda; try { panda = std::make_unique(serial, (index * PANDA_BUS_CNT)); @@ -227,9 +225,9 @@ void can_send_thread(std::vector pandas, bool fake_send) { //Dont send if older than 1 second if ((nanos_since_boot() - event.getLogMonoTime() < 1e9) && !fake_send) { for (const auto& panda : pandas) { - LOGT("sending sendcan to panda: %s", (panda->usb_serial).c_str()); + LOGT("sending sendcan to panda: %s", (panda->hw_serial).c_str()); panda->can_send(event.getSendcan()); - LOGT("sendcan sent to panda: %s", (panda->usb_serial).c_str()); + LOGT("sendcan sent to panda: %s", (panda->hw_serial).c_str()); } } } @@ -357,7 +355,7 @@ std::optional send_panda_states(PubMaster *pm, const std::vector } #endif - if (!panda->comms_healthy) { + if (!panda->comms_healthy()) { evt.setValid(false); } @@ -433,7 +431,7 @@ void send_peripheral_state(PubMaster *pm, Panda *panda) { // build msg MessageBuilder msg; auto evt = msg.initEvent(); - evt.setValid(panda->comms_healthy); + evt.setValid(panda->comms_healthy()); auto ps = evt.initPeripheralState(); ps.setPandaType(panda->hw_type); @@ -526,7 +524,7 @@ void peripheral_control_thread(Panda *panda, bool no_fan_control) { FirstOrderFilter integ_lines_filter(0, 30.0, 0.05); - while (!do_exit && panda->connected) { + while (!do_exit && panda->connected()) { cnt++; sm.update(1000); // TODO: what happens if EINTR is sent while in sm.update? @@ -595,7 +593,7 @@ void boardd_main_thread(std::vector serials) { // connect to all provided serials std::vector pandas; for (int i = 0; i < serials.size() && !do_exit; /**/) { - Panda *p = usb_connect(serials[i], i); + Panda *p = connect(serials[i], i); if (!p) { // send empty pandaState & peripheralState and try again send_empty_panda_state(&pm); diff --git a/selfdrive/boardd/panda.cc b/selfdrive/boardd/panda.cc index 0b8630b0c0..deccee3e76 100644 --- a/selfdrive/boardd/panda.cc +++ b/selfdrive/boardd/panda.cc @@ -4,76 +4,20 @@ #include #include -#include #include "cereal/messaging/messaging.h" #include "panda/board/dlc_to_len.h" -#include "common/gpio.h" #include "common/swaglog.h" #include "common/util.h" -static int init_usb_ctx(libusb_context **context) { - assert(context != nullptr); - - int err = libusb_init(context); - if (err != 0) { - LOGE("libusb initialization error"); - return err; - } - -#if LIBUSB_API_VERSION >= 0x01000106 - libusb_set_option(*context, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO); -#else - libusb_set_debug(*context, 3); -#endif - - return err; -} - - Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { - // init libusb - ssize_t num_devices; - libusb_device **dev_list = NULL; - int err = init_usb_ctx(&ctx); - if (err != 0) { goto fail; } - - // connect by serial - num_devices = libusb_get_device_list(ctx, &dev_list); - if (num_devices < 0) { goto fail; } - for (size_t i = 0; i < num_devices; ++i) { - libusb_device_descriptor desc; - libusb_get_device_descriptor(dev_list[i], &desc); - if (desc.idVendor == 0xbbaa && desc.idProduct == 0xddcc) { - int ret = libusb_open(dev_list[i], &dev_handle); - if (dev_handle == NULL || ret < 0) { goto fail; } - - unsigned char desc_serial[26] = { 0 }; - ret = libusb_get_string_descriptor_ascii(dev_handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); - if (ret < 0) { goto fail; } - - usb_serial = std::string((char *)desc_serial, ret).c_str(); - if (serial.empty() || serial == usb_serial) { - break; - } - libusb_close(dev_handle); - dev_handle = NULL; - } - } - if (dev_handle == NULL) goto fail; - libusb_free_device_list(dev_list, 1); - dev_list = nullptr; - - if (libusb_kernel_driver_active(dev_handle, 0) == 1) { - libusb_detach_kernel_driver(dev_handle, 0); + // TODO: support SPI here one day... + if (serial.find("spi") != std::string::npos) { + handle = std::make_unique(serial); + } else { + handle = std::make_unique(serial); } - err = libusb_set_configuration(dev_handle, 1); - if (err != 0) { goto fail; } - - err = libusb_claim_interface(dev_handle, 0); - if (err != 0) { goto fail; } - hw_type = get_hw_type(); assert((hw_type != cereal::PandaState::PandaType::WHITE_PANDA) && @@ -83,194 +27,44 @@ Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { (hw_type == cereal::PandaState::PandaType::DOS); return; - -fail: - if (dev_list != NULL) { - libusb_free_device_list(dev_list, 1); - } - cleanup(); - throw std::runtime_error("Error connecting to panda"); } -Panda::~Panda() { - std::lock_guard lk(usb_lock); - cleanup(); - connected = false; +bool Panda::connected() { + return handle->connected; } -void Panda::cleanup() { - if (dev_handle) { - libusb_release_interface(dev_handle, 0); - libusb_close(dev_handle); - } - - if (ctx) { - libusb_exit(ctx); - } +bool Panda::comms_healthy() { + return handle->comms_healthy; } std::vector Panda::list() { - // init libusb - ssize_t num_devices; - libusb_context *context = NULL; - libusb_device **dev_list = NULL; - std::vector serials; - - int err = init_usb_ctx(&context); - if (err != 0) { return serials; } - - num_devices = libusb_get_device_list(context, &dev_list); - if (num_devices < 0) { - LOGE("libusb can't get device list"); - goto finish; - } - for (size_t i = 0; i < num_devices; ++i) { - libusb_device *device = dev_list[i]; - libusb_device_descriptor desc; - libusb_get_device_descriptor(device, &desc); - if (desc.idVendor == 0xbbaa && desc.idProduct == 0xddcc) { - libusb_device_handle *handle = NULL; - int ret = libusb_open(device, &handle); - if (ret < 0) { goto finish; } - - unsigned char desc_serial[26] = { 0 }; - ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); - libusb_close(handle); - if (ret < 0) { goto finish; } - - serials.push_back(std::string((char *)desc_serial, ret).c_str()); - } - } - -finish: - if (dev_list != NULL) { - libusb_free_device_list(dev_list, 1); - } - if (context) { - libusb_exit(context); - } - return serials; -} - -void Panda::handle_usb_issue(int err, const char func[]) { - LOGE_100("usb error %d \"%s\" in %s", err, libusb_strerror((enum libusb_error)err), func); - if (err == LIBUSB_ERROR_NO_DEVICE) { - LOGE("lost connection"); - connected = false; - } - // TODO: check other errors, is simply retrying okay? -} - -int Panda::usb_write(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned int timeout) { - int err; - const uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; - - if (!connected) { - return LIBUSB_ERROR_NO_DEVICE; - } - - std::lock_guard lk(usb_lock); - do { - err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, NULL, 0, timeout); - if (err < 0) handle_usb_issue(err, __func__); - } while (err < 0 && connected); - - return err; -} - -int Panda::usb_read(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout) { - int err; - const uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; - - if (!connected) { - return LIBUSB_ERROR_NO_DEVICE; - } - - std::lock_guard lk(usb_lock); - do { - err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, wLength, timeout); - if (err < 0) handle_usb_issue(err, __func__); - } while (err < 0 && connected); - - return err; -} - -int Panda::usb_bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - int err; - int transferred = 0; - - if (!connected) { - return 0; - } - - std::lock_guard lk(usb_lock); - do { - // Try sending can messages. If the receive buffer on the panda is full it will NAK - // and libusb will try again. After 5ms, it will time out. We will drop the messages. - err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); - - if (err == LIBUSB_ERROR_TIMEOUT) { - LOGW("Transmit buffer full"); - break; - } else if (err != 0 || length != transferred) { - handle_usb_issue(err, __func__); - } - } while(err != 0 && connected); - - return transferred; -} - -int Panda::usb_bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - int err; - int transferred = 0; - - if (!connected) { - return 0; - } - - std::lock_guard lk(usb_lock); - - do { - err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); - - if (err == LIBUSB_ERROR_TIMEOUT) { - break; // timeout is okay to exit, recv still happened - } else if (err == LIBUSB_ERROR_OVERFLOW) { - comms_healthy = false; - LOGE_100("overflow got 0x%x", transferred); - } else if (err != 0) { - handle_usb_issue(err, __func__); - } - - } while(err != 0 && connected); - - return transferred; + return PandaUsbHandle::list(); } void Panda::set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param) { - usb_write(0xdc, (uint16_t)safety_model, safety_param); + handle->control_write(0xdc, (uint16_t)safety_model, safety_param); } void Panda::set_alternative_experience(uint16_t alternative_experience) { - usb_write(0xdf, alternative_experience, 0); + handle->control_write(0xdf, alternative_experience, 0); } cereal::PandaState::PandaType Panda::get_hw_type() { unsigned char hw_query[1] = {0}; - usb_read(0xc1, 0, 0, hw_query, 1); + handle->control_read(0xc1, 0, 0, hw_query, 1); return (cereal::PandaState::PandaType)(hw_query[0]); } void Panda::set_rtc(struct tm sys_time) { // tm struct has year defined as years since 1900 - usb_write(0xa1, (uint16_t)(1900 + sys_time.tm_year), 0); - usb_write(0xa2, (uint16_t)(1 + sys_time.tm_mon), 0); - usb_write(0xa3, (uint16_t)sys_time.tm_mday, 0); - // usb_write(0xa4, (uint16_t)(1 + sys_time.tm_wday), 0); - usb_write(0xa5, (uint16_t)sys_time.tm_hour, 0); - usb_write(0xa6, (uint16_t)sys_time.tm_min, 0); - usb_write(0xa7, (uint16_t)sys_time.tm_sec, 0); + handle->control_write(0xa1, (uint16_t)(1900 + sys_time.tm_year), 0); + handle->control_write(0xa2, (uint16_t)(1 + sys_time.tm_mon), 0); + handle->control_write(0xa3, (uint16_t)sys_time.tm_mday, 0); + // handle->control_write(0xa4, (uint16_t)(1 + sys_time.tm_wday), 0); + handle->control_write(0xa5, (uint16_t)sys_time.tm_hour, 0); + handle->control_write(0xa6, (uint16_t)sys_time.tm_min, 0); + handle->control_write(0xa7, (uint16_t)sys_time.tm_sec, 0); } struct tm Panda::get_rtc() { @@ -284,7 +78,7 @@ struct tm Panda::get_rtc() { uint8_t second; } rtc_time = {0}; - usb_read(0xa0, 0, 0, (unsigned char*)&rtc_time, sizeof(rtc_time)); + handle->control_read(0xa0, 0, 0, (unsigned char*)&rtc_time, sizeof(rtc_time)); struct tm new_time = { 0 }; new_time.tm_year = rtc_time.year - 1900; // tm struct has year defined as years since 1900 @@ -298,70 +92,70 @@ struct tm Panda::get_rtc() { } void Panda::set_fan_speed(uint16_t fan_speed) { - usb_write(0xb1, fan_speed, 0); + handle->control_write(0xb1, fan_speed, 0); } uint16_t Panda::get_fan_speed() { uint16_t fan_speed_rpm = 0; - usb_read(0xb2, 0, 0, (unsigned char*)&fan_speed_rpm, sizeof(fan_speed_rpm)); + handle->control_read(0xb2, 0, 0, (unsigned char*)&fan_speed_rpm, sizeof(fan_speed_rpm)); return fan_speed_rpm; } void Panda::set_ir_pwr(uint16_t ir_pwr) { - usb_write(0xb0, ir_pwr, 0); + handle->control_write(0xb0, ir_pwr, 0); } std::optional Panda::get_state() { health_t health {0}; - int err = usb_read(0xd2, 0, 0, (unsigned char*)&health, sizeof(health)); + int err = handle->control_read(0xd2, 0, 0, (unsigned char*)&health, sizeof(health)); return err >= 0 ? std::make_optional(health) : std::nullopt; } std::optional Panda::get_can_state(uint16_t can_number) { can_health_t can_health {0}; - int err = usb_read(0xc2, can_number, 0, (unsigned char*)&can_health, sizeof(can_health)); + int err = handle->control_read(0xc2, can_number, 0, (unsigned char*)&can_health, sizeof(can_health)); return err >= 0 ? std::make_optional(can_health) : std::nullopt; } void Panda::set_loopback(bool loopback) { - usb_write(0xe5, loopback, 0); + handle->control_write(0xe5, loopback, 0); } std::optional> Panda::get_firmware_version() { std::vector fw_sig_buf(128); - int read_1 = usb_read(0xd3, 0, 0, &fw_sig_buf[0], 64); - int read_2 = usb_read(0xd4, 0, 0, &fw_sig_buf[64], 64); + int read_1 = handle->control_read(0xd3, 0, 0, &fw_sig_buf[0], 64); + int read_2 = handle->control_read(0xd4, 0, 0, &fw_sig_buf[64], 64); return ((read_1 == 64) && (read_2 == 64)) ? std::make_optional(fw_sig_buf) : std::nullopt; } std::optional Panda::get_serial() { char serial_buf[17] = {'\0'}; - int err = usb_read(0xd0, 0, 0, (uint8_t*)serial_buf, 16); + int err = handle->control_read(0xd0, 0, 0, (uint8_t*)serial_buf, 16); return err >= 0 ? std::make_optional(serial_buf) : std::nullopt; } void Panda::set_power_saving(bool power_saving) { - usb_write(0xe7, power_saving, 0); + handle->control_write(0xe7, power_saving, 0); } void Panda::enable_deepsleep() { - usb_write(0xfb, 0, 0); + handle->control_write(0xfb, 0, 0); } void Panda::send_heartbeat(bool engaged) { - usb_write(0xf3, engaged, 0); + handle->control_write(0xf3, engaged, 0); } void Panda::set_can_speed_kbps(uint16_t bus, uint16_t speed) { - usb_write(0xde, bus, (speed * 10)); + handle->control_write(0xde, bus, (speed * 10)); } void Panda::set_data_speed_kbps(uint16_t bus, uint16_t speed) { - usb_write(0xf9, bus, (speed * 10)); + handle->control_write(0xf9, bus, (speed * 10)); } void Panda::set_canfd_non_iso(uint16_t bus, bool non_iso) { - usb_write(0xfc, bus, non_iso); + handle->control_write(0xfc, bus, non_iso); } static uint8_t len_to_dlc(uint8_t len) { @@ -399,8 +193,7 @@ void Panda::pack_can_buffer(const capnp::List::Reader &can_data } auto can_data = cmsg.getDat(); uint8_t data_len_code = len_to_dlc(can_data.size()); - assert(can_data.size() <= ((hw_type == cereal::PandaState::PandaType::RED_PANDA || - hw_type == cereal::PandaState::PandaType::RED_PANDA_V2) ? 64 : 8)); + assert(can_data.size() <= 64); assert(can_data.size() == dlc_to_len[data_len_code]); can_header header; @@ -423,14 +216,14 @@ void Panda::pack_can_buffer(const capnp::List::Reader &can_data void Panda::can_send(capnp::List::Reader can_data_list) { pack_can_buffer(can_data_list, [=](uint8_t* data, size_t size) { - usb_bulk_write(3, data, size, 5); + handle->bulk_write(3, data, size, 5); }); } bool Panda::can_receive(std::vector& out_vec) { uint8_t data[RECV_SIZE]; - int recv = usb_bulk_read(0x81, (uint8_t*)data, RECV_SIZE); - if (!comms_healthy) { + int recv = handle->bulk_read(0x81, (uint8_t*)data, RECV_SIZE); + if (!comms_healthy()) { return false; } if (recv == RECV_SIZE) { @@ -445,7 +238,7 @@ bool Panda::unpack_can_buffer(uint8_t *data, int size, std::vector &o for (int i = 0; i < size; i += USBPACKET_MAX_SIZE) { if (data[i] != i / USBPACKET_MAX_SIZE) { LOGE("CAN: MALFORMED USB RECV PACKET"); - comms_healthy = false; + handle->comms_healthy = false; return false; } int chunk_len = std::min(USBPACKET_MAX_SIZE, (size - i)); diff --git a/selfdrive/boardd/panda.h b/selfdrive/boardd/panda.h index c7a0e7a6c1..5b3cbb9a3e 100644 --- a/selfdrive/boardd/panda.h +++ b/selfdrive/boardd/panda.h @@ -1,26 +1,26 @@ #pragma once -#include #include #include #include #include -#include +#include #include #include -#include - #include "cereal/gen/cpp/car.capnp.h" #include "cereal/gen/cpp/log.capnp.h" #include "panda/board/health.h" +#include "selfdrive/boardd/panda_comms.h" + -#define TIMEOUT 0 #define PANDA_CAN_CNT 3 #define PANDA_BUS_CNT 4 -#define RECV_SIZE (0x4000U) + #define USB_TX_SOFT_LIMIT (0x100U) #define USBPACKET_MAX_SIZE (0x40) + +#define RECV_SIZE (0x4000U) #define CANPACKET_HEAD_SIZE 5U #define CANPACKET_MAX_SIZE 72U #define CANPACKET_REJECTED (0xC0U) @@ -37,41 +37,32 @@ struct __attribute__((packed)) can_header { }; struct can_frame { - long address; - std::string dat; - long busTime; - long src; + long address; + std::string dat; + long busTime; + long src; }; + class Panda { - private: - libusb_context *ctx = NULL; - libusb_device_handle *dev_handle = NULL; - std::mutex usb_lock; +private: + std::unique_ptr handle; std::vector recv_buf; - void handle_usb_issue(int err, const char func[]); - void cleanup(); - public: +public: Panda(std::string serial="", uint32_t bus_offset=0); - ~Panda(); - std::string usb_serial; - std::atomic connected = true; - std::atomic comms_healthy = true; + std::string hw_serial; cereal::PandaState::PandaType hw_type = cereal::PandaState::PandaType::UNKNOWN; bool has_rtc = false; const uint32_t bus_offset; + bool connected(); + bool comms_healthy(); + // Static functions static std::vector list(); - // HW communication - int usb_write(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned int timeout=TIMEOUT); - int usb_read(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout=TIMEOUT); - int usb_bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); - int usb_bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); - // Panda functionality cereal::PandaState::PandaType get_hw_type(); void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U); diff --git a/selfdrive/boardd/panda_comms.cc b/selfdrive/boardd/panda_comms.cc new file mode 100644 index 0000000000..e73cb69318 --- /dev/null +++ b/selfdrive/boardd/panda_comms.cc @@ -0,0 +1,232 @@ +#include "selfdrive/boardd/panda.h" + +#include +#include + +#include "common/swaglog.h" + +static int init_usb_ctx(libusb_context **context) { + assert(context != nullptr); + + int err = libusb_init(context); + if (err != 0) { + LOGE("libusb initialization error"); + return err; + } + +#if LIBUSB_API_VERSION >= 0x01000106 + libusb_set_option(*context, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO); +#else + libusb_set_debug(*context, 3); +#endif + + return err; +} + +PandaUsbHandle::PandaUsbHandle(std::string serial) : PandaCommsHandle(serial) { + // init libusb + ssize_t num_devices; + libusb_device **dev_list = NULL; + int err = init_usb_ctx(&ctx); + if (err != 0) { goto fail; } + + // connect by serial + num_devices = libusb_get_device_list(ctx, &dev_list); + if (num_devices < 0) { goto fail; } + for (size_t i = 0; i < num_devices; ++i) { + libusb_device_descriptor desc; + libusb_get_device_descriptor(dev_list[i], &desc); + if (desc.idVendor == 0xbbaa && desc.idProduct == 0xddcc) { + int ret = libusb_open(dev_list[i], &dev_handle); + if (dev_handle == NULL || ret < 0) { goto fail; } + + unsigned char desc_serial[26] = { 0 }; + ret = libusb_get_string_descriptor_ascii(dev_handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); + if (ret < 0) { goto fail; } + + auto hw_serial = std::string((char *)desc_serial, ret); + if (serial.empty() || serial == hw_serial) { + break; + } + libusb_close(dev_handle); + dev_handle = NULL; + } + } + if (dev_handle == NULL) goto fail; + libusb_free_device_list(dev_list, 1); + dev_list = nullptr; + + if (libusb_kernel_driver_active(dev_handle, 0) == 1) { + libusb_detach_kernel_driver(dev_handle, 0); + } + + err = libusb_set_configuration(dev_handle, 1); + if (err != 0) { goto fail; } + + err = libusb_claim_interface(dev_handle, 0); + if (err != 0) { goto fail; } + + return; + +fail: + if (dev_list != NULL) { + libusb_free_device_list(dev_list, 1); + } + cleanup(); + throw std::runtime_error("Error connecting to panda"); +} + +PandaUsbHandle::~PandaUsbHandle() { + std::lock_guard lk(hw_lock); + cleanup(); + connected = false; +} + +void PandaUsbHandle::cleanup() { + if (dev_handle) { + libusb_release_interface(dev_handle, 0); + libusb_close(dev_handle); + } + + if (ctx) { + libusb_exit(ctx); + } +} + +std::vector PandaUsbHandle::list() { + // init libusb + ssize_t num_devices; + libusb_context *context = NULL; + libusb_device **dev_list = NULL; + std::vector serials; + + int err = init_usb_ctx(&context); + if (err != 0) { return serials; } + + num_devices = libusb_get_device_list(context, &dev_list); + if (num_devices < 0) { + LOGE("libusb can't get device list"); + goto finish; + } + for (size_t i = 0; i < num_devices; ++i) { + libusb_device *device = dev_list[i]; + libusb_device_descriptor desc; + libusb_get_device_descriptor(device, &desc); + if (desc.idVendor == 0xbbaa && desc.idProduct == 0xddcc) { + libusb_device_handle *handle = NULL; + int ret = libusb_open(device, &handle); + if (ret < 0) { goto finish; } + + unsigned char desc_serial[26] = { 0 }; + ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); + libusb_close(handle); + if (ret < 0) { goto finish; } + + serials.push_back(std::string((char *)desc_serial, ret).c_str()); + } + } + +finish: + if (dev_list != NULL) { + libusb_free_device_list(dev_list, 1); + } + if (context) { + libusb_exit(context); + } + return serials; +} + +void PandaUsbHandle::handle_usb_issue(int err, const char func[]) { + LOGE_100("usb error %d \"%s\" in %s", err, libusb_strerror((enum libusb_error)err), func); + if (err == LIBUSB_ERROR_NO_DEVICE) { + LOGE("lost connection"); + connected = false; + } + // TODO: check other errors, is simply retrying okay? +} + +int PandaUsbHandle::control_write(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected) { + return LIBUSB_ERROR_NO_DEVICE; + } + + std::lock_guard lk(hw_lock); + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, NULL, 0, timeout); + if (err < 0) handle_usb_issue(err, __func__); + } while (err < 0 && connected); + + return err; +} + +int PandaUsbHandle::control_read(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected) { + return LIBUSB_ERROR_NO_DEVICE; + } + + std::lock_guard lk(hw_lock); + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, wLength, timeout); + if (err < 0) handle_usb_issue(err, __func__); + } while (err < 0 && connected); + + return err; +} + +int PandaUsbHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected) { + return 0; + } + + std::lock_guard lk(hw_lock); + do { + // Try sending can messages. If the receive buffer on the panda is full it will NAK + // and libusb will try again. After 5ms, it will time out. We will drop the messages. + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + + if (err == LIBUSB_ERROR_TIMEOUT) { + LOGW("Transmit buffer full"); + break; + } else if (err != 0 || length != transferred) { + handle_usb_issue(err, __func__); + } + } while(err != 0 && connected); + + return transferred; +} + +int PandaUsbHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected) { + return 0; + } + + std::lock_guard lk(hw_lock); + + do { + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + + if (err == LIBUSB_ERROR_TIMEOUT) { + break; // timeout is okay to exit, recv still happened + } else if (err == LIBUSB_ERROR_OVERFLOW) { + comms_healthy = false; + LOGE_100("overflow got 0x%x", transferred); + } else if (err != 0) { + handle_usb_issue(err, __func__); + } + + } while(err != 0 && connected); + + return transferred; +} diff --git a/selfdrive/boardd/panda_comms.h b/selfdrive/boardd/panda_comms.h new file mode 100644 index 0000000000..f42eadc5b2 --- /dev/null +++ b/selfdrive/boardd/panda_comms.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include + + +#define TIMEOUT 0 +#define SPI_BUF_SIZE 1024 + +const bool PANDA_NO_RETRY = getenv("PANDA_NO_RETRY"); + + +// comms base class +class PandaCommsHandle { +public: + PandaCommsHandle(std::string serial) {}; + virtual ~PandaCommsHandle() {}; + virtual void cleanup() = 0; + + std::atomic connected = true; + std::atomic comms_healthy = true; + static std::vector list(); + + // HW communication + virtual int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT) = 0; + virtual int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT) = 0; + virtual int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT) = 0; + virtual int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT) = 0; + +protected: + std::recursive_mutex hw_lock; +}; + +class PandaUsbHandle : public PandaCommsHandle { +public: + PandaUsbHandle(std::string serial); + ~PandaUsbHandle(); + int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT); + int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT); + int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + void cleanup(); + + static std::vector list(); + +private: + libusb_context *ctx = NULL; + libusb_device_handle *dev_handle = NULL; + std::vector recv_buf; + void handle_usb_issue(int err, const char func[]); +}; + +class PandaSpiHandle : public PandaCommsHandle { +public: + PandaSpiHandle(std::string serial); + ~PandaSpiHandle(); + int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT); + int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT); + int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + void cleanup(); + + static std::vector list(); + +private: + int spi_fd = -1; + uint8_t tx_buf[SPI_BUF_SIZE]; + uint8_t rx_buf[SPI_BUF_SIZE]; + + int wait_for_ack(spi_ioc_transfer &transfer, uint8_t ack); + int bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len); + int spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len); + int spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len); +}; diff --git a/selfdrive/boardd/spi.cc b/selfdrive/boardd/spi.cc new file mode 100644 index 0000000000..2803f58db0 --- /dev/null +++ b/selfdrive/boardd/spi.cc @@ -0,0 +1,287 @@ +#include +#include + +#include +#include +#include + +#include "common/util.h" +#include "common/swaglog.h" +#include "panda/board/comms_definitions.h" +#include "selfdrive/boardd/panda_comms.h" + + +#define SPI_SYNC 0x5AU +#define SPI_HACK 0x79U +#define SPI_DACK 0x85U +#define SPI_NACK 0x1FU +#define SPI_CHECKSUM_START 0xABU + +struct __attribute__((packed)) spi_header { + uint8_t sync; + uint8_t endpoint; + uint16_t tx_len; + uint16_t max_rx_len; +}; + + +PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { + LOGD("opening SPI panda: %s", serial.c_str()); + + int err; + uint32_t spi_mode = SPI_MODE_0; + uint32_t spi_speed = 30000000; + uint8_t spi_bits_per_word = 8; + + spi_fd = open(serial.c_str(), O_RDWR); + if (spi_fd < 0) { + LOGE("failed opening SPI device %d", err); + goto fail; + } + + // SPI settings + err = util::safe_ioctl(spi_fd, SPI_IOC_WR_MODE, &spi_mode); + if (err < 0) { + LOGE("failed setting SPI mode %d", err); + goto fail; + } + + err = util::safe_ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed); + if (err < 0) { + LOGE("failed setting SPI speed"); + goto fail; + } + + err = util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word); + if (err < 0) { + LOGE("failed setting SPI bits per word"); + goto fail; + } + + return; + +fail: + cleanup(); + throw std::runtime_error("Error connecting to panda"); +} + +PandaSpiHandle::~PandaSpiHandle() { + std::lock_guard lk(hw_lock); + cleanup(); +} + +void PandaSpiHandle::cleanup() { + if (spi_fd != -1) { + close(spi_fd); + spi_fd = -1; + } +} + + + +int PandaSpiHandle::control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout) { + ControlPacket_t packet = { + .request = request, + .param1 = param1, + .param2 = param2, + .length = 0 + }; + return spi_transfer_retry(0, (uint8_t *) &packet, sizeof(packet), NULL, 0); +} + +int PandaSpiHandle::control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout) { + ControlPacket_t packet = { + .request = request, + .param1 = param1, + .param2 = param2, + .length = length + }; + return spi_transfer_retry(0, (uint8_t *) &packet, sizeof(packet), data, length); +} + +int PandaSpiHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + return bulk_transfer(endpoint, data, length, NULL, 0); +} +int PandaSpiHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + return bulk_transfer(endpoint, NULL, 0, data, length); +} + +int PandaSpiHandle::bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len) { + std::lock_guard lk(hw_lock); + + const int xfer_size = 0x40; + + int ret = 0; + uint16_t length = (tx_data != NULL) ? tx_len : rx_len; + for (int i = 0; i < (int)std::ceil((float)length / xfer_size); i++) { + int d; + if (tx_data != NULL) { + int len = std::min(xfer_size, tx_len - (xfer_size * i)); + d = spi_transfer_retry(endpoint, tx_data + (xfer_size * i), len, NULL, 0); + } else { + d = spi_transfer_retry(endpoint, NULL, 0, rx_data + (xfer_size * i), xfer_size); + } + + if (d < 0) { + LOGE("SPI: bulk transfer failed with %d", d); + comms_healthy = false; + return -1; + } + + ret += d; + if ((rx_data != NULL) && d < xfer_size) { + break; + } + } + + return ret; +} + + + +std::vector PandaSpiHandle::list() { + // TODO: list all pandas available over SPI + return {}; +} + + + +void add_checksum(uint8_t *data, int data_len) { + data[data_len] = SPI_CHECKSUM_START; + for (int i=0; i < data_len; i++) { + data[data_len] ^= data[i]; + } +} + +bool check_checksum(uint8_t *data, int data_len) { + uint8_t checksum = SPI_CHECKSUM_START; + for (uint16_t i = 0U; i < data_len; i++) { + checksum ^= data[i]; + } + return checksum == 0U; +} + + +int PandaSpiHandle::spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len) { + int ret; + + std::lock_guard lk(hw_lock); + do { + // TODO: handle error + ret = spi_transfer(endpoint, tx_data, tx_len, rx_data, max_rx_len); + } while (ret < 0 && connected && !PANDA_NO_RETRY); + + return ret; +} + +int PandaSpiHandle::wait_for_ack(spi_ioc_transfer &transfer, uint8_t ack) { + // TODO: add timeout? + while (true) { + int ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); + if (ret < 0) { + LOGE("SPI: failed to send ACK request"); + return ret; + } + + if (rx_buf[0] == ack) { + break; + } else if (rx_buf[0] == SPI_NACK) { + LOGW("SPI: got NACK"); + return -1; + } + } + + return 0; +} + +int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len) { + int ret; + uint16_t rx_data_len; + + // needs to be less, since we need to have space for the checksum + assert(tx_len < SPI_BUF_SIZE); + assert(max_rx_len < SPI_BUF_SIZE); + + spi_header header = { + .sync = SPI_SYNC, + .endpoint = endpoint, + .tx_len = tx_len, + .max_rx_len = max_rx_len + }; + + spi_ioc_transfer transfer = { + .tx_buf = (uint64_t)tx_buf, + .rx_buf = (uint64_t)rx_buf + }; + + // Send header + memcpy(tx_buf, &header, sizeof(header)); + add_checksum(tx_buf, sizeof(header)); + transfer.len = sizeof(header) + 1; + ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); + if (ret < 0) { + LOGE("SPI: failed to send header"); + goto transfer_fail; + } + + // Wait for (N)ACK + tx_buf[0] = 0x12; + transfer.len = 1; + ret = wait_for_ack(transfer, SPI_HACK); + if (ret < 0) { + goto transfer_fail; + } + + // Send data + if (tx_data != NULL) { + memcpy(tx_buf, tx_data, tx_len); + } + add_checksum(tx_buf, tx_len); + transfer.len = tx_len + 1; + ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); + if (ret < 0) { + LOGE("SPI: failed to send data"); + goto transfer_fail; + } + + // Wait for (N)ACK + tx_buf[0] = 0xab; + transfer.len = 1; + ret = wait_for_ack(transfer, SPI_DACK); + if (ret < 0) { + goto transfer_fail; + } + + // Read data len + transfer.len = 2; + transfer.rx_buf = (uint64_t)(rx_buf + 1); + ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); + if (ret < 0) { + LOGE("SPI: failed to read rx data len"); + goto transfer_fail; + } + rx_data_len = *(uint16_t *)(rx_buf+1); + assert(rx_data_len < SPI_BUF_SIZE); + + // Read data + transfer.len = rx_data_len + 1; + transfer.rx_buf = (uint64_t)(rx_buf + 2 + 1); + ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); + if (ret < 0) { + LOGE("SPI: failed to read rx data"); + goto transfer_fail; + } + if (!check_checksum(rx_buf, rx_data_len + 4)) { + LOGE("SPI: bad checksum"); + goto transfer_fail; + } + + if (rx_data != NULL) { + memcpy(rx_data, rx_buf + 3, rx_data_len); + } + + return rx_data_len; + +transfer_fail: + return ret; +} diff --git a/selfdrive/car/body/values.py b/selfdrive/car/body/values.py index 66f1b947a8..548039bc70 100644 --- a/selfdrive/car/body/values.py +++ b/selfdrive/car/body/values.py @@ -26,6 +26,12 @@ CAR_INFO: Dict[str, CarInfo] = { CAR.BODY: CarInfo("comma body", package="All"), } +FINGERPRINTS = { + CAR.BODY: [{ + 513: 8, 516: 8, 514: 3, 515: 4, + }], +} + FW_QUERY_CONFIG = FwQueryConfig( requests=[ Request( @@ -40,10 +46,13 @@ FW_VERSIONS = { CAR.BODY: { (Ecu.engine, 0x720, None): [ b'0.0.01', - b'02/27/2022' + b'02/27/2022', + b'0.3.00a', ], + # git hash of the firmware used (Ecu.debug, 0x721, None): [ - b'166bd860' # git hash of the firmware used + b'166bd860', + b'dc780f85', ], }, } diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index 58afed27eb..03313e2ff6 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -40,6 +40,7 @@ def get_all_car_info() -> List[CarInfo]: for _car_info in car_info: if not hasattr(_car_info, "row"): + _car_info.init_make(CP) _car_info.init(CP, footnotes) all_car_info.append(_car_info) diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 8e057a1563..7cf44514d6 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -73,12 +73,11 @@ CarFootnote = namedtuple("CarFootnote", ["text", "column", "docs_only"], default class CommonFootnote(Enum): EXP_LONG_AVAIL = CarFootnote( - "Experimental openpilot longitudinal control is available behind a toggle; the toggle is only available in non-release branches such as `master-ci`. " + - "Using openpilot longitudinal may disable Automatic Emergency Braking (AEB).", + "Experimental openpilot longitudinal control is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `master-ci`. ", Column.LONGITUDINAL, docs_only=True) EXP_LONG_DSU = CarFootnote( - "When the Driver Support Unit (DSU) is disconnected, openpilot Adaptive Cruise Control (ACC) will replace " + - "stock Adaptive Cruise Control (ACC). NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).", + "By default, this car will use the stock Adaptive Cruise Control (ACC) for longitudinal control. If the Driver Support Unit (DSU) is disconnected, openpilot ACC will replace " + + "stock ACC. NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).", Column.LONGITUDINAL) @@ -173,6 +172,9 @@ class CarInfo: return self + def init_make(self, CP: car.CarParams): + """CarInfo subclasses can add make-specific logic for harness selection, footnotes, etc.""" + def get_detail_sentence(self, CP): if not CP.notCar: sentence_builder = "openpilot upgrades your {car_model} with automated lane centering{alc} and adaptive cruise control{acc}." diff --git a/selfdrive/car/gm/carstate.py b/selfdrive/car/gm/carstate.py index af69307a2c..df1b4b2866 100644 --- a/selfdrive/car/gm/carstate.py +++ b/selfdrive/car/gm/carstate.py @@ -51,16 +51,19 @@ class CarState(CarStateBase): else: ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(pt_cp.vl["ECMPRDNL2"]["PRNDL2"], None)) - # Some Volt 2016-17 have loose brake pedal push rod retainers which causes the ECM to believe - # that the brake is being intermittently pressed without user interaction. - # To avoid a cruise fault we need to match the ECM's brake pressed signal and threshold - # https://static.nhtsa.gov/odi/tsbs/2017/MC-10137629-9999.pdf ret.brake = pt_cp.vl["ECMAcceleratorPos"]["BrakePedalPos"] - ret.brakePressed = ret.brake >= 8 + if self.CP.networkLocation == NetworkLocation.fwdCamera: + ret.brakePressed = pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0 + else: + # Some Volt 2016-17 have loose brake pedal push rod retainers which causes the ECM to believe + # that the brake is being intermittently pressed without user interaction. + # To avoid a cruise fault we need to use a conservative brake position threshold + # https://static.nhtsa.gov/odi/tsbs/2017/MC-10137629-9999.pdf + ret.brakePressed = ret.brake >= 8 # Regen braking is braking if self.CP.transmissionType == TransmissionType.direct: - ret.brakePressed = ret.brakePressed or pt_cp.vl["EBCMRegenPaddle"]["RegenPaddle"] != 0 + ret.regenBraking = pt_cp.vl["EBCMRegenPaddle"]["RegenPaddle"] != 0 ret.gas = pt_cp.vl["AcceleratorPedal2"]["AcceleratorPedal2"] / 254. ret.gasPressed = ret.gas > 1e-5 @@ -154,6 +157,7 @@ class CarState(CarStateBase): ("TractionControlOn", "ESPStatus"), ("ParkBrake", "VehicleIgnitionAlt"), ("CruiseMainOn", "ECMEngineStatus"), + ("BrakePressed", "ECMEngineStatus"), ] checks = [ diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 2420098b4a..eb5ab7329a 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -211,17 +211,20 @@ class CarInterface(CarInterfaceBase): ret = self.CS.update(self.cp, self.cp_cam, self.cp_loopback) if self.CS.cruise_buttons != self.CS.prev_cruise_buttons and self.CS.prev_cruise_buttons != CruiseButtons.INIT: - be = create_button_event(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, BUTTONS_DICT, CruiseButtons.UNPRESS) + buttonEvents = [create_button_event(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, BUTTONS_DICT, CruiseButtons.UNPRESS)] + # Handle ACCButtons changing buttons mid-press + if self.CS.cruise_buttons != CruiseButtons.UNPRESS and self.CS.prev_cruise_buttons != CruiseButtons.UNPRESS: + buttonEvents.append(create_button_event(CruiseButtons.UNPRESS, self.CS.prev_cruise_buttons, BUTTONS_DICT, CruiseButtons.UNPRESS)) - # Suppress resume button if we're resuming from stop so we don't adjust speed. - if be.type == ButtonType.accelCruise and (ret.cruiseState.enabled and ret.standstill): - be.type = ButtonType.unknown - - ret.buttonEvents = [be] + ret.buttonEvents = buttonEvents + # The ECM allows enabling on falling edge of set, but only rising edge of resume events = self.create_common_events(ret, extra_gears=[GearShifter.sport, GearShifter.low, GearShifter.eco, GearShifter.manumatic], - pcm_enable=self.CP.pcmCruise) + pcm_enable=self.CP.pcmCruise, enable_buttons=(ButtonType.decelCruise,)) + if not self.CP.pcmCruise: + if any(b.type == ButtonType.accelCruise and b.pressed for b in ret.buttonEvents): + events.add(EventName.buttonEnable) # Enabling at a standstill with brake is allowed # TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index eace6b6aca..0a8cdc6dbb 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -1,5 +1,5 @@ from collections import defaultdict -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import Enum from typing import Dict, List, Union @@ -84,25 +84,30 @@ class Footnote(Enum): @dataclass class GMCarInfo(CarInfo): package: str = "Adaptive Cruise Control (ACC)" - harness: Enum = Harness.obd_ii - footnotes: List[Enum] = field(default_factory=lambda: [Footnote.OBD_II]) + + def init_make(self, CP: car.CarParams): + if CP.networkLocation == car.CarParams.NetworkLocation.fwdCamera: + self.harness = Harness.gm + else: + self.harness = Harness.obd_ii + self.footnotes.append(Footnote.OBD_II) CAR_INFO: Dict[str, Union[GMCarInfo, List[GMCarInfo]]] = { CAR.HOLDEN_ASTRA: GMCarInfo("Holden Astra 2017"), - CAR.VOLT: GMCarInfo("Chevrolet Volt 2017-18", min_enable_speed=0), + CAR.VOLT: GMCarInfo("Chevrolet Volt 2017-18", min_enable_speed=0, video_link="https://youtu.be/QeMCN_4TFfQ"), CAR.CADILLAC_ATS: GMCarInfo("Cadillac ATS Premium Performance 2018"), CAR.MALIBU: GMCarInfo("Chevrolet Malibu Premier 2017"), CAR.ACADIA: GMCarInfo("GMC Acadia 2018", video_link="https://www.youtube.com/watch?v=0ZN6DdsBUZo"), CAR.BUICK_REGAL: GMCarInfo("Buick Regal Essence 2018"), CAR.ESCALADE_ESV: GMCarInfo("Cadillac Escalade ESV 2016", "Adaptive Cruise Control (ACC) & LKAS"), - CAR.BOLT_EV: GMCarInfo("Chevrolet Bolt EV 2022-23", footnotes=[], harness=Harness.gm), - CAR.BOLT_EUV: GMCarInfo("Chevrolet Bolt EUV 2022-23", "Premier or Premier Redline Trim without Super Cruise Package", "https://youtu.be/xvwzGMUA210", footnotes=[], harness=Harness.gm), + CAR.BOLT_EV: GMCarInfo("Chevrolet Bolt EV 2022-23"), + CAR.BOLT_EUV: GMCarInfo("Chevrolet Bolt EUV 2022-23", "Premier or Premier Redline Trim without Super Cruise Package", "https://youtu.be/xvwzGMUA210"), CAR.SILVERADO: [ - GMCarInfo("Chevrolet Silverado 1500 2020-21", "Safety Package II", footnotes=[], harness=Harness.gm), - GMCarInfo("GMC Sierra 1500 2020-21", "Driver Alert Package II", footnotes=[], harness=Harness.gm), + GMCarInfo("Chevrolet Silverado 1500 2020-21", "Safety Package II"), + GMCarInfo("GMC Sierra 1500 2020-21", "Driver Alert Package II", "https://youtu.be/5HbNoBLzRwE"), ], - CAR.EQUINOX: GMCarInfo("Chevrolet Equinox 2019-22", footnotes=[], harness=Harness.gm), + CAR.EQUINOX: GMCarInfo("Chevrolet Equinox 2019-22"), } diff --git a/selfdrive/car/honda/carcontroller.py b/selfdrive/car/honda/carcontroller.py index 5fa475fe08..790dce1810 100644 --- a/selfdrive/car/honda/carcontroller.py +++ b/selfdrive/car/honda/carcontroller.py @@ -99,6 +99,12 @@ HUDData = namedtuple("HUDData", "lanes_visible", "fcw", "acc_alert", "steer_required"]) +def rate_limit_steer(new_steer, last_steer): + # TODO just hardcoded ramp to min/max in 0.33s for all Honda + MAX_DELTA = 3 * DT_CTRL + return clip(new_steer, last_steer - MAX_DELTA, last_steer + MAX_DELTA) + + class CarController: def __init__(self, dbc_name, CP, VM): self.CP = CP @@ -116,6 +122,7 @@ class CarController: self.speed = 0.0 self.gas = 0.0 self.brake = 0.0 + self.last_steer = 0.0 def update(self, CC, CS): actuators = CC.actuators @@ -130,6 +137,10 @@ class CarController: accel = 0.0 gas, brake = 0.0, 0.0 + # *** rate limit steer *** + limited_steer = rate_limit_steer(actuators.steer, self.last_steer) + self.last_steer = limited_steer + # *** apply brake hysteresis *** pre_limit_brake, self.braking, self.brake_steady = actuator_hysteresis(brake, self.braking, self.brake_steady, CS.out.vEgo, self.CP.carFingerprint) @@ -143,7 +154,7 @@ class CarController: # **** process the car messages **** # steer torque is converted back to CAN reference (positive when steering right) - apply_steer = int(interp(-actuators.steer * self.params.STEER_MAX, + apply_steer = int(interp(-limited_steer * self.params.STEER_MAX, self.params.STEER_LOOKUP_BP, self.params.STEER_LOOKUP_V)) # Send CAN commands @@ -250,6 +261,7 @@ class CarController: new_actuators.accel = self.accel new_actuators.gas = self.gas new_actuators.brake = self.brake + new_actuators.steer = self.last_steer self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 2510f2e1ff..151c2140f5 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -106,40 +106,46 @@ class Footnote(Enum): class HondaCarInfo(CarInfo): package: str = "Honda Sensing" + def init_make(self, CP: car.CarParams): + if CP.carFingerprint in HONDA_BOSCH: + self.harness = Harness.bosch_b if CP.carFingerprint in HONDA_BOSCH_RADARLESS else Harness.bosch_a + else: + self.harness = Harness.nidec + CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { CAR.ACCORD: [ - HondaCarInfo("Honda Accord 2018-22", "All", "https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), - HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + HondaCarInfo("Honda Accord 2018-22", "All", "https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS), + HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS), ], - CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-22", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), - CAR.CIVIC: HondaCarInfo("Honda Civic 2016-18", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec, video_link="https://youtu.be/-IkImTe1NYE"), + CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-22", "All", min_steer_speed=3. * CV.MPH_TO_MS), + CAR.CIVIC: HondaCarInfo("Honda Civic 2016-18", min_steer_speed=12. * CV.MPH_TO_MS, video_link="https://youtu.be/-IkImTe1NYE"), CAR.CIVIC_BOSCH: [ - HondaCarInfo("Honda Civic 2019-21", "All", "https://www.youtube.com/watch?v=4Iz1Mz5LGF8", [Footnote.CIVIC_DIESEL], 2. * CV.MPH_TO_MS, harness=Harness.bosch_a), - HondaCarInfo("Honda Civic Hatchback 2017-21", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.bosch_a), + HondaCarInfo("Honda Civic 2019-21", "All", "https://www.youtube.com/watch?v=4Iz1Mz5LGF8", [Footnote.CIVIC_DIESEL], 2. * CV.MPH_TO_MS), + HondaCarInfo("Honda Civic Hatchback 2017-21", min_steer_speed=12. * CV.MPH_TO_MS), ], CAR.CIVIC_BOSCH_DIESEL: None, # same platform CAR.CIVIC_2022: [ - HondaCarInfo("Honda Civic 2022", "All", harness=Harness.bosch_b), - HondaCarInfo("Honda Civic Hatchback 2022", "All", harness=Harness.bosch_b), + HondaCarInfo("Honda Civic 2022", "All"), + HondaCarInfo("Honda Civic Hatchback 2022", "All"), ], - CAR.ACURA_ILX: HondaCarInfo("Acura ILX 2016-19", "AcuraWatch Plus", min_steer_speed=25. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.CRV: HondaCarInfo("Honda CR-V 2015-16", "Touring Trim", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.CRV_5G: HondaCarInfo("Honda CR-V 2017-22", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.bosch_a), + CAR.ACURA_ILX: HondaCarInfo("Acura ILX 2016-19", "AcuraWatch Plus", min_steer_speed=25. * CV.MPH_TO_MS), + CAR.CRV: HondaCarInfo("Honda CR-V 2015-16", "Touring Trim", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.CRV_5G: HondaCarInfo("Honda CR-V 2017-22", min_steer_speed=12. * CV.MPH_TO_MS), CAR.CRV_EU: None, # HondaCarInfo("Honda CR-V EU", "Touring"), # Euro version of CRV Touring - CAR.CRV_HYBRID: HondaCarInfo("Honda CR-V Hybrid 2017-19", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.bosch_a), - CAR.FIT: HondaCarInfo("Honda Fit 2018-20", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.FREED: HondaCarInfo("Honda Freed 2020", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.HRV: HondaCarInfo("Honda HR-V 2019-22", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.ODYSSEY: HondaCarInfo("Honda Odyssey 2018-20", harness=Harness.nidec), + CAR.CRV_HYBRID: HondaCarInfo("Honda CR-V Hybrid 2017-19", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.FIT: HondaCarInfo("Honda Fit 2018-20", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.FREED: HondaCarInfo("Honda Freed 2020", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.HRV: HondaCarInfo("Honda HR-V 2019-22", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.ODYSSEY: HondaCarInfo("Honda Odyssey 2018-20"), CAR.ODYSSEY_CHN: None, # Chinese version of Odyssey - CAR.ACURA_RDX: HondaCarInfo("Acura RDX 2016-18", "AcuraWatch Plus", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.ACURA_RDX_3G: HondaCarInfo("Acura RDX 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), - CAR.PILOT: HondaCarInfo("Honda Pilot 2016-22", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.PASSPORT: HondaCarInfo("Honda Passport 2019-21", "All", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-22", min_steer_speed=12. * CV.MPH_TO_MS, harness=Harness.nidec), - CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), - CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + CAR.ACURA_RDX: HondaCarInfo("Acura RDX 2016-18", "AcuraWatch Plus", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.ACURA_RDX_3G: HondaCarInfo("Acura RDX 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS), + CAR.PILOT: HondaCarInfo("Honda Pilot 2016-22", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.PASSPORT: HondaCarInfo("Honda Passport 2019-21", "All", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-22", min_steer_speed=12. * CV.MPH_TO_MS), + CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS), + CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS), } FW_QUERY_CONFIG = FwQueryConfig( diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index 2f944edc0f..e5fdbfd57a 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -85,7 +85,7 @@ class CarController: # *** common hyundai stuff *** # tester present - w/ no response (keeps relevant ECU disabled) - if self.frame % 100 == 0 and self.CP.openpilotLongitudinalControl: + if self.frame % 100 == 0 and not (self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value) and self.CP.openpilotLongitudinalControl: addr, bus = 0x7d0, 0 if self.CP.flags & HyundaiFlags.CANFD_HDA2.value: addr, bus = 0x730, 5 @@ -122,7 +122,8 @@ class CarController: can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CP, CC.enabled)) if self.CP.openpilotLongitudinalControl: - can_sends.extend(hyundaicanfd.create_adrv_messages(self.packer, self.frame)) + if hda2: + can_sends.extend(hyundaicanfd.create_adrv_messages(self.packer, self.frame)) if self.frame % 2 == 0: can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CP, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override, set_speed_in_units)) @@ -175,7 +176,7 @@ class CarController: if self.frame % 5 == 0 and self.car_fingerprint in (CAR.SONATA, CAR.PALISADE, CAR.IONIQ, CAR.KIA_NIRO_EV, CAR.KIA_NIRO_HEV_2021, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.KIA_CEED, CAR.KIA_SELTOS, CAR.KONA_EV, CAR.KONA_EV_2022, CAR.ELANTRA_2021, CAR.ELANTRA_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_HEV, CAR.SANTA_FE_2022, - CAR.KIA_K5_2021, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.GENESIS_G70_2020, CAR.SANTA_FE_PHEV_2022): + CAR.KIA_K5_2021, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.GENESIS_G70_2020, CAR.SANTA_FE_PHEV_2022, CAR.KIA_STINGER_2022): can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled)) # 5 Hz ACC options diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index 3cce581868..2c309fa0df 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -21,8 +21,9 @@ class CarState(CarStateBase): self.cruise_buttons = deque([Buttons.NONE] * PREV_BUTTON_SAMPLES, maxlen=PREV_BUTTON_SAMPLES) self.main_buttons = deque([Buttons.NONE] * PREV_BUTTON_SAMPLES, maxlen=PREV_BUTTON_SAMPLES) + self.gear_msg_canfd = "GEAR_ALT" if CP.flags & HyundaiFlags.CANFD_ALT_GEARS else "GEAR_SHIFTER" if CP.carFingerprint in CANFD_CAR: - self.shifter_values = can_define.dv["GEAR_SHIFTER"]["GEAR"] + self.shifter_values = can_define.dv[self.gear_msg_canfd]["GEAR"] elif self.CP.carFingerprint in FEATURES["use_cluster_gears"]: self.shifter_values = can_define.dv["CLU15"]["CF_Clu_Gear"] elif self.CP.carFingerprint in FEATURES["use_tcu_gears"]: @@ -154,17 +155,21 @@ class CarState(CarStateBase): def update_canfd(self, cp, cp_cam): ret = car.CarState.new_message() - if self.CP.carFingerprint in EV_CAR: - ret.gas = cp.vl["ACCELERATOR"]["ACCELERATOR_PEDAL"] / 255. - elif self.CP.carFingerprint in HYBRID_CAR: - ret.gas = cp.vl["ACCELERATOR_ALT"]["ACCELERATOR_PEDAL"] / 1023. - ret.gasPressed = ret.gas > 1e-5 + if self.CP.carFingerprint in (EV_CAR | HYBRID_CAR): + if self.CP.carFingerprint in EV_CAR: + ret.gas = cp.vl["ACCELERATOR"]["ACCELERATOR_PEDAL"] / 255. + else: + ret.gas = cp.vl["ACCELERATOR_ALT"]["ACCELERATOR_PEDAL"] / 1023. + ret.gasPressed = ret.gas > 1e-5 + else: + ret.gasPressed = bool(cp.vl["ACCELERATOR_BRAKE_ALT"]["ACCELERATOR_PEDAL_PRESSED"]) + ret.brakePressed = cp.vl["TCS"]["DriverBraking"] == 1 ret.doorOpen = cp.vl["DOORS_SEATBELTS"]["DRIVER_DOOR_OPEN"] == 1 ret.seatbeltUnlatched = cp.vl["DOORS_SEATBELTS"]["DRIVER_SEATBELT_LATCHED"] == 0 - gear = cp.vl["GEAR_SHIFTER"]["GEAR"] + gear = cp.vl[self.gear_msg_canfd]["GEAR"] ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(gear)) # TODO: figure out positions @@ -195,7 +200,7 @@ class CarState(CarStateBase): self.is_metric = cp.vl["CLUSTER_INFO"]["DISTANCE_UNIT"] != 1 if not self.CP.openpilotLongitudinalControl: speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS - cp_cruise_info = cp if self.CP.flags & HyundaiFlags.CANFD_HDA2 else cp_cam + cp_cruise_info = cp_cam if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else cp ret.cruiseState.speed = cp_cruise_info.vl["SCC_CONTROL"]["VSetDis"] * speed_factor ret.cruiseState.standstill = cp_cruise_info.vl["SCC_CONTROL"]["CRUISE_STANDSTILL"] == 1 ret.cruiseState.enabled = cp_cruise_info.vl["SCC_CONTROL"]["ACCMode"] in (1, 2) @@ -411,13 +416,14 @@ class CarState(CarStateBase): def get_can_parser_canfd(CP): cruise_btn_msg = "CRUISE_BUTTONS_ALT" if CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS else "CRUISE_BUTTONS" + gear_msg = "GEAR_ALT" if CP.flags & HyundaiFlags.CANFD_ALT_GEARS else "GEAR_SHIFTER" signals = [ ("WHEEL_SPEED_1", "WHEEL_SPEEDS"), ("WHEEL_SPEED_2", "WHEEL_SPEEDS"), ("WHEEL_SPEED_3", "WHEEL_SPEEDS"), ("WHEEL_SPEED_4", "WHEEL_SPEEDS"), - ("GEAR", "GEAR_SHIFTER"), + ("GEAR", gear_msg), ("STEERING_RATE", "STEERING_SENSORS"), ("STEERING_ANGLE", "STEERING_SENSORS"), @@ -442,8 +448,7 @@ class CarState(CarStateBase): checks = [ ("WHEEL_SPEEDS", 100), - ("GEAR_SHIFTER", 100), - ("BRAKE", 100), + (gear_msg, 100), ("STEERING_SENSORS", 100), ("MDPS", 100), ("TCS", 50), @@ -462,7 +467,7 @@ class CarState(CarStateBase): ("BLINDSPOTS_REAR_CORNERS", 20), ] - if CP.flags & HyundaiFlags.CANFD_HDA2 and not CP.openpilotLongitudinalControl: + if not (CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value) and not CP.openpilotLongitudinalControl: signals += [ ("ACCMode", "SCC_CONTROL"), ("VSetDis", "SCC_CONTROL"), @@ -486,17 +491,26 @@ class CarState(CarStateBase): checks += [ ("ACCELERATOR_ALT", 100), ] + else: + signals += [ + ("ACCELERATOR_PEDAL_PRESSED", "ACCELERATOR_BRAKE_ALT"), + ] + checks += [ + ("ACCELERATOR_BRAKE_ALT", 100), + ] bus = 5 if CP.flags & HyundaiFlags.CANFD_HDA2 else 4 return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, bus) @staticmethod def get_cam_can_parser_canfd(CP): + signals = [] + checks = [] if CP.flags & HyundaiFlags.CANFD_HDA2: - signals = [(f"BYTE{i}", "CAM_0x2a4") for i in range(3, 24)] - checks = [("CAM_0x2a4", 20)] - else: - signals = [ + signals += [(f"BYTE{i}", "CAM_0x2a4") for i in range(3, 24)] + checks += [("CAM_0x2a4", 20)] + elif CP.flags & HyundaiFlags.CANFD_CAMERA_SCC: + signals += [ ("COUNTER", "SCC_CONTROL"), ("NEW_SIGNAL_1", "SCC_CONTROL"), ("MainMode_ACC", "SCC_CONTROL"), @@ -509,7 +523,7 @@ class CarState(CarStateBase): ("VSetDis", "SCC_CONTROL"), ] - checks = [ + checks += [ ("SCC_CONTROL", 50), ] diff --git a/selfdrive/car/hyundai/hyundaican.py b/selfdrive/car/hyundai/hyundaican.py index dcb8430976..c2ffffbf22 100644 --- a/selfdrive/car/hyundai/hyundaican.py +++ b/selfdrive/car/hyundai/hyundaican.py @@ -20,7 +20,8 @@ def create_lkas11(packer, frame, car_fingerprint, apply_steer, steer_req, if car_fingerprint in (CAR.SONATA, CAR.PALISADE, CAR.KIA_NIRO_EV, CAR.KIA_NIRO_HEV_2021, CAR.SANTA_FE, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.KIA_SELTOS, CAR.ELANTRA_2021, CAR.GENESIS_G70_2020, CAR.ELANTRA_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_EV, CAR.KONA_HEV, CAR.KONA_EV_2022, - CAR.SANTA_FE_2022, CAR.KIA_K5_2021, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022): + CAR.SANTA_FE_2022, CAR.KIA_K5_2021, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, + CAR.SANTA_FE_PHEV_2022, CAR.KIA_STINGER_2022): values["CF_Lkas_LdwsActivemode"] = int(left_lane) + (int(right_lane) << 1) values["CF_Lkas_LdwsOpt_USM"] = 2 diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 4b4d51f3f1..8738aabd17 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -2,7 +2,7 @@ from cereal import car from panda import Panda from common.conversions import Conversions as CV -from selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CANFD_CAR, CAMERA_SCC_CAR, EV_CAR, HYBRID_CAR, LEGACY_SAFETY_MODE_CAR, Buttons, CarControllerParams +from selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CANFD_CAR, CAMERA_SCC_CAR, CANFD_RADAR_SCC_CAR, EV_CAR, HYBRID_CAR, LEGACY_SAFETY_MODE_CAR, Buttons, CarControllerParams from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR from selfdrive.car import STD_CARGO_KG, create_button_event, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase @@ -41,6 +41,11 @@ class CarInterface(CarInterfaceBase): # non-HDA2 if 0x1cf not in fingerprint[4]: ret.flags |= HyundaiFlags.CANFD_ALT_BUTTONS.value + # ICE cars do not have 0x130; GEARS message on 0x40 instead + if 0x130 not in fingerprint[4]: + ret.flags |= HyundaiFlags.CANFD_ALT_GEARS.value + if candidate not in CANFD_RADAR_SCC_CAR: + ret.flags |= HyundaiFlags.CANFD_CAMERA_SCC.value ret.steerActuatorDelay = 0.1 # Default delay ret.steerLimitTimer = 0.4 @@ -120,6 +125,10 @@ class CarInterface(CarInterfaceBase): ret.wheelbase = 2.756 ret.steerRatio = 16. tire_stiffness_factor = 0.385 + elif candidate == CAR.SANTA_CRUZ_1ST_GEN: + ret.mass = 1870. + STD_CARGO_KG # weight from Limited trim - the only supported trim + ret.wheelbase = 3.000 + ret.steerRatio = 14.2 # steering ratio according to Hyundai News https://www.hyundainews.com/assets/documents/original/48035-2022SantaCruzProductGuideSpecsv2081521.pdf # Kia elif candidate == CAR.KIA_SORENTO: @@ -138,6 +147,10 @@ class CarInterface(CarInterfaceBase): ret.wheelbase = 2.63 ret.steerRatio = 14.56 tire_stiffness_factor = 1 + elif candidate == CAR.KIA_SPORTAGE_5TH_GEN: + ret.mass = 1700. + STD_CARGO_KG # weight from SX and above trims, average of FWD and AWD versions + ret.wheelbase = 2.756 + ret.steerRatio = 13.6 # steering ratio according to Kia News https://www.kiamedia.com/us/en/models/sportage/2023/specifications elif candidate in (CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.KIA_OPTIMA_H): ret.mass = 3558. * CV.LB_TO_KG ret.wheelbase = 2.80 @@ -145,7 +158,7 @@ class CarInterface(CarInterfaceBase): tire_stiffness_factor = 0.5 if candidate == CAR.KIA_OPTIMA_G4: ret.minSteerSpeed = 32 * CV.MPH_TO_MS - elif candidate == CAR.KIA_STINGER: + elif candidate in (CAR.KIA_STINGER, CAR.KIA_STINGER_2022): ret.mass = 1825. + STD_CARGO_KG ret.wheelbase = 2.78 ret.steerRatio = 14.4 * 1.15 # 15% higher at the center seems reasonable @@ -189,6 +202,10 @@ class CarInterface(CarInterfaceBase): ret.mass = 3673.0 * CV.LB_TO_KG + STD_CARGO_KG ret.wheelbase = 2.83 ret.steerRatio = 12.9 + elif candidate == CAR.GENESIS_GV70_1ST_GEN: + ret.mass = 1950. + STD_CARGO_KG + ret.wheelbase = 2.87 + ret.steerRatio = 14.6 elif candidate == CAR.GENESIS_G80: ret.mass = 2060. + STD_CARGO_KG ret.wheelbase = 3.01 @@ -202,7 +219,7 @@ class CarInterface(CarInterfaceBase): if candidate in CANFD_CAR: ret.longitudinalTuning.kpV = [0.1] ret.longitudinalTuning.kiV = [0.0] - ret.experimentalLongitudinalAvailable = bool(ret.flags & HyundaiFlags.CANFD_HDA2) + ret.experimentalLongitudinalAvailable = candidate in (HYBRID_CAR | EV_CAR) and candidate not in CANFD_RADAR_SCC_CAR else: ret.longitudinalTuning.kpV = [0.5] ret.longitudinalTuning.kiV = [0.0] @@ -233,6 +250,8 @@ class CarInterface(CarInterfaceBase): ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2 if ret.flags & HyundaiFlags.CANFD_ALT_BUTTONS: ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_ALT_BUTTONS + if ret.flags & HyundaiFlags.CANFD_CAMERA_SCC: + ret.safetyConfigs[1].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC else: if candidate in LEGACY_SAFETY_MODE_CAR: # these cars require a special panda safety mode due to missing counters and checksums in the messages @@ -264,7 +283,7 @@ class CarInterface(CarInterfaceBase): @staticmethod def init(CP, logcan, sendcan): - if CP.openpilotLongitudinalControl: + if CP.openpilotLongitudinalControl and not (CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value): addr, bus = 0x7d0, 0 if CP.flags & HyundaiFlags.CANFD_HDA2.value: addr, bus = 0x730, 5 diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index c760c724a9..ecba7b7494 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -47,6 +47,8 @@ class CarControllerParams: class HyundaiFlags(IntFlag): CANFD_HDA2 = 1 CANFD_ALT_BUTTONS = 2 + CANFD_ALT_GEARS = 4 + CANFD_CAMERA_SCC = 8 class CAR: @@ -77,6 +79,7 @@ class CAR: SONATA_HYBRID = "HYUNDAI SONATA HYBRID 2021" IONIQ_5 = "HYUNDAI IONIQ 5 2022" TUCSON_HYBRID_4TH_GEN = "HYUNDAI TUCSON HYBRID 4TH GEN" + SANTA_CRUZ_1ST_GEN = "HYUNDAI SANTA CRUZ 1ST GEN" # Kia KIA_FORTE = "KIA FORTE E 2018 & GT 2021" @@ -88,15 +91,18 @@ class CAR: KIA_OPTIMA_G4_FL = "KIA OPTIMA 4TH GEN FACELIFT" KIA_OPTIMA_H = "KIA OPTIMA HYBRID 2017 & SPORTS 2019" KIA_SELTOS = "KIA SELTOS 2021" + KIA_SPORTAGE_5TH_GEN = "KIA SPORTAGE 5TH GEN" KIA_SORENTO = "KIA SORENTO GT LINE 2018" KIA_SPORTAGE_HYBRID_5TH_GEN = "KIA SPORTAGE HYBRID 5TH GEN" KIA_STINGER = "KIA STINGER GT2 2018" + KIA_STINGER_2022 = "KIA STINGER 2022" KIA_CEED = "KIA CEED INTRO ED 2019" KIA_EV6 = "KIA EV6 2022" # Genesis GENESIS_G70 = "GENESIS G70 2018" GENESIS_G70_2020 = "GENESIS G70 2020" + GENESIS_GV70_1ST_GEN = "GENESIS GV70 1ST GEN" GENESIS_G80 = "GENESIS G80 2017" GENESIS_G90 = "GENESIS G90 2017" @@ -137,15 +143,16 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { ], CAR.PALISADE: [ HyundaiCarInfo("Hyundai Palisade 2020-22", "All", "https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), - HyundaiCarInfo("Kia Telluride 2020", "All", harness=Harness.hyundai_h), + HyundaiCarInfo("Kia Telluride 2020-22", "All", harness=Harness.hyundai_h), ], CAR.VELOSTER: HyundaiCarInfo("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, harness=Harness.hyundai_e), CAR.SONATA_HYBRID: HyundaiCarInfo("Hyundai Sonata Hybrid 2020-22", "All", harness=Harness.hyundai_a), CAR.IONIQ_5: [ - HyundaiCarInfo("Hyundai Ioniq 5 (without HDA II) 2022" , "Highway Driving Assist", harness=Harness.hyundai_k), - HyundaiCarInfo("Hyundai Ioniq 5 (with HDA II) 2022", "Highway Driving Assist II", harness=Harness.hyundai_q), + HyundaiCarInfo("Hyundai Ioniq 5 (without HDA II) 2022-23" , "Highway Driving Assist", harness=Harness.hyundai_k), + HyundaiCarInfo("Hyundai Ioniq 5 (with HDA II) 2022-23", "Highway Driving Assist II", harness=Harness.hyundai_q), ], CAR.TUCSON_HYBRID_4TH_GEN: HyundaiCarInfo("Hyundai Tucson Hybrid 2022", "All", harness=Harness.hyundai_n), + CAR.SANTA_CRUZ_1ST_GEN: HyundaiCarInfo("Hyundai Santa Cruz 2021-22", "Smart Cruise Control (SCC)", harness=Harness.hyundai_n), # Kia CAR.KIA_FORTE: HyundaiCarInfo("Kia Forte 2019-21", harness=Harness.hyundai_g), @@ -168,12 +175,14 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { HyundaiCarInfo("Kia Optima Hybrid 2019"), ], CAR.KIA_SELTOS: HyundaiCarInfo("Kia Seltos 2021", harness=Harness.hyundai_a), + CAR.KIA_SPORTAGE_5TH_GEN: HyundaiCarInfo("Kia Sportage 2023", "Smart Cruise Control (SCC)", harness=Harness.hyundai_n), CAR.KIA_SORENTO: [ HyundaiCarInfo("Kia Sorento 2018", "Advanced Smart Cruise Control", "https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_c), HyundaiCarInfo("Kia Sorento 2019", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_e), ], CAR.KIA_SPORTAGE_HYBRID_5TH_GEN: HyundaiCarInfo("Kia Sportage Hybrid 2023", harness=Harness.hyundai_n), CAR.KIA_STINGER: HyundaiCarInfo("Kia Stinger 2018-20", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0", harness=Harness.hyundai_c), + CAR.KIA_STINGER_2022: HyundaiCarInfo("Kia Stinger 2022", "All", harness=Harness.hyundai_k), CAR.KIA_CEED: HyundaiCarInfo("Kia Ceed 2019", harness=Harness.hyundai_e), CAR.KIA_EV6: [ HyundaiCarInfo("Kia EV6 (without HDA II) 2022", "Highway Driving Assist", harness=Harness.hyundai_l), @@ -183,6 +192,7 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { # Genesis CAR.GENESIS_G70: HyundaiCarInfo("Genesis G70 2018-19", "All", harness=Harness.hyundai_f), CAR.GENESIS_G70_2020: HyundaiCarInfo("Genesis G70 2020", "All", harness=Harness.hyundai_f), + CAR.GENESIS_GV70_1ST_GEN: HyundaiCarInfo("Genesis GV70 2022-23", "All", harness=Harness.hyundai_l), CAR.GENESIS_G80: HyundaiCarInfo("Genesis G80 2017-19", "All", harness=Harness.hyundai_h), CAR.GENESIS_G90: HyundaiCarInfo("Genesis G90 2017-18", "All", harness=Harness.hyundai_c), } @@ -460,6 +470,7 @@ FW_VERSIONS = { b'\xf1\x82DNBWN5TMDCXXXG2E', b'\xf1\x82DNCVN5GMCCXXXF0A', b'\xf1\x82DNCVN5GMCCXXXG2B', + b'\xf1\x870\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf1\x81HM6M1_0a0_J10', b'\xf1\x870\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf1\x82DNDWN5TMDCXXXJ1A', b'\xf1\x87391162M003', b'\xf1\x87391162M013', @@ -484,6 +495,7 @@ FW_VERSIONS = { b'\xf1\x8756310L0010\x00\xf1\x00DN8 MDPS C 1.00 1.01 56310L0010\x00 4DNAC101', b'\xf1\x8756310L0210\x00\xf1\x00DN8 MDPS C 1.00 1.01 56310L0210\x00 4DNAC101', b'\xf1\x8757700-L0000\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP100', + b'\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP101', ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00DN8 MFC AT KOR LHD 1.00 1.02 99211-L1000 190422', @@ -504,6 +516,7 @@ FW_VERSIONS = { b'\xf1\x00HT6WA250BLHT6WA910A1SDN8G25NB1\x00\x00\x00\x00\x00\x00\x96\xa1\xf1\x92', b'\xf1\x00HT6WA280BLHT6WAD10A1SDN8G25NB2\x00\x00\x00\x00\x00\x00\x08\xc9O:', b'\xf1\x00T02601BL T02730A1 VDN8T25XXX730NS5\xf7_\x92\xf5', + b'\xf1\x00T02601BL T02832A1 VDN8T25XXX832NS8G\x0e\xfeE', b'\xf1\x87954A02N060\x00\x00\x00\x00\x00\xf1\x81T02730A1 \xf1\x00T02601BL T02730A1 VDN8T25XXX730NS5\xf7_\x92\xf5', b'\xf1\x87SAKFBA2926554GJ2VefVww\x87xwwwww\x88\x87xww\x87wTo\xfb\xffvUo\xff\x8d\x16\xf1\x81U903\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U903\x00\x00\x00\x00\x00\x00SDN8T16NB0z{\xd4v', b'\xf1\x87SAKFBA3030524GJ2UVugww\x97yx\x88\x87\x88vw\x87gww\x87wto\xf9\xfffUo\xff\xa2\x0c\xf1\x81U903\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U903\x00\x00\x00\x00\x00\x00SDN8T16NB0z{\xd4v', @@ -779,6 +792,23 @@ FW_VERSIONS = { b'\xf1\x00bcsh8p54 E21\x00\x00\x00\x00\x00\x00\x00SCK0T33NB0\x88\xa2\xe6\xf0', ], }, + CAR.KIA_STINGER_2022: { + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00CK__ SCC F-CUP 1.00 1.00 99110-J5500 ', + ], + (Ecu.engine, 0x7e0, None): [ + b'\xf1\x81640R0051\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00CK MDPS R 1.00 5.03 57700-J5380 4C2VR503', + ], + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00CK MFC AT AUS RHD 1.00 1.00 99211-J5500 210622', + ], + (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x87VCNLF11383972DK1vffV\x99\x99\x89\x98\x86eUU\x88wg\x89vfff\x97fff\x99\x87o\xff"\xc1\xf1\x81E30\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E30\x00\x00\x00\x00\x00\x00\x00SCK0T33GH0\xbe`\xfb\xc6', + ], + }, CAR.PALISADE: { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00LX2_ SCC F-CUP 1.00 1.04 99110-S8100 ', @@ -1052,6 +1082,7 @@ FW_VERSIONS = { b'\xf1\x8758520-K4010\xf1\x00OS IEB \x02 101 \x11\x13 58520-K4010', b'\xf1\x8758520-K4010\xf1\x00OS IEB \x04 101 \x11\x13 58520-K4010', b'\xf1\x8758520-K4010\xf1\x00OS IEB \x03 101 \x11\x13 58520-K4010', + b'\xf1\x00OS IEB \r 102"\x05\x16 58520-K4010', # TODO: these return from the MULTI request, above return from LONG b'\x01\x04\x7f\xff\xff\xf8\xff\xff\x00\x00\x01\xd3\x00\x00\x00\x00\xff\xb7\xff\xee\xff\xe0\x00\xc0\xc0\xfc\xd5\xfc\x00\x00U\x10\xffP\xf5\xff\xfd\x00\x00\x00\x00\xfc\x00\x01', b'\x01\x04\x7f\xff\xff\xf8\xff\xff\x00\x00\x01\xdb\x00\x00\x00\x00\xff\xb1\xff\xd9\xff\xd2\x00\xc0\xc0\xfc\xd5\xfc\x00\x00U\x10\xff\xd6\xf5\x00\x06\x00\x00\x00\x14\xfd\x00\x04', @@ -1061,10 +1092,12 @@ FW_VERSIONS = { b'\xf1\x00OSP LKA AT CND LHD 1.00 1.02 99211-J9110 802', b'\xf1\x00OSP LKA AT EUR RHD 1.00 1.02 99211-J9110 802', b'\xf1\x00OSP LKA AT AUS RHD 1.00 1.04 99211-J9200 904', + b'\xf1\x00OSP LKA AT EUR LHD 1.00 1.04 99211-J9200 904', ], (Ecu.eps, 0x7D4, None): [ b'\xf1\x00OSP MDPS C 1.00 1.02 56310K4260\x00 4OEPC102', b'\xf1\x00OSP MDPS C 1.00 1.02 56310/K4970 4OEPC102', + b'\xf1\x00OSP MDPS C 1.00 1.02 56310/K4271 4OEPC102', ], (Ecu.fwdRadar, 0x7D0, None): [ b'\xf1\x00YB__ FCA ----- 1.00 1.01 99110-K4500 \x00\x00\x00', @@ -1366,6 +1399,7 @@ FW_VERSIONS = { b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.05 99210-CV000 211027', b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.06 99210-CV000 220328', b'\xf1\x00CV1 MFC AT EUR LHD 1.00 1.05 99210-CV000 211027', + b'\xf1\x00CV1 MFC AT EUR LHD 1.00 1.06 99210-CV000 220328', ], }, CAR.IONIQ_5: { @@ -1376,6 +1410,7 @@ FW_VERSIONS = { (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.02 99211-GI010 211206', b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.06 99211-GI000 210813', + b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.05 99211-GI010 220614', ], }, CAR.TUCSON_HYBRID_4TH_GEN: { @@ -1395,6 +1430,32 @@ FW_VERSIONS = { b'\xf1\x00NQ5__ 1.01 1.03 99110-CH000 ', ], }, + CAR.SANTA_CRUZ_1ST_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-CW000 14M', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NX4__ 1.00 1.00 99110-K5000 ', + ], + }, + CAR.KIA_SPORTAGE_5TH_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1030 662', + b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1040 663', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00NQ5__ 1.00 1.02 99110-P1000 ', + b'\xf1\x00NQ5__ 1.00 1.03 99110-P1000 ', + ], + }, + CAR.GENESIS_GV70_1ST_GEN: { + (Ecu.fwdCamera, 0x7c4, None): [ + b'\xf1\x00JK1 MFC AT USA LHD 1.00 1.04 99211-AR000 210204', + ], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00JK1_ SCC FHCUP 1.00 1.02 99110-AR000 ', + ], + }, } CHECKSUM = { @@ -1409,10 +1470,13 @@ FEATURES = { "use_elect_gears": {CAR.KIA_NIRO_EV, CAR.KIA_NIRO_PHEV, CAR.KIA_NIRO_HEV_2021, CAR.KIA_OPTIMA_H, CAR.IONIQ_EV_LTD, CAR.KONA_EV, CAR.IONIQ, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.ELANTRA_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_HEV, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.IONIQ_PHEV_2019, CAR.KONA_EV_2022}, # these cars use the FCA11 message for the AEB and FCW signals, all others use SCC12 - "use_fca": {CAR.SONATA, CAR.SONATA_HYBRID, CAR.ELANTRA, CAR.ELANTRA_2021, CAR.ELANTRA_HEV_2021, CAR.KIA_STINGER, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.KONA_EV, CAR.KIA_FORTE, CAR.KIA_NIRO_EV, CAR.PALISADE, CAR.GENESIS_G70, CAR.GENESIS_G70_2020, CAR.KONA, CAR.SANTA_FE, CAR.KIA_SELTOS, CAR.KONA_HEV, CAR.SANTA_FE_2022, CAR.KIA_K5_2021, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.TUCSON, CAR.KONA_EV_2022}, + "use_fca": {CAR.SONATA, CAR.SONATA_HYBRID, CAR.ELANTRA, CAR.ELANTRA_2021, CAR.ELANTRA_HEV_2021, CAR.KIA_STINGER, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.KONA_EV, CAR.KIA_FORTE, CAR.KIA_NIRO_EV, CAR.PALISADE, CAR.GENESIS_G70, CAR.GENESIS_G70_2020, CAR.KONA, CAR.SANTA_FE, CAR.KIA_SELTOS, CAR.KONA_HEV, CAR.SANTA_FE_2022, CAR.KIA_K5_2021, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.TUCSON, CAR.KONA_EV_2022, CAR.KIA_STINGER_2022}, } -CANFD_CAR = {CAR.KIA_EV6, CAR.IONIQ_5, CAR.TUCSON_HYBRID_4TH_GEN, CAR.KIA_SPORTAGE_HYBRID_5TH_GEN} +CANFD_CAR = {CAR.KIA_EV6, CAR.IONIQ_5, CAR.TUCSON_HYBRID_4TH_GEN, CAR.KIA_SPORTAGE_HYBRID_5TH_GEN, CAR.SANTA_CRUZ_1ST_GEN, CAR.KIA_SPORTAGE_5TH_GEN, CAR.GENESIS_GV70_1ST_GEN} + +# The radar does SCC on these cars when HDA I, rather than the camera +CANFD_RADAR_SCC_CAR = {CAR.GENESIS_GV70_1ST_GEN, } # The camera does SCC on these cars, rather than the radar CAMERA_SCC_CAR = {CAR.KONA_EV_2022, } @@ -1451,6 +1515,7 @@ DBC = { CAR.KIA_SELTOS: dbc_dict('hyundai_kia_generic', None), CAR.KIA_SORENTO: dbc_dict('hyundai_kia_generic', None), # Has 0x5XX messages, but different format CAR.KIA_STINGER: dbc_dict('hyundai_kia_generic', None), + CAR.KIA_STINGER_2022: dbc_dict('hyundai_kia_generic', None), CAR.KONA: dbc_dict('hyundai_kia_generic', None), CAR.KONA_EV: dbc_dict('hyundai_kia_generic', None), CAR.KONA_EV_2022: dbc_dict('hyundai_kia_generic', None), @@ -1469,5 +1534,8 @@ DBC = { CAR.SONATA_HYBRID: dbc_dict('hyundai_kia_generic', 'hyundai_kia_mando_front_radar_generated'), CAR.TUCSON_HYBRID_4TH_GEN: dbc_dict('hyundai_canfd', None), CAR.IONIQ_5: dbc_dict('hyundai_canfd', None), + CAR.SANTA_CRUZ_1ST_GEN: dbc_dict('hyundai_canfd', None), + CAR.KIA_SPORTAGE_5TH_GEN: dbc_dict('hyundai_canfd', None), CAR.KIA_SPORTAGE_HYBRID_5TH_GEN: dbc_dict('hyundai_canfd', None), + CAR.GENESIS_GV70_1ST_GEN: dbc_dict('hyundai_canfd', None), } diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index 4647a04244..8e8872a539 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -23,7 +23,7 @@ TorqueFromLateralAccelCallbackType = Callable[[float, car.CarParams.LateralTorqu MAX_CTRL_SPEED = (V_CRUISE_MAX + 4) * CV.KPH_TO_MS ACCEL_MAX = 2.0 ACCEL_MIN = -3.5 -FRICTION_THRESHOLD = 0.2 +FRICTION_THRESHOLD = 0.3 TORQUE_PARAMS_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/params.yaml') TORQUE_OVERRIDE_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/override.yaml') @@ -250,8 +250,8 @@ class CarInterfaceBase(ABC): # Enable OP long on falling edge of enable buttons (defaults to accelCruise and decelCruise, overridable per-port) if not self.CP.pcmCruise and (b.type in enable_buttons and not b.pressed): events.add(EventName.buttonEnable) - # Disable on rising edge of cancel for both stock and OP long - if b.type == ButtonType.cancel and b.pressed: + # Disable on rising and falling edge of cancel for both stock and OP long + if b.type == ButtonType.cancel: events.add(EventName.buttonCancel) # Handle permanent and temporary steering faults diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index da5ff1785a..9975e495dd 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -196,6 +196,7 @@ FW_VERSIONS = { b'\xaa!dt\a', b'\xc5!ar\a', b'\xbe!as\a', + b'\xc5!as\x07', b'\xc5!ds\a', b'\xc5!`s\a', b'\xaa!au\a', @@ -460,6 +461,7 @@ FW_VERSIONS = { b'\xa1 \x08\x02', b'\xa1 \x06\x02', b'\xa1 \x08\x00', + b'\xa1 "\t\x00', ], (Ecu.eps, 0x746, None): [ b'\x9b\xc0\x10\x00', @@ -481,6 +483,7 @@ FW_VERSIONS = { b'\xe2"`p\x07', b'\xf1\x82\xe2,\xa0@\x07', b'\xbc"`q\x07', + b'\xe3,\xa0@\x07', ], (Ecu.transmission, 0x7e1, None): [ b'\xa5\xfe\xf7@\x00', diff --git a/selfdrive/car/tests/routes.py b/selfdrive/car/tests/routes.py index d833203427..cd61528439 100644 --- a/selfdrive/car/tests/routes.py +++ b/selfdrive/car/tests/routes.py @@ -81,7 +81,9 @@ routes = [ CarTestRoute("6fe86b4e410e4c37|2020-07-22--16-27-13", HYUNDAI.HYUNDAI_GENESIS), CarTestRoute("70c5bec28ec8e345|2020-08-08--12-22-23", HYUNDAI.GENESIS_G70), + CarTestRoute("ca4de5b12321bd98|2022-10-18--21-15-59", HYUNDAI.GENESIS_GV70_1ST_GEN), CarTestRoute("6b301bf83f10aa90|2020-11-22--16-45-07", HYUNDAI.GENESIS_G80), + CarTestRoute("f0709d2bc6ca451f|2022-10-15--08-13-54", HYUNDAI.SANTA_CRUZ_1ST_GEN), CarTestRoute("4dbd55df87507948|2022-03-01--09-45-38", HYUNDAI.SANTA_FE), CarTestRoute("bf43d9df2b660eb0|2021-09-23--14-16-37", HYUNDAI.SANTA_FE_2022), CarTestRoute("37398f32561a23ad|2021-11-18--00-11-35", HYUNDAI.SANTA_FE_HEV_2022), @@ -109,6 +111,7 @@ routes = [ CarTestRoute("ff973b941a69366f|2022-07-28--22-01-19", HYUNDAI.KONA_EV_2022, segment=11), CarTestRoute("49f3c13141b6bc87|2021-07-28--08-05-13", HYUNDAI.KONA_HEV), CarTestRoute("5dddcbca6eb66c62|2020-07-26--13-24-19", HYUNDAI.KIA_STINGER), + CarTestRoute("5b50b883a4259afb|2022-11-09--15-00-42", HYUNDAI.KIA_STINGER_2022), CarTestRoute("d624b3d19adce635|2020-08-01--14-59-12", HYUNDAI.VELOSTER), CarTestRoute("d545129f3ca90f28|2022-10-19--09-22-54", HYUNDAI.KIA_EV6), # HDA2 CarTestRoute("68d6a96e703c00c9|2022-09-10--16-09-39", HYUNDAI.KIA_EV6), # HDA1 @@ -117,6 +120,7 @@ routes = [ CarTestRoute("173219cf50acdd7b|2021-07-05--10-27-41", HYUNDAI.KIA_NIRO_PHEV), CarTestRoute("34a875f29f69841a|2021-07-29--13-02-09", HYUNDAI.KIA_NIRO_HEV_2021), CarTestRoute("50a2212c41f65c7b|2021-05-24--16-22-06", HYUNDAI.KIA_FORTE), + CarTestRoute("192283cdbb7a58c2|2022-10-15--01-43-18", HYUNDAI.KIA_SPORTAGE_5TH_GEN), CarTestRoute("c5ac319aa9583f83|2021-06-01--18-18-31", HYUNDAI.ELANTRA), CarTestRoute("734ef96182ddf940|2022-10-02--16-41-44", HYUNDAI.ELANTRA), # 2019 Elantra GT CarTestRoute("82e9cdd3f43bf83e|2021-05-15--02-42-51", HYUNDAI.ELANTRA_2021), diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index aabf652c8c..8d22173671 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -34,15 +34,18 @@ class TestCarInterfaces(unittest.TestCase): self.assertGreater(car_params.maxLateralAccel, 0) if car_params.steerControlType != car.CarParams.SteerControlType.angle: - tuning = car_params.lateralTuning.which() - if tuning == 'pid': - self.assertTrue(len(car_params.lateralTuning.pid.kpV)) - elif tuning == 'torque': - kf = car_params.lateralTuning.torque.kf - self.assertTrue(not math.isnan(kf) and kf > 0) - self.assertTrue(not math.isnan(car_params.lateralTuning.torque.friction)) - elif tuning == 'indi': - self.assertTrue(len(car_params.lateralTuning.indi.outerLoopGainV)) + tune = car_params.lateralTuning + if tune.which() == 'pid': + self.assertTrue(not math.isnan(tune.pid.kf) and tune.pid.kf > 0) + self.assertTrue(len(tune.pid.kpV) > 0 and len(tune.pid.kpV) == len(tune.pid.kpBP)) + self.assertTrue(len(tune.pid.kiV) > 0 and len(tune.pid.kiV) == len(tune.pid.kiBP)) + + elif tune.which() == 'torque': + self.assertTrue(not math.isnan(tune.torque.kf) and tune.torque.kf > 0) + self.assertTrue(not math.isnan(tune.torque.friction)) + + elif tune.which() == 'indi': + self.assertTrue(len(tune.indi.outerLoopGainV)) # Run car interface CC = car.CarControl.new_message() diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index af74714901..3514162ba8 100755 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -263,8 +263,8 @@ class TestCarModelBase(unittest.TestCase): if CS.brakePressed and not self.safety.get_brake_pressed_prev(): if self.CP.carFingerprint in (HONDA.PILOT, HONDA.PASSPORT, HONDA.RIDGELINE) and CS.brake > 0.05: brake_pressed = False - safety_brake_pressed = self.safety.get_brake_pressed_prev() or self.safety.get_regen_braking_prev() - checks['brakePressed'] += brake_pressed != safety_brake_pressed + checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev() + checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev() if self.CP.pcmCruise: # On most pcmCruise cars, openpilot's state is always tied to the PCM's cruise state. diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml index 460b9a9097..6ec782444c 100644 --- a/selfdrive/car/torque_data/override.yaml +++ b/selfdrive/car/torque_data/override.yaml @@ -30,8 +30,10 @@ CHEVROLET SILVERADO 1500 2020: [1.9, 1.9, 0.112] CHEVROLET EQUINOX 2019: [2.0, 2.0, 0.05] VOLKSWAGEN PASSAT NMS: [2.5, 2.5, 0.1] VOLKSWAGEN SHARAN 2ND GEN: [2.5, 2.5, 0.1] -HYUNDAI TUCSON HYBRID 4TH GEN: [2.5, 2.5, 0.0] -KIA SPORTAGE HYBRID 5TH GEN: [2.5, 2.5, 0.0] +HYUNDAI SANTA CRUZ 1ST GEN: [2.7, 2.7, 0.1] +KIA SPORTAGE 5TH GEN: [2.7, 2.7, 0.1] +KIA SPORTAGE HYBRID 5TH GEN: [2.5, 2.5, 0.1] +GENESIS GV70 1ST GEN: [2.42, 2.42, 0.1] # Dashcam or fallback configured as ideal car mock: [10.0, 10, 0.0] diff --git a/selfdrive/car/torque_data/params.yaml b/selfdrive/car/torque_data/params.yaml index 5c828fa669..a9023b4edc 100644 --- a/selfdrive/car/torque_data/params.yaml +++ b/selfdrive/car/torque_data/params.yaml @@ -25,6 +25,7 @@ HONDA ODYSSEY 2018: [1.8774809275211801, 0.8394431662987996, 0.2096978613792822] HONDA PASSPORT 2021: [1.5305538930036766, 0.7956063674638759, 0.19599407381531284] HONDA PILOT 2017: [1.7262026201812795, 0.9470005614967523, 0.21351430733218763] HONDA RIDGELINE 2017: [1.4146525028237624, 0.7356572861629564, 0.23307177552211328] +HYUNDAI ELANTRA 2021: [3.169, 2.1259108157250735, 0.0819] HYUNDAI GENESIS 2015-2016: [1.8466226943929824, 1.5552063647830634, 0.0984484465421171] HYUNDAI IONIQ ELECTRIC LIMITED 2019: [1.7662975472852054, 1.613755614526594, 0.17087579756306276] HYUNDAI IONIQ PHEV 2020: [3.2928700076638537, 2.1193482926455656, 0.12463700961468778] @@ -37,6 +38,7 @@ HYUNDAI SANTA FE PlUG-IN HYBRID 2022: [1.6953050513611045, 1.5837614296206861, 0 HYUNDAI SONATA 2019: [2.2200457811703953, 1.2967330275895228, 0.14039920986586393] HYUNDAI SONATA 2020: [2.9638737459977467, 2.1259108157250735, 0.07813665616927593] HYUNDAI SONATA HYBRID 2021: [2.8990264092395734, 2.061410192222139, 0.0899805488717382] +HYUNDAI TUCSON HYBRID 4TH GEN: [2.035545, 2.035545, 0.110272] JEEP GRAND CHEROKEE 2019: [1.7321233388827006, 1.289689569171081, 0.15046331002097185] JEEP GRAND CHEROKEE V6 2018: [1.8776598027756923, 1.4057367824262523, 0.11725947414922003] KIA EV6 2022: [3.2, 3.0, 0.05] diff --git a/selfdrive/car/torque_data/substitute.yaml b/selfdrive/car/torque_data/substitute.yaml index acd7526003..77236e393e 100644 --- a/selfdrive/car/torque_data/substitute.yaml +++ b/selfdrive/car/torque_data/substitute.yaml @@ -35,9 +35,9 @@ HYUNDAI IONIQ HYBRID 2020-2022: HYUNDAI IONIQ PLUG-IN HYBRID 2019 HYUNDAI IONIQ ELECTRIC 2020: HYUNDAI IONIQ PLUG-IN HYBRID 2019 HYUNDAI ELANTRA 2017: HYUNDAI SONATA 2019 HYUNDAI ELANTRA HYBRID 2021: HYUNDAI SONATA 2020 -HYUNDAI ELANTRA 2021: HYUNDAI SONATA 2020 HYUNDAI TUCSON 2019: HYUNDAI SANTA FE 2019 HYUNDAI SANTA FE 2022: HYUNDAI SANTA FE HYBRID 2022 +KIA STINGER 2022: KIA STINGER GT2 2018 GENESIS G90 2017: GENESIS G70 2018 GENESIS G80 2017: GENESIS G70 2018 GENESIS G70 2020: HYUNDAI SONATA 2020 diff --git a/selfdrive/car/toyota/carstate.py b/selfdrive/car/toyota/carstate.py index 4758149916..8efb2c79e3 100644 --- a/selfdrive/car/toyota/carstate.py +++ b/selfdrive/car/toyota/carstate.py @@ -15,6 +15,8 @@ class CarState(CarStateBase): can_define = CANDefine(DBC[CP.carFingerprint]["pt"]) self.shifter_values = can_define.dv["GEAR_PACKET"]["GEAR"] self.eps_torque_scale = EPS_SCALE[CP.carFingerprint] / 100. + self.cluster_speed_hyst_gap = CV.KPH_TO_MS / 2. + self.cluster_min_speed = CV.KPH_TO_MS / 2. # On cars with cp.vl["STEER_TORQUE_SENSOR"]["STEER_ANGLE"] # the signal is zeroed to where the steering angle is at start. @@ -52,6 +54,7 @@ class CarState(CarStateBase): ) ret.vEgoRaw = mean([ret.wheelSpeeds.fl, ret.wheelSpeeds.fr, ret.wheelSpeeds.rl, ret.wheelSpeeds.rr]) ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) + ret.vEgoCluster = ret.vEgo * 1.015 # minimum of all the cars ret.standstill = ret.vEgoRaw == 0 diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 02e4caa9d6..3f4edb36d2 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -110,6 +110,21 @@ class CarInterface(CarInterfaceBase): ret.steerRatio = 14.3 tire_stiffness_factor = 0.7933 ret.mass = 3585. * CV.LB_TO_KG + STD_CARGO_KG # Average between ICE and Hybrid + ret.lateralTuning.init('pid') + ret.lateralTuning.pid.kiBP = [0.0] + ret.lateralTuning.pid.kpBP = [0.0] + ret.lateralTuning.pid.kpV = [0.6] + ret.lateralTuning.pid.kiV = [0.1] + ret.lateralTuning.pid.kf = 0.00007818594 + + # 2019+ RAV4 TSS2 uses two different steering racks and specific tuning seems to be necessary. + # See https://github.com/commaai/openpilot/pull/21429#issuecomment-873652891 + for fw in car_fw: + if fw.ecu == "eps" and (fw.fwVersion.startswith(b'\x02') or fw.fwVersion in [b'8965B42181\x00\x00\x00\x00\x00\x00']): + ret.lateralTuning.pid.kpV = [0.15] + ret.lateralTuning.pid.kiV = [0.05] + ret.lateralTuning.pid.kf = 0.00004 + break elif candidate in (CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2): stop_and_go = True diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 9e96118085..cf37a3c2f2 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -119,7 +119,7 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { CAR.COROLLA: ToyotaCarInfo("Toyota Corolla 2017-19"), CAR.COROLLA_TSS2: [ ToyotaCarInfo("Toyota Corolla 2020-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"), - ToyotaCarInfo("Toyota Corolla Cross (Non-US only) 2020-21", min_enable_speed=7.5), + ToyotaCarInfo("Toyota Corolla Cross (Non-US only) 2020-23", min_enable_speed=7.5), ToyotaCarInfo("Toyota Corolla Hatchback 2019-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"), ], CAR.COROLLAH_TSS2: [ @@ -713,6 +713,7 @@ FW_VERSIONS = { }, CAR.COROLLA_TSS2: { (Ecu.engine, 0x700, None): [ + b'\x01896630A22000\x00\x00\x00\x00', b'\x01896630ZG2000\x00\x00\x00\x00', b'\x01896630ZG5000\x00\x00\x00\x00', b'\x01896630ZG5100\x00\x00\x00\x00', @@ -792,6 +793,7 @@ FW_VERSIONS = { b'F152602191\x00\x00\x00\x00\x00\x00', b'\x01F152612862\x00\x00\x00\x00\x00\x00', b'\x01F152612B91\x00\x00\x00\x00\x00\x00', + b'\x01F15260A070\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ b'\x018821F3301100\x00\x00\x00\x00', @@ -809,6 +811,7 @@ FW_VERSIONS = { b'\x028646F1202100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F1202200\x00\x00\x00\x008646G2601500\x00\x00\x00\x00', b'\x028646F1601100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', + b'\x028646F1601300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', ], }, CAR.COROLLAH_TSS2: { @@ -818,6 +821,7 @@ FW_VERSIONS = { b'\x01896637621000\x00\x00\x00\x00', b'\x01896637624000\x00\x00\x00\x00', b'\x01896637626000\x00\x00\x00\x00', + b'\x01896637639000\x00\x00\x00\x00', b'\x01896637648000\x00\x00\x00\x00', b'\x01896637643000\x00\x00\x00\x00', b'\x02896630A07000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', @@ -1011,6 +1015,7 @@ FW_VERSIONS = { b'\x01F15264873500\x00\x00\x00\x00', b'\x01F152648C6300\x00\x00\x00\x00', b'\x01F152648J4000\x00\x00\x00\x00', + b'\x01F152648J5000\x00\x00\x00\x00', b'\x01F152648J6000\x00\x00\x00\x00', ], (Ecu.engine, 0x700, None): [ @@ -1018,6 +1023,7 @@ FW_VERSIONS = { b'\x01896630EA1000\x00\x00\x00\x00', b'\x01896630EE4000\x00\x00\x00\x00', b'\x01896630EE4100\x00\x00\x00\x00', + b'\x01896630EE5000\x00\x00\x00\x00', b'\x01896630EE6000\x00\x00\x00\x00', b'\x02896630E66000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', b'\x02896630E66100\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', @@ -1389,6 +1395,7 @@ FW_VERSIONS = { b'\x02896634A14001\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00', b'\x02896634A23000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02896634A23001\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00', + b'\x02896634A23101\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00', b'\x02896634A14001\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', b'\x02896634A14101\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', ], diff --git a/selfdrive/car/volkswagen/carstate.py b/selfdrive/car/volkswagen/carstate.py index d09420cf7a..def14ab019 100644 --- a/selfdrive/car/volkswagen/carstate.py +++ b/selfdrive/car/volkswagen/carstate.py @@ -12,6 +12,7 @@ class CarState(CarStateBase): super().__init__(CP) self.CCP = CarControllerParams(CP) self.button_states = {button.event_type: False for button in self.CCP.BUTTONS} + self.esp_hold_confirmation = False def create_button_events(self, pt_cp, buttons): button_events = [] diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index ca6d1fa7f8..816e7fcf34 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -27,12 +27,14 @@ class CarInterface(CarInterfaceBase): ret.carName = "volkswagen" ret.radarOffCan = True + use_off_car_defaults = len(fingerprint[0]) == 0 # Pick sensible carParams during offline doc generation/CI jobs + if candidate in PQ_CARS: # Set global PQ35/PQ46/NMS parameters ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.volkswagenPq)] ret.enableBsm = 0x3BA in fingerprint[0] # SWA_1 - if 0x440 in fingerprint[0] or len(fingerprint[0]) == 0: # Getriebe_1, or empty FP for CI/docs generation + if 0x440 in fingerprint[0] or use_off_car_defaults: # Getriebe_1 ret.transmissionType = TransmissionType.automatic else: ret.transmissionType = TransmissionType.manual @@ -55,7 +57,7 @@ class CarInterface(CarInterfaceBase): ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.volkswagen)] ret.enableBsm = 0x30F in fingerprint[0] # SWA_01 - if 0xAD in fingerprint[0] or len(fingerprint[0]) == 0: # Getriebe_11, or empty FP for CI/docs generation + if 0xAD in fingerprint[0] or use_off_car_defaults: # Getriebe_11 ret.transmissionType = TransmissionType.automatic elif 0x187 in fingerprint[0]: # EV_Gearshift ret.transmissionType = TransmissionType.direct @@ -81,8 +83,8 @@ class CarInterface(CarInterfaceBase): # Global longitudinal tuning defaults, can be overridden per-vehicle - ret.experimentalLongitudinalAvailable = ret.networkLocation == NetworkLocation.gateway and False # Disabled for now - if experimental_long and False: # Disabled for now + ret.experimentalLongitudinalAvailable = ret.networkLocation == NetworkLocation.gateway or use_off_car_defaults + if experimental_long: # Proof-of-concept, prep for E2E only. No radar points available. Panda ALLOW_DEBUG firmware required. ret.openpilotLongitudinalControl = True ret.safetyConfigs[0].safetyParam |= Panda.FLAG_VOLKSWAGEN_LONG_CONTROL diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 695868452d..f24448adbc 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -151,75 +151,76 @@ class Footnote(Enum): PASSAT = CarFootnote( "Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.", Column.MODEL) - VW_HARNESS = CarFootnote( - "Model-years 2021 and beyond may have a new camera harness design, which isn't yet available from the comma " + - "store. Before ordering, remove the Lane Assist camera cover and check to see if the connector is black " + - "(older design) or light brown (newer design). In the interim, if your car has a J533 connector CAN gateway " + - "inside the dashboard, choose \"VW J533 Development\" from the vehicle drop-down for a suitable harness. " + - "(Some newer models are also observed to not have a J533 connector.)", - Column.MODEL) - VW_VARIANT = CarFootnote( - "Includes versions with extra rear cargo space (may be called Variant, Estate, SportWagen, Shooting Brake, etc.)", - Column.MODEL) + VW_EXP_LONG = CarFootnote ( + "Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness " + + "are limited to using stock ACC.", + Column.LONGITUDINAL) + VW_MQB_A0 = CarFootnote( + "Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot " + + "in software, but doesn't yet have a harness available from the comma store.", + Column.HARNESS) @dataclass class VWCarInfo(CarInfo): package: str = "Adaptive Cruise Control (ACC) & Lane Assist" - harness: Enum = Harness.vw + harness: Enum = Harness.j533 + + def init_make(self, CP: car.CarParams): + self.footnotes.insert(0, Footnote.VW_EXP_LONG) CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { CAR.ARTEON_MK1: [ - VWCarInfo("Volkswagen Arteon 2018-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533, video_link="https://youtu.be/FAomFKPFlDA"), - VWCarInfo("Volkswagen Arteon R 2020-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533, video_link="https://youtu.be/FAomFKPFlDA"), - VWCarInfo("Volkswagen Arteon eHybrid 2020-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533, video_link="https://youtu.be/FAomFKPFlDA"), - VWCarInfo("Volkswagen CC 2018-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533, video_link="https://youtu.be/FAomFKPFlDA"), + VWCarInfo("Volkswagen Arteon 2018-22", video_link="https://youtu.be/FAomFKPFlDA"), + VWCarInfo("Volkswagen Arteon R 2020-22", video_link="https://youtu.be/FAomFKPFlDA"), + VWCarInfo("Volkswagen Arteon eHybrid 2020-22", video_link="https://youtu.be/FAomFKPFlDA"), + VWCarInfo("Volkswagen CC 2018-22", video_link="https://youtu.be/FAomFKPFlDA"), ], CAR.ATLAS_MK1: [ - VWCarInfo("Volkswagen Atlas 2018-23", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("Volkswagen Atlas Cross Sport 2021-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("Volkswagen Teramont 2018-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("Volkswagen Teramont Cross Sport 2021-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("Volkswagen Teramont X 2021-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Atlas 2018-23"), + VWCarInfo("Volkswagen Atlas Cross Sport 2021-22"), + VWCarInfo("Volkswagen Teramont 2018-22"), + VWCarInfo("Volkswagen Teramont Cross Sport 2021-22"), + VWCarInfo("Volkswagen Teramont X 2021-22"), ], CAR.GOLF_MK7: [ VWCarInfo("Volkswagen e-Golf 2014-20"), - VWCarInfo("Volkswagen Golf 2015-20", footnotes=[Footnote.VW_VARIANT]), + VWCarInfo("Volkswagen Golf 2015-20"), VWCarInfo("Volkswagen Golf Alltrack 2015-19"), VWCarInfo("Volkswagen Golf GTD 2015-20"), VWCarInfo("Volkswagen Golf GTE 2015-20"), VWCarInfo("Volkswagen Golf GTI 2015-21"), - VWCarInfo("Volkswagen Golf R 2015-19", footnotes=[Footnote.VW_VARIANT]), + VWCarInfo("Volkswagen Golf R 2015-19"), VWCarInfo("Volkswagen Golf SportsVan 2015-20"), ], CAR.JETTA_MK7: [ - VWCarInfo("Volkswagen Jetta 2018-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("Volkswagen Jetta GLI 2021-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Jetta 2018-22"), + VWCarInfo("Volkswagen Jetta GLI 2021-22"), ], CAR.PASSAT_MK8: [ - VWCarInfo("Volkswagen Passat 2015-22", footnotes=[Footnote.VW_HARNESS, Footnote.PASSAT, Footnote.VW_VARIANT], harness=Harness.j533), - VWCarInfo("Volkswagen Passat Alltrack 2015-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("Volkswagen Passat GTE 2015-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533), + VWCarInfo("Volkswagen Passat 2015-22", footnotes=[Footnote.PASSAT]), + VWCarInfo("Volkswagen Passat Alltrack 2015-22"), + VWCarInfo("Volkswagen Passat GTE 2015-22"), ], - CAR.PASSAT_NMS: VWCarInfo("Volkswagen Passat NMS 2017-22", harness=Harness.j533), + CAR.PASSAT_NMS: VWCarInfo("Volkswagen Passat NMS 2017-22"), CAR.POLO_MK6: [ - VWCarInfo("Volkswagen Polo 2020-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("Volkswagen Polo GTI 2020-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Polo 2020-22", footnotes=[Footnote.VW_MQB_A0]), + VWCarInfo("Volkswagen Polo GTI 2020-22", footnotes=[Footnote.VW_MQB_A0]), ], CAR.SHARAN_MK2: [ - VWCarInfo("Volkswagen Sharan 2018-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("SEAT Alhambra 2018-20", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Sharan 2018-22"), + VWCarInfo("SEAT Alhambra 2018-20"), ], - CAR.TAOS_MK1: VWCarInfo("Volkswagen Taos 2022", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - CAR.TCROSS_MK1: VWCarInfo("Volkswagen T-Cross 2021", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - CAR.TIGUAN_MK2: VWCarInfo("Volkswagen Tiguan 2019-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + CAR.TAOS_MK1: VWCarInfo("Volkswagen Taos 2022"), + CAR.TCROSS_MK1: VWCarInfo("Volkswagen T-Cross 2021", footnotes=[Footnote.VW_MQB_A0]), + CAR.TIGUAN_MK2: VWCarInfo("Volkswagen Tiguan 2019-22"), CAR.TOURAN_MK2: VWCarInfo("Volkswagen Touran 2017"), CAR.TRANSPORTER_T61: [ - VWCarInfo("Volkswagen Caravelle 2020", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - VWCarInfo("Volkswagen California 2021", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Caravelle 2020"), + VWCarInfo("Volkswagen California 2021"), ], - CAR.TROC_MK1: VWCarInfo("Volkswagen T-Roc 2021", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + CAR.TROC_MK1: VWCarInfo("Volkswagen T-Roc 2021", footnotes=[Footnote.VW_MQB_A0]), CAR.AUDI_A3_MK3: [ VWCarInfo("Audi A3 2014-19"), VWCarInfo("Audi A3 Sportback e-tron 2017-18"), @@ -227,14 +228,14 @@ CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { VWCarInfo("Audi S3 2015-17"), ], CAR.AUDI_Q2_MK1: VWCarInfo("Audi Q2 2018"), - CAR.AUDI_Q3_MK2: VWCarInfo("Audi Q3 2020-21"), + CAR.AUDI_Q3_MK2: VWCarInfo("Audi Q3 2019-23"), CAR.SEAT_ATECA_MK1: VWCarInfo("SEAT Ateca 2018"), CAR.SEAT_LEON_MK3: VWCarInfo("SEAT Leon 2014-20"), - CAR.SKODA_KAMIQ_MK1: VWCarInfo("Škoda Kamiq 2021", footnotes=[Footnote.KAMIQ]), - CAR.SKODA_KAROQ_MK1: VWCarInfo("Škoda Karoq 2019-21", footnotes=[Footnote.VW_HARNESS]), + CAR.SKODA_KAMIQ_MK1: VWCarInfo("Škoda Kamiq 2021", footnotes=[Footnote.VW_MQB_A0, Footnote.KAMIQ]), + CAR.SKODA_KAROQ_MK1: VWCarInfo("Škoda Karoq 2019-21"), CAR.SKODA_KODIAQ_MK1: VWCarInfo("Škoda Kodiaq 2018-19"), - CAR.SKODA_SCALA_MK1: VWCarInfo("Škoda Scala 2020"), - CAR.SKODA_SUPERB_MK3: VWCarInfo("Škoda Superb 2015-18"), + CAR.SKODA_SCALA_MK1: VWCarInfo("Škoda Scala 2020", footnotes=[Footnote.VW_MQB_A0]), + CAR.SKODA_SUPERB_MK3: VWCarInfo("Škoda Superb 2015-22"), CAR.SKODA_OCTAVIA_MK3: [ VWCarInfo("Škoda Octavia 2015, 2018-19"), VWCarInfo("Škoda Octavia RS 2016"), @@ -872,20 +873,24 @@ FW_VERSIONS = { CAR.AUDI_Q3_MK2: { (Ecu.engine, 0x7e0, None): [ b'\xf1\x8705E906018N \xf1\x899970', + b'\xf1\x8705L906022M \xf1\x890901', b'\xf1\x8783A906259 \xf1\x890001', b'\xf1\x8783A906259 \xf1\x890005', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x8709G927158CN\xf1\x893608', + b'\xf1\x870GC300045D \xf1\x892802', b'\xf1\x870GC300046F \xf1\x892701', ], (Ecu.srs, 0x715, None): [ b'\xf1\x875Q0959655BF\xf1\x890403\xf1\x82\x1321211111211200311121232152219321422111', + b'\xf1\x875Q0959655CC\xf1\x890421\xf1\x82\x131111111111120031111224118A119321532111', b'\xf1\x875Q0959655CC\xf1\x890421\xf1\x82\x131111111111120031111237116A119321532111', ], (Ecu.eps, 0x712, None): [ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567G6000300', b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567G6000800', + b'\xf1\x875QF909144B \xf1\x895582\xf1\x82\x0571G60533A1', ], (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x872Q0907572R \xf1\x890372', @@ -1061,6 +1066,7 @@ FW_VERSIONS = { }, CAR.SKODA_SUPERB_MK3: { (Ecu.engine, 0x7e0, None): [ + b'\xf1\x8704L906026ET\xf1\x891343', b'\xf1\x8704L906026FP\xf1\x891196', b'\xf1\x8704L906026KB\xf1\x894071', b'\xf1\x8704L906026KD\xf1\x894798', @@ -1071,9 +1077,11 @@ FW_VERSIONS = { b'\xf1\x870CW300042H \xf1\x891601', b'\xf1\x870D9300011T \xf1\x894801', b'\xf1\x870D9300012 \xf1\x894940', + b'\xf1\x870D9300041H \xf1\x894905', b'\xf1\x870GC300043 \xf1\x892301', ], (Ecu.srs, 0x715, None): [ + b'\xf1\x875Q0959655AE\xf1\x890130\xf1\x82\x12111200111121001121110012211292221111', b'\xf1\x875Q0959655AE\xf1\x890130\xf1\x82\022111200111121001121118112231292221111', b'\xf1\x875Q0959655AK\xf1\x890130\xf1\x82\022111200111121001121110012211292221111', b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\02331310031313100313131013141319331413100', diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 1adbba4171..a395f85580 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -12,12 +12,11 @@ import cereal.messaging as messaging from common.conversions import Conversions as CV from panda import ALTERNATIVE_EXPERIENCE from system.swaglog import cloudlog -from system.version import is_tested_branch, get_short_branch +from system.version import is_release_branch, get_short_branch from selfdrive.boardd.boardd import can_list_to_can_capnp from selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can from selfdrive.controls.lib.lateral_planner import CAMERA_OFFSET -from selfdrive.controls.lib.drive_helpers import V_CRUISE_INITIAL, update_v_cruise, initialize_v_cruise -from selfdrive.controls.lib.drive_helpers import get_lag_adjusted_curvature +from selfdrive.controls.lib.drive_helpers import VCruiseHelper, get_lag_adjusted_curvature from selfdrive.controls.lib.latcontrol import LatControl from selfdrive.controls.lib.longcontrol import LongControl from selfdrive.controls.lib.latcontrol_pid import LatControlPID @@ -49,7 +48,6 @@ Desire = log.LateralPlan.Desire LaneChangeState = log.LateralPlan.LaneChangeState LaneChangeDirection = log.LateralPlan.LaneChangeDirection EventName = car.CarEvent.EventName -ButtonEvent = car.CarState.ButtonEvent ButtonType = car.CarState.ButtonEvent.Type SafetyModel = car.CarParams.SafetyModel @@ -134,7 +132,7 @@ class Controls: safety_config.safetyModel = car.CarParams.SafetyModel.noOutput self.CP.safetyConfigs = [safety_config] - if is_tested_branch(): + if is_release_branch(): self.CP.experimentalLongitudinalAvailable = False # Write CarParams for radard @@ -147,7 +145,7 @@ class Controls: if not self.CP.experimentalLongitudinalAvailable: self.params.remove("ExperimentalLongitudinalEnabled") if not self.CP.openpilotLongitudinalControl: - self.params.remove("EndToEndLong") + self.params.remove("ExperimentalMode") self.CC = car.CarControl.new_message() self.CS_prev = car.CarState.new_message() @@ -173,9 +171,6 @@ class Controls: self.active = False self.can_rcv_timeout = False self.soft_disable_timer = 0 - self.v_cruise_kph = V_CRUISE_INITIAL - self.v_cruise_cluster_kph = V_CRUISE_INITIAL - self.v_cruise_kph_last = 0 self.mismatch_counter = 0 self.cruise_mismatch_counter = 0 self.can_rcv_timeout_counter = 0 @@ -185,11 +180,11 @@ class Controls: self.events_prev = [] self.current_alert_types = [ET.PERMANENT] self.logged_comm_issue = None - self.button_timers = {ButtonEvent.Type.decelCruise: 0, ButtonEvent.Type.accelCruise: 0} self.last_actuators = car.CarControl.Actuators.new_message() self.steer_limited = False self.desired_curvature = 0.0 self.desired_curvature_rate = 0.0 + self.v_cruise_helper = VCruiseHelper(self.CP) # TODO: no longer necessary, aside from process replay self.sm['liveParameters'].valid = True @@ -219,7 +214,7 @@ class Controls: controls_state = Params().get("ReplayControlsState") if controls_state is not None: controls_state = log.ControlsState.from_bytes(controls_state) - self.v_cruise_kph = controls_state.vCruise + self.v_cruise_helper.v_cruise_kph = controls_state.vCruise if any(ps.controlsAllowed for ps in self.sm['pandaStates']): self.state = State.enabled @@ -245,12 +240,13 @@ class Controls: # Block resume if cruise never previously enabled resume_pressed = any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in CS.buttonEvents) - if not self.CP.pcmCruise and self.v_cruise_kph == V_CRUISE_INITIAL and resume_pressed: + if not self.CP.pcmCruise and not self.v_cruise_helper.v_cruise_initialized and resume_pressed: self.events.add(EventName.resumeBlocked) # Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0 if (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator) or \ - (CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)): + (CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)) or \ + (CS.regenBraking and (not self.CS_prev.regenBraking or not CS.standstill)): self.events.add(EventName.pedalPressed) if CS.gasPressed: @@ -477,20 +473,7 @@ class Controls: def state_transition(self, CS): """Compute conditional state transitions and execute actions on state transitions""" - self.v_cruise_kph_last = self.v_cruise_kph - - if CS.cruiseState.available: - # if stock cruise is completely disabled, then we can use our own set speed logic - if not self.CP.pcmCruise: - self.v_cruise_kph = update_v_cruise(self.v_cruise_kph, CS.vEgo, CS.gasPressed, CS.buttonEvents, - self.button_timers, self.enabled, self.is_metric) - self.v_cruise_cluster_kph = self.v_cruise_kph - else: - self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH - self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH - else: - self.v_cruise_kph = V_CRUISE_INITIAL - self.v_cruise_cluster_kph = V_CRUISE_INITIAL + self.v_cruise_helper.update_v_cruise(CS, self.enabled, self.is_metric) # decrement the soft disable timer at every step, as it's reset on # entrance in SOFT_DISABLING state @@ -568,9 +551,7 @@ class Controls: else: self.state = State.enabled self.current_alert_types.append(ET.ENABLE) - if not self.CP.pcmCruise: - self.v_cruise_kph = initialize_v_cruise(CS.vEgo, CS.buttonEvents, self.v_cruise_kph_last) - self.v_cruise_cluster_kph = self.v_cruise_kph + self.v_cruise_helper.initialize_v_cruise(CS) # Check if openpilot is engaged and actuators are enabled self.enabled = self.state in ENABLED_STATES @@ -618,7 +599,7 @@ class Controls: if not self.joystick_mode: # accel PID loop - pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, CS.vEgo, self.v_cruise_kph * CV.KPH_TO_MS) + pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, CS.vEgo, self.v_cruise_helper.v_cruise_kph * CV.KPH_TO_MS) t_since_plan = (self.sm.frame - self.sm.rcv_frame['longitudinalPlan']) * DT_CTRL actuators.accel = self.LoC.update(CC.longActive, CS, long_plan, pid_accel_limits, t_since_plan) @@ -682,16 +663,6 @@ class Controls: return CC, lac_log - def update_button_timers(self, buttonEvents): - # increment timer for buttons still pressed - for k in self.button_timers: - if self.button_timers[k] > 0: - self.button_timers[k] += 1 - - for b in buttonEvents: - if b.type.raw in self.button_timers: - self.button_timers[b.type.raw] = 1 if b.pressed else 0 - def publish_logs(self, CS, start_time, CC, lac_log): """Send actuators and hud commands to the car, send controlsstate and MPC logging""" @@ -714,7 +685,7 @@ class Controls: CC.cruiseControl.resume = self.enabled and CS.cruiseState.standstill and speeds[-1] > 0.1 hudControl = CC.hudControl - hudControl.setSpeed = float(self.v_cruise_cluster_kph * CV.KPH_TO_MS) + hudControl.setSpeed = float(self.v_cruise_helper.v_cruise_cluster_kph * CV.KPH_TO_MS) hudControl.speedVisible = self.enabled hudControl.lanesVisible = self.enabled hudControl.leadVisible = self.sm['longitudinalPlan'].hasLead @@ -797,8 +768,8 @@ class Controls: controlsState.engageable = not self.events.any(ET.NO_ENTRY) controlsState.longControlState = self.LoC.long_control_state controlsState.vPid = float(self.LoC.v_pid) - controlsState.vCruise = float(self.v_cruise_kph) - controlsState.vCruiseCluster = float(self.v_cruise_cluster_kph) + controlsState.vCruise = float(self.v_cruise_helper.v_cruise_kph) + controlsState.vCruiseCluster = float(self.v_cruise_helper.v_cruise_cluster_kph) controlsState.upAccelCmd = float(self.LoC.pid.p) controlsState.uiAccelCmd = float(self.LoC.pid.i) controlsState.ufAccelCmd = float(self.LoC.pid.f) @@ -806,6 +777,7 @@ class Controls: controlsState.startMonoTime = int(start_time * 1e9) controlsState.forceDecel = bool(force_decel) controlsState.canErrorCounter = self.can_rcv_timeout_counter + controlsState.experimentalMode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl lat_tuning = self.CP.lateralTuning.which() if self.joystick_mode: @@ -879,7 +851,6 @@ class Controls: self.publish_logs(CS, start_time, CC, lac_log) self.prof.checkpoint("Sent") - self.update_button_timers(CS.buttonEvents) self.CS_prev = CS def controlsd_thread(self): @@ -888,6 +859,7 @@ class Controls: self.rk.monitor_time() self.prof.display() + def main(sm=None, pm=None, logcan=None): controls = Controls(sm, pm, logcan) controls.controlsd_thread() diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index d41d6780e3..4790b8f0eb 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/selfdrive/controls/lib/desire_helper.py @@ -5,7 +5,7 @@ from common.realtime import DT_MDL LaneChangeState = log.LateralPlan.LaneChangeState LaneChangeDirection = log.LateralPlan.LaneChangeDirection -LANE_CHANGE_SPEED_MIN = 15 * CV.MPH_TO_MS +LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS LANE_CHANGE_TIME_MAX = 10. DESIRES = { diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index e74be7199e..f0dc2e9467 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -12,6 +12,7 @@ V_CRUISE_MAX = 145 # kph V_CRUISE_MIN = 8 # kph V_CRUISE_ENABLE_MIN = 40 # kph V_CRUISE_INITIAL = 255 # kph +IMPERIAL_INCREMENT = 1.6 # should be CV.MPH_TO_KPH, but this causes rounding errors MIN_SPEED = 1.0 LAT_MPC_N = 16 @@ -22,6 +23,7 @@ CAR_ROTATION_RADIUS = 0.0 # EU guidelines MAX_LATERAL_JERK = 5.0 +ButtonEvent = car.CarState.ButtonEvent ButtonType = car.CarState.ButtonEvent.Type CRUISE_LONG_PRESS = 50 CRUISE_NEAREST_FUNC = { @@ -34,68 +36,121 @@ CRUISE_INTERVAL_SIGN = { } -def apply_deadzone(error, deadzone): - if error > deadzone: - error -= deadzone - elif error < - deadzone: - error += deadzone - else: - error = 0. - return error +class VCruiseHelper: + def __init__(self, CP): + self.CP = CP + self.v_cruise_kph = V_CRUISE_INITIAL + self.v_cruise_cluster_kph = V_CRUISE_INITIAL + self.v_cruise_kph_last = 0 + self.button_timers = {ButtonType.decelCruise: 0, ButtonType.accelCruise: 0} + self.button_change_states = {btn: {"standstill": False, "enabled": False} for btn in self.button_timers} + + @property + def v_cruise_initialized(self): + return self.v_cruise_kph != V_CRUISE_INITIAL + + def update_v_cruise(self, CS, enabled, is_metric): + self.v_cruise_kph_last = self.v_cruise_kph + + if CS.cruiseState.available: + if not self.CP.pcmCruise: + # if stock cruise is completely disabled, then we can use our own set speed logic + self._update_v_cruise_non_pcm(CS, enabled, is_metric) + self.v_cruise_cluster_kph = self.v_cruise_kph + self.update_button_timers(CS, enabled) + else: + self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH + self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH + else: + self.v_cruise_kph = V_CRUISE_INITIAL + self.v_cruise_cluster_kph = V_CRUISE_INITIAL + def _update_v_cruise_non_pcm(self, CS, enabled, is_metric): + # handle button presses. TODO: this should be in state_control, but a decelCruise press + # would have the effect of both enabling and changing speed is checked after the state transition + if not enabled: + return -def rate_limit(new_value, last_value, dw_step, up_step): - return clip(new_value, last_value + dw_step, last_value + up_step) + long_press = False + button_type = None + v_cruise_delta = 1. if is_metric else IMPERIAL_INCREMENT -def update_v_cruise(v_cruise_kph, v_ego, gas_pressed, buttonEvents, button_timers, enabled, metric): - # handle button presses. TODO: this should be in state_control, but a decelCruise press - # would have the effect of both enabling and changing speed is checked after the state transition - if not enabled: - return v_cruise_kph + for b in CS.buttonEvents: + if b.type.raw in self.button_timers and not b.pressed: + if self.button_timers[b.type.raw] > CRUISE_LONG_PRESS: + return # end long press + button_type = b.type.raw + break + else: + for k in self.button_timers.keys(): + if self.button_timers[k] and self.button_timers[k] % CRUISE_LONG_PRESS == 0: + button_type = k + long_press = True + break - long_press = False - button_type = None + if button_type is None: + return - # should be CV.MPH_TO_KPH, but this causes rounding errors - v_cruise_delta = 1. if metric else 1.6 + # Don't adjust speed when pressing resume to exit standstill + cruise_standstill = self.button_change_states[button_type]["standstill"] or CS.cruiseState.standstill + if button_type == ButtonType.accelCruise and cruise_standstill: + return - for b in buttonEvents: - if b.type.raw in button_timers and not b.pressed: - if button_timers[b.type.raw] > CRUISE_LONG_PRESS: - return v_cruise_kph # end long press - button_type = b.type.raw - break - else: - for k in button_timers.keys(): - if button_timers[k] and button_timers[k] % CRUISE_LONG_PRESS == 0: - button_type = k - long_press = True - break + # Don't adjust speed if we've enabled since the button was depressed (some ports enable on rising edge) + if not self.button_change_states[button_type]["enabled"]: + return - if button_type: v_cruise_delta = v_cruise_delta * (5 if long_press else 1) - if long_press and v_cruise_kph % v_cruise_delta != 0: # partial interval - v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](v_cruise_kph / v_cruise_delta) * v_cruise_delta + if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval + self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta else: - v_cruise_kph += v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type] + self.v_cruise_kph += v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type] # If set is pressed while overriding, clip cruise speed to minimum of vEgo - if gas_pressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise): - v_cruise_kph = max(v_cruise_kph, v_ego * CV.MS_TO_KPH) + if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise): + self.v_cruise_kph = max(self.v_cruise_kph, CS.vEgo * CV.MS_TO_KPH) + + self.v_cruise_kph = clip(round(self.v_cruise_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX) - v_cruise_kph = clip(round(v_cruise_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX) + def update_button_timers(self, CS, enabled): + # increment timer for buttons still pressed + for k in self.button_timers: + if self.button_timers[k] > 0: + self.button_timers[k] += 1 - return v_cruise_kph + for b in CS.buttonEvents: + if b.type.raw in self.button_timers: + # Start/end timer and store current state on change of button pressed + self.button_timers[b.type.raw] = 1 if b.pressed else 0 + self.button_change_states[b.type.raw] = {"standstill": CS.cruiseState.standstill, "enabled": enabled} + def initialize_v_cruise(self, CS): + # initializing is handled by the PCM + if self.CP.pcmCruise: + return -def initialize_v_cruise(v_ego, buttonEvents, v_cruise_last): - for b in buttonEvents: # 250kph or above probably means we never had a set speed - if b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) and v_cruise_last < 250: - return v_cruise_last + if any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents) and self.v_cruise_kph_last < 250: + self.v_cruise_kph = self.v_cruise_kph_last + else: + self.v_cruise_kph = int(round(clip(CS.vEgo * CV.MS_TO_KPH, V_CRUISE_ENABLE_MIN, V_CRUISE_MAX))) + + self.v_cruise_cluster_kph = self.v_cruise_kph - return int(round(clip(v_ego * CV.MS_TO_KPH, V_CRUISE_ENABLE_MIN, V_CRUISE_MAX))) + +def apply_deadzone(error, deadzone): + if error > deadzone: + error -= deadzone + elif error < - deadzone: + error += deadzone + else: + error = 0. + return error + + +def rate_limit(new_value, last_value, dw_step, up_step): + return clip(new_value, last_value + dw_step, last_value + up_step) def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates): diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py index 44426620e9..a761cceecb 100644 --- a/selfdrive/controls/lib/events.py +++ b/selfdrive/controls/lib/events.py @@ -597,6 +597,7 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { EventName.buttonCancel: { ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + ET.NO_ENTRY: NoEntryAlert("Cancel Pressed"), }, EventName.brakeHold: { diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 51676086ba..d10d39d945 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -17,7 +17,8 @@ from selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY # friction in the steering wheel that needs to be overcome to # move it at all, this is compensated for too. -LOW_SPEED_FACTOR = 200 +LOW_SPEED_X = [0, 10, 20, 30] +LOW_SPEED_Y = [15, 13, 10, 5] class LatControlTorque(LatControl): @@ -57,8 +58,9 @@ class LatControlTorque(LatControl): actual_lateral_accel = actual_curvature * CS.vEgo ** 2 lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 - setpoint = desired_lateral_accel + LOW_SPEED_FACTOR * desired_curvature - measurement = actual_lateral_accel + LOW_SPEED_FACTOR * actual_curvature + low_speed_factor = interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)**2 + setpoint = desired_lateral_accel + low_speed_factor * desired_curvature + measurement = actual_lateral_accel + low_speed_factor * actual_curvature error = setpoint - measurement gravity_adjusted_lateral_accel = desired_lateral_accel - params.roll * ACCELERATION_DUE_TO_GRAVITY pid_log.error = self.torque_from_lateral_accel(error, self.torque_params, error, diff --git a/selfdrive/controls/lib/longcontrol.py b/selfdrive/controls/lib/longcontrol.py index db5bf4d3e6..92a4f1f99b 100644 --- a/selfdrive/controls/lib/longcontrol.py +++ b/selfdrive/controls/lib/longcontrol.py @@ -103,6 +103,7 @@ class LongControl: elif self.long_control_state == LongCtrlState.stopping: if output_accel > self.CP.stopAccel: + output_accel = min(output_accel, 0.0) output_accel -= self.CP.stoppingDecelRate * DT_CTRL self.reset(CS.vEgo) diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index 49eb5988e2..79a9ec4f0c 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -36,7 +36,7 @@ A_EGO_COST = 0. J_EGO_COST = 5.0 A_CHANGE_COST = 200. DANGER_ZONE_COST = 100. -CRASH_DISTANCE = .5 +CRASH_DISTANCE = .25 LEAD_DANGER_FACTOR = 0.75 LIMIT_COST = 1e6 ACADOS_SOLVER_TYPE = 'SQP_RTI' @@ -49,6 +49,7 @@ MAX_T = 10.0 T_IDXS_LST = [index_function(idx, max_val=MAX_T, max_idx=N) for idx in range(N+1)] T_IDXS = np.array(T_IDXS_LST) +FCW_IDXS = T_IDXS < 5.0 T_DIFFS = np.diff(T_IDXS, prepend=[0.]) MIN_ACCEL = -3.5 MAX_ACCEL = 2.0 @@ -300,8 +301,10 @@ class LongitudinalMpc: return lead_xv def set_accel_limits(self, min_a, max_a): + # TODO this sets a max accel limit, but the minimum limit is only for cruise decel + # needs refactor self.cruise_min_a = min_a - self.cruise_max_a = max_a + self.max_a = max_a def update(self, carstate, radarstate, v_cruise, x, v, a, j): v_ego = self.x0[1] @@ -316,16 +319,17 @@ class LongitudinalMpc: lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1]) lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1]) + self.params[:,0] = MIN_ACCEL + self.params[:,1] = self.max_a + # Update in ACC mode or ACC/e2e blend if self.mode == 'acc': - self.params[:,0] = MIN_ACCEL - self.params[:,1] = self.cruise_max_a self.params[:,5] = LEAD_DANGER_FACTOR # Fake an obstacle for cruise, this ensures smooth acceleration to set speed # when the leads are no factor. v_lower = v_ego + (T_IDXS * self.cruise_min_a * 1.05) - v_upper = v_ego + (T_IDXS * self.cruise_max_a * 1.05) + v_upper = v_ego + (T_IDXS * self.max_a * 1.05) v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper) @@ -337,9 +341,6 @@ class LongitudinalMpc: x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0 elif self.mode == 'blended': - self.params[:,0] = MIN_ACCEL - self.params[:,1] = MAX_ACCEL - self.params[:,5] = 1.0 x_obstacles = np.column_stack([lead_0_obstacle, @@ -351,7 +352,7 @@ class LongitudinalMpc: x_and_cruise = np.column_stack([x, cruise_target]) x = np.min(x_and_cruise, axis=1) - self.source = 'e2e' if x_and_cruise[0,0] < x_and_cruise[0,1] else 'cruise' + self.source = 'e2e' if x_and_cruise[1,0] < x_and_cruise[1,1] else 'cruise' else: raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner update') @@ -369,7 +370,7 @@ class LongitudinalMpc: self.params[:,4] = T_FOLLOW self.run() - if (np.any(lead_xv_0[:,0] - self.x_sol[:,0] < CRASH_DISTANCE) and + if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and radarstate.leadOne.modelProb > 0.9): self.crash_cnt += 1 else: diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 457065d3b5..def0a1208a 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -10,7 +10,7 @@ from common.params import Params from common.realtime import DT_MDL from selfdrive.modeld.constants import T_IDXS from selfdrive.controls.lib.longcontrol import LongCtrlState -from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc +from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, MIN_ACCEL, MAX_ACCEL from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, CONTROL_N from system.swaglog import cloudlog @@ -58,6 +58,7 @@ class LongitudinalPlanner: self.a_desired = init_a self.v_desired_filter = FirstOrderFilter(init_v, 2.0, DT_MDL) + self.v_model_error = 0.0 self.v_desired_trajectory = np.zeros(CONTROL_N) self.a_desired_trajectory = np.zeros(CONTROL_N) @@ -65,15 +66,16 @@ class LongitudinalPlanner: self.solverExecutionTime = 0.0 def read_param(self): - e2e = self.params.get_bool('EndToEndLong') and self.CP.openpilotLongitudinalControl + e2e = self.params.get_bool('ExperimentalMode') and self.CP.openpilotLongitudinalControl self.mpc.mode = 'blended' if e2e else 'acc' - def parse_model(self, model_msg): + @staticmethod + def parse_model(model_msg, model_error): if (len(model_msg.position.x) == 33 and len(model_msg.velocity.x) == 33 and len(model_msg.acceleration.x) == 33): - x = np.interp(T_IDXS_MPC, T_IDXS, model_msg.position.x) - v = np.interp(T_IDXS_MPC, T_IDXS, model_msg.velocity.x) + x = np.interp(T_IDXS_MPC, T_IDXS, model_msg.position.x) - model_error * T_IDXS_MPC + v = np.interp(T_IDXS_MPC, T_IDXS, model_msg.velocity.x) - model_error a = np.interp(T_IDXS_MPC, T_IDXS, model_msg.acceleration.x) j = np.zeros(len(T_IDXS_MPC)) else: @@ -102,8 +104,12 @@ class LongitudinalPlanner: # No change cost when user is controlling the speed, or when standstill prev_accel_constraint = not (reset_state or sm['carState'].standstill) - accel_limits = [A_CRUISE_MIN, get_max_accel(v_ego)] - accel_limits_turns = limit_accel_in_turns(v_ego, sm['carState'].steeringAngleDeg, accel_limits, self.CP) + if self.mpc.mode == 'acc': + accel_limits = [A_CRUISE_MIN, get_max_accel(v_ego)] + accel_limits_turns = limit_accel_in_turns(v_ego, sm['carState'].steeringAngleDeg, accel_limits, self.CP) + else: + accel_limits = [MIN_ACCEL, MAX_ACCEL] + accel_limits_turns = [MIN_ACCEL, MAX_ACCEL] if reset_state: self.v_desired_filter.x = v_ego @@ -112,6 +118,9 @@ class LongitudinalPlanner: # Prevent divergence, smooth in current v_ego self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) + # Compute model v_ego error + if len(sm['modelV2'].temporalPose.trans): + self.v_model_error = sm['modelV2'].temporalPose.trans[0] - v_ego if force_slow_decel: # if required so, force a smooth deceleration @@ -124,7 +133,7 @@ class LongitudinalPlanner: self.mpc.set_weights(prev_accel_constraint) self.mpc.set_accel_limits(accel_limits_turns[0], accel_limits_turns[1]) self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired) - x, v, a, j = self.parse_model(sm['modelV2']) + x, v, a, j = self.parse_model(sm['modelV2'], self.v_model_error) self.mpc.update(sm['carState'], sm['radarState'], v_cruise, x, v, a, j) self.v_desired_trajectory = np.interp(T_IDXS[:CONTROL_N], T_IDXS_MPC, self.mpc.v_solution) @@ -132,8 +141,7 @@ class LongitudinalPlanner: self.j_desired_trajectory = np.interp(T_IDXS[:CONTROL_N], T_IDXS_MPC[:-1], self.mpc.j_solution) # TODO counter is only needed because radar is glitchy, remove once radar is gone - # TODO write fcw in e2e_long mode - self.fcw = self.mpc.mode == 'acc' and self.mpc.crash_cnt > 5 and not sm['carState'].standstill + self.fcw = self.mpc.crash_cnt > 2 and not sm['carState'].standstill if self.fcw: cloudlog.info("FCW triggered") diff --git a/selfdrive/controls/tests/test_cruise_speed.py b/selfdrive/controls/tests/test_cruise_speed.py old mode 100644 new mode 100755 index a972bfb073..a635198ceb --- a/selfdrive/controls/tests/test_cruise_speed.py +++ b/selfdrive/controls/tests/test_cruise_speed.py @@ -1,10 +1,17 @@ #!/usr/bin/env python3 -import unittest import numpy as np +from parameterized import parameterized_class +import unittest + +from selfdrive.controls.lib.drive_helpers import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_ENABLE_MIN, IMPERIAL_INCREMENT +from cereal import car +from common.conversions import Conversions as CV from common.params import Params +from selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver +ButtonEvent = car.CarState.ButtonEvent +ButtonType = car.CarState.ButtonEvent.Type -from selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver def run_cruise_simulation(cruise, t_end=20.): man = Maneuver( @@ -19,14 +26,14 @@ def run_cruise_simulation(cruise, t_end=20.): ) valid, output = man.evaluate() assert valid - return output[-1,3] + return output[-1, 3] class TestCruiseSpeed(unittest.TestCase): def test_cruise_speed(self): params = Params() for e2e in [False, True]: - params.put_bool("EndToEndLong", e2e) + params.put_bool("ExperimentalMode", e2e) for speed in np.arange(5, 40, 5): print(f'Testing {speed} m/s') cruise_speed = float(speed) @@ -35,5 +42,111 @@ class TestCruiseSpeed(unittest.TestCase): self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {speed} m/s') +# TODO: test pcmCruise +@parameterized_class(('pcm_cruise',), [(False,)]) +class TestVCruiseHelper(unittest.TestCase): + def setUp(self): + self.CP = car.CarParams(pcmCruise=self.pcm_cruise) # pylint: disable=E1101 + self.v_cruise_helper = VCruiseHelper(self.CP) + self.reset_cruise_speed_state() + + def reset_cruise_speed_state(self): + # Two resets previous cruise speed + for _ in range(2): + self.v_cruise_helper.update_v_cruise(car.CarState(cruiseState={"available": False}), enabled=False, is_metric=False) + + def enable(self, v_ego): + # Simulates user pressing set with a current speed + self.v_cruise_helper.initialize_v_cruise(car.CarState(vEgo=v_ego)) + + def test_adjust_speed(self): + """ + Asserts speed changes on falling edges of buttons. + """ + + self.enable(V_CRUISE_ENABLE_MIN * CV.KPH_TO_MS) + + for btn in (ButtonType.accelCruise, ButtonType.decelCruise): + for pressed in (True, False): + CS = car.CarState(cruiseState={"available": True}) + CS.buttonEvents = [ButtonEvent(type=btn, pressed=pressed)] + + self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False) + self.assertEqual(pressed, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last) + + def test_rising_edge_enable(self): + """ + Some car interfaces may enable on rising edge of a button, + ensure we don't adjust speed if enabled changes mid-press. + """ + + # NOTE: enabled is always one frame behind the result from button press in controlsd + for enabled, pressed in ((False, False), + (False, True), + (True, False)): + CS = car.CarState(cruiseState={"available": True}) + CS.buttonEvents = [ButtonEvent(type=ButtonType.decelCruise, pressed=pressed)] + self.v_cruise_helper.update_v_cruise(CS, enabled=enabled, is_metric=False) + if pressed: + self.enable(V_CRUISE_ENABLE_MIN * CV.KPH_TO_MS) + + # Expected diff on enabling. Speed should not change on falling edge of pressed + self.assertEqual(not pressed, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last) + + def test_resume_in_standstill(self): + """ + Asserts we don't increment set speed if user presses resume/accel to exit cruise standstill. + """ + + self.enable(0) + + for standstill in (True, False): + for pressed in (True, False): + CS = car.CarState(cruiseState={"available": True, "standstill": standstill}) + CS.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=pressed)] + self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False) + + # speed should only update if not at standstill and button falling edge + should_equal = standstill or pressed + self.assertEqual(should_equal, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last) + + def test_set_gas_pressed(self): + """ + Asserts pressing set while enabled with gas pressed sets + the speed to the maximum of vEgo and current cruise speed. + """ + + for v_ego in np.linspace(0, 100, 101): + self.reset_cruise_speed_state() + self.enable(V_CRUISE_ENABLE_MIN * CV.KPH_TO_MS) + + # first decrement speed, then perform gas pressed logic + expected_v_cruise_kph = self.v_cruise_helper.v_cruise_kph - IMPERIAL_INCREMENT + expected_v_cruise_kph = max(expected_v_cruise_kph, v_ego * CV.MS_TO_KPH) # clip to min of vEgo + expected_v_cruise_kph = float(np.clip(round(expected_v_cruise_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX)) + + CS = car.CarState(vEgo=float(v_ego), gasPressed=True, cruiseState={"available": True}) + CS.buttonEvents = [ButtonEvent(type=ButtonType.decelCruise, pressed=False)] + self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False) + + # TODO: fix skipping first run due to enabled on rising edge exception + if v_ego == 0.0: + continue + self.assertEqual(expected_v_cruise_kph, self.v_cruise_helper.v_cruise_kph) + + def test_initialize_v_cruise(self): + """ + Asserts allowed cruise speeds on enabling with SET. + """ + + for v_ego in np.linspace(0, 100, 101): + self.reset_cruise_speed_state() + self.assertFalse(self.v_cruise_helper.v_cruise_initialized) + + self.enable(float(v_ego)) + self.assertTrue(V_CRUISE_ENABLE_MIN <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX) + self.assertTrue(self.v_cruise_helper.v_cruise_initialized) + + if __name__ == "__main__": unittest.main() diff --git a/selfdrive/controls/tests/test_following_distance.py b/selfdrive/controls/tests/test_following_distance.py index 3534f58235..0535caab84 100644 --- a/selfdrive/controls/tests/test_following_distance.py +++ b/selfdrive/controls/tests/test_following_distance.py @@ -27,7 +27,7 @@ class TestFollowingDistance(unittest.TestCase): def test_following_distance(self): params = Params() for e2e in [False, True]: - params.put_bool("EndToEndLong", e2e) + params.put_bool("ExperimentalMode", e2e) for speed in np.arange(0, 40, 5): print(f'Testing {speed} m/s') v_lead = float(speed) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 2769f394c5..6936d88acc 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -187,6 +187,7 @@ class Laikad: "gpsTimeOfWeek": tow, "positionECEF": measurement_msg(value=ecef_pos.tolist(), std=pos_std.tolist(), valid=kf_valid), "velocityECEF": measurement_msg(value=ecef_vel.tolist(), std=vel_std.tolist(), valid=kf_valid), + # TODO std is incorrectly the dimension of the measurements and not 3D "positionFixECEF": measurement_msg(value=self.last_pos_fix, std=self.last_pos_residual, valid=self.last_pos_fix_t == t), "ubloxMonoTime": gnss_mono_time, "correctedMeasurements": meas_msgs diff --git a/selfdrive/locationd/liblocationd.cc b/selfdrive/locationd/liblocationd.cc index 49404668a4..da57fb7ff4 100755 --- a/selfdrive/locationd/liblocationd.cc +++ b/selfdrive/locationd/liblocationd.cc @@ -26,4 +26,16 @@ extern "C" { memcpy(std_buff, stdev.data(), sizeof(double) * stdev.size()); } + bool is_gps_ok(Localizer *localizer){ + return localizer->is_gps_ok(); + } + + bool are_inputs_ok(Localizer *localizer){ + return localizer->are_inputs_ok(); + } + + void observation_timings_invalid_reset(Localizer *localizer){ + localizer->observation_timings_invalid_reset(); + } + } diff --git a/selfdrive/locationd/locationd.cc b/selfdrive/locationd/locationd.cc index e156af5d64..4325900c0e 100755 --- a/selfdrive/locationd/locationd.cc +++ b/selfdrive/locationd/locationd.cc @@ -19,6 +19,9 @@ const double VALID_TIME_SINCE_RESET = 1.0; // s const double VALID_POS_STD = 50.0; // m const double MAX_RESET_TRACKER = 5.0; const double SANE_GPS_UNCERTAINTY = 1500.0; // m +const double INPUT_INVALID_THRESHOLD = 5.0; // same as reset tracker +const double DECAY = 0.99995; // same as reset tracker +const double MAX_FILTER_REWIND_TIME = 0.8; // s // TODO: GPS sensor time offsets are empirically calculated // They should be replaced with synced time from a real clock @@ -200,6 +203,14 @@ VectorXd Localizer::get_stdev() { return this->kf->get_P().diagonal().array().sqrt(); } +bool Localizer::are_inputs_ok() { + return this->critical_services_valid(this->observation_values_invalid) && !this->observation_timings_invalid; +} + +void Localizer::observation_timings_invalid_reset(){ + this->observation_timings_invalid = false; +} + void Localizer::handle_sensor(double current_time, const cereal::SensorEventData::Reader& log) { // TODO does not yet account for double sensor readings in the log @@ -209,10 +220,15 @@ void Localizer::handle_sensor(double current_time, const cereal::SensorEventData } double sensor_time = 1e-9 * log.getTimestamp(); - + // sensor time and log time should be close if (std::abs(current_time - sensor_time) > 0.1) { LOGE("Sensor reading ignored, sensor timestamp more than 100ms off from log time"); + this->observation_timings_invalid = true; + return; + } + else if (!this->is_timestamp_valid(sensor_time)) { + this->observation_timings_invalid = true; return; } @@ -227,6 +243,10 @@ void Localizer::handle_sensor(double current_time, const cereal::SensorEventData auto meas = Vector3d(-v[2], -v[1], -v[0]); if (meas.norm() < ROTATION_SANITY_CHECK) { this->kf->predict_and_observe(sensor_time, OBSERVATION_PHONE_GYRO, { meas }); + this->observation_values_invalid["gyroscope"] *= DECAY; + } + else{ + this->observation_values_invalid["gyroscope"] += 1.0; } } @@ -242,6 +262,10 @@ void Localizer::handle_sensor(double current_time, const cereal::SensorEventData auto meas = Vector3d(-v[2], -v[1], -v[0]); if (meas.norm() < ACCEL_SANITY_CHECK) { this->kf->predict_and_observe(sensor_time, OBSERVATION_PHONE_ACCEL, { meas }); + this->observation_values_invalid["accelerometer"] *= DECAY; + } + else{ + this->observation_values_invalid["accelerometer"] += 1.0; } } } @@ -270,15 +294,22 @@ void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::R bool gps_lat_lng_alt_insane = ((std::abs(log.getLatitude()) > 90) || (std::abs(log.getLongitude()) > 180) || (std::abs(log.getAltitude()) > ALTITUDE_SANITY_CHECK)); bool gps_vel_insane = (floatlist2vector(log.getVNED()).norm() > TRANS_SANITY_CHECK); - if (gps_invalid_flag || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane) { + // quectel gps verticalAccuracy is clipped to 500 + bool gps_accuracy_insane_quectel = false; + if (!ublox_available) { + gps_accuracy_insane_quectel = log.getVerticalAccuracy() == 500; + } + + if (gps_invalid_flag || gps_unreasonable || gps_accuracy_insane || gps_lat_lng_alt_insane || gps_vel_insane || gps_accuracy_insane_quectel) { + this->gps_valid = false; this->determine_gps_mode(current_time); return; } - + double sensor_time = current_time - sensor_time_offset; // Process message - this->last_gps_fix = sensor_time; + this->gps_valid = true; this->gps_mode = true; Geodetic geodetic = { log.getLatitude(), log.getLongitude(), log.getAltitude() }; this->converter = std::make_unique(geodetic); @@ -328,8 +359,14 @@ void Localizer::handle_car_state(double current_time, const cereal::CarState::Re void Localizer::handle_cam_odo(double current_time, const cereal::CameraOdometry::Reader& log) { VectorXd rot_device = this->device_from_calib * floatlist2vector(log.getRot()); VectorXd trans_device = this->device_from_calib * floatlist2vector(log.getTrans()); + + if (!this->is_timestamp_valid(current_time)) { + this->observation_timings_invalid = true; + return; + } if ((rot_device.norm() > ROTATION_SANITY_CHECK) || (trans_device.norm() > TRANS_SANITY_CHECK)) { + this->observation_values_invalid["cameraOdometry"] += 1.0; return; } @@ -337,10 +374,12 @@ void Localizer::handle_cam_odo(double current_time, const cereal::CameraOdometry VectorXd trans_calib_std = floatlist2vector(log.getTransStd()); if ((rot_calib_std.minCoeff() <= MIN_STD_SANITY_CHECK) || (trans_calib_std.minCoeff() <= MIN_STD_SANITY_CHECK)) { + this->observation_values_invalid["cameraOdometry"] += 1.0; return; } if ((rot_calib_std.norm() > 10 * ROTATION_SANITY_CHECK) || (trans_calib_std.norm() > 10 * TRANS_SANITY_CHECK)) { + this->observation_values_invalid["cameraOdometry"] += 1.0; return; } @@ -356,12 +395,19 @@ void Localizer::handle_cam_odo(double current_time, const cereal::CameraOdometry { rot_device }, { rot_device_cov }); this->kf->predict_and_observe(current_time, OBSERVATION_CAMERA_ODO_TRANSLATION, { trans_device }, { trans_device_cov }); + this->observation_values_invalid["cameraOdometry"] *= DECAY; } void Localizer::handle_live_calib(double current_time, const cereal::LiveCalibrationData::Reader& log) { + if (!this->is_timestamp_valid(current_time)) { + this->observation_timings_invalid = true; + return; + } + if (log.getRpyCalib().size() > 0) { auto live_calib = floatlist2vector(log.getRpyCalib()); if ((live_calib.minCoeff() < -CALIB_RPY_SANITY_CHECK) || (live_calib.maxCoeff() > CALIB_RPY_SANITY_CHECK)) { + this->observation_values_invalid["liveCalibration"] += 1.0; return; } @@ -369,6 +415,7 @@ void Localizer::handle_live_calib(double current_time, const cereal::LiveCalibra this->device_from_calib = euler2rot(this->calib); this->calib_from_device = this->device_from_calib.transpose(); this->calibrated = log.getCalStatus() == 1; + this->observation_values_invalid["liveCalibration"] *= DECAY; } } @@ -400,8 +447,8 @@ void Localizer::time_check(double current_time) { void Localizer::update_reset_tracker() { // reset tracker is tuned to trigger when over 1reset/10s over 2min period - if (this->isGpsOK()) { - this->reset_tracker *= .99995; + if (this->is_gps_ok()) { + this->reset_tracker *= DECAY; } else { this->reset_tracker = 0.0; } @@ -476,9 +523,26 @@ kj::ArrayPtr Localizer::get_message_bytes(MessageBuilder& msg_build return msg_builder.toBytes(); } +bool Localizer::is_gps_ok() { + return this->gps_valid; +} -bool Localizer::isGpsOK() { - return this->kf->get_filter_time() - this->last_gps_fix < 1.0; +bool Localizer::critical_services_valid(std::map critical_services) { + for (auto &kv : critical_services){ + if (kv.second >= INPUT_INVALID_THRESHOLD){ + return false; + } + } + return true; +} + +bool Localizer::is_timestamp_valid(double current_time) { + double filter_time = this->kf->get_filter_time(); + if (!std::isnan(filter_time) && ((filter_time - current_time) > MAX_FILTER_REWIND_TIME)) { + LOGE("Observation timestamp is older than the max rewind threshold of the filter"); + return false; + } + return true; } void Localizer::determine_gps_mode(double current_time) { @@ -498,8 +562,10 @@ void Localizer::determine_gps_mode(double current_time) { } int Localizer::locationd_thread() { + ublox_available = Params().getBool("UbloxAvailable", true); + const char* gps_location_socket; - if (Params().getBool("UbloxAvailable", true)) { + if (ublox_available) { gps_location_socket = "gpsLocationExternal"; } else { gps_location_socket = "gpsLocation"; @@ -513,10 +579,15 @@ int Localizer::locationd_thread() { uint64_t cnt = 0; bool filterInitialized = false; + const std::vector critical_input_services = {"cameraOdometry", "liveCalibration", "accelerometer", "gyroscope"}; + for (std::string service : critical_input_services) { + this->observation_values_invalid.insert({service, 0.0}); + } while (!do_exit) { sm.update(); if (filterInitialized){ + this->observation_timings_invalid_reset(); for (const char* service : service_list) { if (sm.updated(service) && sm.valid(service)){ const cereal::Event::Reader log = sm[service]; @@ -530,8 +601,8 @@ int Localizer::locationd_thread() { // 100Hz publish for notcars, 20Hz for cars const char* trigger_msg = sm["carParams"].getCarParams().getNotCar() ? "accelerometer" : "cameraOdometry"; if (sm.updated(trigger_msg)) { - bool inputsOK = sm.allAliveAndValid(); - bool gpsOK = this->isGpsOK(); + bool inputsOK = sm.allAliveAndValid() && this->are_inputs_ok(); + bool gpsOK = this->is_gps_ok(); bool sensorsOK = sm.allAliveAndValid({"accelerometer", "gyroscope"}); MessageBuilder msg_builder; diff --git a/selfdrive/locationd/locationd.h b/selfdrive/locationd/locationd.h index 280296b06c..f0872d9f56 100755 --- a/selfdrive/locationd/locationd.h +++ b/selfdrive/locationd/locationd.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "cereal/messaging/messaging.h" @@ -32,8 +33,12 @@ public: void finite_check(double current_time = NAN); void time_check(double current_time = NAN); void update_reset_tracker(); - bool isGpsOK(); + bool is_gps_ok(); + bool critical_services_valid(std::map critical_services); + bool is_timestamp_valid(double current_time); void determine_gps_mode(double current_time); + bool are_inputs_ok(); + void observation_timings_invalid_reset(); kj::ArrayPtr get_message_bytes(MessageBuilder& msg_builder, bool inputsOK, bool sensorsOK, bool gpsOK, bool msgValid); @@ -68,8 +73,11 @@ private: std::unique_ptr converter; int64_t unix_timestamp_millis = 0; - double last_gps_fix = 0; double reset_tracker = 0.0; bool device_fell = false; bool gps_mode = false; + bool gps_valid = false; + bool ublox_available = true; + bool observation_timings_invalid = false; + std::map observation_values_invalid; }; diff --git a/selfdrive/locationd/paramsd.py b/selfdrive/locationd/paramsd.py index 86672b0460..9bd4ed0837 100755 --- a/selfdrive/locationd/paramsd.py +++ b/selfdrive/locationd/paramsd.py @@ -92,7 +92,7 @@ class ParamsLearner: self.speed = msg.vEgo in_linear_region = abs(self.steering_angle) < 45 or not self.steering_pressed - self.active = self.speed > 5 and in_linear_region + self.active = self.speed > 1 and in_linear_region if self.active: self.kf.predict_and_observe(t, ObservationKind.STEER_ANGLE, np.array([[math.radians(msg.steeringAngleDeg)]])) diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index 42dff60087..588bca1578 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -16,9 +16,9 @@ from selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY HISTORY = 5 # secs POINTS_PER_BUCKET = 1500 MIN_POINTS_TOTAL = 4000 -MIN_POINTS_TOTAL_QLOG = 800 +MIN_POINTS_TOTAL_QLOG = 600 FIT_POINTS_TOTAL = 2000 -FIT_POINTS_TOTAL_QLOG = 800 +FIT_POINTS_TOTAL_QLOG = 600 MIN_VEL = 15 # m/s FRICTION_FACTOR = 1.5 # ~85% of data coverage FACTOR_SANITY = 0.3 diff --git a/selfdrive/loggerd/loggerd.h b/selfdrive/loggerd/loggerd.h index 6eafbe08d0..1fa6349828 100644 --- a/selfdrive/loggerd/loggerd.h +++ b/selfdrive/loggerd/loggerd.h @@ -10,6 +10,7 @@ #include #include #include +#include #include "cereal/messaging/messaging.h" #include "cereal/services.h" diff --git a/selfdrive/loggerd/tests/test_logger.cc b/selfdrive/loggerd/tests/test_logger.cc index 18a0e57df7..ba7835d632 100644 --- a/selfdrive/loggerd/tests/test_logger.cc +++ b/selfdrive/loggerd/tests/test_logger.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include "catch2/catch.hpp" #include "cereal/messaging/messaging.h" diff --git a/selfdrive/manager/test/test_manager.py b/selfdrive/manager/test/test_manager.py index 7ac2c5f506..6d4df0423a 100755 --- a/selfdrive/manager/test/test_manager.py +++ b/selfdrive/manager/test/test_manager.py @@ -4,6 +4,7 @@ import signal import time import unittest +from common.params import Params import selfdrive.manager.manager as manager from selfdrive.manager.process import DaemonProcess from selfdrive.manager.process_config import managed_processes @@ -20,6 +21,10 @@ class TestManager(unittest.TestCase): os.environ['PASSIVE'] = '0' HARDWARE.set_power_save(False) + # ensure clean CarParams + params = Params() + params.clear_all() + def tearDown(self): manager.manager_cleanup() @@ -40,6 +45,7 @@ class TestManager(unittest.TestCase): Ensure all processes exit cleanly when stopped. """ HARDWARE.set_power_save(False) + manager.manager_init() manager.manager_prepare() for p in ALL_PROCESSES: managed_processes[p].start() diff --git a/selfdrive/modeld/models/driving.cc b/selfdrive/modeld/models/driving.cc index cc4a83de62..4015731c42 100644 --- a/selfdrive/modeld/models/driving.cc +++ b/selfdrive/modeld/models/driving.cc @@ -337,6 +337,17 @@ void fill_model(cereal::ModelDataV2::Builder &framed, const ModelOutput &net_out for (int i=0; i 0 else abs(pitch_error) # no positive pitch limit yaw_error = abs(yaw_error) - if pitch_error > self.settings._POSE_PITCH_THRESHOLD*self.pose.cfactor_pitch or \ + if pitch_error > (self.settings._POSE_PITCH_THRESHOLD*self.pose.cfactor_pitch if self.pose_calibrated else self.settings._PITCH_NATURAL_THRESHOLD) or \ yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw: distracted_types.append(DistractedType.DISTRACTED_POSE) @@ -205,7 +207,7 @@ class DriverStatus(): distracted_types.append(DistractedType.DISTRACTED_BLINK) if self.ee1_calibrated: - ee1_dist = self.eev1 > min(self.ee1_offseter.filtered_stat.M, self.settings._EE_MAX_OFFSET1) * self.settings._EE_THRESH12 + ee1_dist = self.eev1 > max(min(self.ee1_offseter.filtered_stat.M, self.settings._EE_MAX_OFFSET1), self.settings._EE_MIN_OFFSET1) * self.settings._EE_THRESH12 else: ee1_dist = self.eev1 > self.settings._EE_THRESH11 # if self.ee2_calibrated: diff --git a/selfdrive/sensord/sensors/lsm6ds3_accel.cc b/selfdrive/sensord/sensors/lsm6ds3_accel.cc index 27cd4d0c70..c19e3de7ed 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_accel.cc +++ b/selfdrive/sensord/sensors/lsm6ds3_accel.cc @@ -1,17 +1,132 @@ #include "lsm6ds3_accel.h" #include +#include +#include #include "common/swaglog.h" #include "common/timing.h" +#include "common/util.h" LSM6DS3_Accel::LSM6DS3_Accel(I2CBus *bus, int gpio_nr, bool shared_gpio) : I2CSensor(bus, gpio_nr, shared_gpio) {} +void LSM6DS3_Accel::wait_for_data_ready() { + uint8_t drdy = 0; + uint8_t buffer[6]; + + do { + read_register(LSM6DS3_ACCEL_I2C_REG_STAT_REG, &drdy, sizeof(drdy)); + drdy &= LSM6DS3_ACCEL_DRDY_XLDA; + } while (drdy == 0); + + read_register(LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL, buffer, sizeof(buffer)); +} + +void LSM6DS3_Accel::read_and_avg_data(float* out_buf) { + uint8_t drdy = 0; + uint8_t buffer[6]; + + float scaling = 0.061f; + if (source == cereal::SensorEventData::SensorSource::LSM6DS3TRC) { + scaling = 0.122f; + } + + for (int i = 0; i < 5; i++) { + do { + read_register(LSM6DS3_ACCEL_I2C_REG_STAT_REG, &drdy, sizeof(drdy)); + drdy &= LSM6DS3_ACCEL_DRDY_XLDA; + } while (drdy == 0); + + int len = read_register(LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL, buffer, sizeof(buffer)); + assert(len == sizeof(buffer)); + + for (int j = 0; j < 3; j++) { + out_buf[j] += (float)read_16_bit(buffer[j*2], buffer[j*2+1]) * scaling; + } + } + + for (int i = 0; i < 3; i++) { + out_buf[i] /= 5.0f; + } +} + +int LSM6DS3_Accel::self_test(int test_type) { + float val_st_off[3] = {0}; + float val_st_on[3] = {0}; + float test_val[3] = {0}; + uint8_t ODR_FS_MO = LSM6DS3_ACCEL_ODR_52HZ; // full scale: +-2g, ODR: 52Hz + + // prepare sensor for self-test + + // enable block data update and automatic increment + int ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL3_C, LSM6DS3_ACCEL_IF_INC_BDU); + if (ret < 0) { + return ret; + } + + if (source == cereal::SensorEventData::SensorSource::LSM6DS3TRC) { + ODR_FS_MO = LSM6DS3_ACCEL_FS_4G | LSM6DS3_ACCEL_ODR_52HZ; + } + ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, ODR_FS_MO); + if (ret < 0) { + return ret; + } + + // wait for stable output, and discard first values + util::sleep_for(100); + wait_for_data_ready(); + read_and_avg_data(val_st_off); + + // enable Self Test positive (or negative) + ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL5_C, test_type); + if (ret < 0) { + return ret; + } + + // wait for stable output, and discard first values + util::sleep_for(100); + wait_for_data_ready(); + read_and_avg_data(val_st_on); + + // disable sensor + ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, 0); + if (ret < 0) { + return ret; + } + + // disable self test + ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL5_C, 0); + if (ret < 0) { + return ret; + } + + // calculate the mg values for self test + for (int i = 0; i < 3; i++) { + test_val[i] = fabs(val_st_on[i] - val_st_off[i]); + } + + // verify test result + for (int i = 0; i < 3; i++) { + if ((LSM6DS3_ACCEL_MIN_ST_LIMIT_mg > test_val[i]) || + (test_val[i] > LSM6DS3_ACCEL_MAX_ST_LIMIT_mg)) { + return -1; + } + } + + return ret; +} + int LSM6DS3_Accel::init() { int ret = 0; uint8_t buffer[1]; uint8_t value = 0; + bool do_self_test = false; + + const char* env_lsm_selftest =env_lsm_selftest = std::getenv("LSM_SELF_TEST"); + if (env_lsm_selftest != nullptr && strncmp(env_lsm_selftest, "1", 1) == 0) { + do_self_test = true; + } ret = read_register(LSM6DS3_ACCEL_I2C_REG_ID, buffer, 1); if(ret < 0) { @@ -29,11 +144,29 @@ int LSM6DS3_Accel::init() { source = cereal::SensorEventData::SensorSource::LSM6DS3TRC; } + ret = self_test(LSM6DS3_ACCEL_POSITIVE_TEST); + if (ret < 0) { + LOGE("LSM6DS3 accel positive self-test failed!"); + if (do_self_test) goto fail; + } + + ret = self_test(LSM6DS3_ACCEL_NEGATIVE_TEST); + if (ret < 0) { + LOGE("LSM6DS3 accel negative self-test failed!"); + if (do_self_test) goto fail; + } + ret = init_gpio(); if (ret < 0) { goto fail; } + // enable continuous update, and automatic increase + ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL3_C, LSM6DS3_ACCEL_IF_INC); + if (ret < 0) { + goto fail; + } + // TODO: set scale and bandwidth. Default is +- 2G, 50 Hz ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, LSM6DS3_ACCEL_ODR_104HZ); if (ret < 0) { diff --git a/selfdrive/sensord/sensors/lsm6ds3_accel.h b/selfdrive/sensord/sensors/lsm6ds3_accel.h index 84084fc916..c3f66f5803 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_accel.h +++ b/selfdrive/sensord/sensors/lsm6ds3_accel.h @@ -10,21 +10,37 @@ #define LSM6DS3_ACCEL_I2C_REG_ID 0x0F #define LSM6DS3_ACCEL_I2C_REG_INT1_CTRL 0x0D #define LSM6DS3_ACCEL_I2C_REG_CTRL1_XL 0x10 +#define LSM6DS3_ACCEL_I2C_REG_CTRL3_C 0x12 +#define LSM6DS3_ACCEL_I2C_REG_CTRL5_C 0x14 +#define LSM6DS3_ACCEL_I2C_REG_CTR9_XL 0x18 #define LSM6DS3_ACCEL_I2C_REG_STAT_REG 0x1E #define LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL 0x28 // Constants #define LSM6DS3_ACCEL_CHIP_ID 0x69 #define LSM6DS3TRC_ACCEL_CHIP_ID 0x6A +#define LSM6DS3_ACCEL_FS_4G (0b10 << 2) +#define LSM6DS3_ACCEL_ODR_52HZ (0b0011 << 4) #define LSM6DS3_ACCEL_ODR_104HZ (0b0100 << 4) #define LSM6DS3_ACCEL_INT1_DRDY_XL 0b1 #define LSM6DS3_ACCEL_DRDY_XLDA 0b1 #define LSM6DS3_ACCEL_DRDY_PULSE_MODE (1 << 7) - +#define LSM6DS3_ACCEL_IF_INC 0b00000100 +#define LSM6DS3_ACCEL_IF_INC_BDU 0b01000100 +#define LSM6DS3_ACCEL_XYZ_DEN 0b11100000 +#define LSM6DS3_ACCEL_POSITIVE_TEST 0b01 +#define LSM6DS3_ACCEL_NEGATIVE_TEST 0b10 +#define LSM6DS3_ACCEL_MIN_ST_LIMIT_mg 90.0f +#define LSM6DS3_ACCEL_MAX_ST_LIMIT_mg 1700.0f class LSM6DS3_Accel : public I2CSensor { uint8_t get_device_address() {return LSM6DS3_ACCEL_I2C_ADDR;} cereal::SensorEventData::SensorSource source = cereal::SensorEventData::SensorSource::LSM6DS3; + + // self test functions + int self_test(int test_type); + void wait_for_data_ready(); + void read_and_avg_data(float* val_st_off); public: LSM6DS3_Accel(I2CBus *bus, int gpio_nr = 0, bool shared_gpio = false); int init(); diff --git a/selfdrive/sensord/sensors/lsm6ds3_gyro.cc b/selfdrive/sensord/sensors/lsm6ds3_gyro.cc index 014a72bb73..f306be0fe8 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_gyro.cc +++ b/selfdrive/sensord/sensors/lsm6ds3_gyro.cc @@ -2,19 +2,120 @@ #include #include +#include #include "common/swaglog.h" #include "common/timing.h" +#include "common/util.h" #define DEG2RAD(x) ((x) * M_PI / 180.0) LSM6DS3_Gyro::LSM6DS3_Gyro(I2CBus *bus, int gpio_nr, bool shared_gpio) : I2CSensor(bus, gpio_nr, shared_gpio) {} +void LSM6DS3_Gyro::wait_for_data_ready() { + uint8_t drdy = 0; + uint8_t buffer[6]; + + do { + read_register(LSM6DS3_GYRO_I2C_REG_STAT_REG, &drdy, sizeof(drdy)); + drdy &= LSM6DS3_GYRO_DRDY_GDA; + } while (drdy == 0); + + read_register(LSM6DS3_GYRO_I2C_REG_OUTX_L_G, buffer, sizeof(buffer)); +} + +void LSM6DS3_Gyro::read_and_avg_data(float* out_buf) { + uint8_t drdy = 0; + uint8_t buffer[6]; + + for (int i = 0; i < 5; i++) { + do { + read_register(LSM6DS3_GYRO_I2C_REG_STAT_REG, &drdy, sizeof(drdy)); + drdy &= LSM6DS3_GYRO_DRDY_GDA; + } while (drdy == 0); + + int len = read_register(LSM6DS3_GYRO_I2C_REG_OUTX_L_G, buffer, sizeof(buffer)); + assert(len == sizeof(buffer)); + + for (int j = 0; j < 3; j++) { + out_buf[j] += (float)read_16_bit(buffer[j*2], buffer[j*2+1]) * 70.0f; + } + } + + // calculate the mg average values + for (int i = 0; i < 3; i++) { + out_buf[i] /= 5.0f; + } +} + +int LSM6DS3_Gyro::self_test(int test_type) { + float val_st_off[3] = {0}; + float val_st_on[3] = {0}; + float test_val[3] = {0}; + + // prepare sensor for self-test + + // full scale: 2000dps, ODR: 208Hz + int ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL2_G, LSM6DS3_GYRO_ODR_208HZ | LSM6DS3_GYRO_FS_2000dps); + if (ret < 0) { + return ret; + } + + // wait for stable output, and discard first values + util::sleep_for(150); + wait_for_data_ready(); + read_and_avg_data(val_st_off); + + // enable Self Test positive (or negative) + ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL5_C, test_type); + if (ret < 0) { + return ret; + } + + // wait for stable output, and discard first values + util::sleep_for(50); + wait_for_data_ready(); + read_and_avg_data(val_st_on); + + // disable sensor + ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL2_G, 0); + if (ret < 0) { + return ret; + } + + // disable self test + ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL5_C, 0); + if (ret < 0) { + return ret; + } + + // calculate the mg values for self test + for (int i = 0; i < 3; i++) { + test_val[i] = fabs(val_st_on[i] - val_st_off[i]); + } + + // verify test result + for (int i = 0; i < 3; i++) { + if ((LSM6DS3_GYRO_MIN_ST_LIMIT_mdps > test_val[i]) || + (test_val[i] > LSM6DS3_GYRO_MAX_ST_LIMIT_mdps)) { + return -1; + } + } + + return ret; +} + int LSM6DS3_Gyro::init() { int ret = 0; uint8_t buffer[1]; uint8_t value = 0; + bool do_self_test = false; + + const char* env_lsm_selftest =env_lsm_selftest = std::getenv("LSM_SELF_TEST"); + if (env_lsm_selftest != nullptr && strncmp(env_lsm_selftest, "1", 1) == 0) { + do_self_test = true; + } ret = read_register(LSM6DS3_GYRO_I2C_REG_ID, buffer, 1); if(ret < 0) { @@ -37,6 +138,18 @@ int LSM6DS3_Gyro::init() { goto fail; } + ret = self_test(LSM6DS3_GYRO_POSITIVE_TEST); + if (ret < 0 ) { + LOGE("LSM6DS3 gyro positive self-test failed!"); + if (do_self_test) goto fail; + } + + ret = self_test(LSM6DS3_GYRO_NEGATIVE_TEST); + if (ret < 0) { + LOGE("LSM6DS3 gyro negative self-test failed!"); + if (do_self_test) goto fail; + } + // TODO: set scale. Default is +- 250 deg/s ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL2_G, LSM6DS3_GYRO_ODR_104HZ); if (ret < 0) { @@ -65,7 +178,7 @@ fail: int LSM6DS3_Gyro::shutdown() { int ret = 0; - // disable data ready interrupt for accel on INT1 + // disable data ready interrupt for gyro on INT1 uint8_t value = 0; ret = read_register(LSM6DS3_GYRO_I2C_REG_INT1_CTRL, &value, 1); if (ret < 0) { diff --git a/selfdrive/sensord/sensors/lsm6ds3_gyro.h b/selfdrive/sensord/sensors/lsm6ds3_gyro.h index 6c61ffcef2..220e6b0cec 100644 --- a/selfdrive/sensord/sensors/lsm6ds3_gyro.h +++ b/selfdrive/sensord/sensors/lsm6ds3_gyro.h @@ -10,21 +10,33 @@ #define LSM6DS3_GYRO_I2C_REG_ID 0x0F #define LSM6DS3_GYRO_I2C_REG_INT1_CTRL 0x0D #define LSM6DS3_GYRO_I2C_REG_CTRL2_G 0x11 +#define LSM6DS3_GYRO_I2C_REG_CTRL5_C 0x14 #define LSM6DS3_GYRO_I2C_REG_STAT_REG 0x1E #define LSM6DS3_GYRO_I2C_REG_OUTX_L_G 0x22 +#define LSM6DS3_GYRO_POSITIVE_TEST (0b01 << 2) +#define LSM6DS3_GYRO_NEGATIVE_TEST (0b11 << 2) // Constants #define LSM6DS3_GYRO_CHIP_ID 0x69 #define LSM6DS3TRC_GYRO_CHIP_ID 0x6A +#define LSM6DS3_GYRO_FS_2000dps (0b11 << 2) #define LSM6DS3_GYRO_ODR_104HZ (0b0100 << 4) +#define LSM6DS3_GYRO_ODR_208HZ (0b0101 << 4) #define LSM6DS3_GYRO_INT1_DRDY_G 0b10 #define LSM6DS3_GYRO_DRDY_GDA 0b10 #define LSM6DS3_GYRO_DRDY_PULSE_MODE (1 << 7) +#define LSM6DS3_GYRO_MIN_ST_LIMIT_mdps 150000.0f +#define LSM6DS3_GYRO_MAX_ST_LIMIT_mdps 700000.0f class LSM6DS3_Gyro : public I2CSensor { uint8_t get_device_address() {return LSM6DS3_GYRO_I2C_ADDR;} cereal::SensorEventData::SensorSource source = cereal::SensorEventData::SensorSource::LSM6DS3; + + // self test functions + int self_test(int test_type); + void wait_for_data_ready(); + void read_and_avg_data(float* val_st_off); public: LSM6DS3_Gyro(I2CBus *bus, int gpio_nr = 0, bool shared_gpio = false); int init(); diff --git a/selfdrive/sensord/tests/test_sensord.py b/selfdrive/sensord/tests/test_sensord.py index 82bd28446b..c6fe33129a 100755 --- a/selfdrive/sensord/tests/test_sensord.py +++ b/selfdrive/sensord/tests/test_sensord.py @@ -104,6 +104,9 @@ class TestSensord(unittest.TestCase): # make sure gpiochip0 is readable HARDWARE.initialize_hardware() + # enable LSM self test + os.environ["LSM_SELF_TEST"] = "1" + # read initial sensor values every test case can use os.system("pkill -f ./_sensord") try: @@ -118,6 +121,8 @@ class TestSensord(unittest.TestCase): @classmethod def tearDownClass(cls): managed_processes["sensord"].stop() + if "LSM_SELF_TEST" in os.environ: + del os.environ['LSM_SELF_TEST'] def tearDown(self): managed_processes["sensord"].stop() diff --git a/selfdrive/test/longitudinal_maneuvers/maneuver.py b/selfdrive/test/longitudinal_maneuvers/maneuver.py index dad5d89844..071eaada12 100644 --- a/selfdrive/test/longitudinal_maneuvers/maneuver.py +++ b/selfdrive/test/longitudinal_maneuvers/maneuver.py @@ -17,6 +17,7 @@ class Maneuver(): self.only_lead2 = kwargs.get("only_lead2", False) self.only_radar = kwargs.get("only_radar", False) self.ensure_start = kwargs.get("ensure_start", False) + self.enabled = kwargs.get("enabled", True) self.duration = duration self.title = title @@ -26,23 +27,24 @@ class Maneuver(): lead_relevancy=self.lead_relevancy, speed=self.speed, distance_lead=self.distance_lead, + enabled=self.enabled, only_lead2=self.only_lead2, only_radar=self.only_radar, ) valid = True logs = [] - while plant.current_time() < self.duration: - speed_lead = np.interp(plant.current_time(), self.breakpoints, self.speed_lead_values) - prob = np.interp(plant.current_time(), self.breakpoints, self.prob_lead_values) - cruise = np.interp(plant.current_time(), self.breakpoints, self.cruise_values) + while plant.current_time < self.duration: + speed_lead = np.interp(plant.current_time, self.breakpoints, self.speed_lead_values) + prob = np.interp(plant.current_time, self.breakpoints, self.prob_lead_values) + cruise = np.interp(plant.current_time, self.breakpoints, self.cruise_values) log = plant.step(speed_lead, prob, cruise) d_rel = log['distance_lead'] - log['distance'] if self.lead_relevancy else 200. v_rel = speed_lead - log['speed'] if self.lead_relevancy else 0. log['d_rel'] = d_rel log['v_rel'] = v_rel - logs.append(np.array([plant.current_time(), + logs.append(np.array([plant.current_time, log['distance'], log['distance_lead'], log['speed'], diff --git a/selfdrive/test/longitudinal_maneuvers/plant.py b/selfdrive/test/longitudinal_maneuvers/plant.py index e81510e9ba..c3af1eee03 100755 --- a/selfdrive/test/longitudinal_maneuvers/plant.py +++ b/selfdrive/test/longitudinal_maneuvers/plant.py @@ -10,11 +10,12 @@ from selfdrive.modeld.constants import T_IDXS from selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner from selfdrive.controls.lib.radar_helpers import _LEAD_ACCEL_TAU -class Plant(): + +class Plant: messaging_initialized = False def __init__(self, lead_relevancy=False, speed=0.0, distance_lead=2.0, - only_lead2=False, only_radar=False): + enabled=True, only_lead2=False, only_radar=False): self.rate = 1. / DT_MDL if not Plant.messaging_initialized: @@ -32,10 +33,11 @@ class Plant(): self.speeds = [] # lead car - self.distance_lead = distance_lead self.lead_relevancy = lead_relevancy - self.only_lead2=only_lead2 - self.only_radar=only_radar + self.distance_lead = distance_lead + self.enabled = enabled + self.only_lead2 = only_lead2 + self.only_radar = only_radar self.rk = Ratekeeper(self.rate, print_delay_threshold=100.0) self.ts = 1. / self.rate @@ -47,6 +49,7 @@ class Plant(): self.planner = LongitudinalPlanner(CarInterface.get_params(CAR.CIVIC), init_v=self.speed) + @property def current_time(self): return float(self.rk.frame) / self.rate @@ -104,9 +107,7 @@ class Plant(): acceleration.x = [float(x) for x in np.zeros_like(T_IDXS)] model.modelV2.acceleration = acceleration - - - control.controlsState.longControlState = LongCtrlState.pid + control.controlsState.longControlState = LongCtrlState.pid if self.enabled else LongCtrlState.off control.controlsState.vCruise = float(v_cruise * 3.6) car_state.carState.vEgo = float(self.speed) car_state.carState.standstill = self.speed < 0.01 @@ -141,7 +142,7 @@ class Plant(): # print at 5hz if (self.rk.frame % (self.rate // 5)) == 0: print("%2.2f sec %6.2f m %6.2f m/s %6.2f m/s2 lead_rel: %6.2f m %6.2f m/s" - % (self.current_time(), self.distance, self.speed, self.acceleration, d_rel, v_rel)) + % (self.current_time, self.distance, self.speed, self.acceleration, d_rel, v_rel)) # ******** update prevs ******** diff --git a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py b/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py index c7c2915878..e859952445 100755 --- a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py +++ b/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py @@ -10,7 +10,7 @@ from selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver # TODO: make new FCW tests maneuvers = [ Maneuver( - 'approach stopped car at 20m/s, initial distance: 120m', + 'approach stopped car at 25m/s, initial distance: 120m', duration=20., initial_speed=25., lead_relevancy=True, @@ -118,6 +118,13 @@ maneuvers = [ breakpoints=[1., 10., 15.], ensure_start=True, ), + Maneuver( + 'cruising at 25 m/s while disabled', + duration=20., + initial_speed=25., + lead_relevancy=False, + enabled=False, + ), ] @@ -143,11 +150,11 @@ def run_maneuver_worker(k): params = Params() man = maneuvers[k] - params.put_bool("EndToEndLong", True) + params.put_bool("ExperimentalMode", True) print(man.title, ' in e2e mode') valid, _ = man.evaluate() self.assertTrue(valid, msg=man.title) - params.put_bool("EndToEndLong", False) + params.put_bool("ExperimentalMode", False) print(man.title, ' in acc mode') valid, _ = man.evaluate() self.assertTrue(valid, msg=man.title) diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index d13ced3a53..cdd6ed7a6b 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -49ea844254883ac61caa2ac425f453799aeb28a6 +ca02aa240e629920ad88a6510213cb0a153af92b diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 68bd35c38c..4aa9d60ab5 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -01b24beff6855e8c4d2fb0efeeefafb46343e013 +959e63af52d9efdb62156cab4b773f88b154fd75 diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 5c754d9312..c58909bf7f 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -18,7 +18,7 @@ from tools.lib.logreader import LogReader source_segments = [ ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.BODY ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.SONATA - ("HYUNDAI2", "d545129f3ca90f28|2022-10-19--09-22-54--9"), # HYUNDAI.KIA_EV6 + ("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.KIA_EV6 ("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.PRIUS (INDI) ("TOYOTA2", "0982d79ebb0de295|2021-01-03--20-03-36--6"), # TOYOTA.RAV4 (LQR) ("TOYOTA3", "f7d7e3538cda1a2a|2021-08-16--08-55-34--6"), # TOYOTA.COROLLA_TSS2 @@ -41,7 +41,7 @@ source_segments = [ segments = [ ("BODY", "regenFA002A80700|2022-09-27--15-37-02--0"), ("HYUNDAI", "regenBE53A59065B|2022-09-27--16-52-03--0"), - ("HYUNDAI2", "d545129f3ca90f28|2022-10-19--09-22-54--9"), + ("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), ("TOYOTA", "regen929C5790007|2022-09-27--16-27-47--0"), ("TOYOTA2", "regenEA3950D7F22|2022-09-27--15-43-24--0"), ("TOYOTA3", "regen89026F6BD8D|2022-09-27--15-45-37--0"), diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 84e055752a..669c214746 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -58,7 +58,7 @@ qt_env.Program("qt/spinner", ["qt/spinner.cc"], LIBS=qt_libs) qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc", "qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", "qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc", - "qt/offroad/driverview.cc"] + "qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc"] qt_env.Program("_ui", qt_src + [asset_obj], LIBS=qt_libs) if GetOption('test'): qt_src.remove("main.cc") # replaced by test_runner diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index 3fe00e6ed9..3f3c9a5885 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -4,6 +4,7 @@ #include #include +#include "selfdrive/ui/qt/offroad/experimental_mode.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/widgets/drive_stats.h" #include "selfdrive/ui/qt/widgets/prime.h" @@ -22,7 +23,8 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { slayout = new QStackedLayout(); main_layout->addLayout(slayout); - home = new OffroadHome(); + home = new OffroadHome(this); + QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings); slayout->addWidget(home); onroad = new OnroadWindow(this); @@ -128,11 +130,24 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { main_layout->addSpacing(25); center_layout = new QStackedLayout(); + // Vertical experimental button and drive stats layout + QWidget* statsAndExperimentalModeButtonWidget = new QWidget(this); + QVBoxLayout* statsAndExperimentalModeButton = new QVBoxLayout(statsAndExperimentalModeButtonWidget); + statsAndExperimentalModeButton->setSpacing(30); + statsAndExperimentalModeButton->setMargin(0); + + ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this); + QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings); + + statsAndExperimentalModeButton->addWidget(experimental_mode, 1); + statsAndExperimentalModeButton->addWidget(new DriveStats, 1); + + // Horizontal experimental + drive stats and setup widget QWidget* statsAndSetupWidget = new QWidget(this); QHBoxLayout* statsAndSetup = new QHBoxLayout(statsAndSetupWidget); statsAndSetup->setMargin(0); statsAndSetup->setSpacing(30); - statsAndSetup->addWidget(new DriveStats, 1); + statsAndSetup->addWidget(statsAndExperimentalModeButtonWidget, 1); statsAndSetup->addWidget(new SetupWidget); center_layout->addWidget(statsAndSetupWidget); diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index 6636da56ec..ed1c215467 100644 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -22,6 +22,9 @@ class OffroadHome : public QFrame { public: explicit OffroadHome(QWidget* parent = 0); +signals: + void openSettings(int index = 0, const QString ¶m = ""); + private: void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; @@ -45,7 +48,7 @@ public: explicit HomeWindow(QWidget* parent = 0); signals: - void openSettings(); + void openSettings(int index = 0, const QString ¶m = ""); void closeSettings(); public slots: diff --git a/selfdrive/ui/qt/offroad/driverview.cc b/selfdrive/ui/qt/offroad/driverview.cc index 0ff786fb91..1377bb3b23 100644 --- a/selfdrive/ui/qt/offroad/driverview.cc +++ b/selfdrive/ui/qt/offroad/driverview.cc @@ -35,6 +35,7 @@ void DriverViewScene::showEvent(QShowEvent* event) { } void DriverViewScene::hideEvent(QHideEvent* event) { + // TODO: stop vipc thread ? params.putBool("IsDriverViewEnabled", false); } diff --git a/selfdrive/ui/qt/offroad/experimental_mode.cc b/selfdrive/ui/qt/offroad/experimental_mode.cc new file mode 100644 index 0000000000..b99220c1d1 --- /dev/null +++ b/selfdrive/ui/qt/offroad/experimental_mode.cc @@ -0,0 +1,76 @@ +#include "selfdrive/ui/qt/offroad/experimental_mode.h" + +#include +#include +#include +#include +#include + +#include "selfdrive/ui/ui.h" + +ExperimentalModeButton::ExperimentalModeButton(QWidget *parent) : QPushButton(parent) { + chill_pixmap = QPixmap("../assets/img_couch.svg").scaledToWidth(img_width, Qt::SmoothTransformation); + experimental_pixmap = QPixmap("../assets/img_experimental_grey.svg").scaledToWidth(img_width, Qt::SmoothTransformation); + + // go to toggles and expand experimental mode description + connect(this, &QPushButton::clicked, [=]() { emit openSettings(2, "ExperimentalMode"); }); + + setFixedHeight(125); + QHBoxLayout *main_layout = new QHBoxLayout; + main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0); + + mode_label = new QLabel; + mode_icon = new QLabel; + mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + + main_layout->addWidget(mode_label, 1, Qt::AlignLeft); + main_layout->addWidget(mode_icon, 0, Qt::AlignRight); + + setLayout(main_layout); + + setStyleSheet(R"( + QPushButton { + border: none; + } + + QLabel { + font-size: 45px; + font-weight: 300; + text-align: left; + font-family: JetBrainsMono; + color: #000000; + } + )"); +} + +void ExperimentalModeButton::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.setPen(Qt::NoPen); + p.setRenderHint(QPainter::Antialiasing); + + QPainterPath path; + path.addRoundedRect(rect(), 10, 10); + + // gradient + bool pressed = isDown(); + QLinearGradient gradient(rect().left(), 0, rect().right(), 0); + if (experimental_mode) { + gradient.setColorAt(0, QColor(255, 155, 63, pressed ? 0xcc : 0xff)); + gradient.setColorAt(1, QColor(219, 56, 34, pressed ? 0xcc : 0xff)); + } else { + gradient.setColorAt(0, QColor(20, 255, 171, pressed ? 0xcc : 0xff)); + gradient.setColorAt(1, QColor(35, 149, 255, pressed ? 0xcc : 0xff)); + } + p.fillPath(path, gradient); + + // vertical line + p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine)); + int line_x = rect().right() - img_width - (2 * horizontal_padding); + p.drawLine(line_x, rect().bottom(), line_x, rect().top()); +} + +void ExperimentalModeButton::showEvent(QShowEvent *event) { + experimental_mode = params.getBool("ExperimentalMode"); + mode_icon->setPixmap(experimental_mode ? experimental_pixmap : chill_pixmap); + mode_label->setText(experimental_mode ? tr("EXPERIMENTAL MODE ON") : tr("CHILL MODE ON")); +} diff --git a/selfdrive/ui/qt/offroad/experimental_mode.h b/selfdrive/ui/qt/offroad/experimental_mode.h new file mode 100644 index 0000000000..bfb7638bbe --- /dev/null +++ b/selfdrive/ui/qt/offroad/experimental_mode.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "common/params.h" + +class ExperimentalModeButton : public QPushButton { + Q_OBJECT + +public: + explicit ExperimentalModeButton(QWidget* parent = 0); + +signals: + void openSettings(int index = 0, const QString &toggle = ""); + +private: + void showEvent(QShowEvent *event) override; + + Params params; + bool experimental_mode; + int img_width = 100; + int horizontal_padding = 30; + QPixmap experimental_pixmap; + QPixmap chill_pixmap; + QLabel *mode_label; + QLabel *mode_icon; + +protected: + void paintEvent(QPaintEvent *event) override; +}; diff --git a/selfdrive/ui/qt/offroad/networking.cc b/selfdrive/ui/qt/offroad/networking.cc index 13697adfb5..d69d67edeb 100644 --- a/selfdrive/ui/qt/offroad/networking.cc +++ b/selfdrive/ui/qt/offroad/networking.cc @@ -314,7 +314,7 @@ void WifiUI::refresh() { QPushButton *forgetBtn = new QPushButton(tr("FORGET")); forgetBtn->setObjectName("forgetBtn"); QObject::connect(forgetBtn, &QPushButton::clicked, [=]() { - if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"%1\"?").arg(QString::fromUtf8(network.ssid)), this)) { + if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"%1\"?").arg(QString::fromUtf8(network.ssid)), tr("Forget"), this)) { wifi->forgetConnection(network.ssid); } }); diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index ae878c5419..bde8628dc4 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -27,7 +27,7 @@ #include "selfdrive/ui/qt/widgets/input.h" TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { - // param, title, desc, icon + // param, title, desc, icon, confirm std::vector> toggle_defs{ { "OpenpilotEnabledToggle", @@ -35,6 +35,20 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { tr("Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off."), "../assets/offroad/icon_openpilot.png", }, + { + "ExperimentalMode", + tr("Experimental Mode"), + "", + "../assets/img_experimental_white.svg", + }, + { + "ExperimentalLongitudinalEnabled", + tr("Experimental openpilot Longitudinal Control"), + QString("%1
%2") + .arg(tr("WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB).")) + .arg(tr("On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control.")), + "../assets/offroad/icon_speed_limit.png", + }, { "IsLdwEnabled", tr("Enable Lane Departure Warnings"), @@ -55,22 +69,10 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { }, { "DisengageOnAccelerator", - tr("Disengage On Accelerator Pedal"), + tr("Disengage on Accelerator Pedal"), tr("When enabled, pressing the accelerator pedal will disengage openpilot."), "../assets/offroad/icon_disengage_on_accelerator.svg", }, - { - "EndToEndLong", - tr("🌮 End-to-end longitudinal (extremely alpha) 🌮"), - "", - "../assets/offroad/icon_road.png", - }, - { - "ExperimentalLongitudinalEnabled", - tr("Experimental openpilot longitudinal control"), - tr("WARNING: openpilot longitudinal control is experimental for this car and will disable AEB."), - "../assets/offroad/icon_speed_limit.png", - }, #ifdef ENABLE_MAPS { "NavSettingTime24h", @@ -85,7 +87,6 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "../assets/offroad/icon_road.png", }, #endif - }; for (auto &[param, title, desc, icon] : toggle_defs) { @@ -98,19 +99,38 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { toggles[param.toStdString()] = toggle; } + // Toggles with confirmation dialogs + toggles["ExperimentalMode"]->setActiveIcon("../assets/img_experimental.svg"); + toggles["ExperimentalMode"]->setConfirmation(true, true); + toggles["ExperimentalLongitudinalEnabled"]->setConfirmation(true, false); + connect(toggles["ExperimentalLongitudinalEnabled"], &ToggleControl::toggleFlipped, [=]() { updateToggles(); }); } +void TogglesPanel::expandToggleDescription(const QString ¶m) { + toggles[param.toStdString()]->showDescription(); +} + void TogglesPanel::showEvent(QShowEvent *event) { updateToggles(); } void TogglesPanel::updateToggles() { - auto e2e_toggle = toggles["EndToEndLong"]; + auto e2e_toggle = toggles["ExperimentalMode"]; auto op_long_toggle = toggles["ExperimentalLongitudinalEnabled"]; - const QString e2e_description = tr("Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would. Super experimental."); + const QString e2e_description = QString("%1
" + "

%2


" + "%3
" + "

%4


" + "%5") + .arg(tr("openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. Experimental features are listed below:")) + .arg(tr("🌮 End-to-End Longitudinal Control 🌮")) + .arg(tr("Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " + "Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected.")) + .arg(tr("New Driving Visualization")) + .arg(tr("The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner.")); auto cp_bytes = params.get("CarParamsPersistent"); if (!cp_bytes.empty()) { @@ -132,10 +152,10 @@ void TogglesPanel::updateToggles() { } else { // no long for now e2e_toggle->setEnabled(false); - params.remove("EndToEndLong"); + params.remove("ExperimentalMode"); - const QString no_long = tr("openpilot longitudinal control is not currently available for this car."); - const QString exp_long = tr("Enable experimental longitudinal control to enable this."); + const QString no_long = tr("Experimental mode is currently unavailable on this car, since the car's stock ACC is used for longitudinal control."); + const QString exp_long = tr("Enable experimental longitudinal control to allow experimental mode."); e2e_toggle->setDescription("" + (CP.getExperimentalLongitudinalAvailable() ? exp_long : no_long) + "

" + e2e_description); } @@ -161,7 +181,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); connect(resetCalibBtn, &ButtonControl::clicked, [&]() { - if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { params.remove("CalibrationParams"); } }); @@ -170,7 +190,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { if (!params.getBool("Passive")) { auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of openpilot")); connect(retrainingBtn, &ButtonControl::clicked, [=]() { - if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) { emit reviewTrainingGuide(); } }); @@ -181,7 +201,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { auto regulatoryBtn = new ButtonControl(tr("Regulatory"), tr("VIEW"), ""); connect(regulatoryBtn, &ButtonControl::clicked, [=]() { const std::string txt = util::read_file("../assets/offroad/fcc.html"); - RichTextDialog::alert(QString::fromStdString(txt), this); + ConfirmationDialog::rich(QString::fromStdString(txt), this); }); addItem(regulatoryBtn); } @@ -258,7 +278,7 @@ void DevicePanel::updateCalibDescription() { void DevicePanel::reboot() { if (!uiState()->engaged()) { - if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), tr("Reboot"), this)) { // Check engaged again in case it changed while the dialog was open if (!uiState()->engaged()) { Params().putBool("DoReboot", true); @@ -271,7 +291,7 @@ void DevicePanel::reboot() { void DevicePanel::poweroff() { if (!uiState()->engaged()) { - if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), tr("Power Off"), this)) { // Check engaged again in case it changed while the dialog was open if (!uiState()->engaged()) { Params().putBool("DoShutdown", true); @@ -283,8 +303,15 @@ void DevicePanel::poweroff() { } void SettingsWindow::showEvent(QShowEvent *event) { - panel_widget->setCurrentIndex(0); - nav_btns->buttons()[0]->setChecked(true); + setCurrentPanel(0); +} + +void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { + panel_widget->setCurrentIndex(index); + nav_btns->buttons()[index]->setChecked(true); + if (!param.isEmpty()) { + emit expandToggleDescription(param); + } } SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { @@ -325,10 +352,13 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { QObject::connect(device, &DevicePanel::reviewTrainingGuide, this, &SettingsWindow::reviewTrainingGuide); QObject::connect(device, &DevicePanel::showDriverView, this, &SettingsWindow::showDriverView); + TogglesPanel *toggles = new TogglesPanel(this); + QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + QList> panels = { {tr("Device"), device}, {tr("Network"), new Networking(this)}, - {tr("Toggles"), new TogglesPanel(this)}, + {tr("Toggles"), toggles}, {tr("Software"), new SoftwarePanel(this)}, }; diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 4177f28cf4..c63be4e138 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -17,6 +17,7 @@ class SettingsWindow : public QFrame { public: explicit SettingsWindow(QWidget *parent = 0); + void setCurrentPanel(int index, const QString ¶m = ""); protected: void showEvent(QShowEvent *event) override; @@ -25,6 +26,7 @@ signals: void closeSettings(); void reviewTrainingGuide(); void showDriverView(); + void expandToggleDescription(const QString ¶m); private: QPushButton *sidebar_alert_widget; @@ -56,6 +58,9 @@ public: explicit TogglesPanel(SettingsWindow *parent); void showEvent(QShowEvent *event) override; +public slots: + void expandToggleDescription(const QString ¶m); + private: Params params; std::map toggles; diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc index e08a02a4d6..12d62e63fb 100644 --- a/selfdrive/ui/qt/offroad/software_settings.cc +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -77,7 +77,7 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { // uninstall button auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); connect(uninstallBtn, &ButtonControl::clicked, [&]() { - if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), tr("Uninstall"), this)) { params.putBool("DoUninstall", true); } }); diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index d3bc931b67..50f891dd56 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -174,6 +174,7 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* par pm = std::make_unique>({"uiDebug"}); engage_img = loadPixmap("../assets/img_chffr_wheel.png", {img_size, img_size}); + experimental_img = loadPixmap("../assets/img_experimental.svg", {img_size - 5, img_size - 5}); dm_img = loadPixmap("../assets/img_driver_face.png", {img_size, img_size}); } @@ -378,8 +379,9 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) { // engage-ability icon if (engageable) { + SubMaster &sm = *(uiState()->sm); drawIcon(p, rect().right() - radius / 2 - bdr_s * 2, radius / 2 + int(bdr_s * 1.5), - engage_img, bg_colors[status], 1.0); + sm["controlsState"].getControlsState().getExperimentalMode() ? experimental_img : engage_img, blackColor(166), 1.0); } // dm icon @@ -404,11 +406,12 @@ void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &t } void AnnotatedCameraWidget::drawIcon(QPainter &p, int x, int y, QPixmap &img, QBrush bg, float opacity) { + p.setOpacity(1.0); // bg dictates opacity of ellipse p.setPen(Qt::NoPen); p.setBrush(bg); p.drawEllipse(x - radius / 2, y - radius / 2, radius, radius); p.setOpacity(opacity); - p.drawPixmap(x - img_size / 2, y - img_size / 2, img); + p.drawPixmap(x - img.size().width() / 2, y - img.size().height() / 2, img); } @@ -445,6 +448,8 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) { painter.save(); const UIScene &scene = s->scene; + SubMaster &sm = *(s->sm); + // lanelines for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) { painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp(scene.lane_line_probs[i], 0.0, 0.7))); @@ -460,15 +465,15 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) { // paint path QLinearGradient bg(0, height(), 0, height() / 4); float start_hue, end_hue; - if (scene.end_to_end_long) { - const auto &acceleration = (*s->sm)["modelV2"].getModelV2().getAcceleration(); + if (sm["controlsState"].getControlsState().getExperimentalMode()) { + const auto &acceleration = sm["modelV2"].getModelV2().getAcceleration(); float acceleration_future = 0; if (acceleration.getZ().size() > 16) { acceleration_future = acceleration.getX()[16]; // 2.5 seconds } start_hue = 60; // speed up: 120, slow down: 0 - end_hue = fmax(fmin(start_hue + acceleration_future * 30, 120), 0); + end_hue = fmax(fmin(start_hue + acceleration_future * 45, 148), 0); // FIXME: painter.drawPolygon can be slow if hue is not rounded end_hue = int(end_hue * 100 + 0.5) / 100; @@ -477,21 +482,9 @@ void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) { bg.setColorAt(0.5, QColor::fromHslF(end_hue / 360., 1.0, 0.68, 0.35)); bg.setColorAt(1.0, QColor::fromHslF(end_hue / 360., 1.0, 0.68, 0.0)); } else { - const auto &orientation = (*s->sm)["modelV2"].getModelV2().getOrientation(); - float orientation_future = 0; - if (orientation.getZ().size() > 16) { - orientation_future = std::abs(orientation.getZ()[16]); // 2.5 seconds - } - start_hue = 148; - // straight: 112, in turns: 70 - end_hue = fmax(70, 112 - (orientation_future * 420)); - - // FIXME: painter.drawPolygon can be slow if hue is not rounded - end_hue = int(end_hue * 100 + 0.5) / 100; - - bg.setColorAt(0.0, QColor::fromHslF(start_hue / 360., 0.94, 0.51, 0.4)); - bg.setColorAt(0.5, QColor::fromHslF(end_hue / 360., 1.0, 0.68, 0.35)); - bg.setColorAt(1.0, QColor::fromHslF(end_hue / 360., 1.0, 0.68, 0.0)); + bg.setColorAt(0.0, QColor::fromHslF(148 / 360., 0.94, 0.51, 0.4)); + bg.setColorAt(0.5, QColor::fromHslF(112 / 360., 1.0, 0.68, 0.35)); + bg.setColorAt(1.0, QColor::fromHslF(112 / 360., 1.0, 0.68, 0.0)); } painter.setBrush(bg); @@ -566,6 +559,7 @@ void AnnotatedCameraWidget::paintGL() { } else if (v_ego > 15) { wide_cam_requested = false; } + wide_cam_requested = wide_cam_requested && sm["controlsState"].getControlsState().getExperimentalMode(); // TODO: also detect when ecam vision stream isn't available // for replay of old routes, never go to widecam wide_cam_requested = wide_cam_requested && s->scene.calibration_wide_valid; diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index 7edca6b3d5..9e18355970 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -51,6 +51,7 @@ private: void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255); QPixmap engage_img; + QPixmap experimental_img; QPixmap dm_img; const int radius = 192; const int img_size = (radius / 2) * 1.5; diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index 53ad7467ac..fb96e1d540 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -20,7 +20,7 @@ public: explicit Sidebar(QWidget* parent = 0); signals: - void openSettings(); + void openSettings(int index = 0, const QString ¶m = ""); void valueChanged(); public slots: diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index a606d6893e..347cdb1dca 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -102,6 +102,7 @@ CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, bool CameraWidget::~CameraWidget() { makeCurrent(); + stopVipcThread(); if (isValid()) { glDeleteVertexArrays(1, &frame_vao); glDeleteBuffers(1, &frame_vbo); @@ -171,6 +172,15 @@ void CameraWidget::showEvent(QShowEvent *event) { } } +void CameraWidget::stopVipcThread() { + if (vipc_thread) { + vipc_thread->requestInterruption(); + vipc_thread->quit(); + vipc_thread->wait(); + vipc_thread = nullptr; + } +} + void CameraWidget::updateFrameMat() { int w = width(), h = height(); diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index 0698d1fb9a..7cc3847f99 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -51,6 +51,7 @@ protected: void updateCalibration(const mat3 &calib); void vipcThread(); void clearFrames(); + void stopVipcThread(); bool zoomed_view; GLuint frame_vao, frame_vbo, frame_ibo; diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index d3b77935df..456cf748f4 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -26,10 +26,10 @@ AbstractControl::AbstractControl(const QString &title, const QString &desc, cons hlayout->setSpacing(20); // left icon + icon_label = new QLabel(); if (!icon.isEmpty()) { - QPixmap pix(icon); - QLabel *icon_label = new QLabel(); - icon_label->setPixmap(pix.scaledToWidth(80, Qt::SmoothTransformation)); + icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); + icon_label->setPixmap(icon_pixmap); icon_label->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); hlayout->addWidget(icon_label); } @@ -38,7 +38,7 @@ AbstractControl::AbstractControl(const QString &title, const QString &desc, cons title_label = new QPushButton(title); title_label->setFixedHeight(120); title_label->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left"); - hlayout->addWidget(title_label); + hlayout->addWidget(title_label, 1); // value next to control button value = new ElidedLabel(); diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index f11f9baf59..770b9b92dd 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -7,6 +7,7 @@ #include #include "common/params.h" +#include "selfdrive/ui/qt/widgets/input.h" #include "selfdrive/ui/qt/widgets/toggle.h" QFrame *horizontal_line(QWidget *parent = nullptr); @@ -49,6 +50,13 @@ public: value->setText(val); } + const QString getDescription() { + return description->text(); + } + + QLabel *icon_label; + QPixmap icon_pixmap; + public slots: void showDescription() { description->setVisible(true); @@ -137,13 +145,35 @@ public: ParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr) : ToggleControl(title, desc, icon, false, parent) { key = param.toStdString(); QObject::connect(this, &ParamControl::toggleFlipped, [=](bool state) { - params.putBool(key, state); + QString content("

" + title + "


" + "

" + getDescription() + "

"); + ConfirmationDialog dialog(content, tr("Enable"), tr("Cancel"), true, this); + + bool confirmed = store_confirm && params.getBool(key + "Confirmed"); + if (!confirm || confirmed || !state || dialog.exec()) { + if (store_confirm && state) params.putBool(key + "Confirmed", true); + params.putBool(key, state); + setIcon(state); + } else { + toggle.togglePosition(); + } }); } + void setConfirmation(bool _confirm, bool _store_confirm) { + confirm = _confirm; + store_confirm = _store_confirm; + }; + + void setActiveIcon(const QString &icon) { + active_icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation); + } + void refresh() { - if (params.getBool(key) != toggle.on) { + bool state = params.getBool(key); + if (state != toggle.on) { toggle.togglePosition(); + setIcon(state); } }; @@ -152,8 +182,19 @@ public: }; private: + void setIcon(bool state) { + if (state && !active_icon_pixmap.isNull()) { + icon_label->setPixmap(active_icon_pixmap); + } else if (!icon_pixmap.isNull()) { + icon_label->setPixmap(icon_pixmap); + } + }; + std::string key; Params params; + QPixmap active_icon_pixmap; + bool confirm = false; + bool store_confirm = false; }; class ListWidget : public QWidget { diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index d703825885..75453e1b90 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -183,17 +183,21 @@ void InputDialog::setMinLength(int length) { // ConfirmationDialog ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, const QString &cancel_text, - QWidget *parent) : QDialogBase(parent) { + const bool rich, QWidget *parent) : QDialogBase(parent) { QFrame *container = new QFrame(this); - container->setStyleSheet("QFrame { border-radius: 0; background-color: #ECECEC; }"); + container->setStyleSheet(R"( + QFrame { background-color: #1B1B1B; color: #C9C9C9; } + #confirm_btn { background-color: #465BEA; } + #confirm_btn:pressed { background-color: #3049F4; } + )"); QVBoxLayout *main_layout = new QVBoxLayout(container); - main_layout->setContentsMargins(32, 120, 32, 32); + main_layout->setContentsMargins(32, rich ? 32 : 120, 32, 32); QLabel *prompt = new QLabel(prompt_text, this); prompt->setWordWrap(true); - prompt->setAlignment(Qt::AlignHCenter); - prompt->setStyleSheet("font-size: 70px; font-weight: bold; color: black;"); - main_layout->addWidget(prompt, 1, Qt::AlignTop | Qt::AlignHCenter); + prompt->setAlignment(rich ? Qt::AlignLeft : Qt::AlignHCenter); + prompt->setStyleSheet((rich ? "font-size: 42px; font-weight: light;" : "font-size: 70px; font-weight: bold;") + QString(" margin: 45px;")); + main_layout->addWidget(rich ? (QWidget*)new ScrollView(prompt, this) : (QWidget*)prompt, 1, Qt::AlignTop); // cancel + confirm buttons QHBoxLayout *btn_layout = new QHBoxLayout(); @@ -208,54 +212,29 @@ ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString if (confirm_text.length()) { QPushButton* confirm_btn = new QPushButton(confirm_text); + confirm_btn->setObjectName("confirm_btn"); btn_layout->addWidget(confirm_btn); QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); } QVBoxLayout *outer_layout = new QVBoxLayout(this); - outer_layout->setContentsMargins(210, 170, 210, 170); + int margin = rich ? 100 : 200; + outer_layout->setContentsMargins(margin, margin, margin, margin); outer_layout->addWidget(container); } bool ConfirmationDialog::alert(const QString &prompt_text, QWidget *parent) { - ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), "", parent); + ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), "", false, parent); return d.exec(); } -bool ConfirmationDialog::confirm(const QString &prompt_text, QWidget *parent) { - ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), tr("Cancel"), parent); +bool ConfirmationDialog::confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent) { + ConfirmationDialog d = ConfirmationDialog(prompt_text, confirm_text, tr("Cancel"), false, parent); return d.exec(); } - -// RichTextDialog - -RichTextDialog::RichTextDialog(const QString &prompt_text, const QString &btn_text, - QWidget *parent) : QDialogBase(parent) { - QFrame *container = new QFrame(this); - container->setStyleSheet("QFrame { background-color: #1B1B1B; }"); - QVBoxLayout *main_layout = new QVBoxLayout(container); - main_layout->setContentsMargins(32, 32, 32, 32); - - QLabel *prompt = new QLabel(prompt_text, this); - prompt->setWordWrap(true); - prompt->setAlignment(Qt::AlignLeft); - prompt->setTextFormat(Qt::RichText); - prompt->setStyleSheet("font-size: 42px; font-weight: light; color: #C9C9C9; margin: 45px;"); - main_layout->addWidget(new ScrollView(prompt, this), 1, Qt::AlignTop); - - // confirm button - QPushButton* confirm_btn = new QPushButton(btn_text); - main_layout->addWidget(confirm_btn); - QObject::connect(confirm_btn, &QPushButton::clicked, this, &QDialog::accept); - - QVBoxLayout *outer_layout = new QVBoxLayout(this); - outer_layout->setContentsMargins(100, 100, 100, 100); - outer_layout->addWidget(container); -} - -bool RichTextDialog::alert(const QString &prompt_text, QWidget *parent) { - auto d = RichTextDialog(prompt_text, tr("Ok"), parent); +bool ConfirmationDialog::rich(const QString &prompt_text, QWidget *parent) { + ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), "", true, parent); return d.exec(); } diff --git a/selfdrive/ui/qt/widgets/input.h b/selfdrive/ui/qt/widgets/input.h index 6c47a31d87..e6c0fba86d 100644 --- a/selfdrive/ui/qt/widgets/input.h +++ b/selfdrive/ui/qt/widgets/input.h @@ -55,18 +55,10 @@ class ConfirmationDialog : public QDialogBase { public: explicit ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, - const QString &cancel_text, QWidget* parent); - static bool alert(const QString &prompt_text, QWidget *parent); - static bool confirm(const QString &prompt_text, QWidget *parent); -}; - -// larger ConfirmationDialog for rich text -class RichTextDialog : public QDialogBase { - Q_OBJECT - -public: - explicit RichTextDialog(const QString &prompt_text, const QString &btn_text, QWidget* parent); + const QString &cancel_text, const bool rich, QWidget* parent); static bool alert(const QString &prompt_text, QWidget *parent); + static bool confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent); + static bool rich(const QString &prompt_text, QWidget *parent); }; class MultiOptionDialog : public QDialogBase { diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc index 04ce15ef23..198b1edbf6 100644 --- a/selfdrive/ui/qt/window.cc +++ b/selfdrive/ui/qt/window.cc @@ -53,6 +53,7 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { QFontDatabase::addApplicationFont("../assets/fonts/Inter-Regular.ttf"); QFontDatabase::addApplicationFont("../assets/fonts/Inter-SemiBold.ttf"); QFontDatabase::addApplicationFont("../assets/fonts/Inter-Thin.ttf"); + QFontDatabase::addApplicationFont("../assets/fonts/JetBrainsMono-Medium.ttf"); // no outline to prevent the focus rectangle setStyleSheet(R"( @@ -64,8 +65,9 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_NoSystemBackground); } -void MainWindow::openSettings() { +void MainWindow::openSettings(int index, const QString ¶m) { main_layout->setCurrentWidget(settingsWindow); + settingsWindow->setCurrentPanel(index, param); } void MainWindow::closeSettings() { diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h index 0bd328aa8a..71fc466c20 100644 --- a/selfdrive/ui/qt/window.h +++ b/selfdrive/ui/qt/window.h @@ -15,7 +15,7 @@ public: private: bool eventFilter(QObject *obj, QEvent *event) override; - void openSettings(); + void openSettings(int index = 0, const QString ¶m = ""); void closeSettings(); Device device; diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index 11ecd30ae0..26d6c39349 100755 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -9,6 +9,7 @@ import xml.etree.ElementTree as ET from selfdrive.ui.update_translations import TRANSLATIONS_DIR, LANGUAGES_FILE, update_translations TMP_TRANSLATIONS_DIR = os.path.join(TRANSLATIONS_DIR, "tmp") +UNFINISHED_TRANSLATION_TAG = "" not in cur_translations, + self.assertTrue(UNFINISHED_TRANSLATION_TAG not in cur_translations, f"{file} ({name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist") def test_vanished_translations(self): diff --git a/selfdrive/ui/translations/create_badges.py b/selfdrive/ui/translations/create_badges.py index 32904f242a..575584dd50 100755 --- a/selfdrive/ui/translations/create_badges.py +++ b/selfdrive/ui/translations/create_badges.py @@ -2,19 +2,22 @@ import json import os import requests +import xml.etree.ElementTree as ET from common.basedir import BASEDIR +from selfdrive.ui.tests.test_translations import UNFINISHED_TRANSLATION_TAG from selfdrive.ui.update_translations import LANGUAGES_FILE, TRANSLATIONS_DIR TRANSLATION_TAG = "'] + badge_svg = [] + max_badge_width = 0 # keep track of max width to set parent element for idx, (name, file) in enumerate(translation_files.items()): with open(os.path.join(TRANSLATIONS_DIR, f"{file}.ts"), "r") as tr_f: tr_file = tr_f.read() @@ -28,19 +31,30 @@ if __name__ == "__main__": unfinished_translations += 1 percent_finished = int(100 - (unfinished_translations / total_translations * 100.)) - color = "green" if percent_finished == 100 else "orange" if percent_finished >= 70 else "red" + color = "green" if percent_finished == 100 else "orange" if percent_finished > 90 else "red" - r = requests.get(f"https://img.shields.io/badge/LANGUAGE {name}-{percent_finished}%25 complete-{color}", timeout=10) + # Download badge + badge_label = f"LANGUAGE {name}" + badge_message = f"{percent_finished}% complete" + if unfinished_translations != 0: + badge_message += f" ({unfinished_translations} unfinished)" + + r = requests.get(f"{SHIELDS_URL}/{badge_label}-{badge_message}-{color}", timeout=10) assert r.status_code == 200, "Error downloading badge" content_svg = r.content.decode("utf-8") - # make tag ids in each badge unique + xml = ET.fromstring(content_svg) + assert "width" in xml.attrib + max_badge_width = max(max_badge_width, int(xml.attrib["width"])) + + # Make tag ids in each badge unique to combine them into one svg for tag in ("r", "s"): content_svg = content_svg.replace(f'id="{tag}"', f'id="{tag}{idx}"') content_svg = content_svg.replace(f'"url(#{tag})"', f'"url(#{tag}{idx})"') badge_svg.extend([f'', content_svg, ""]) + badge_svg.insert(0, f'') badge_svg.append("") with open(os.path.join(BASEDIR, "translation_badge.svg"), "w") as badge_f: diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 3c47e6e8e8..07a84fca08 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -1023,7 +1023,7 @@ location set قم بتحميل البيانات من الكاميرا المواجهة للسائق وساعد في تحسين خوارزمية مراقبة السائق. - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal فك الارتباط على دواسة التسريع @@ -1059,7 +1059,7 @@ location set - Experimental openpilot longitudinal control + Experimental openpilot Longitudinal Control diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index a11333a54d..8226dd59f9 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -60,11 +60,11 @@ Cellular Metered - + 従量制通信設定 Prevent large data uploads when on a metered connection - + 大量のデータのアップロードを防止します。 @@ -238,6 +238,14 @@ Disengage to Power Off openpilot をキャンセルしてシャットダウンができます + + Reset + リセット + + + Review + 確認 + DriveStats @@ -273,6 +281,17 @@ カメラを起動しています + + ExperimentalModeButton + + EXPERIMENTAL MODE ON + 実験モード + + + CHILL MODE ON + チルモード + + InputDialog @@ -463,6 +482,17 @@ location set 「connect.comma.ai」をホーム画面に追加して、アプリのように使うことができます。 + + ParamControl + + Cancel + キャンセル + + + Enable + を有効化 + + PrimeAdWidget @@ -585,13 +615,6 @@ location set 「data」パーティションをマウントできません。「確認」ボタンを押すとデバイスが初期化されます。 - - RichTextDialog - - Ok - OK - - SettingsWindow @@ -850,6 +873,10 @@ location set CHECK 確認 + + Uninstall + アンインストール + SshControl @@ -951,31 +978,11 @@ location set 車内カメラの映像をアップロードし、ドライバー監視システムのアルゴリズムの向上に役立てます。 - 🌮 End-to-end longitudinal (extremely alpha) 🌮 - 🌮 エンドツーエンドのアクセル制御 (超α版) 🌮 - - - Experimental openpilot longitudinal control + Experimental openpilot Longitudinal Control 実験段階のopenpilotによるアクセル制御 - <b>WARNING: openpilot longitudinal control is experimental for this car and will disable AEB.</b> - <b>警告: openpilotによるアクセル制御は実験段階であり、AEBを無効化します。</b> - - - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would. Super experimental. - アクセルとブレーキの制御をopenpilotに任せます。openpilotが人間と同じように運転します。最初期の実験段階です。 - - - openpilot longitudinal control is not currently available for this car. - openpilotによるアクセル制御は、この車では現在利用できません。 - - - Enable experimental longitudinal control to enable this. - ここ機能を使う為には、「実験段階のopenpilotによるアクセル制御」を先に有効化してください。 - - - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal アクセルを踏むと openpilot を中断 @@ -998,6 +1005,46 @@ location set Show map on left side when in split screen view. 分割画面表示の場合、ディスプレイの左側にマップを表示します。 + + Experimental Mode + 実験モード + + + WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). + 警告: この車種でのopenpilotによるアクセル制御は実験段階であり、衝突被害軽減ブレーキ(AEB)を無効化します。 + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. + openpilotはこの車の場合、車に内蔵されているACCを標準で利用します。この機能を有効にすることで実験段階のopenpilotによるアクセル制御を利用できます。実験モードと合わせて利用することをお勧めします。 + + + Experimental mode is currently unavailable on this car, since the car's stock ACC is used for longitudinal control. + この車のACCがアクセル制御を行うため、実験モードを利用することができません。 + + + Enable experimental longitudinal control to allow experimental mode. + 実験段階のopenpilotによるアクセル制御を有効にしてください。 + + + openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + openpilotは標準ではゆっくりとくつろげる運転を提供します。この実験モードを有効にすると、以下のくつろげる段階ではない開発中の機能を利用する事ができます。 + + + 🌮 End-to-End Longitudinal Control 🌮 + 🌮 エンドツーエンドアクセル制御 🌮 + + + Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + openpilotにアクセルとブレーキを任せます。openpilotは赤信号や一時停止サインでの停止を含み、人間と同じように考えて運転を行います。openpilotが運転速度を決定するため、あなたが設定する速度は上限速度になります。この機能は実験段階のため、openpilotの運転ミスに常に備えて注意してください。 + + + New Driving Visualization + 新しい運転画面 + + + The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. + 新しい運転画面では、低速時に広角カメラの映像を表示することで、曲がる際の道路の視覚を向上します。実験段階を表すマークが右上に表示されます。 + Updater @@ -1052,5 +1099,9 @@ location set Forget Wi-Fi Network "%1"? Wi-Fiネットワーク%1を削除してもよろしいですか? + + Forget + 削除 + diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 021243595e..bda64c53ab 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -238,6 +238,14 @@ Disengage to Power Off 전원을 종료하려면 해제하세요 + + Reset + 리셋 + + + Review + 다시보기 + DriveStats @@ -273,6 +281,17 @@ 카메라 시작중 + + ExperimentalModeButton + + EXPERIMENTAL MODE ON + 실험적 모드 사용 + + + CHILL MODE ON + 안정적 모드 사용 + + InputDialog @@ -463,6 +482,17 @@ location set connect.comma.ai을 앱처럼 사용하려면 홈 화면에 바로가기를 만드십시오 + + ParamControl + + Cancel + 취소 + + + Enable + 사용 + + PrimeAdWidget @@ -585,13 +615,6 @@ location set 데이터 파티션을 마운트할 수 없습니다. 확인 버튼을 눌러 장치를 리셋합니다. - - RichTextDialog - - Ok - 확인 - - SettingsWindow @@ -850,6 +873,10 @@ location set CHECK 확인 + + Uninstall + 제거 + SshControl @@ -951,31 +978,11 @@ location set 운전자 카메라에서 데이터를 업로드하고 운전자 모니터링 알고리즘을 개선합니다. - 🌮 End-to-end longitudinal (extremely alpha) 🌮 - 🌮 e2e 롱컨트롤 사용 (매우 실험적) 🌮 - - - Experimental openpilot longitudinal control + Experimental openpilot Longitudinal Control openpilot 롱컨트롤 (실험적) - <b>WARNING: openpilot longitudinal control is experimental for this car and will disable AEB.</b> - <b>경고: openpilot 롱컨트롤은 실험적인 기능으로 차량의 AEB를 비활성화합니다.</b> - - - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would. Super experimental. - 주행모델이 가속과 감속을 제어하도록 하면 openpilot은 운전자가 생각하는것처럼 운전합니다. (매우 실험적) - - - openpilot longitudinal control is not currently available for this car. - 현재 이 차량에는 openpilot 롱컨트롤을 사용할 수 없습니다. - - - Enable experimental longitudinal control to enable this. - openpilot 롱컨트롤을 활성화합니다. (실험적) - - - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal 가속페달 조작시 해제 @@ -998,6 +1005,46 @@ location set Show map on left side when in split screen view. 분할 화면 보기에서 지도를 왼쪽에 표시합니다. + + Experimental Mode + 실험적 모드 + + + WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). + 경고: openpilot 롱컨트롤은 실험적인 기능으로 차량의 자동긴급제동(AEB)를 비활성화합니다. + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. + 이 차량은 openpilot 롱컨트롤 대신 차량의 내장 ACC로 기본 설정됩니다. openpilot 롱컨트롤을 사용하려면 이 옵션을 활성화하세요. 실험적 openpilot 롱컨트롤을 사용하는 경우 실험적 모드를 활성화 하세요. + + + Experimental mode is currently unavailable on this car, since the car's stock ACC is used for longitudinal control. + 차량의 기본 ACC가 롱컨트롤에 사용되기 때문에 현재 이 차량에서는 실험적 모드를 사용할수 없습니다. + + + Enable experimental longitudinal control to allow experimental mode. + 실험적 롱컨트롤을 사용하려면 실험적 모드를 활성화 하세요. + + + openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + openpilot은 기본적으로 <b>안정적 모드</b>로 주행합니다. 실험적 모드는 안정적 모드에 준비되지 않은 <b>알파 수준 기능</b>을 활성화 합니다. 실험 모드의 특징은 아래에 나열되어 있습니다 + + + 🌮 End-to-End Longitudinal Control 🌮 + 🌮 E2E 롱컨트롤 🌮 + + + Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + 주행모델이 가속과 감속을 제어하도록 합니다. openpilot은 신호등과 정지표지판을 보고 멈추는 것을 포함하여 운전자가 생각하는것처럼 주행합니다. 주행 모델이 주행할 속도를 결정하므로 설정된 속도는 상한선으로만 작용합니다. 이것은 알파 기능이므로 사용에 주의해야 합니다. + + + New Driving Visualization + 새로운 주행 시각화 + + + The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. + 주행 시각화는 저속에서 도로를 향하는 광각 카메라로 전환되어 일부 회전을 더 잘 보여줍니다. 실험적 모드 로고도 우측상단에 표시됩니다. + Updater @@ -1052,5 +1099,9 @@ location set Forget Wi-Fi Network "%1"? wifi 네트워크 저장안함 "%1"? + + Forget + 저장안함 + diff --git a/selfdrive/ui/translations/main_nl.ts b/selfdrive/ui/translations/main_nl.ts index 24d2031f31..10651a4160 100644 --- a/selfdrive/ui/translations/main_nl.ts +++ b/selfdrive/ui/translations/main_nl.ts @@ -1007,7 +1007,7 @@ ingesteld Upload gegevens van de bestuurders camera en help het algoritme voor het monitoren van de bestuurder te verbeteren. - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal Deactiveren Met Gaspedaal @@ -1043,7 +1043,7 @@ ingesteld - Experimental openpilot longitudinal control + Experimental openpilot Longitudinal Control diff --git a/selfdrive/ui/translations/main_pl.ts b/selfdrive/ui/translations/main_pl.ts index bdce181375..4f8b03ef50 100644 --- a/selfdrive/ui/translations/main_pl.ts +++ b/selfdrive/ui/translations/main_pl.ts @@ -1011,7 +1011,7 @@ nie zostało ustawione Prześlij dane z kamery skierowanej na kierowcę i pomóż poprawiać algorytm monitorowania kierowcy. - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal Odłącz poprzez naciśnięcie gazu @@ -1047,7 +1047,7 @@ nie zostało ustawione - Experimental openpilot longitudinal control + Experimental openpilot Longitudinal Control diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 2afdaf3388..105d6f77f0 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -238,6 +238,14 @@ Disengage to Power Off Desacione para Desligar + + Reset + Resetar + + + Review + Revisar + DriveStats @@ -273,6 +281,17 @@ câmera iniciando + + ExperimentalModeButton + + EXPERIMENTAL MODE ON + MODO EXPERIMENTAL ATIVADO + + + CHILL MODE ON + MODO CHILL ATIVADO + + InputDialog @@ -464,6 +483,17 @@ trabalho definido Salve connect.comma.ai como sua página inicial para utilizar como um app + + ParamControl + + Cancel + Cancelar + + + Enable + Ativar + + PrimeAdWidget @@ -589,13 +619,6 @@ trabalho definido Não foi possível montar a partição de dados. Pressione confirmar para resetar seu dispositivo. - - RichTextDialog - - Ok - Ok - - SettingsWindow @@ -854,6 +877,10 @@ trabalho definido CHECK VERIFICAR + + Uninstall + Desinstalar + SshControl @@ -955,31 +982,11 @@ trabalho definido Upload dados da câmera voltada para o motorista e ajude a melhorar o algoritmo de monitoramentor. - 🌮 End-to-end longitudinal (extremely alpha) 🌮 - 🌮 End-to-end longitudinal (experimental) 🌮 - - - Experimental openpilot longitudinal control + Experimental openpilot Longitudinal Control Controle longitudinal experimental openpilot - <b>WARNING: openpilot longitudinal control is experimental for this car and will disable AEB.</b> - <b>AVISO: o controle longitudinal openpilot é experimental para este carro e irá desabilitar AEB.</b> - - - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would. Super experimental. - Deixe o modelo controlar o acelerador e os freios. openpilot irá conduzir como pensa que um humano faria. Super experimental. - - - openpilot longitudinal control is not currently available for this car. - controle longitudinal openpilot não está disponível para este carro. - - - Enable experimental longitudinal control to enable this. - Habilite o controle longitudinal experimental para habilitar isso. - - - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal Desacionar Com Pedal Do Acelerador @@ -1002,6 +1009,46 @@ trabalho definido Show map on left side when in split screen view. Exibir mapa do lado esquerdo quando a tela for dividida. + + Experimental Mode + Modo Experimental + + + WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). + ATENÇÃO: o controle longitudinal do openpilot é experimental para este carro e desativará a Frenagem Automática de Emergência (AEB). + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. + Neste carro o penpilot por padrão utiliza o ACC nativo do veículo ao invés de controlar longitudinalmente. Ative isto para mudar para o controle longitudinal do openpilot. Ativar o Modo Experimental é recomendado quando em uso do controle longitudinal experimental do openpilot. + + + Experimental mode is currently unavailable on this car, since the car's stock ACC is used for longitudinal control. + O Modo Experimental está atualmente indisponível para este carro, já que o ACC original do carro é usado para controle longitudinal. + + + Enable experimental longitudinal control to allow experimental mode. + Ative o controle longitudinal experimental para permitir o modo experimental. + + + openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + openpilot por padrão funciona em <b>modo chill</b>. modo Experimental ativa <b>recursos de nível-alfa</b> que não estão prontos para o modo chill. Recursos experimentais estão listados abaixo: + + + 🌮 End-to-End Longitudinal Control 🌮 + 🌮 Controle Longitudinal de Ponta a Ponta 🌮 + + + Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + Deixe o modelo de IA controlar o acelerador e os freios. O openpilot irá dirigir como pensa que um humano faria, incluindo parar em sinais vermelhos e sinais de parada. Uma vez que o modelo de condução decide a velocidade a conduzir, a velocidade definida apenas funcionará como um limite superior. Este é um recurso de qualidade alfa; erros devem ser esperados. + + + New Driving Visualization + Nova Visualização de Condução + + + The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. + A visualização da direção fará a transição para a câmera grande angular voltada para a estrada em baixas velocidades para mostrar melhor algumas curvas. O logotipo do modo Experimental também será exibido no canto superior direito. + Updater @@ -1056,5 +1103,9 @@ trabalho definido Forget Wi-Fi Network "%1"? Esquecer Rede Wi-Fi "%1"? + + Forget + Esquecer + diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 95880e69c9..0de0ba5f9a 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -951,7 +951,7 @@ location set อัปโหลดข้อมูลจากกล้องที่หันหน้าไปทางคนขับ และช่วยปรับปรุงอัลกอริธึมการตรวจสอบผู้ขับขี่ - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal ยกเลิกระบบช่วยขับเมื่อเหยียบคันเร่ง @@ -979,7 +979,7 @@ location set 🌮 ควบคุมเร่ง/เบรคแบบ End-to-end (อยู่ขั้นพัฒนา) 🌮 - Experimental openpilot longitudinal control + Experimental openpilot Longitudinal Control ทดลองใช้ระบบควบคุมการเร่ง/เบรคโดย openpilot diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 9e7c354444..31202e45f2 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -60,11 +60,11 @@ Cellular Metered - + 按流量计费的手机移动网络 Prevent large data uploads when on a metered connection - + 当使用按流量计费的连接时,避免上传大流量数据 @@ -140,7 +140,7 @@ Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - 打开并预览驾驶员摄像头,以确保驾驶员监控具有良好视野。仅熄火时可用。 + 打开并预览驾驶员摄像头,以确保驾驶员监控具有良好视野。(仅熄火时可用) Reset Calibration @@ -204,7 +204,7 @@ Your device is pointed %1° %2 and %3° %4. - 您的设备校准为%1° %2、%3° %4。 + 您的设备校准为%1° %2、%3° %4。 down @@ -238,6 +238,14 @@ Disengage to Power Off 取消openpilot以关机 + + Reset + 重置 + + + Review + 预览 + DriveStats @@ -273,6 +281,17 @@ 正在启动相机 + + ExperimentalModeButton + + EXPERIMENTAL MODE ON + 试验模式运行 + + + CHILL MODE ON + 轻松模式运行 + + InputDialog @@ -368,7 +387,7 @@ Get turn-by-turn directions displayed and more with a comma prime subscription. Sign up now: https://connect.comma.ai - 订阅comma prime以获取导航。 + 订阅comma prime以获取导航。 立即注册:https://connect.comma.ai @@ -461,6 +480,17 @@ location set 将 connect.comma.ai 收藏到您的主屏幕,以便像应用程序一样使用它 + + ParamControl + + Cancel + 取消 + + + Enable + 启用 + + PrimeAdWidget @@ -583,13 +613,6 @@ location set 无法挂载数据分区。 确认以重置您的设备。 - - RichTextDialog - - Ok - 好的 - - SettingsWindow @@ -802,35 +825,35 @@ location set SoftwarePanel Updates are only downloaded while the car is off. - + 车辆熄火时才能下载升级文件。 Current Version - + 当前版本 Download - + 下载 Install Update - + 安装更新 INSTALL - + 安装 Target Branch - + 目标分支 SELECT - + 选择 Select a branch - + 选择分支 UNINSTALL @@ -848,6 +871,10 @@ location set CHECK 查看 + + Uninstall + 卸载 + SshControl @@ -949,31 +976,11 @@ location set 上传驾驶员摄像头的数据,帮助改进驾驶员监控算法。 - 🌮 End-to-end longitudinal (extremely alpha) 🌮 - 🌮 端对端纵向控制(实验性功能) 🌮 + Experimental openpilot Longitudinal Control + 试验性的openpilot纵向控制 - Experimental openpilot longitudinal control - - - - <b>WARNING: openpilot longitudinal control is experimental for this car and will disable AEB.</b> - - - - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would. Super experimental. - 让驾驶模型直接控制油门和刹车,openpilot将会模仿人类司机的驾驶方式。该功能仍非常实验性。 - - - openpilot longitudinal control is not currently available for this car. - - - - Enable experimental longitudinal control to enable this. - - - - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal 踩油门时取消控制 @@ -996,6 +1003,46 @@ location set Show map on left side when in split screen view. 在分屏模式中,将地图置于屏幕左侧。 + + Experimental Mode + 测试模式 + + + WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). + 警告: 此车辆的openpilot纵向控制是试验性功能,且将禁用AEB自动刹车功能。 + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. + 针对此车辆,openpilot默认使用车辆自带的ACC,而非openpilot的纵向控制。启用此选项将切换到openpilot纵向控制。当使用试验性的openpilot纵向控制时,建议同时启用试验模式。 + + + Experimental mode is currently unavailable on this car, since the car's stock ACC is used for longitudinal control. + 由于此车辆使用自带的ACC纵向控制,当前无法使用试验模式。 + + + Enable experimental longitudinal control to allow experimental mode. + 启用试验性的纵向控制,以便允许使用试验模式。 + + + openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + openpilot 默认 <b>轻松模式</b>驾驶车辆。试验模式启用一些轻松模式之外的 <b>试验性功能</b>。试验性功能包括: + + + 🌮 End-to-End Longitudinal Control 🌮 + 🌮 端到端(End-to-End) 纵向控制 🌮 + + + Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + 允许驾驶模型控制加速和制动,openpilot将模仿人类驾驶车辆,包括在红灯和停车让行标识前停车。鉴于驾驶模型确定行驶车速,所设定的车速仅作为上限。此功能尚处于早期测试状态,有可能会出现操作错误。 + + + New Driving Visualization + 新驾驶视角 + + + The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. + 当低速行驶时,驾驶视角将切换到前向广角摄像头,便于更完整地显示转向路径。右上角将显示试验模式图标。 + Updater @@ -1044,11 +1091,15 @@ location set FORGET - 忘记 + 忽略 Forget Wi-Fi Network "%1"? - 忘记WiFi网络 "%1"? + 忽略WiFi网络 "%1"? + + + Forget + 忽略 diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 513135c7f4..0379e926c4 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -60,11 +60,11 @@ Cellular Metered - + 行動網路 Prevent large data uploads when on a metered connection - + 防止使用行動網路上傳大量的數據 @@ -113,7 +113,7 @@ Decline, uninstall %1 - 拒絕並卸載 %1 + 拒絕並解除安裝 %1 @@ -132,7 +132,7 @@ Driver Camera - 駕駛員攝像頭 + 駕駛員監控鏡頭 PREVIEW @@ -238,6 +238,14 @@ Disengage to Power Off 請先取消控車才能關機 + + Reset + 重設 + + + Review + 回顧 + DriveStats @@ -273,6 +281,17 @@ 開啟相機中 + + ExperimentalModeButton + + EXPERIMENTAL MODE ON + 實驗模式 ON + + + CHILL MODE ON + 輕鬆模式 ON + + InputDialog @@ -463,6 +482,17 @@ location set 將 connect.comma.ai 加入您的主屏幕,以便像手機 App 一樣使用它 + + ParamControl + + Cancel + 取消 + + + Enable + 啟用 + + PrimeAdWidget @@ -585,13 +615,6 @@ location set 無法掛載數據分區。請按確認重置您的設備。 - - RichTextDialog - - Ok - 確定 - - SettingsWindow @@ -836,20 +859,24 @@ location set UNINSTALL - 卸載 + 解除安裝 Uninstall %1 - 卸載 %1 + 解除安裝 %1 Are you sure you want to uninstall? - 您確定您要卸載嗎? + 您確定您要解除安裝嗎? CHECK 檢查 + + Uninstall + 解除安裝 + SshControl @@ -951,31 +978,11 @@ location set 上傳駕駛監控的錄像來協助我們提升駕駛監控的準確率。 - 🌮 End-to-end longitudinal (extremely alpha) 🌮 - 🌮 端對端縱向控制(實驗性功能) 🌮 - - - Experimental openpilot longitudinal control + Experimental openpilot Longitudinal Control 使用 openpilot 縱向控制(實驗) - <b>WARNING: openpilot longitudinal control is experimental for this car and will disable AEB.</b> - <b>注意:這台車的 openpilot 縱向控制仍然是實驗中的功能,開啟這功能將會關閉自動緊急煞車 (AEB)。</b> - - - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would. Super experimental. - 讓駕駛模型直接控製油門和剎車,openpilot將會模仿人類司機的駕駛方式。該功能仍非常實驗性。 - - - openpilot longitudinal control is not currently available for this car. - openpilot 縱向控制目前不適用於這輛車。 - - - Enable experimental longitudinal control to enable this. - 打開縱向控制(實驗)以啟用此功能。 - - - Disengage On Accelerator Pedal + Disengage on Accelerator Pedal 油門取消控車 @@ -998,6 +1005,46 @@ location set Show map on left side when in split screen view. 進入分割畫面後,地圖將會顯示在畫面的左側。 + + Experimental Mode + 實驗模式 + + + WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). + 警告:openpilot 縱向控制在這輛車上仍屬實驗性質,啟用後會喪失自動緊急煞車 (AEB) 功能。 + + + On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when using experimental openpilot longitudinal control. + 在本車輛中,openpilot預設將使用原車內建的ACC系統,而非openpilot縱向控制。開啟此開關來啟用openpilot縱向控制,使用此選項時建議一併啟用實驗模式。 + + + Experimental mode is currently unavailable on this car, since the car's stock ACC is used for longitudinal control. + 因車輛使用內建ACC系統,無法在本車輛上啟動實驗模式。 + + + Enable experimental longitudinal control to allow experimental mode. + 啟用實驗性縱向控制以使用實驗模式。 + + + openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + openpilot 預設以 <b>輕鬆模式</b> 駕駛。 實驗模式啟用了尚未準備好進入輕鬆模式的 <b>alpha 級功能</b>。實驗功能如下: + + + 🌮 End-to-End Longitudinal Control 🌮 + 🌮端到端縱向控制🌮 + + + Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + 讓駕駛模型來控制油門及煞車。openpilot將會模擬人類的駕駛行為,包含在看見紅燈及停止標示時停車。由於車速將由駕駛模型決定,因此您設定的時速將成為速度上限。本功能仍在早期實驗階段,請預期模型有犯錯的可能性。 + + + New Driving Visualization + 新的駕駛視覺介面 + + + The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. + 低速行駛時,將會切換成路側廣角鏡頭,以完整顯示轉彎路徑,右上角將出現實驗模式圖案。 + Updater @@ -1052,5 +1099,9 @@ location set Forget Wi-Fi Network "%1"? 清除 Wi-Fi 網路 "%1"? + + Forget + 清除 + diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 821064f81f..2d4533afe1 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -63,6 +63,8 @@ void update_line_data(const UIState *s, const cereal::ModelDataV2::XYZTData::Rea right_points.reserve(max_idx + 1); for (int i = 0; i <= max_idx; i++) { + // highly negative x positions are drawn above the frame and cause flickering, clip to zy plane of camera + if (line_x[i] < 0) continue; QPointF left, right; bool l = calib_frame_to_full_frame(s, line_x[i], line_y[i] - y_off, line_z[i] + z_off, &left); bool r = calib_frame_to_full_frame(s, line_x[i], line_y[i] + y_off, line_z[i] + z_off, &right); @@ -172,7 +174,6 @@ void ui_update_params(UIState *s) { auto params = Params(); s->scene.is_metric = params.getBool("IsMetric"); s->scene.map_on_left = params.getBool("NavSettingLeftSide"); - s->scene.end_to_end_long = params.getBool("EndToEndLong"); } void UIState::updateStatus() { diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index e550afd5f2..d6f5c3e2e0 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -104,7 +104,7 @@ typedef struct UIScene { QPointF lead_vertices[2]; float light_sensor; - bool started, ignition, is_metric, map_on_left, longitudinal_control, end_to_end_long; + bool started, ignition, is_metric, map_on_left, longitudinal_control; uint64_t started_frame; } UIScene; diff --git a/selfdrive/updated.py b/selfdrive/updated.py index c261a92a84..9da2a05a11 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -175,7 +175,7 @@ def finalize_update() -> None: shutil.copytree(OVERLAY_MERGED, FINALIZED, symlinks=True) run(["git", "reset", "--hard"], FINALIZED) - run(["git", "submodule", "foreach", "--recursive", "git", "reset"], FINALIZED) + run(["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], FINALIZED) cloudlog.info("Starting git cleanup in finalized update") t = time.monotonic() @@ -374,8 +374,8 @@ class Updater: ["git", "reset", "--hard"], ["git", "clean", "-xdff"], ["git", "submodule", "sync"], - ["git", "submodule", "init"], - ["git", "submodule", "update"], + ["git", "submodule", "update", "--init", "--recursive"], + ["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], ] r = [run(cmd, OVERLAY_MERGED) for cmd in cmds] cloudlog.info("git reset success: %s", '\n'.join(r)) diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc index 78848af5be..2ee06e372a 100644 --- a/system/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -80,30 +80,26 @@ const float sensor_analog_gains_AR0231[] = { 7.0/2.0, 8.0/2.0, 8.0/1.0}; // 12, 13, 14, 15 = bypass const float sensor_analog_gains_OX03C10[] = { - 1.0, 1.125, 1.25, 1.3125, 1.5625, - 1.6875, 2.0, 2.25, 2.625, 3.125, - 3.625, 4.0, 4.5, 5.0, 5.5, - 6.0, 6.5, 7.0, 7.5, 8.0, - 8.5, 9.0, 9.5, 10.0, 10.5, - 11.0, 11.5, 12.0, 12.5, 13.0, - 13.5, 14.0, 14.5, 15.0, 15.5}; + 1.0, 1.0625, 1.125, 1.1875, 1.25, 1.3125, 1.375, 1.4375, 1.5, 1.5625, 1.6875, + 1.8125, 1.9375, 2.0, 2.125, 2.25, 2.375, 2.5, 2.625, 2.75, 2.875, 3.0, + 3.125, 3.375, 3.625, 3.875, 4.0, 4.25, 4.5, 4.75, 5.0, 5.25, 5.5, + 5.75, 6.0, 6.25, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, + 10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.5, 14.0, 14.5, 15.0, 15.5}; const uint32_t ox03c10_analog_gains_reg[] = { - 0x100, 0x120, 0x140, 0x150, 0x190, - 0x1B0, 0x200, 0x240, 0x2A0, 0x320, - 0x3A0, 0x400, 0x480, 0x500, 0x580, - 0x600, 0x680, 0x700, 0x780, 0x800, - 0x880, 0x900, 0x980, 0xA00, 0xA80, - 0xB00, 0xB80, 0xC00, 0xC80, 0xD00, - 0xD80, 0xE00, 0xE80, 0xF00, 0xF80}; + 0x100, 0x110, 0x120, 0x130, 0x140, 0x150, 0x160, 0x170, 0x180, 0x190, 0x1B0, + 0x1D0, 0x1F0, 0x200, 0x220, 0x240, 0x260, 0x280, 0x2A0, 0x2C0, 0x2E0, 0x300, + 0x320, 0x360, 0x3A0, 0x3E0, 0x400, 0x440, 0x480, 0x4C0, 0x500, 0x540, 0x580, + 0x5C0, 0x600, 0x640, 0x680, 0x700, 0x780, 0x800, 0x880, 0x900, 0x980, 0xA00, + 0xA80, 0xB00, 0xB80, 0xC00, 0xC80, 0xD00, 0xD80, 0xE00, 0xE80, 0xF00, 0xF80}; const int ANALOG_GAIN_MIN_IDX_AR0231 = 0x1; // 0.25x const int ANALOG_GAIN_REC_IDX_AR0231 = 0x6; // 0.8x const int ANALOG_GAIN_MAX_IDX_AR0231 = 0xD; // 4.0x const int ANALOG_GAIN_MIN_IDX_OX03C10 = 0x0; -const int ANALOG_GAIN_REC_IDX_OX03C10 = 0x6; // 2x -const int ANALOG_GAIN_MAX_IDX_OX03C10 = 0x22; +const int ANALOG_GAIN_REC_IDX_OX03C10 = 0x11; // 2.5x +const int ANALOG_GAIN_MAX_IDX_OX03C10 = 0x36; const int EXPOSURE_TIME_MIN_AR0231 = 2; // with HDR, fastest ss const int EXPOSURE_TIME_MAX_AR0231 = 0x0855; // with HDR, slowest ss, 40ms diff --git a/system/camerad/cameras/camera_qcom2.h b/system/camerad/cameras/camera_qcom2.h index 5023c82458..1e25e605c5 100644 --- a/system/camerad/cameras/camera_qcom2.h +++ b/system/camerad/cameras/camera_qcom2.h @@ -12,6 +12,7 @@ #include "common/util.h" #define FRAME_BUF_COUNT 4 +#define ANALOG_GAIN_MAX_CNT 55 class CameraState { public: @@ -36,7 +37,7 @@ public: float dc_gain_on_grey; float dc_gain_off_grey; - float sensor_analog_gains[35]; + float sensor_analog_gains[ANALOG_GAIN_MAX_CNT]; int analog_gain_min_idx; int analog_gain_max_idx; int analog_gain_rec_idx; diff --git a/system/camerad/test/test_camerad.py b/system/camerad/test/test_camerad.py index 378d4b7058..6c2ef1c7bc 100755 --- a/system/camerad/test/test_camerad.py +++ b/system/camerad/test/test_camerad.py @@ -4,12 +4,14 @@ import unittest from collections import defaultdict import cereal.messaging as messaging +from cereal import log from cereal.services import service_list from selfdrive.manager.process_config import managed_processes from system.hardware import TICI TEST_TIMESPAN = 30 -LAG_FRAME_TOLERANCE = 0.5 # ms +LAG_FRAME_TOLERANCE = {log.FrameData.ImageSensor.ar0321: 0.5, # ARs use synced pulses for frame starts + log.FrameData.ImageSensor.ox03c10: 1.0} # OXs react to out-of-sync at next frame CAMERAS = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') @@ -62,15 +64,21 @@ class TestCamerad(unittest.TestCase): assert len(skips) == 0, f"Found frame skips, missing cameras for the following frames: {skips}" def test_frame_sync(self): + sensor_type = [getattr(msgs[0], msgs[0].which()).sensor for frame_id, msgs in self.log_by_frame_id.items()][0].raw frame_times = {frame_id: [getattr(m, m.which()).timestampSof for m in msgs] for frame_id, msgs in self.log_by_frame_id.items()} diffs = {frame_id: (max(ts) - min(ts))/1e6 for frame_id, ts in frame_times.items()} - def get_desc(fid, diff): cam_times = [(m.which(), getattr(m, m.which()).timestampSof/1e6) for m in self.log_by_frame_id[fid]] - return f"{diff=} {cam_times=}" - laggy_frames = {k: get_desc(k, v) for k, v in diffs.items() if v > LAG_FRAME_TOLERANCE} - assert len(laggy_frames) == 0, f"Frames not synced properly: {laggy_frames=}" + return (diff, cam_times) + laggy_frames = {k: get_desc(k, v) for k, v in diffs.items() if v > LAG_FRAME_TOLERANCE[sensor_type]} + + def in_tol(diff): + return 50 - LAG_FRAME_TOLERANCE[sensor_type] < diff and diff < 50 + LAG_FRAME_TOLERANCE[sensor_type] + if len(laggy_frames) != 0 and all( in_tol(laggy_frames[lf][0]) for lf in laggy_frames): + print("TODO: handle camera out of sync") + else: + assert len(laggy_frames) == 0, f"Frames not synced properly: {laggy_frames=}" if __name__ == "__main__": unittest.main() diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 8534c8a978..7876b1af1f 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,9 +1,9 @@ [ { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-57626d7737ab2fa1318e8707a202b1295b5da79ad2fa0a36377cc9481ad0d136.img.xz", - "hash": "57626d7737ab2fa1318e8707a202b1295b5da79ad2fa0a36377cc9481ad0d136", - "hash_raw": "57626d7737ab2fa1318e8707a202b1295b5da79ad2fa0a36377cc9481ad0d136", + "url": "https://commadist.azureedge.net/agnosupdate/boot-72662ec5d586c7a22659a1c8b140932d5472914176020fe76ba4204edbbb214a.img.xz", + "hash": "72662ec5d586c7a22659a1c8b140932d5472914176020fe76ba4204edbbb214a", + "hash_raw": "72662ec5d586c7a22659a1c8b140932d5472914176020fe76ba4204edbbb214a", "size": 14780416, "sparse": false, "full_check": true, @@ -41,9 +41,9 @@ }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-b40b08912576bb972907acba7c201c1399395cbc0cba06ce6e5e3f70ab565cb5.img.xz", - "hash": "6e8fbcc21a265f7f58062abce7675dc05540e2b60cee2df56992a151ba64936f", - "hash_raw": "b40b08912576bb972907acba7c201c1399395cbc0cba06ce6e5e3f70ab565cb5", + "url": "https://commadist.azureedge.net/agnosupdate/system-9efdc9368f05e06008a7a1dbbee21b564e89988dc94d6ddee3a3a88e42268f0e.img.xz", + "hash": "48209ce7e8cc2fff4ec024f0cd82fc2e3e097b5c0629be2b292acf64e6701449", + "hash_raw": "9efdc9368f05e06008a7a1dbbee21b564e89988dc94d6ddee3a3a88e42268f0e", "size": 10737418240, "sparse": true, "full_check": false, diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index e2fd20c1be..b5f5e00410 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -416,6 +416,7 @@ class Tici(HardwareBase): # *** IRQ config *** affine_irq(5, 565) # kgsl-3d0 + affine_irq(4, 126) # SPI goes on boardd core affine_irq(4, 740) # xhci-hcd:usb1 goes on the boardd core affine_irq(4, 1069) # xhci-hcd:usb3 goes on the boardd core for irq in range(237, 246): diff --git a/system/version.py b/system/version.py index f0817b3a9f..6031531556 100644 --- a/system/version.py +++ b/system/version.py @@ -7,7 +7,8 @@ from functools import lru_cache from common.basedir import BASEDIR from system.swaglog import cloudlog -TESTED_BRANCHES = ['devel', 'release3-staging', 'dashcam3-staging', 'release3', 'dashcam3'] +RELEASE_BRANCHES = ['release3-staging', 'dashcam3-staging', 'release3', 'dashcam3'] +TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging'] training_version: bytes = b"0.2.0" terms_version: bytes = b"2" @@ -96,6 +97,9 @@ def is_comma_remote() -> bool: def is_tested_branch() -> bool: return get_short_branch() in TESTED_BRANCHES +@cache +def is_release_branch() -> bool: + return get_short_branch() in RELEASE_BRANCHES @cache def is_dirty() -> bool: diff --git a/tools/cabana/README.md b/tools/cabana/README.md index 99d0d4c9ce..dd131880a6 100644 --- a/tools/cabana/README.md +++ b/tools/cabana/README.md @@ -8,7 +8,7 @@ Cabana is a tool developed to view raw CAN data. One use for this is creating an ```bash $ ./cabana -h -Usage: ./_cabana [options] route +Usage: ./cabana [options] route Options: -h, --help Displays this help. diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index b7321e1f8d..3ff4862800 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -19,7 +19,7 @@ prev_moc_path = cabana_env['QT_MOCHPREFIX'] cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_' cabana_env.Execute('./generate_dbc_json.py --out car_fingerprint_to_dbc.json') cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', - 'canmessages.cc', 'messageswidget.cc', 'settings.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) + 'canmessages.cc', 'commands.cc', 'messageswidget.cc', 'settings.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('_cabana', ['cabana.cc', cabana_lib], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if GetOption('test'): diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index 875bd034ec..dae4591976 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -11,7 +11,11 @@ // BinaryView -const int CELL_HEIGHT = 30; +const int CELL_HEIGHT = 26; + +inline int get_bit_index(const QModelIndex &index, bool little_endian) { + return index.row() * 8 + (little_endian ? 7 - index.column() : index.column()); +} BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { model = new BinaryViewModel(this); @@ -19,11 +23,13 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { delegate = new BinaryItemDelegate(this); setItemDelegate(delegate); horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + verticalHeader()->setSectionsClickable(false); horizontalHeader()->hide(); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); setFrameShape(QFrame::NoFrame); + setShowGrid(false); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); setMouseTracking(true); } @@ -37,33 +43,35 @@ void BinaryView::highlight(const Signal *sig) { } void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) { - QModelIndex tl = indexAt({qMin(rect.left(), rect.right()), qMin(rect.top(), rect.bottom())}); - QModelIndex br = indexAt({qMax(rect.left(), rect.right()), qMax(rect.top(), rect.bottom())}); - if (!tl.isValid() || !br.isValid()) + auto index = indexAt(viewport()->mapFromGlobal(QCursor::pos())); + if (!anchor_index.isValid() || !index.isValid()) return; - if (tl < anchor_index) { - br = anchor_index; - } else if (anchor_index < br) { - tl = anchor_index; - } QItemSelection selection; - for (int row = tl.row(); row <= br.row(); ++row) { - int left_col = (row == tl.row()) ? tl.column() : 0; - int right_col = (row == br.row()) ? br.column() : 7; - selection.merge({model->index(row, left_col), model->index(row, right_col)}, flags); + auto [start, size, is_lb] = getSelection(index); + for (int i = start; i < start + size; ++i) { + auto idx = model->bitIndex(i, is_lb); + selection.merge({idx, idx}, flags); } selectionModel()->select(selection, flags); } void BinaryView::mousePressEvent(QMouseEvent *event) { delegate->setSelectionColor(style()->standardPalette().color(QPalette::Active, QPalette::Highlight)); - anchor_index = indexAt(event->pos()); - if (getResizingSignal() != nullptr) { + if (auto index = indexAt(event->pos()); index.isValid() && index.column() != 8) { + anchor_index = index; auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); - delegate->setSelectionColor(item->bg_color); + int bit_idx = get_bit_index(anchor_index, true); + for (auto s : item->sigs) { + if (bit_idx == s->lsb || bit_idx == s->msb) { + anchor_index = model->bitIndex(bit_idx == s->lsb ? s->msb : s->lsb, true); + resize_sig = s; + delegate->setSelectionColor(item->bg_color); + break; + } + } } - QTableView::mousePressEvent(event); + event->accept(); } void BinaryView::mouseMoveEvent(QMouseEvent *event) { @@ -80,23 +88,21 @@ void BinaryView::mouseMoveEvent(QMouseEvent *event) { void BinaryView::mouseReleaseEvent(QMouseEvent *event) { QTableView::mouseReleaseEvent(event); - if (auto indexes = selectedIndexes(); !indexes.isEmpty()) { - int from = indexes.first().row() * 8 + indexes.first().column(); - int to = indexes.back().row() * 8 + indexes.back().column(); - if (auto sig = getResizingSignal()) { - auto [sig_from, sig_to] = getSignalRange(sig); - if (from >= sig_from && to <= sig_to) { // reduce size - emit(from == sig_from ? resizeSignal(sig, std::min(to + 1, sig_to), sig_to) - : resizeSignal(sig, sig_from, std::max(from - 1, sig_from))); - } else { // increase size - emit resizeSignal(sig, std::min(from, sig_from), std::max(to, sig_to)); - } + auto release_index = indexAt(event->pos()); + if (release_index.isValid() && anchor_index.isValid()) { + if (selectionModel()->hasSelection()) { + auto [start_bit, size, is_lb] = getSelection(release_index); + resize_sig ? emit resizeSignal(resize_sig, start_bit, size) + : emit addSignal(start_bit, size, is_lb); } else { - emit addSignal(from, to); + auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); + if (item && item->sigs.size() > 0) + emit signalClicked(item->sigs.back()); } - clearSelection(); } + clearSelection(); anchor_index = QModelIndex(); + resize_sig = nullptr; } void BinaryView::leaveEvent(QEvent *event) { @@ -105,93 +111,69 @@ void BinaryView::leaveEvent(QEvent *event) { } void BinaryView::setMessage(const QString &message_id) { - msg_id = message_id; model->setMessage(message_id); clearSelection(); + anchor_index = QModelIndex(); + resize_sig = nullptr; + hovered_sig = nullptr; updateState(); - updateGeometry(); -} - -void BinaryView::updateState() { - model->updateState(); -} - -const Signal *BinaryView::getResizingSignal() const { - if (anchor_index.isValid()) { - auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); - if (item && item->sigs.size() > 0) { - int archor_pos = anchor_index.row() * 8 + anchor_index.column(); - for (auto s : item->sigs) { - auto [sig_from, sig_to] = getSignalRange(s); - if (archor_pos == sig_from || archor_pos == sig_to) - return s; - } - } - } - return nullptr; } QSet BinaryView::getOverlappingSignals() const { QSet overlapping; - for (int i = 0; i < model->rowCount(); ++i) { - for (int j = 0; j < model->columnCount() - 1; ++j) { - auto item = (const BinaryViewModel::Item *)model->index(i, j).internalPointer(); - if (item && item->sigs.size() > 1) { - for (auto s : item->sigs) - overlapping.insert(s); - } - } + for (auto &item : model->items) { + if (item.sigs.size() > 1) + for (auto s : item.sigs) overlapping += s; } return overlapping; } +std::tuple BinaryView::getSelection(QModelIndex index) { + if (index.column() == 8) { + index = model->index(index.row(), 7); + } + bool is_lb = (resize_sig && resize_sig->is_little_endian) || (!resize_sig && index < anchor_index); + int cur_bit_idx = get_bit_index(index, is_lb); + int anchor_bit_idx = get_bit_index(anchor_index, is_lb); + auto [start_bit, end_bit] = std::minmax(cur_bit_idx, anchor_bit_idx); + return {start_bit, end_bit - start_bit + 1, is_lb}; +} + // BinaryViewModel void BinaryViewModel::setMessage(const QString &message_id) { - msg_id = message_id; - beginResetModel(); + msg_id = message_id; items.clear(); - row_count = 0; - - dbc_msg = dbc()->msg(msg_id); - if (dbc_msg) { + if ((dbc_msg = dbc()->msg(msg_id))) { row_count = dbc_msg->size; items.resize(row_count * column_count); - for (int i = 0; i < dbc_msg->sigs.size(); ++i) { - const auto &sig = dbc_msg->sigs[i]; - auto [start, end] = getSignalRange(&sig); + int i = 0; + for (auto sig : dbc_msg->getSignals()) { + auto [start, end] = getSignalRange(sig); for (int j = start; j <= end; ++j) { - int idx = column_count * (j / 8) + j % 8; + int bit_index = sig->is_little_endian ? bigEndianBitIndex(j) : j; + int idx = column_count * (bit_index / 8) + bit_index % 8; if (idx >= items.size()) { - qWarning() << "signal " << sig.name.c_str() << "out of bounds.start_bit:" << sig.start_bit << "size:" << sig.size; + qWarning() << "signal " << sig->name.c_str() << "out of bounds.start_bit:" << sig->start_bit << "size:" << sig->size; break; } - if (j == start) { - sig.is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true; - } else if (j == end) { - sig.is_little_endian ? items[idx].is_msb = true : items[idx].is_lsb = true; - } + if (j == start) sig->is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true; + if (j == end) sig->is_little_endian ? items[idx].is_msb = true : items[idx].is_lsb = true; items[idx].bg_color = getColor(i); - items[idx].sigs.push_back(&dbc_msg->sigs[i]); + items[idx].sigs.push_back(sig); } + ++i; } + } else { + row_count = can->lastMessage(msg_id).dat.size(); + items.resize(row_count * column_count); } - endResetModel(); } -QModelIndex BinaryViewModel::index(int row, int column, const QModelIndex &parent) const { - return createIndex(row, column, (void *)&items[row * column_count + column]); -} - -Qt::ItemFlags BinaryViewModel::flags(const QModelIndex &index) const { - return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable; -} - void BinaryViewModel::updateState() { auto prev_items = items; - const auto &binary = can->lastMessage(msg_id).dat; // data size may changed. if (!dbc_msg && binary.size() != row_count) { @@ -201,11 +183,10 @@ void BinaryViewModel::updateState() { items.resize(row_count * column_count); endResetModel(); } - char hex[3] = {'\0'}; for (int i = 0; i < std::min(binary.size(), row_count); ++i) { for (int j = 0; j < column_count - 1; ++j) { - items[i * column_count + j].val = QChar((binary[i] >> (7 - j)) & 1 ? '1' : '0'); + items[i * column_count + j].val = ((binary[i] >> (7 - j)) & 1) != 0 ? '1' : '0'; } hex[0] = toHex(binary[i] >> 4); hex[1] = toHex(binary[i] & 0xf); @@ -251,19 +232,17 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op BinaryView *bin_view = (BinaryView *)parent(); painter->save(); - bool hover = std::find_if(item->sigs.begin(), item->sigs.end(), [=](auto s) { return s == bin_view->hoveredSignal(); }) != - item->sigs.end(); // background - QColor bg_color = hover ? hoverColor(item->bg_color) : item->bg_color; if (option.state & QStyle::State_Selected) { - bg_color = selection_color; + painter->fillRect(option.rect, selection_color); + } else if (!bin_view->selectionModel()->hasSelection() || !item->sigs.contains(bin_view->resize_sig)) { + painter->fillRect(option.rect, item->bg_color); } - painter->fillRect(option.rect, bg_color); // text - if (index.column() == 8) { // hex column + if (index.column() == 8) { // hex column painter->setFont(hex_font); - } else if (hover) { + } else if (option.state & QStyle::State_Selected || (!bin_view->resize_sig && item->sigs.contains(bin_view->hovered_sig))) { painter->setPen(Qt::white); } painter->drawText(option.rect, Qt::AlignCenter, item->val); diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h index 0f58e9ed20..0ea111d917 100644 --- a/tools/cabana/binaryview.h +++ b/tools/cabana/binaryview.h @@ -28,12 +28,17 @@ public: BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {} void setMessage(const QString &message_id); void updateState(); - Qt::ItemFlags flags(const QModelIndex &index) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { return {}; } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; } + inline QModelIndex bitIndex(int bit, bool is_lb) const { return index(bit / 8, is_lb ? (7 - bit % 8) : bit % 8); } + 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; + } struct Item { QColor bg_color = QColor(Qt::white); @@ -42,13 +47,13 @@ public: QString val = "0"; QList sigs; }; + std::vector items; private: QString msg_id; - const Msg *dbc_msg; + const DBCMsg *dbc_msg; int row_count = 0; const int column_count = 9; - std::vector items; }; class BinaryView : public QTableView { @@ -57,27 +62,28 @@ class BinaryView : public QTableView { public: BinaryView(QWidget *parent = nullptr); void setMessage(const QString &message_id); - void updateState(); void highlight(const Signal *sig); - const Signal *hoveredSignal() const { return hovered_sig; } QSet getOverlappingSignals() const; + inline void updateState() { model->updateState(); } signals: + void signalClicked(const Signal *sig); void signalHovered(const Signal *sig); - void addSignal(int from, int size); + void addSignal(int start_bit, int size, bool little_endian); void resizeSignal(const Signal *sig, int from, int size); private: + std::tuple getSelection(QModelIndex index); void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void leaveEvent(QEvent *event) override; - const Signal *getResizingSignal() const; - QString msg_id; QModelIndex anchor_index; BinaryViewModel *model; BinaryItemDelegate *delegate; + const Signal *resize_sig = nullptr; const Signal *hovered_sig = nullptr; + friend class BinaryItemDelegate; }; diff --git a/tools/cabana/canmessages.cc b/tools/cabana/canmessages.cc index 3ffc29916f..a8e881c5fe 100644 --- a/tools/cabana/canmessages.cc +++ b/tools/cabana/canmessages.cc @@ -1,6 +1,5 @@ #include "tools/cabana/canmessages.h" -#include #include #include "tools/cabana/dbcmanager.h" @@ -9,7 +8,6 @@ CANMessages *can = nullptr; CANMessages::CANMessages(QObject *parent) : QObject(parent) { can = this; - QObject::connect(this, &CANMessages::received, this, &CANMessages::process, Qt::QueuedConnection); QObject::connect(&settings, &Settings::changed, this, &CANMessages::settingChanged); } @@ -24,11 +22,11 @@ static bool event_filter(const Event *e, void *opaque) { } bool CANMessages::loadRoute(const QString &route, const QString &data_dir, bool use_qcam) { - routeName = route; replay = new Replay(route, {"can", "roadEncodeIdx", "carParams"}, {}, nullptr, use_qcam ? REPLAY_FLAG_QCAMERA : 0, data_dir, this); replay->setSegmentCacheLimit(settings.cached_segment_limit); replay->installEventFilter(event_filter, this); QObject::connect(replay, &Replay::segmentsMerged, this, &CANMessages::eventsMerged); + QObject::connect(replay, &Replay::streamStarted, this, &CANMessages::streamStarted); if (replay->load()) { replay->start(); return true; @@ -40,16 +38,13 @@ QList CANMessages::findSignalValues(const QString &id, const Signal *si auto evts = events(); if (!evts) return {}; - auto l = id.split(':'); - int bus = l[0].toInt(); - uint32_t address = l[1].toUInt(nullptr, 16); - QList ret; ret.reserve(max_count); + auto [bus, address] = DBCManager::parseId(id); for (auto &evt : *evts) { if (evt->which != cereal::Event::Which::CAN) continue; - for (auto c : evt->event.getCan()) { + for (const auto &c : evt->event.getCan()) { if (bus == c.getSrc() && address == c.getAddress()) { double val = get_raw_value((uint8_t *)c.getDat().begin(), c.getDat().size(), *signal); if ((flag == EQ && val == value) || (flag == LT && val < value) || (flag == GT && val > value)) { @@ -67,8 +62,10 @@ void CANMessages::process(QHash *messages) { for (auto it = messages->begin(); it != messages->end(); ++it) { can_msgs[it.key()] = it.value(); } - delete messages; emit updated(); + emit msgsReceived(messages); + delete messages; + processing = false; } bool CANMessages::eventFilter(const Event *event) { @@ -82,7 +79,7 @@ bool CANMessages::eventFilter(const Event *event) { } double current_sec = replay->currentSeconds(); - if (counters_begin_sec == 0) { + if (counters_begin_sec == 0 || counters_begin_sec >= current_sec) { counters.clear(); counters_begin_sec = current_sec; } @@ -98,19 +95,19 @@ bool CANMessages::eventFilter(const Event *event) { } CanData &data = list.emplace_front(); data.ts = current_sec; - data.bus_time = c.getBusTime(); data.dat.append((char *)c.getDat().begin(), c.getDat().size()); - auto &count = counters[id]; - data.count = ++count; + data.count = ++counters[id]; if (double delta = (current_sec - counters_begin_sec); delta > 0) { - data.freq = count / delta; + data.freq = data.count / delta; } (*new_msgs)[id] = data; } double ts = millis_since_boot(); - if ((ts - prev_update_ts) > (1000.0 / settings.fps)) { + if ((ts - prev_update_ts) > (1000.0 / settings.fps) && !processing) { + // delay posting CAN message if UI thread is busy + processing = true; prev_update_ts = ts; // use pointer to avoid data copy in queued connection. emit received(new_msgs.release()); @@ -125,8 +122,9 @@ const std::deque CANMessages::messages(const QString &id) { } void CANMessages::seekTo(double ts) { - replay->seekTo(ts, false); + replay->seekTo(std::max(double(0), ts), false); counters_begin_sec = 0; + emit updated(); } void CANMessages::settingChanged() { diff --git a/tools/cabana/canmessages.h b/tools/cabana/canmessages.h index 5fbccdbe12..4cb0f403a0 100644 --- a/tools/cabana/canmessages.h +++ b/tools/cabana/canmessages.h @@ -16,7 +16,6 @@ struct CanData { double ts = 0.; uint32_t count = 0; uint32_t freq = 0; - uint16_t bus_time = 0; QByteArray dat; }; @@ -32,7 +31,7 @@ public: QList findSignalValues(const QString&id, const Signal* signal, double value, FindFlags flag, int max_count); bool eventFilter(const Event *event); - inline QString route() const { return routeName; } + inline QString route() const { return replay->route()->name(); } inline QString carFingerprint() const { return replay->carFingerprint().c_str(); } inline double totalSeconds() const { return replay->totalSeconds(); } inline double routeStartTime() const { return replay->routeStartTime() / (double)1e9; } @@ -47,8 +46,10 @@ public: inline const std::vector> getTimeline() { return replay->getTimeline(); } signals: + void streamStarted(); void eventsMerged(); void updated(); + void msgsReceived(const QHash *); void received(QHash *); public: @@ -58,11 +59,10 @@ protected: void process(QHash *); void settingChanged(); - QString routeName; Replay *replay = nullptr; - std::mutex lock; std::atomic counters_begin_sec = 0; + std::atomic processing = false; QHash counters; QHash> received_msgs; }; @@ -80,11 +80,5 @@ inline const QString &getColor(int i) { return SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)]; } -inline QColor hoverColor(const QColor &color) { - QColor c = color.convertTo(QColor::Hsv); - c.setHsv(color.hue(), 180, 180); - return c; -} - // A global pointer referring to the unique CANMessages object extern CANMessages *can; diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc index 5bf66ebb2d..5d530a8c8a 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chartswidget.cc @@ -2,11 +2,10 @@ #include #include -#include #include #include -#include -#include +#include +#include #include // ChartsWidget @@ -15,41 +14,24 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); - // title bar - title_bar = new QWidget(this); - title_bar->setVisible(false); - QHBoxLayout *title_layout = new QHBoxLayout(title_bar); - title_layout->setContentsMargins(0, 0, 0, 0); - title_label = new QLabel(tr("Charts")); - - title_layout->addWidget(title_label); - title_layout->addStretch(); - - range_label = new QLabel(); - title_layout->addWidget(range_label); - - reset_zoom_btn = new QPushButton("⟲", this); - reset_zoom_btn->setFixedSize(30, 30); + // toolbar + QToolBar *toolbar = new QToolBar(tr("Charts"), this); + title_label = new QLabel(); + title_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolbar->addWidget(title_label); + toolbar->addWidget(range_label = new QLabel()); + reset_zoom_btn = toolbar->addAction("⟲"); reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)")); - title_layout->addWidget(reset_zoom_btn); - - remove_all_btn = new QPushButton("✖", this); + remove_all_btn = toolbar->addAction("✖"); remove_all_btn->setToolTip(tr("Remove all charts")); - remove_all_btn->setFixedSize(30, 30); - title_layout->addWidget(remove_all_btn); - - dock_btn = new QPushButton(); - dock_btn->setFixedSize(30, 30); - title_layout->addWidget(dock_btn); - - main_layout->addWidget(title_bar, 0, Qt::AlignTop); + dock_btn = toolbar->addAction(""); + main_layout->addWidget(toolbar); + updateToolBar(); // charts QWidget *charts_container = new QWidget(this); - QVBoxLayout *charts_main = new QVBoxLayout(charts_container); - charts_layout = new QVBoxLayout(); - charts_main->addLayout(charts_layout); - charts_main->addStretch(); + charts_layout = new QVBoxLayout(charts_container); + charts_layout->addStretch(); QScrollArea *charts_scroll = new QScrollArea(this); charts_scroll->setWidgetResizable(true); @@ -58,43 +40,48 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { main_layout->addWidget(charts_scroll); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() { removeAll(nullptr); }); - QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartsWidget::removeAll); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartsWidget::signalUpdated); - QObject::connect(dbc(), &DBCManager::msgUpdated, [this](const QString &msg_id) { - for (auto c : charts) { - if (c->id == msg_id) c->updateTitle(); - } - }); + QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); QObject::connect(can, &CANMessages::eventsMerged, this, &ChartsWidget::eventsMerged); QObject::connect(can, &CANMessages::updated, this, &ChartsWidget::updateState); - QObject::connect(remove_all_btn, &QPushButton::clicked, [this]() { removeAll(); }); - QObject::connect(reset_zoom_btn, &QPushButton::clicked, this, &ChartsWidget::zoomReset); - QObject::connect(dock_btn, &QPushButton::clicked, [this]() { + QObject::connect(remove_all_btn, &QAction::triggered, this, &ChartsWidget::removeAll); + QObject::connect(reset_zoom_btn, &QAction::triggered, this, &ChartsWidget::zoomReset); + QObject::connect(dock_btn, &QAction::triggered, [this]() { emit dock(!docking); docking = !docking; - updateTitleBar(); + updateToolBar(); }); } void ChartsWidget::eventsMerged() { if (auto events = can->events(); events && !events->empty()) { - auto it = std::find_if(events->begin(), events->end(), [=](const Event *e) { return e->which == cereal::Event::Which::CAN; }); - event_range.first = it == events->end() ? 0 : (*it)->mono_time / (double)1e9 - can->routeStartTime(); - event_range.second = it == events->end() ? 0 : events->back()->mono_time / (double)1e9 - can->routeStartTime(); - if (display_range.first == 0 && event_range.second == 0) { - display_range.first = event_range.first; - display_range.second = std::min(event_range.first + settings.max_chart_x_range, event_range.second); - } + event_range.first = (events->front()->mono_time / (double)1e9) - can->routeStartTime(); + event_range.second = (events->back()->mono_time / (double)1e9) - can->routeStartTime(); + updateDisplayRange(); + } +} + +void ChartsWidget::updateDisplayRange() { + auto prev_range = display_range; + double current_sec = can->currentSec(); + if (current_sec < display_range.first || current_sec >= (display_range.second - 5)) { + // reached the end, or seeked to a timestamp out of range. + display_range.first = current_sec - 5; + } + display_range.first = std::max(display_range.first, event_range.first); + display_range.second = std::min(display_range.first + settings.max_chart_x_range, event_range.second); + if (prev_range != display_range) { + QFutureSynchronizer future_synchronizer; + for (auto c : charts) + future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::setEventsRange, display_range)); } } void ChartsWidget::zoomIn(double min, double max) { zoomed_range = {min, max}; is_zoomed = zoomed_range != display_range; - updateTitleBar(); - emit rangeChanged(min, max, is_zoomed); + updateToolBar(); updateState(); + emit rangeChanged(min, max, is_zoomed); } void ChartsWidget::zoomReset() { @@ -104,210 +91,215 @@ void ChartsWidget::zoomReset() { void ChartsWidget::updateState() { if (charts.isEmpty()) return; - const double current_sec = can->currentSec(); - if (is_zoomed) { - if (current_sec < zoomed_range.first || current_sec >= zoomed_range.second) { - can->seekTo(zoomed_range.first); - } - } else { - auto prev_range = display_range; - if (current_sec < display_range.first || current_sec >= (display_range.second - 5)) { - // line marker reached the end, or seeked to a timestamp out of range. - display_range.first = current_sec - 5; - } - display_range.first = std::max(display_range.first, event_range.first); - display_range.second = std::min(display_range.first + settings.max_chart_x_range, event_range.second); - if (prev_range != display_range) { - QFutureSynchronizer future_synchronizer; - for (auto c : charts) - future_synchronizer.addFuture(QtConcurrent::run(c->chart_view, &ChartView::updateSeries, display_range)); - } + if (!is_zoomed) { + updateDisplayRange(); + } else if (can->currentSec() < zoomed_range.first || can->currentSec() >= zoomed_range.second) { + can->seekTo(zoomed_range.first); } const auto &range = is_zoomed ? zoomed_range : display_range; for (auto c : charts) { - c->chart_view->setRange(range.first, range.second); - c->chart_view->updateLineMarker(current_sec); + c->setDisplayRange(range.first, range.second); + c->scene()->invalidate({}, QGraphicsScene::ForegroundLayer); } } -void ChartsWidget::updateTitleBar() { - title_bar->setVisible(!charts.isEmpty()); - if (charts.isEmpty()) return; - - range_label->setVisible(is_zoomed); +void ChartsWidget::updateToolBar() { + remove_all_btn->setEnabled(!charts.isEmpty()); reset_zoom_btn->setEnabled(is_zoomed); - if (is_zoomed) { - range_label->setText(tr("%1 - %2").arg(zoomed_range.first, 0, 'f', 2).arg(zoomed_range.second, 0, 'f', 2)); - } - title_label->setText(tr("Charts (%1)").arg(charts.size())); + range_label->setText(is_zoomed ? tr("%1 - %2").arg(zoomed_range.first, 0, 'f', 2).arg(zoomed_range.second, 0, 'f', 2) : ""); + title_label->setText(charts.size() > 0 ? tr("Charts (%1)").arg(charts.size()) : tr("Charts")); dock_btn->setText(docking ? "⬈" : "⬋"); dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); } -void ChartsWidget::showChart(const QString &id, const Signal *sig, bool show) { - auto it = std::find_if(charts.begin(), charts.end(), [=](auto c) { return c->id == id && c->signal == sig; }); - if (it != charts.end()) { - if (!show) removeChart((*it)); - } else if (show) { - auto chart = new ChartWidget(id, sig, this); - chart->chart_view->updateSeries(display_range); - QObject::connect(chart, &ChartWidget::remove, [=]() { removeChart(chart); }); - QObject::connect(chart->chart_view, &ChartView::zoomIn, this, &ChartsWidget::zoomIn); - QObject::connect(chart->chart_view, &ChartView::zoomReset, this, &ChartsWidget::zoomReset); - charts_layout->insertWidget(0, chart); - charts.push_back(chart); - emit chartOpened(chart->id, chart->signal); - updateState(); - } - updateTitleBar(); +ChartView *ChartsWidget::findChart(const QString &id, const Signal *sig) { + for (auto c : charts) + if (c->hasSeries(id, sig)) return c; + return nullptr; } -bool ChartsWidget::isChartOpened(const QString &id, const Signal *sig) { - auto it = std::find_if(charts.begin(), charts.end(), [=](auto c) { return c->id == id && c->signal == sig; }); - return it != charts.end(); +void ChartsWidget::showChart(const QString &id, const Signal *sig, bool show, bool merge) { + setUpdatesEnabled(false); + if (show) { + ChartView *chart = merge && charts.size() > 0 ? charts.back() : nullptr; + if (!chart) { + chart = new ChartView(this); + chart->setEventsRange(display_range); + auto range = is_zoomed ? zoomed_range : display_range; + chart->setDisplayRange(range.first, range.second); + QObject::connect(chart, &ChartView::remove, [=]() { removeChart(chart); }); + QObject::connect(chart, &ChartView::zoomIn, this, &ChartsWidget::zoomIn); + QObject::connect(chart, &ChartView::zoomReset, this, &ChartsWidget::zoomReset); + QObject::connect(chart, &ChartView::seriesRemoved, this, &ChartsWidget::chartClosed); + charts_layout->insertWidget(0, chart); + charts.push_back(chart); + } + chart->addSeries(id, sig); + emit chartOpened(id, sig); + } else if (ChartView *chart = findChart(id, sig)) { + chart->removeSeries(id, sig); + } + updateToolBar(); + setUpdatesEnabled(true); } -void ChartsWidget::removeChart(ChartWidget *chart) { +void ChartsWidget::removeChart(ChartView *chart) { charts.removeOne(chart); chart->deleteLater(); - updateTitleBar(); - emit chartClosed(chart->id, chart->signal); -} - -void ChartsWidget::removeAll(const Signal *sig) { - QMutableListIterator it(charts); - while (it.hasNext()) { - auto c = it.next(); - if (sig == nullptr || c->signal == sig) { - c->deleteLater(); - emit chartClosed(c->id, c->signal); - it.remove(); - } - } - updateTitleBar(); + updateToolBar(); } -void ChartsWidget::signalUpdated(const Signal *sig) { - for (auto c : charts) { - if (c->signal == sig) { - c->updateTitle(); - c->chart_view->updateSeries(display_range); - c->chart_view->setRange(display_range.first, display_range.second, true); - } - } +void ChartsWidget::removeAll() { + for (auto c : charts.toVector()) + removeChart(c); } bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) { if (obj != this && event->type() == QEvent::Close) { - emit dock_btn->clicked(); + emit dock_btn->triggered(); return true; } return false; } -// ChartWidget - -ChartWidget::ChartWidget(const QString &id, const Signal *sig, QWidget *parent) : id(id), signal(sig), QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setSpacing(0); - main_layout->setContentsMargins(0, 0, 0, 0); - - header = new QWidget(this); - QGridLayout *header_layout = new QGridLayout(header); - header_layout->setContentsMargins(11, 11, 11, 0); - msg_name_label = new QLabel(this); - msg_name_label->setTextFormat(Qt::RichText); - header_layout->addWidget(msg_name_label, 0, 0, Qt::AlignLeft); - sig_name_label = new QLabel(this); - header_layout->addWidget(sig_name_label, 0, 1, Qt::AlignCenter); //, 0, Qt::AlignCenter); - - remove_btn = new QPushButton("✖", this); - remove_btn->setFixedSize(20, 20); - remove_btn->setToolTip(tr("Remove chart")); - header_layout->addWidget(remove_btn, 0, 2, Qt::AlignRight); - main_layout->addWidget(header); - - chart_view = new ChartView(id, sig, this); - main_layout->addWidget(chart_view); - main_layout->addStretch(); - - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - updateTitle(); - updateFromSettings(); - - QObject::connect(remove_btn, &QPushButton::clicked, [=]() { emit remove(id, sig); }); - QObject::connect(&settings, &Settings::changed, this, &ChartWidget::updateFromSettings); -} - -void ChartWidget::updateTitle() { - msg_name_label->setText(tr("%1 %2").arg(dbc()->msg(id)->name.c_str()).arg(id)); - sig_name_label->setText(signal->name.c_str()); -} - -void ChartWidget::updateFromSettings() { - header->setStyleSheet(settings.chart_theme == 0 ? "background-color:white" : "background-color:#23242c"); - QString color_style = settings.chart_theme == 0 ? "color:black" : "color:white"; - sig_name_label->setStyleSheet("font-weight:bold;" + color_style); - msg_name_label->setStyleSheet(color_style); - remove_btn->setStyleSheet(color_style); - chart_view->updateFromSettings(); -} - // ChartView -ChartView::ChartView(const QString &id, const Signal *sig, QWidget *parent) - : id(id), signal(sig), QChartView(nullptr, parent) { - QLineSeries *series = new QLineSeries(); +ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) { QChart *chart = new QChart(); chart->setBackgroundRoundness(0); - chart->addSeries(series); - chart->createDefaultAxes(); - chart->legend()->hide(); - chart->setMargins({0, 0, 0, 0}); + axis_x = new QValueAxis(this); + axis_y = new QValueAxis(this); + chart->addAxis(axis_x, Qt::AlignBottom); + chart->addAxis(axis_y, Qt::AlignLeft); + chart->legend()->setShowToolTips(true); chart->layout()->setContentsMargins(0, 0, 0, 0); + chart->setMargins(QMargins(20, 11, 11, 11)); track_line = new QGraphicsLineItem(chart); - track_line->setZValue(chart->zValue() + 10); track_line->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); track_ellipse = new QGraphicsEllipseItem(chart); - track_ellipse->setZValue(chart->zValue() + 10); track_ellipse->setBrush(Qt::darkGray); value_text = new QGraphicsTextItem(chart); - value_text->setZValue(chart->zValue() + 10); - line_marker = new QGraphicsLineItem(chart); - line_marker->setZValue(chart->zValue() + 10); + item_group = scene()->createItemGroup({track_line, track_ellipse, value_text}); + item_group->setZValue(chart->zValue() + 10); + + // title + QToolButton *remove_btn = new QToolButton(); + remove_btn->setText("X"); + remove_btn->setAutoRaise(true); + remove_btn->setToolTip(tr("Remove Chart")); + close_btn_proxy = new QGraphicsProxyWidget(chart); + close_btn_proxy->setWidget(remove_btn); + close_btn_proxy->setZValue(chart->zValue() + 11); setChart(chart); - setRenderHint(QPainter::Antialiasing); setRubberBand(QChartView::HorizontalRubberBand); - if (auto rubber = findChild()) { - QPalette pal; - pal.setBrush(QPalette::Base, QColor(0, 0, 0, 80)); - rubber->setPalette(pal); + updateFromSettings(); + + QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved); + QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated); + QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved); + QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated); + QObject::connect(&settings, &Settings::changed, this, &ChartView::updateFromSettings); + QObject::connect(remove_btn, &QToolButton::clicked, this, &ChartView::remove); +} + +ChartView::~ChartView() { + for (auto &s : sigs) + emit seriesRemoved(s.msg_id, s.sig); +} + +void ChartView::addSeries(const QString &msg_id, const Signal *sig) { + QLineSeries *series = new QLineSeries(this); + chart()->addSeries(series); + series->attachAxis(axis_x); + series->attachAxis(axis_y); + auto [source, address] = DBCManager::parseId(msg_id); + sigs.push_back({.msg_id = msg_id, .address = address, .source = source, .sig = sig, .series = series}); + updateTitle(); + updateSeries(sig); +} + +void ChartView::removeSeries(const QString &msg_id, const Signal *sig) { + auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); + if (it != sigs.end()) { + it = removeSeries(it); } +} - QTimer *timer = new QTimer(this); - timer->setInterval(100); - timer->setSingleShot(true); - timer->callOnTimeout(this, &ChartView::adjustChartMargins); +bool ChartView::hasSeries(const QString &msg_id, const Signal *sig) const { + auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); + return it != sigs.end(); +} - QObject::connect(chart, &QChart::plotAreaChanged, [=](const QRectF &plotArea) { - // use a singleshot timer to avoid recursion call. - timer->start(); - }); +QList::iterator ChartView::removeSeries(const QList::iterator &it) { + chart()->removeSeries(it->series); + it->series->deleteLater(); + emit seriesRemoved(it->msg_id, it->sig); + + auto ret = sigs.erase(it); + if (!sigs.isEmpty()) { + updateAxisY(); + } else { + emit remove(); + } + return ret; +} + +void ChartView::signalUpdated(const Signal *sig) { + auto it = std::find_if(sigs.begin(), sigs.end(), [=](auto &s) { return s.sig == sig; }); + if (it != sigs.end()) { + updateTitle(); + // TODO: don't update series if only name changed. + updateSeries(sig); + } +} + +void ChartView::signalRemoved(const Signal *sig) { + for (auto it = sigs.begin(); it != sigs.end(); /**/) { + it = (it->sig == sig) ? removeSeries(it) : ++it; + } +} + +void ChartView::msgUpdated(uint32_t address) { + auto it = std::find_if(sigs.begin(), sigs.end(), [=](auto &s) { return s.address == address; }); + if (it != sigs.end()) + updateTitle(); +} + +void ChartView::msgRemoved(uint32_t address) { + for (auto it = sigs.begin(); it != sigs.end(); /**/) { + it = (it->address == address) ? removeSeries(it) : ++it; + } +} + +void ChartView::resizeEvent(QResizeEvent *event) { + QChartView::resizeEvent(event); + close_btn_proxy->setPos(event->size().width() - close_btn_proxy->size().width() - 11, 8); +} + +void ChartView::updateTitle() { + for (auto &s : sigs) { + s.series->setName(QString("%1 %2 %3").arg(s.sig->name.c_str()).arg(msgName(s.msg_id)).arg(s.msg_id)); + } } void ChartView::updateFromSettings() { setFixedHeight(settings.chart_height); chart()->setTheme(settings.chart_theme == 0 ? QChart::ChartThemeLight : QChart::QChart::ChartThemeDark); - line_marker->setPen(QPen(settings.chart_theme == 0 ? Qt::black : Qt::white, 2)); } -void ChartView::setRange(double min, double max, bool force_update) { - auto axis_x = dynamic_cast(chart()->axisX()); - if (force_update || (min != axis_x->min() || max != axis_x->max())) { +void ChartView::setEventsRange(const std::pair &range) { + if (range != events_range) { + events_range = range; + updateSeries(); + } +} + +void ChartView::setDisplayRange(double min, double max) { + if (min != axis_x->min() || max != axis_x->max()) { axis_x->setRange(min, max); updateAxisY(); } @@ -316,81 +308,86 @@ void ChartView::setRange(double min, double max, bool force_update) { void ChartView::adjustChartMargins() { // TODO: Remove hardcoded aligned_pos const int aligned_pos = 60; - if (chart()->plotArea().left() != aligned_pos) { + if ((int)chart()->plotArea().left() != aligned_pos) { const float left_margin = chart()->margins().left() + aligned_pos - chart()->plotArea().left(); - chart()->setMargins(QMargins(left_margin, 0, 0, 0)); - } -} - -void ChartView::updateLineMarker(double current_sec) { - auto axis_x = dynamic_cast(chart()->axisX()); - int x = chart()->plotArea().left() + - chart()->plotArea().width() * (current_sec - axis_x->min()) / (axis_x->max() - axis_x->min()); - if (int(line_marker->line().x1()) != x) { - line_marker->setLine(x, 0, x, height()); + chart()->setMargins(QMargins(left_margin, 11, 11, 11)); + scene()->invalidate({}, QGraphicsScene::ForegroundLayer); } } -void ChartView::updateSeries(const std::pair range) { +void ChartView::updateSeries(const Signal *sig) { auto events = can->events(); - if (!events) return; - - auto l = id.split(':'); - int bus = l[0].toInt(); - uint32_t address = l[1].toUInt(nullptr, 16); - - vals.clear(); - vals.reserve((range.second - range.first) * 1000); // [n]seconds * 1000hz - double route_start_time = can->routeStartTime(); - Event begin_event(cereal::Event::Which::INIT_DATA, (route_start_time + range.first) * 1e9); - auto begin = std::lower_bound(events->begin(), events->end(), &begin_event, Event::lessThan()); - double end_ns = (route_start_time + range.second) * 1e9; - for (auto it = begin; it != events->end() && (*it)->mono_time <= end_ns; ++it) { - if ((*it)->which == cereal::Event::Which::CAN) { - for (auto c : (*it)->event.getCan()) { - if (bus == c.getSrc() && address == c.getAddress()) { - auto dat = c.getDat(); - double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *signal); - double ts = ((*it)->mono_time / (double)1e9) - route_start_time; // seconds - vals.push_back({ts, value}); + if (!events || sigs.isEmpty()) return; + + for (auto &s : sigs) { + if (!sig || s.sig == sig) { + s.vals.clear(); + s.vals.reserve((events_range.second - events_range.first) * 1000); // [n]seconds * 1000hz + s.min_y = std::numeric_limits::max(); + s.max_y = std::numeric_limits::lowest(); + + double route_start_time = can->routeStartTime(); + Event begin_event(cereal::Event::Which::INIT_DATA, (route_start_time + events_range.first) * 1e9); + auto begin = std::lower_bound(events->begin(), events->end(), &begin_event, Event::lessThan()); + double end_ns = (route_start_time + events_range.second) * 1e9; + + for (auto it = begin; it != events->end() && (*it)->mono_time <= end_ns; ++it) { + if ((*it)->which == cereal::Event::Which::CAN) { + for (const auto &c : (*it)->event.getCan()) { + if (s.source == c.getSrc() && s.address == c.getAddress()) { + auto dat = c.getDat(); + double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *s.sig); + double ts = ((*it)->mono_time / (double)1e9) - route_start_time; // seconds + s.vals.push_back({ts, value}); + + if (value < s.min_y) s.min_y = value; + if (value > s.max_y) s.max_y = value; + } + } } } + s.series->replace(s.vals); } } - QLineSeries *series = (QLineSeries *)chart()->series()[0]; - series->replace(vals); + updateAxisY(); } // auto zoom on yaxis void ChartView::updateAxisY() { - const auto axis_x = dynamic_cast(chart()->axisX()); - const auto axis_y = dynamic_cast(chart()->axisY()); - auto begin = std::lower_bound(vals.begin(), vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; }); - if (begin == vals.end()) - return; - - auto end = std::upper_bound(vals.begin(), vals.end(), axis_x->max(), [](double x, auto &p) { return x < p.x(); }); - const auto [min, max] = std::minmax_element(begin, end, [](auto &p1, auto &p2) { return p1.y() < p2.y(); }); - if (max->y() == min->y()) { - axis_y->setRange(min->y() - 1, max->y() + 1); + if (sigs.isEmpty()) return; + + double min_y = std::numeric_limits::max(); + double max_y = std::numeric_limits::lowest(); + if (events_range == std::pair{axis_x->min(), axis_x->max()}) { + for (auto &s : sigs) { + if (s.min_y < min_y) min_y = s.min_y; + if (s.max_y > max_y) max_y = s.max_y; + } } else { - double range = max->y() - min->y(); - axis_y->setRange(min->y() - range * 0.05, max->y() + range * 0.05); + for (auto &s : sigs) { + auto begin = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; }); + for (auto it = begin; it != s.vals.end() && it->x() <= axis_x->max(); ++it) { + if (it->y() < min_y) min_y = it->y(); + if (it->y() > max_y) max_y = it->y(); + } + } + } + + if (min_y == std::numeric_limits::max()) min_y = 0; + if (max_y == std::numeric_limits::lowest()) max_y = 0; + if (max_y == min_y) { + axis_y->setRange(min_y - 1, max_y + 1); + } else { + double range = max_y - min_y; + axis_y->setRange(min_y - range * 0.05, max_y + range * 0.05); axis_y->applyNiceNumbers(); } -} -void ChartView::enterEvent(QEvent *event) { - track_line->setVisible(true); - value_text->setVisible(true); - track_ellipse->setVisible(true); - QChartView::enterEvent(event); + QTimer::singleShot(0, this, &ChartView::adjustChartMargins); } void ChartView::leaveEvent(QEvent *event) { - track_line->setVisible(false); - value_text->setVisible(false); - track_ellipse->setVisible(false); + item_group->setVisible(false); QChartView::leaveEvent(event); } @@ -399,10 +396,8 @@ void ChartView::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) { rubber->hide(); QRectF rect = rubber->geometry().normalized(); - rect.translate(-chart()->plotArea().topLeft()); - const auto axis_x = dynamic_cast(chart()->axisX()); - double min = axis_x->min() + (rect.left() / chart()->plotArea().width()) * (axis_x->max() - axis_x->min()); - double max = axis_x->min() + (rect.right() / chart()->plotArea().width()) * (axis_x->max() - axis_x->min()); + double min = chart()->mapToValue(rect.topLeft()).x(); + double max = chart()->mapToValue(rect.bottomRight()).x(); if (rubber->width() <= 0) { // no rubber dragged, seek to mouse position can->seekTo(min); @@ -417,35 +412,51 @@ void ChartView::mouseReleaseEvent(QMouseEvent *event) { } else { QGraphicsView::mouseReleaseEvent(event); } - setViewportUpdateMode(QGraphicsView::MinimalViewportUpdate); } void ChartView::mouseMoveEvent(QMouseEvent *ev) { auto rubber = findChild(); bool is_zooming = rubber && rubber->isVisible(); - if (!is_zooming) { - const auto plot_area = chart()->plotArea(); - auto axis_x = dynamic_cast(chart()->axisX()); - double x = std::clamp((double)ev->pos().x(), plot_area.left(), plot_area.right()-1); - double sec = axis_x->min() + (axis_x->max() - axis_x->min()) * (x - plot_area.left()) / plot_area.width(); - auto value = std::upper_bound(vals.begin(), vals.end(), sec, [](double x, auto &p) { return x < p.x(); }); - if (value != vals.end()) { - QPointF pos = chart()->mapToPosition((*value)); - track_line->setLine(pos.x(), plot_area.top(), pos.x(), plot_area.bottom()); - track_ellipse->setRect(pos.x() - 5, pos.y() - 5, 10, 10); - value_text->setHtml(tr("
%1, %2)
") - .arg(value->x(), 0, 'f', 3).arg(value->y())); - int text_x = pos.x() + 8; - if ((text_x + value_text->boundingRect().width()) > plot_area.right()) { - text_x = pos.x() - value_text->boundingRect().width() - 8; + const auto plot_area = chart()->plotArea(); + + if (!is_zooming && plot_area.contains(ev->pos())) { + QStringList text_list; + QPointF pos = {}; + const double sec = chart()->mapToValue(ev->pos()).x(); + for (auto &s : sigs) { + QString value = "--"; + // use reverse iterator to find last item <= sec. + auto it = std::lower_bound(s.vals.rbegin(), s.vals.rend(), sec, [](auto &p, double x) { return p.x() > x; }); + if (it != s.vals.rend() && it->x() >= axis_x->min()) { + value = QString::number(it->y()); + auto value_pos = chart()->mapToPosition(*it); + if (value_pos.x() > pos.x()) pos = value_pos; } - value_text->setPos(text_x, pos.y() - 10); + text_list.push_back(QString(" %1 : %2 ").arg(sigs.size() > 1 ? s.sig->name.c_str() : "Value").arg(value)); } - track_line->setVisible(value != vals.end()); - value_text->setVisible(value != vals.end()); - track_ellipse->setVisible(value != vals.end()); + if (pos.x() == 0) pos = ev->pos(); + + QString time = QString::number(chart()->mapToValue(pos).x(), 'f', 3); + value_text->setHtml(QString("
 Time: %1  
%2
") + .arg(time).arg(text_list.join("
"))); + + QRectF text_rect = value_text->boundingRect(); + int text_x = pos.x() + 8; + if ((text_x + text_rect.width()) > plot_area.right()) { + text_x = pos.x() - text_rect.width() - 8; + } + value_text->setPos(text_x, pos.y() - text_rect.height() / 2); + track_line->setLine(pos.x(), plot_area.top(), pos.x(), plot_area.bottom()); + track_ellipse->setRect(pos.x() - 5, pos.y() - 5, 10, 10); + item_group->setVisible(true); } else { - setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + item_group->setVisible(false); } QChartView::mouseMoveEvent(ev); } + +void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { + qreal x = chart()->mapToPosition(QPointF{can->currentSec(), 0}).x(); + painter->setPen(QPen(chart()->titleBrush().color(), 2)); + painter->drawLine(QPointF{x, chart()->plotArea().top() - 2}, QPointF{x, chart()->plotArea().bottom() + 2}); +} diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h index ff56008e7d..c8e5a1040c 100644 --- a/tools/cabana/chartswidget.h +++ b/tools/cabana/chartswidget.h @@ -1,15 +1,14 @@ #pragma once -#include - #include #include #include +#include #include -#include #include -#include #include +#include +#include #include "tools/cabana/canmessages.h" #include "tools/cabana/dbcmanager.h" @@ -20,62 +19,69 @@ class ChartView : public QChartView { Q_OBJECT public: - ChartView(const QString &id, const Signal *sig, QWidget *parent = nullptr); - void updateSeries(const std::pair range); - void setRange(double min, double max, bool force_update = false); - void updateLineMarker(double current_sec); - void updateFromSettings(); + ChartView(QWidget *parent = nullptr); + ~ChartView(); + void addSeries(const QString &msg_id, const Signal *sig); + void removeSeries(const QString &msg_id, const Signal *sig); + bool hasSeries(const QString &msg_id, const Signal *sig) const; + void updateSeries(const Signal *sig = nullptr); + void setEventsRange(const std::pair &range); + void setDisplayRange(double min, double max); + + struct SigItem { + QString msg_id; + uint8_t source = 0; + uint32_t address = 0; + const Signal *sig = nullptr; + QLineSeries *series = nullptr; + double min_y = 0; + double max_y = 0; + QVector vals; + }; signals: + void seriesRemoved(const QString &id, const Signal *sig); void zoomIn(double min, double max); void zoomReset(); + void remove(); + +private slots: + void msgRemoved(uint32_t address); + void msgUpdated(uint32_t address); + void signalUpdated(const Signal *sig); + void signalRemoved(const Signal *sig); private: + QList::iterator removeSeries(const QList::iterator &it); void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *ev) override; - void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; + void resizeEvent(QResizeEvent *event) override; void adjustChartMargins(); void updateAxisY(); + void updateTitle(); + void updateFromSettings(); + void drawForeground(QPainter *painter, const QRectF &rect) override; + QValueAxis *axis_x; + QValueAxis *axis_y; + QGraphicsItemGroup *item_group; QGraphicsLineItem *track_line; QGraphicsEllipseItem *track_ellipse; QGraphicsTextItem *value_text; - QGraphicsLineItem *line_marker; - QVector vals; - QString id; - const Signal *signal; -}; - -class ChartWidget : public QWidget { -Q_OBJECT - -public: - ChartWidget(const QString &id, const Signal *sig, QWidget *parent); - void updateTitle(); - void updateFromSettings(); - -signals: - void remove(const QString &msg_id, const Signal *sig); - -public: - QString id; - const Signal *signal; - QWidget *header; - QLabel *msg_name_label; - QLabel *sig_name_label; - QPushButton *remove_btn; - ChartView *chart_view = nullptr; -}; + QGraphicsProxyWidget *close_btn_proxy; + std::pair events_range = {0, 0}; + QList sigs; + }; class ChartsWidget : public QWidget { Q_OBJECT public: ChartsWidget(QWidget *parent = nullptr); - void showChart(const QString &id, const Signal *sig, bool show); - void removeChart(ChartWidget *chart); - bool isChartOpened(const QString &id, const Signal *sig); + void showChart(const QString &id, const Signal *sig, bool show, bool merge); + void removeChart(ChartView *chart); + inline bool isChartOpened(const QString &id, const Signal *sig) { return findChart(id, sig) != nullptr; } signals: void dock(bool floating); @@ -86,23 +92,22 @@ signals: private: void eventsMerged(); void updateState(); + void updateDisplayRange(); void zoomIn(double min, double max); void zoomReset(); - void signalUpdated(const Signal *sig); - void updateTitleBar(); - void removeAll(const Signal *sig = nullptr); + void updateToolBar(); + void removeAll(); bool eventFilter(QObject *obj, QEvent *event) override; + ChartView *findChart(const QString &id, const Signal *sig); - QWidget *title_bar; QLabel *title_label; QLabel *range_label; bool docking = true; - QPushButton *dock_btn; - QPushButton *reset_zoom_btn; - QPushButton *remove_all_btn; + QAction *dock_btn; + QAction *reset_zoom_btn; + QAction *remove_all_btn; QVBoxLayout *charts_layout; - QList charts; - + QList charts; bool is_zoomed = false; std::pair event_range; std::pair display_range; diff --git a/tools/cabana/commands.cc b/tools/cabana/commands.cc new file mode 100644 index 0000000000..b3f5cb1c66 --- /dev/null +++ b/tools/cabana/commands.cc @@ -0,0 +1,75 @@ +#include "tools/cabana/commands.h" + +// EditMsgCommand + +EditMsgCommand::EditMsgCommand(const QString &id, const QString &title, int size, QUndoCommand *parent) + : id(id), new_title(title), new_size(size), QUndoCommand(parent) { + if (auto msg = dbc()->msg(id)) { + old_title = msg->name; + old_size = msg->size; + } + setText(QObject::tr("Edit message %1:%2").arg(DBCManager::parseId(id).second).arg(title)); +} + +void EditMsgCommand::undo() { + if (old_title.isEmpty()) + dbc()->removeMsg(id); + else + dbc()->updateMsg(id, old_title, old_size); +} + +void EditMsgCommand::redo() { + dbc()->updateMsg(id, new_title, new_size); +} + +// RemoveMsgCommand + +RemoveMsgCommand::RemoveMsgCommand(const QString &id, QUndoCommand *parent) : id(id), QUndoCommand(parent) { + if (auto msg = dbc()->msg(id)) { + message = *msg; + setText(QObject::tr("Remove message %1:%2").arg(DBCManager::parseId(id).second).arg(message.name)); + } +} + +void RemoveMsgCommand::undo() { + if (!message.name.isEmpty()) { + dbc()->updateMsg(id, message.name, message.size); + for (auto &[name, s] : message.sigs) + dbc()->addSignal(id, s); + } +} + +void RemoveMsgCommand::redo() { + if (!message.name.isEmpty()) + dbc()->removeMsg(id); +} + +// AddSigCommand + +AddSigCommand::AddSigCommand(const QString &id, const Signal &sig, QUndoCommand *parent) + : id(id), signal(sig), QUndoCommand(parent) { + setText(QObject::tr("Add signal %1 to %2").arg(sig.name.c_str()).arg(DBCManager::parseId(id).second)); +} + +void AddSigCommand::undo() { dbc()->removeSignal(id, signal.name.c_str()); } +void AddSigCommand::redo() { dbc()->addSignal(id, signal); } + +// RemoveSigCommand + +RemoveSigCommand::RemoveSigCommand(const QString &id, const Signal *sig, QUndoCommand *parent) + : id(id), signal(*sig), QUndoCommand(parent) { + setText(QObject::tr("Remove signal %1 from %2").arg(signal.name.c_str()).arg(DBCManager::parseId(id).second)); +} + +void RemoveSigCommand::undo() { dbc()->addSignal(id, signal); } +void RemoveSigCommand::redo() { dbc()->removeSignal(id, signal.name.c_str()); } + +// EditSignalCommand + +EditSignalCommand::EditSignalCommand(const QString &id, const Signal *sig, const Signal &new_sig, QUndoCommand *parent) + : id(id), old_signal(*sig), new_signal(new_sig), QUndoCommand(parent) { + setText(QObject::tr("Edit signal %1").arg(old_signal.name.c_str())); +} + +void EditSignalCommand::undo() { dbc()->updateSignal(id, new_signal.name.c_str(), old_signal); } +void EditSignalCommand::redo() { dbc()->updateSignal(id, old_signal.name.c_str(), new_signal); } diff --git a/tools/cabana/commands.h b/tools/cabana/commands.h new file mode 100644 index 0000000000..7ea1f66653 --- /dev/null +++ b/tools/cabana/commands.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include "tools/cabana/canmessages.h" +#include "tools/cabana/dbcmanager.h" + +class EditMsgCommand : public QUndoCommand { +public: + EditMsgCommand(const QString &id, const QString &title, int size, QUndoCommand *parent = nullptr); + void undo() override; + void redo() override; + +private: + const QString id; + QString old_title, new_title; + int old_size = 0, new_size = 0; +}; + +class RemoveMsgCommand : public QUndoCommand { +public: + RemoveMsgCommand(const QString &id, QUndoCommand *parent = nullptr); + void undo() override; + void redo() override; + +private: + const QString id; + DBCMsg message; +}; + +class AddSigCommand : public QUndoCommand { +public: + AddSigCommand(const QString &id, const Signal &sig, QUndoCommand *parent = nullptr); + void undo() override; + void redo() override; + +private: + const QString id; + Signal signal = {}; +}; + +class RemoveSigCommand : public QUndoCommand { +public: + RemoveSigCommand(const QString &id, const Signal *sig, QUndoCommand *parent = nullptr); + void undo() override; + void redo() override; + +private: + const QString id; + Signal signal = {}; +}; + +class EditSignalCommand : public QUndoCommand { +public: + EditSignalCommand(const QString &id, const Signal *sig, const Signal &new_sig, QUndoCommand *parent = nullptr); + void undo() override; + void redo() override; + +private: + const QString id; + Signal old_signal = {}; + Signal new_signal = {}; +}; diff --git a/tools/cabana/dbcmanager.cc b/tools/cabana/dbcmanager.cc index 0ab67dd305..e7d3ead9c6 100644 --- a/tools/cabana/dbcmanager.cc +++ b/tools/cabana/dbcmanager.cc @@ -9,22 +9,24 @@ DBCManager::DBCManager(QObject *parent) : QObject(parent) {} DBCManager::~DBCManager() {} void DBCManager::open(const QString &dbc_file_name) { - dbc_name = dbc_file_name; - dbc = const_cast(dbc_lookup(dbc_name.toStdString())); - msg_map.clear(); - for (auto &msg : dbc->msgs) { - msg_map[msg.address] = &msg; - } - emit DBCFileChanged(); + dbc = const_cast(dbc_lookup(dbc_file_name.toStdString())); + initMsgMap(); } void DBCManager::open(const QString &name, const QString &content) { - this->dbc_name = name; std::istringstream stream(content.toStdString()); dbc = const_cast(dbc_parse_from_stream(name.toStdString(), stream)); - msg_map.clear(); + initMsgMap(); +} + +void DBCManager::initMsgMap() { + msgs.clear(); for (auto &msg : dbc->msgs) { - msg_map[msg.address] = &msg; + auto &m = msgs[msg.address]; + m.name = msg.name.c_str(); + m.size = msg.size; + for (auto &s : msg.sigs) + m.sigs[QString::fromStdString(s.name)] = s; } emit DBCFileChanged(); } @@ -33,11 +35,11 @@ QString DBCManager::generateDBC() { if (!dbc) return {}; QString dbc_string; - for (auto &m : dbc->msgs) { - dbc_string += QString("BO_ %1 %2: %3 XXX\n").arg(m.address).arg(m.name.c_str()).arg(m.size); - for (auto &sig : m.sigs) { + for (auto &[address, m] : msgs) { + dbc_string += QString("BO_ %1 %2: %3 XXX\n").arg(address).arg(m.name).arg(m.size); + for (auto &[name, sig] : m.sigs) { dbc_string += QString(" SG_ %1 : %2|%3@%4%5 (%6,%7) [0|0] \"\" XXX\n") - .arg(sig.name.c_str()) + .arg(name) .arg(sig.start_bit) .arg(sig.size) .arg(sig.is_little_endian ? '1' : '0') @@ -51,47 +53,53 @@ QString DBCManager::generateDBC() { } void DBCManager::updateMsg(const QString &id, const QString &name, uint32_t size) { - auto m = const_cast(msg(id)); - if (m) { - m->name = name.toStdString(); - m->size = size; - } else { - uint32_t address = addressFromId(id); - dbc->msgs.push_back({.address = address, .name = name.toStdString(), .size = size}); - msg_map[address] = &dbc->msgs.back(); - } - emit msgUpdated(id); + auto [_, address] = parseId(id); + auto &m = msgs[address]; + m.name = name; + m.size = size; + emit msgUpdated(address); +} + +void DBCManager::removeMsg(const QString &id) { + uint32_t address = parseId(id).second; + msgs.erase(address); + emit msgRemoved(address); } void DBCManager::addSignal(const QString &id, const Signal &sig) { - if (Msg *m = const_cast(msg(id))) { - m->sigs.push_back(sig); - emit signalAdded(&m->sigs.back()); + if (auto m = const_cast(msg(id))) { + auto &s = m->sigs[sig.name.c_str()]; + s = sig; + emit signalAdded(&s); } } void DBCManager::updateSignal(const QString &id, const QString &sig_name, const Signal &sig) { - if (Msg *m = const_cast(msg(id))) { - auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); - if (it != m->sigs.end()) { - *it = sig; - emit signalUpdated(&(*it)); - } + if (auto m = const_cast(msg(id))) { + // change key name + QString new_name = QString::fromStdString(sig.name); + auto node = m->sigs.extract(sig_name); + node.key() = new_name; + auto it = m->sigs.insert(std::move(node)); + auto &s = m->sigs[new_name]; + s = sig; + emit signalUpdated(&s); } } void DBCManager::removeSignal(const QString &id, const QString &sig_name) { - if (Msg *m = const_cast(msg(id))) { - auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); + if (auto m = const_cast(msg(id))) { + auto it = m->sigs.find(sig_name); if (it != m->sigs.end()) { - emit signalRemoved(&(*it)); + emit signalRemoved(&(it->second)); m->sigs.erase(it); } } } -uint32_t DBCManager::addressFromId(const QString &id) { - return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16); +std::pair DBCManager::parseId(const QString &id) { + const auto list = id.split(':'); + return {list[0].toInt(), list[1].toUInt(nullptr, 16)}; } DBCManager *dbc() { @@ -99,6 +107,15 @@ DBCManager *dbc() { return &dbc_manager; } +// DBCMsg + +std::vector DBCMsg::getSignals() const { + std::vector ret; + for (auto &[name, sig] : sigs) ret.push_back(&sig); + std::sort(ret.begin(), ret.end(), [](auto l, auto r) { return l->start_bit < r->start_bit; }); + return ret; +} + // helper functions static QVector BIG_ENDIAN_START_BITS = []() { @@ -140,9 +157,9 @@ double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig) { return value; } -void updateSigSizeParamsFromRange(Signal &s, int from, int to) { - s.start_bit = s.is_little_endian ? from : bigEndianBitIndex(from); - s.size = to - from + 1; +void updateSigSizeParamsFromRange(Signal &s, int start_bit, int size) { + s.start_bit = s.is_little_endian ? start_bit : bigEndianBitIndex(start_bit); + s.size = size; if (s.is_little_endian) { s.lsb = s.start_bit; s.msb = s.start_bit + s.size - 1; @@ -157,3 +174,11 @@ std::pair getSignalRange(const Signal *s) { int to = from + s->size - 1; return {from, to}; } + +bool operator==(const Signal &l, const Signal &r) { + return l.name == r.name && l.size == r.size && + l.start_bit == r.start_bit && + l.msb == r.msb && l.lsb == r.lsb && + l.is_signed == r.is_signed && l.is_little_endian == r.is_little_endian && + l.factor == r.factor && l.offset == r.offset; +} diff --git a/tools/cabana/dbcmanager.h b/tools/cabana/dbcmanager.h index 913445d44e..4e0bc91069 100644 --- a/tools/cabana/dbcmanager.h +++ b/tools/cabana/dbcmanager.h @@ -1,9 +1,17 @@ #pragma once +#include #include - +#include #include "opendbc/can/common_dbc.h" +struct DBCMsg { + QString name; + uint32_t size; + std::map sigs; + std::vector getSignals() const; +}; + class DBCManager : public QObject { Q_OBJECT @@ -18,36 +26,43 @@ public: void updateSignal(const QString &id, const QString &sig_name, const Signal &sig); void removeSignal(const QString &id, const QString &sig_name); - static uint32_t addressFromId(const QString &id); + static std::pair parseId(const QString &id); inline static std::vector allDBCNames() { return get_dbc_names(); } - inline QString name() const { return dbc_name; } + inline QString name() const { return dbc ? dbc->name.c_str() : ""; } void updateMsg(const QString &id, const QString &name, uint32_t size); - inline const DBC *getDBC() const { return dbc; } - inline const Msg *msg(const QString &id) const { return msg(addressFromId(id)); } - inline const Msg *msg(uint32_t address) const { - auto it = msg_map.find(address); - return it != msg_map.end() ? it->second : nullptr; + void removeMsg(const QString &id); + inline const std::map &messages() const { return msgs; } + inline const DBCMsg *msg(const QString &id) const { return msg(parseId(id).second); } + inline const DBCMsg *msg(uint32_t address) const { + auto it = msgs.find(address); + return it != msgs.end() ? &it->second : nullptr; } signals: void signalAdded(const Signal *sig); void signalRemoved(const Signal *sig); void signalUpdated(const Signal *sig); - void msgUpdated(const QString &id); + void msgUpdated(uint32_t address); + void msgRemoved(uint32_t address); void DBCFileChanged(); private: - QString dbc_name; + void initMsgMap(); DBC *dbc = nullptr; - std::unordered_map msg_map; + std::map msgs; }; // TODO: Add helper function in dbc.h double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig); +bool operator==(const Signal &l, const Signal &r); +inline bool operator!=(const Signal &l, const Signal &r) { return !(l == r); } int bigEndianStartBitsIndex(int start_bit); int bigEndianBitIndex(int index); -void updateSigSizeParamsFromRange(Signal &s, int from, int to); +void updateSigSizeParamsFromRange(Signal &s, int start_bit, int size); std::pair getSignalRange(const Signal *s); - DBCManager *dbc(); +inline QString msgName(const QString &id, const char *def = "untitled") { + auto msg = dbc()->msg(id); + return msg ? msg->name : def; +} diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 9bbc442154..52ae530a56 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -6,20 +6,23 @@ #include #include #include -#include #include "selfdrive/ui/qt/util.h" #include "tools/cabana/canmessages.h" +#include "tools/cabana/commands.h" #include "tools/cabana/dbcmanager.h" // DetailWidget DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) { + undo_stack = new QUndoStack(this); + + setMinimumWidth(500); QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); main_layout->setSpacing(0); - // tabbar + // tabbar tabbar = new QTabBar(this); tabbar->setTabsClosable(true); tabbar->setDrawBase(false); @@ -31,23 +34,23 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QFrame *title_frame = new QFrame(this); QVBoxLayout *frame_layout = new QVBoxLayout(title_frame); title_frame->setFrameShape(QFrame::StyledPanel); - title_frame->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); // message title - QHBoxLayout *title_layout = new QHBoxLayout(); - title_layout->addWidget(new QLabel("time:")); + toolbar = new QToolBar(this); + toolbar->addWidget(new QLabel("time:")); time_label = new QLabel(this); time_label->setStyleSheet("font-weight:bold"); - title_layout->addWidget(time_label); - title_layout->addStretch(); + toolbar->addWidget(time_label); name_label = new QLabel(this); name_label->setStyleSheet("font-weight:bold;"); - title_layout->addWidget(name_label); - title_layout->addStretch(); - edit_btn = new QPushButton(tr("Edit"), this); - edit_btn->setVisible(false); - title_layout->addWidget(edit_btn); - frame_layout->addLayout(title_layout); + name_label->setAlignment(Qt::AlignCenter); + name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolbar->addWidget(name_label); + toolbar->addAction("🖍", this, &DetailWidget::editMsg)->setToolTip(tr("Edit Message")); + remove_msg_act = toolbar->addAction("X", this, &DetailWidget::removeMsg); + remove_msg_act->setToolTip(tr("Remove Message")); + toolbar->setVisible(false); + frame_layout->addWidget(toolbar); // warning warning_widget = new QWidget(this); @@ -62,35 +65,37 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart frame_layout->addWidget(warning_widget); main_layout->addWidget(title_frame); - QWidget *container = new QWidget(this); - QVBoxLayout *container_layout = new QVBoxLayout(container); - container_layout->setSpacing(0); - container_layout->setContentsMargins(0, 0, 0, 0); + // msg widget + QWidget *msg_widget = new QWidget(this); + QVBoxLayout *msg_layout = new QVBoxLayout(msg_widget); + msg_layout->setContentsMargins(0, 0, 0, 0); + // binary view + binary_view = new BinaryView(this); + msg_layout->addWidget(binary_view); + // signals + signals_layout = new QVBoxLayout(); + signals_layout->setSpacing(0); + msg_layout->addLayout(signals_layout); + msg_layout->addStretch(0); scroll = new QScrollArea(this); - scroll->setWidget(container); + scroll->setFrameShape(QFrame::NoFrame); + scroll->setWidget(msg_widget); scroll->setWidgetResizable(true); scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - main_layout->addWidget(scroll); - - // binary view - binary_view = new BinaryView(this); - container_layout->addWidget(binary_view); - - // signals - signals_container = new QWidget(this); - signals_container->setLayout(new QVBoxLayout); - signals_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - container_layout->addWidget(signals_container); - // history log + tab_widget = new QTabWidget(this); + tab_widget->setTabPosition(QTabWidget::South); + tab_widget->addTab(scroll, "Msg"); history_log = new HistoryLog(this); - container_layout->addWidget(history_log); + tab_widget->addTab(history_log, "Logs"); + main_layout->addWidget(tab_widget); - QObject::connect(edit_btn, &QPushButton::clicked, this, &DetailWidget::editMsg); + QObject::connect(binary_view, &BinaryView::signalClicked, this, &DetailWidget::showForm); QObject::connect(binary_view, &BinaryView::resizeSignal, this, &DetailWidget::resizeSignal); QObject::connect(binary_view, &BinaryView::addSignal, this, &DetailWidget::addSignal); - QObject::connect(can, &CANMessages::updated, this, &DetailWidget::updateState); + QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); }); + QObject::connect(can, &CANMessages::msgsReceived, this, &DetailWidget::updateState); QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() { dbcMsgChanged(); }); QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu); QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { @@ -101,6 +106,10 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(tabbar, &QTabBar::tabCloseRequested, tabbar, &QTabBar::removeTab); QObject::connect(charts, &ChartsWidget::chartOpened, [this](const QString &id, const Signal *sig) { updateChartState(id, sig, true); }); QObject::connect(charts, &ChartsWidget::chartClosed, [this](const QString &id, const Signal *sig) { updateChartState(id, sig, false); }); + QObject::connect(undo_stack, &QUndoStack::indexChanged, [this]() { + if (undo_stack->count() > 0) + dbcMsgChanged(); + }); } void DetailWidget::showTabBarContextMenu(const QPoint &pt) { @@ -109,153 +118,156 @@ void DetailWidget::showTabBarContextMenu(const QPoint &pt) { QMenu menu(this); menu.addAction(tr("Close Other Tabs")); if (menu.exec(tabbar->mapToGlobal(pt))) { - tabbar->setCurrentIndex(index); - // remove all tabs before the one to keep - for (int i = 0; i < index; ++i) { - tabbar->removeTab(0); - } - // remove all tabs after the one to keep - while (tabbar->count() > 1) { + tabbar->moveTab(index, 0); + tabbar->setCurrentIndex(0); + while (tabbar->count() > 1) tabbar->removeTab(1); - } } } } void DetailWidget::setMessage(const QString &message_id) { - if (message_id.isEmpty()) return; - - int index = -1; - for (int i = 0; i < tabbar->count(); ++i) { - if (tabbar->tabText(i) == message_id) { - index = i; - break; - } - } + msg_id = message_id; + int index = tabbar->count() - 1; + for (/**/; index >= 0 && tabbar->tabText(index) != msg_id; --index) { /**/ } if (index == -1) { index = tabbar->addTab(message_id); - auto msg = dbc()->msg(message_id); - tabbar->setTabToolTip(index, msg ? msg->name.c_str() : "untitled"); + tabbar->setTabToolTip(index, msgName(message_id)); } tabbar->setCurrentIndex(index); - msg_id = message_id; dbcMsgChanged(); - scroll->verticalScrollBar()->setValue(0); } void DetailWidget::dbcMsgChanged(int show_form_idx) { if (msg_id.isEmpty()) return; - warning_widget->hide(); - QStringList warnings; + setUpdatesEnabled(false); + + binary_view->setMessage(msg_id); + history_log->setMessage(msg_id); - clearLayout(signals_container->layout()); - QString msg_name = tr("untitled"); - if (auto msg = dbc()->msg(msg_id)) { - for (int i = 0; i < msg->sigs.size(); ++i) { - auto form = new SignalEdit(i, msg_id, &(msg->sigs[i])); - form->setChartOpened(charts->isChartOpened(msg_id, &(msg->sigs[i]))); - signals_container->layout()->addWidget(form); - QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showForm); - QObject::connect(form, &SignalEdit::remove, this, &DetailWidget::removeSignal); - QObject::connect(form, &SignalEdit::save, this, &DetailWidget::saveSignal); - QObject::connect(form, &SignalEdit::highlight, binary_view, &BinaryView::highlight); - QObject::connect(binary_view, &BinaryView::signalHovered, form, &SignalEdit::signalHovered); - QObject::connect(form, &SignalEdit::showChart, [this, sig = &msg->sigs[i]](bool show) { charts->showChart(msg_id, sig, show); }); - if (i == show_form_idx) { - QTimer::singleShot(0, [=]() { emit form->showFormClicked(); }); + int i = 0; + QStringList warnings; + const DBCMsg *msg = dbc()->msg(msg_id); + if (msg) { + for (auto sig : msg->getSignals()) { + SignalEdit *form = i < signal_list.size() ? signal_list[i] : nullptr; + if (!form) { + form = new SignalEdit(i); + QObject::connect(form, &SignalEdit::remove, this, &DetailWidget::removeSignal); + QObject::connect(form, &SignalEdit::save, this, &DetailWidget::saveSignal); + QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showFormClicked); + QObject::connect(form, &SignalEdit::highlight, binary_view, &BinaryView::highlight); + QObject::connect(binary_view, &BinaryView::signalHovered, form, &SignalEdit::signalHovered); + QObject::connect(form, &SignalEdit::showChart, charts, &ChartsWidget::showChart); + signals_layout->addWidget(form); + signal_list.push_back(form); } + form->setSignal(msg_id, sig); + form->setChartOpened(charts->isChartOpened(msg_id, sig)); + ++i; } - msg_name = msg->name.c_str(); if (msg->size != can->lastMessage(msg_id).dat.size()) warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size)); } - edit_btn->setVisible(true); - name_label->setText(msg_name); + for (/**/; i < signal_list.size(); ++i) + signal_list[i]->hide(); - binary_view->setMessage(msg_id); - history_log->setMessage(msg_id); + toolbar->setVisible(!msg_id.isEmpty()); + remove_msg_act->setEnabled(msg != nullptr); + name_label->setText(msgName(msg_id)); - // Check overlapping bits - if (auto overlapping = binary_view->getOverlappingSignals(); !overlapping.isEmpty()) { - for (auto s : overlapping) - warnings.push_back(tr("%1 has overlapping bits.").arg(s->name.c_str())); - } + for (auto s : binary_view->getOverlappingSignals()) + warnings.push_back(tr("%1 has overlapping bits.").arg(s->name.c_str())); - if (!warnings.isEmpty()) { - warning_label->setText(warnings.join('\n')); - warning_widget->show(); - } + warning_label->setText(warnings.join('\n')); + warning_widget->setVisible(!warnings.isEmpty()); + setUpdatesEnabled(true); } -void DetailWidget::updateState() { +void DetailWidget::updateState(const QHash * msgs) { time_label->setText(QString::number(can->currentSec(), 'f', 3)); - if (msg_id.isEmpty()) return; + if (msg_id.isEmpty() || (msgs && !msgs->contains(msg_id))) + return; + + if (tab_widget->currentIndex() == 0) + binary_view->updateState(); + else + history_log->updateState(); +} - binary_view->updateState(); - history_log->updateState(); +void DetailWidget::showFormClicked() { + auto s = qobject_cast(sender()); + showForm(s->sig); } -void DetailWidget::showForm() { - SignalEdit *sender = qobject_cast(QObject::sender()); - for (auto f : signals_container->findChildren()) { - f->setFormVisible(f == sender && !f->isFormVisible()); - if (f == sender) - QTimer::singleShot(0, [=]() { scroll->ensureWidgetVisible(f); }); +void DetailWidget::showForm(const Signal *sig) { + setUpdatesEnabled(false); + for (auto f : signal_list) { + f->updateForm(f->sig == sig && !f->isFormVisible()); + if (f->sig == sig) scroll->ensureWidgetVisible(f); } + setUpdatesEnabled(true); } void DetailWidget::updateChartState(const QString &id, const Signal *sig, bool opened) { - if (id == msg_id) { - for (auto f : signals_container->findChildren()) - if (f->sig == sig) f->setChartOpened(opened); - } + for (auto f : signal_list) + if (f->msg_id == id && f->sig == sig) f->setChartOpened(opened); } void DetailWidget::editMsg() { - auto msg = dbc()->msg(msg_id); - QString name = msg ? msg->name.c_str() : "untitled"; - int size = msg ? msg->size : can->lastMessage(msg_id).dat.size(); - EditMessageDialog dlg(msg_id, name, size, this); + QString id = msg_id; + auto msg = dbc()->msg(id); + int size = msg ? msg->size : can->lastMessage(id).dat.size(); + EditMessageDialog dlg(id, msgName(id), size, this); if (dlg.exec()) { - dbc()->updateMsg(msg_id, dlg.name_edit->text(), dlg.size_spin->value()); - dbcMsgChanged(); + undo_stack->push(new EditMsgCommand(msg_id, dlg.name_edit->text(), dlg.size_spin->value())); } } -void DetailWidget::addSignal(int from, int to) { - if (auto msg = dbc()->msg(msg_id)) { - Signal sig = {}; +void DetailWidget::removeMsg() { + undo_stack->push(new RemoveMsgCommand(msg_id)); +} + +void DetailWidget::addSignal(int start_bit, int size, bool little_endian) { + auto msg = dbc()->msg(msg_id); + if (!msg) { for (int i = 1; /**/; ++i) { - sig.name = "NEW_SIGNAL_" + std::to_string(i); - auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [&](auto &s) { return sig.name == s.name; }); - if (it == msg->sigs.end()) break; + QString name = QString("NEW_MSG_%1").arg(i); + auto it = std::find_if(dbc()->messages().begin(), dbc()->messages().end(), [&](auto &m) { return m.second.name == name; }); + if (it == dbc()->messages().end()) { + undo_stack->push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size())); + msg = dbc()->msg(msg_id); + break; + } } - sig.is_little_endian = false, - updateSigSizeParamsFromRange(sig, from, to); - dbc()->addSignal(msg_id, sig); - dbcMsgChanged(msg->sigs.size() - 1); } + Signal sig = {.is_little_endian = little_endian, .factor = 1}; + for (int i = 1; /**/; ++i) { + sig.name = "NEW_SIGNAL_" + std::to_string(i); + if (msg->sigs.count(sig.name.c_str()) == 0) break; + } + updateSigSizeParamsFromRange(sig, start_bit, size); + undo_stack->push(new AddSigCommand(msg_id, sig)); } -void DetailWidget::resizeSignal(const Signal *sig, int from, int to) { +void DetailWidget::resizeSignal(const Signal *sig, int start_bit, int size) { Signal s = *sig; - updateSigSizeParamsFromRange(s, from, to); + updateSigSizeParamsFromRange(s, start_bit, size); saveSignal(sig, s); } void DetailWidget::saveSignal(const Signal *sig, const Signal &new_sig) { auto msg = dbc()->msg(msg_id); if (new_sig.name != sig->name) { - auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [&](auto &s) { return s.name == new_sig.name; }); + auto it = msg->sigs.find(new_sig.name.c_str()); if (it != msg->sigs.end()) { QString warning_str = tr("There is already a signal with the same name '%1'").arg(new_sig.name.c_str()); QMessageBox::warning(this, tr("Failed to save signal"), warning_str); return; } } - auto [start, end] = getSignalRange(&new_sig); if (start < 0 || end >= msg->size * 8) { QString warning_str = tr("Signal size [%1] exceed limit").arg(new_sig.size); @@ -263,29 +275,22 @@ void DetailWidget::saveSignal(const Signal *sig, const Signal &new_sig) { return; } - dbc()->updateSignal(msg_id, sig->name.c_str(), new_sig); - // update binary view and history log - dbcMsgChanged(); + undo_stack->push(new EditSignalCommand(msg_id, sig, new_sig)); } void DetailWidget::removeSignal(const Signal *sig) { - QString text = tr("Are you sure you want to remove signal '%1'").arg(sig->name.c_str()); - if (QMessageBox::Yes == QMessageBox::question(this, tr("Remove signal"), text)) { - dbc()->removeSignal(msg_id, sig->name.c_str()); - dbcMsgChanged(); - } + undo_stack->push(new RemoveSigCommand(msg_id, sig)); } // EditMessageDialog EditMessageDialog::EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Edit message")); - QVBoxLayout *main_layout = new QVBoxLayout(this); - - QFormLayout *form_layout = new QFormLayout(); + QFormLayout *form_layout = new QFormLayout(this); form_layout->addRow("ID", new QLabel(msg_id)); name_edit = new QLineEdit(title, this); + name_edit->setValidator(new QRegExpValidator(QRegExp("^(\\w+)"), name_edit)); form_layout->addRow(tr("Name"), name_edit); size_spin = new QSpinBox(this); @@ -294,10 +299,8 @@ EditMessageDialog::EditMessageDialog(const QString &msg_id, const QString &title size_spin->setValue(size); form_layout->addRow(tr("Size"), size_spin); - main_layout->addLayout(form_layout); - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox); + form_layout->addRow(buttonBox); setFixedWidth(parent->width() * 0.9); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index d8784f3f14..41fa0edd41 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -1,7 +1,9 @@ #pragma once #include -#include +#include +#include +#include #include "tools/cabana/binaryview.h" #include "tools/cabana/chartswidget.h" @@ -9,8 +11,6 @@ #include "tools/cabana/signaledit.h" class EditMessageDialog : public QDialog { - Q_OBJECT - public: EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent); @@ -25,29 +25,32 @@ public: DetailWidget(ChartsWidget *charts, QWidget *parent); void setMessage(const QString &message_id); void dbcMsgChanged(int show_form_idx = -1); - -signals: - void binaryViewMoved(bool in); + QUndoStack *undo_stack = nullptr; private: + void showForm(const Signal *sig); + void showFormClicked(); void updateChartState(const QString &id, const Signal *sig, bool opened); void showTabBarContextMenu(const QPoint &pt); - void addSignal(int start_bit, int to); + void addSignal(int start_bit, int size, bool little_endian); void resizeSignal(const Signal *sig, int from, int to); void saveSignal(const Signal *sig, const Signal &new_sig); void removeSignal(const Signal *sig); void editMsg(); - void showForm(); - void updateState(); + void removeMsg(); + void updateState(const QHash * msgs = nullptr); QString msg_id; QLabel *name_label, *time_label, *warning_label; QWidget *warning_widget; - QPushButton *edit_btn; - QWidget *signals_container; + QVBoxLayout *signals_layout; QTabBar *tabbar; + QTabWidget *tab_widget; + QToolBar *toolbar; + QAction *remove_msg_act; HistoryLog *history_log; BinaryView *binary_view; QScrollArea *scroll; ChartsWidget *charts; + QList signal_list; }; diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc index 5737421f28..1b0898afbd 100644 --- a/tools/cabana/historylog.cc +++ b/tools/cabana/historylog.cc @@ -1,8 +1,12 @@ #include "tools/cabana/historylog.h" #include -#include -#include + +// HistoryLogModel + +inline const Signal &get_signal(const DBCMsg *m, int index) { + return std::next(m->sigs.begin(), index)->second; +} QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { bool has_signal = dbc_msg && !dbc_msg->sigs.empty(); @@ -11,7 +15,7 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { if (index.column() == 0) { return QString::number(m.ts, 'f', 2); } - return has_signal ? QString::number(get_raw_value((uint8_t *)m.dat.begin(), m.dat.size(), dbc_msg->sigs[index.column() - 1])) + return has_signal ? QString::number(get_raw_value((uint8_t *)m.dat.begin(), m.dat.size(), get_signal(dbc_msg, index.column() - 1))) : toHex(m.dat); } else if (role == Qt::FontRole && index.column() == 1 && !has_signal) { return QFontDatabase::systemFont(QFontDatabase::FixedFont); @@ -37,7 +41,7 @@ QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, i if (section == 0) { return "Time"; } - return has_signal ? dbc_msg->sigs[section - 1].name.c_str() : "Data"; + return has_signal ? QString::fromStdString(get_signal(dbc_msg, section - 1).name).replace('_', ' ') : "Data"; } else if (role == Qt::BackgroundRole && section > 0 && has_signal) { return QBrush(QColor(getColor(section - 1))); } @@ -64,15 +68,30 @@ void HistoryLogModel::updateState() { } } +// HeaderView + +QSize HeaderView::sectionSizeFromContents(int logicalIndex) const { + const QString text = model()->headerData(logicalIndex, this->orientation(), Qt::DisplayRole).toString(); + const QRect rect = fontMetrics().boundingRect(QRect(0, 0, sectionSize(logicalIndex), 1000), defaultAlignment(), text); + return rect.size() + QSize{10, 5}; +} + +// HistoryLog + HistoryLog::HistoryLog(QWidget *parent) : QTableView(parent) { model = new HistoryLogModel(this); setModel(model); + setHorizontalHeader(new HeaderView(Qt::Horizontal, this)); horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | (Qt::Alignment)Qt::TextWordWrap); horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); verticalHeader()->setVisible(false); - setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); setFrameShape(QFrame::NoFrame); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); setStyleSheet("QTableView::item { border:0px; padding-left:5px; padding-right:5px; }"); } + +int HistoryLog::sizeHintForColumn(int column) const { + // sizeHintForColumn is only called for column 0 (ResizeToContents) + return itemDelegate()->sizeHint(viewOptions(), model->index(0, 0)).width() + 1; // +1 for grid +} diff --git a/tools/cabana/historylog.h b/tools/cabana/historylog.h index 0bfc409fe1..21be8fc129 100644 --- a/tools/cabana/historylog.h +++ b/tools/cabana/historylog.h @@ -1,10 +1,17 @@ #pragma once +#include #include #include "tools/cabana/canmessages.h" #include "tools/cabana/dbcmanager.h" +class HeaderView : public QHeaderView { +public: + HeaderView(Qt::Orientation orientation, QWidget *parent = nullptr) : QHeaderView(orientation, parent) {} + QSize sectionSizeFromContents(int logicalIndex) const override; +}; + class HistoryLogModel : public QAbstractTableModel { Q_OBJECT @@ -21,7 +28,7 @@ private: QString msg_id; int row_count = 0; int column_count = 2; - const Msg *dbc_msg = nullptr; + const DBCMsg *dbc_msg = nullptr; std::deque messages; }; @@ -33,5 +40,6 @@ public: void setMessage(const QString &message_id) { model->setMessage(message_id); } void updateState() { model->updateState(); } private: + int sizeHintForColumn(int column) const override; HistoryLogModel *model; }; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index c2baca4d22..324323ac44 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -1,36 +1,64 @@ #include "tools/cabana/mainwin.h" +#include #include +#include +#include +#include +#include +#include #include +#include +#include +#include #include +#include +#include #include +#include #include "tools/replay/util.h" static MainWindow *main_win = nullptr; void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { + if (type == QtDebugMsg) std::cout << msg.toStdString() << std::endl; if (main_win) emit main_win->showMessage(msg, 0); } -MainWindow::MainWindow() : QWidget() { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(11, 11, 11, 5); +MainWindow::MainWindow() : QMainWindow() { + setWindowTitle("Cabana"); + QWidget *central_widget = new QWidget(this); + QHBoxLayout *main_layout = new QHBoxLayout(central_widget); + main_layout->setContentsMargins(11, 11, 11, 0); main_layout->setSpacing(0); - QHBoxLayout *h_layout = new QHBoxLayout(); - h_layout->setContentsMargins(0, 0, 0, 0); - main_layout->addLayout(h_layout); - splitter = new QSplitter(Qt::Horizontal, this); splitter->setHandleWidth(11); + + // DBC file selector + QWidget *messages_container = new QWidget(this); + QVBoxLayout *messages_layout = new QVBoxLayout(messages_container); + messages_layout->setContentsMargins(0, 0, 0, 0); + dbc_combo = new QComboBox(this); + auto dbc_names = dbc()->allDBCNames(); + for (const auto &name : dbc_names) { + dbc_combo->addItem(QString::fromStdString(name)); + } + dbc_combo->model()->sort(0); + dbc_combo->setInsertPolicy(QComboBox::NoInsert); + messages_layout->addWidget(dbc_combo); + messages_widget = new MessagesWidget(this); - splitter->addWidget(messages_widget); + messages_layout->addWidget(messages_widget); + splitter->addWidget(messages_container); charts_widget = new ChartsWidget(this); detail_widget = new DetailWidget(charts_widget, this); splitter->addWidget(detail_widget); - - h_layout->addWidget(splitter); + if (!settings.splitter_state.isEmpty()) { + splitter->restoreState(settings.splitter_state); + } + main_layout->addWidget(splitter); // right widgets QWidget *right_container = new QWidget(this); @@ -38,35 +66,22 @@ MainWindow::MainWindow() : QWidget() { r_layout = new QVBoxLayout(right_container); r_layout->setContentsMargins(11, 0, 0, 0); QHBoxLayout *right_hlayout = new QHBoxLayout(); - QLabel *fingerprint_label = new QLabel(this); - right_hlayout->addWidget(fingerprint_label); + fingerprint_label = new QLabel(this); + right_hlayout->addWidget(fingerprint_label, 0, Qt::AlignLeft); // TODO: click to select another route. - right_hlayout->addWidget(new QLabel(can->route())); - QPushButton *settings_btn = new QPushButton("Settings"); - right_hlayout->addWidget(settings_btn, 0, Qt::AlignRight); - + right_hlayout->addWidget(new QLabel(can->route()), 0, Qt::AlignRight); r_layout->addLayout(right_hlayout); video_widget = new VideoWidget(this); r_layout->addWidget(video_widget, 0, Qt::AlignTop); + r_layout->addWidget(charts_widget, 1); + r_layout->addStretch(0); + main_layout->addWidget(right_container); - r_layout->addWidget(charts_widget); - - h_layout->addWidget(right_container); - - // status bar - status_bar = new QStatusBar(this); - status_bar->setFixedHeight(20); - status_bar->setContentsMargins(0, 0, 0, 0); - status_bar->setSizeGripEnabled(true); - progress_bar = new QProgressBar(); - progress_bar->setRange(0, 100); - progress_bar->setTextVisible(true); - progress_bar->setFixedSize({230, 16}); - progress_bar->setVisible(false); - status_bar->addPermanentWidget(progress_bar); - main_layout->addWidget(status_bar); + setCentralWidget(central_widget); + createActions(); + createStatusBar(); qRegisterMetaType("uint64_t"); qRegisterMetaType("ReplyMsgType"); @@ -78,21 +93,123 @@ MainWindow::MainWindow() : QWidget() { emit updateProgressBar(cur, total, success); }); - QObject::connect(this, &MainWindow::showMessage, status_bar, &QStatusBar::showMessage); + main_win = this; + qInstallMessageHandler(qLogMessageHandler); + QFile json_file("./car_fingerprint_to_dbc.json"); + if (json_file.open(QIODevice::ReadOnly)) { + fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); + } + + QObject::connect(dbc_combo, SIGNAL(activated(const QString &)), SLOT(loadDBCFromName(const QString &))); + QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage); QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress); QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, detail_widget, &DetailWidget::setMessage); - QObject::connect(detail_widget, &DetailWidget::binaryViewMoved, [this](bool in) { splitter->setSizes({in ? 100 : 0, 500}); }); QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts); QObject::connect(charts_widget, &ChartsWidget::rangeChanged, video_widget, &VideoWidget::rangeChanged); - QObject::connect(settings_btn, &QPushButton::clicked, this, &MainWindow::setOption); - QObject::connect(can, &CANMessages::eventsMerged, [=]() { fingerprint_label->setText(can->carFingerprint() ); }); + QObject::connect(can, &CANMessages::streamStarted, this, &MainWindow::loadDBCFromFingerprint); + QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() { + detail_widget->undo_stack->clear(); + dbc_combo->setCurrentText(QFileInfo(dbc()->name()).baseName()); + setWindowTitle(tr("%1 - Cabana").arg(dbc()->name())); + }); + QObject::connect(detail_widget->undo_stack, &QUndoStack::indexChanged, [this](int index) { + setWindowTitle(tr("%1%2 - Cabana").arg(index > 0 ? "* " : "").arg(dbc()->name())); + }); +} - main_win = this; - qInstallMessageHandler(qLogMessageHandler); +void MainWindow::createActions() { + QMenu *file_menu = menuBar()->addMenu(tr("&File")); + file_menu->addAction(tr("Open DBC File..."), this, &MainWindow::loadDBCFromFile); + file_menu->addAction(tr("Load DBC From Clipboard"), this, &MainWindow::loadDBCFromClipboard); + file_menu->addSeparator(); + file_menu->addAction(tr("Save DBC As..."), this, &MainWindow::saveDBCToFile); + file_menu->addAction(tr("Copy DBC To Clipboard"), this, &MainWindow::saveDBCToClipboard); + file_menu->addSeparator(); + file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption); + + QMenu *edit_menu = menuBar()->addMenu(tr("&Edit")); + auto undo_act = detail_widget->undo_stack->createUndoAction(this, tr("&Undo")); + undo_act->setShortcuts(QKeySequence::Undo); + edit_menu->addAction(undo_act); + auto redo_act = detail_widget->undo_stack->createRedoAction(this, tr("&Rndo")); + redo_act->setShortcuts(QKeySequence::Redo); + edit_menu->addAction(redo_act); + edit_menu->addSeparator(); + + QMenu *commands_menu = edit_menu->addMenu(tr("Command &List")); + auto undo_view = new QUndoView(detail_widget->undo_stack); + undo_view->setWindowTitle(tr("Command List")); + QWidgetAction *commands_act = new QWidgetAction(this); + commands_act->setDefaultWidget(undo_view); + commands_menu->addAction(commands_act); + + QMenu *help_menu = menuBar()->addMenu(tr("&Help")); + help_menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt); +} + +void MainWindow::createStatusBar() { + progress_bar = new QProgressBar(); + progress_bar->setRange(0, 100); + progress_bar->setTextVisible(true); + progress_bar->setFixedSize({230, 16}); + progress_bar->setVisible(false); + statusBar()->addPermanentWidget(progress_bar); +} + +void MainWindow::loadDBCFromName(const QString &name) { + if (name != dbc()->name()) + dbc()->open(name); +} + +void MainWindow::loadDBCFromFile() { + QString file_name = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); + if (!file_name.isEmpty()) { + settings.last_dir = QFileInfo(file_name).absolutePath(); + QFile file(file_name); + if (file.open(QIODevice::ReadOnly)) { + auto dbc_name = QFileInfo(file_name).baseName(); + dbc()->open(dbc_name, file.readAll()); + } + } +} + +void MainWindow::loadDBCFromClipboard() { + QString dbc_str = QGuiApplication::clipboard()->text(); + dbc()->open("From Clipboard", dbc_str); + QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!")); +} + +void MainWindow::loadDBCFromFingerprint() { + auto fingerprint = can->carFingerprint(); + fingerprint_label->setText(fingerprint); + if (!fingerprint.isEmpty() && dbc()->name().isEmpty()) { + auto dbc_name = fingerprint_to_dbc[fingerprint]; + if (dbc_name != QJsonValue::Undefined) { + loadDBCFromName(dbc_name.toString()); + } + } +} + +void MainWindow::saveDBCToFile() { + QString file_name = QFileDialog::getSaveFileName(this, tr("Save File"), + QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); + if (!file_name.isEmpty()) { + settings.last_dir = QFileInfo(file_name).absolutePath(); + QFile file(file_name); + if (file.open(QIODevice::WriteOnly)) { + file.write(dbc()->generateDBC().toUtf8()); + detail_widget->undo_stack->clear(); + } + } +} + +void MainWindow::saveDBCToClipboard() { + QGuiApplication::clipboard()->setText(dbc()->generateDBC()); + QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!")); } void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) { - if (success && cur < total) { + 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(); @@ -104,11 +221,12 @@ void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool succe void MainWindow::dockCharts(bool dock) { if (dock && floating_window) { floating_window->removeEventFilter(charts_widget); - r_layout->addWidget(charts_widget); + r_layout->insertWidget(2, charts_widget, 1); floating_window->deleteLater(); floating_window = nullptr; } else if (!dock && !floating_window) { floating_window = new QWidget(nullptr); + floating_window->setWindowTitle("Charts - Cabana"); floating_window->setLayout(new QVBoxLayout()); floating_window->layout()->addWidget(charts_widget); floating_window->installEventFilter(charts_widget); @@ -118,9 +236,22 @@ void MainWindow::dockCharts(bool dock) { } void MainWindow::closeEvent(QCloseEvent *event) { + if (detail_widget->undo_stack->index() > 0) { + auto ret = QMessageBox::question(this, tr("Unsaved Changes"), + tr("Are you sure you want to exit without saving?\nAny unsaved changes will be lost."), + QMessageBox::Yes | QMessageBox::No); + if (ret == QMessageBox::No) { + event->ignore(); + return; + } + } + main_win = nullptr; if (floating_window) floating_window->deleteLater(); + + settings.splitter_state = splitter->saveState(); + settings.save(); QWidget::closeEvent(event); } diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index b77744ba9c..bb9280c3ea 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #include #include @@ -9,19 +12,29 @@ #include "tools/cabana/messageswidget.h" #include "tools/cabana/videowidget.h" -class MainWindow : public QWidget { +class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(); void dockCharts(bool dock); - void showStatusMessage(const QString &msg, int timeout = 0) { status_bar->showMessage(msg, timeout); } + void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); } + +public slots: + void loadDBCFromName(const QString &name); + void loadDBCFromFingerprint(); + void loadDBCFromFile(); + void loadDBCFromClipboard(); + void saveDBCToFile(); + void saveDBCToClipboard(); signals: void showMessage(const QString &msg, int timeout); void updateProgressBar(uint64_t cur, uint64_t total, bool success); protected: + void createActions(); + void createStatusBar(); void closeEvent(QCloseEvent *event) override; void updateDownloadProgress(uint64_t cur, uint64_t total, bool success); void setOption(); @@ -34,5 +47,7 @@ protected: QWidget *floating_window = nullptr; QVBoxLayout *r_layout; QProgressBar *progress_bar; - QStatusBar *status_bar; + QLabel *fingerprint_label; + QJsonDocument fingerprint_to_dbc; + QComboBox *dbc_combo; }; diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index e80a66bce9..dbf87a3da2 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -1,15 +1,8 @@ #include "tools/cabana/messageswidget.h" -#include -#include -#include -#include -#include #include #include #include -#include -#include #include #include "tools/cabana/dbcmanager.h" @@ -18,31 +11,6 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); - // DBC file selector - QHBoxLayout *dbc_file_layout = new QHBoxLayout(); - dbc_combo = new QComboBox(this); - auto dbc_names = dbc()->allDBCNames(); - for (const auto &name : dbc_names) { - dbc_combo->addItem(QString::fromStdString(name)); - } - dbc_combo->model()->sort(0); - dbc_combo->setEditable(true); - dbc_combo->setCurrentText(QString()); - dbc_combo->setInsertPolicy(QComboBox::NoInsert); - dbc_combo->completer()->setCompletionMode(QCompleter::PopupCompletion); - QFont font; - font.setBold(true); - dbc_combo->lineEdit()->setFont(font); - dbc_file_layout->addWidget(dbc_combo); - - QPushButton *load_from_paste = new QPushButton(tr("Load from paste"), this); - dbc_file_layout->addWidget(load_from_paste); - - dbc_file_layout->addStretch(); - QPushButton *save_btn = new QPushButton(tr("Save DBC"), this); - dbc_file_layout->addWidget(save_btn); - main_layout->addLayout(dbc_file_layout); - // message filter QLineEdit *filter = new QLineEdit(this); filter->setClearButtonEnabled(true); @@ -67,55 +35,22 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { // signals/slots QObject::connect(filter, &QLineEdit::textChanged, model, &MessageListModel::setFilterString); - QObject::connect(can, &CANMessages::eventsMerged, this, &MessagesWidget::loadDBCFromFingerprint); - QObject::connect(can, &CANMessages::updated, [this]() { model->updateState(); }); - QObject::connect(dbc_combo, SIGNAL(activated(const QString &)), SLOT(loadDBCFromName(const QString &))); - QObject::connect(load_from_paste, &QPushButton::clicked, this, &MessagesWidget::loadDBCFromPaste); - QObject::connect(save_btn, &QPushButton::clicked, this, &MessagesWidget::saveDBC); + QObject::connect(can, &CANMessages::msgsReceived, model, &MessageListModel::msgsReceived); + QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::sortMessages); + QObject::connect(dbc(), &DBCManager::msgUpdated, model, &MessageListModel::sortMessages); + QObject::connect(dbc(), &DBCManager::msgRemoved, model, &MessageListModel::sortMessages); QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { - if (current.isValid()) { - emit msgSelectionChanged(current.data(Qt::UserRole).toString()); + if (current.isValid() && current.row() < model->msgs.size()) { + if (model->msgs[current.row()] != current_msg_id) { + current_msg_id = model->msgs[current.row()]; + emit msgSelectionChanged(current_msg_id); + } } }); - - QFile json_file("./car_fingerprint_to_dbc.json"); - if (json_file.open(QIODevice::ReadOnly)) { - fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); - } -} - -void MessagesWidget::loadDBCFromName(const QString &name) { - if (name != dbc()->name()) { - dbc()->open(name); - dbc_combo->setCurrentText(name); - // re-sort model to refresh column 'Name' - model->updateState(true); - } -} - -void MessagesWidget::loadDBCFromPaste() { - LoadDBCDialog dlg(this); - if (dlg.exec()) { - dbc()->open("from paste", dlg.dbc_edit->toPlainText()); - dbc_combo->setCurrentText("loaded from paste"); - model->updateState(true); - } -} - -void MessagesWidget::loadDBCFromFingerprint() { - auto fingerprint = can->carFingerprint(); - if (!fingerprint.isEmpty() && dbc()->name().isEmpty()) { - auto dbc_name = fingerprint_to_dbc[fingerprint]; - if (dbc_name != QJsonValue::Undefined) { - loadDBCFromName(dbc_name.toString()); - } - } -} - -void MessagesWidget::saveDBC() { - SaveDBCDialog dlg(this); - dlg.dbc_edit->setText(dbc()->generateDBC()); - dlg.exec(); + QObject::connect(model, &MessageListModel::modelReset, [this]() { + if (int row = model->msgs.indexOf(current_msg_id); row != -1) + table_widget->selectionModel()->setCurrentIndex(model->index(row, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); + }); } // MessageListModel @@ -128,103 +63,71 @@ QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, QVariant MessageListModel::data(const QModelIndex &index, int role) const { if (role == Qt::DisplayRole) { - const auto &m = msgs[index.row()]; - auto &can_data = can->lastMessage(m->id); + const auto &id = msgs[index.row()]; + auto &can_data = can->lastMessage(id); switch (index.column()) { - case 0: return m->name; - case 1: return m->id; + case 0: return msgName(id); + case 1: return id; case 2: return can_data.freq; case 3: return can_data.count; case 4: return toHex(can_data.dat); } - } else if (role == Qt::UserRole) { - return msgs[index.row()]->id; - } else if (role == Qt::FontRole) { - if (index.column() == columnCount() - 1) { - return QFontDatabase::systemFont(QFontDatabase::FixedFont); - } + } else if (role == Qt::FontRole && index.column() == columnCount() - 1) { + return QFontDatabase::systemFont(QFontDatabase::FixedFont); } return {}; } -void MessageListModel::setFilterString(const QString &string) { +void MessageListModel::setFilterString(const QString &string) { filter_str = string; - updateState(true); -} - -bool MessageListModel::updateMessages(bool sort) { - if (msgs.size() == can->can_msgs.size() && filter_str.isEmpty() && !sort) - return false; - - // update message list - int i = 0; bool search_id = filter_str.contains(':'); + msgs.clear(); for (auto it = can->can_msgs.begin(); it != can->can_msgs.end(); ++it) { - const Msg *msg = dbc()->msg(it.key()); - QString msg_name = msg ? msg->name.c_str() : "untitled"; - if (!filter_str.isEmpty() && !(search_id ? it.key() : msg_name).contains(filter_str, Qt::CaseInsensitive)) - continue; - auto &m = i < msgs.size() ? msgs[i] : msgs.emplace_back(new Message); - m->id = it.key(); - m->name = msg_name; - ++i; + if ((search_id ? it.key() : msgName(it.key())).contains(filter_str, Qt::CaseInsensitive)) + msgs.push_back(it.key()); } - msgs.resize(i); + sortMessages(); +} +void MessageListModel::sortMessages() { + beginResetModel(); if (sort_column == 0) { std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { - bool ret = l->name < r->name || (l->name == r->name && l->id < r->id); + bool ret = std::pair{msgName(l), l} < std::pair{msgName(r), r}; return sort_order == Qt::AscendingOrder ? ret : !ret; }); } else if (sort_column == 1) { std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { - return sort_order == Qt::AscendingOrder ? l->id < r->id : l->id > r->id; + return sort_order == Qt::AscendingOrder ? l < r : l > r; }); } else if (sort_column == 2) { - // sort by frequency std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { - uint32_t lfreq = can->lastMessage(l->id).freq; - uint32_t rfreq = can->lastMessage(r->id).freq; - bool ret = lfreq < rfreq || (lfreq == rfreq && l->id < r->id); + bool ret = std::pair{can->lastMessage(l).freq, l} < std::pair{can->lastMessage(r).freq, r}; return sort_order == Qt::AscendingOrder ? ret : !ret; }); } else if (sort_column == 3) { - // sort by count std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { - uint32_t lcount = can->lastMessage(l->id).count; - uint32_t rcount = can->lastMessage(r->id).count; - bool ret = lcount < rcount || (lcount == rcount && l->id < r->id); + bool ret = std::pair{can->lastMessage(l).count, l} < std::pair{can->lastMessage(r).count, r}; return sort_order == Qt::AscendingOrder ? ret : !ret; }); } - return true; + endResetModel(); } -void MessageListModel::updateState(bool sort) { +void MessageListModel::msgsReceived(const QHash *new_msgs) { int prev_row_count = msgs.size(); - auto prev_idx = persistentIndexList(); - QString selected_msg_id = prev_idx.empty() ? "" : prev_idx[0].data(Qt::UserRole).toString(); - - bool msg_updated = updateMessages(sort); - int delta = msgs.size() - prev_row_count; - if (delta > 0) { - beginInsertRows({}, prev_row_count, msgs.size() - 1); - endInsertRows(); - } else if (delta < 0) { - beginRemoveRows({}, msgs.size(), prev_row_count - 1); - endRemoveRows(); + if (filter_str.isEmpty() && msgs.size() != can->can_msgs.size()) { + msgs = can->can_msgs.keys(); } - - if (!msgs.empty()) { - if (msg_updated && !prev_idx.isEmpty()) { - // keep selection - auto it = std::find_if(msgs.begin(), msgs.end(), [&](auto &m) { return m->id == selected_msg_id; }); - if (it != msgs.end()) { - for (auto &idx : prev_idx) - changePersistentIndex(idx, index(std::distance(msgs.begin(), it), idx.column())); - } + if (msgs.size() != prev_row_count) { + sortMessages(); + return; + } + for (int i = 0; i < msgs.size(); ++i) { + if (new_msgs->contains(msgs[i])) { + for (int col = 2; col < columnCount(); ++col) + emit dataChanged(index(i, col), index(i, col), {Qt::DisplayRole}); } - emit dataChanged(index(0, 0), index(msgs.size() - 1, 3), {Qt::DisplayRole}); } } @@ -232,63 +135,6 @@ void MessageListModel::sort(int column, Qt::SortOrder order) { if (column != columnCount() - 1) { sort_column = column; sort_order = order; - updateState(true); - } -} - -// LoadDBCDialog - -LoadDBCDialog::LoadDBCDialog(QWidget *parent) : QDialog(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - dbc_edit = new QTextEdit(this); - dbc_edit->setAcceptRichText(false); - dbc_edit->setPlaceholderText(tr("paste DBC file here")); - main_layout->addWidget(dbc_edit); - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox); - - setMinimumSize({640, 480}); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -// SaveDBCDialog - -SaveDBCDialog::SaveDBCDialog(QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("Save DBC")); - QVBoxLayout *main_layout = new QVBoxLayout(this); - dbc_edit = new QTextEdit(this); - dbc_edit->setAcceptRichText(false); - main_layout->addWidget(dbc_edit); - - QPushButton *copy_to_clipboard = new QPushButton(tr("Copy To Clipboard"), this); - QPushButton *save_as = new QPushButton(tr("Save As"), this); - - QHBoxLayout *btn_layout = new QHBoxLayout(); - btn_layout->addStretch(); - btn_layout->addWidget(copy_to_clipboard); - btn_layout->addWidget(save_as); - main_layout->addLayout(btn_layout); - setMinimumSize({640, 480}); - - QObject::connect(copy_to_clipboard, &QPushButton::clicked, this, &SaveDBCDialog::copytoClipboard); - QObject::connect(save_as, &QPushButton::clicked, this, &SaveDBCDialog::saveAs); -} - -void SaveDBCDialog::copytoClipboard() { - dbc_edit->selectAll(); - dbc_edit->copy(); - QDialog::accept(); -} - -void SaveDBCDialog::saveAs() { - QString file_name = QFileDialog::getSaveFileName(this, tr("Save File"), - QDir::homePath() + "/untitled.dbc", tr("DBC (*.dbc)")); - if (!file_name.isEmpty()) { - QFile file(file_name); - if (file.open(QIODevice::WriteOnly)) { - file.write(dbc_edit->toPlainText().toUtf8()); - } - QDialog::accept(); + sortMessages(); } } diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index 255dce7dc8..3a42bed4be 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -1,32 +1,10 @@ #pragma once #include -#include -#include -#include #include -#include #include "tools/cabana/canmessages.h" -class LoadDBCDialog : public QDialog { - Q_OBJECT - -public: - LoadDBCDialog(QWidget *parent); - QTextEdit *dbc_edit; -}; - -class SaveDBCDialog : public QDialog { - Q_OBJECT - -public: - SaveDBCDialog(QWidget *parent); - void copytoClipboard(); - void saveAs(); - QTextEdit *dbc_edit; -}; - class MessageListModel : public QAbstractTableModel { Q_OBJECT @@ -37,16 +15,12 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; int rowCount(const QModelIndex &parent = QModelIndex()) const override { return msgs.size(); } void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; - void updateState(bool sort = false); void setFilterString(const QString &string); + void msgsReceived(const QHash *new_msgs = nullptr); + void sortMessages(); + QStringList msgs; private: - bool updateMessages(bool sort); - - struct Message { - QString id, name; - }; - std::vector> msgs; QString filter_str; int sort_column = 0; Qt::SortOrder sort_order = Qt::AscendingOrder; @@ -57,19 +31,11 @@ class MessagesWidget : public QWidget { public: MessagesWidget(QWidget *parent); - -public slots: - void loadDBCFromName(const QString &name); - void loadDBCFromFingerprint(); - void loadDBCFromPaste(); - void saveDBC(); - signals: void msgSelectionChanged(const QString &message_id); protected: QTableView *table_widget; - QComboBox *dbc_combo; + QString current_msg_id; MessageListModel *model; - QJsonDocument fingerprint_to_dbc; }; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index bba59c0d74..be806aa705 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -1,6 +1,7 @@ #include "tools/cabana/settings.h" #include +#include #include #include @@ -19,7 +20,8 @@ void Settings::save() { s.setValue("chart_height", chart_height); s.setValue("chart_theme", chart_theme); s.setValue("max_chart_x_range", max_chart_x_range); - emit changed(); + s.setValue("last_dir", last_dir); + s.setValue("splitter_state", splitter_state); } void Settings::load() { @@ -30,14 +32,15 @@ void Settings::load() { chart_height = s.value("chart_height", 200).toInt(); chart_theme = s.value("chart_theme", 0).toInt(); max_chart_x_range = s.value("max_chart_x_range", 3 * 60).toInt(); + last_dir = s.value("last_dir", QDir::homePath()).toString(); + splitter_state = s.value("splitter_state").toByteArray(); } // SettingsDlg SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Settings")); - QVBoxLayout *main_layout = new QVBoxLayout(this); - QFormLayout *form_layout = new QFormLayout(); + QFormLayout *form_layout = new QFormLayout(this); fps = new QSpinBox(this); fps->setRange(10, 100); @@ -74,10 +77,8 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { chart_theme->setCurrentIndex(settings.chart_theme == 1 ? 1 : 0); form_layout->addRow(tr("Chart theme"), chart_theme); - main_layout->addLayout(form_layout); - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox); + form_layout->addRow(buttonBox); setFixedWidth(360); connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); @@ -93,4 +94,5 @@ void SettingsDlg::save() { settings.max_chart_x_range = max_chart_x_range->value() * 60; settings.save(); accept(); + emit settings.changed(); } diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index cb858de873..624a1ce33d 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -18,6 +19,8 @@ public: int chart_height = 200; int chart_theme = 0; int max_chart_x_range = 3 * 60; // 3 minutes + QString last_dir; + QByteArray splitter_state; signals: void changed(); diff --git a/tools/cabana/signaledit.cc b/tools/cabana/signaledit.cc index ef0a85eba3..5d627a49da 100644 --- a/tools/cabana/signaledit.cc +++ b/tools/cabana/signaledit.cc @@ -1,52 +1,47 @@ #include "tools/cabana/signaledit.h" -#include #include #include +#include #include -#include -#include #include +#include #include #include "selfdrive/ui/qt/util.h" // SignalForm -SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) { +SignalForm::SignalForm(QWidget *parent) : QWidget(parent) { QFormLayout *form_layout = new QFormLayout(this); - name = new QLineEdit(sig.name.c_str()); + name = new QLineEdit(); + name->setValidator(new QRegExpValidator(QRegExp("^(\\w+)"), name)); form_layout->addRow(tr("Name"), name); size = new QSpinBox(); size->setMinimum(1); - size->setValue(sig.size); form_layout->addRow(tr("Size"), size); endianness = new QComboBox(); endianness->addItems({"Little", "Big"}); - endianness->setCurrentIndex(sig.is_little_endian ? 0 : 1); form_layout->addRow(tr("Endianness"), endianness); - form_layout->addRow(tr("lsb"), new QLabel(QString::number(sig.lsb))); - form_layout->addRow(tr("msb"), new QLabel(QString::number(sig.msb))); + form_layout->addRow(tr("lsb"), lsb = new QLabel()); + form_layout->addRow(tr("msb"), msb = new QLabel()); sign = new QComboBox(); sign->addItems({"Signed", "Unsigned"}); - sign->setCurrentIndex(sig.is_signed ? 0 : 1); form_layout->addRow(tr("sign"), sign); auto double_validator = new QDoubleValidator(this); factor = new QLineEdit(); factor->setValidator(double_validator); - factor->setText(QString::number(sig.factor)); form_layout->addRow(tr("Factor"), factor); offset = new QLineEdit(); offset->setValidator(double_validator); - offset->setText(QString::number(sig.offset)); form_layout->addRow(tr("Offset"), offset); // TODO: parse the following parameters in opendbc @@ -62,53 +57,59 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : QWidget(parent) { form_layout->addRow(tr("Maximum value"), max_val); val_desc = new QLineEdit(); form_layout->addRow(tr("Value descriptions"), val_desc); + + QObject::connect(name, &QLineEdit::textEdited, this, &SignalForm::changed); + QObject::connect(factor, &QLineEdit::textEdited, this, &SignalForm::changed); + QObject::connect(offset, &QLineEdit::textEdited, this, &SignalForm::changed); + QObject::connect(sign, SIGNAL(activated(int)), SIGNAL(changed())); + QObject::connect(endianness, SIGNAL(activated(int)), SIGNAL(changed())); + QObject::connect(size, SIGNAL(valueChanged(int)), SIGNAL(changed())); } // SignalEdit -SignalEdit::SignalEdit(int index, const QString &msg_id, const Signal *sig, QWidget *parent) : msg_id(msg_id), sig(sig), form_idx(index), QWidget(parent) { +SignalEdit::SignalEdit(int index, QWidget *parent) : form_idx(index), QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); + main_layout->setSpacing(0); // title bar - QHBoxLayout *title_layout = new QHBoxLayout(); - icon = new QLabel(">"); - icon->setStyleSheet("font-weight:bold"); + auto title_bar = new QWidget(this); + title_bar->setFixedHeight(32); + QHBoxLayout *title_layout = new QHBoxLayout(title_bar); + title_layout->setContentsMargins(0, 0, 0, 0); + title_bar->setStyleSheet("QToolButton {width:15px;height:15px;font-size:15px}"); + color_label = new QLabel(this); + color_label->setFixedWidth(25); + color_label->setContentsMargins(5, 0, 0, 0); + title_layout->addWidget(color_label); + icon = new QLabel(this); title_layout->addWidget(icon); title = new ElidedLabel(this); - title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); - title->setText(QString("%1. %2").arg(index + 1).arg(sig->name.c_str())); - title->setStyleSheet(QString("font-weight:bold; color:%1").arg(getColor(index))); - title_layout->addWidget(title, 1); + title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + title_layout->addWidget(title); - QPushButton *seek_btn = new QPushButton("⌕"); - seek_btn->setStyleSheet("QPushButton{font-weight:bold;font-size:18px}"); + plot_btn = new QToolButton(this); + plot_btn->setText("📈"); + plot_btn->setCheckable(true); + plot_btn->setAutoRaise(true); + title_layout->addWidget(plot_btn); + auto seek_btn = new QToolButton(this); + seek_btn->setText("🔍"); + seek_btn->setAutoRaise(true); seek_btn->setToolTip(tr("Find signal values")); - seek_btn->setFixedSize(25, 25); title_layout->addWidget(seek_btn); - - plot_btn = new QPushButton(this); - plot_btn->setStyleSheet("QPushButton {font-size:18px}"); - plot_btn->setFixedSize(25, 25); - title_layout->addWidget(plot_btn); - main_layout->addLayout(title_layout); + auto remove_btn = new QToolButton(this); + remove_btn->setAutoRaise(true); + remove_btn->setText("x"); + remove_btn->setToolTip(tr("Remove signal")); + title_layout->addWidget(remove_btn); + main_layout->addWidget(title_bar); // signal form - form_container = new QWidget(this); - QVBoxLayout *v_layout = new QVBoxLayout(form_container); - form = new SignalForm(*sig, this); - v_layout->addWidget(form); - - QHBoxLayout *h = new QHBoxLayout(); - QPushButton *remove_btn = new QPushButton(tr("Remove Signal")); - h->addWidget(remove_btn); - h->addStretch(); - QPushButton *save_btn = new QPushButton(tr("Save")); - h->addWidget(save_btn); - v_layout->addLayout(h); - - form_container->setVisible(false); - main_layout->addWidget(form_container); + form = new SignalForm(this); + form->setVisible(false); + main_layout->addWidget(form); // bottom line QFrame *hline = new QFrame(); @@ -116,24 +117,52 @@ SignalEdit::SignalEdit(int index, const QString &msg_id, const Signal *sig, QWid hline->setFrameShadow(QFrame::Sunken); main_layout->addWidget(hline); - QObject::connect(remove_btn, &QPushButton::clicked, [this]() { emit remove(this->sig); }); + save_timer = new QTimer(this); + save_timer->setInterval(300); + save_timer->setSingleShot(true); + save_timer->callOnTimeout(this, &SignalEdit::saveSignal); + QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked); - QObject::connect(save_btn, &QPushButton::clicked, this, &SignalEdit::saveSignal); - QObject::connect(plot_btn, &QPushButton::clicked, [this]() { emit showChart(!chart_opened); }); - QObject::connect(seek_btn, &QPushButton::clicked, [this, msg_id]() { - SignalFindDlg dlg(msg_id, this->sig, this); - dlg.exec(); + QObject::connect(plot_btn, &QToolButton::clicked, [this](bool checked) { + emit showChart(msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier); }); + QObject::connect(seek_btn, &QToolButton::clicked, [this]() { SignalFindDlg(msg_id, sig, this).exec(); }); + QObject::connect(remove_btn, &QToolButton::clicked, [this]() { emit remove(sig); }); + QObject::connect(form, &SignalForm::changed, [this]() { save_timer->start(); }); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +void SignalEdit::setSignal(const QString &message_id, const Signal *signal) { + sig = signal; + updateForm(msg_id == message_id && form->isVisible()); + msg_id = message_id; + color_label->setText(QString::number(form_idx + 1)); + color_label->setStyleSheet(QString("background-color:%1").arg(getColor(form_idx))); + title->setText(sig->name.c_str()); + show(); } void SignalEdit::saveSignal() { + if (!sig || !form->changed_by_user) return; + Signal s = *sig; s.name = form->name->text().toStdString(); s.size = form->size->text().toInt(); s.offset = form->offset->text().toDouble(); s.factor = form->factor->text().toDouble(); s.is_signed = form->sign->currentIndex() == 0; - s.is_little_endian = form->endianness->currentIndex() == 0; + bool little_endian = form->endianness->currentIndex() == 0; + if (little_endian != s.is_little_endian) { + int start = std::floor(s.start_bit / 8); + if (little_endian) { + int end = std::floor((s.start_bit - s.size + 1) / 8); + s.start_bit = start == end ? s.start_bit - s.size + 1 : bigEndianStartBitsIndex(s.start_bit); + } else { + int end = std::floor((s.start_bit + s.size - 1) / 8); + s.start_bit = start == end ? s.start_bit + s.size - 1 : bigEndianBitIndex(s.start_bit); + } + s.is_little_endian = little_endian; + } if (s.is_little_endian) { s.lsb = s.start_bit; s.msb = s.start_bit + s.size - 1; @@ -141,24 +170,37 @@ void SignalEdit::saveSignal() { s.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(s.start_bit) + s.size - 1); s.msb = s.start_bit; } - title->setText(QString("%1. %2").arg(form_idx + 1).arg(form->name->text())); - emit save(this->sig, s); + if (s != *sig) + emit save(this->sig, s); } void SignalEdit::setChartOpened(bool opened) { - plot_btn->setText(opened ? "☒" : "📈"); - plot_btn->setToolTip(opened ? tr("Close Plot") :tr("Show Plot")); - chart_opened = opened; + plot_btn->setToolTip(opened ? tr("Close Plot") : tr("Show Plot\nSHIFT click to add to previous opened chart")); + plot_btn->setChecked(opened); } -void SignalEdit::setFormVisible(bool visible) { - form_container->setVisible(visible); - icon->setText(visible ? "▼" : ">"); +void SignalEdit::updateForm(bool visible) { + if (visible && sig) { + form->changed_by_user = false; + if (form->name->text() != sig->name.c_str()) { + form->name->setText(sig->name.c_str()); + } + form->endianness->setCurrentIndex(sig->is_little_endian ? 0 : 1); + form->sign->setCurrentIndex(sig->is_signed ? 0 : 1); + form->factor->setText(QString::number(sig->factor)); + form->offset->setText(QString::number(sig->offset)); + form->msb->setText(QString::number(sig->msb)); + form->lsb->setText(QString::number(sig->lsb)); + form->size->setValue(sig->size); + form->changed_by_user = true; + } + form->setVisible(visible); + icon->setText(visible ? "▼ " : "> "); } void SignalEdit::signalHovered(const Signal *s) { - auto color = sig == s ? hoverColor(getColor(form_idx)) : QColor(getColor(form_idx)); - title->setStyleSheet(QString("font-weight:bold; color:%1").arg(color.name())); + auto color = sig == s ? "white" : "black"; + color_label->setStyleSheet(QString("color:%1; background-color:%2").arg(color).arg(getColor(form_idx))); } void SignalEdit::enterEvent(QEvent *event) { @@ -183,7 +225,7 @@ SignalFindDlg::SignalFindDlg(const QString &id, const Signal *signal, QWidget *p comp_box->addItems({">", "=", "<"}); h->addWidget(comp_box); QLineEdit *value_edit = new QLineEdit("0", this); - value_edit->setValidator( new QDoubleValidator(-500000, 500000, 6, this) ); + value_edit->setValidator(new QDoubleValidator(-500000, 500000, 6, this)); h->addWidget(value_edit, 1); QPushButton *search_btn = new QPushButton(tr("Find"), this); h->addWidget(search_btn); diff --git a/tools/cabana/signaledit.h b/tools/cabana/signaledit.h index e3d38d5b25..f035797e72 100644 --- a/tools/cabana/signaledit.h +++ b/tools/cabana/signaledit.h @@ -4,8 +4,9 @@ #include #include #include -#include #include +#include +#include #include "selfdrive/ui/qt/widgets/controls.h" @@ -13,50 +14,54 @@ #include "tools/cabana/dbcmanager.h" class SignalForm : public QWidget { + Q_OBJECT public: - SignalForm(const Signal &sig, QWidget *parent); - + SignalForm(QWidget *parent); QLineEdit *name, *unit, *comment, *val_desc, *offset, *factor, *min_val, *max_val; + QLabel *lsb, *msb; QSpinBox *size; QComboBox *sign, *endianness; + bool changed_by_user = false; + + signals: + void changed(); }; class SignalEdit : public QWidget { Q_OBJECT public: - SignalEdit(int index, const QString &msg_id, const Signal *sig, QWidget *parent = nullptr); + SignalEdit(int index, QWidget *parent = nullptr); + void setSignal(const QString &msg_id, const Signal *sig); void setChartOpened(bool opened); - void setFormVisible(bool show); void signalHovered(const Signal *sig); - inline bool isFormVisible() const { return form_container->isVisible(); } + void updateForm(bool show); + inline bool isFormVisible() const { return form->isVisible(); } const Signal *sig = nullptr; + QString msg_id; signals: void highlight(const Signal *sig); - void showChart(bool show); - void showFormClicked(); + void showChart(const QString &name, const Signal *sig, bool show, bool merge); void remove(const Signal *sig); void save(const Signal *sig, const Signal &new_sig); + void showFormClicked(); protected: void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; void saveSignal(); - SignalForm *form; + SignalForm *form = nullptr; ElidedLabel *title; - QWidget *form_container; + QLabel *color_label; QLabel *icon; int form_idx = 0; - QString msg_id; - bool chart_opened = false; - QPushButton *plot_btn; + QToolButton *plot_btn; + QTimer *save_timer; }; class SignalFindDlg : public QDialog { - Q_OBJECT - public: SignalFindDlg(const QString &id, const Signal *signal, QWidget *parent); }; diff --git a/tools/cabana/tests/test_cabana.cc b/tools/cabana/tests/test_cabana.cc index d0aa2cbb4f..586422ffc8 100644 --- a/tools/cabana/tests/test_cabana.cc +++ b/tools/cabana/tests/test_cabana.cc @@ -1,35 +1,67 @@ +#include "opendbc/can/common.h" +#undef INFO #include "catch2/catch.hpp" #include "tools/cabana/dbcmanager.h" +#include "tools/replay/logreader.h" + +// demo route, first segment +const std::string TEST_RLOG_URL = "https://commadata2.blob.core.windows.net/commadata2/4cf7a6ad03080c90/2021-09-29--13-46-36/0/rlog.bz2"; TEST_CASE("DBCManager::generateDBC") { DBCManager dbc_origin(nullptr); dbc_origin.open("toyota_new_mc_pt_generated"); - QString dbc_string = dbc_origin.generateDBC(); - DBCManager dbc_from_generated(nullptr); - dbc_from_generated.open("", dbc_string); + dbc_from_generated.open("", dbc_origin.generateDBC()); + + auto &msgs = dbc_origin.messages(); + auto &new_msgs = dbc_from_generated.messages(); + REQUIRE(msgs.size() == new_msgs.size()); + for (auto &[address, m] : msgs) { + auto new_m = new_msgs.at(address); + REQUIRE(m.name == new_m.name); + REQUIRE(m.size == new_m.size); + REQUIRE(m.sigs.size() == new_m.sigs.size()); + for (auto &[name, sig] : m.sigs) + REQUIRE(sig == new_m.sigs[name]); + } +} + +TEST_CASE("Parse can messages") { + DBCManager dbc(nullptr); + dbc.open("toyota_new_mc_pt_generated"); + CANParser can_parser(0, "toyota_new_mc_pt_generated", {}, {}); + + LogReader log; + REQUIRE(log.load(TEST_RLOG_URL, nullptr, {}, true)); + REQUIRE(log.events.size() > 0); + for (auto e : log.events) { + if (e->which == cereal::Event::Which::CAN) { + std::map, std::vector> values_1; + for (const auto &c : e->event.getCan()) { + const auto msg = dbc.msg(c.getAddress()); + if (c.getSrc() == 0 && msg) { + for (auto &[name, sig] : msg->sigs) { + double val = get_raw_value((uint8_t *)c.getDat().begin(), c.getDat().size(), sig); + values_1[{c.getAddress(), name.toStdString()}].push_back(val); + } + } + } - auto dbc = dbc_origin.getDBC(); - auto new_dbc = dbc_from_generated.getDBC(); - REQUIRE(dbc->msgs.size() == new_dbc->msgs.size()); - for (int i = 0; i < dbc->msgs.size(); ++i) { - REQUIRE(dbc->msgs[i].name == new_dbc->msgs[i].name); - REQUIRE(dbc->msgs[i].address == new_dbc->msgs[i].address); - REQUIRE(dbc->msgs[i].size == new_dbc->msgs[i].size); - REQUIRE(dbc->msgs[i].sigs.size() == new_dbc->msgs[i].sigs.size()); - auto &sig = dbc->msgs[i].sigs; - auto &new_sig = new_dbc->msgs[i].sigs; - for (int j = 0; j < sig.size(); ++j) { - REQUIRE(sig[j].name == new_sig[j].name); - REQUIRE(sig[j].start_bit == new_sig[j].start_bit); - REQUIRE(sig[j].msb == new_sig[j].msb); - REQUIRE(sig[j].lsb == new_sig[j].lsb); - REQUIRE(sig[j].size == new_sig[j].size); - REQUIRE(sig[j].is_signed == new_sig[j].is_signed); - REQUIRE(sig[j].factor == new_sig[j].factor); - REQUIRE(sig[j].offset == new_sig[j].offset); - REQUIRE(sig[j].is_little_endian == new_sig[j].is_little_endian); + can_parser.UpdateCans(e->mono_time, e->event.getCan()); + auto values_2 = can_parser.query_latest(); + for (auto &[key, v1] : values_1) { + bool found = false; + for (auto &v2 : values_2) { + if (v2.address == key.first && v2.name == key.second) { + REQUIRE(v2.all_values.size() == v1.size()); + REQUIRE(v2.all_values == v1); + found = true; + break; + } + } + REQUIRE(found); + } } } } diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 9a28f71bb9..d85b23b7e6 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -28,18 +28,16 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { slider = new Slider(this); slider->setSingleStep(0); - slider->setMinimum(0); - slider->setMaximum(can->totalSeconds() * 1000); slider_layout->addWidget(slider); - end_time_label = new QLabel(formatTime(can->totalSeconds())); + end_time_label = new QLabel(this); slider_layout->addWidget(end_time_label); main_layout->addLayout(slider_layout); // btn controls QHBoxLayout *control_layout = new QHBoxLayout(); play_btn = new QPushButton("⏸"); - play_btn->setStyleSheet("font-weight:bold"); + play_btn->setStyleSheet("font-weight:bold; height:16px"); control_layout->addWidget(play_btn); QButtonGroup *group = new QButtonGroup(this); @@ -61,6 +59,10 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(formatTime(value / 1000)); }); QObject::connect(cam_widget, &CameraWidget::clicked, [this]() { pause(!can->isPaused()); }); QObject::connect(play_btn, &QPushButton::clicked, [=]() { pause(!can->isPaused()); }); + QObject::connect(can, &CANMessages::streamStarted, [this]() { + end_time_label->setText(formatTime(can->totalSeconds())); + slider->setRange(0, can->totalSeconds() * 1000); + }); } void VideoWidget::pause(bool pause) { @@ -74,8 +76,7 @@ void VideoWidget::rangeChanged(double min, double max, bool is_zoomed) { max = can->totalSeconds(); } end_time_label->setText(formatTime(max)); - slider->setMinimum(min * 1000); - slider->setMaximum(max * 1000); + slider->setRange(min * 1000, max * 1000); } void VideoWidget::updateState() { @@ -91,7 +92,7 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { timeline = can->getTimeline(); update(); }); - timer->start(); + QObject::connect(can, SIGNAL(streamStarted()), timer, SLOT(start())); } void Slider::sliderChange(QAbstractSlider::SliderChange change) { diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 51dae4c76f..16f60b0b03 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "selfdrive/ui/qt/widgets/cameraview.h" #include "tools/cabana/canmessages.h" diff --git a/tools/camerastream/compressed_vipc.py b/tools/camerastream/compressed_vipc.py index 42a416985e..cab11493f2 100755 --- a/tools/camerastream/compressed_vipc.py +++ b/tools/camerastream/compressed_vipc.py @@ -91,9 +91,14 @@ def main(addr, cams, nvidia=False): vipc_server.create_buffers(vst, 4, False, W, H) vipc_server.start_listener() + procs = [] for k, v in cams.items(): - multiprocessing.Process(target=decoder, args=(addr, k, vipc_server, v, nvidia)).start() + p = multiprocessing.Process(target=decoder, args=(addr, k, vipc_server, v, nvidia)) + p.start() + procs.append(p) + for p in procs: + p.join() if __name__ == "__main__": parser = argparse.ArgumentParser(description="Decode video streams and broadcast on VisionIPC") diff --git a/tools/gpstest/run_unittest.sh b/tools/gpstest/run_unittest.sh index 9f93fdfc9a..e0ca017a6d 100755 --- a/tools/gpstest/run_unittest.sh +++ b/tools/gpstest/run_unittest.sh @@ -4,7 +4,7 @@ # run limeGPS with random static location timeout 300 ./simulate_gps_signal.py 32.7518 -117.1962 & -gps_PID=$(ps -aux | grep -m 1 "timeout 300" | cut -d ' ' -f 7) +gps_PID=$(ps -aux | grep -m 1 "timeout 300" | awk '{print $2}') echo "starting limeGPS..." sleep 10 diff --git a/tools/gpstest/test_gps_qcom.py b/tools/gpstest/test_gps_qcom.py index 0909316c5e..b3ce93fc81 100644 --- a/tools/gpstest/test_gps_qcom.py +++ b/tools/gpstest/test_gps_qcom.py @@ -40,14 +40,10 @@ class TestGPS(unittest.TestCase): if ublox_available: raise unittest.SkipTest - @unittest.skip("Skip cold start test due to time") - def test_quectel_cold_start(self): + def test_a_quectel_cold_start(self): # delete assistance data to enforce cold start for GNSS # testing shows that this takes up to 20min - # invalidate supl setting, cannot be reset - _, err = exec_mmcli("--location-set-supl-server=unittest:1") - _, err = exec_mmcli("--command='AT+QGPSDEL=0'") assert len(err) == 0, f"GPSDEL failed: {err}" @@ -55,27 +51,6 @@ class TestGPS(unittest.TestCase): start_time = time.monotonic() glo = messaging.sub_sock("gpsLocation", timeout=0.1) - timeout = 10*60*25 # 25 minute - timedout = wait_for_location(glo, timeout) - managed_processes['rawgpsd'].stop() - - assert timedout is False, "Waiting for location timed out (25min)!" - - duration = time.monotonic() - start_time - assert duration < 50, f"Received GPS location {duration}!" - - - def test_a_quectel_cold_start_AGPS(self): - _, err = exec_mmcli("--command='AT+QGPSDEL=0'") - assert len(err) == 0, f"GPSDEL failed: {err}" - - # setup AGPS - exec_mmcli("--location-set-supl-server=supl.google.com:7276") - - managed_processes['rawgpsd'].start() - start_time = time.monotonic() - glo = messaging.sub_sock("gpsLocation", timeout=0.1) - timeout = 10*60*3 # 3 minute timedout = wait_for_location(glo, timeout) managed_processes['rawgpsd'].stop() @@ -87,15 +62,11 @@ class TestGPS(unittest.TestCase): def test_b_quectel_startup(self): - - # setup AGPS - exec_mmcli("--location-set-supl-server=supl.google.com:7276") - managed_processes['rawgpsd'].start() start_time = time.monotonic() glo = messaging.sub_sock("gpsLocation", timeout=0.1) - timeout = 10*60*3 # 3 minute + timeout = 10*60 # 1 minute timedout = wait_for_location(glo, timeout) managed_processes['rawgpsd'].stop() diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index 1337a4ef2c..1464a6cf57 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -358,6 +358,7 @@ void Replay::publishFrame(const Event *e) { void Replay::stream() { cereal::Event::Which cur_which = cereal::Event::Which::INIT_DATA; + double prev_replay_speed = 1.0; std::unique_lock lk(stream_lock_); while (true) { @@ -397,10 +398,11 @@ void Replay::stream() { long rtime = nanos_since_boot() - loop_start_ts; long behind_ns = etime - rtime; // if behind_ns is greater than 1 second, it means that an invalid segemnt is skipped by seeking/replaying - if (behind_ns >= 1 * 1e9) { - // reset start times + if (behind_ns >= 1 * 1e9 || speed_ != prev_replay_speed) { + // reset event start times evt_start_ts = cur_mono_time_; loop_start_ts = nanos_since_boot(); + prev_replay_speed = speed_; } else if (behind_ns > 0 && !hasFlag(REPLAY_FLAG_FULL_SPEED)) { precise_nano_sleep(behind_ns); }