diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index c404bd6e95..bf4f6712c5 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -59,6 +59,8 @@ jobs: cp .pre-commit-config.yaml $STRIPPED_DIR cp .pylintrc $STRIPPED_DIR cp mypy.ini $STRIPPED_DIR + cp pyproject.toml $STRIPPED_DIR + cp poetry.lock $STRIPPED_DIR cd $STRIPPED_DIR ${{ env.RUN }} "pre-commit run --all" @@ -207,6 +209,7 @@ jobs: $UNIT_TEST system/loggerd && \ $UNIT_TEST selfdrive/car && \ $UNIT_TEST selfdrive/locationd && \ + $UNIT_TEST system/tests && \ $UNIT_TEST system/ubloxd && \ selfdrive/locationd/test/_test_locationd_lib.py && \ ./system/ubloxd/tests/test_glonass_runner && \ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d98f905b8c..9edc424829 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,9 @@ repos: - repo: https://github.com/python-poetry/poetry - rev: '1.4.0' + rev: '1.5.0' hooks: - id: poetry-check + - id: poetry-lock + name: validate poetry lock + args: + - --check diff --git a/.pylintrc b/.pylintrc index 58988c5d74..ae91dd3d7c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,7 +3,7 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code -extension-pkg-whitelist=scipy,cereal.messaging.messaging_pyx,PyQt5,av +extension-pkg-whitelist=scipy,cereal.messaging.messaging_pyx,PyQt5,av,pycurl # Add files or directories to the blacklist. They should be base names, not # paths. @@ -466,4 +466,4 @@ check-str-concat-over-line-jumps=yes # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions= diff --git a/Jenkinsfile b/Jenkinsfile index 8a2f83be2f..128273792e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -154,6 +154,7 @@ pipeline { ["build openpilot", "cd selfdrive/manager && ./build.py"], ["check dirty", "release/check-dirty.sh"], ["onroad tests", "cd selfdrive/test/ && ./test_onroad.py"], + ["time to onroad", "cd selfdrive/test/ && pytest test_time_to_onroad.py"], ["test car interfaces", "cd selfdrive/car/tests/ && ./test_car_interfaces.py"], ]) } @@ -174,12 +175,12 @@ pipeline { steps { phone_steps("tici-common", [ ["build", "cd selfdrive/manager && ./build.py"], + ["test pandad", "python selfdrive/boardd/tests/test_pandad.py"], ["test power draw", "python system/hardware/tici/tests/test_power_draw.py"], ["test loggerd", "python system/loggerd/tests/test_loggerd.py"], ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python system/loggerd/tests/test_encoder.py"], ["test pigeond", "python system/sensord/tests/test_pigeond.py"], ["test manager", "python selfdrive/manager/test/test_manager.py"], - ["test pandad", "python selfdrive/boardd/tests/test_pandad.py"], ]) } } @@ -219,7 +220,7 @@ pipeline { steps { phone_steps("tici-common", [ ["build", "cd selfdrive/manager && ./build.py"], - ["model replay", "cd selfdrive/test/process_replay && NO_NAV=1 ./model_replay.py"], + ["model replay", "cd selfdrive/test/process_replay && ./model_replay.py"], ]) } } diff --git a/RELEASES.md b/RELEASES.md index a4b2a76875..8e5deddd7e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,16 @@ +Version 0.9.3 (2023-06-20) +======================== +* New driving model + * Improved height estimation and added height tracking in liveCalibration + * Model inputs refactor +* New driving personality setting + * Three settings: aggressive, standard, and relaxed + * Standard is recommended and the default + * In aggressive mode, lead follow distance is shorter and acceleration response is quicker + * In relaxed mode, lead follow distance is longer +* Improved fuzzy fingerprinting for Hyundai, Kia, and Genesis +* Improved thermal management logic + Version 0.9.2 (2023-05-22) ======================== * New driving model diff --git a/cereal b/cereal index d58979cda8..06c6cbb790 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit d58979cda8785076b15122348c89a85ab3859164 +Subproject commit 06c6cbb7901945e78eadbc56c68d40e60b8fac93 diff --git a/common/logging_extra.py b/common/logging_extra.py index 5baaac1f90..899ad7a391 100644 --- a/common/logging_extra.py +++ b/common/logging_extra.py @@ -153,9 +153,9 @@ class SwagLogger(logging.Logger): def bind_global(self, **kwargs): self.global_ctx.update(kwargs) - def event(self, event_name, *args, **kwargs): + def event(self, event, *args, **kwargs): evt = NiceOrderedDict() - evt['event'] = event_name + evt['event'] = event if args: evt['args'] = args evt.update(kwargs) diff --git a/common/params.cc b/common/params.cc index 83560ca26f..680a86f6e7 100644 --- a/common/params.cc +++ b/common/params.cc @@ -104,6 +104,7 @@ std::unordered_map keys = { {"DisablePowerDown", PERSISTENT}, {"ExperimentalMode", PERSISTENT}, {"ExperimentalModeConfirmed", PERSISTENT}, + {"LongitudinalPersonality", PERSISTENT}, {"ExperimentalLongitudinalEnabled", PERSISTENT}, {"DisableUpdates", PERSISTENT}, {"DisengageOnAccelerator", PERSISTENT}, @@ -112,7 +113,7 @@ std::unordered_map keys = { {"DoShutdown", CLEAR_ON_MANAGER_START}, {"DoUninstall", CLEAR_ON_MANAGER_START}, {"FirmwareQueryDone", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"ForcePowerDown", CLEAR_ON_MANAGER_START}, + {"ForcePowerDown", PERSISTENT}, {"GitBranch", PERSISTENT}, {"GitCommit", PERSISTENT}, {"GitDiff", PERSISTENT}, @@ -150,6 +151,7 @@ std::unordered_map keys = { {"LiveParameters", PERSISTENT}, {"LiveTorqueCarParams", PERSISTENT}, {"LiveTorqueParameters", PERSISTENT | DONT_LOG}, + {"LastOffroadStatusPacket", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"NavDestination", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"NavDestinationWaypoints", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"NavSettingTime24h", PERSISTENT}, @@ -185,11 +187,9 @@ std::unordered_map keys = { {"UpdaterNewReleaseNotes", CLEAR_ON_MANAGER_START}, {"Version", PERSISTENT}, {"VisionRadarToggle", PERSISTENT}, - {"WideCameraOnly", PERSISTENT}, {"ApiCache_Device", PERSISTENT}, {"ApiCache_DriveStats", PERSISTENT}, {"ApiCache_NavDestinations", PERSISTENT}, - {"ApiCache_Owner", PERSISTENT}, {"Offroad_BadNvme", CLEAR_ON_MANAGER_START}, {"Offroad_CarUnrecognized", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"Offroad_ConnectivityNeeded", CLEAR_ON_MANAGER_START}, diff --git a/common/time.py b/common/time.py new file mode 100644 index 0000000000..8dac17815d --- /dev/null +++ b/common/time.py @@ -0,0 +1,3 @@ +import datetime + +MIN_DATE = datetime.datetime(year=2023, month=6, day=1) diff --git a/common/util.cc b/common/util.cc index ee1080cbdf..55a8b1fb3e 100644 --- a/common/util.cc +++ b/common/util.cc @@ -189,7 +189,7 @@ bool create_directories(const std::string& dir, mode_t mode) { return createDirectory(dir, mode); } -std::string getenv(const char* key, const char* default_val) { +std::string getenv(const char* key, std::string default_val) { const char* val = ::getenv(key); return val ? val : default_val; } @@ -260,7 +260,7 @@ struct tm get_time() { bool time_valid(struct tm sys_time) { int year = 1900 + sys_time.tm_year; int month = 1 + sys_time.tm_mon; - return (year > 2021) || (year == 2021 && month >= 6); + return (year > 2023) || (year == 2023 && month >= 6); } } // namespace util diff --git a/common/util.h b/common/util.h index c80dc234f2..34721700e7 100644 --- a/common/util.h +++ b/common/util.h @@ -70,7 +70,7 @@ std::string string_format(const std::string& format, Args... args) { return std::string(buf.get(), buf.get() + size - 1); } -std::string getenv(const char* key, const char* default_val = ""); +std::string getenv(const char* key, std::string default_val = ""); int getenv(const char* key, int default_val); float getenv(const char* key, float default_val); diff --git a/docs/CARS.md b/docs/CARS.md index 46f8bd2787..a7fb408644 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -6,262 +6,262 @@ A supported vehicle is one that just works when you install a comma three. All s # 252 Supported Cars -|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Harness Kit
 |Video| +|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| -|Acura|ILX 2016-19|AcuraWatch Plus|openpilot|25 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Acura|RDX 2016-18|AcuraWatch Plus|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Audi|Q3 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Buick|LaCrosse 2017-19[3](#footnotes)|Driver Confidence Package 2|openpilot|18 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 USB-C coupler
- 1 long OBD-C cable
|| -|Cadillac|Escalade 2017[3](#footnotes)|Driver Assist Package|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 USB-C coupler
- 1 long OBD-C cable
|| -|Cadillac|Escalade ESV 2016[3](#footnotes)|Adaptive Cruise Control (ACC) & LKAS|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 USB-C coupler
- 1 long OBD-C cable
|| -|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)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Chevrolet|Volt 2017-18[3](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 USB-C coupler
- 1 long OBD-C cable
|| -|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Chrysler|Pacifica 2021|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Chrysler|Pacifica Hybrid 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| +|Acura|ILX 2016-19|AcuraWatch Plus|openpilot|25 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Acura|RDX 2016-18|AcuraWatch Plus|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Audi|Q3 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Buick|LaCrosse 2017-19[3](#footnotes)|Driver Confidence Package 2|openpilot|18 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 comma three
- 2 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Cadillac|Escalade 2017[3](#footnotes)|Driver Assist Package|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 comma three
- 2 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Cadillac|Escalade ESV 2016[3](#footnotes)|Adaptive Cruise Control (ACC) & LKAS|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 comma three
- 2 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chevrolet|Volt 2017-18[3](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 comma three
- 2 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chrysler|Pacifica 2021|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Chrysler|Pacifica Hybrid 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|| -|Ford|Bronco Sport 2021-22|Co-Pilot360 Assist+|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Ford|Escape 2020-22|Co-Pilot360 Assist+|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Ford|Explorer 2020-22|Co-Pilot360 Assist+|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Ford|Kuga 2020-22|Adaptive Cruise Control with Lane Centering|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Ford|Maverick 2022-23|Co-Pilot360 Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|G70 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|G70 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|GV60 (Advanced Trim) 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|GV60 (Performance Trim) 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|GV70 (2.5T Trim) 2022-23[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|GV70 (3.5T Trim) 2022-23[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai M connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Genesis|GV80 2023[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai M connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|GMC|Acadia 2018[3](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 USB-C coupler
- 1 long OBD-C cable
|| -|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)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Civic 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Civic Hatchback 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|CR-V 2015-16|Touring Trim|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|CR-V Hybrid 2017-19|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Fit 2018-20|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Freed 2020|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|HR-V 2019-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|HR-V 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Odyssey 2018-20|Honda Sensing|openpilot|25 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Passport 2019-22|All|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Pilot 2016-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Honda|Ridgeline 2017-23|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Elantra 2017-19|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Elantra 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)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Elantra GT 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Ioniq 5 (Southeast Asia only) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Ioniq 5 (with HDA II) 2022-23[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Ioniq 5 (without HDA II) 2022-23[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Ioniq Electric 2020|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Ioniq Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Kona 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)](##)|
View- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai O connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai I connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Santa Cruz 2022-23[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai D connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Santa Fe Plug-in Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Tucson 2022[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Tucson 2023[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Hyundai|Tucson Hybrid 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Ceed 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|EV6 (Southeast Asia only) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|EV6 (with HDA II) 2022-23[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|EV6 (without HDA II) 2022-23[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Forte 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)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|K5 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)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Niro EV 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Niro Hybrid 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)](##)|
View- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Niro Hybrid 2023[5](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Niro Plug-in Hybrid 2018-19|All|openpilot available[1](#footnotes)|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Niro Plug-in Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai D connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Sorento 2018|Advanced Smart Cruise Control|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Sorento 2021-23[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Sorento Plug-in Hybrid 2022-23[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Sportage 2023[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Sportage Hybrid 2023[5](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Kia|Stinger 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|ES 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|ES Hybrid 2019-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|RX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lexus|UX Hybrid 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Lincoln|Aviator 2021|Co-Pilot360 Plus|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|MAN|eTGE 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|MAN|TGE 2017-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Mazda|CX-5 2022-23|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Mazda connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Mazda connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Nissan|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Nissan B connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Nissan|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Nissan A connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Nissan|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Nissan A connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Nissan|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Nissan A connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Ram|1500 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ram connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|SEAT|Ateca 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Subaru|Ascent 2019-21|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|Forester 2019-21|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|Impreza 2017-19|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|Impreza 2020-22|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|Legacy 2020-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|Outback 2020-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|XV 2018-19|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Subaru|XV 2020-21|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Subaru A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Škoda|Fabia 2022-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
[10](#footnotes)|| -|Škoda|Kamiq 2021[7](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
[10](#footnotes)|| -|Škoda|Karoq 2019-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Škoda|Kodiaq 2017-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Škoda|Octavia 2015, 2018-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Škoda|Octavia RS 2016|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Škoda|Scala 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
[10](#footnotes)|| -|Škoda|Superb 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Avalon 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Avalon 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Avalon Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|C-HR 2021|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|C-HR Hybrid 2021-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Camry 2018-20|All|Stock|0 mph[6](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Camry 2021-23|All|openpilot|0 mph[6](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Camry Hybrid 2021-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Corolla Hybrid (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Prius 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|RAV4 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)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|RAV4 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Toyota|RAV4 Hybrid 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|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)](##)|
View- 1 Toyota connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 harness box
|| -|Volkswagen|Arteon 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Arteon eHybrid 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Arteon R 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Atlas Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|California 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Crafter 2017-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|e-Crafter 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Grand California 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Jetta 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Jetta GLI 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Passat 2015-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
[10](#footnotes)|| -|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
[10](#footnotes)|| -|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
[10](#footnotes)|| -|Volkswagen|T-Roc 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
[10](#footnotes)|| -|Volkswagen|Taos 2022|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| -|Volkswagen|Touran 2017|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 harness box
- 1 long OBD-C cable
|| +|Ford|Bronco Sport 2021-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Explorer 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Kuga 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ford|Maverick 2022-23|Co-Pilot360 Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|G70 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|G70 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|GV60 (Advanced Trim) 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|GV60 (Performance Trim) 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|GV70 (2.5T Trim) 2022-23[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|GV70 (3.5T Trim) 2022-23[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai M connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Genesis|GV80 2023[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai M connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|GMC|Acadia 2018[3](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 OBD-II connector
- 1 comma three
- 2 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 GM connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Civic 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Civic Hatchback 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|CR-V 2015-16|Touring Trim|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|CR-V Hybrid 2017-19|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Fit 2018-20|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Freed 2020|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|HR-V 2019-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|HR-V 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Honda Bosch A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Odyssey 2018-20|Honda Sensing|openpilot|25 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Passport 2019-23|All|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Pilot 2016-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Honda|Ridgeline 2017-23|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Honda Nidec connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Elantra 2017-19|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Elantra 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)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Elantra GT 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq 5 (Southeast Asia only) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq 5 (with HDA II) 2022-23[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq 5 (without HDA II) 2022-23[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq Electric 2020|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Kona 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)](##)|
View- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai O connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai I connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Santa Cruz 2022-23[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai D connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Santa Fe Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Santa Fe Plug-in Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Tucson 2022[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Tucson 2023[5](#footnotes)|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Hyundai|Tucson Hybrid 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Ceed 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|EV6 (Southeast Asia only) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|EV6 (with HDA II) 2022-23[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|EV6 (without HDA II) 2022-23[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Forte 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)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|K5 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)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Niro EV 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Niro Hybrid 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)](##)|
View- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Niro Hybrid 2023[5](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Niro Plug-in Hybrid 2018-19|All|openpilot available[1](#footnotes)|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Niro Plug-in Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai D connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Sorento 2018|Advanced Smart Cruise Control|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Sorento 2021-23[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Sorento Plug-in Hybrid 2022-23[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Sportage 2023[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Sportage Hybrid 2023[5](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Kia|Stinger 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|ES 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|ES Hybrid 2019-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|RX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lexus|UX Hybrid 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Lincoln|Aviator 2020-21|Co-Pilot360 Plus|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|MAN|eTGE 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|MAN|TGE 2017-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Mazda|CX-5 2022-23|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Mazda connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 Mazda connector
- 1 RJ45 cable (7 ft)
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Nissan|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Nissan B connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Nissan|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Nissan A connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Nissan|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Nissan A connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Nissan|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 Nissan A connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Ram|1500 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Ram connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|SEAT|Ateca 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|Ascent 2019-21|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|Forester 2019-21|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|Impreza 2017-19|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|Impreza 2020-22|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|Legacy 2020-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru B connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|Outback 2020-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru B connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|XV 2018-19|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Subaru|XV 2020-21|EyeSight Driver Assistance|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Škoda|Fabia 2022-23[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[11](#footnotes)|| +|Škoda|Kamiq 2021[7,9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[11](#footnotes)|| +|Škoda|Karoq 2019-21[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Škoda|Kodiaq 2017-23[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Škoda|Octavia 2015, 2018-19[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Škoda|Octavia RS 2016[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Škoda|Scala 2020[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[11](#footnotes)|| +|Škoda|Superb 2015-22[9](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Avalon 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Avalon 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Avalon Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|C-HR 2021|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|C-HR Hybrid 2021-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Camry 2018-20|All|Stock|0 mph[6](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Camry 2021-23|All|openpilot|0 mph[6](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Camry Hybrid 2021-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Corolla Hybrid (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Prius 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 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)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Toyota|RAV4 Hybrid 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|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)](##)|
View- 1 RJ45 cable (7 ft)
- 1 Toyota connector
- 1 comma power v2
- 1 comma three
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Arteon 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Arteon eHybrid 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Arteon R 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Atlas Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|California 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Crafter 2017-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|e-Crafter 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Grand California 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Jetta 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Jetta GLI 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Passat 2015-22[8](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[11](#footnotes)|| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[11](#footnotes)|| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[11](#footnotes)|| +|Volkswagen|T-Roc 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[11](#footnotes)|| +|Volkswagen|Taos 2022|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,10](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
View- 1 J533 connector
- 1 USB-C coupler
- 1 comma three
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| - +### Footnotes 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).
@@ -270,8 +270,9 @@ A supported vehicle is one that just works when you install a comma three. All s 6openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
7Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
8Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
-9Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
-10Model-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.
+9Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma three functionality.
+10Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
+11Model-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_chffrplus.sh b/launch_chffrplus.sh index 45757e1a61..925e030a15 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -9,11 +9,6 @@ source "$BASEDIR/launch_env.sh" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" function agnos_init { - # wait longer for weston to come up - if [ -f "$BASEDIR/prebuilt" ]; then - sleep 3 - fi - # TODO: move this to agnos sudo rm -f /data/etc/NetworkManager/system-connections/*.nmmeta diff --git a/opendbc b/opendbc index 4e9cbea8ea..67da43f7a7 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 4e9cbea8eab73cb4475ef68bc367b2dedb958bdb +Subproject commit 67da43f7a7e62f2d7c2d8eec8ef7e649c6e1d181 diff --git a/panda b/panda index bb7d6dbdaa..deb49f107e 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit bb7d6dbdaa15e95d04a3ea31eb5560aff296ef30 +Subproject commit deb49f107e24db66bce7aa828747d48f06d67ea8 diff --git a/poetry.lock b/poetry.lock index e2d894d3c3..679d4b40a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -188,15 +188,15 @@ tests = ["pytest"] [[package]] name = "astroid" -version = "2.12.12" +version = "2.15.5" description = "An abstract syntax tree for Python with inference support." -category = "main" +category = "dev" optional = false python-versions = ">=3.7.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} [[package]] @@ -837,7 +837,7 @@ tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytes name = "dill" version = "0.3.5.1" description = "serialize all of python" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" @@ -1631,7 +1631,7 @@ six = "*" name = "isort" version = "5.10.1" description = "A Python utility / library to sort Python imports." -category = "main" +category = "dev" optional = false python-versions = ">=3.6.1,<4.0" @@ -1982,7 +1982,7 @@ tabulate = "*" name = "lazy-object-proxy" version = "1.7.1" description = "A fast and thorough lazy object proxy." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -3253,16 +3253,16 @@ python-versions = "*" [[package]] name = "pylint" -version = "2.15.4" +version = "2.17.4" description = "python code static checker" -category = "main" +category = "dev" optional = false python-versions = ">=3.7.2" [package.dependencies] -astroid = ">=2.12.11,<=2.14.0-dev0" +astroid = ">=2.15.4,<=2.17.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = ">=0.2" +dill = {version = ">=0.2", markers = "python_version < \"3.11\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" @@ -4403,7 +4403,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -4565,6 +4565,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "types-python-dateutil" +version = "2.8.19.13" +description = "Typing stubs for python-dateutil" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "types-pyyaml" version = "6.0.12" @@ -4709,7 +4717,7 @@ python-versions = ">=3.7" name = "wrapt" version = "1.14.1" description = "Module for decorators, wrappers and monkey patching." -category = "main" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" @@ -4804,7 +4812,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "1.1" python-versions = "~3.8" -content-hash = "0f8861fe679c2ba9a2a6365498a02ee353023e1a3ef882b117516fb7ea28fb58" +content-hash = "2e03ebb1f6c441a0154531ee92f6f1f8c9e74544d4295d3753029bb8dcd457d1" [metadata.files] adal = [ @@ -4969,8 +4977,8 @@ argon2-cffi-bindings = [ {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, ] astroid = [ - {file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"}, - {file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"}, + {file = "astroid-2.15.5-py3-none-any.whl", hash = "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324"}, + {file = "astroid-2.15.5.tar.gz", hash = "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f"}, ] asttokens = [ {file = "asttokens-2.0.8-py2.py3-none-any.whl", hash = "sha256:e3305297c744ae53ffa032c45dc347286165e4ffce6875dc662b205db0623d86"}, @@ -7569,8 +7577,8 @@ pylev = [ {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"}, ] pylint = [ - {file = "pylint-2.15.4-py3-none-any.whl", hash = "sha256:629cf1dbdfb6609d7e7a45815a8bb59300e34aa35783b5ac563acaca2c4022e9"}, - {file = "pylint-2.15.4.tar.gz", hash = "sha256:5441e9294335d354b7bad57c1044e5bd7cce25c433475d76b440e53452fa5cb8"}, + {file = "pylint-2.17.4-py3-none-any.whl", hash = "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c"}, + {file = "pylint-2.17.4.tar.gz", hash = "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1"}, ] pymsalruntime = [ {file = "pymsalruntime-0.11.2-cp310-cp310-win32.whl", hash = "sha256:a45e35c9fa58c196029bb9f5b8bedd313b2a8ac971d576c57c31cb06139de247"}, @@ -8597,6 +8605,10 @@ types-pycurl = [ {file = "types-pycurl-7.45.1.tar.gz", hash = "sha256:82e00aa2981595bfa55e5a3bac42221eb3435b0026dffbe1177f6ac9f2d51200"}, {file = "types_pycurl-7.45.1-py3-none-any.whl", hash = "sha256:9eab3414da4a1b1e9a628bd288fc5172b8c182e1d9fb6d8d082441b0fd64baed"}, ] +types-python-dateutil = [ + {file = "types-python-dateutil-2.8.19.13.tar.gz", hash = "sha256:09a0275f95ee31ce68196710ed2c3d1b9dc42e0b61cc43acc369a42cb939134f"}, + {file = "types_python_dateutil-2.8.19.13-py3-none-any.whl", hash = "sha256:0b0e7c68e7043b0354b26a1e0225cb1baea7abb1b324d02b50e2d08f1221043f"}, +] types-pyyaml = [ {file = "types-PyYAML-6.0.12.tar.gz", hash = "sha256:f6f350418125872f3f0409d96a62a5a5ceb45231af5cc07ee0034ec48a3c82fa"}, {file = "types_PyYAML-6.0.12-py3-none-any.whl", hash = "sha256:29228db9f82df4f1b7febee06bbfb601677882e98a3da98132e31c6874163e15"}, diff --git a/pyproject.toml b/pyproject.toml index 2963337733..8877a262ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ libusb1 = "^3.0.0" nose = "^1.3.7" numpy = "^1.23.0" onnx = "^1.12.0" -onnxruntime-gpu = { version = "^1.11.1", platform = "linux" } +onnxruntime-gpu = { version = "^1.11.1", platform = "linux", markers = "platform_machine == 'x86_64'" } pillow = "^9.2.0" poetry = "==1.2.2" protobuf = "==3.20.1" @@ -37,7 +37,6 @@ psutil = "^5.9.1" pycapnp = "==1.1.0" pycryptodome = "^3.15.0" PyJWT = "^2.5.0" -pylint = "^2.15.4" pyopencl = "^2022.2.4" pyserial = "^3.5" python-dateutil = "^2.8.2" @@ -66,7 +65,7 @@ sconscontrib = {git = "https://github.com/SCons/scons-contrib.git"} av = "^9.2.0" azure-storage-blob = "~2.1" breathe = "^4.34.0" -carla = { version = "==0.9.13", platform = "linux" } +carla = { version = "==0.9.13", platform = "linux", markers = "platform_machine == 'x86_64'" } control = "^0.9.2" coverage = "^6.4.1" dictdiffer = "^0.9.0" @@ -92,6 +91,7 @@ pprofile = "^2.1.0" pre-commit = "^2.19.0" pycurl = "^7.45.1" pygame = "^2.1.2" +pylint = "^2.17.4" pyprof2calltree = "^1.4.5" pytest = "^7.1.2" pytest-xdist = "^2.5.0" @@ -106,6 +106,7 @@ tenacity = "^8.0.1" types-atomicwrites = "^1.4.5" types-certifi = "^2021.10.8" types-pycurl = "^7.45.1" +types-python-dateutil = "^2.8.19.13" types-PyYAML = "^6.0" types-requests = "^2.28.11" types-tabulate = "^0.8.10" diff --git a/release/files_common b/release/files_common index b2055b8386..820dcc713f 100644 --- a/release/files_common +++ b/release/files_common @@ -39,6 +39,7 @@ common/filter_simple.py common/stat_live.py common/spinner.py common/text_window.py +common/time.py common/kalman/.gitignore common/kalman/* @@ -200,7 +201,6 @@ selfdrive/controls/lib/drive_helpers.py selfdrive/controls/lib/events.py selfdrive/controls/lib/lane_planner.py selfdrive/controls/lib/latcontrol_angle.py -selfdrive/controls/lib/latcontrol_indi.py selfdrive/controls/lib/latcontrol_torque.py selfdrive/controls/lib/latcontrol_pid.py selfdrive/controls/lib/latcontrol.py @@ -312,9 +312,11 @@ selfdrive/thermald/power_monitoring.py selfdrive/thermald/fan_controller.py selfdrive/test/__init__.py +selfdrive/test/fuzzy_generation.py selfdrive/test/helpers.py selfdrive/test/setup_device_ci.sh selfdrive/test/test_onroad.py +selfdrive/test/test_time_to_onroad.py selfdrive/ui/.gitignore selfdrive/ui/SConscript @@ -496,6 +498,7 @@ body/crypto/** cereal/.gitignore cereal/__init__.py cereal/car.capnp +cereal/custom.capnp cereal/legacy.capnp cereal/log.capnp cereal/services.py @@ -505,6 +508,10 @@ cereal/logger/logger.h cereal/messaging/.gitignore cereal/messaging/__init__.py cereal/messaging/bridge.cc +cereal/messaging/event.cc +cereal/messaging/event.h +cereal/messaging/impl_fake.cc +cereal/messaging/impl_fake.h cereal/messaging/impl_msgq.cc cereal/messaging/impl_msgq.h cereal/messaging/impl_zmq.cc @@ -588,8 +595,8 @@ opendbc/hyundai_kia_mando_front_radar_generated.dbc opendbc/mazda_2017.dbc -opendbc/nissan_x_trail_2017.dbc -opendbc/nissan_leaf_2018.dbc +opendbc/nissan_x_trail_2017_generated.dbc +opendbc/nissan_leaf_2018_generated.dbc opendbc/subaru_global_2017_generated.dbc opendbc/subaru_outback_2015_generated.dbc diff --git a/selfdrive/assets/offroad/icon_wifi_uploading.svg b/selfdrive/assets/offroad/icon_wifi_uploading.svg new file mode 100644 index 0000000000..95cb0e283e --- /dev/null +++ b/selfdrive/assets/offroad/icon_wifi_uploading.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/selfdrive/athena/athenad.py b/selfdrive/athena/athenad.py index 8e335c2b58..842c649672 100755 --- a/selfdrive/athena/athenad.py +++ b/selfdrive/athena/athenad.py @@ -171,7 +171,7 @@ def jsonrpc_handler(end_event: threading.Event) -> None: try: data = recv_queue.get(timeout=1) if "method" in data: - cloudlog.debug(f"athena.jsonrpc_handler.call_method {data}") + cloudlog.event("athena.jsonrpc_handler.call_method", data=data) response = JSONRPCResponseManager.handle(data, dispatcher) send_queue.put_nowait(response.json) elif "id" in data and ("result" in data or "error" in data): diff --git a/selfdrive/boardd/boardd.cc b/selfdrive/boardd/boardd.cc index 72c0a8c310..fabe3e774a 100644 --- a/selfdrive/boardd/boardd.cc +++ b/selfdrive/boardd/boardd.cc @@ -94,11 +94,12 @@ void sync_time(Panda *panda, SyncTimeDir dir) { } } } else if (dir == SyncTimeDir::FROM_PANDA) { + LOGW("System time: %s, RTC time: %s", get_time_str(sys_time).c_str(), get_time_str(rtc_time).c_str()); + if (!util::time_valid(sys_time) && util::time_valid(rtc_time)) { const struct timeval tv = {mktime(&rtc_time), 0}; settimeofday(&tv, 0); - LOGE("System time wrong, setting from RTC. System: %s RTC: %s", - get_time_str(sys_time).c_str(), get_time_str(rtc_time).c_str()); + LOGE("System time wrong, setting from RTC."); } } } @@ -473,6 +474,11 @@ void panda_state_thread(PubMaster *pm, std::vector pandas, bool spoofin bool is_onroad_last = false; std::future safety_future; + std::vector connected_serials; + for (Panda *p : pandas) { + connected_serials.push_back(p->hw_serial()); + } + LOGD("start panda state thread"); // run at 2hz @@ -500,10 +506,15 @@ void panda_state_thread(PubMaster *pm, std::vector pandas, bool spoofin LOGE("Reconnecting, communication to pandas not healthy"); do_exit = true; - // TODO: list is slow, takes 16ms - } else if (pandas.size() != Panda::list().size()) { - LOGW("Reconnecting to changed amount of pandas!"); - do_exit = true; + } else { + // check for new pandas + for (std::string &s : Panda::list(true)) { + if (!std::count(connected_serials.begin(), connected_serials.end(), s)) { + LOGW("Reconnecting to new panda: %s", s.c_str()); + do_exit = true; + break; + } + } } if (do_exit) { diff --git a/selfdrive/boardd/panda.cc b/selfdrive/boardd/panda.cc index 1e7b510eca..625e4218f7 100644 --- a/selfdrive/boardd/panda.cc +++ b/selfdrive/boardd/panda.cc @@ -13,11 +13,11 @@ Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { // try USB first, then SPI try { handle = std::make_unique(serial); - LOGW("conntected to %s over USB", serial.c_str()); + LOGW("connected to %s over USB", serial.c_str()); } catch (std::exception &e) { #ifndef __APPLE__ handle = std::make_unique(serial); - LOGW("conntected to %s over SPI", serial.c_str()); + LOGW("connected to %s over SPI", serial.c_str()); #endif } @@ -44,13 +44,15 @@ std::string Panda::hw_serial() { return handle->hw_serial; } -std::vector Panda::list() { +std::vector Panda::list(bool usb_only) { std::vector serials = PandaUsbHandle::list(); #ifndef __APPLE__ - for (auto s : PandaSpiHandle::list()) { - if (std::find(serials.begin(), serials.end(), s) == serials.end()) { - serials.push_back(s); + if (!usb_only) { + for (auto s : PandaSpiHandle::list()) { + if (std::find(serials.begin(), serials.end(), s) == serials.end()) { + serials.push_back(s); + } } } #endif diff --git a/selfdrive/boardd/panda.h b/selfdrive/boardd/panda.h index 69df2e2b66..ea93229725 100644 --- a/selfdrive/boardd/panda.h +++ b/selfdrive/boardd/panda.h @@ -57,7 +57,7 @@ public: std::string hw_serial(); // Static functions - static std::vector list(); + static std::vector list(bool usb_only=false); // Panda functionality cereal::PandaState::PandaType get_hw_type(); diff --git a/selfdrive/boardd/panda_comms.h b/selfdrive/boardd/panda_comms.h index f93d292170..b07aec509e 100644 --- a/selfdrive/boardd/panda_comms.h +++ b/selfdrive/boardd/panda_comms.h @@ -74,9 +74,9 @@ private: uint8_t rx_buf[SPI_BUF_SIZE]; inline static std::recursive_mutex hw_lock; - 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); + int wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout); + int bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len, unsigned int timeout); + int spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout); + int spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout); }; #endif diff --git a/selfdrive/boardd/pandad.py b/selfdrive/boardd/pandad.py index 50027f340f..6ab6d05b08 100755 --- a/selfdrive/boardd/pandad.py +++ b/selfdrive/boardd/pandad.py @@ -7,16 +7,18 @@ import subprocess from typing import List, NoReturn from functools import cmp_to_key -from panda import Panda, PandaDFU +from panda import Panda, PandaDFU, FW_PATH from common.basedir import BASEDIR from common.params import Params +from selfdrive.boardd.set_time import set_time from system.hardware import HARDWARE from system.swaglog import cloudlog def get_expected_signature(panda: Panda) -> bytes: try: - return Panda.get_signature_from_firmware(panda.get_mcu_type().config.app_path) + fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn) + return Panda.get_signature_from_firmware(fn) except Exception: cloudlog.exception("Error computing expected signature") return b"" @@ -132,6 +134,11 @@ def main() -> NoReturn: cloudlog.event("heartbeat lost", deviceState=health, serial=panda.get_usb_serial()) if first_run: + if panda.is_internal(): + # update time from RTC + set_time(cloudlog) + + # reset panda to ensure we're in a good state cloudlog.info(f"Resetting panda {panda.get_usb_serial()}") if panda.is_internal(): HARDWARE.reset_internal_panda() diff --git a/selfdrive/boardd/set_time.py b/selfdrive/boardd/set_time.py index 2159eba5eb..93453dcd97 100755 --- a/selfdrive/boardd/set_time.py +++ b/selfdrive/boardd/set_time.py @@ -3,7 +3,7 @@ import os import datetime from panda import Panda -MIN_DATE = datetime.datetime(year=2023, month=4, day=1) +from common.time import MIN_DATE def set_time(logger): sys_time = datetime.datetime.today() diff --git a/selfdrive/boardd/spi.cc b/selfdrive/boardd/spi.cc index 82a95f7759..e10fd744d5 100644 --- a/selfdrive/boardd/spi.cc +++ b/selfdrive/boardd/spi.cc @@ -22,6 +22,12 @@ #define SPI_NACK 0x1FU #define SPI_CHECKSUM_START 0xABU + +enum SpiError { + NACK = -2, + ACK_TIMEOUT = -3, +}; + struct __attribute__((packed)) spi_header { uint8_t sync; uint8_t endpoint; @@ -29,8 +35,7 @@ struct __attribute__((packed)) spi_header { uint16_t max_rx_len; }; -const int SPI_MAX_RETRIES = 5; -const int SPI_ACK_TIMEOUT = 50; // milliseconds +const unsigned int SPI_ACK_TIMEOUT = 500; // milliseconds const std::string SPI_DEVICE = "/dev/spidev0.0"; class LockEx { @@ -41,8 +46,8 @@ public: }; ~LockEx() { - m.unlock(); flock(fd, LOCK_UN); + m.unlock(); } private: @@ -63,6 +68,10 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { // revs of the comma three may not support this speed uint32_t spi_speed = 50000000; + if (!util::file_exists(SPI_DEVICE)) { + goto fail; + } + spi_fd = open(SPI_DEVICE.c_str(), O_RDWR); if (spi_fd < 0) { LOGE("failed opening SPI device %d", spi_fd); @@ -89,7 +98,7 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { } // get hw UID/serial - ret = control_read(0xc3, 0, 0, uid, uid_len); + ret = control_read(0xc3, 0, 0, uid, uid_len, 100); if (ret == uid_len) { std::stringstream stream; for (int i = 0; i < uid_len; i++) { @@ -127,37 +136,33 @@ void PandaSpiHandle::cleanup() { int PandaSpiHandle::control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout) { - LockEx lock(spi_fd, hw_lock); ControlPacket_t packet = { .request = request, .param1 = param1, .param2 = param2, .length = 0 }; - return spi_transfer_retry(0, (uint8_t *) &packet, sizeof(packet), NULL, 0); + return spi_transfer_retry(0, (uint8_t *) &packet, sizeof(packet), NULL, 0, timeout); } int PandaSpiHandle::control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout) { - LockEx lock(spi_fd, hw_lock); ControlPacket_t packet = { .request = request, .param1 = param1, .param2 = param2, .length = length }; - return spi_transfer_retry(0, (uint8_t *) &packet, sizeof(packet), data, length); + return spi_transfer_retry(0, (uint8_t *) &packet, sizeof(packet), data, length, timeout); } int PandaSpiHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - LockEx lock(spi_fd, hw_lock); - return bulk_transfer(endpoint, data, length, NULL, 0); + return bulk_transfer(endpoint, data, length, NULL, 0, timeout); } int PandaSpiHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { - LockEx lock(spi_fd, hw_lock); - return bulk_transfer(endpoint, NULL, 0, data, length); + return bulk_transfer(endpoint, NULL, 0, data, length, timeout); } -int PandaSpiHandle::bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len) { +int PandaSpiHandle::bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len, unsigned int timeout) { const int xfer_size = 0x40 * 15; int ret = 0; @@ -166,16 +171,16 @@ int PandaSpiHandle::bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t t 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); + d = spi_transfer_retry(endpoint, tx_data + (xfer_size * i), len, NULL, 0, timeout); } else { uint16_t to_read = std::min(xfer_size, rx_len - ret); - d = spi_transfer_retry(endpoint, NULL, 0, rx_data + (xfer_size * i), to_read); + d = spi_transfer_retry(endpoint, NULL, 0, rx_data + (xfer_size * i), to_read, timeout); } if (d < 0) { LOGE("SPI: bulk transfer failed with %d", d); comms_healthy = false; - return -1; + return d; } ret += d; @@ -213,21 +218,43 @@ bool check_checksum(uint8_t *data, int data_len) { } -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 PandaSpiHandle::spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout) { int ret; - - int count = SPI_MAX_RETRIES; + int count = 0; + bool timed_out = false; + double start_time = millis_since_boot(); do { - // TODO: handle error - ret = spi_transfer(endpoint, tx_data, tx_len, rx_data, max_rx_len); - count--; - } while (ret < 0 && connected && count > 0); + ret = spi_transfer(endpoint, tx_data, tx_len, rx_data, max_rx_len, timeout); + + if (ret < 0) { + timed_out = (timeout != 0) && (count > 5); + count += ret == SpiError::ACK_TIMEOUT; + std::this_thread::yield(); + } + } while (ret < 0 && connected && !timed_out); + + if (ret < 0) { + LOGE("transfer failed, after %d tries, %.2fms", count, millis_since_boot() - start_time); + } return ret; } -int PandaSpiHandle::wait_for_ack(spi_ioc_transfer &transfer, uint8_t ack) { +int PandaSpiHandle::wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout) { double start_millis = millis_since_boot(); + if (timeout == 0) { + timeout = SPI_ACK_TIMEOUT; + } + timeout = std::clamp(timeout, 100U, SPI_ACK_TIMEOUT); + + spi_ioc_transfer transfer = { + .tx_buf = (uint64_t)tx_buf, + .rx_buf = (uint64_t)rx_buf, + .delay_usecs = 5, + .len = 1 + }; + tx_buf[0] = tx; + while (true) { int ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); if (ret < 0) { @@ -238,23 +265,24 @@ int PandaSpiHandle::wait_for_ack(spi_ioc_transfer &transfer, uint8_t ack) { if (rx_buf[0] == ack) { break; } else if (rx_buf[0] == SPI_NACK) { - LOGW("SPI: got NACK"); - return -1; + LOGD("SPI: got NACK"); + return SpiError::NACK; } // handle timeout - if (millis_since_boot() - start_millis > SPI_ACK_TIMEOUT) { + if (millis_since_boot() - start_millis > timeout) { LOGD("SPI: timed out waiting for ACK"); - return -1; + return SpiError::ACK_TIMEOUT; } } 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 PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len, unsigned int timeout) { int ret; uint16_t rx_data_len; + LockEx lock(spi_fd, hw_lock); // needs to be less, since we need to have space for the checksum assert(tx_len < SPI_BUF_SIZE); @@ -283,9 +311,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx } // Wait for (N)ACK - tx_buf[0] = 0x11; - transfer.len = 1; - ret = wait_for_ack(transfer, SPI_HACK); + ret = wait_for_ack(SPI_HACK, 0x11, timeout); if (ret < 0) { goto transfer_fail; } @@ -303,9 +329,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx } // Wait for (N)ACK - tx_buf[0] = 0x13; - transfer.len = 1; - ret = wait_for_ack(transfer, SPI_DACK); + ret = wait_for_ack(SPI_DACK, 0x13, timeout); if (ret < 0) { goto transfer_fail; } diff --git a/selfdrive/boardd/tests/bootstub.panda.bin b/selfdrive/boardd/tests/bootstub.panda.bin new file mode 100755 index 0000000000..43db537061 Binary files /dev/null and b/selfdrive/boardd/tests/bootstub.panda.bin differ diff --git a/selfdrive/boardd/tests/bootstub.panda_h7.bin b/selfdrive/boardd/tests/bootstub.panda_h7.bin new file mode 100755 index 0000000000..5cf2fa4519 Binary files /dev/null and b/selfdrive/boardd/tests/bootstub.panda_h7.bin differ diff --git a/selfdrive/boardd/tests/test_boardd_loopback.py b/selfdrive/boardd/tests/test_boardd_loopback.py index 4be5b3f7e8..d0504d6bf2 100755 --- a/selfdrive/boardd/tests/test_boardd_loopback.py +++ b/selfdrive/boardd/tests/test_boardd_loopback.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 import os +import copy import random import time import unittest from collections import defaultdict +from pprint import pprint import cereal.messaging as messaging from cereal import car, log @@ -59,16 +61,18 @@ class TestBoardd(unittest.TestCase): sendcan = messaging.pub_sock('sendcan') can = messaging.sub_sock('can', conflate=False, timeout=100) + sm = messaging.SubMaster(['pandaStates']) time.sleep(0.5) n = 200 for i in range(n): + print(f"boardd loopback {i}/{n}") self.spinner.update(f"boardd loopback {i}/{n}") sent_msgs = defaultdict(set) - for _ in range(random.randrange(10)): + for _ in range(random.randrange(20, 100)): to_send = [] - for __ in range(random.randrange(100)): + for __ in range(random.randrange(20)): bus = random.choice([b for b in range(3*num_pandas) if b % 4 != 3]) addr = random.randrange(1, 1<<29) dat = bytes(random.getrandbits(8) for _ in range(random.randrange(1, 9))) @@ -76,21 +80,29 @@ class TestBoardd(unittest.TestCase): to_send.append(make_can_msg(addr, dat, bus)) sendcan.send(can_list_to_can_capnp(to_send, msgtype='sendcan')) - for _ in range(100 * 2): + sent_loopback = copy.deepcopy(sent_msgs) + sent_loopback.update({k+128: copy.deepcopy(v) for k, v in sent_msgs.items()}) + sent_total = {k: len(v) for k, v in sent_loopback.items()} + for _ in range(100 * 5): + sm.update(0) recvd = messaging.drain_sock(can, wait_for_one=True) for msg in recvd: for m in msg.can: - if m.src >= 128: - key = (m.address, m.dat) - assert key in sent_msgs[m.src-128], f"got unexpected msg: {m.src=} {m.address=} {m.dat=}" - sent_msgs[m.src-128].discard(key) + key = (m.address, m.dat) + assert key in sent_loopback[m.src], f"got unexpected msg: {m.src=} {m.address=} {m.dat=}" + sent_loopback[m.src].discard(key) - if all(len(v) == 0 for v in sent_msgs.values()): + if all(len(v) == 0 for v in sent_loopback.values()): break # if a set isn't empty, messages got dropped - for bus in sent_msgs.keys(): - assert not len(sent_msgs[bus]), f"loop {i}: bus {bus} missing {len(sent_msgs[bus])} messages" + pprint(sent_msgs) + pprint(sent_loopback) + print({k: len(x) for k, x in sent_loopback.items()}) + print(sum([len(x) for x in sent_loopback.values()])) + pprint(sm['pandaStates']) # may drop messages due to RX buffer overflow + for bus in sent_loopback.keys(): + assert not len(sent_loopback[bus]), f"loop {i}: bus {bus} missing {len(sent_loopback[bus])} out of {sent_total[bus]} messages" if __name__ == "__main__": diff --git a/selfdrive/boardd/tests/test_pandad.py b/selfdrive/boardd/tests/test_pandad.py index 50d24f4fe3..fff17523e3 100755 --- a/selfdrive/boardd/tests/test_pandad.py +++ b/selfdrive/boardd/tests/test_pandad.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 +import os import time import unittest import cereal.messaging as messaging from cereal import log from common.gpio import gpio_set, gpio_init -from panda import Panda +from panda import Panda, PandaDFU from selfdrive.test.helpers import phone_only from selfdrive.manager.process_config import managed_processes from system.hardware import HARDWARE from system.hardware.tici.pins import GPIO +HERE = os.path.dirname(os.path.realpath(__file__)) + class TestPandad(unittest.TestCase): @@ -27,11 +30,13 @@ class TestPandad(unittest.TestCase): if sm['peripheralState'].pandaType == log.PandaState.PandaType.unknown: raise Exception("boardd failed to start") + def _go_to_dfu(self): + HARDWARE.recover_internal_panda() + assert Panda.wait_for_dfu(None, 10) + @phone_only def test_in_dfu(self): HARDWARE.recover_internal_panda() - time.sleep(1) - managed_processes['pandad'].start() self._wait_for_boardd(60) @@ -66,9 +71,25 @@ class TestPandad(unittest.TestCase): managed_processes['pandad'].start() self._wait_for_boardd(8) + @phone_only + def test_release_to_devel_bootstub(self): + if HARDWARE.get_device_type() != 'tici': + self.skipTest("TODO: fix reset timeout") - #def test_out_of_date_fw(self): - # pass + # flash release bootstub + self._go_to_dfu() + pd = PandaDFU(None) + fn = os.path.join(HERE, pd.get_mcu_type().config.bootstub_fn) + with open(fn, "rb") as f: + pd.program_bootstub(f.read()) + pd.reset() + + assert Panda.wait_for_panda(None, 20) + with Panda() as p: + assert p.bootstub + + managed_processes['pandad'].start() + self._wait_for_boardd(60) if __name__ == "__main__": diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index d1d18b6e3d..4d76e253cc 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -1,10 +1,10 @@ {% set footnote_tag = '[{}](#footnotes)' %} {% set star_icon = '[![star](assets/icon-star-{}.svg)](##)' %} {% set video_icon = '' %} -{# Force harness column wider by using a blank image with max width. #} +{# Force hardware column wider by using a blank image with max width. #} {% set width_tag = '%s
 ' %} -{% set harness_col_name = 'Harness Kit' %} -{% set wide_harness_col_name = width_tag|format(harness_col_name) -%} +{% set hardware_col_name = 'Hardware Needed' %} +{% set wide_hardware_col_name = width_tag|format(hardware_col_name) -%} @@ -14,14 +14,14 @@ A supported vehicle is one that just works when you install a comma three. All s # {{all_car_info | length}} Supported Cars -|{{Column | map(attribute='value') | join('|') | replace(harness_col_name, wide_harness_col_name)}}| +|{{Column | map(attribute='value') | join('|') | replace(hardware_col_name, wide_hardware_col_name)}}| |---|---|---|{% for _ in range((Column | length) - 3) %}{{':---:|'}}{% endfor +%} {% for car_info in all_car_info %} |{% for column in Column %}{{car_info.get_column(column, star_icon, video_icon, footnote_tag)}}|{% endfor %} {% endfor %} - +### Footnotes {% for footnote in footnotes %} {{loop.index}}{{footnote}}
{% endfor %} diff --git a/selfdrive/car/__init__.py b/selfdrive/car/__init__.py index 615d06c2ee..6bff316c8d 100644 --- a/selfdrive/car/__init__.py +++ b/selfdrive/car/__init__.py @@ -1,10 +1,13 @@ # functions common among cars -import capnp +import math from collections import namedtuple +from typing import Dict, Optional + +import capnp from cereal import car from common.numpy_fast import clip, interp -from typing import Dict + # kg of standard extra cargo to count for drive, gas, etc... STD_CARGO_KG = 136. @@ -181,3 +184,15 @@ def get_safety_config(safety_model, safety_param = None): if safety_param is not None: ret.safetyParam = safety_param return ret + + +class CanBusBase: + offset: int + + def __init__(self, CP, fingerprint: Optional[Dict[int, Dict[int, int]]]) -> None: + if CP is None: + assert fingerprint is not None + num = math.ceil(max([k for k, v in fingerprint.items() if len(v)], default=1) / 4) + else: + num = len(CP.safetyConfigs) + self.offset = 4 * (num - 1) diff --git a/selfdrive/car/body/carcontroller.py b/selfdrive/car/body/carcontroller.py index bcaf6f6f93..64c5617ef3 100644 --- a/selfdrive/car/body/carcontroller.py +++ b/selfdrive/car/body/carcontroller.py @@ -40,7 +40,7 @@ class CarController: torque_l = 0 torque_r = 0 - llk_valid = len(CC.orientationNED) > 0 and len(CC.angularVelocity) > 0 + llk_valid = len(CC.orientationNED) > 1 and len(CC.angularVelocity) > 1 if CC.enabled and llk_valid: # Read these from the joystick # TODO: this isn't acceleration, okay? diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 8339f137b4..c7d1f9905d 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Union from cereal import car from panda.python import uds from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarInfo, Harness, HarnessKit +from selfdrive.car.docs_definitions import CarHarness, CarInfo, CarParts from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, p16 Ecu = car.CarParams.Ecu @@ -69,7 +69,7 @@ RAM_CARS = RAM_DT | RAM_HD @dataclass class ChryslerCarInfo(CarInfo): package: str = "Adaptive Cruise Control (ACC)" - harness_kit: HarnessKit = HarnessKit(Harness.fca) + car_parts: CarParts = CarParts.common([CarHarness.fca]) CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { @@ -83,10 +83,10 @@ CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { ], CAR.JEEP_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), CAR.JEEP_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), - CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-23", harness_kit=HarnessKit(Harness.ram)), + CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-23", car_parts=CarParts.common([CarHarness.ram])), CAR.RAM_HD: [ - ChryslerCarInfo("Ram 2500 2020-22", harness_kit=HarnessKit(Harness.ram)), - ChryslerCarInfo("Ram 3500 2019-22", harness_kit=HarnessKit(Harness.ram)), + ChryslerCarInfo("Ram 2500 2020-22", car_parts=CarParts.common([CarHarness.ram])), + ChryslerCarInfo("Ram 3500 2019-22", car_parts=CarParts.common([CarHarness.ram])), ], } @@ -233,9 +233,11 @@ FW_VERSIONS = { b'68453511AC', b'68453513AD', b'68453514AD', + b'68510280AG', b'68510283AG', - b'68527375AD', b'68527346AE', + b'68527375AD', + b'68527382AE', ], (Ecu.srs, 0x744, None): [ b'68428609AB', @@ -244,6 +246,7 @@ FW_VERSIONS = { b'68490898AA', b'68500728AA', b'68615033AA', + b'68615034AA', ], (Ecu.abs, 0x747, None): [ b'68292406AH', @@ -257,12 +260,13 @@ FW_VERSIONS = { b'68438456AF', b'68535469AB', b'68535470AC', - b'68586307AB', b'68548900AB', + b'68586307AB', ], (Ecu.fwdRadar, 0x753, None): [ b'04672892AB', b'04672932AB', + b'22DTRHD_AA', b'68320950AH', b'68320950AI', b'68320950AJ', @@ -274,8 +278,10 @@ FW_VERSIONS = { b'68475160AG', ], (Ecu.eps, 0x75A, None): [ + b'21590101AA', b'68273275AF', b'68273275AG', + b'68273275AH', b'68312176AE', b'68312176AG', b'68440789AC', @@ -284,23 +290,28 @@ FW_VERSIONS = { b'68522583AB', b'68522585AB', b'68552788AA', - b'68552790AA', - b'68585112AB', b'68552789AA', + b'68552790AA', + b'68585109AB', + b'68585112AB', ], (Ecu.engine, 0x7e0, None): [ b'05036065AE ', b'05036066AE ', + b'05149591AD ', + b'05149846AA ', + b'05149848AA ', b'68378701AI ', + b'68378748AL ', b'68378758AM ', b'68448163AJ', b'68448165AK', b'68500630AD', b'68500630AE', b'68539650AD', - b'05149846AA ', ], (Ecu.transmission, 0x7e1, None): [ + b'05149536AC', b'68360078AL', b'68360080AM', b'68360081AM', @@ -310,8 +321,8 @@ FW_VERSIONS = { b'68445533AB', b'68484467AC', b'68502994AD', - b'68540431AB', b'68520867AE', + b'68540431AB', ], }, @@ -323,6 +334,7 @@ FW_VERSIONS = { b'68525485AB', b'68525487AB', b'68525498AB', + b'68528791AF', ], (Ecu.srs, 0x744, None): [ b'68399794AC', @@ -338,9 +350,11 @@ FW_VERSIONS = { b'68504022AC', b'68530686AB', b'68530686AC', + b'68544596AC', ], (Ecu.fwdRadar, 0x753, None): [ b'04672895AB', + b'04672934AB', b'56029827AG', b'56029827AH', b'68462657AE', @@ -350,6 +364,7 @@ FW_VERSIONS = { (Ecu.eps, 0x761, None): [ b'68421036AC', b'68507906AB', + b'68534023AC', ], (Ecu.engine, 0x7e0, None): [ b'52370131AF', @@ -358,6 +373,7 @@ FW_VERSIONS = { b'52370931CT', b'52401032AE', b'52421132AF', + b'52421332AF', b'68527616AD ', b'M2370131MB', b'M2421132MB', diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index 74fd4616fd..143d64bd52 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -10,7 +10,7 @@ from typing import Dict, List from cereal import car from common.basedir import BASEDIR from selfdrive.car import gen_empty_fingerprint -from selfdrive.car.docs_definitions import CarInfo, Column, CommonFootnote +from selfdrive.car.docs_definitions import CarInfo, Column, CommonFootnote, PartType from selfdrive.car.car_helpers import interfaces, get_interface_attr @@ -61,8 +61,9 @@ def generate_cars_md(all_car_info: List[CarInfo], template_fn: str) -> str: template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True) footnotes = [fn.value.text for fn in get_all_footnotes()] - cars_md: str = template.render(all_car_info=all_car_info, group_by_make=group_by_make, - footnotes=footnotes, Column=Column) + cars_md: str = template.render(all_car_info=all_car_info, PartType=PartType, + group_by_make=group_by_make, footnotes=footnotes, + Column=Column) return cars_md diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index bfb182c5e9..23b460147e 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -1,6 +1,5 @@ import re from collections import namedtuple -import copy from dataclasses import dataclass, field from enum import Enum from typing import Dict, List, Optional, Tuple, Union @@ -21,7 +20,7 @@ class Column(Enum): FSR_STEERING = "No ALC below" STEERING_TORQUE = "Steering Torque" AUTO_RESUME = "Resume from stop" - HARNESS = "Harness Kit" + HARDWARE = "Hardware Needed" VIDEO = "Video" @@ -31,60 +30,130 @@ class Star(Enum): EMPTY = "empty" -class Harness(Enum): - nidec = "Honda Nidec" - bosch_a = "Honda Bosch A" - bosch_b = "Honda Bosch B" - toyota = "Toyota" - subaru_a = "Subaru A" - subaru_b = "Subaru B" - fca = "FCA" - ram = "Ram" - vw = "VW" - j533 = "J533" - hyundai_a = "Hyundai A" - hyundai_b = "Hyundai B" - hyundai_c = "Hyundai C" - hyundai_d = "Hyundai D" - hyundai_e = "Hyundai E" - hyundai_f = "Hyundai F" - hyundai_g = "Hyundai G" - hyundai_h = "Hyundai H" - hyundai_i = "Hyundai I" - hyundai_j = "Hyundai J" - hyundai_k = "Hyundai K" - hyundai_l = "Hyundai L" - hyundai_m = "Hyundai M" - hyundai_n = "Hyundai N" - hyundai_o = "Hyundai O" - hyundai_p = "Hyundai P" - hyundai_q = "Hyundai Q" - custom = "Developer" - obd_ii = "OBD-II" - gm = "GM" - nissan_a = "Nissan A" - nissan_b = "Nissan B" - mazda = "Mazda" - ford_q3 = "Ford Q3" - ford_q4 = "Ford Q4" - none = "None" +# A part + its comprised parts +@dataclass +class BasePart: + name: str + parts: List[Enum] = field(default_factory=list) + + def all_parts(self): + # Recursively get all parts + _parts = 'parts' + parts = [] + parts.extend(getattr(self, _parts)) + for part in getattr(self, _parts): + parts.extend(part.value.all_parts()) + + return parts -class HarnessPart(Enum): - harness_box = "harness box" - comma_power_v2 = "comma power v2" - rj45_cable = "RJ45 cable (7 ft)" - long_obdc_cable = "long OBD-C cable" - usbc_coupler = "USB-C coupler" +class EnumBase(Enum): + @property + def type(self): + return PartType(self.__class__) -DEFAULT_HARNESS_PARTS: List[HarnessPart] = [HarnessPart.harness_box, HarnessPart.comma_power_v2, HarnessPart.rj45_cable] +class Mount(EnumBase): + mount = BasePart("mount") + angled_mount_8_degrees = BasePart("angled mount (8 degrees)") + + +class Cable(EnumBase): + rj45_cable_7ft = BasePart("RJ45 cable (7 ft)") + long_obdc_cable = BasePart("long OBD-C cable") + usb_a_2_a_cable = BasePart("USB A-A cable") + usbc_otg_cable = BasePart("USB C OTG cable") + usbc_coupler = BasePart("USB-C coupler") + obd_c_cable_1_5ft = BasePart("OBD-C cable (1.5 ft)") + right_angle_obd_c_cable_1_5ft = BasePart("right angle OBD-C cable (1.5 ft)") + + +class Accessory(EnumBase): + harness_box = BasePart("harness box") + comma_power_v2 = BasePart("comma power v2") @dataclass -class HarnessKit: - connector: Harness = Harness.none - parts: List[HarnessPart] = field(default_factory=lambda: copy.copy(DEFAULT_HARNESS_PARTS)) +class BaseCarHarness(BasePart): + parts: List[Enum] = field(default_factory=lambda: [Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft]) + has_connector: bool = True # without are hidden on the harness connector page + + +class CarHarness(EnumBase): + nidec = BaseCarHarness("Honda Nidec connector") + bosch_a = BaseCarHarness("Honda Bosch A connector") + bosch_b = BaseCarHarness("Honda Bosch B connector") + toyota = BaseCarHarness("Toyota connector") + subaru_a = BaseCarHarness("Subaru A connector") + subaru_b = BaseCarHarness("Subaru B connector") + fca = BaseCarHarness("FCA connector") + ram = BaseCarHarness("Ram connector") + vw = BaseCarHarness("VW connector") + j533 = BaseCarHarness("J533 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) + hyundai_a = BaseCarHarness("Hyundai A connector") + hyundai_b = BaseCarHarness("Hyundai B connector") + hyundai_c = BaseCarHarness("Hyundai C connector") + hyundai_d = BaseCarHarness("Hyundai D connector") + hyundai_e = BaseCarHarness("Hyundai E connector") + hyundai_f = BaseCarHarness("Hyundai F connector") + hyundai_g = BaseCarHarness("Hyundai G connector") + hyundai_h = BaseCarHarness("Hyundai H connector") + hyundai_i = BaseCarHarness("Hyundai I connector") + hyundai_j = BaseCarHarness("Hyundai J connector") + hyundai_k = BaseCarHarness("Hyundai K connector") + hyundai_l = BaseCarHarness("Hyundai L connector") + hyundai_m = BaseCarHarness("Hyundai M connector") + hyundai_n = BaseCarHarness("Hyundai N connector") + hyundai_o = BaseCarHarness("Hyundai O connector") + hyundai_p = BaseCarHarness("Hyundai P connector") + hyundai_q = BaseCarHarness("Hyundai Q connector") + custom = BaseCarHarness("Developer connector") + obd_ii = BaseCarHarness("OBD-II connector", parts=[Cable.long_obdc_cable, Cable.long_obdc_cable], has_connector=False) + gm = BaseCarHarness("GM connector") + nissan_a = BaseCarHarness("Nissan A connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler]) + nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler]) + mazda = BaseCarHarness("Mazda connector") + ford_q3 = BaseCarHarness("Ford Q3 connector") + ford_q4 = BaseCarHarness("Ford Q4 connector") + + +class Device(EnumBase): + three = BasePart("comma three", parts=[Mount.mount, Cable.right_angle_obd_c_cable_1_5ft]) + # variant of comma three with angled mounts + three_angled_mount = BasePart("comma three", parts=[Mount.angled_mount_8_degrees, Cable.right_angle_obd_c_cable_1_5ft]) + red_panda = BasePart("red panda") + + +class Kit(EnumBase): + red_panda_kit = BasePart("CAN FD panda kit", parts=[Device.red_panda, Accessory.harness_box, Cable.usb_a_2_a_cable, Cable.usbc_otg_cable, Cable.obd_c_cable_1_5ft]) + + +class PartType(Enum): + accessory = Accessory + cable = Cable + connector = CarHarness + device = Device + kit = Kit + mount = Mount + + +DEFAULT_CAR_PARTS: List[EnumBase] = [Device.three] + + +@dataclass +class CarParts: + parts: List[EnumBase] = field(default_factory=list) + + @classmethod + def common(cls, add: List[EnumBase] = None, remove: List[EnumBase] = None): + p = [part for part in (add or []) + DEFAULT_CAR_PARTS if part not in (remove or [])] + return cls(p) + + def all_parts(self): + parts = [] + for part in self.parts: + parts.extend(part.value.all_parts()) + return self.parts + parts CarFootnote = namedtuple("CarFootnote", ["text", "column", "docs_only", "shop_footnote"], defaults=(False, False)) @@ -153,9 +222,10 @@ class CarInfo: footnotes: List[Enum] = field(default_factory=list) min_steer_speed: Optional[float] = None min_enable_speed: Optional[float] = None + auto_resume: Optional[bool] = None - # harness connectors + all the parts needed - harness_kit: HarnessKit = HarnessKit() + # all the parts needed for the supported car + car_parts: CarParts = CarParts() def init(self, CP: car.CarParams, all_footnotes: Dict[Enum, int]): self.car_name = CP.carName @@ -184,13 +254,17 @@ class CarInfo: if self.min_enable_speed is None: self.min_enable_speed = CP.minEnableSpeed - # harness column - harness_col = self.harness_kit.connector.value - if self.harness_kit.connector is not Harness.none: + if self.auto_resume is None: + self.auto_resume = CP.autoResumeSng + + # hardware column + hardware_col = "None" + if self.car_parts.parts: model_years = self.model + (' ' + self.years if self.years else '') - harness_connector = f'- 1 {harness_col} connector' - harness_parts = '
'.join([f"- {self.harness_kit.parts.count(part)} {part.value}" for part in sorted(set(self.harness_kit.parts), key=lambda part: part.value)]) - harness_col = f'
View{harness_connector}
{harness_parts}
' + buy_link = f'Buy Here' + car_parts_docs = self.car_parts.all_parts() + parts = '
'.join([f"- {car_parts_docs.count(part)} {part.value.name}" for part in sorted(set(car_parts_docs), key=lambda part: str(part.value.name))]) + hardware_col = f'
View{parts}
{buy_link}
' self.row: Dict[Enum, Union[str, Star]] = { Column.MAKE: self.make, @@ -200,8 +274,8 @@ class CarInfo: Column.FSR_LONGITUDINAL: f"{max(self.min_enable_speed * CV.MS_TO_MPH, 0):.0f} mph", Column.FSR_STEERING: f"{max(self.min_steer_speed * CV.MS_TO_MPH, 0):.0f} mph", Column.STEERING_TORQUE: Star.EMPTY, - Column.AUTO_RESUME: Star.FULL if CP.autoResumeSng else Star.EMPTY, - Column.HARNESS: harness_col, + Column.AUTO_RESUME: Star.FULL if self.auto_resume else Star.EMPTY, + Column.HARDWARE: hardware_col, Column.VIDEO: self.video_link if self.video_link is not None else "", # replaced with an image and link from template in get_column } @@ -232,7 +306,7 @@ class CarInfo: acc = "" if self.min_enable_speed > 0: acc = f" while driving above {self.min_enable_speed * CV.MS_TO_MPH:.0f} mph" - elif CP.autoResumeSng: + elif self.auto_resume: acc = " that automatically resumes from a stop" if self.row[Column.STEERING_TORQUE] != Star.FULL: diff --git a/selfdrive/car/ford/carcontroller.py b/selfdrive/car/ford/carcontroller.py index d9a9ae6bc0..2a930e735e 100644 --- a/selfdrive/car/ford/carcontroller.py +++ b/selfdrive/car/ford/carcontroller.py @@ -2,9 +2,9 @@ from cereal import car from common.numpy_fast import clip from opendbc.can.packer import CANPacker from selfdrive.car import apply_std_steer_angle_limits -from selfdrive.car.ford.fordcan import create_acc_msg, create_acc_ui_msg, create_button_msg, create_lat_ctl_msg, \ - create_lat_ctl2_msg, create_lka_msg, create_lkas_ui_msg -from selfdrive.car.ford.values import CANBUS, CANFD_CARS, CarControllerParams +from selfdrive.car.ford.fordcan import CanBus, create_acc_msg, create_acc_ui_msg, create_button_msg, \ + create_lat_ctl_msg, create_lat_ctl2_msg, create_lka_msg, create_lkas_ui_msg +from selfdrive.car.ford.values import CANFD_CARS, CarControllerParams LongCtrlState = car.CarControl.Actuators.LongControlState VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -27,6 +27,7 @@ class CarController: self.CP = CP self.VM = VM self.packer = CANPacker(dbc_name) + self.CAN = CanBus(CP) self.frame = 0 self.apply_curvature_last = 0 @@ -45,15 +46,15 @@ class CarController: ### acc buttons ### if CC.cruiseControl.cancel: - can_sends.append(create_button_msg(self.packer, CS.buttons_stock_values, cancel=True)) - can_sends.append(create_button_msg(self.packer, CS.buttons_stock_values, cancel=True, bus=CANBUS.main)) + can_sends.append(create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, cancel=True)) + can_sends.append(create_button_msg(self.packer, self.CAN.main, CS.buttons_stock_values, cancel=True)) elif CC.cruiseControl.resume and (self.frame % CarControllerParams.BUTTONS_STEP) == 0: - can_sends.append(create_button_msg(self.packer, CS.buttons_stock_values, resume=True)) - can_sends.append(create_button_msg(self.packer, CS.buttons_stock_values, resume=True, bus=CANBUS.main)) + can_sends.append(create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, resume=True)) + can_sends.append(create_button_msg(self.packer, self.CAN.main, CS.buttons_stock_values, resume=True)) # if stock lane centering isn't off, send a button press to toggle it off # the stock system checks for steering pressed, and eventually disengages cruise control elif CS.acc_tja_status_stock_values["Tja_D_Stat"] != 0 and (self.frame % CarControllerParams.ACC_UI_STEP) == 0: - can_sends.append(create_button_msg(self.packer, CS.buttons_stock_values, tja_toggle=True)) + can_sends.append(create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, tja_toggle=True)) ### lateral control ### # send steer msg at 20Hz @@ -70,14 +71,14 @@ class CarController: if self.CP.carFingerprint in CANFD_CARS: # TODO: extended mode mode = 1 if CC.latActive else 0 - counter = self.frame // CarControllerParams.STEER_STEP - can_sends.append(create_lat_ctl2_msg(self.packer, mode, 0., 0., -apply_curvature, 0., counter)) + counter = (self.frame // CarControllerParams.STEER_STEP) % 0xF + can_sends.append(create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -apply_curvature, 0., counter)) else: - can_sends.append(create_lat_ctl_msg(self.packer, CC.latActive, 0., 0., -apply_curvature, 0.)) + can_sends.append(create_lat_ctl_msg(self.packer, self.CAN, CC.latActive, 0., 0., -apply_curvature, 0.)) # send lka msg at 33Hz if (self.frame % CarControllerParams.LKA_STEP) == 0: - can_sends.append(create_lka_msg(self.packer)) + can_sends.append(create_lka_msg(self.packer, self.CAN)) ### longitudinal control ### # send acc msg at 50Hz @@ -89,16 +90,16 @@ class CarController: gas = CarControllerParams.INACTIVE_GAS stopping = CC.actuators.longControlState == LongCtrlState.stopping - can_sends.append(create_acc_msg(self.packer, CC.longActive, gas, accel, stopping)) + can_sends.append(create_acc_msg(self.packer, self.CAN, CC.longActive, gas, accel, stopping)) ### ui ### send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != CC.latActive) or (self.steer_alert_last != steer_alert) # send lkas ui msg at 1Hz or if ui state changes if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui: - can_sends.append(create_lkas_ui_msg(self.packer, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values)) + can_sends.append(create_lkas_ui_msg(self.packer, self.CAN, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values)) # send acc ui msg at 5Hz or if ui state changes if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui: - can_sends.append(create_acc_ui_msg(self.packer, self.CP, main_on, CC.latActive, + can_sends.append(create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, CC.latActive, CS.out.cruiseState.standstill, hud_control, CS.acc_tja_status_stock_values)) diff --git a/selfdrive/car/ford/carstate.py b/selfdrive/car/ford/carstate.py index 9be2c7637c..b768749325 100644 --- a/selfdrive/car/ford/carstate.py +++ b/selfdrive/car/ford/carstate.py @@ -3,7 +3,8 @@ from common.conversions import Conversions as CV from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser from selfdrive.car.interfaces import CarStateBase -from selfdrive.car.ford.values import CANBUS, DBC, CarControllerParams +from selfdrive.car.ford.fordcan import CanBus +from selfdrive.car.ford.values import DBC, CarControllerParams GearShifter = car.CarState.GearShifter TransmissionType = car.CarParams.TransmissionType @@ -211,7 +212,7 @@ class CarState(CarStateBase): ("Side_Detect_R_Stat", 5), ] - return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CANBUS.main) + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CanBus(CP).main) @staticmethod def get_cam_can_parser(CP): @@ -268,4 +269,4 @@ class CarState(CarStateBase): ("IPMA_Data", 1), ] - return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CANBUS.camera) + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CanBus(CP).camera) diff --git a/selfdrive/car/ford/fordcan.py b/selfdrive/car/ford/fordcan.py index 97a8c025d4..30a53597d6 100644 --- a/selfdrive/car/ford/fordcan.py +++ b/selfdrive/car/ford/fordcan.py @@ -1,9 +1,26 @@ from cereal import car -from selfdrive.car.ford.values import CANBUS +from selfdrive.car import CanBusBase HUDControl = car.CarControl.HUDControl +class CanBus(CanBusBase): + def __init__(self, CP=None, fingerprint=None) -> None: + super().__init__(CP, fingerprint) + + @property + def main(self) -> int: + return self.offset + + @property + def radar(self): + return self.offset + 1 + + @property + def camera(self): + return self.offset + 2 + + def calculate_lat_ctl2_checksum(mode: int, counter: int, dat: bytearray): curvature = (dat[2] << 3) | ((dat[3]) >> 5) curvature_rate = (dat[6] << 3) | ((dat[7]) >> 5) @@ -17,7 +34,7 @@ def calculate_lat_ctl2_checksum(mode: int, counter: int, dat: bytearray): return 0xFF - (checksum & 0xFF) -def create_lka_msg(packer): +def create_lka_msg(packer, CAN: CanBus): """ Creates an empty CAN message for the Ford LKA Command. @@ -26,10 +43,10 @@ def create_lka_msg(packer): Frequency is 33Hz. """ - return packer.make_can_msg("Lane_Assist_Data1", CANBUS.main, {}) + return packer.make_can_msg("Lane_Assist_Data1", CAN.main, {}) -def create_lat_ctl_msg(packer, lat_active: bool, path_offset: float, path_angle: float, curvature: float, +def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, path_offset: float, path_angle: float, curvature: float, curvature_rate: float): """ Creates a CAN message for the Ford TJA/LCA Command. @@ -66,10 +83,10 @@ def create_lat_ctl_msg(packer, lat_active: bool, path_offset: float, path_angle: "LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2 "LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter } - return packer.make_can_msg("LateralMotionControl", CANBUS.main, values) + return packer.make_can_msg("LateralMotionControl", CAN.main, values) -def create_lat_ctl2_msg(packer, mode: int, path_offset: float, path_angle: float, curvature: float, +def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, path_offset: float, path_angle: float, curvature: float, curvature_rate: float, counter: int): """ Create a CAN message for the new Ford Lane Centering command. @@ -95,13 +112,13 @@ def create_lat_ctl2_msg(packer, mode: int, path_offset: float, path_angle: float } # calculate checksum - dat = packer.make_can_msg("LateralMotionControl2", CANBUS.main, values)[2] + dat = packer.make_can_msg("LateralMotionControl2", 0, values)[2] values["LatCtlPath_No_Cs"] = calculate_lat_ctl2_checksum(mode, counter, dat) - return packer.make_can_msg("LateralMotionControl2", CANBUS.main, values) + return packer.make_can_msg("LateralMotionControl2", CAN.main, values) -def create_acc_msg(packer, long_active: bool, gas: float, accel: float, stopping: bool): +def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: float, stopping: bool): """ Creates a CAN message for the Ford ACC Command. @@ -122,10 +139,10 @@ def create_acc_msg(packer, long_active: bool, gas: float, accel: float, stopping "AccBrkDecel_B_Rq": 1 if decel else 0, # Deceleration request: 0=Inactive, 1=Active "AccStopStat_B_Rq": 1 if stopping else 0, } - return packer.make_can_msg("ACCDATA", CANBUS.main, values) + return packer.make_can_msg("ACCDATA", CAN.main, values) -def create_acc_ui_msg(packer, CP, main_on: bool, enabled: bool, standstill: bool, hud_control, +def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, standstill: bool, hud_control, stock_values: dict): """ Creates a CAN message for the Ford IPC adaptive cruise, forward collision warning and traffic jam @@ -197,10 +214,11 @@ def create_acc_ui_msg(packer, CP, main_on: bool, enabled: bool, standstill: bool "AccTGap_D_Dsply": 4, # Fixed time gap in UI }) - return packer.make_can_msg("ACCDATA_3", CANBUS.main, values) + return packer.make_can_msg("ACCDATA_3", CAN.main, values) -def create_lkas_ui_msg(packer, main_on: bool, enabled: bool, steer_alert: bool, hud_control, stock_values: dict): +def create_lkas_ui_msg(packer, CAN: CanBus, main_on: bool, enabled: bool, steer_alert: bool, hud_control, + stock_values: dict): """ Creates a CAN message for the Ford IPC IPMA/LKAS status. @@ -263,11 +281,10 @@ def create_lkas_ui_msg(packer, main_on: bool, enabled: bool, steer_alert: bool, "LaActvStats_D_Dsply": lines, # LKAS status (lines) [0|31] "LaHandsOff_D_Dsply": hands_on_wheel_dsply, # 0=HandsOn, 1=Level1 (w/o chime), 2=Level2 (w/ chime), 3=Suppressed }) - return packer.make_can_msg("IPMA_Data", CANBUS.main, values) + return packer.make_can_msg("IPMA_Data", CAN.main, values) -def create_button_msg(packer, stock_values: dict, cancel=False, resume=False, tja_toggle=False, - bus: int = CANBUS.camera): +def create_button_msg(packer, bus: int, stock_values: dict, cancel=False, resume=False, tja_toggle=False): """ Creates a CAN message for the Ford SCCM buttons/switches. diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index 937980899f..fef7fcefc8 100644 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 from cereal import car +from panda import Panda from common.conversions import Conversions as CV from selfdrive.car import STD_CARGO_KG, get_safety_config +from selfdrive.car.ford.fordcan import CanBus from selfdrive.car.ford.values import CAR, Ecu from selfdrive.car.interfaces import CarInterfaceBase @@ -13,7 +15,6 @@ class CarInterface(CarInterfaceBase): @staticmethod def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): ret.carName = "ford" - ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.ford)] # These cars are dashcam only for lack of test coverage. # Once a user confirms each car works and a test route is @@ -25,6 +26,17 @@ class CarInterface(CarInterfaceBase): ret.steerActuatorDelay = 0.2 ret.steerLimitTimer = 1.0 + CAN = CanBus(fingerprint=fingerprint) + cfgs = [get_safety_config(car.CarParams.SafetyModel.ford)] + if CAN.main >= 4: + cfgs.insert(0, get_safety_config(car.CarParams.SafetyModel.noOutput)) + ret.safetyConfigs = cfgs + + ret.experimentalLongitudinalAvailable = True + if experimental_long: + ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_FORD_LONG_CONTROL + ret.openpilotLongitudinalControl = True + if candidate == CAR.BRONCO_SPORT_MK1: ret.wheelbase = 2.67 ret.steerRatio = 17.7 @@ -55,7 +67,7 @@ class CarInterface(CarInterfaceBase): # Auto Transmission: 0x732 ECU or Gear_Shift_by_Wire_FD1 found_ecus = [fw.ecu for fw in car_fw] - if Ecu.shiftByWire in found_ecus or 0x5A in fingerprint[0] or docs: + if Ecu.shiftByWire in found_ecus or 0x5A in fingerprint[CAN.main] or docs: ret.transmissionType = TransmissionType.automatic else: ret.transmissionType = TransmissionType.manual @@ -63,7 +75,7 @@ class CarInterface(CarInterfaceBase): # BSM: Side_Detect_L_Stat, Side_Detect_R_Stat # TODO: detect bsm in car_fw? - ret.enableBsm = 0x3A6 in fingerprint[0] and 0x3A7 in fingerprint[0] + ret.enableBsm = 0x3A6 in fingerprint[CAN.main] and 0x3A7 in fingerprint[CAN.main] # LCA can steer down to zero ret.minSteerSpeed = 0. diff --git a/selfdrive/car/ford/radar_interface.py b/selfdrive/car/ford/radar_interface.py index ee4efb311d..e44730ca4f 100644 --- a/selfdrive/car/ford/radar_interface.py +++ b/selfdrive/car/ford/radar_interface.py @@ -3,7 +3,8 @@ from math import cos, sin from cereal import car from opendbc.can.parser import CANParser from common.conversions import Conversions as CV -from selfdrive.car.ford.values import CANBUS, DBC, RADAR +from selfdrive.car.ford.fordcan import CanBus +from selfdrive.car.ford.values import DBC, RADAR from selfdrive.car.interfaces import RadarInterfaceBase DELPHI_ESR_RADAR_MSGS = list(range(0x500, 0x540)) @@ -12,16 +13,16 @@ DELPHI_MRR_RADAR_START_ADDR = 0x120 DELPHI_MRR_RADAR_MSG_COUNT = 64 -def _create_delphi_esr_radar_can_parser(): +def _create_delphi_esr_radar_can_parser(CP) -> CANParser: msg_n = len(DELPHI_ESR_RADAR_MSGS) signals = list(zip(['X_Rel'] * msg_n + ['Angle'] * msg_n + ['V_Rel'] * msg_n, DELPHI_ESR_RADAR_MSGS * 3)) checks = list(zip(DELPHI_ESR_RADAR_MSGS, [20] * msg_n)) - return CANParser(RADAR.DELPHI_ESR, signals, checks, CANBUS.radar) + return CANParser(RADAR.DELPHI_ESR, signals, checks, CanBus(CP).radar) -def _create_delphi_mrr_radar_can_parser(): +def _create_delphi_mrr_radar_can_parser(CP) -> CANParser: signals = [] checks = [] @@ -37,7 +38,7 @@ def _create_delphi_mrr_radar_can_parser(): ] checks += [(msg, 20)] - return CANParser(RADAR.DELPHI_MRR, signals, checks, CANBUS.radar) + return CANParser(RADAR.DELPHI_MRR, signals, checks, CanBus(CP).radar) class RadarInterface(RadarInterfaceBase): @@ -50,11 +51,11 @@ class RadarInterface(RadarInterfaceBase): if self.radar is None or CP.radarUnavailable: self.rcp = None elif self.radar == RADAR.DELPHI_ESR: - self.rcp = _create_delphi_esr_radar_can_parser() + self.rcp = _create_delphi_esr_radar_can_parser(CP) self.trigger_msg = DELPHI_ESR_RADAR_MSGS[-1] self.valid_cnt = {key: 0 for key in DELPHI_ESR_RADAR_MSGS} elif self.radar == RADAR.DELPHI_MRR: - self.rcp = _create_delphi_mrr_radar_can_parser() + self.rcp = _create_delphi_mrr_radar_can_parser(CP) self.trigger_msg = DELPHI_MRR_RADAR_START_ADDR + DELPHI_MRR_RADAR_MSG_COUNT - 1 else: raise ValueError(f"Unsupported radar: {self.radar}") diff --git a/selfdrive/car/ford/values.py b/selfdrive/car/ford/values.py index 6425179a27..d3a5951b79 100644 --- a/selfdrive/car/ford/values.py +++ b/selfdrive/car/ford/values.py @@ -4,7 +4,7 @@ from typing import Dict, List, Set, Union from cereal import car from selfdrive.car import AngleRateLimit, dbc_dict -from selfdrive.car.docs_definitions import CarInfo, Harness, HarnessKit +from selfdrive.car.docs_definitions import CarHarness, CarInfo, CarParts, Device from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries Ecu = car.CarParams.Ecu @@ -29,8 +29,8 @@ class CarControllerParams: ANGLE_RATE_LIMIT_DOWN = AngleRateLimit(speed_bp=[5, 25], angle_v=[0.000225, 0.00015]) CURVATURE_ERROR = 0.002 # ~6 degrees at 10 m/s, ~10 degrees at 35 m/s - ACCEL_MAX = 2.0 # m/s^s max acceleration - ACCEL_MIN = -3.5 # m/s^s max deceleration + ACCEL_MAX = 2.0 # m/s^2 max acceleration + ACCEL_MIN = -3.5 # m/s^2 max deceleration MIN_GAS = -0.5 INACTIVE_GAS = -5.0 @@ -38,12 +38,6 @@ class CarControllerParams: pass -class CANBUS: - main = 0 - radar = 1 - camera = 2 - - class CAR: BRONCO_SPORT_MK1 = "FORD BRONCO SPORT 1ST GEN" ESCAPE_MK4 = "FORD ESCAPE 4TH GEN" @@ -66,7 +60,11 @@ DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict("ford_lincoln_base @dataclass class FordCarInfo(CarInfo): package: str = "Co-Pilot360 Assist+" - harness_kit: HarnessKit = HarnessKit(Harness.ford_q3) + car_parts: CarParts = CarParts.common([CarHarness.ford_q3]) + + def init_make(self, CP: car.CarParams): + if CP.carFingerprint in (CAR.BRONCO_SPORT_MK1, CAR.MAVERICK_MK1): + self.car_parts = CarParts([Device.three_angled_mount, CarHarness.ford_q3]) CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { @@ -77,7 +75,7 @@ CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { ], CAR.EXPLORER_MK6: [ FordCarInfo("Ford Explorer 2020-22"), - FordCarInfo("Lincoln Aviator 2021", "Co-Pilot360 Plus"), + FordCarInfo("Lincoln Aviator 2020-21", "Co-Pilot360 Plus"), ], CAR.FOCUS_MK4: FordCarInfo("Ford Focus EU 2018", "Adaptive Cruise Control with Lane Centering"), CAR.MAVERICK_MK1: FordCarInfo("Ford Maverick 2022-23", "Co-Pilot360 Assist"), @@ -97,6 +95,9 @@ FW_QUERY_CONFIG = FwQueryConfig( whitelist_ecus=[Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.shiftByWire], ), ], + extra_ecus=[ + (Ecu.shiftByWire, 0x732, None), + ], ) FW_VERSIONS = { @@ -119,10 +120,6 @@ FW_VERSIONS = { b'M1PA-14C204-GF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'N1PA-14C204-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], - (Ecu.shiftByWire, 0x732, None): [ - b'LX6P-14G395-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - b'PZ1P-14G395-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - ], }, CAR.ESCAPE_MK4: { (Ecu.eps, 0x730, None): [ @@ -144,16 +141,12 @@ FW_VERSIONS = { ], (Ecu.engine, 0x7E0, None): [ b'LX6A-14C204-BJV\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'LX6A-14C204-BJX\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'LX6A-14C204-ESG\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'MX6A-14C204-BEF\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'MX6A-14C204-BEJ\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'NX6A-14C204-BLE\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], - (Ecu.shiftByWire, 0x732, None): [ - b'LX6P-14G395-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - b'LX6P-14G395-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - b'PZ1P-14G395-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - ], }, CAR.EXPLORER_MK6: { (Ecu.eps, 0x730, None): [ @@ -180,18 +173,13 @@ FW_VERSIONS = { ], (Ecu.engine, 0x7E0, None): [ b'LB5A-14C204-ATJ\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'LB5A-14C204-AZL\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'LB5A-14C204-BUJ\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'LB5A-14C204-EAC\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'MB5A-14C204-MD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'MB5A-14C204-RC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'NB5A-14C204-HB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], - (Ecu.shiftByWire, 0x732, None): [ - b'L1MP-14C561-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - b'L1MP-14G395-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - b'L1MP-14G395-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - b'L1MP-14G395-JB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - ], }, CAR.FOCUS_MK4: { (Ecu.eps, 0x730, None): [ @@ -209,8 +197,6 @@ FW_VERSIONS = { (Ecu.engine, 0x7E0, None): [ b'JX6A-14C204-BPL\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], - (Ecu.shiftByWire, 0x732, None): [ - ], }, CAR.MAVERICK_MK1: { (Ecu.eps, 0x730, None): [ @@ -232,8 +218,5 @@ FW_VERSIONS = { b'NZ6A-14C204-ZA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PZ6A-14C204-JC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], - (Ecu.shiftByWire, 0x732, None): [ - b'NZ6P-14G395-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - ], }, } diff --git a/selfdrive/car/fw_query_definitions.py b/selfdrive/car/fw_query_definitions.py index 7ae9bee404..f9f8e30a68 100755 --- a/selfdrive/car/fw_query_definitions.py +++ b/selfdrive/car/fw_query_definitions.py @@ -3,7 +3,7 @@ import capnp import copy from dataclasses import dataclass, field import struct -from typing import Dict, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Set, Tuple import panda.python.uds as uds @@ -74,6 +74,9 @@ class FwQueryConfig: non_essential_ecus: Dict[capnp.lib.capnp._EnumModule, List[str]] = field(default_factory=dict) # Ecus added for data collection, not to be fingerprinted on extra_ecus: List[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]]] = field(default_factory=list) + # Function a brand can implement to provide better fuzzy matching. Takes in FW versions, + # returns set of candidates. Only will match if one candidate is returned + match_fw_to_car_fuzzy: Optional[Callable[[Dict[Tuple[int, Optional[int]], Set[bytes]]], Set[str]]] = None def __post_init__(self): for i in range(len(self.requests)): diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 1c0d5003ec..5a7a2174c7 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 from collections import defaultdict -from typing import Any, Dict, List, Set +from typing import Any, DefaultDict, Dict, List, Optional, Set, Tuple from tqdm import tqdm +import capnp import panda.python.uds as uds from cereal import car @@ -14,6 +15,7 @@ from system.swaglog import cloudlog Ecu = car.CarParams.Ecu ESSENTIAL_ECUS = [Ecu.engine, Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.vsa] +FUZZY_EXCLUDE_ECUS = [Ecu.fwdCamera, Ecu.fwdRadar, Ecu.eps, Ecu.debug] FW_QUERY_CONFIGS = get_interface_attr('FW_QUERY_CONFIG', ignore_none=True) VERSIONS = get_interface_attr('FW_VERSIONS', ignore_none=True) @@ -27,36 +29,36 @@ def chunks(l, n=128): yield l[i:i + n] -def build_fw_dict(fw_versions, filter_brand=None): +def is_brand(brand: str, filter_brand: Optional[str]) -> bool: + """Returns if brand matches filter_brand or no brand filter is specified""" + return filter_brand is None or brand == filter_brand + + +def build_fw_dict(fw_versions: List[capnp.lib.capnp._DynamicStructBuilder], + filter_brand: Optional[str] = None) -> Dict[Tuple[int, Optional[int]], Set[bytes]]: fw_versions_dict = defaultdict(set) for fw in fw_versions: - if (filter_brand is None or fw.brand == filter_brand) and not fw.logging: + if is_brand(fw.brand, filter_brand) and not fw.logging: sub_addr = fw.subAddress if fw.subAddress != 0 else None fw_versions_dict[(fw.address, sub_addr)].add(fw.fwVersion) return dict(fw_versions_dict) -def get_brand_addrs(): - brand_addrs = defaultdict(set) +def get_brand_addrs() -> Dict[str, Set[Tuple[int, Optional[int]]]]: + brand_addrs: DefaultDict[str, Set[Tuple[int, Optional[int]]]] = defaultdict(set) for brand, cars in VERSIONS.items(): # Add ecus in database + extra ecus to match against brand_addrs[brand] |= {(addr, sub_addr) for _, addr, sub_addr in FW_QUERY_CONFIGS[brand].extra_ecus} for fw in cars.values(): brand_addrs[brand] |= {(addr, sub_addr) for _, addr, sub_addr in fw.keys()} - return brand_addrs + return dict(brand_addrs) -def match_fw_to_car_fuzzy(fw_versions_dict, log=True, exclude=None): +def match_fw_to_car_fuzzy(live_fw_versions, log=True, exclude=None): """Do a fuzzy FW match. This function will return a match, and the number of firmware version that were matched uniquely to that specific car. If multiple ECUs uniquely match to different cars the match is rejected.""" - # These ECUs are known to be shared between models (EPS only between hybrid/ICE version) - # Getting this exactly right isn't crucial, but excluding camera and radar makes it almost - # impossible to get 3 matching versions, even if two models with shared parts are released at the same - # time and only one is in our database. - exclude_types = [Ecu.fwdCamera, Ecu.fwdRadar, Ecu.eps, Ecu.debug] - # Build lookup table from (addr, sub_addr, fw) to list of candidate cars all_fw_versions = defaultdict(list) for candidate, fw_by_addr in FW_VERSIONS.items(): @@ -64,49 +66,56 @@ def match_fw_to_car_fuzzy(fw_versions_dict, log=True, exclude=None): continue for addr, fws in fw_by_addr.items(): - if addr[0] in exclude_types: + # These ECUs are known to be shared between models (EPS only between hybrid/ICE version) + # Getting this exactly right isn't crucial, but excluding camera and radar makes it almost + # impossible to get 3 matching versions, even if two models with shared parts are released at the same + # time and only one is in our database. + if addr[0] in FUZZY_EXCLUDE_ECUS: continue for f in fws: all_fw_versions[(addr[1], addr[2], f)].append(candidate) - match_count = 0 + matched_ecus = set() candidate = None - for addr, versions in fw_versions_dict.items(): + for addr, versions in live_fw_versions.items(): + ecu_key = (addr[0], addr[1]) for version in versions: # All cars that have this FW response on the specified address - candidates = all_fw_versions[(addr[0], addr[1], version)] + candidates = all_fw_versions[(*ecu_key, version)] if len(candidates) == 1: - match_count += 1 + matched_ecus.add(ecu_key) if candidate is None: candidate = candidates[0] # We uniquely matched two different cars. No fuzzy match possible elif candidate != candidates[0]: return set() - if match_count >= 2: + # Note that it is possible to match to a candidate without all its ECUs being present + # if there are enough matches. FIXME: parameterize this or require all ECUs to exist like exact matching + if len(matched_ecus) >= 2: if log: - cloudlog.error(f"Fingerprinted {candidate} using fuzzy match. {match_count} matching ECUs") + cloudlog.error(f"Fingerprinted {candidate} using fuzzy match. {len(matched_ecus)} matching ECUs") return {candidate} else: return set() -def match_fw_to_car_exact(fw_versions_dict) -> Set[str]: +def match_fw_to_car_exact(live_fw_versions, log=True) -> Set[str]: """Do an exact FW match. Returns all cars that match the given FW versions for a list of "essential" ECUs. If an ECU is not considered essential the FW version can be missing to get a fingerprint, but if it's present it needs to match the database.""" - invalid = [] + invalid = set() candidates = FW_VERSIONS for candidate, fws in candidates.items(): + config = FW_QUERY_CONFIGS[MODEL_TO_BRAND[candidate]] for ecu, expected_versions in fws.items(): - config = FW_QUERY_CONFIGS[MODEL_TO_BRAND[candidate]] ecu_type = ecu[0] addr = ecu[1:] - found_versions = fw_versions_dict.get(addr, set()) + found_versions = live_fw_versions.get(addr, set()) if not len(found_versions): # Some models can sometimes miss an ecu, or show on two different addresses if candidate in config.non_essential_ecus.get(ecu_type, []): @@ -121,13 +130,13 @@ def match_fw_to_car_exact(fw_versions_dict) -> Set[str]: continue if not any([found_version in expected_versions for found_version in found_versions]): - invalid.append(candidate) + invalid.add(candidate) break - return set(candidates.keys()) - set(invalid) + return set(candidates.keys()) - invalid -def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True): +def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True, log=True): # Try exact matching first exact_matches = [] if allow_exact: @@ -140,7 +149,12 @@ def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True): matches = set() for brand in VERSIONS.keys(): fw_versions_dict = build_fw_dict(fw_versions, filter_brand=brand) - matches |= match_func(fw_versions_dict) + matches |= match_func(fw_versions_dict, log=log) + + # If specified and no matches so far, fall back to brand's fuzzy fingerprinting function + config = FW_QUERY_CONFIGS[brand] + if not exact_match and not len(matches) and config.match_fw_to_car_fuzzy is not None: + matches |= config.match_fw_to_car_fuzzy(fw_versions_dict) if len(matches): return exact_match, matches @@ -214,7 +228,8 @@ def set_obd_multiplexing(params: Params, obd_multiplexing: bool): cloudlog.warning("OBD multiplexing set successfully") -def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pandas=1, debug=False, progress=False): +def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \ + List[capnp.lib.capnp._DynamicStructBuilder]: """Queries for FW versions ordering brands by likelihood, breaks when exact match is found""" all_car_fw = [] @@ -235,14 +250,11 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pand return all_car_fw -def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, num_pandas=1, debug=False, progress=False): +def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \ + List[capnp.lib.capnp._DynamicStructBuilder]: versions = VERSIONS.copy() params = Params() - # Each brand can define extra ECUs to query for data collection - for brand, config in FW_QUERY_CONFIGS.items(): - versions[brand]["debug"] = {ecu: [] for ecu in config.extra_ecus} - if query_brand is not None: versions = {query_brand: versions[query_brand]} @@ -256,8 +268,10 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, ecu_types = {} for brand, brand_versions in versions.items(): + config = FW_QUERY_CONFIGS[brand] for ecu in brand_versions.values(): - for ecu_type, addr, sub_addr in ecu.keys(): + # Each brand can define extra ECUs to query for data collection + for ecu_type, addr, sub_addr in list(ecu) + config.extra_ecus: a = (brand, addr, sub_addr) if a not in ecu_types: ecu_types[a] = ecu_type @@ -273,7 +287,7 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, # Get versions and build capnp list to put into CarParams car_fw = [] - requests = [(brand, config, r) for brand, config, r in REQUESTS if query_brand is None or brand == query_brand] + requests = [(brand, config, r) for brand, config, r in REQUESTS if is_brand(brand, query_brand)] for addr in tqdm(addrs, disable=not progress): for addr_chunk in chunks(addr): for brand, config, r in requests: @@ -286,11 +300,11 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, set_obd_multiplexing(params, r.obd_multiplexing) try: - addrs = [(a, s) for (b, a, s) in addr_chunk if b in (brand, 'any') and - (len(r.whitelist_ecus) == 0 or ecu_types[(b, a, s)] in r.whitelist_ecus)] + query_addrs = [(a, s) for (b, a, s) in addr_chunk if b in (brand, 'any') and + (len(r.whitelist_ecus) == 0 or ecu_types[(b, a, s)] in r.whitelist_ecus)] - if addrs: - query = IsoTpParallelQuery(sendcan, logcan, r.bus, addrs, r.request, r.response, r.rx_offset, debug=debug) + if query_addrs: + query = IsoTpParallelQuery(sendcan, logcan, r.bus, query_addrs, r.request, r.response, r.rx_offset, debug=debug) for (tx_addr, sub_addr), version in query.get_data(timeout).items(): f = car.CarParams.CarFw.new_message() diff --git a/selfdrive/car/gm/gmcan.py b/selfdrive/car/gm/gmcan.py index 4295301825..5b6923526d 100644 --- a/selfdrive/car/gm/gmcan.py +++ b/selfdrive/car/gm/gmcan.py @@ -20,10 +20,20 @@ def create_buttons(packer, bus, idx, button): def create_pscm_status(packer, bus, pscm_status): - checksum_mod = int(1 - pscm_status["HandsOffSWlDetectionStatus"]) << 5 - pscm_status["HandsOffSWlDetectionStatus"] = 1 - pscm_status["PSCMStatusChecksum"] += checksum_mod - return packer.make_can_msg("PSCMStatus", bus, pscm_status) + values = {s: pscm_status[s] for s in [ + "HandsOffSWDetectionMode", + "HandsOffSWlDetectionStatus", + "LKATorqueDeliveredStatus", + "LKADriverAppldTrq", + "LKATorqueDelivered", + "LKATotalTorqueDelivered", + "RollingCounter", + "PSCMStatusChecksum", + ]} + checksum_mod = int(1 - values["HandsOffSWlDetectionStatus"]) << 5 + values["HandsOffSWlDetectionStatus"] = 1 + values["PSCMStatusChecksum"] += checksum_mod + return packer.make_can_msg("PSCMStatus", bus, values) def create_steering_control(packer, bus, apply_steer, idx, lkas_active): diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 865ba8ed0f..b21c303d85 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -5,7 +5,7 @@ from typing import Dict, List, Union from cereal import car from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarFootnote, CarInfo, Column, Harness, HarnessKit, HarnessPart +from selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarInfo, CarParts, Column Ecu = car.CarParams.Ecu @@ -89,9 +89,9 @@ class GMCarInfo(CarInfo): def init_make(self, CP: car.CarParams): if CP.networkLocation == car.CarParams.NetworkLocation.fwdCamera: - self.harness_kit = HarnessKit(Harness.gm) + self.car_parts = CarParts.common([CarHarness.gm]) else: - self.harness_kit = HarnessKit(Harness.obd_ii, parts=[HarnessPart.long_obdc_cable, HarnessPart.usbc_coupler]) + self.car_parts = CarParts.common([CarHarness.obd_ii]) self.footnotes.append(Footnote.OBD_II) diff --git a/selfdrive/car/honda/carstate.py b/selfdrive/car/honda/carstate.py index 95badd6122..88c42f90e4 100644 --- a/selfdrive/car/honda/carstate.py +++ b/selfdrive/car/honda/carstate.py @@ -155,7 +155,6 @@ class CarState(CarStateBase): self.shifter_values = can_define.dv[self.gearbox_msg]["GEAR_SHIFTER"] self.steer_status_values = defaultdict(lambda: "UNKNOWN", can_define.dv["STEER_STATUS"]["STEER_STATUS"]) - self.brake_error = False self.brake_switch_prev = False self.brake_switch_active = False self.cruise_setting = 0 @@ -203,11 +202,19 @@ class CarState(CarStateBase): ret.steerFaultTemporary = steer_status not in ("NORMAL", "LOW_SPEED_LOCKOUT", "NO_TORQUE_ALERT_2") if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS: - self.brake_error = cp.vl["CRUISE_FAULT_STATUS"]["CRUISE_FAULT"] + ret.accFaulted = bool(cp.vl["CRUISE_FAULT_STATUS"]["CRUISE_FAULT"]) elif self.CP.carFingerprint == CAR.CLARITY: - self.brake_error = cp.vl["BRAKE_ERROR"]["BRAKE_ERROR_1"] or cp.vl["BRAKE_ERROR"]["BRAKE_ERROR_2"] - elif self.CP.openpilotLongitudinalControl: - self.brake_error = cp.vl["STANDSTILL"]["BRAKE_ERROR_1"] or cp.vl["STANDSTILL"]["BRAKE_ERROR_2"] + ret.accFaulted = bool(cp.vl["BRAKE_ERROR"]["BRAKE_ERROR_1"] or cp.vl["BRAKE_ERROR"]["BRAKE_ERROR_2"]) + else: + # On some cars, these two signals are always 1, this flag is masking a bug in release + # FIXME: find and set the ACC faulted signals on more platforms + if self.CP.openpilotLongitudinalControl: + ret.accFaulted = bool(cp.vl["STANDSTILL"]["BRAKE_ERROR_1"] or cp.vl["STANDSTILL"]["BRAKE_ERROR_2"]) + + # Log non-critical stock ACC/LKAS faults if Nidec (camera) + if self.CP.carFingerprint not in HONDA_BOSCH: + ret.carFaultedNonCritical = bool(cp_cam.vl["ACC_HUD"]["ACC_PROBLEM"] or cp_cam.vl["LKAS_HUD"]["LKAS_PROBLEM"]) + ret.espDisabled = cp.vl["VSA_STATUS"]["ESP_DISABLED"] != 0 ret.wheelSpeeds = self.get_wheel_speeds( @@ -353,12 +360,15 @@ class CarState(CarStateBase): ("AEB_REQ_1", "BRAKE_COMMAND"), ("FCW", "BRAKE_COMMAND"), ("CHIME", "BRAKE_COMMAND"), + ("LKAS_PROBLEM", "LKAS_HUD"), ("FCM_OFF", "ACC_HUD"), ("FCM_OFF_2", "ACC_HUD"), ("FCM_PROBLEM", "ACC_HUD"), + ("ACC_PROBLEM", "ACC_HUD"), ("ICONS", "ACC_HUD")] checks += [ ("ACC_HUD", 10), + ("LKAS_HUD", 10), ("BRAKE_COMMAND", 50), ] diff --git a/selfdrive/car/honda/interface.py b/selfdrive/car/honda/interface.py index e82f0a549b..395be441d1 100755 --- a/selfdrive/car/honda/interface.py +++ b/selfdrive/car/honda/interface.py @@ -314,8 +314,8 @@ class CarInterface(CarInterfaceBase): # min speed to enable ACC. if car can do stop and go, then set enabling speed # to a negative value, so it won't matter. Otherwise, add 0.5 mph margin to not # conflict with PCM acc - stop_and_go = candidate in (HONDA_BOSCH | {CAR.CIVIC, CAR.CLARITY}) or ret.enableGasInterceptor - ret.minEnableSpeed = -1. if stop_and_go else 25.5 * CV.MPH_TO_MS + ret.autoResumeSng = candidate in (HONDA_BOSCH | {CAR.CIVIC, CAR.CLARITY}) or ret.enableGasInterceptor + ret.minEnableSpeed = -1. if ret.autoResumeSng else 25.5 * CV.MPH_TO_MS # TODO: start from empirically derived lateral slip stiffness for the civic and scale by # mass and CG position, so all cars will have approximately similar dyn behaviors @@ -379,15 +379,11 @@ class CarInterface(CarInterfaceBase): # events events = self.create_common_events(ret, c, extra_gears=[GearShifter.sport, GearShifter.low], pcm_enable=False) - - events, ret = self.create_sp_events(self.CS, ret, events) - - if self.CS.brake_error: - events.add(EventName.brakeUnavailable) - if self.CP.pcmCruise and ret.vEgo < self.CP.minEnableSpeed and not self.CS.madsEnabled: events.add(EventName.belowEngageSpeed) + events, ret = self.create_sp_events(self.CS, ret, events) + #if self.CP.pcmCruise: # # we engage when pcm is active (rising edge) # if ret.cruiseState.enabled and not self.CS.out.cruiseState.enabled: diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index a5871e961f..57a8a20f74 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -6,7 +6,7 @@ from cereal import car from common.conversions import Conversions as CV from panda.python import uds from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarFootnote, CarInfo, Column, Harness, HarnessKit +from selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarInfo, CarParts, Column from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16 Ecu = car.CarParams.Ecu @@ -111,9 +111,9 @@ class HondaCarInfo(CarInfo): def init_make(self, CP: car.CarParams): if CP.carFingerprint in HONDA_BOSCH: - self.harness_kit = HarnessKit(Harness.bosch_b) if CP.carFingerprint in HONDA_BOSCH_RADARLESS else HarnessKit(Harness.bosch_a) + self.car_parts = CarParts.common([CarHarness.bosch_b]) if CP.carFingerprint in HONDA_BOSCH_RADARLESS else CarParts.common([CarHarness.bosch_a]) else: - self.harness_kit = HarnessKit(Harness.nidec) + self.car_parts = CarParts.common([CarHarness.nidec]) CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { @@ -147,7 +147,7 @@ CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { 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), - HondaCarInfo("Honda Passport 2019-22", "All", min_steer_speed=12. * CV.MPH_TO_MS), + HondaCarInfo("Honda Passport 2019-23", "All", min_steer_speed=12. * CV.MPH_TO_MS), ], CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-23", min_steer_speed=12. * CV.MPH_TO_MS), CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS), @@ -1132,6 +1132,7 @@ FW_VERSIONS = { b'37805-RLV-B210\x00\x00', b'37805-RLV-L160\x00\x00', b'37805-RLV-B420\x00\x00', + b'37805-RLV-F120\x00\x00', ], (Ecu.gateway, 0x18daeff1, None): [ b'38897-TG7-A030\x00\x00', diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index d5cd3bafdb..07e16e0213 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -222,8 +222,8 @@ class CarState(CarStateBase): ret.brakePressed = ret.brakeLights = 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 + ret.doorOpen = cp.vl["DOORS_SEATBELTS"]["DRIVER_DOOR"] == 1 + ret.seatbeltUnlatched = cp.vl["DOORS_SEATBELTS"]["DRIVER_SEATBELT"] == 0 gear = cp.vl[self.gear_msg_canfd]["GEAR"] ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(gear)) @@ -585,8 +585,8 @@ class CarState(CarStateBase): ("LEFT_LAMP", "BLINKERS"), ("RIGHT_LAMP", "BLINKERS"), - ("DRIVER_DOOR_OPEN", "DOORS_SEATBELTS"), - ("DRIVER_SEATBELT_LATCHED", "DOORS_SEATBELTS"), + ("DRIVER_DOOR", "DOORS_SEATBELTS"), + ("DRIVER_SEATBELT", "DOORS_SEATBELTS"), ] checks = [ @@ -614,6 +614,8 @@ class CarState(CarStateBase): if not (CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value) and not CP.openpilotLongitudinalControl: signals += [ + ("COUNTER", "SCC_CONTROL"), + ("CHECKSUM", "SCC_CONTROL"), ("ACCMode", "SCC_CONTROL"), ("VSetDis", "SCC_CONTROL"), ("CRUISE_STANDSTILL", "SCC_CONTROL"), @@ -660,6 +662,7 @@ class CarState(CarStateBase): elif CP.flags & HyundaiFlags.CANFD_CAMERA_SCC: signals += [ ("COUNTER", "SCC_CONTROL"), + ("CHECKSUM", "SCC_CONTROL"), ("NEW_SIGNAL_1", "SCC_CONTROL"), ("MainMode_ACC", "SCC_CONTROL"), ("ACCMode", "SCC_CONTROL"), diff --git a/selfdrive/car/hyundai/hyundaican.py b/selfdrive/car/hyundai/hyundaican.py index 17a9f85c34..e47c20ad09 100644 --- a/selfdrive/car/hyundai/hyundaican.py +++ b/selfdrive/car/hyundai/hyundaican.py @@ -22,8 +22,8 @@ def create_lkas11(packer, frame, car_fingerprint, apply_steer, steer_req, 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.KIA_STINGER_2022, CAR.KIA_K5_HEV_2020, CAR.ELANTRA_2022_NON_SCC, - CAR.GENESIS_G70_2021_NON_SCC): + CAR.SANTA_FE_PHEV_2022, CAR.KIA_STINGER_2022, CAR.KIA_K5_HEV_2020, CAR.KIA_CEED, + CAR.ELANTRA_2022_NON_SCC, CAR.GENESIS_G70_2021_NON_SCC): values["CF_Lkas_LdwsActivemode"] = int(left_lane) + (int(right_lane) << 1) values["CF_Lkas_LdwsOpt_USM"] = 2 diff --git a/selfdrive/car/hyundai/hyundaicanfd.py b/selfdrive/car/hyundai/hyundaicanfd.py index 60ffa14f41..c86b201eb9 100644 --- a/selfdrive/car/hyundai/hyundaicanfd.py +++ b/selfdrive/car/hyundai/hyundaicanfd.py @@ -1,17 +1,15 @@ -import math - from common.numpy_fast import clip +from selfdrive.car import CanBusBase from selfdrive.car.hyundai.values import HyundaiFlags -class CanBus: - def __init__(self, CP, hda2=None, fingerprint=None): - if CP is None: - assert None not in (hda2, fingerprint) - num = math.ceil(max([k for k, v in fingerprint.items() if len(v)], default=1) / 4) - else: +class CanBus(CanBusBase): + def __init__(self, CP, hda2=None, fingerprint=None) -> None: + super().__init__(CP, fingerprint) + + if hda2 is None: + assert CP is not None hda2 = CP.flags & HyundaiFlags.CANFD_HDA2.value - num = len(CP.safetyConfigs) # On the CAN-FD platforms, the LKAS camera is on both A-CAN and E-CAN. HDA2 cars # have a different harness than the HDA1 and non-HDA variants in order to split @@ -20,10 +18,9 @@ class CanBus: if hda2: self._a, self._e = 0, 1 - offset = 4*(num - 1) - self._a += offset - self._e += offset - self._cam = 2 + offset + self._a += self.offset + self._e += self.offset + self._cam = 2 + self.offset @property def ECAN(self): diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 3f2c219923..e9ed98ccf5 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -343,7 +343,7 @@ class CarInterface(CarInterfaceBase): # for blinkers if CP.flags & HyundaiFlags.ENABLE_BLINKERS: - disable_ecu(logcan, sendcan, bus=CanBus(CP.ECAN), addr=0x7B1, com_cont_req=b'\x28\x83\x01') + disable_ecu(logcan, sendcan, bus=CanBus(CP).ECAN, addr=0x7B1, com_cont_req=b'\x28\x83\x01') def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) diff --git a/selfdrive/car/hyundai/tests/print_platform_codes.py b/selfdrive/car/hyundai/tests/print_platform_codes.py new file mode 100755 index 0000000000..1bc8a4e366 --- /dev/null +++ b/selfdrive/car/hyundai/tests/print_platform_codes.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +from cereal import car +from selfdrive.car.hyundai.values import FW_VERSIONS, PLATFORM_CODE_ECUS, get_platform_codes + +Ecu = car.CarParams.Ecu +ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()} + +if __name__ == "__main__": + for car_model, ecus in FW_VERSIONS.items(): + print() + print(car_model) + for ecu in sorted(ecus, key=lambda x: int(x[0])): + if ecu[0] not in PLATFORM_CODE_ECUS: + continue + + platform_codes = get_platform_codes(ecus[ecu]) + codes = {code for code, _ in platform_codes} + dates = {date for _, date in platform_codes if date is not None} + print(f' (Ecu.{ECU_NAME[ecu[0]]}, {hex(ecu[1])}, {ecu[2]}):') + print(f' Codes: {codes}') + print(f' Dates: {dates}') diff --git a/selfdrive/car/hyundai/tests/test_hyundai.py b/selfdrive/car/hyundai/tests/test_hyundai.py index fba99ae74d..36d2b3dc4c 100755 --- a/selfdrive/car/hyundai/tests/test_hyundai.py +++ b/selfdrive/car/hyundai/tests/test_hyundai.py @@ -2,11 +2,38 @@ import unittest from cereal import car -from selfdrive.car.hyundai.values import CANFD_CAR, FW_QUERY_CONFIG, FW_VERSIONS, CAN_GEARS, LEGACY_SAFETY_MODE_CAR, CHECKSUM, CAMERA_SCC_CAR +from selfdrive.car.fw_versions import build_fw_dict +from selfdrive.car.hyundai.values import CAMERA_SCC_CAR, CANFD_CAR, CAN_GEARS, CAR, CHECKSUM, DATE_FW_ECUS, \ + EV_CAR, FW_QUERY_CONFIG, FW_VERSIONS, LEGACY_SAFETY_MODE_CAR, \ + PLATFORM_CODE_ECUS, get_platform_codes Ecu = car.CarParams.Ecu ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()} +# Some platforms have date codes in a different format we don't yet parse (or are missing). +# For now, assert list of expected missing date cars +NO_DATES_PLATFORMS = { + # CAN FD + CAR.KIA_SPORTAGE_5TH_GEN, + CAR.KIA_SPORTAGE_HYBRID_5TH_GEN, + CAR.SANTA_CRUZ_1ST_GEN, + CAR.TUCSON_4TH_GEN, + CAR.TUCSON_HYBRID_4TH_GEN, + # CAN + CAR.ELANTRA, + CAR.KIA_CEED, + CAR.KIA_FORTE, + CAR.KIA_OPTIMA_G4, + CAR.KIA_OPTIMA_G4_FL, + CAR.KIA_SORENTO, + CAR.KONA, + CAR.KONA_EV, + CAR.KONA_EV_2022, + CAR.KONA_HEV, + CAR.SONATA_LF, + CAR.VELOSTER, +} + class TestHyundaiFingerprint(unittest.TestCase): def test_canfd_not_in_can_features(self): @@ -21,8 +48,115 @@ class TestHyundaiFingerprint(unittest.TestCase): for car_model in CANFD_CAR: ecus = {fw[0] for fw in FW_VERSIONS[car_model].keys()} ecus_not_in_whitelist = ecus - whitelisted_ecus - ecu_strings = ", ".join([f'Ecu.{ECU_NAME[ecu]}' for ecu in ecus_not_in_whitelist]) - self.assertEqual(len(ecus_not_in_whitelist), 0, f'{car_model}: Car model has ECUs not in auxiliary request whitelists: {ecu_strings}') + ecu_strings = ", ".join([f"Ecu.{ECU_NAME[ecu]}" for ecu in ecus_not_in_whitelist]) + self.assertEqual(len(ecus_not_in_whitelist), 0, + f"{car_model}: Car model has ECUs not in auxiliary request whitelists: {ecu_strings}") + + # Tests for platform codes, part numbers, and FW dates which Hyundai will use to fuzzy + # fingerprint in the absence of full FW matches: + def test_platform_code_ecus_available(self): + # TODO: add queries for these non-CAN FD cars to get EPS + no_eps_platforms = CANFD_CAR | {CAR.KIA_SORENTO, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, + CAR.SONATA_LF, CAR.TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80} + + # Asserts ECU keys essential for fuzzy fingerprinting are available on all platforms + for car_model, ecus in FW_VERSIONS.items(): + with self.subTest(car_model=car_model): + for platform_code_ecu in PLATFORM_CODE_ECUS: + if platform_code_ecu in (Ecu.fwdRadar, Ecu.eps) and car_model == CAR.HYUNDAI_GENESIS: + continue + if platform_code_ecu == Ecu.eps and car_model in no_eps_platforms: + continue + self.assertIn(platform_code_ecu, [e[0] for e in ecus]) + + def test_fw_format(self): + # Asserts: + # - every supported ECU FW version returns one platform code + # - every supported ECU FW version has a part number + # - expected parsing of ECU FW dates + + for car_model, ecus in FW_VERSIONS.items(): + with self.subTest(car_model=car_model): + for ecu, fws in ecus.items(): + if ecu[0] not in PLATFORM_CODE_ECUS: + continue + + codes = set() + for fw in fws: + result = get_platform_codes([fw]) + self.assertEqual(1, len(result), f"Unable to parse FW: {fw}") + codes |= result + + if ecu[0] not in DATE_FW_ECUS or car_model in NO_DATES_PLATFORMS: + self.assertTrue(all({date is None for _, date in codes})) + else: + self.assertTrue(all({date is not None for _, date in codes})) + + if car_model == CAR.HYUNDAI_GENESIS: + raise unittest.SkipTest("No part numbers for car model") + + # Hyundai places the ECU part number in their FW versions, assert all parsable + # Some examples of valid formats: b"56310-L0010", b"56310L0010", b"56310/M6300" + self.assertTrue(all({b"-" in code for code, _ in codes}), + f"FW does not have part number: {fw}") + + def test_platform_codes_spot_check(self): + # Asserts basic platform code parsing behavior for a few cases + results = get_platform_codes([b"\xf1\x00DH LKAS 1.1 -150210"]) + self.assertEqual(results, {(b"DH", b"150210")}) + + # Some cameras and all radars do not have dates + results = get_platform_codes([b"\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 "]) + self.assertEqual(results, {(b"AEhe-G2000", None)}) + + results = get_platform_codes([b"\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 "]) + self.assertEqual(results, {(b"CV1-CV000", None)}) + + results = get_platform_codes([ + b"\xf1\x00DH LKAS 1.1 -150210", + b"\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 ", + b"\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ", + ]) + self.assertEqual(results, {(b"DH", b"150210"), (b"AEhe-G2000", None), (b"CV1-CV000", None)}) + + results = get_platform_codes([ + b"\xf1\x00LX2 MFC AT USA LHD 1.00 1.07 99211-S8100 220222", + b"\xf1\x00LX2 MFC AT USA LHD 1.00 1.08 99211-S8100 211103", + b"\xf1\x00ON MFC AT USA LHD 1.00 1.01 99211-S9100 190405", + b"\xf1\x00ON MFC AT USA LHD 1.00 1.03 99211-S9100 190720", + ]) + self.assertEqual(results, {(b"LX2-S8100", b"220222"), (b"LX2-S8100", b"211103"), + (b"ON-S9100", b"190405"), (b"ON-S9100", b"190720")}) + + def test_fuzzy_excluded_platforms(self): + # Asserts a list of platforms that will not fuzzy fingerprint with platform codes due to them being shared. + # This list can be shrunk as we combine platforms and detect features + excluded_platforms = { + CAR.GENESIS_G70, # shared platform code, part number, and date + CAR.GENESIS_G70_2020, + CAR.TUCSON_4TH_GEN, # shared platform code and part number + CAR.TUCSON_HYBRID_4TH_GEN, + } + excluded_platforms |= CANFD_CAR - EV_CAR # shared platform codes + excluded_platforms |= NO_DATES_PLATFORMS # date codes are required to match + + platforms_with_shared_codes = set() + for platform, fw_by_addr in FW_VERSIONS.items(): + car_fw = [] + for ecu, fw_versions in fw_by_addr.items(): + ecu_name, addr, sub_addr = ecu + for fw in fw_versions: + car_fw.append({"ecu": ecu_name, "fwVersion": fw, "address": addr, + "subAddress": 0 if sub_addr is None else sub_addr}) + + CP = car.CarParams.new_message(carFw=car_fw) + matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw)) + if len(matches) == 1: + self.assertEqual(list(matches)[0], platform) + else: + platforms_with_shared_codes.add(platform) + + self.assertEqual(platforms_with_shared_codes, excluded_platforms) if __name__ == "__main__": diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 8fab34c054..5c2bdb4a90 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -1,12 +1,13 @@ +import re from dataclasses import dataclass from enum import Enum, IntFlag -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Set, Tuple, Union from cereal import car from panda.python import uds from common.conversions import Conversions as CV from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarFootnote, CarInfo, Column, Harness, HarnessKit +from selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarInfo, CarParts, Column from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, p16 Ecu = car.CarParams.Ecu @@ -154,114 +155,114 @@ class HyundaiCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { CAR.ELANTRA: [ - HyundaiCarInfo("Hyundai Elantra 2017-19", min_enable_speed=19 * CV.MPH_TO_MS, harness_kit=HarnessKit(Harness.hyundai_b)), - HyundaiCarInfo("Hyundai Elantra GT 2017-19", harness_kit=HarnessKit(Harness.hyundai_e)), - HyundaiCarInfo("Hyundai i30 2017-19", harness_kit=HarnessKit(Harness.hyundai_e)), + HyundaiCarInfo("Hyundai Elantra 2017-19", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_b])), + HyundaiCarInfo("Hyundai Elantra GT 2017-19", car_parts=CarParts.common([CarHarness.hyundai_e])), + HyundaiCarInfo("Hyundai i30 2017-19", car_parts=CarParts.common([CarHarness.hyundai_e])), ], - CAR.ELANTRA_2021: HyundaiCarInfo("Hyundai Elantra 2021-23", video_link="https://youtu.be/_EdYQtV52-c", harness_kit=HarnessKit(Harness.hyundai_k)), - CAR.ELANTRA_HEV_2021: HyundaiCarInfo("Hyundai Elantra Hybrid 2021-23", video_link="https://youtu.be/_EdYQtV52-c", harness_kit=HarnessKit(Harness.hyundai_k)), + CAR.ELANTRA_2021: HyundaiCarInfo("Hyundai Elantra 2021-23", video_link="https://youtu.be/_EdYQtV52-c", car_parts=CarParts.common([CarHarness.hyundai_k])), + CAR.ELANTRA_HEV_2021: HyundaiCarInfo("Hyundai Elantra Hybrid 2021-23", video_link="https://youtu.be/_EdYQtV52-c", car_parts=CarParts.common([CarHarness.hyundai_k])), CAR.HYUNDAI_GENESIS: [ - HyundaiCarInfo("Hyundai Genesis 2015-16", min_enable_speed=19 * CV.MPH_TO_MS, harness_kit=HarnessKit(Harness.hyundai_j)), # TODO: check 2015 packages - HyundaiCarInfo("Genesis G80 2017", "All", min_enable_speed=19 * CV.MPH_TO_MS, harness_kit=HarnessKit(Harness.hyundai_j)), + HyundaiCarInfo("Hyundai Genesis 2015-16", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_j])), # TODO: check 2015 packages + HyundaiCarInfo("Genesis G80 2017", "All", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_j])), ], - CAR.IONIQ: HyundaiCarInfo("Hyundai Ioniq Hybrid 2017-19", harness_kit=HarnessKit(Harness.hyundai_c)), - CAR.IONIQ_HEV_2022: HyundaiCarInfo("Hyundai Ioniq Hybrid 2020-22", harness_kit=HarnessKit(Harness.hyundai_h)), # TODO: confirm 2020-21 harness - CAR.IONIQ_EV_LTD: HyundaiCarInfo("Hyundai Ioniq Electric 2019", harness_kit=HarnessKit(Harness.hyundai_c)), - CAR.IONIQ_EV_2020: HyundaiCarInfo("Hyundai Ioniq Electric 2020", "All", harness_kit=HarnessKit(Harness.hyundai_h)), - CAR.IONIQ_PHEV_2019: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2019", harness_kit=HarnessKit(Harness.hyundai_c)), - CAR.IONIQ_PHEV: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2020-22", "All", harness_kit=HarnessKit(Harness.hyundai_h)), - CAR.KONA: HyundaiCarInfo("Hyundai Kona 2020", harness_kit=HarnessKit(Harness.hyundai_b)), - CAR.KONA_EV: HyundaiCarInfo("Hyundai Kona Electric 2018-21", harness_kit=HarnessKit(Harness.hyundai_g)), - CAR.KONA_EV_2022: HyundaiCarInfo("Hyundai Kona Electric 2022", harness_kit=HarnessKit(Harness.hyundai_o)), - CAR.KONA_HEV: HyundaiCarInfo("Hyundai Kona Hybrid 2020", video_link="https://youtu.be/0dwpAHiZgFo", harness_kit=HarnessKit(Harness.hyundai_i)), # TODO: check packages - CAR.SANTA_FE: HyundaiCarInfo("Hyundai Santa Fe 2019-20", "All", harness_kit=HarnessKit(Harness.hyundai_d)), - CAR.SANTA_FE_2022: HyundaiCarInfo("Hyundai Santa Fe 2021-22", "All", video_link="https://youtu.be/VnHzSTygTS4", harness_kit=HarnessKit(Harness.hyundai_l)), - CAR.SANTA_FE_HEV_2022: HyundaiCarInfo("Hyundai Santa Fe Hybrid 2022", "All", harness_kit=HarnessKit(Harness.hyundai_l)), - CAR.SANTA_FE_PHEV_2022: HyundaiCarInfo("Hyundai Santa Fe Plug-in Hybrid 2022", "All", harness_kit=HarnessKit(Harness.hyundai_l)), - CAR.SONATA: HyundaiCarInfo("Hyundai Sonata 2020-23", "All", video_link="https://www.youtube.com/watch?v=ix63r9kE3Fw", harness_kit=HarnessKit(Harness.hyundai_a)), - CAR.SONATA_LF: HyundaiCarInfo("Hyundai Sonata 2018-19", harness_kit=HarnessKit(Harness.hyundai_e)), + CAR.IONIQ: HyundaiCarInfo("Hyundai Ioniq Hybrid 2017-19", car_parts=CarParts.common([CarHarness.hyundai_c])), + CAR.IONIQ_HEV_2022: HyundaiCarInfo("Hyundai Ioniq Hybrid 2020-22", car_parts=CarParts.common([CarHarness.hyundai_h])), # TODO: confirm 2020-21 harness + CAR.IONIQ_EV_LTD: HyundaiCarInfo("Hyundai Ioniq Electric 2019", car_parts=CarParts.common([CarHarness.hyundai_c])), + CAR.IONIQ_EV_2020: HyundaiCarInfo("Hyundai Ioniq Electric 2020", "All", car_parts=CarParts.common([CarHarness.hyundai_h])), + CAR.IONIQ_PHEV_2019: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2019", car_parts=CarParts.common([CarHarness.hyundai_c])), + CAR.IONIQ_PHEV: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2020-22", "All", car_parts=CarParts.common([CarHarness.hyundai_h])), + CAR.KONA: HyundaiCarInfo("Hyundai Kona 2020", car_parts=CarParts.common([CarHarness.hyundai_b])), + CAR.KONA_EV: HyundaiCarInfo("Hyundai Kona Electric 2018-21", car_parts=CarParts.common([CarHarness.hyundai_g])), + CAR.KONA_EV_2022: HyundaiCarInfo("Hyundai Kona Electric 2022", car_parts=CarParts.common([CarHarness.hyundai_o])), + CAR.KONA_HEV: HyundaiCarInfo("Hyundai Kona Hybrid 2020", video_link="https://youtu.be/0dwpAHiZgFo", car_parts=CarParts.common([CarHarness.hyundai_i])), # TODO: check packages + CAR.SANTA_FE: HyundaiCarInfo("Hyundai Santa Fe 2019-20", "All", car_parts=CarParts.common([CarHarness.hyundai_d])), + CAR.SANTA_FE_2022: HyundaiCarInfo("Hyundai Santa Fe 2021-22", "All", video_link="https://youtu.be/VnHzSTygTS4", car_parts=CarParts.common([CarHarness.hyundai_l])), + CAR.SANTA_FE_HEV_2022: HyundaiCarInfo("Hyundai Santa Fe Hybrid 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l])), + CAR.SANTA_FE_PHEV_2022: HyundaiCarInfo("Hyundai Santa Fe Plug-in Hybrid 2022", "All", car_parts=CarParts.common([CarHarness.hyundai_l])), + CAR.SONATA: HyundaiCarInfo("Hyundai Sonata 2020-23", "All", video_link="https://www.youtube.com/watch?v=ix63r9kE3Fw", car_parts=CarParts.common([CarHarness.hyundai_a])), + CAR.SONATA_LF: HyundaiCarInfo("Hyundai Sonata 2018-19", car_parts=CarParts.common([CarHarness.hyundai_e])), CAR.TUCSON: [ - HyundaiCarInfo("Hyundai Tucson 2021", min_enable_speed=19 * CV.MPH_TO_MS, harness_kit=HarnessKit(Harness.hyundai_l)), - HyundaiCarInfo("Hyundai Tucson Diesel 2019", harness_kit=HarnessKit(Harness.hyundai_l)), + HyundaiCarInfo("Hyundai Tucson 2021", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_l])), + HyundaiCarInfo("Hyundai Tucson Diesel 2019", car_parts=CarParts.common([CarHarness.hyundai_l])), ], CAR.PALISADE: [ - HyundaiCarInfo("Hyundai Palisade 2020-22", "All", video_link="https://youtu.be/TAnDqjF4fDY?t=456", harness_kit=HarnessKit(Harness.hyundai_h)), - HyundaiCarInfo("Kia Telluride 2020-22", "All", harness_kit=HarnessKit(Harness.hyundai_h)), + HyundaiCarInfo("Hyundai Palisade 2020-22", "All", video_link="https://youtu.be/TAnDqjF4fDY?t=456", car_parts=CarParts.common([CarHarness.hyundai_h])), + HyundaiCarInfo("Kia Telluride 2020-22", "All", car_parts=CarParts.common([CarHarness.hyundai_h])), ], - CAR.VELOSTER: HyundaiCarInfo("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, harness_kit=HarnessKit(Harness.hyundai_e)), - CAR.SONATA_HYBRID: HyundaiCarInfo("Hyundai Sonata Hybrid 2020-22", "All", harness_kit=HarnessKit(Harness.hyundai_a)), + CAR.VELOSTER: HyundaiCarInfo("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_e])), + CAR.SONATA_HYBRID: HyundaiCarInfo("Hyundai Sonata Hybrid 2020-22", "All", car_parts=CarParts.common([CarHarness.hyundai_a])), CAR.IONIQ_5: [ - HyundaiCarInfo("Hyundai Ioniq 5 (Southeast Asia only) 2022-23", "All", harness_kit=HarnessKit(Harness.hyundai_q)), - HyundaiCarInfo("Hyundai Ioniq 5 (without HDA II) 2022-23", "Highway Driving Assist", harness_kit=HarnessKit(Harness.hyundai_k)), - HyundaiCarInfo("Hyundai Ioniq 5 (with HDA II) 2022-23", "Highway Driving Assist II", harness_kit=HarnessKit(Harness.hyundai_q)), + HyundaiCarInfo("Hyundai Ioniq 5 (Southeast Asia only) 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_q])), + HyundaiCarInfo("Hyundai Ioniq 5 (without HDA II) 2022-23", "Highway Driving Assist", car_parts=CarParts.common([CarHarness.hyundai_k])), + HyundaiCarInfo("Hyundai Ioniq 5 (with HDA II) 2022-23", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_q])), ], CAR.TUCSON_4TH_GEN: [ - HyundaiCarInfo("Hyundai Tucson 2022", harness_kit=HarnessKit(Harness.hyundai_n)), - HyundaiCarInfo("Hyundai Tucson 2023", "All", harness_kit=HarnessKit(Harness.hyundai_n)), + HyundaiCarInfo("Hyundai Tucson 2022", car_parts=CarParts.common([CarHarness.hyundai_n])), + HyundaiCarInfo("Hyundai Tucson 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_n])), ], - CAR.TUCSON_HYBRID_4TH_GEN: HyundaiCarInfo("Hyundai Tucson Hybrid 2022-23", "All", harness_kit=HarnessKit(Harness.hyundai_n)), - CAR.SANTA_CRUZ_1ST_GEN: HyundaiCarInfo("Hyundai Santa Cruz 2022-23", harness_kit=HarnessKit(Harness.hyundai_n)), + CAR.TUCSON_HYBRID_4TH_GEN: HyundaiCarInfo("Hyundai Tucson Hybrid 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_n])), + CAR.SANTA_CRUZ_1ST_GEN: HyundaiCarInfo("Hyundai Santa Cruz 2022-23", car_parts=CarParts.common([CarHarness.hyundai_n])), # Kia CAR.KIA_FORTE: [ - HyundaiCarInfo("Kia Forte 2019-21", harness_kit=HarnessKit(Harness.hyundai_g)), - HyundaiCarInfo("Kia Forte 2023", harness_kit=HarnessKit(Harness.hyundai_e)), + HyundaiCarInfo("Kia Forte 2019-21", car_parts=CarParts.common([CarHarness.hyundai_g])), + HyundaiCarInfo("Kia Forte 2023", car_parts=CarParts.common([CarHarness.hyundai_e])), ], - CAR.KIA_K5_2021: HyundaiCarInfo("Kia K5 2021-22", harness_kit=HarnessKit(Harness.hyundai_a)), - CAR.KIA_K5_HEV_2020: HyundaiCarInfo("Kia K5 Hybrid 2020", harness_kit=HarnessKit(Harness.hyundai_a)), + CAR.KIA_K5_2021: HyundaiCarInfo("Kia K5 2021-22", car_parts=CarParts.common([CarHarness.hyundai_a])), + CAR.KIA_K5_HEV_2020: HyundaiCarInfo("Kia K5 Hybrid 2020", car_parts=CarParts.common([CarHarness.hyundai_a])), CAR.KIA_NIRO_EV: [ - HyundaiCarInfo("Kia Niro EV 2019", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness_kit=HarnessKit(Harness.hyundai_h)), - HyundaiCarInfo("Kia Niro EV 2020", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness_kit=HarnessKit(Harness.hyundai_f)), - HyundaiCarInfo("Kia Niro EV 2021", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness_kit=HarnessKit(Harness.hyundai_c)), - HyundaiCarInfo("Kia Niro EV 2022", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness_kit=HarnessKit(Harness.hyundai_h)), + HyundaiCarInfo("Kia Niro EV 2019", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", car_parts=CarParts.common([CarHarness.hyundai_h])), + HyundaiCarInfo("Kia Niro EV 2020", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", car_parts=CarParts.common([CarHarness.hyundai_f])), + HyundaiCarInfo("Kia Niro EV 2021", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", car_parts=CarParts.common([CarHarness.hyundai_c])), + HyundaiCarInfo("Kia Niro EV 2022", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", car_parts=CarParts.common([CarHarness.hyundai_h])), ], - CAR.KIA_NIRO_EV_2ND_GEN: HyundaiCarInfo("Kia Niro EV 2023", "All", harness_kit=HarnessKit(Harness.hyundai_a)), + CAR.KIA_NIRO_EV_2ND_GEN: HyundaiCarInfo("Kia Niro EV 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_a])), CAR.KIA_NIRO_PHEV: [ - HyundaiCarInfo("Kia Niro Plug-in Hybrid 2018-19", "All", min_enable_speed=10. * CV.MPH_TO_MS, harness_kit=HarnessKit(Harness.hyundai_c)), - HyundaiCarInfo("Kia Niro Plug-in Hybrid 2020", "All", harness_kit=HarnessKit(Harness.hyundai_d)), + HyundaiCarInfo("Kia Niro Plug-in Hybrid 2018-19", "All", min_enable_speed=10. * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_c])), + HyundaiCarInfo("Kia Niro Plug-in Hybrid 2020", "All", car_parts=CarParts.common([CarHarness.hyundai_d])), ], CAR.KIA_NIRO_HEV_2021: [ - HyundaiCarInfo("Kia Niro Hybrid 2021-22", harness_kit=HarnessKit(Harness.hyundai_f)), # TODO: 2021 could be hyundai_d, verify + HyundaiCarInfo("Kia Niro Hybrid 2021-22", car_parts=CarParts.common([CarHarness.hyundai_f])), # TODO: 2021 could be hyundai_d, verify ], - CAR.KIA_NIRO_HEV_2ND_GEN: HyundaiCarInfo("Kia Niro Hybrid 2023", harness_kit=HarnessKit(Harness.hyundai_a)), - CAR.KIA_OPTIMA_G4: HyundaiCarInfo("Kia Optima 2017", "Advanced Smart Cruise Control", harness_kit=HarnessKit(Harness.hyundai_b)), # TODO: may support 2016, 2018 - CAR.KIA_OPTIMA_G4_FL: HyundaiCarInfo("Kia Optima 2019-20", harness_kit=HarnessKit(Harness.hyundai_g)), + CAR.KIA_NIRO_HEV_2ND_GEN: HyundaiCarInfo("Kia Niro Hybrid 2023", car_parts=CarParts.common([CarHarness.hyundai_a])), + CAR.KIA_OPTIMA_G4: HyundaiCarInfo("Kia Optima 2017", "Advanced Smart Cruise Control", car_parts=CarParts.common([CarHarness.hyundai_b])), # TODO: may support 2016, 2018 + CAR.KIA_OPTIMA_G4_FL: HyundaiCarInfo("Kia Optima 2019-20", car_parts=CarParts.common([CarHarness.hyundai_g])), CAR.KIA_OPTIMA_H: [ HyundaiCarInfo("Kia Optima Hybrid 2017", "Advanced Smart Cruise Control"), # TODO: may support adjacent years HyundaiCarInfo("Kia Optima Hybrid 2019"), ], - CAR.KIA_SELTOS: HyundaiCarInfo("Kia Seltos 2021", harness_kit=HarnessKit(Harness.hyundai_a)), - CAR.KIA_SPORTAGE_5TH_GEN: HyundaiCarInfo("Kia Sportage 2023", harness_kit=HarnessKit(Harness.hyundai_n)), + CAR.KIA_SELTOS: HyundaiCarInfo("Kia Seltos 2021", car_parts=CarParts.common([CarHarness.hyundai_a])), + CAR.KIA_SPORTAGE_5TH_GEN: HyundaiCarInfo("Kia Sportage 2023", car_parts=CarParts.common([CarHarness.hyundai_n])), CAR.KIA_SORENTO: [ - HyundaiCarInfo("Kia Sorento 2018", "Advanced Smart Cruise Control", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness_kit=HarnessKit(Harness.hyundai_c)), - HyundaiCarInfo("Kia Sorento 2019", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness_kit=HarnessKit(Harness.hyundai_e)), + HyundaiCarInfo("Kia Sorento 2018", "Advanced Smart Cruise Control", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", car_parts=CarParts.common([CarHarness.hyundai_c])), + HyundaiCarInfo("Kia Sorento 2019", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", car_parts=CarParts.common([CarHarness.hyundai_e])), ], - CAR.KIA_SORENTO_4TH_GEN: HyundaiCarInfo("Kia Sorento 2021-23", harness_kit=HarnessKit(Harness.hyundai_k)), - CAR.KIA_SORENTO_PHEV_4TH_GEN: HyundaiCarInfo("Kia Sorento Plug-in Hybrid 2022-23", harness_kit=HarnessKit(Harness.hyundai_a)), - CAR.KIA_SPORTAGE_HYBRID_5TH_GEN: HyundaiCarInfo("Kia Sportage Hybrid 2023", harness_kit=HarnessKit(Harness.hyundai_n)), - CAR.KIA_STINGER: HyundaiCarInfo("Kia Stinger 2018-20", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0", harness_kit=HarnessKit(Harness.hyundai_c)), - CAR.KIA_STINGER_2022: HyundaiCarInfo("Kia Stinger 2022", "All", harness_kit=HarnessKit(Harness.hyundai_k)), - CAR.KIA_CEED: HyundaiCarInfo("Kia Ceed 2019", harness_kit=HarnessKit(Harness.hyundai_e)), + CAR.KIA_SORENTO_4TH_GEN: HyundaiCarInfo("Kia Sorento 2021-23", car_parts=CarParts.common([CarHarness.hyundai_k])), + CAR.KIA_SORENTO_PHEV_4TH_GEN: HyundaiCarInfo("Kia Sorento Plug-in Hybrid 2022-23", car_parts=CarParts.common([CarHarness.hyundai_a])), + CAR.KIA_SPORTAGE_HYBRID_5TH_GEN: HyundaiCarInfo("Kia Sportage Hybrid 2023", car_parts=CarParts.common([CarHarness.hyundai_n])), + CAR.KIA_STINGER: HyundaiCarInfo("Kia Stinger 2018-20", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0", car_parts=CarParts.common([CarHarness.hyundai_c])), + CAR.KIA_STINGER_2022: HyundaiCarInfo("Kia Stinger 2022", "All", car_parts=CarParts.common([CarHarness.hyundai_k])), + CAR.KIA_CEED: HyundaiCarInfo("Kia Ceed 2019", car_parts=CarParts.common([CarHarness.hyundai_e])), CAR.KIA_EV6: [ - HyundaiCarInfo("Kia EV6 (Southeast Asia only) 2022-23", "All", harness_kit=HarnessKit(Harness.hyundai_p)), - HyundaiCarInfo("Kia EV6 (without HDA II) 2022-23", "Highway Driving Assist", harness_kit=HarnessKit(Harness.hyundai_l)), - HyundaiCarInfo("Kia EV6 (with HDA II) 2022-23", "Highway Driving Assist II", harness_kit=HarnessKit(Harness.hyundai_p)) + HyundaiCarInfo("Kia EV6 (Southeast Asia only) 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_p])), + HyundaiCarInfo("Kia EV6 (without HDA II) 2022-23", "Highway Driving Assist", car_parts=CarParts.common([CarHarness.hyundai_l])), + HyundaiCarInfo("Kia EV6 (with HDA II) 2022-23", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_p])) ], # Genesis CAR.GENESIS_GV60_EV_1ST_GEN: [ - HyundaiCarInfo("Genesis GV60 (Advanced Trim) 2023", "All", harness_kit=HarnessKit(Harness.hyundai_a)), - HyundaiCarInfo("Genesis GV60 (Performance Trim) 2023", "All", harness_kit=HarnessKit(Harness.hyundai_k)), + HyundaiCarInfo("Genesis GV60 (Advanced Trim) 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_a])), + HyundaiCarInfo("Genesis GV60 (Performance Trim) 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_k])), ], - CAR.GENESIS_G70: HyundaiCarInfo("Genesis G70 2018-19", "All", harness_kit=HarnessKit(Harness.hyundai_f)), - CAR.GENESIS_G70_2020: HyundaiCarInfo("Genesis G70 2020", "All", harness_kit=HarnessKit(Harness.hyundai_f)), + CAR.GENESIS_G70: HyundaiCarInfo("Genesis G70 2018-19", "All", car_parts=CarParts.common([CarHarness.hyundai_f])), + CAR.GENESIS_G70_2020: HyundaiCarInfo("Genesis G70 2020", "All", car_parts=CarParts.common([CarHarness.hyundai_f])), CAR.GENESIS_GV70_1ST_GEN: [ - HyundaiCarInfo("Genesis GV70 (2.5T Trim) 2022-23", "All", harness_kit=HarnessKit(Harness.hyundai_l)), - HyundaiCarInfo("Genesis GV70 (3.5T Trim) 2022-23", "All", harness_kit=HarnessKit(Harness.hyundai_m)), + HyundaiCarInfo("Genesis GV70 (2.5T Trim) 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l])), + HyundaiCarInfo("Genesis GV70 (3.5T Trim) 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_m])), ], - CAR.GENESIS_G80: HyundaiCarInfo("Genesis G80 2018-19", "All", harness_kit=HarnessKit(Harness.hyundai_h)), - CAR.GENESIS_G90: HyundaiCarInfo("Genesis G90 2017-18", "All", harness_kit=HarnessKit(Harness.hyundai_c)), - CAR.GENESIS_GV80: HyundaiCarInfo("Genesis GV80 2023", "All", harness_kit=HarnessKit(Harness.hyundai_m)), + CAR.GENESIS_G80: HyundaiCarInfo("Genesis G80 2018-19", "All", car_parts=CarParts.common([CarHarness.hyundai_h])), + CAR.GENESIS_G90: HyundaiCarInfo("Genesis G90 2017-18", "All", car_parts=CarParts.common([CarHarness.hyundai_c])), + CAR.GENESIS_GV80: HyundaiCarInfo("Genesis GV80 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_m])), # Non-SCC Cars CAR.ELANTRA_2022_NON_SCC: HyundaiCarInfo("Hyundai Elantra Non-SCC 2022"), @@ -355,6 +356,76 @@ FINGERPRINTS = { }], } + +def get_platform_codes(fw_versions: List[bytes]) -> Set[Tuple[bytes, Optional[bytes]]]: + # Returns unique, platform-specific identification codes for a set of versions + codes = set() # (code-Optional[part], date) + for fw in fw_versions: + code_match = PLATFORM_CODE_FW_PATTERN.search(fw) + part_match = PART_NUMBER_FW_PATTERN.search(fw) + date_match = DATE_FW_PATTERN.search(fw) + if code_match is not None: + code: bytes = code_match.group() + part = part_match.group() if part_match else None + date = date_match.group() if date_match else None + if part is not None: + # part number starts with generic ECU part type, add what is specific to platform + code += b"-" + part[-5:] + + codes.add((code, date)) + return codes + + +def match_fw_to_car_fuzzy(live_fw_versions) -> Set[str]: + # Non-electric CAN FD platforms often do not have platform code specifiers needed + # to distinguish between hybrid and ICE. All EVs so far are either exclusively + # electric or specify electric in the platform code. + fuzzy_platform_blacklist = set(CANFD_CAR - EV_CAR) + candidates = set() + + for candidate, fws in FW_VERSIONS.items(): + # Keep track of ECUs which pass all checks (platform codes, within date range) + valid_found_ecus = set() + valid_expected_ecus = {ecu[1:] for ecu in fws if ecu[0] in PLATFORM_CODE_ECUS} + for ecu, expected_versions in fws.items(): + addr = ecu[1:] + # Only check ECUs expected to have platform codes + if ecu[0] not in PLATFORM_CODE_ECUS: + continue + + # Expected platform codes & dates + codes = get_platform_codes(expected_versions) + expected_platform_codes = {code for code, _ in codes} + expected_dates = {date for _, date in codes if date is not None} + + # Found platform codes & dates + codes = get_platform_codes(live_fw_versions.get(addr, set())) + found_platform_codes = {code for code, _ in codes} + found_dates = {date for _, date in codes if date is not None} + + # Check platform code + part number matches for any found versions + if not any(found_platform_code in expected_platform_codes for found_platform_code in found_platform_codes): + break + + if ecu[0] in DATE_FW_ECUS: + # If ECU can have a FW date, require it to exist + # (this excludes candidates in the database without dates) + if not len(expected_dates) or not len(found_dates): + break + + # Check any date within range in the database, format is %y%m%d + if not any(min(expected_dates) <= found_date <= max(expected_dates) for found_date in found_dates): + break + + valid_found_ecus.add(addr) + + # If all live ECUs pass all checks for candidate, add it as a match + if valid_expected_ecus.issubset(valid_found_ecus): + candidates.add(candidate) + + return candidates - fuzzy_platform_blacklist + + HYUNDAI_VERSION_REQUEST_LONG = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ p16(0xf100) # Long description @@ -368,6 +439,18 @@ HYUNDAI_VERSION_REQUEST_MULTI = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER] HYUNDAI_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) +# Regex patterns for parsing platform code, FW date, and part number from FW versions +PLATFORM_CODE_FW_PATTERN = re.compile(b'((?<=' + HYUNDAI_VERSION_REQUEST_LONG[1:] + + b')[A-Z]{2}[A-Za-z0-9]{0,2})') +DATE_FW_PATTERN = re.compile(b'(?<=[ -])([0-9]{6}$)') +PART_NUMBER_FW_PATTERN = re.compile(b'(?<=[0-9][.,][0-9]{2} )([0-9]{5}[-/]?[A-Z][A-Z0-9]{3}[0-9])') + +# List of ECUs expected to have platform codes, camera and radar should exist on all cars +# TODO: use abs, it has the platform code and part number on many platforms +PLATFORM_CODE_ECUS = [Ecu.fwdRadar, Ecu.fwdCamera, Ecu.eps] +# So far we've only seen dates in fwdCamera +DATE_FW_ECUS = [Ecu.fwdCamera] + FW_QUERY_CONFIG = FwQueryConfig( requests=[ # TODO: minimize shared whitelists for CAN and cornerRadar for CAN-FD @@ -424,6 +507,8 @@ FW_QUERY_CONFIG = FwQueryConfig( (Ecu.hvac, 0x7b3, None), # HVAC Control Assembly (Ecu.cornerRadar, 0x7b7, None), ], + # Custom fuzzy fingerprinting function using platform codes, part numbers + FW dates: + match_fw_to_car_fuzzy=match_fw_to_car_fuzzy, ) FW_VERSIONS = { @@ -555,14 +640,11 @@ FW_VERSIONS = { }, CAR.SONATA: { (Ecu.fwdRadar, 0x7d0, None): [ - b'\xf1\x00DN8 1.00 99110-L0000 \xaa\xaa\xaa\xaa\xaa\xaa\xaa ', - b'\xf1\x00DN8 1.00 99110-L0000 \xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x00DN8_ SCC F-CU- 1.00 1.00 99110-L0000 ', b'\xf1\x00DN8_ SCC F-CUP 1.00 1.00 99110-L0000 ', b'\xf1\x00DN8_ SCC F-CUP 1.00 1.02 99110-L1000 ', b'\xf1\x00DN8_ SCC FHCUP 1.00 1.00 99110-L0000 ', b'\xf1\x00DN8_ SCC FHCUP 1.00 1.01 99110-L1000 ', - b'\xf1\x00DN89110-L0000 \xaa\xaa\xaa\xaa\xaa\xaa\xaa ', ], (Ecu.abs, 0x7d1, None): [ b'\xf1\x00DN ESC \x07 106 \x07\x01 58910-L0100', @@ -603,11 +685,9 @@ FW_VERSIONS = { b'\xf1\x00DN8 MDPS C 1,00 1,01 56310L0010\x00 4DNAC101', # modified firmware b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0210\x00 4DNAC102', b'\xf1\x8756310L0010\x00\xf1\x00DN8 MDPS C 1,00 1,01 56310L0010\x00 4DNAC101', # modified firmware - b'\xf1\x00DN8 MDPS C 1.00 1.01 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 4DNAC101', b'\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0010 4DNAC101', b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0010\x00 4DNAC101', b'\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP100', - b'\xf1\x87\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf1\x00DN8 MDPS C 1.00 1.01 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 4DNAC101', b'\xf1\x8756310-L0010\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0010 4DNAC101', b'\xf1\x8756310-L0210\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0210 4DNAC101', b'\xf1\x8756310-L1010\xf1\x00DN8 MDPS C 1.00 1.03 56310-L1010 4DNDC103', @@ -768,7 +848,7 @@ FW_VERSIONS = { b'\xf1\x00TM MDPS C 1.00 1.00 56340-S2000 8409', b'\xf1\x00TM MDPS C 1.00 1.00 56340-S2000 8A12', b'\xf1\x00TM MDPS C 1.00 1.01 56340-S2000 9129', - b'\xf1\x00TM MDPS R 1.00 1.02 57700-S1100 4TMDP102' + b'\xf1\x00TM MDPS R 1.00 1.02 57700-S1100 4TMDP102', ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00TM MFC AT EUR LHD 1.00 1.01 99211-S1010 181207', @@ -863,19 +943,25 @@ FW_VERSIONS = { ], (Ecu.eps, 0x7d4, None): [ b'\xf1\x00TM MDPS C 1.00 1.02 56310-CLAC0 4TSHC102', + b'\xf1\x00TM MDPS C 1.00 1.02 56310-CLEC0 4TSHC102', b'\xf1\x00TM MDPS R 1.00 1.05 57700-CL000 4TSHP105', + b'\xf1\x00TM MDPS C 1.00 1.02 56310-GA000 4TSHA100', ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00TMH MFC AT EUR LHD 1.00 1.06 99211-S1500 220727', b'\xf1\x00TMH MFC AT USA LHD 1.00 1.03 99211-S1500 210224', + b'\xf1\x00TMH MFC AT USA LHD 1.00 1.06 99211-S1500 220727', + b'\xf1\x00TMA MFC AT USA LHD 1.00 1.03 99211-S2500 220414', ], (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x00PSBG2333 E16\x00\x00\x00\x00\x00\x00\x00TTM2H16SA3\xa3\x1b\xe14', b'\xf1\x00PSBG2333 E16\x00\x00\x00\x00\x00\x00\x00TTM2H16UA3I\x94\xac\x8f', b'\xf1\x87959102T250\x00\x00\x00\x00\x00\xf1\x81E14\x00\x00\x00\x00\x00\x00\x00\xf1\x00PSBG2333 E14\x00\x00\x00\x00\x00\x00\x00TTM2H16SA2\x80\xd7l\xb2', ], (Ecu.engine, 0x7e0, None): [ b'\xf1\x87391312MTC1', b'\xf1\x87391312MTE0', + b'\xf1\x87391312MTL0', ], }, CAR.SANTA_FE_PHEV_2022: { @@ -1114,12 +1200,14 @@ FW_VERSIONS = { b'\xf1\x00IK MDPS R 1.00 1.07 57700-G9220 4I2VL107', b'\xf1\x00IK MDPS R 1.00 1.07 57700-G9420 4I4VL107', b'\xf1\x00IK MDPS R 1.00 1.08 57700-G9420 4I4VL108', + b'\xf1\x00IK MDPS R 1.00 1.08 57700-G9200 4I2CL108', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x87VCJLP18407832DN3\x88vXfvUVT\x97eFU\x87d7v\x88eVeveFU\x89\x98\x7f\xff\xb2\xb0\xf1\x81E25\x00\x00\x00', b'\x00\x00\x00\x00\xf1\x00bcsh8p54 E25\x00\x00\x00\x00\x00\x00\x00SIK0T33NB4\xecE\xefL', b'\xf1\x87VDKLT18912362DN4wfVfwefeveVUwfvw\x88vWfvUFU\x89\xa9\x8f\xff\x87w\xf1\x81E25\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E25\x00\x00\x00\x00\x00\x00\x00SIK0T33NB4\xecE\xefL', b'\xf1\x87VDJLC18480772DK9\x88eHfwfff\x87eFUeDEU\x98eFe\x86T5DVyo\xff\x87s\xf1\x81E25\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E25\x00\x00\x00\x00\x00\x00\x00SIK0T33KB5\x9f\xa5&\x81', + b'\xf1\x00bcsh8p54 E25\x00\x00\x00\x00\x00\x00\x00SIK0T20KB3Wuvz', ], (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00IK__ SCC F-CUP 1.00 1.02 96400-G9100 ', @@ -1133,6 +1221,7 @@ FW_VERSIONS = { (Ecu.engine, 0x7e0, None): [ b'\xf1\x81640J0051\x00\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x81640H0051\x00\x00\x00\x00\x00\x00\x00\x00', + b'\xf1\x81606G2051\x00\x00\x00\x00\x00\x00\x00\x00', ], }, CAR.GENESIS_G70_2021_NON_SCC: { @@ -1169,7 +1258,10 @@ FW_VERSIONS = { ], }, CAR.GENESIS_G90: { - (Ecu.transmission, 0x7e1, None): [b'\xf1\x87VDGMD15866192DD3x\x88x\x89wuFvvfUf\x88vWwgwwwvfVgx\x87o\xff\xbc^\xf1\x81E14\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcshcm49 E14\x00\x00\x00\x00\x00\x00\x00SHI0G50NB1tc5\xb7'], + (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x87VDGMD15352242DD3w\x87gxwvgv\x87wvw\x88wXwffVfffUfw\x88o\xff\x06J\xf1\x81E14\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcshcm49 E14\x00\x00\x00\x00\x00\x00\x00SHI0G50NB1tc5\xb7', + b'\xf1\x87VDGMD15866192DD3x\x88x\x89wuFvvfUf\x88vWwgwwwvfVgx\x87o\xff\xbc^\xf1\x81E14\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcshcm49 E14\x00\x00\x00\x00\x00\x00\x00SHI0G50NB1tc5\xb7', + ], (Ecu.fwdRadar, 0x7d0, None): [b'\xf1\x00HI__ SCC F-CUP 1.00 1.01 96400-D2100 '], (Ecu.fwdCamera, 0x7c4, None): [b'\xf1\x00HI LKAS AT USA LHD 1.00 1.00 95895-D2020 160302'], (Ecu.engine, 0x7e0, None): [b'\xf1\x810000000000\x00'], @@ -1545,10 +1637,8 @@ FW_VERSIONS = { b'\xf1\x8799110AA000\xf1\x00CN7_ SCC F-CUP 1.00 1.01 99110-AA000 ', ], (Ecu.eps, 0x7d4, None): [ - b'\xf1\x87\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf1\x00CN7 MDPS C 1.00 1.06 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 4CNDC106', - b'\xf1\x8756310/AA070\xf1\x00CN7 MDPS C 1.00 1.06 56310/AA070 4CNDC106', - b'\xf1\x8756310AA050\x00\xf1\x00CN7 MDPS C 1.00 1.06 56310AA050\x00 4CNDC106\xf1\xa01.06', b'\xf1\x00CN7 MDPS C 1.00 1.06 56310AA050\x00 4CNDC106', + b'\xf1\x00CN7 MDPS C 1.00 1.06 56310/AA070 4CNDC106', ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.00 99210-AB000 200819', diff --git a/selfdrive/car/isotp_parallel_query.py b/selfdrive/car/isotp_parallel_query.py index 965d2e1836..8ab9728e5f 100644 --- a/selfdrive/car/isotp_parallel_query.py +++ b/selfdrive/car/isotp_parallel_query.py @@ -90,12 +90,14 @@ class IsoTpParallelQuery: for addr in self.functional_addrs: self._create_isotp_msg(addr, None, -1).send(self.request[0]) - # If querying functional addrs, set up physical IsoTpMessages to send consecutive frames + # Send first frame (single or first) to all addresses and receive asynchronously in the loop below. + # If querying functional addrs, only set up physical IsoTpMessages to send consecutive frames for msg in msgs.values(): msg.send(self.request[0], setup_only=len(self.functional_addrs) > 0) results = {} start_time = time.monotonic() + addrs_responded = set() # track addresses that have ever sent a valid iso-tp frame for timeout logging response_timeouts = {tx_addr: start_time + timeout for tx_addr in self.msg_addrs} while True: self.rx() @@ -110,6 +112,7 @@ class IsoTpParallelQuery: # Extend timeout for each consecutive ISO-TP frame to avoid timing out on long responses if rx_in_progress: + addrs_responded.add(tx_addr) response_timeouts[tx_addr] = time.monotonic() + timeout if not dat: @@ -117,7 +120,7 @@ class IsoTpParallelQuery: counter = request_counter[tx_addr] expected_response = self.response[counter] - response_valid = dat[:len(expected_response)] == expected_response + response_valid = dat.startswith(expected_response) if response_valid: if counter + 1 < len(self.request): @@ -140,8 +143,14 @@ class IsoTpParallelQuery: cur_time = time.monotonic() for tx_addr in response_timeouts: if cur_time - response_timeouts[tx_addr] > 0: - if request_counter[tx_addr] > 0 and not request_done[tx_addr]: - cloudlog.error(f"iso-tp query timeout after receiving response: {tx_addr}") + if not request_done[tx_addr]: + if request_counter[tx_addr] > 0: + cloudlog.error(f"iso-tp query timeout after receiving partial response: {tx_addr}") + elif tx_addr in addrs_responded: + cloudlog.error(f"iso-tp query timeout while receiving response: {tx_addr}") + # TODO: handle functional addresses + # else: + # cloudlog.error(f"iso-tp query timeout with no response: {tx_addr}") request_done[tx_addr] = True # Break if all requests are done (finished or timed out) diff --git a/selfdrive/car/mazda/carstate.py b/selfdrive/car/mazda/carstate.py index 10c487530a..05a45947d4 100644 --- a/selfdrive/car/mazda/carstate.py +++ b/selfdrive/car/mazda/carstate.py @@ -51,8 +51,8 @@ class CarState(CarStateBase): ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None)) ret.genericToggle = bool(cp.vl["BLINK_INFO"]["HIGH_BEAMS"]) - ret.leftBlindspot = cp.vl["BSM"]["LEFT_BS1"] == 1 - ret.rightBlindspot = cp.vl["BSM"]["RIGHT_BS1"] == 1 + ret.leftBlindspot = cp.vl["BSM"]["LEFT_BS_STATUS"] != 0 + ret.rightBlindspot = cp.vl["BSM"]["RIGHT_BS_STATUS"] != 0 ret.leftBlinker, ret.rightBlinker = ret.leftBlinkerOn, ret.rightBlinkerOn = self.update_blinker_from_lamp(40, cp.vl["BLINK_INFO"]["LEFT_BLINK"] == 1, cp.vl["BLINK_INFO"]["RIGHT_BLINK"] == 1) @@ -169,8 +169,8 @@ class CarState(CarStateBase): ("PEDAL_GAS", "ENGINE_DATA"), ("SPEED", "ENGINE_DATA"), ("CTR", "CRZ_BTNS"), - ("LEFT_BS1", "BSM"), - ("RIGHT_BS1", "BSM"), + ("LEFT_BS_STATUS", "BSM"), + ("RIGHT_BS_STATUS", "BSM"), ("SET_P", "CRZ_BTNS"), ("SET_M", "CRZ_BTNS"), diff --git a/selfdrive/car/mazda/mazdacan.py b/selfdrive/car/mazda/mazdacan.py index 040b9ada04..3dce07ae6b 100644 --- a/selfdrive/car/mazda/mazdacan.py +++ b/selfdrive/car/mazda/mazdacan.py @@ -1,5 +1,3 @@ -import copy - from selfdrive.car.mazda.values import GEN1, Buttons @@ -46,6 +44,7 @@ def create_steering_control(packer, car_fingerprint, frame, apply_steer, lkas): csum = csum % 256 + values = {} if car_fingerprint in GEN1: values = { "LKAS_REQUEST": apply_steer, @@ -64,7 +63,17 @@ def create_steering_control(packer, car_fingerprint, frame, apply_steer, lkas): def create_alert_command(packer, cam_msg: dict, ldw: bool, steer_required: bool): - values = copy.copy(cam_msg) + values = {s: cam_msg[s] for s in [ + "LINE_VISIBLE", + "LINE_NOT_VISIBLE", + "LANE_LINES", + "BIT1", + "BIT2", + "BIT3", + "NO_ERR_BIT", + "S1", + "S1_HBEAM", + ]} values.update({ # TODO: what's the difference between all these? do we need to send all? "HANDS_WARN_3_BITS": 0b111 if steer_required else 0, diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index a17ada570a..7d7ce6269e 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -3,7 +3,7 @@ from typing import Dict, List, Union from cereal import car from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarInfo, Harness, HarnessKit +from selfdrive.car.docs_definitions import CarHarness, CarInfo, CarParts from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries Ecu = car.CarParams.Ecu @@ -45,7 +45,7 @@ BUTTON_STATES = { @dataclass class MazdaCarInfo(CarInfo): package: str = "All" - harness_kit: HarnessKit = HarnessKit(Harness.mazda) + car_parts: CarParts = CarParts.common([CarHarness.mazda]) CAR_INFO: Dict[str, Union[MazdaCarInfo, List[MazdaCarInfo]]] = { diff --git a/selfdrive/car/nissan/carcontroller.py b/selfdrive/car/nissan/carcontroller.py index 5c12ba7368..bba013e9e2 100644 --- a/selfdrive/car/nissan/carcontroller.py +++ b/selfdrive/car/nissan/carcontroller.py @@ -81,6 +81,7 @@ class CarController: can_sends.append(nissancan.create_steering_control( self.packer, apply_angle, self.frame, CC.latActive, self.lkas_max_torque)) + # Below are the HUD messages. We copy the stock message and modify if self.CP.carFingerprint != CAR.ALTIMA: if self.frame % 2 == 0: can_sends.append(nissancan.create_lkas_hud_msg( diff --git a/selfdrive/car/nissan/carstate.py b/selfdrive/car/nissan/carstate.py index ca06a74e01..72673e9fc3 100644 --- a/selfdrive/car/nissan/carstate.py +++ b/selfdrive/car/nissan/carstate.py @@ -173,6 +173,7 @@ class CarState(CarStateBase): ("USER_BRAKE_PRESSED", "CRUISE_THROTTLE"), ("NEW_SIGNAL_2", "CRUISE_THROTTLE"), ("GAS_PRESSED_INVERTED", "CRUISE_THROTTLE"), + ("COUNTER", "CRUISE_THROTTLE"), ("unsure1", "CRUISE_THROTTLE"), ("unsure2", "CRUISE_THROTTLE"), ("unsure3", "CRUISE_THROTTLE"), diff --git a/selfdrive/car/nissan/nissancan.py b/selfdrive/car/nissan/nissancan.py index db8f3cca31..c63687357d 100644 --- a/selfdrive/car/nissan/nissancan.py +++ b/selfdrive/car/nissan/nissancan.py @@ -1,4 +1,3 @@ -import copy import crcmod from selfdrive.car.nissan.values import CAR @@ -23,7 +22,23 @@ def create_steering_control(packer, apply_steer, frame, steer_on, lkas_max_torqu def create_acc_cancel_cmd(packer, car_fingerprint, cruise_throttle_msg): - values = copy.copy(cruise_throttle_msg) + values = {s: cruise_throttle_msg[s] for s in [ + "COUNTER", + "PROPILOT_BUTTON", + "CANCEL_BUTTON", + "GAS_PEDAL_INVERTED", + "SET_BUTTON", + "RES_BUTTON", + "FOLLOW_DISTANCE_BUTTON", + "NO_BUTTON_PRESSED", + "GAS_PEDAL", + "USER_BRAKE_PRESSED", + "NEW_SIGNAL_2", + "GAS_PRESSED_INVERTED", + "unsure1", + "unsure2", + "unsure3", + ]} can_bus = 1 if car_fingerprint == CAR.ALTIMA else 2 values["CANCEL_BUTTON"] = 1 @@ -37,7 +52,12 @@ def create_acc_cancel_cmd(packer, car_fingerprint, cruise_throttle_msg): def create_cancel_msg(packer, cancel_msg, cruise_cancel): - values = copy.copy(cancel_msg) + values = {s: cancel_msg[s] for s in [ + "CANCEL_SEATBELT", + "NEW_SIGNAL_1", + "NEW_SIGNAL_2", + "NEW_SIGNAL_3", + ]} if cruise_cancel: values["CANCEL_SEATBELT"] = 1 @@ -46,7 +66,34 @@ def create_cancel_msg(packer, cancel_msg, cruise_cancel): def create_lkas_hud_msg(packer, lkas_hud_msg, lat_active, blinking_icon, lateral_paused, left_line, right_line, left_lane_depart, right_lane_depart): - values = lkas_hud_msg + values = {s: lkas_hud_msg[s] for s in [ + "LARGE_WARNING_FLASHING", + "SIDE_RADAR_ERROR_FLASHING1", + "SIDE_RADAR_ERROR_FLASHING2", + "LEAD_CAR", + "LEAD_CAR_ERROR", + "FRONT_RADAR_ERROR", + "FRONT_RADAR_ERROR_FLASHING", + "SIDE_RADAR_ERROR_FLASHING3", + "LKAS_ERROR_FLASHING", + "SAFETY_SHIELD_ACTIVE", + "RIGHT_LANE_GREEN_FLASH", + "LEFT_LANE_GREEN_FLASH", + "FOLLOW_DISTANCE", + "AUDIBLE_TONE", + "SPEED_SET_ICON", + "SMALL_STEERING_WHEEL_ICON", + "unknown59", + "unknown55", + "unknown26", + "unknown28", + "unknown31", + "SET_SPEED", + "unknown43", + "unknown08", + "unknown05", + "unknown02", + ]} values["RIGHT_LANE_YELLOW_FLASH"] = 1 if right_lane_depart else 0 values["LEFT_LANE_YELLOW_FLASH"] = 1 if left_lane_depart else 0 @@ -59,7 +106,47 @@ def create_lkas_hud_msg(packer, lkas_hud_msg, lat_active, blinking_icon, lateral def create_lkas_hud_info_msg(packer, lkas_hud_info_msg, steer_hud_alert): - values = lkas_hud_info_msg + values = {s: lkas_hud_info_msg[s] for s in [ + "NA_HIGH_ACCEL_TEMP", + "SIDE_RADAR_NA_HIGH_CABIN_TEMP", + "SIDE_RADAR_MALFUNCTION", + "LKAS_MALFUNCTION", + "FRONT_RADAR_MALFUNCTION", + "SIDE_RADAR_NA_CLEAN_REAR_CAMERA", + "NA_POOR_ROAD_CONDITIONS", + "CURRENTLY_UNAVAILABLE", + "SAFETY_SHIELD_OFF", + "FRONT_COLLISION_NA_FRONT_RADAR_OBSTRUCTION", + "PEDAL_MISSAPPLICATION_SYSTEM_ACTIVATED", + "SIDE_IMPACT_NA_RADAR_OBSTRUCTION", + "WARNING_DO_NOT_ENTER", + "SIDE_IMPACT_SYSTEM_OFF", + "SIDE_IMPACT_MALFUNCTION", + "FRONT_COLLISION_MALFUNCTION", + "SIDE_RADAR_MALFUNCTION2", + "LKAS_MALFUNCTION2", + "FRONT_RADAR_MALFUNCTION2", + "PROPILOT_NA_MSGS", + "BOTTOM_MSG", + "HANDS_ON_WHEEL_WARNING", + "WARNING_STEP_ON_BRAKE_NOW", + "PROPILOT_NA_FRONT_CAMERA_OBSTRUCTED", + "PROPILOT_NA_HIGH_CABIN_TEMP", + "WARNING_PROPILOT_MALFUNCTION", + "ACC_UNAVAILABLE_HIGH_CABIN_TEMP", + "ACC_NA_FRONT_CAMERA_IMPARED", + "unknown07", + "unknown10", + "unknown15", + "unknown23", + "unknown19", + "unknown31", + "unknown32", + "unknown46", + "unknown61", + "unknown55", + "unknown50", + ]} if steer_hud_alert: values["HANDS_ON_WHEEL_WARNING"] = 1 diff --git a/selfdrive/car/nissan/values.py b/selfdrive/car/nissan/values.py index cbe62645d5..568c33630b 100644 --- a/selfdrive/car/nissan/values.py +++ b/selfdrive/car/nissan/values.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional, Union from cereal import car from panda.python import uds from selfdrive.car import AngleRateLimit, dbc_dict -from selfdrive.car.docs_definitions import CarInfo, Harness, HarnessKit, HarnessPart +from selfdrive.car.docs_definitions import CarInfo, CarHarness, CarParts from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries Ecu = car.CarParams.Ecu @@ -30,13 +30,10 @@ class CAR: ALTIMA = "NISSAN ALTIMA 2020" -NISSAN_HARNESS_PARTS = [HarnessPart.harness_box, HarnessPart.rj45_cable, HarnessPart.long_obdc_cable, HarnessPart.usbc_coupler] - - @dataclass class NissanCarInfo(CarInfo): package: str = "ProPILOT Assist" - harness_kit: HarnessKit = HarnessKit(Harness.nissan_a, parts=NISSAN_HARNESS_PARTS) + car_parts: CarParts = CarParts.common([CarHarness.nissan_a]) CAR_INFO: Dict[str, Optional[Union[NissanCarInfo, List[NissanCarInfo]]]] = { @@ -44,7 +41,7 @@ CAR_INFO: Dict[str, Optional[Union[NissanCarInfo, List[NissanCarInfo]]]] = { CAR.LEAF: NissanCarInfo("Nissan Leaf 2018-23", video_link="https://youtu.be/vaMbtAh_0cY"), CAR.LEAF_IC: None, # same platforms CAR.ROGUE: NissanCarInfo("Nissan Rogue 2018-20"), - CAR.ALTIMA: NissanCarInfo("Nissan Altima 2019-20", harness_kit=HarnessKit(Harness.nissan_b, parts=NISSAN_HARNESS_PARTS)), + CAR.ALTIMA: NissanCarInfo("Nissan Altima 2019-20", car_parts=CarParts.common([CarHarness.nissan_b])), } FINGERPRINTS = { @@ -171,9 +168,9 @@ FW_VERSIONS = { } DBC = { - CAR.XTRAIL: dbc_dict('nissan_x_trail_2017', None), - CAR.LEAF: dbc_dict('nissan_leaf_2018', None), - CAR.LEAF_IC: dbc_dict('nissan_leaf_2018', None), - CAR.ROGUE: dbc_dict('nissan_x_trail_2017', None), - CAR.ALTIMA: dbc_dict('nissan_x_trail_2017', None), + CAR.XTRAIL: dbc_dict('nissan_x_trail_2017_generated', None), + CAR.LEAF: dbc_dict('nissan_leaf_2018_generated', None), + CAR.LEAF_IC: dbc_dict('nissan_leaf_2018_generated', None), + CAR.ROGUE: dbc_dict('nissan_x_trail_2017_generated', None), + CAR.ALTIMA: dbc_dict('nissan_x_trail_2017_generated', None), } diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py index 460bb24c80..1fbaeea74f 100644 --- a/selfdrive/car/subaru/carcontroller.py +++ b/selfdrive/car/subaru/carcontroller.py @@ -10,10 +10,6 @@ class CarController: self.apply_steer_last = 0 self.frame = 0 - self.es_lkas_state_cnt = -1 - self.es_distance_cnt = -1 - self.es_dashstatus_cnt = -1 - self.infotainmentstatus_cnt = -1 self.cruise_button_prev = 0 self.last_cancel_frame = 0 @@ -29,7 +25,6 @@ class CarController: # *** steering *** if (self.frame % self.p.STEER_STEP) == 0: - apply_steer = int(round(actuators.steer * self.p.STEER_MAX)) # limits due to driver torque @@ -41,9 +36,9 @@ class CarController: apply_steer = 0 if self.CP.carFingerprint in PREGLOBAL_CARS: - can_sends.append(subarucan.create_preglobal_steering_control(self.packer, apply_steer, self.frame, self.p.STEER_STEP)) + can_sends.append(subarucan.create_preglobal_steering_control(self.packer, apply_steer, CC.latActive, self.frame, self.p.STEER_STEP)) else: - can_sends.append(subarucan.create_steering_control(self.packer, apply_steer)) + can_sends.append(subarucan.create_steering_control(self.packer, apply_steer, CC.latActive)) self.apply_steer_last = apply_steer @@ -51,7 +46,7 @@ class CarController: # *** alerts and pcm cancel *** if self.CP.carFingerprint in PREGLOBAL_CARS: - if self.es_distance_cnt != CS.es_distance_msg["COUNTER"]: + if self.frame % 5 == 0: # 1 = main, 2 = set shallow, 3 = set deep, 4 = resume shallow, 5 = resume deep # disengage ACC when OP is disengaged if pcm_cancel_cmd: @@ -68,7 +63,6 @@ class CarController: self.cruise_button_prev = cruise_button can_sends.append(subarucan.create_preglobal_es_distance(self.packer, cruise_button, CS.es_distance_msg)) - self.es_distance_cnt = CS.es_distance_msg["COUNTER"] else: if pcm_cancel_cmd and (self.frame - self.last_cancel_frame) > 0.2: @@ -76,19 +70,15 @@ class CarController: can_sends.append(subarucan.create_es_distance(self.packer, CS.es_distance_msg, bus, pcm_cancel_cmd)) self.last_cancel_frame = self.frame - if self.es_dashstatus_cnt != CS.es_dashstatus_msg["COUNTER"]: + if self.frame % 10 == 0: can_sends.append(subarucan.create_es_dashstatus(self.packer, CS.es_dashstatus_msg)) - self.es_dashstatus_cnt = CS.es_dashstatus_msg["COUNTER"] - if self.es_lkas_state_cnt != CS.es_lkas_state_msg["COUNTER"]: can_sends.append(subarucan.create_es_lkas_state(self.packer, CS.es_lkas_state_msg, CC.latActive, CS.madsEnabled, hud_control.visualAlert, hud_control.leftLaneVisible, hud_control.rightLaneVisible, hud_control.leftLaneDepart, hud_control.rightLaneDepart)) - self.es_lkas_state_cnt = CS.es_lkas_state_msg["COUNTER"] - if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT and self.infotainmentstatus_cnt != CS.es_infotainmentstatus_msg["COUNTER"]: - can_sends.append(subarucan.create_infotainmentstatus(self.packer, CS.es_infotainmentstatus_msg, hud_control.visualAlert)) - self.infotainmentstatus_cnt = CS.es_infotainmentstatus_msg["COUNTER"] + if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT: + can_sends.append(subarucan.create_infotainmentstatus(self.packer, CS.es_infotainmentstatus_msg, hud_control.visualAlert)) new_actuators = actuators.copy() new_actuators.steer = self.apply_steer_last / self.p.STEER_MAX diff --git a/selfdrive/car/subaru/carstate.py b/selfdrive/car/subaru/carstate.py index 2b788af151..ad5aaba430 100644 --- a/selfdrive/car/subaru/carstate.py +++ b/selfdrive/car/subaru/carstate.py @@ -125,6 +125,7 @@ class CarState(CarStateBase): def get_global_es_distance_signals(): signals = [ ("COUNTER", "ES_Distance"), + ("CHECKSUM", "ES_Distance"), ("Signal1", "ES_Distance"), ("Cruise_Fault", "ES_Distance"), ("Cruise_Throttle", "ES_Distance"), @@ -268,6 +269,7 @@ class CarState(CarStateBase): else: signals = [ ("COUNTER", "ES_DashStatus"), + ("CHECKSUM", "ES_DashStatus"), ("PCB_Off", "ES_DashStatus"), ("LDW_Off", "ES_DashStatus"), ("Signal1", "ES_DashStatus"), @@ -295,6 +297,7 @@ class CarState(CarStateBase): ("Cruise_State", "ES_DashStatus"), ("COUNTER", "ES_LKAS_State"), + ("CHECKSUM", "ES_LKAS_State"), ("LKAS_Alert_Msg", "ES_LKAS_State"), ("Signal1", "ES_LKAS_State"), ("LKAS_ACTIVE", "ES_LKAS_State"), @@ -322,6 +325,8 @@ class CarState(CarStateBase): if CP.flags & SubaruFlags.SEND_INFOTAINMENT: signals += [ + ("COUNTER", "INFOTAINMENT_STATUS"), + ("CHECKSUM", "INFOTAINMENT_STATUS"), ("LKAS_State_Infotainment", "INFOTAINMENT_STATUS"), ("LKAS_Blue_Lines", "INFOTAINMENT_STATUS"), ("Signal1", "INFOTAINMENT_STATUS"), diff --git a/selfdrive/car/subaru/subarucan.py b/selfdrive/car/subaru/subarucan.py index c637fe1ddd..1f7fbc2e55 100644 --- a/selfdrive/car/subaru/subarucan.py +++ b/selfdrive/car/subaru/subarucan.py @@ -1,13 +1,12 @@ -import copy from cereal import car VisualAlert = car.CarControl.HUDControl.VisualAlert -def create_steering_control(packer, apply_steer): +def create_steering_control(packer, apply_steer, steer_req): values = { "LKAS_Output": apply_steer, - "LKAS_Request": 1 if apply_steer != 0 else 0, + "LKAS_Request": steer_req, "SET_1": 1 } return packer.make_can_msg("ES_LKAS", 0, values) @@ -18,7 +17,28 @@ def create_steering_status(packer): def create_es_distance(packer, es_distance_msg, bus, pcm_cancel_cmd): - values = copy.copy(es_distance_msg) + values = {s: es_distance_msg[s] for s in [ + "CHECKSUM", + "COUNTER", + "Signal1", + "Cruise_Fault", + "Cruise_Throttle", + "Signal2", + "Car_Follow", + "Signal3", + "Cruise_Soft_Disable", + "Signal7", + "Cruise_Brake_Active", + "Distance_Swap", + "Cruise_EPB", + "Signal4", + "Close_Distance", + "Signal5", + "Cruise_Cancel", + "Cruise_Set", + "Cruise_Resume", + "Signal6", + ]} values["COUNTER"] = (values["COUNTER"] + 1) % 0x10 if pcm_cancel_cmd: values["Cruise_Cancel"] = 1 @@ -26,8 +46,24 @@ def create_es_distance(packer, es_distance_msg, bus, pcm_cancel_cmd): def create_es_lkas_state(packer, es_lkas_state_msg, lat_active, mads_enabled, visual_alert, left_line, right_line, left_lane_depart, right_lane_depart): - - values = copy.copy(es_lkas_state_msg) + values = {s: es_lkas_state_msg[s] for s in [ + "CHECKSUM", + "COUNTER", + "LKAS_Alert_Msg", + "Signal1", + "LKAS_ACTIVE", + "LKAS_Dash_State", + "Signal2", + "Backward_Speed_Limit_Menu", + "LKAS_Left_Line_Enable", + "LKAS_Left_Line_Light_Blink", + "LKAS_Right_Line_Enable", + "LKAS_Right_Line_Light_Blink", + "LKAS_Left_Line_Visible", + "LKAS_Right_Line_Visible", + "LKAS_Alert", + "Signal3", + ]} # Filter the stock LKAS "Keep hands on wheel" alert if values["LKAS_Alert_Msg"] == 1: @@ -73,7 +109,35 @@ def create_es_lkas_state(packer, es_lkas_state_msg, lat_active, mads_enabled, vi def create_es_dashstatus(packer, dashstatus_msg): - values = copy.copy(dashstatus_msg) + values = {s: dashstatus_msg[s] for s in [ + "CHECKSUM", + "COUNTER", + "PCB_Off", + "LDW_Off", + "Signal1", + "Cruise_State_Msg", + "LKAS_State_Msg", + "Signal2", + "Cruise_Soft_Disable", + "Cruise_Status_Msg", + "Signal3", + "Cruise_Distance", + "Signal4", + "Conventional_Cruise", + "Signal5", + "Cruise_Disengaged", + "Cruise_Activated", + "Signal6", + "Cruise_Set_Speed", + "Cruise_Fault", + "Cruise_On", + "Display_Own_Car", + "Brake_Lights", + "Car_Follow", + "Signal7", + "Far_Distance", + "Cruise_State", + ]} # Filter stock LKAS disabled and Keep hands on steering wheel OFF alerts if values["LKAS_State_Msg"] in (2, 3): @@ -84,18 +148,26 @@ def create_es_dashstatus(packer, dashstatus_msg): def create_infotainmentstatus(packer, infotainmentstatus_msg, visual_alert): # Filter stock LKAS disabled and Keep hands on steering wheel OFF alerts - if infotainmentstatus_msg["LKAS_State_Infotainment"] in (3, 4): - infotainmentstatus_msg["LKAS_State_Infotainment"] = 0 + values = {s: infotainmentstatus_msg[s] for s in [ + "CHECKSUM", + "COUNTER", + "LKAS_State_Infotainment", + "LKAS_Blue_Lines", + "Signal1", + "Signal2", + ]} + if values["LKAS_State_Infotainment"] in (3, 4): + values["LKAS_State_Infotainment"] = 0 # Show Keep hands on wheel alert for openpilot steerRequired alert if visual_alert == VisualAlert.steerRequired: - infotainmentstatus_msg["LKAS_State_Infotainment"] = 3 + values["LKAS_State_Infotainment"] = 3 # Show Obstacle Detected for fcw if visual_alert == VisualAlert.fcw: - infotainmentstatus_msg["LKAS_State_Infotainment"] = 2 + values["LKAS_State_Infotainment"] = 2 - return packer.make_can_msg("INFOTAINMENT_STATUS", 0, infotainmentstatus_msg) + return packer.make_can_msg("INFOTAINMENT_STATUS", 0, values) # *** Subaru Pre-global *** @@ -105,14 +177,14 @@ def subaru_preglobal_checksum(packer, values, addr): return (sum(dat[:7])) % 256 -def create_preglobal_steering_control(packer, apply_steer, frame, steer_step): +def create_preglobal_steering_control(packer, apply_steer, steer_req, frame, steer_step): idx = (frame / steer_step) % 8 values = { "COUNTER": idx, "LKAS_Command": apply_steer, - "LKAS_Active": 1 if apply_steer != 0 else 0 + "LKAS_Active": steer_req, } values["Checksum"] = subaru_preglobal_checksum(packer, values, "ES_LKAS") @@ -120,9 +192,27 @@ def create_preglobal_steering_control(packer, apply_steer, frame, steer_step): def create_preglobal_es_distance(packer, cruise_button, es_distance_msg): - values = copy.copy(es_distance_msg) - values["Cruise_Button"] = cruise_button + values = {s: es_distance_msg[s] for s in [ + "Cruise_Throttle", + "Signal1", + "Car_Follow", + "Signal2", + "Brake_On", + "Distance_Swap", + "Standstill", + "Signal3", + "Close_Distance", + "Signal4", + "Standstill_2", + "Cruise_Fault", + "Signal5", + "COUNTER", + "Signal6", + "Cruise_Button", + "Signal7", + ]} + values["Cruise_Button"] = cruise_button values["Checksum"] = subaru_preglobal_checksum(packer, values, "ES_Distance") return packer.make_can_msg("ES_Distance", 0, values) diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index 82650e77d5..e4a6d3fbd2 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -6,7 +6,7 @@ from typing import Dict, List, Union from cereal import car from panda.python import uds from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarInfo, Harness, HarnessKit +from selfdrive.car.docs_definitions import CarHarness, CarInfo, CarParts from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16 Ecu = car.CarParams.Ecu @@ -56,13 +56,13 @@ class CAR: @dataclass class SubaruCarInfo(CarInfo): package: str = "EyeSight Driver Assistance" - harness_kit: HarnessKit = HarnessKit(Harness.subaru_a) + car_parts: CarParts = CarParts.common([CarHarness.subaru_a]) CAR_INFO: Dict[str, Union[SubaruCarInfo, List[SubaruCarInfo]]] = { CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-21", "All"), - CAR.OUTBACK: SubaruCarInfo("Subaru Outback 2020-22", "All", harness_kit=HarnessKit(Harness.subaru_b)), - CAR.LEGACY: SubaruCarInfo("Subaru Legacy 2020-22", "All", harness_kit=HarnessKit(Harness.subaru_b)), + CAR.OUTBACK: SubaruCarInfo("Subaru Outback 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])), + CAR.LEGACY: SubaruCarInfo("Subaru Legacy 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])), CAR.IMPREZA: [ SubaruCarInfo("Subaru Impreza 2017-19"), SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), @@ -513,6 +513,7 @@ FW_VERSIONS = { b'\xf1\x82\xe2,\xa0@\x07', b'\xbc"`q\x07', b'\xe3,\xa0@\x07', + b'\xbc,\xa0u\x07', ], (Ecu.transmission, 0x7e1, None): [ b'\xa5\xfe\xf7@\x00', @@ -522,6 +523,7 @@ FW_VERSIONS = { b'\xf1\x82\xa7\xf6D@\x00', b'\xa7\xf6D@\x00', b'\xa7\xfe\xf4@\x00', + b'\xa5\xfe\xf8@\x00', ], }, } diff --git a/selfdrive/car/tesla/carcontroller.py b/selfdrive/car/tesla/carcontroller.py index 1f18c3d0ea..aeaaba88e7 100644 --- a/selfdrive/car/tesla/carcontroller.py +++ b/selfdrive/car/tesla/carcontroller.py @@ -24,17 +24,18 @@ class CarController: hands_on_fault = CS.steer_warning == "EAC_ERROR_HANDS_ON" and CS.hands_on_level >= 3 lkas_enabled = CC.latActive and not hands_on_fault - if lkas_enabled: - # Angular rate limit based on speed - apply_angle = apply_std_steer_angle_limits(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgo, CarControllerParams) + if self.frame % 2 == 0: + if lkas_enabled: + # Angular rate limit based on speed + apply_angle = apply_std_steer_angle_limits(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgo, CarControllerParams) - # To not fault the EPS - apply_angle = clip(apply_angle, CS.out.steeringAngleDeg - 20, CS.out.steeringAngleDeg + 20) - else: - apply_angle = CS.out.steeringAngleDeg + # To not fault the EPS + apply_angle = clip(apply_angle, CS.out.steeringAngleDeg - 20, CS.out.steeringAngleDeg + 20) + else: + apply_angle = CS.out.steeringAngleDeg - self.apply_angle_last = apply_angle - can_sends.append(self.tesla_can.create_steering_control(apply_angle, lkas_enabled, self.frame)) + self.apply_angle_last = apply_angle + can_sends.append(self.tesla_can.create_steering_control(apply_angle, lkas_enabled, (self.frame // 2) % 16)) # Longitudinal control (in sync with stock message, about 40Hz) if self.CP.openpilotLongitudinalControl: @@ -59,7 +60,7 @@ class CarController: # TODO: HUD control new_actuators = actuators.copy() - new_actuators.steeringAngleDeg = apply_angle + new_actuators.steeringAngleDeg = self.apply_angle_last self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/tesla/teslacan.py b/selfdrive/car/tesla/teslacan.py index e5d904f80e..a491c030f8 100644 --- a/selfdrive/car/tesla/teslacan.py +++ b/selfdrive/car/tesla/teslacan.py @@ -1,4 +1,3 @@ -import copy import crcmod from common.conversions import Conversions as CV @@ -18,12 +17,12 @@ class TeslaCAN: ret += sum(dat) return ret & 0xFF - def create_steering_control(self, angle, enabled, frame): + def create_steering_control(self, angle, enabled, counter): values = { "DAS_steeringAngleRequest": -angle, "DAS_steeringHapticRequest": 0, "DAS_steeringControlType": 1 if enabled else 0, - "DAS_steeringControlCounter": (frame % 16), + "DAS_steeringControlCounter": counter, } data = self.packer.make_can_msg("DAS_steeringControl", CANBUS.chassis, values)[2] @@ -31,7 +30,40 @@ class TeslaCAN: return self.packer.make_can_msg("DAS_steeringControl", CANBUS.chassis, values) def create_action_request(self, msg_stw_actn_req, cancel, bus, counter): - values = copy.copy(msg_stw_actn_req) + # We copy this whole message when spamming cancel + values = {s: msg_stw_actn_req[s] for s in [ + "SpdCtrlLvr_Stat", + "VSL_Enbl_Rq", + "SpdCtrlLvrStat_Inv", + "DTR_Dist_Rq", + "TurnIndLvr_Stat", + "HiBmLvr_Stat", + "WprWashSw_Psd", + "WprWash_R_Sw_Posn_V2", + "StW_Lvr_Stat", + "StW_Cond_Flt", + "StW_Cond_Psd", + "HrnSw_Psd", + "StW_Sw00_Psd", + "StW_Sw01_Psd", + "StW_Sw02_Psd", + "StW_Sw03_Psd", + "StW_Sw04_Psd", + "StW_Sw05_Psd", + "StW_Sw06_Psd", + "StW_Sw07_Psd", + "StW_Sw08_Psd", + "StW_Sw09_Psd", + "StW_Sw10_Psd", + "StW_Sw11_Psd", + "StW_Sw12_Psd", + "StW_Sw13_Psd", + "StW_Sw14_Psd", + "StW_Sw15_Psd", + "WprSw6Posn", + "MC_STW_ACTN_RQ", + "CRC_STW_ACTN_RQ", + ]} if cancel: values["SpdCtrlLvr_Stat"] = 1 diff --git a/selfdrive/car/tesla/values.py b/selfdrive/car/tesla/values.py index bac025a9c5..f79e5f0097 100644 --- a/selfdrive/car/tesla/values.py +++ b/selfdrive/car/tesla/values.py @@ -103,8 +103,8 @@ BUTTONS = [ ] class CarControllerParams: - ANGLE_RATE_LIMIT_UP = AngleRateLimit(speed_bp=[0., 5., 15.], angle_v=[5., .8, .15]) - ANGLE_RATE_LIMIT_DOWN = AngleRateLimit(speed_bp=[0., 5., 15.], angle_v=[5., 3.5, 0.4]) + ANGLE_RATE_LIMIT_UP = AngleRateLimit(speed_bp=[0., 5., 15.], angle_v=[10., 1.6, .3]) + ANGLE_RATE_LIMIT_DOWN = AngleRateLimit(speed_bp=[0., 5., 15.], angle_v=[10., 7.0, 0.8]) JERK_LIMIT_MAX = 8 JERK_LIMIT_MIN = -8 ACCEL_TO_SPEED_MULTIPLIER = 3 diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 7198218d6a..918d2cf873 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import math import unittest +from hypothesis import given, settings import importlib from parameterized import parameterized @@ -8,12 +9,15 @@ from cereal import car from selfdrive.car import gen_empty_fingerprint from selfdrive.car.car_helpers import interfaces from selfdrive.car.fingerprints import _FINGERPRINTS as FINGERPRINTS, all_known_cars +from selfdrive.test.fuzzy_generation import get_random_msg class TestCarInterfaces(unittest.TestCase): @parameterized.expand([(car,) for car in all_known_cars()]) - def test_car_interfaces(self, car_name): + @settings(max_examples=5) + @given(cc_msg=get_random_msg(car.CarControl, real_floats=True)) + def test_car_interfaces(self, car_name, cc_msg): if car_name in FINGERPRINTS: fingerprint = FINGERPRINTS[car_name][0] else: @@ -57,13 +61,13 @@ class TestCarInterfaces(unittest.TestCase): self.assertTrue(len(tune.indi.outerLoopGainV)) # Run car interface - CC = car.CarControl.new_message() + CC = car.CarControl.new_message(**cc_msg) for _ in range(10): car_interface.update(CC, []) car_interface.apply(CC, 0) car_interface.apply(CC, 0) - CC = car.CarControl.new_message() + CC = car.CarControl.new_message(**cc_msg) CC.enabled = True for _ in range(10): car_interface.update(CC, []) diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index e36bdc5ded..7ea9f6c9fe 100755 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 from collections import defaultdict +import os import re import unittest +from common.basedir import BASEDIR from selfdrive.car.car_helpers import interfaces, get_interface_attr from selfdrive.car.docs import CARS_MD_OUT, CARS_MD_TEMPLATE, generate_cars_md, get_all_car_info -from selfdrive.car.docs_definitions import Column, Harness, Star +from selfdrive.car.docs_definitions import Cable, Column, PartType, Star from selfdrive.car.honda.values import CAR as HONDA +from selfdrive.debug.dump_car_info import dump_car_info +from selfdrive.debug.print_docs_diff import print_car_info_diff class TestCarDocs(unittest.TestCase): @@ -22,6 +26,12 @@ class TestCarDocs(unittest.TestCase): self.assertEqual(generated_cars_md, current_cars_md, "Run selfdrive/car/docs.py to update the compatibility documentation") + def test_docs_diff(self): + dump_path = os.path.join(BASEDIR, "selfdrive", "car", "tests", "cars_dump") + dump_car_info(dump_path) + print_car_info_diff(dump_path) + os.remove(dump_path) + def test_duplicate_years(self): make_model_years = defaultdict(list) for car in self.all_cars: @@ -74,8 +84,12 @@ class TestCarDocs(unittest.TestCase): if car.name == "comma body": raise unittest.SkipTest - self.assertNotIn(car.harness_kit.connector, [None, Harness.none], f"Need to specify car harness: {car.name}") - self.assertTrue(car.harness_kit.parts, f"Need to specify harness parts: {car.name} with {car.harness_kit.connector.value} connector") + car_part_type = [p.type for p in car.car_parts.all_parts()] + car_parts = [p for p in car.car_parts.all_parts()] + self.assertTrue(len(car_parts) > 0, f"Need to specify car parts: {car.name}") + self.assertTrue(car_part_type.count(PartType.connector) == 1, f"Need to specify one harness connector: {car.name}") + self.assertTrue(car_part_type.count(PartType.mount) == 1, f"Need to specify one mount: {car.name}") + self.assertTrue(Cable.right_angle_obd_c_cable_1_5ft in car_parts, f"Need to specify a right angle OBD-C cable (1.5ft): {car.name}") if __name__ == "__main__": diff --git a/selfdrive/car/tests/test_fw_fingerprint.py b/selfdrive/car/tests/test_fw_fingerprint.py index c069280d11..883c4e34a0 100755 --- a/selfdrive/car/tests/test_fw_fingerprint.py +++ b/selfdrive/car/tests/test_fw_fingerprint.py @@ -10,7 +10,7 @@ from cereal import car from common.params import Params from selfdrive.car.car_helpers import interfaces from selfdrive.car.fingerprints import FW_VERSIONS -from selfdrive.car.fw_versions import FW_QUERY_CONFIGS, VERSIONS, match_fw_to_car, get_fw_versions +from selfdrive.car.fw_versions import FW_QUERY_CONFIGS, FUZZY_EXCLUDE_ECUS, VERSIONS, build_fw_dict, match_fw_to_car, get_fw_versions CarFw = car.CarParams.CarFw Ecu = car.CarParams.Ecu @@ -33,27 +33,73 @@ class TestFwFingerprint(unittest.TestCase): self.assertEqual(candidates[0], expected) @parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e]) - def test_fw_fingerprint(self, brand, car_model, ecus): + def test_exact_match(self, brand, car_model, ecus): CP = car.CarParams.new_message() for _ in range(200): fw = [] for ecu, fw_versions in ecus.items(): - if not len(fw_versions): - raise unittest.SkipTest("Car model has no FW versions") ecu_name, addr, sub_addr = ecu fw.append({"ecu": ecu_name, "fwVersion": random.choice(fw_versions), 'brand': brand, "address": addr, "subAddress": 0 if sub_addr is None else sub_addr}) CP.carFw = fw - _, matches = match_fw_to_car(CP.carFw) + _, matches = match_fw_to_car(CP.carFw, allow_fuzzy=False) self.assertFingerprints(matches, car_model) - def test_no_duplicate_fw_versions(self): + @parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e]) + def test_custom_fuzzy_match(self, brand, car_model, ecus): + # Assert brand-specific fuzzy fingerprinting function doesn't disagree with standard fuzzy function + config = FW_QUERY_CONFIGS[brand] + if config.match_fw_to_car_fuzzy is None: + raise unittest.SkipTest("Brand does not implement custom fuzzy fingerprinting function") + + CP = car.CarParams.new_message() + for _ in range(5): + fw = [] + for ecu, fw_versions in ecus.items(): + ecu_name, addr, sub_addr = ecu + fw.append({"ecu": ecu_name, "fwVersion": random.choice(fw_versions), 'brand': brand, + "address": addr, "subAddress": 0 if sub_addr is None else sub_addr}) + CP.carFw = fw + _, matches = match_fw_to_car(CP.carFw, allow_exact=False, log=False) + brand_matches = config.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw)) + + # If both have matches, they must agree + if len(matches) == 1 and len(brand_matches) == 1: + self.assertEqual(matches, brand_matches) + + @parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e]) + def test_fuzzy_match_ecu_count(self, brand, car_model, ecus): + # Asserts that fuzzy matching does not count matching FW, but ECU address keys + valid_ecus = [e for e in ecus if e[0] not in FUZZY_EXCLUDE_ECUS] + if not len(valid_ecus): + raise unittest.SkipTest("Car model has no compatible ECUs for fuzzy matching") + + fw = [] + for ecu in valid_ecus: + ecu_name, addr, sub_addr = ecu + for _ in range(5): + # Add multiple FW versions to simulate ECU returning to multiple queries in a brand + fw.append({"ecu": ecu_name, "fwVersion": random.choice(ecus[ecu]), 'brand': brand, + "address": addr, "subAddress": 0 if sub_addr is None else sub_addr}) + CP = car.CarParams.new_message(carFw=fw) + _, matches = match_fw_to_car(CP.carFw, allow_exact=False, log=False) + + # Assert no match if there are not enough unique ECUs + unique_ecus = {(f['address'], f['subAddress']) for f in fw} + if len(unique_ecus) < 2: + self.assertEqual(len(matches), 0, car_model) + # There won't always be a match due to shared FW, but if there is it should be correct + elif len(matches): + self.assertFingerprints(matches, car_model) + + def test_fw_version_lists(self): for car_model, ecus in FW_VERSIONS.items(): with self.subTest(car_model=car_model): for ecu, ecu_fw in ecus.items(): with self.subTest(ecu): duplicates = {fw for fw in ecu_fw if ecu_fw.count(fw) > 1} - self.assertFalse(len(duplicates), f"{car_model}: Duplicate FW versions: Ecu.{ECU_NAME[ecu[0]]}, {duplicates}") + self.assertFalse(len(duplicates), f'{car_model}: Duplicate FW versions: Ecu.{ECU_NAME[ecu[0]]}, {duplicates}') + self.assertGreater(len(ecu_fw), 0, f'{car_model}: No FW versions: Ecu.{ECU_NAME[ecu[0]]}') def test_all_addrs_map_to_one_ecu(self): for brand, cars in VERSIONS.items(): @@ -168,7 +214,7 @@ class TestFwFingerprintTiming(unittest.TestCase): if not len(multi_panda_requests) and num_pandas > 1: raise unittest.SkipTest("No multi-panda FW queries") - avg_time = self._benchmark(brand, num_pandas, 10) + avg_time = self._benchmark(brand, num_pandas, 5) total_time += avg_time self._assert_timing(avg_time, brand_ref_times[num_pandas][brand], tol) print(f'{brand=}, {num_pandas=}, {len(config.requests)=}, avg FW query time={avg_time} seconds') diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 00552485a8..422c7560fe 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -6,7 +6,7 @@ from typing import Dict, List, Union from cereal import car from common.conversions import Conversions as CV from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarFootnote, CarInfo, Column, Harness, HarnessKit +from selfdrive.car.docs_definitions import CarFootnote, CarInfo, Column, CarParts, CarHarness from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries Ecu = car.CarParams.Ecu @@ -102,7 +102,7 @@ class Footnote(Enum): @dataclass class ToyotaCarInfo(CarInfo): package: str = "All" - harness_kit: HarnessKit = HarnessKit(Harness.toyota) + car_parts: CarParts = CarParts.common([CarHarness.toyota]) CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { @@ -1131,7 +1131,6 @@ FW_VERSIONS = { b'\x01896630EF8000\x00\x00\x00\x00', b'\x02896630E66000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', b'\x02896630E66100\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', - b'\x01896630EA1000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', b'\x02896630EB3000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', b'\x02896630EB3100\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', ], diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py index 47eb07bdc9..76e87e95d6 100644 --- a/selfdrive/car/volkswagen/carcontroller.py +++ b/selfdrive/car/volkswagen/carcontroller.py @@ -170,8 +170,7 @@ class CarController: gra_send_ready = self.CP.pcmCruise and CS.gra_stock_values["COUNTER"] != self.gra_acc_counter_last if gra_send_ready and (CC.cruiseControl.cancel or CC.cruiseControl.resume): - counter = (CS.gra_stock_values["COUNTER"] + 1) % 16 - can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, ext_bus, CS.gra_stock_values, counter, + can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, ext_bus, CS.gra_stock_values, cancel=CC.cruiseControl.cancel, resume=CC.cruiseControl.resume)) if not (CC.cruiseControl.cancel or CC.cruiseControl.resume) and CS.out.cruiseState.enabled: if gra_send_ready and not self.CP.pcmCruiseSpeed: diff --git a/selfdrive/car/volkswagen/mqbcan.py b/selfdrive/car/volkswagen/mqbcan.py index 055441acf4..04bbd6825e 100644 --- a/selfdrive/car/volkswagen/mqbcan.py +++ b/selfdrive/car/volkswagen/mqbcan.py @@ -11,7 +11,15 @@ def create_steering_control(packer, bus, apply_steer, lkas_enabled): def create_lka_hud_control(packer, bus, ldw_stock_values, enabled, lat_active, steering_pressed, hud_alert, hud_control): - values = ldw_stock_values.copy() + values = {} + if len(ldw_stock_values): + values = {s: ldw_stock_values[s] for s in [ + "LDW_SW_Warnung_links", # Blind spot in warning mode on left side due to lane departure + "LDW_SW_Warnung_rechts", # Blind spot in warning mode on right side due to lane departure + "LDW_Seite_DLCTLC", # Direction of most likely lane departure (left or right) + "LDW_DLC", # Lane departure, distance to line crossing + "LDW_TLC", # Lane departure, time to line crossing + ]} values.update({ "LDW_Status_LED_gelb": 1 if lat_active and steering_pressed else 0, @@ -23,8 +31,14 @@ def create_lka_hud_control(packer, bus, ldw_stock_values, enabled, lat_active, s return packer.make_can_msg("LDW_02", bus, values) -def create_acc_buttons_control(packer, bus, gra_stock_values, counter, buttons=0, cancel=False, resume=False): - values = gra_stock_values.copy() +def create_acc_buttons_control(packer, bus, gra_stock_values, buttons=0, cancel=False, resume=False): + values = {s: gra_stock_values[s] for s in [ + "GRA_Hauptschalter", # ACC button, on/off + "GRA_Typ_Hauptschalter", # ACC main button type + "GRA_Codierung", # ACC button configuration/coding + "GRA_Tip_Stufe_2", # unknown related to stalk type + "GRA_ButtonTypeInfo", # unknown related to stalk type + ]} accel_cruise = 1 if buttons == 1 else 0 decel_cruise = 1 if buttons == 2 else 0 @@ -32,7 +46,7 @@ def create_acc_buttons_control(packer, bus, gra_stock_values, counter, buttons=0 set_cruise = 1 if buttons == 4 else 0 values.update({ - "COUNTER": counter, + "COUNTER": (gra_stock_values["COUNTER"] + 1) % 16, "GRA_Abbrechen": cancel, "GRA_Tip_Wiederaufnahme": resume or resume_cruise, "GRA_Tip_Setzen": set_cruise, diff --git a/selfdrive/car/volkswagen/pqcan.py b/selfdrive/car/volkswagen/pqcan.py index 36baa08e40..e8c89f067b 100644 --- a/selfdrive/car/volkswagen/pqcan.py +++ b/selfdrive/car/volkswagen/pqcan.py @@ -10,7 +10,15 @@ def create_steering_control(packer, bus, apply_steer, lkas_enabled): def create_lka_hud_control(packer, bus, ldw_stock_values, enabled, lat_active, steering_pressed, hud_alert, hud_control): - values = ldw_stock_values.copy() + values = {} + if len(ldw_stock_values): + values = {s: ldw_stock_values[s] for s in [ + "LDW_SW_Warnung_links", # Blind spot in warning mode on left side due to lane departure + "LDW_SW_Warnung_rechts", # Blind spot in warning mode on right side due to lane departure + "LDW_Seite_DLCTLC", # Direction of most likely lane departure (left or right) + "LDW_DLC", # Lane departure, distance to line crossing + "LDW_TLC", # Lane departure, time to line crossing + ]} values.update({ "LDW_Lampe_gelb": 1 if lat_active and steering_pressed else 0, @@ -23,8 +31,13 @@ def create_lka_hud_control(packer, bus, ldw_stock_values, enabled, lat_active, s return packer.make_can_msg("LDW_Status", bus, values) -def create_acc_buttons_control(packer, bus, gra_stock_values, counter, buttons=0, cancel=False, resume=False): - values = gra_stock_values.copy() +def create_acc_buttons_control(packer, bus, gra_stock_values, buttons=0, cancel=False, resume=False): + values = {s: gra_stock_values[s] for s in [ + "GRA_Hauptschalt", # ACC button, on/off + "GRA_Typ_Hauptschalt", # ACC button, momentary vs latching + "GRA_Kodierinfo", # ACC button, configuration + "GRA_Sender", # ACC button, CAN message originator + ]} accel_cruise = 1 if buttons == 1 else 0 decel_cruise = 1 if buttons == 2 else 0 @@ -32,7 +45,7 @@ def create_acc_buttons_control(packer, bus, gra_stock_values, counter, buttons=0 set_cruise = 1 if buttons == 4 else 0 values.update({ - "COUNTER": counter, + "COUNTER": (gra_stock_values["COUNTER"] + 1) % 16, "GRA_Abbrechen": cancel, "GRA_Recall": resume or resume_cruise, "GRA_Neu_Setzen": set_cruise, diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index b1df280e49..93c0a98cd2 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -7,7 +7,8 @@ from cereal import car from panda.python import uds from opendbc.can.can_define import CANDefine from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarFootnote, CarInfo, Column, Harness, HarnessKit, HarnessPart +from selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarInfo, CarParts, Column, \ + Device from selfdrive.car.fw_query_definitions import FwQueryConfig, Request, p16 Ecu = car.CarParams.Ecu @@ -163,23 +164,32 @@ 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_EXP_LONG = CarFootnote ( + SKODA_HEATED_WINDSHIELD = CarFootnote( + "Some Škoda vehicles are equipped with heated windshields, which are known " + + "to block GPS signal needed for some comma three functionality.", + 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) + Column.HARDWARE) @dataclass class VWCarInfo(CarInfo): package: str = "Adaptive Cruise Control (ACC) & Lane Assist" - harness_kit: HarnessKit = HarnessKit(Harness.j533, parts=[HarnessPart.harness_box, HarnessPart.long_obdc_cable, HarnessPart.usbc_coupler]) + car_parts: CarParts = CarParts.common([CarHarness.j533]) def init_make(self, CP: car.CarParams): - self.footnotes.insert(0, Footnote.VW_EXP_LONG) + self.footnotes.append(Footnote.VW_EXP_LONG) + if "SKODA" in CP.carFingerprint: + self.footnotes.append(Footnote.SKODA_HEATED_WINDSHIELD) + + if CP.carFingerprint in (CAR.CRAFTER_MK2, CAR.TRANSPORTER_T61): + self.car_parts = CarParts([Device.three_angled_mount, CarHarness.j533]) CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { @@ -205,11 +215,11 @@ CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { ], CAR.GOLF_MK7: [ VWCarInfo("Volkswagen e-Golf 2014-20"), - VWCarInfo("Volkswagen Golf 2015-20"), - VWCarInfo("Volkswagen Golf Alltrack 2015-19"), + VWCarInfo("Volkswagen Golf 2015-20", auto_resume=False), + VWCarInfo("Volkswagen Golf Alltrack 2015-19", auto_resume=False), VWCarInfo("Volkswagen Golf GTD 2015-20"), VWCarInfo("Volkswagen Golf GTE 2015-20"), - VWCarInfo("Volkswagen Golf GTI 2015-21"), + VWCarInfo("Volkswagen Golf GTI 2015-21", auto_resume=False), VWCarInfo("Volkswagen Golf R 2015-19"), VWCarInfo("Volkswagen Golf SportsVan 2015-20"), ], @@ -237,7 +247,7 @@ CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { VWCarInfo("Volkswagen Tiguan 2018-23"), VWCarInfo("Volkswagen Tiguan eHybrid 2021-23"), ], - CAR.TOURAN_MK2: VWCarInfo("Volkswagen Touran 2017"), + CAR.TOURAN_MK2: VWCarInfo("Volkswagen Touran 2016-23"), CAR.TRANSPORTER_T61: [ VWCarInfo("Volkswagen Caravelle 2020"), VWCarInfo("Volkswagen California 2021"), @@ -302,33 +312,35 @@ FW_QUERY_CONFIG = FwQueryConfig( FW_VERSIONS = { CAR.ARTEON_MK1: { (Ecu.engine, 0x7e0, None): [ - b'\xf1\x873G0906259M \xf1\x890003', b'\xf1\x873G0906259F \xf1\x890004', + b'\xf1\x873G0906259G \xf1\x890004', + b'\xf1\x873G0906259G \xf1\x890005', + b'\xf1\x873G0906259M \xf1\x890003', b'\xf1\x873G0906259N \xf1\x890004', b'\xf1\x873G0906259P \xf1\x890001', b'\xf1\x875NA907115H \xf1\x890002', - b'\xf1\x873G0906259G \xf1\x890004', ], (Ecu.transmission, 0x7e1, None): [ - b'\xf1\x870DL300014C \xf1\x893704', b'\xf1\x8709G927158L \xf1\x893611', + b'\xf1\x870DL300014C \xf1\x893704', b'\xf1\x870GC300011L \xf1\x891401', b'\xf1\x870GC300014M \xf1\x892802', b'\xf1\x870GC300040P \xf1\x891401', ], (Ecu.srs, 0x715, None): [ - b'\xf1\x875QF959655AP\xf1\x890755\xf1\x82\x1311110011111311111100110200--1611125F49', b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e1616001613121157161111572900', b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e1616001613121177161113772900', - b'\xf1\x873Q0959655DL\xf1\x890732\xf1\x82\0161812141812171105141123052J00', b'\xf1\x873Q0959655CK\xf1\x890711\xf1\x82\x0e1712141712141105121122052900', + b'\xf1\x873Q0959655DA\xf1\x890720\xf1\x82\x0e1712141712141105121122052900', + b'\xf1\x873Q0959655DL\xf1\x890732\xf1\x82\0161812141812171105141123052J00', + b'\xf1\x875QF959655AP\xf1\x890755\xf1\x82\x1311110011111311111100110200--1611125F49', ], (Ecu.eps, 0x712, None): [ - b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x002NB4202N7N', b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571B41815A1', b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571B00817A1', b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567B0020800', b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x002MB4092M7N', + b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x002NB4202N7N', ], (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x872Q0907572AA\xf1\x890396', @@ -402,6 +414,7 @@ FW_VERSIONS = { CAR.GOLF_MK7: { (Ecu.engine, 0x7e0, None): [ b'\xf1\x8704E906016A \xf1\x897697', + b'\xf1\x8704E906016N \xf1\x899105', b'\xf1\x8704E906016AD\xf1\x895758', b'\xf1\x8704E906016CE\xf1\x899096', b'\xf1\x8704E906016CH\xf1\x899226', @@ -544,6 +557,7 @@ FW_VERSIONS = { b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A00504A1', b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A00604A1', b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A07A02A1', + b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A00407A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A00507A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A07B04A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A20B03A1', @@ -641,6 +655,7 @@ FW_VERSIONS = { b'\xf1\x870DD300045T \xf1\x891601', b'\xf1\x870DL300011H \xf1\x895201', b'\xf1\x870CW300042H \xf1\x891601', + b'\xf1\x870CW300042H \xf1\x891607', b'\xf1\x870GC300042H \xf1\x891404', b'\xf1\x870D9300018C \xf1\x895297', b'\xf1\x870GC300043 \xf1\x892301', @@ -648,6 +663,7 @@ FW_VERSIONS = { (Ecu.srs, 0x715, None): [ b'\xf1\x873Q0959655AE\xf1\x890195\xf1\x82\r56140056130012416612124111', b'\xf1\x873Q0959655AF\xf1\x890195\xf1\x82\r56140056130012026612120211', + b'\xf1\x873Q0959655AN\xf1\x890305\xf1\x82\r58160058140013036914110311', b'\xf1\x873Q0959655AN\xf1\x890306\xf1\x82\r58160058140013036914110311', b'\xf1\x873Q0959655BA\xf1\x890195\xf1\x82\r56140056130012516612125111', b'\xf1\x873Q0959655BB\xf1\x890195\xf1\x82\r56140056130012026612120211', @@ -664,6 +680,7 @@ FW_VERSIONS = { b'\xf1\x875Q0909143K \xf1\x892033\xf1\x820514B0060703', b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0060803', b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0080803', + b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820526B0060905', b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\00521B00606A1', b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\00516B00501A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\00521B00703A1', @@ -676,6 +693,7 @@ FW_VERSIONS = { b'\xf1\x873Q0907572B \xf1\x890192', b'\xf1\x873Q0907572C \xf1\x890195', b'\xf1\x873Q0907572C \xf1\x890196', + b'\xf1\x875Q0907572P \xf1\x890682', b'\xf1\x875Q0907572R \xf1\x890771', ], }, @@ -870,24 +888,28 @@ FW_VERSIONS = { b'\xf1\x8704L906056AL\xf1\x899970', b'\xf1\x8704L906057AP\xf1\x891186', b'\xf1\x8704L906057N \xf1\x890413', + b'\xf1\x8705L906023E \xf1\x891352', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x870BT300012G \xf1\x893102', b'\xf1\x870BT300012E \xf1\x893105', b'\xf1\x870BT300046R \xf1\x893102', + b'\xf1\x870DV300012B \xf1\x893701', ], (Ecu.srs, 0x715, None): [ b'\xf1\x872Q0959655AE\xf1\x890506\xf1\x82\x1316170411110411--04041704161611152S1411', b'\xf1\x872Q0959655AE\xf1\x890506\xf1\x82\x1316170411110411--04041704171711152S1411', b'\xf1\x872Q0959655AF\xf1\x890506\xf1\x82\x1316171111110411--04041711121211152S1413', + b'\xf1\x872Q0959655AQ\xf1\x890511\xf1\x82\x1316170411110411--0404170426261215391421', ], (Ecu.eps, 0x712, None): [ b'\xf1\x877LA909144F \xf1\x897150\xf1\x82\x0532380518A2', b'\xf1\x877LA909144G \xf1\x897160\xf1\x82\x05333A5519A2', - b'\xf1\x877LA909144F \xf1\x897150\xf1\x82\005323A5519A2', + b'\xf1\x877LA909144F \xf1\x897150\xf1\x82\x05323A5519A2', ], (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x872Q0907572R \xf1\x890372', + b'\xf1\x872Q0907572AA\xf1\x890396', ], }, CAR.TROC_MK1: { @@ -917,6 +939,7 @@ FW_VERSIONS = { b'\xf1\x8704E906027CJ\xf1\x897798', b'\xf1\x8704L997022N \xf1\x899459', b'\xf1\x875G0906259A \xf1\x890004', + b'\xf1\x875G0906259D \xf1\x890002', b'\xf1\x875G0906259L \xf1\x890002', b'\xf1\x875G0906259Q \xf1\x890002', b'\xf1\x878V0906259F \xf1\x890002', @@ -926,7 +949,7 @@ FW_VERSIONS = { b'\xf1\x878V0906264B \xf1\x890003', b'\xf1\x878V0907115B \xf1\x890007', b'\xf1\x878V0907404A \xf1\x890005', - b'\xf1\x875G0906259D \xf1\x890002', + b'\xf1\x878V0907404G \xf1\x890005', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x870CW300044T \xf1\x895245', @@ -943,6 +966,7 @@ FW_VERSIONS = { b'\xf1\x870DD300046F \xf1\x891602', b'\xf1\x870DD300046G \xf1\x891601', b'\xf1\x870DL300012E \xf1\x892012', + b'\xf1\x870DL300012H \xf1\x892112', b'\xf1\x870GC300011 \xf1\x890403', b'\xf1\x870GC300013M \xf1\x892402', b'\xf1\x870GC300042J \xf1\x891402', @@ -955,9 +979,9 @@ FW_VERSIONS = { b'\xf1\x875Q0959655BJ\xf1\x890339\xf1\x82\x1311110011131100311111011731179321342100', b'\xf1\x875Q0959655J \xf1\x890825\xf1\x82\x13111112111111--241115141112221291163221', b'\xf1\x875Q0959655J \xf1\x890825\xf1\x82\023111112111111--171115141112221291163221', - b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\023121111111211--261117141112231291163221', b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13111112111111--241115141112221291163221', b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13121111111111--341117141212231291163221', + b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13121111111211--261117141112231291163221', b'\xf1\x875Q0959655N \xf1\x890361\xf1\x82\0211212001112110004110411111421149114', b'\xf1\x875Q0959655N \xf1\x890361\xf1\x82\0211212001112111104110411111521159114', ], @@ -966,6 +990,7 @@ FW_VERSIONS = { b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566G0HA14A1', b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571G01A16A1', b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571G0HA16A1', + b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571G0JA13A1', b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571G0JA14A1', b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\00521G0G809A1', b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\00503G00303A0', @@ -1055,24 +1080,29 @@ FW_VERSIONS = { b'\xf1\x8704L906021EL\xf1\x897542', b'\xf1\x8704L906026BP\xf1\x891198', b'\xf1\x8704L906026BP\xf1\x897608', + b'\xf1\x8704L906056CR\xf1\x892181', b'\xf1\x8704L906056CR\xf1\x892797', b'\xf1\x8705E906018AS\xf1\x899596', b'\xf1\x878V0906264H \xf1\x890005', + b'\xf1\x878V0907115E \xf1\x890002', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x870CW300041D \xf1\x891004', b'\xf1\x870CW300041G \xf1\x891003', b'\xf1\x870CW300050J \xf1\x891908', b'\xf1\x870D9300042M \xf1\x895016', + b'\xf1\x870GC300043A \xf1\x892304', ], (Ecu.srs, 0x715, None): [ b'\xf1\x873Q0959655AC\xf1\x890189\xf1\x82\r11110011110011021511110200', b'\xf1\x873Q0959655AS\xf1\x890200\xf1\x82\r11110011110011021511110200', b'\xf1\x873Q0959655AS\xf1\x890200\xf1\x82\r12110012120012021612110200', b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e1312001313001305171311052900', + b'\xf1\x873Q0959655BH\xf1\x890712\xf1\x82\x0e1312001313001305171311052900', b'\xf1\x873Q0959655CM\xf1\x890720\xf1\x82\0161312001313001305171311052900', ], (Ecu.eps, 0x712, None): [ + b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521N01842A1', b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\00521N01342A1', b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\00511N01805A0', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521N01309A1', @@ -1083,6 +1113,7 @@ FW_VERSIONS = { b'\xf1\x875Q0907572H \xf1\x890620', b'\xf1\x875Q0907572K \xf1\x890402\xf1\x82\x0101', b'\xf1\x875Q0907572P \xf1\x890682', + b'\xf1\x875Q0907572R \xf1\x890771', ], }, CAR.SKODA_FABIA_MK4: { @@ -1121,6 +1152,7 @@ FW_VERSIONS = { }, CAR.SKODA_KAROQ_MK1: { (Ecu.engine, 0x7e0, None): [ + b'\xf1\x8705E906018P \xf1\x895472', b'\xf1\x8705E906018P \xf1\x896020', b'\xf1\x8705L906022BS\xf1\x890913', ], @@ -1129,14 +1161,17 @@ FW_VERSIONS = { b'\xf1\x870GC300014L \xf1\x892802', ], (Ecu.srs, 0x715, None): [ + b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e1213001211001101131112012100', b'\xf1\x873Q0959655BH\xf1\x890712\xf1\x82\0161213001211001101131122012100', b'\xf1\x873Q0959655DE\xf1\x890731\xf1\x82\x0e1213001211001101131121012J00', ], (Ecu.eps, 0x712, None): [ + b'\xf1\x875Q0910143B \xf1\x892201\xf1\x82\x0563T6090500', b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\00567T6100500', b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T6100700', ], (Ecu.fwdRadar, 0x757, None): [ + b'\xf1\x872Q0907572AB\xf1\x890397', b'\xf1\x872Q0907572M \xf1\x890233', b'\xf1\x872Q0907572T \xf1\x890383', ], @@ -1144,6 +1179,7 @@ FW_VERSIONS = { CAR.SKODA_KODIAQ_MK1: { (Ecu.engine, 0x7e0, None): [ b'\xf1\x8704E906027DD\xf1\x893123', + b'\xf1\x8704E906027NB\xf1\x896517', b'\xf1\x8704L906026DE\xf1\x895418', b'\xf1\x8704L906026EJ\xf1\x893661', b'\xf1\x8704L906026HT\xf1\x893617', @@ -1152,6 +1188,7 @@ FW_VERSIONS = { b'\xf1\x8705E906018DJ\xf1\x891903', b'\xf1\x875NA907115E \xf1\x890003', b'\xf1\x875NA907115E \xf1\x890005', + b'\xf1\x875NA906259E \xf1\x890003', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x870D9300043 \xf1\x895202', @@ -1160,6 +1197,7 @@ FW_VERSIONS = { b'\xf1\x870DL300012N \xf1\x892110', b'\xf1\x870DL300013G \xf1\x892119', b'\xf1\x870GC300014N \xf1\x892801', + b'\xf1\x870GC300019H \xf1\x892806', b'\xf1\x870GC300046Q \xf1\x892802', ], (Ecu.srs, 0x715, None): [ @@ -1169,6 +1207,7 @@ FW_VERSIONS = { b'\xf1\x873Q0959655CN\xf1\x890720\xf1\x82\x0e1213001211001205212112052100', b'\xf1\x873Q0959655CQ\xf1\x890720\xf1\x82\x0e1213111211001205212112052111', b'\xf1\x873Q0959655DJ\xf1\x890731\xf1\x82\x0e1513001511001205232113052J00', + b'\xf1\x875QF959655AT\xf1\x890755\xf1\x82\x1311110011110011111100010200--1121240749', ], (Ecu.eps, 0x712, None): [ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820527T6050405', @@ -1176,6 +1215,7 @@ FW_VERSIONS = { b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820527T6070405', b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T600G500', b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T600G600', + b'\xf1\x875TA907145F \xf1\x891063\xf1\x82\x002LT61A2LOM', ], (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x872Q0907572Q \xf1\x890342', @@ -1264,9 +1304,10 @@ FW_VERSIONS = { b'\xf1\x870D9300012 \xf1\x894940', b'\xf1\x870D9300013A \xf1\x894905', b'\xf1\x870D9300041H \xf1\x894905', - b'\xf1\x870GC300014M \xf1\x892801', - b'\xf1\x870GC300043 \xf1\x892301', b'\xf1\x870D9300043F \xf1\x895202', + b'\xf1\x870GC300014M \xf1\x892801', + b'\xf1\x870GC300019G \xf1\x892803', + b'\xf1\x870GC300043 \xf1\x892301', ], (Ecu.srs, 0x715, None): [ b'\xf1\x875Q0959655AE\xf1\x890130\xf1\x82\x12111200111121001121110012211292221111', @@ -1275,6 +1316,7 @@ FW_VERSIONS = { b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\02331310031313100313131013141319331413100', b'\xf1\x875Q0959655CA\xf1\x890403\xf1\x82\x1331310031313100313151013141319331423100', b'\xf1\x875Q0959655CH\xf1\x890421\xf1\x82\x1333310031313100313152025350539331463100', + b'\xf1\x875Q0959655CH\xf1\x890421\xf1\x82\x1333310031313100313152855372539331463100', ], (Ecu.eps, 0x712, None): [ b'\xf1\x875Q0909143K \xf1\x892033\xf1\x820514UZ070203', diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index a61e12c6d3..090c6d5c1d 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -9,6 +9,7 @@ from common.realtime import sec_since_boot, config_realtime_process, Priority, R from common.profiler import Profiler from common.params import Params, put_nonblocking import cereal.messaging as messaging +from cereal.visionipc import VisionIpcClient, VisionStreamType from common.conversions import Conversions as CV from panda import ALTERNATIVE_EXPERIENCE from system.swaglog import cloudlog @@ -20,7 +21,6 @@ from selfdrive.controls.lib.drive_helpers import VCruiseHelper, get_lag_adjusted from selfdrive.controls.lib.latcontrol import LatControl, MIN_LATERAL_CONTROL_SPEED from selfdrive.controls.lib.longcontrol import LongControl from selfdrive.controls.lib.latcontrol_pid import LatControlPID -from selfdrive.controls.lib.latcontrol_indi import LatControlINDI from selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD from selfdrive.controls.lib.latcontrol_torque import LatControlTorque from selfdrive.controls.lib.events import Events, ET @@ -34,6 +34,7 @@ LANE_DEPARTURE_THRESHOLD = 0.1 REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ +TESTING_CLOSET = "TESTING_CLOSET" in os.environ NOSENSOR = "NOSENSOR" in os.environ IGNORE_PROCESSES = {"loggerd", "encoderd", "statsd", "mapd", "gpxd"} @@ -82,12 +83,10 @@ class Controls: ignore = ['testJoystick'] if SIMULATION: ignore += ['driverCameraState', 'managerState'] - if self.params.get_bool('WideCameraOnly'): - ignore += ['roadCameraState'] self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', 'driverMonitoringState', 'longitudinalPlan', 'lateralPlan', 'liveLocationKalman', 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', 'testJoystick'] + self.camera_packets, - ignore_alive=ignore, ignore_avg_freq=['radarState', 'longitudinalPlan', 'testJoystick']) + ignore_alive=ignore, ignore_avg_freq=['radarState', 'testJoystick']) if CI is None: # wait for one pandaState and one CAN packet @@ -165,8 +164,6 @@ class Controls: self.LaC = LatControlAngle(self.CP, self.CI) elif self.CP.lateralTuning.which() == 'pid': self.LaC = LatControlPID(self.CP, self.CI) - elif self.CP.lateralTuning.which() == 'indi': - self.LaC = LatControlINDI(self.CP, self.CI) elif self.CP.lateralTuning.which() == 'torque': self.LaC = LatControlTorque(self.CP, self.CI) @@ -188,12 +185,14 @@ class Controls: self.events_prev = [] self.current_alert_types = [ET.PERMANENT] self.logged_comm_issue = None + self.not_running_prev = None self.last_actuators = car.CarControl.Actuators.new_message() self.steer_limited = False self.desired_curvature = 0.0 self.desired_curvature_rate = 0.0 self.experimental_mode = False self.v_cruise_helper = VCruiseHelper(self.CP) + self.recalibrating_seen = False self.reverse_acc_change = False @@ -287,7 +286,6 @@ class Controls: if self.sm['deviceState'].freeSpacePercent < 7 and not SIMULATION: # under 7% of space free no enable allowed self.events.add(EventName.outOfSpace) - # TODO: make tici threshold the same if self.sm['deviceState'].memoryUsagePercent > 90 and not SIMULATION: self.events.add(EventName.lowMemory) @@ -311,7 +309,9 @@ class Controls: if cal_status == log.LiveCalibrationData.Status.uncalibrated: self.events.add(EventName.calibrationIncomplete) elif cal_status == log.LiveCalibrationData.Status.recalibrating: - set_offroad_alert("Offroad_Recalibration", True) + if not self.recalibrating_seen: + set_offroad_alert("Offroad_Recalibration", True) + self.recalibrating_seen = True self.events.add(EventName.calibrationRecalibrating) else: self.events.add(EventName.calibrationInvalid) @@ -362,6 +362,9 @@ class Controls: not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning} if self.sm.rcv_frame['managerState'] and (not_running - IGNORE_PROCESSES): self.events.add(EventName.processNotRunning) + if not_running != self.not_running_prev: + cloudlog.event("process_not_running", not_running=not_running, error=True) + self.not_running_prev = not_running else: if not SIMULATION and not self.rk.lagging: if not self.sm.all_alive(self.camera_packets): @@ -403,7 +406,7 @@ class Controls: else: self.logged_comm_issue = None - if not self.sm['liveParameters'].valid: + if not self.sm['liveParameters'].valid and not TESTING_CLOSET: self.events.add(EventName.vehicleModelInvalid) if not self.sm['lateralPlan'].mpcSolutionValid: self.events.add(EventName.plannerError) @@ -467,6 +470,12 @@ class Controls: all_valid = CS.canValid and self.sm.all_checks() timed_out = self.sm.frame * DT_CTRL > (6. if REPLAY else 3.5) if all_valid or timed_out or SIMULATION: + available_streams = VisionIpcClient.available_streams("camerad", block=False) + if VisionStreamType.VISION_STREAM_ROAD not in available_streams: + self.sm.ignore_alive.append('roadCameraState') + if VisionStreamType.VISION_STREAM_WIDE_ROAD not in available_streams: + self.sm.ignore_alive.append('wideRoadCameraState') + if not self.read_only: self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan']) diff --git a/selfdrive/controls/lib/alerts_offroad.json b/selfdrive/controls/lib/alerts_offroad.json index 42d2512e51..9226c94d85 100644 --- a/selfdrive/controls/lib/alerts_offroad.json +++ b/selfdrive/controls/lib/alerts_offroad.json @@ -30,7 +30,7 @@ "severity": 0 }, "Offroad_UnofficialHardware": { - "text": "Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, contact support@comma.ai.", + "text": "Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support.", "severity": 1 }, "Offroad_StorageMissing": { diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py index 6add7cf310..ad996a00de 100644 --- a/selfdrive/controls/lib/events.py +++ b/selfdrive/controls/lib/events.py @@ -1012,12 +1012,6 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { ET.NO_ENTRY: NoEntryAlert("LKAS Fault: Restart the Car"), }, - EventName.brakeUnavailable: { - ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Cruise Fault: Restart the Car"), - ET.PERMANENT: NormalPermanentAlert("Cruise Fault: Restart the car to engage"), - ET.NO_ENTRY: NoEntryAlert("Cruise Fault: Restart the Car"), - }, - EventName.reverseGear: { ET.PERMANENT: Alert( "Reverse\nGear", diff --git a/selfdrive/controls/lib/latcontrol_indi.py b/selfdrive/controls/lib/latcontrol_indi.py deleted file mode 100644 index dca82c6729..0000000000 --- a/selfdrive/controls/lib/latcontrol_indi.py +++ /dev/null @@ -1,120 +0,0 @@ -import math -import numpy as np - -from cereal import log -from common.filter_simple import FirstOrderFilter -from common.numpy_fast import clip, interp -from common.realtime import DT_CTRL -from selfdrive.controls.lib.latcontrol import LatControl - - -class LatControlINDI(LatControl): - def __init__(self, CP, CI): - super().__init__(CP, CI) - self.angle_steers_des = 0. - - A = np.array([[1.0, DT_CTRL, 0.0], - [0.0, 1.0, DT_CTRL], - [0.0, 0.0, 1.0]]) - C = np.array([[1.0, 0.0, 0.0], - [0.0, 1.0, 0.0]]) - - # Q = np.matrix([[1e-2, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 10.0]]) - # R = np.matrix([[1e-2, 0.0], [0.0, 1e3]]) - - # (x, l, K) = control.dare(np.transpose(A), np.transpose(C), Q, R) - # K = np.transpose(K) - K = np.array([[7.30262179e-01, 2.07003658e-04], - [7.29394177e+00, 1.39159419e-02], - [1.71022442e+01, 3.38495381e-02]]) - - self.speed = 0. - - self.K = K - self.A_K = A - np.dot(K, C) - self.x = np.array([[0.], [0.], [0.]]) - - self._RC = (CP.lateralTuning.indi.timeConstantBP, CP.lateralTuning.indi.timeConstantV) - self._G = (CP.lateralTuning.indi.actuatorEffectivenessBP, CP.lateralTuning.indi.actuatorEffectivenessV) - self._outer_loop_gain = (CP.lateralTuning.indi.outerLoopGainBP, CP.lateralTuning.indi.outerLoopGainV) - self._inner_loop_gain = (CP.lateralTuning.indi.innerLoopGainBP, CP.lateralTuning.indi.innerLoopGainV) - - self.steer_filter = FirstOrderFilter(0., self.RC, DT_CTRL) - self.reset() - - @property - def RC(self): - return interp(self.speed, self._RC[0], self._RC[1]) - - @property - def G(self): - return interp(self.speed, self._G[0], self._G[1]) - - @property - def outer_loop_gain(self): - return interp(self.speed, self._outer_loop_gain[0], self._outer_loop_gain[1]) - - @property - def inner_loop_gain(self): - return interp(self.speed, self._inner_loop_gain[0], self._inner_loop_gain[1]) - - def reset(self): - super().reset() - self.steer_filter.x = 0. - self.speed = 0. - - def update(self, active, CS, VM, params, last_actuators, steer_limited, desired_curvature, desired_curvature_rate, llk): - self.speed = CS.vEgo - # Update Kalman filter - y = np.array([[math.radians(CS.steeringAngleDeg)], [math.radians(CS.steeringRateDeg)]]) - self.x = np.dot(self.A_K, self.x) + np.dot(self.K, y) - - indi_log = log.ControlsState.LateralINDIState.new_message() - indi_log.steeringAngleDeg = math.degrees(self.x[0]) - indi_log.steeringRateDeg = math.degrees(self.x[1]) - indi_log.steeringAccelDeg = math.degrees(self.x[2]) - - steers_des = VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll) - steers_des += math.radians(params.angleOffsetDeg) - indi_log.steeringAngleDesiredDeg = math.degrees(steers_des) - - # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature - rate_des = VM.get_steer_from_curvature(-desired_curvature_rate, CS.vEgo, 0) - indi_log.steeringRateDesiredDeg = math.degrees(rate_des) - - if not active: - indi_log.active = False - self.steer_filter.x = 0.0 - output_steer = 0 - else: - # Expected actuator value - self.steer_filter.update_alpha(self.RC) - self.steer_filter.update(last_actuators.steer) - - # Compute acceleration error - rate_sp = self.outer_loop_gain * (steers_des - self.x[0]) + rate_des - accel_sp = self.inner_loop_gain * (rate_sp - self.x[1]) - accel_error = accel_sp - self.x[2] - - # Compute change in actuator - g_inv = 1. / self.G - delta_u = g_inv * accel_error - - # If steering pressed, only allow wind down - if CS.steeringPressed and (delta_u * last_actuators.steer > 0): - delta_u = 0 - - output_steer = self.steer_filter.x + delta_u - - output_steer = clip(output_steer, -self.steer_max, self.steer_max) - - indi_log.active = True - indi_log.rateSetPoint = float(rate_sp) - indi_log.accelSetPoint = float(accel_sp) - indi_log.accelError = float(accel_error) - indi_log.delayedOutput = float(self.steer_filter.x) - indi_log.delta = float(delta_u) - indi_log.output = float(output_steer) - indi_log.saturated = self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS, steer_limited) - - return float(output_steer), float(steers_des), indi_log diff --git a/selfdrive/controls/lib/lateral_planner.py b/selfdrive/controls/lib/lateral_planner.py index baf096221b..655da4cf6a 100644 --- a/selfdrive/controls/lib/lateral_planner.py +++ b/selfdrive/controls/lib/lateral_planner.py @@ -140,7 +140,7 @@ class LateralPlanner: self.last_cloudlog_t = t cloudlog.warning("Lateral mpc - nan: True") - if self.lat_mpc.cost > 20000. or mpc_nans: + if self.lat_mpc.cost > 1e6 or mpc_nans: self.solution_invalid_cnt += 1 else: self.solution_invalid_cnt = 0 diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index ca3b8d512d..cfe2ed8ba4 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -183,6 +183,7 @@ def gen_long_ocp(): ocp.constraints.x0 = x0 ocp.parameter_values = np.array([-1.2, 1.2, 0.0, 0.0, get_T_FOLLOW(), LEAD_DANGER_FACTOR]) + # We put all constraint cost weights to 0 and only set them at runtime cost_weights = np.zeros(CONSTR_DIM) ocp.cost.zl = cost_weights diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index b8f942251d..2f8d41ec2e 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -2,10 +2,10 @@ import math import numpy as np from common.numpy_fast import clip, interp -from cereal import log +from common.params import Params +from cereal import car, log import cereal.messaging as messaging -from cereal import car from common.conversions import Conversions as CV from common.filter_simple import FirstOrderFilter from common.realtime import DT_MDL @@ -65,6 +65,10 @@ class LongitudinalPlanner: self.a_desired_trajectory = np.zeros(CONTROL_N) self.j_desired_trajectory = np.zeros(CONTROL_N) self.solverExecutionTime = 0.0 + self.params = Params() + self.param_read_counter = 0 + self.read_param() + self.personality = log.LongitudinalPersonality.standard self.cruise_source = 'cruise' self.vision_turn_controller = VisionTurnController(CP) @@ -73,7 +77,12 @@ class LongitudinalPlanner: self.turn_speed_controller = TurnSpeedController() self.read_gac(gac_tr=3) - self.personality = log.LongitudinalPersonality.standard + + def read_param(self): + try: + self.personality = int(self.params.get('LongitudinalPersonality')) + except (ValueError, TypeError): + self.personality = log.LongitudinalPersonality.standard def read_gac(self, gac_tr=3): gac_tr = clip(gac_tr, 1, 3) @@ -96,6 +105,9 @@ class LongitudinalPlanner: return x, v, a, j def update(self, sm): + if self.param_read_counter % 50 == 0: + self.read_param() + self.param_read_counter += 1 self.read_gac(gac_tr=sm['carState'].gapAdjustCruiseTr) self.mpc.mode = 'blended' if sm['controlsState'].experimentalMode else 'acc' diff --git a/selfdrive/controls/lib/radar_helpers.py b/selfdrive/controls/lib/radar_helpers.py index 4bb0179267..4184340dc5 100644 --- a/selfdrive/controls/lib/radar_helpers.py +++ b/selfdrive/controls/lib/radar_helpers.py @@ -130,15 +130,16 @@ class Cluster(): "aLeadTau": float(self.aLeadTau) } - def get_RadarState_from_vision(self, lead_msg, v_ego): + def get_RadarState_from_vision(self, lead_msg, v_ego, model_v_ego): + lead_v_rel_pred = lead_msg.v[0] - model_v_ego return { "dRel": float(lead_msg.x[0] - RADAR_TO_CAMERA), "yRel": float(-lead_msg.y[0]), - "vRel": float(lead_msg.v[0] - v_ego), - "vLead": float(lead_msg.v[0]), - "vLeadK": float(lead_msg.v[0]), - "aLeadK": float(0), - "aLeadTau": _LEAD_ACCEL_TAU, + "vRel": float(lead_v_rel_pred), + "vLead": float(v_ego + lead_v_rel_pred), + "vLeadK": float(v_ego + lead_v_rel_pred), + "aLeadK": 0.0, + "aLeadTau": 0.3, "fcw": False, "modelProb": float(lead_msg.prob), "radar": False, diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/lib/tests/test_latcontrol.py index 993774f719..b504b3d125 100755 --- a/selfdrive/controls/lib/tests/test_latcontrol.py +++ b/selfdrive/controls/lib/tests/test_latcontrol.py @@ -10,14 +10,13 @@ from selfdrive.car.toyota.values import CAR as TOYOTA from selfdrive.car.nissan.values import CAR as NISSAN from selfdrive.controls.lib.latcontrol_pid import LatControlPID from selfdrive.controls.lib.latcontrol_torque import LatControlTorque -from selfdrive.controls.lib.latcontrol_indi import LatControlINDI from selfdrive.controls.lib.latcontrol_angle import LatControlAngle from selfdrive.controls.lib.vehicle_model import VehicleModel class TestLatControl(unittest.TestCase): - @parameterized.expand([(HONDA.CIVIC, LatControlPID), (TOYOTA.RAV4, LatControlTorque), (TOYOTA.PRIUS, LatControlINDI), (NISSAN.LEAF, LatControlAngle)]) + @parameterized.expand([(HONDA.CIVIC, LatControlPID), (TOYOTA.RAV4, LatControlTorque), (NISSAN.LEAF, LatControlAngle)]) def test_saturation(self, car_name, controller): CarInterface, CarController, CarState = interfaces[car_name] CP = CarInterface.get_non_essential_params(car_name) diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index 34f0f274fe..521cea816f 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -64,7 +64,7 @@ def match_vision_to_cluster(v_ego, lead, clusters): return None -def get_lead(v_ego, ready, clusters, lead_msg, low_speed_override=True): +def get_lead(v_ego, ready, clusters, lead_msg, model_v_ego, low_speed_override=True): # Determine leads, this is where the essential logic happens if len(clusters) > 0 and ready and lead_msg.prob > .5: cluster = match_vision_to_cluster(v_ego, lead_msg, clusters) @@ -75,7 +75,7 @@ def get_lead(v_ego, ready, clusters, lead_msg, low_speed_override=True): if cluster is not None: lead_dict = cluster.get_RadarState(lead_msg.prob) elif (cluster is None) and ready and (lead_msg.prob > .5): - lead_dict = Cluster().get_RadarState_from_vision(lead_msg, v_ego) + lead_dict = Cluster().get_RadarState_from_vision(lead_msg, v_ego, model_v_ego) if low_speed_override: low_speed_clusters = [c for c in clusters if c.potential_low_speed_lead(v_ego)] @@ -168,10 +168,14 @@ class RadarD(): radarState.radarErrors = list(rr.errors) radarState.carStateMonoTime = sm.logMonoTime['carState'] + if len(sm['modelV2'].temporalPose.trans): + model_v_ego = sm['modelV2'].temporalPose.trans[0] + else: + model_v_ego = self.v_ego leads_v3 = sm['modelV2'].leadsV3 if len(leads_v3) > 1: - radarState.leadOne = get_lead(self.v_ego, self.ready, clusters, leads_v3[0], low_speed_override=True) - radarState.leadTwo = get_lead(self.v_ego, self.ready, clusters, leads_v3[1], low_speed_override=False) + radarState.leadOne = get_lead(self.v_ego, self.ready, clusters, leads_v3[0], model_v_ego, low_speed_override=True) + radarState.leadTwo = get_lead(self.v_ego, self.ready, clusters, leads_v3[1], model_v_ego, low_speed_override=False) return dat diff --git a/selfdrive/controls/tests/test_alerts.py b/selfdrive/controls/tests/test_alerts.py index 9ed7eee122..60c080163f 100755 --- a/selfdrive/controls/tests/test_alerts.py +++ b/selfdrive/controls/tests/test_alerts.py @@ -6,11 +6,12 @@ import random from PIL import Image, ImageDraw, ImageFont from cereal import log, car +from cereal.messaging import SubMaster from common.basedir import BASEDIR from common.params import Params from selfdrive.controls.lib.events import Alert, EVENTS, ET from selfdrive.controls.lib.alertmanager import set_offroad_alert -from selfdrive.test.process_replay.process_replay import FakeSubMaster, CONFIGS +from selfdrive.test.process_replay.process_replay import CONFIGS AlertSize = log.ControlsState.AlertSize @@ -34,7 +35,7 @@ class TestAlerts(unittest.TestCase): cls.CS = car.CarState.new_message() cls.CP = car.CarParams.new_message() cfg = [c for c in CONFIGS if c.proc_name == 'controlsd'][0] - cls.sm = FakeSubMaster(cfg.pub_sub.keys()) + cls.sm = SubMaster(cfg.pubs) def test_events_defined(self): # Ensure all events in capnp schema are defined in events.py diff --git a/selfdrive/debug/count_events.py b/selfdrive/debug/count_events.py index 42b671e5e3..a81d797b89 100755 --- a/selfdrive/debug/count_events.py +++ b/selfdrive/debug/count_events.py @@ -23,6 +23,7 @@ if __name__ == "__main__": alerts: List[Tuple[float, str]] = [] start_time = math.inf end_time = -math.inf + ignition_off = None for q in tqdm(r.qlog_paths()): if q is None: continue @@ -38,6 +39,11 @@ if __name__ == "__main__": if len(alerts) == 0 or alerts[-1][1] != msg.controlsState.alertType: t = (msg.logMonoTime - start_time) / 1e9 alerts.append((t, msg.controlsState.alertType)) + elif msg.which() == 'pandaStates': + if ignition_off is None: + ign = any(ps.ignitionLine or ps.ignitionCan for ps in msg.pandaStates) + if not ign: + ignition_off = msg.logMonoTime elif msg.which() in cams: cnt_cameras[msg.which()] += 1 @@ -64,6 +70,9 @@ if __name__ == "__main__": print("Alerts") for t, a in alerts: print(f"{t:8.2f} {a}") + if ignition_off is not None: + ignition_off = round((ignition_off - start_time) / 1e9, 2) + print("Ignition off at", ignition_off) print("\n") print("Route duration", datetime.timedelta(seconds=duration)) diff --git a/selfdrive/debug/run_process_on_route.py b/selfdrive/debug/run_process_on_route.py index 63b0733bba..0ea76d260f 100755 --- a/selfdrive/debug/run_process_on_route.py +++ b/selfdrive/debug/run_process_on_route.py @@ -2,10 +2,10 @@ import argparse -from selfdrive.test.process_replay.compare_logs import save_log from selfdrive.test.process_replay.process_replay import CONFIGS, replay_process from tools.lib.logreader import MultiLogIterator from tools.lib.route import Route +from tools.lib.helpers import save_log if __name__ == "__main__": parser = argparse.ArgumentParser(description="Run process on route and create new logs", diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index dc4ffbb1f7..c9d9340a2d 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -24,6 +24,8 @@ MIN_SPEED_FILTER = 15 * CV.MPH_TO_MS MAX_VEL_ANGLE_STD = np.radians(0.25) MAX_YAW_RATE_FILTER = np.radians(2) # per second +MAX_HEIGHT_STD = np.exp(-3.5) + # This is at model frequency, blocks needed for efficiency SMOOTH_CYCLES = 10 BLOCK_SIZE = 100 @@ -32,9 +34,10 @@ INPUTS_WANTED = 50 # We want a little bit more than we need for stability MAX_ALLOWED_SPREAD = np.radians(2) RPY_INIT = np.array([0.0,0.0,0.0]) WIDE_FROM_DEVICE_EULER_INIT = np.array([0.0, 0.0, 0.0]) +HEIGHT_INIT = np.array([1.22]) -# These values are needed to accommodate biggest modelframe -PITCH_LIMITS = np.array([-0.09074112085129739, 0.14907572052989657]) +# These values are needed to accommodate the model frame in the narrow cam of the C3 +PITCH_LIMITS = np.array([-0.09074112085129739, 0.17]) YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235]) DEBUG = os.getenv("DEBUG") is not None @@ -50,6 +53,8 @@ def sanity_clip(rpy: np.ndarray) -> np.ndarray: np.clip(rpy[1], PITCH_LIMITS[0] - .005, PITCH_LIMITS[1] + .005), np.clip(rpy[2], YAW_LIMITS[0] - .005, YAW_LIMITS[1] + .005)]) +def moving_avg_with_linear_decay(prev_mean: np.ndarray, new_val: np.ndarray, idx: int, block_size: float) -> np.ndarray: + return (idx*prev_mean + (block_size - idx) * new_val) / block_size class Calibrator: def __init__(self, param_put: bool = False): @@ -62,6 +67,7 @@ class Calibrator: calibration_params = params.get("CalibrationParams") rpy_init = RPY_INIT wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT + height = HEIGHT_INIT valid_blocks = 0 self.cal_status = log.LiveCalibrationData.Status.uncalibrated @@ -71,21 +77,28 @@ class Calibrator: rpy_init = np.array(msg.liveCalibration.rpyCalib) valid_blocks = msg.liveCalibration.validBlocks wide_from_device_euler = np.array(msg.liveCalibration.wideFromDeviceEuler) + height = np.array(msg.liveCalibration.height) except Exception: cloudlog.exception("Error reading cached CalibrationParams") - self.reset(rpy_init, valid_blocks, wide_from_device_euler) + self.reset(rpy_init, valid_blocks, wide_from_device_euler, height) self.update_status() def reset(self, rpy_init: np.ndarray = RPY_INIT, valid_blocks: int = 0, wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_INIT, + height_init: np.ndarray = HEIGHT_INIT, smooth_from: Optional[np.ndarray] = None) -> None: if not np.isfinite(rpy_init).all(): self.rpy = RPY_INIT.copy() else: self.rpy = rpy_init.copy() + if not np.isfinite(height_init).all() or len(height_init) != 1: + self.height = HEIGHT_INIT.copy() + else: + self.height = height_init.copy() + if not np.isfinite(wide_from_device_euler_init).all() or len(wide_from_device_euler_init) != 3: self.wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT.copy() else: @@ -98,6 +111,7 @@ class Calibrator: self.rpys = np.tile(self.rpy, (INPUTS_WANTED, 1)) self.wide_from_device_eulers = np.tile(self.wide_from_device_euler, (INPUTS_WANTED, 1)) + self.heights = np.tile(self.height, (INPUTS_WANTED, 1)) self.idx = 0 self.block_idx = 0 @@ -120,6 +134,7 @@ class Calibrator: valid_idxs = self.get_valid_idxs() if valid_idxs: self.wide_from_device_euler = np.mean(self.wide_from_device_eulers[valid_idxs], axis=0) + self.height = np.mean(self.heights[valid_idxs], axis=0) rpys = self.rpys[valid_idxs] self.rpy = np.mean(rpys, axis=0) max_rpy_calib = np.array(np.max(rpys, axis=0)) @@ -140,6 +155,7 @@ class Calibrator: # If spread is too high, assume mounting was changed and reset to last block. # Make the transition smooth. Abrupt transitions are not good for feedback loop through supercombo model. + # TODO: add height spread check with smooth transition too if max(self.calib_spread) > MAX_ALLOWED_SPREAD and self.cal_status == log.LiveCalibrationData.Status.calibrated: self.reset(self.rpys[self.block_idx - 1], valid_blocks=1, smooth_from=self.rpy) self.cal_status = log.LiveCalibrationData.Status.recalibrating @@ -160,13 +176,21 @@ class Calibrator: def handle_cam_odom(self, trans: List[float], rot: List[float], wide_from_device_euler: List[float], - trans_std: List[float]) -> Optional[np.ndarray]: + trans_std: List[float], + road_transform_trans: List[float], + road_transform_trans_std: List[float]) -> Optional[np.ndarray]: self.old_rpy_weight = max(0.0, self.old_rpy_weight - 1/SMOOTH_CYCLES) straight_and_fast = ((self.v_ego > MIN_SPEED_FILTER) and (trans[0] > MIN_SPEED_FILTER) and (abs(rot[2]) < MAX_YAW_RATE_FILTER)) angle_std_threshold = MAX_VEL_ANGLE_STD - certain_if_calib = ((np.arctan2(trans_std[1], trans[0]) < angle_std_threshold) or - (self.valid_blocks < INPUTS_NEEDED)) + height_std_threshold = MAX_HEIGHT_STD + rpy_certain = np.arctan2(trans_std[1], trans[0]) < angle_std_threshold + if len(road_transform_trans_std) == 3: + height_certain = road_transform_trans_std[2] < height_std_threshold + else: + height_certain = True + + certain_if_calib = (rpy_certain and height_certain) or (self.valid_blocks < INPUTS_NEEDED) if not (straight_and_fast and certain_if_calib): return None @@ -180,10 +204,16 @@ class Calibrator: new_wide_from_device_euler = np.array(wide_from_device_euler) else: new_wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT - self.rpys[self.block_idx] = (self.idx*self.rpys[self.block_idx] + - (BLOCK_SIZE - self.idx) * new_rpy) / float(BLOCK_SIZE) - self.wide_from_device_eulers[self.block_idx] = (self.idx*self.wide_from_device_eulers[self.block_idx] + - (BLOCK_SIZE - self.idx) * new_wide_from_device_euler) / float(BLOCK_SIZE) + + if (len(road_transform_trans) == 3): + new_height = np.array([road_transform_trans[2]]) + else: + new_height = HEIGHT_INIT + + self.rpys[self.block_idx] = moving_avg_with_linear_decay(self.rpys[self.block_idx], new_rpy, self.idx, float(BLOCK_SIZE)) + self.wide_from_device_eulers[self.block_idx] = moving_avg_with_linear_decay(self.wide_from_device_eulers[self.block_idx], new_wide_from_device_euler, self.idx, float(BLOCK_SIZE)) + self.heights[self.block_idx] = moving_avg_with_linear_decay(self.heights[self.block_idx], new_height, self.idx, float(BLOCK_SIZE)) + self.idx = (self.idx + 1) % BLOCK_SIZE if self.idx == 0: self.block_idx += 1 @@ -206,6 +236,7 @@ class Calibrator: liveCalibration.rpyCalib = smooth_rpy.tolist() liveCalibration.rpyCalibSpread = self.calib_spread.tolist() liveCalibration.wideFromDeviceEuler = self.wide_from_device_euler.tolist() + liveCalibration.height = self.height.tolist() if self.not_car: liveCalibration.validBlocks = INPUTS_NEEDED @@ -243,7 +274,9 @@ def calibrationd_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[m new_rpy = calibrator.handle_cam_odom(sm['cameraOdometry'].trans, sm['cameraOdometry'].rot, sm['cameraOdometry'].wideFromDeviceEuler, - sm['cameraOdometry'].transStd) + sm['cameraOdometry'].transStd, + sm['cameraOdometry'].roadTransformTrans, + sm['cameraOdometry'].roadTransformTransStd) if DEBUG and new_rpy is not None: print('got new rpy', new_rpy) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 71b81cc307..ed922f89c3 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -192,15 +192,15 @@ class Laikad: def is_good_report(self, gnss_msg): if gnss_msg.which() == 'drMeasurementReport' and self.use_qcom: - constellation_id = ConstellationId.from_qcom_source(gnss_msg.drMeasurementReport.source) # TODO: Understand and use remaining unknown constellations try: + constellation_id = ConstellationId.from_qcom_source(gnss_msg.drMeasurementReport.source) good_constellation = constellation_id in [ConstellationId.GPS, ConstellationId.SBAS, ConstellationId.GLONASS] + report_time = self.gps_time_from_qcom_report(gnss_msg) except NotImplementedError: - good_constellation = False + return False # Garbage timestamps with week > 32767 are sometimes sent by module. # This is an issue with gpsTime and GLONASS time. - report_time = self.gps_time_from_qcom_report(gnss_msg) good_week = report_time.week < np.iinfo(np.int16).max return good_constellation and good_week elif gnss_msg.which() == 'measurementReport' and not self.use_qcom: @@ -457,12 +457,10 @@ def main(sm=None, pm=None): pm = messaging.PubMaster(['gnssMeasurements']) # disable until set as main gps source, to better analyze startup time + # TODO ensure low CPU usage before enabling use_internet = False # "LAIKAD_NO_INTERNET" not in os.environ replay = "REPLAY" in os.environ - if replay: - use_internet = True - laikad = Laikad(save_ephemeris=not replay, auto_fetch_navs=use_internet, use_qcom=use_qcom) while True: diff --git a/selfdrive/locationd/liblocationd.cc b/selfdrive/locationd/liblocationd.cc index 32fec7724a..6f298deab6 100755 --- a/selfdrive/locationd/liblocationd.cc +++ b/selfdrive/locationd/liblocationd.cc @@ -4,7 +4,7 @@ extern "C" { typedef Localizer* Localizer_t; Localizer *localizer_init(bool has_ublox) { - return new Localizer(has_ublox); + return new Localizer(has_ublox ? LocalizerGnssSource::UBLOX : LocalizerGnssSource::QCOM); } void localizer_get_message_bytes(Localizer *localizer, bool inputsOK, bool sensorsOK, bool gpsOK, bool msgValid, diff --git a/selfdrive/locationd/locationd.cc b/selfdrive/locationd/locationd.cc index 9ee9e33fb4..3616f0af85 100755 --- a/selfdrive/locationd/locationd.cc +++ b/selfdrive/locationd/locationd.cc @@ -35,6 +35,8 @@ const float GPS_VEL_STD_RESET_THRESHOLD = 0.5; const float GPS_ORIENTATION_ERROR_RESET_THRESHOLD = 1.0; const int GPS_ORIENTATION_ERROR_RESET_CNT = 3; +const bool DEBUG = getenv("DEBUG") != nullptr && std::string(getenv("DEBUG")) != "0"; + static VectorXd floatlist2vector(const capnp::List::Reader& floatlist) { VectorXd res(floatlist.size()); for (int i = 0; i < floatlist.size(); i++) { @@ -68,7 +70,7 @@ static VectorXd rotate_std(const MatrixXdr& rot_matrix, const VectorXd& std_in) return rotate_cov(rot_matrix, std_in.array().square().matrix().asDiagonal()).diagonal().array().sqrt(); } -Localizer::Localizer() { +Localizer::Localizer(LocalizerGnssSource gnss_source) { this->kf = std::make_unique(); this->reset_kalman(); @@ -82,10 +84,9 @@ Localizer::Localizer() { VectorXd ecef_pos = this->kf->get_x().segment(STATE_ECEF_POS_START); this->converter = std::make_unique((ECEF) { .x = ecef_pos[0], .y = ecef_pos[1], .z = ecef_pos[2] }); + this->configure_gnss_source(gnss_source); } -Localizer::Localizer(bool has_ublox) : Localizer() { ublox_available = has_ublox; } - void Localizer::build_live_location(cereal::LiveLocationKalman::Builder& fix) { VectorXd predicted_state = this->kf->get_x(); MatrixXdr predicted_cov = this->kf->get_P(); @@ -161,6 +162,9 @@ void Localizer::build_live_location(cereal::LiveLocationKalman::Builder& fix) { init_measurement(fix.initVelocityCalibrated(), vel_calib, vel_calib_std, this->calibrated); init_measurement(fix.initAngularVelocityCalibrated(), ang_vel_calib, ang_vel_calib_std, this->calibrated); init_measurement(fix.initAccelerationCalibrated(), acc_calib, acc_calib_std, this->calibrated); + if (DEBUG) { + init_measurement(fix.initFilterState(), predicted_state, predicted_std, true); + } double old_mean = 0.0, new_mean = 0.0; int i = 0; @@ -320,12 +324,7 @@ void Localizer::handle_gps(double current_time, const cereal::GpsLocationData::R VectorXd ecef_pos = this->converter->ned2ecef({ 0.0, 0.0, 0.0 }).to_vector(); VectorXd ecef_vel = this->converter->ned2ecef({ log.getVNED()[0], log.getVNED()[1], log.getVNED()[2] }).to_vector() - ecef_pos; - float ecef_pos_std; - if (ublox_available) { - ecef_pos_std = std::sqrt(std::pow(log.getAccuracy(), 2) + std::pow(log.getVerticalAccuracy(), 2)); - } else { - ecef_pos_std = std::sqrt(3 * std::pow(log.getVerticalAccuracy(), 2)); - } + float ecef_pos_std = std::sqrt(this->gps_variance_factor * std::pow(log.getAccuracy(), 2) + this->gps_vertical_variance_factor * std::pow(log.getVerticalAccuracy(), 2)); MatrixXdr ecef_pos_R = Vector3d::Constant(std::pow(this->gps_std_factor * ecef_pos_std, 2)).asDiagonal(); MatrixXdr ecef_vel_R = Vector3d::Constant(std::pow(this->gps_std_factor * log.getSpeedAccuracy(), 2)).asDiagonal(); @@ -367,10 +366,7 @@ void Localizer::handle_gnss(double current_time, const cereal::GnssMeasurements: } double sensor_time = log.getMeasTime() * 1e-9; - if (ublox_available) - sensor_time -= GPS_UBLOX_SENSOR_TIME_OFFSET; - else - sensor_time -= GPS_QUECTEL_SENSOR_TIME_OFFSET; + sensor_time -= this->gps_time_offset; auto ecef_pos_v = log.getPositionECEF().getValue(); VectorXd ecef_pos = Vector3d(ecef_pos_v[0], ecef_pos_v[1], ecef_pos_v[2]); @@ -659,16 +655,33 @@ void Localizer::determine_gps_mode(double current_time) { } } -int Localizer::locationd_thread() { - ublox_available = Params().getBool("UbloxAvailable", true); - const char* gps_location_socket; - if (ublox_available) { - gps_location_socket = "gpsLocationExternal"; +void Localizer::configure_gnss_source(LocalizerGnssSource source) { + this->gnss_source = source; + if (source == LocalizerGnssSource::UBLOX) { this->gps_std_factor = 10.0; + this->gps_variance_factor = 1.0; + this->gps_vertical_variance_factor = 1.0; + this->gps_time_offset = GPS_UBLOX_SENSOR_TIME_OFFSET; } else { - gps_location_socket = "gpsLocation"; this->gps_std_factor = 2.0; + this->gps_variance_factor = 0.0; + this->gps_vertical_variance_factor = 3.0; + this->gps_time_offset = GPS_QUECTEL_SENSOR_TIME_OFFSET; } +} + +int Localizer::locationd_thread() { + LocalizerGnssSource source; + const char* gps_location_socket; + if (Params().getBool("UbloxAvailable", true)) { + source = LocalizerGnssSource::UBLOX; + gps_location_socket = "gpsLocationExternal"; + } else { + source = LocalizerGnssSource::QCOM; + gps_location_socket = "gpsLocation"; + } + + this->configure_gnss_source(source); const std::initializer_list service_list = {gps_location_socket, "cameraOdometry", "liveCalibration", "carState", "carParams", "accelerometer", "gyroscope"}; diff --git a/selfdrive/locationd/locationd.h b/selfdrive/locationd/locationd.h index e2b2096afc..e8f2f04a2c 100755 --- a/selfdrive/locationd/locationd.h +++ b/selfdrive/locationd/locationd.h @@ -21,10 +21,13 @@ #define POSENET_STD_HIST_HALF 20 +enum LocalizerGnssSource { + UBLOX, QCOM +}; + class Localizer { public: - Localizer(); - Localizer(bool has_ublox); + Localizer(LocalizerGnssSource gnss_source = LocalizerGnssSource::UBLOX); int locationd_thread(); @@ -81,10 +84,15 @@ private: double first_valid_log_time = NAN; double ttff = NAN; double last_gps_msg = 0; - bool ublox_available = true; + LocalizerGnssSource gnss_source; bool observation_timings_invalid = false; std::map observation_values_invalid; bool standstill = true; int32_t orientation_reset_count = 0; float gps_std_factor; + float gps_variance_factor; + float gps_vertical_variance_factor; + double gps_time_offset; + + void configure_gnss_source(LocalizerGnssSource source); }; diff --git a/selfdrive/locationd/paramsd.py b/selfdrive/locationd/paramsd.py index e13538ff11..8980994395 100755 --- a/selfdrive/locationd/paramsd.py +++ b/selfdrive/locationd/paramsd.py @@ -17,8 +17,11 @@ from system.swaglog import cloudlog MAX_ANGLE_OFFSET_DELTA = 20 * DT_MDL # Max 20 deg/s ROLL_MAX_DELTA = math.radians(20.0) * DT_MDL # 20deg in 1 second is well within curvature limits ROLL_MIN, ROLL_MAX = math.radians(-10), math.radians(10) +ROLL_LOWERED_MAX = math.radians(8) ROLL_STD_MAX = math.radians(1.5) LATERAL_ACC_SENSOR_THRESHOLD = 4.0 +OFFSET_MAX = 10.0 +OFFSET_LOWERED_MAX = 8.0 class ParamsLearner: @@ -103,6 +106,14 @@ class ParamsLearner: self.kf.filter.reset_rewind() +def check_valid_with_hysteresis(current_valid: bool, val: float, threshold: float, lowered_threshold: float): + if current_valid: + current_valid = abs(val) < threshold + else: + current_valid = abs(val) < lowered_threshold + return current_valid + + def main(sm=None, pm=None): config_realtime_process([0, 1, 2, 3], 5) @@ -131,10 +142,8 @@ def main(sm=None, pm=None): # Check if starting values are sane if params is not None: try: - angle_offset_sane = abs(params.get('angleOffsetAverageDeg')) < 10.0 steer_ratio_sane = min_sr <= params['steerRatio'] <= max_sr - params_sane = angle_offset_sane and steer_ratio_sane - if not params_sane: + if not steer_ratio_sane: cloudlog.info(f"Invalid starting values found {params}") params = None except Exception as e: @@ -158,6 +167,9 @@ def main(sm=None, pm=None): angle_offset_average = params['angleOffsetAverageDeg'] angle_offset = angle_offset_average roll = 0.0 + avg_offset_valid = True + total_offset_valid = True + roll_valid = True while True: sm.update() @@ -181,6 +193,9 @@ def main(sm=None, pm=None): roll_std = float(P[States.ROAD_ROLL]) # Account for the opposite signs of the yaw rates sensors_valid = bool(abs(learner.speed * (x[States.YAW_RATE] + learner.yaw_rate)) < LATERAL_ACC_SENSOR_THRESHOLD) + avg_offset_valid = check_valid_with_hysteresis(avg_offset_valid, angle_offset_average, OFFSET_MAX, OFFSET_LOWERED_MAX) + total_offset_valid = check_valid_with_hysteresis(total_offset_valid, angle_offset, OFFSET_MAX, OFFSET_LOWERED_MAX) + roll_valid = check_valid_with_hysteresis(roll_valid, roll, ROLL_MAX, ROLL_LOWERED_MAX) msg = messaging.new_message('liveParameters') @@ -193,9 +208,9 @@ def main(sm=None, pm=None): liveParameters.angleOffsetAverageDeg = angle_offset_average liveParameters.angleOffsetDeg = angle_offset liveParameters.valid = all(( - abs(liveParameters.angleOffsetAverageDeg) < 10.0, - abs(liveParameters.angleOffsetDeg) < 10.0, - abs(liveParameters.roll) < ROLL_MAX, + avg_offset_valid, + total_offset_valid, + roll_valid, roll_std < ROLL_STD_MAX, 0.2 <= liveParameters.stiffnessFactor <= 5.0, min_sr <= liveParameters.steerRatio <= max_sr, diff --git a/selfdrive/locationd/test/test_calibrationd.py b/selfdrive/locationd/test/test_calibrationd.py index 18125cc43f..96f996413d 100755 --- a/selfdrive/locationd/test/test_calibrationd.py +++ b/selfdrive/locationd/test/test_calibrationd.py @@ -7,7 +7,7 @@ import numpy as np import cereal.messaging as messaging from cereal import log from common.params import Params -from selfdrive.locationd.calibrationd import Calibrator, INPUTS_NEEDED, INPUTS_WANTED, BLOCK_SIZE, MIN_SPEED_FILTER, MAX_YAW_RATE_FILTER, SMOOTH_CYCLES +from selfdrive.locationd.calibrationd import Calibrator, INPUTS_NEEDED, INPUTS_WANTED, BLOCK_SIZE, MIN_SPEED_FILTER, MAX_YAW_RATE_FILTER, SMOOTH_CYCLES, HEIGHT_INIT class TestCalibrationd(unittest.TestCase): @@ -16,10 +16,12 @@ class TestCalibrationd(unittest.TestCase): msg = messaging.new_message('liveCalibration') msg.liveCalibration.validBlocks = random.randint(1, 10) msg.liveCalibration.rpyCalib = [random.random() for _ in range(3)] + msg.liveCalibration.height = [random.random() for _ in range(1)] Params().put("CalibrationParams", msg.to_bytes()) c = Calibrator(param_put=True) np.testing.assert_allclose(msg.liveCalibration.rpyCalib, c.rpy) + np.testing.assert_allclose(msg.liveCalibration.height, c.height) self.assertEqual(msg.liveCalibration.validBlocks, c.valid_blocks) @@ -27,51 +29,79 @@ class TestCalibrationd(unittest.TestCase): c = Calibrator(param_put=False) for _ in range(BLOCK_SIZE * INPUTS_WANTED): c.handle_v_ego(MIN_SPEED_FILTER + 1) - c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + c.handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3], + [0.0, 0.0, HEIGHT_INIT.item()], [1e-3, 1e-3, 1e-3]) self.assertEqual(c.valid_blocks, INPUTS_WANTED) np.testing.assert_allclose(c.rpy, np.zeros(3)) + np.testing.assert_allclose(c.height, HEIGHT_INIT) c.reset() + def test_calibration_low_speed_reject(self): c = Calibrator(param_put=False) for _ in range(BLOCK_SIZE * INPUTS_WANTED): c.handle_v_ego(MIN_SPEED_FILTER - 1) - c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + c.handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3], + [0.0, 0.0, HEIGHT_INIT.item()], [1e-3, 1e-3, 1e-3]) for _ in range(BLOCK_SIZE * INPUTS_WANTED): c.handle_v_ego(MIN_SPEED_FILTER + 1) - c. handle_cam_odom([MIN_SPEED_FILTER - 1, 0.0, 0.0], + c.handle_cam_odom([MIN_SPEED_FILTER - 1, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3], + [0.0, 0.0, HEIGHT_INIT.item()], [1e-3, 1e-3, 1e-3]) self.assertEqual(c.valid_blocks, 0) np.testing.assert_allclose(c.rpy, np.zeros(3)) + np.testing.assert_allclose(c.height, HEIGHT_INIT) def test_calibration_yaw_rate_reject(self): c = Calibrator(param_put=False) for _ in range(BLOCK_SIZE * INPUTS_WANTED): c.handle_v_ego(MIN_SPEED_FILTER + 1) - c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + c.handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], [0.0, 0.0, MAX_YAW_RATE_FILTER ], [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3], + [0.0, 0.0, HEIGHT_INIT.item()], [1e-3, 1e-3, 1e-3]) self.assertEqual(c.valid_blocks, 0) np.testing.assert_allclose(c.rpy, np.zeros(3)) + np.testing.assert_allclose(c.height, HEIGHT_INIT) + - def test_calibration_speed_std_reject(self): c = Calibrator(param_put=False) for _ in range(BLOCK_SIZE * INPUTS_WANTED): c.handle_v_ego(MIN_SPEED_FILTER + 1) - c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + c.handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], + [1e3, 1e3, 1e3], + [0.0, 0.0, HEIGHT_INIT.item()], + [1e-3, 1e-3, 1e-3]) + self.assertEqual(c.valid_blocks, INPUTS_NEEDED) + np.testing.assert_allclose(c.rpy, np.zeros(3)) + + + def test_calibration_speed_std_height_reject(self): + c = Calibrator(param_put=False) + for _ in range(BLOCK_SIZE * INPUTS_WANTED): + c.handle_v_ego(MIN_SPEED_FILTER + 1) + c.handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3], + [0.0, 0.0, HEIGHT_INIT.item()], [1e3, 1e3, 1e3]) self.assertEqual(c.valid_blocks, INPUTS_NEEDED) np.testing.assert_allclose(c.rpy, np.zeros(3)) @@ -81,9 +111,11 @@ class TestCalibrationd(unittest.TestCase): c = Calibrator(param_put=False) for _ in range(BLOCK_SIZE * INPUTS_WANTED): c.handle_v_ego(MIN_SPEED_FILTER + 1) - c. handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], + c.handle_cam_odom([MIN_SPEED_FILTER + 1, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3], + [0.0, 0.0, HEIGHT_INIT.item()], [1e-3, 1e-3, 1e-3]) self.assertEqual(c.valid_blocks, INPUTS_WANTED) np.testing.assert_allclose(c.rpy, [0.0, 0.0, 0.0]) @@ -95,6 +127,8 @@ class TestCalibrationd(unittest.TestCase): c.handle_cam_odom([MIN_SPEED_FILTER + 1, -0.05 * MIN_SPEED_FILTER, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], + [1e-3, 1e-3, 1e-3], + [0.0, 0.0, HEIGHT_INIT.item()], [1e-3, 1e-3, 1e-3]) self.assertEqual(c.valid_blocks, 1) self.assertEqual(c.cal_status, log.LiveCalibrationData.Status.recalibrating) diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index e184dc3ec4..2902790fa5 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -2,12 +2,9 @@ import time import unittest from cereal import log -import cereal.messaging as messaging from common.params import Params from datetime import datetime from unittest import mock -#from unittest.mock import patch -from tqdm import tqdm from laika.constants import SECS_IN_DAY @@ -19,45 +16,22 @@ from laika.raw_gnss import GNSSMeasurement, read_raw_ublox, read_raw_qcom from selfdrive.locationd.laikad import EPHEMERIS_CACHE, Laikad from selfdrive.test.openpilotci import get_url from tools.lib.logreader import LogReader -from selfdrive.manager.process_config import managed_processes -from selfdrive.test.process_replay.helpers import OpenpilotPrefix +from selfdrive.test.process_replay.process_replay import get_process_config, replay_process + +GPS_TIME_PREDICTION_ORBITS_RUSSIAN_SRC = GPSTime.from_datetime(datetime(2022, month=1, day=29, hour=12)) -def get_ublox_gnss(ubloxraw): - with OpenpilotPrefix(): - managed_processes['ubloxd'].start() - timeout_ms = 30 - pm = messaging.PubMaster(['ubloxRaw']) - sock = messaging.sub_sock('ubloxGnss', timeout=timeout_ms) - - log_msgs = [] - log_t = [] - for x in tqdm(ubloxraw): - pm.send(x.which(), x.as_builder()) - ret = messaging.recv_one(sock) - if ret is not None: - msg = log.Event.new_message(ubloxGnss=ret.ubloxGnss.to_dict()) - msg.logMonoTime = x.logMonoTime - log_msgs.append(msg) - log_t.append(1e-9 * x.logMonoTime) - assert managed_processes['ubloxd'].get_process_state_msg().running - assert len(log_msgs) > 1 or len(ubloxraw) == 0 - managed_processes['ubloxd'].stop() - return log_t, log_msgs - - - -def get_log(segs=range(0)): +def get_log_ublox(segs=range(0)): logs = [] for i in segs: logs.extend(LogReader(get_url("4cf7a6ad03080c90|2021-09-29--13-46-36", i))) - raw_logs = [m for m in logs if m.which() == 'ubloxRaw'] - all_logs = get_ublox_gnss(raw_logs)[1] + ublox_cfg = get_process_config("ubloxd") + all_logs = replay_process(ublox_cfg, logs) low_gnss = [] for m in all_logs: - if m.ubloxGnss.which() != 'measurementReport': + if m.which() != "ubloxGnss" or m.ubloxGnss.which() != 'measurementReport': continue MAX_MEAS = 7 @@ -72,6 +46,7 @@ def get_log(segs=range(0)): low_gnss.append(m) return all_logs, low_gnss + def get_log_qcom(segs=range(0)): logs = [] for i in segs: @@ -79,13 +54,16 @@ def get_log_qcom(segs=range(0)): all_logs = [m for m in logs if m.which() == 'qcomGnss'] return all_logs + def verify_messages(lr, laikad, return_one_success=False): good_msgs = [] for m in lr: if m.which() == 'ubloxGnss': gnss_msg = m.ubloxGnss - else: + elif m.which() == 'qcomGnss': gnss_msg = m.qcomGnss + else: + continue msg = laikad.process_gnss_msg(gnss_msg, m.logMonoTime, block=True) if msg is not None and len(msg.gnssMeasurements.correctedMeasurements) > 0: good_msgs.append(msg) @@ -101,7 +79,7 @@ def get_first_gps_time(logs): new_meas = read_raw_ublox(m.ubloxGnss.measurementReport) if len(new_meas) > 0: return new_meas[0].recv_time - else: + elif m.which() == "qcomGnss": if m.qcomGnss.which == 'measurementReport': new_meas = read_raw_qcom(m.qcomGnss.measurementReport) if len(new_meas) > 0: @@ -116,14 +94,11 @@ def get_measurement_mock(gpstime, sat_ephemeris): return meas -GPS_TIME_PREDICTION_ORBITS_RUSSIAN_SRC = GPSTime.from_datetime(datetime(2022, month=1, day=29, hour=12)) - - class TestLaikad(unittest.TestCase): @classmethod def setUpClass(cls): - logs, low_gnss = get_log(range(1)) + logs, low_gnss = get_log_ublox(range(1)) cls.logs = logs cls.low_gnss = low_gnss cls.logs_qcom = get_log_qcom(range(1)) @@ -190,6 +165,9 @@ class TestLaikad(unittest.TestCase): self.assertFalse(all(laikad.kf_valid(m.logMonoTime * 1e-9))) kf_valid = False for m in self.logs: + if m.which() != 'ubloxGnss': + continue + laikad.process_gnss_msg(m.ubloxGnss, m.logMonoTime, block=True) kf_valid = all(laikad.kf_valid(m.logMonoTime * 1e-9)) if kf_valid: @@ -247,6 +225,9 @@ class TestLaikad(unittest.TestCase): seen_internet_eph = False for m in logs: + if m.which() != 'ubloxGnss' and m.which() != 'qcomGnss': + continue + gnss_msg = m.qcomGnss if use_qcom else m.ubloxGnss out_msg = laikad.process_gnss_msg(gnss_msg, m.logMonoTime, block=False) if laikad.orbit_fetch_future is not None: @@ -301,8 +282,6 @@ class TestLaikad(unittest.TestCase): self.assertTrue(any([x.source=='cache' for x in msg.gnssMeasurements.ephemerisStatuses])) self.assertIsNotNone(msg) - - #TODO test cache with only orbits #with patch('selfdrive.locationd.laikad.get_orbit_data', return_value=None) as mock_method: # # Verify no orbit downloads even if orbit fetch times is reset since the cache has recently been saved and we don't want to download high frequently @@ -333,5 +312,7 @@ class TestLaikad(unittest.TestCase): def dict_has_values(self, dct): self.assertGreater(len(dct), 0) self.assertGreater(min([len(v) for v in dct.values()]), 0) + + if __name__ == "__main__": unittest.main() diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index c5bcc7c5c2..e3a27f9bdf 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -7,6 +7,7 @@ import sys import traceback from typing import List, Tuple, Union +from cereal import log import cereal.messaging as messaging import selfdrive.sentry as sentry from common.basedir import BASEDIR @@ -36,6 +37,8 @@ def manager_init() -> None: params = Params() params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) + params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) + params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) default_params: List[Tuple[str, Union[str, bytes]]] = [ ("CompletedTrainingVersion", "0"), @@ -44,6 +47,7 @@ def manager_init() -> None: ("HasAcceptedTerms", "0"), ("LanguageSetting", "main_en"), ("OpenpilotEnabledToggle", "1"), + ("LongitudinalPersonality", str(log.LongitudinalPersonality.standard)), ("AccMadsCombo", "1"), ("AutoLaneChangeTimer", "0"), diff --git a/selfdrive/manager/process.py b/selfdrive/manager/process.py index ce18073d9b..f4d2b39841 100644 --- a/selfdrive/manager/process.py +++ b/selfdrive/manager/process.py @@ -91,7 +91,7 @@ class ManagerProcess(ABC): pass def restart(self) -> None: - self.stop() + self.stop(sig=signal.SIGKILL) self.start() def check_watchdog(self, started: bool) -> None: @@ -108,21 +108,21 @@ class ManagerProcess(ABC): dt = sec_since_boot() - self.last_watchdog_time / 1e9 if dt > self.watchdog_max_dt: - # Only restart while offroad for now if self.watchdog_seen and ENABLE_WATCHDOG: cloudlog.error(f"Watchdog timeout for {self.name} (exitcode {self.proc.exitcode}) restarting ({started=})") self.restart() else: self.watchdog_seen = True - def stop(self, retry: bool=True, block: bool=True) -> Optional[int]: + def stop(self, retry: bool = True, block: bool = True, sig: Optional[signal.Signals] = None) -> Optional[int]: if self.proc is None: return None if self.proc.exitcode is None: if not self.shutting_down: cloudlog.info(f"killing {self.name}") - sig = signal.SIGKILL if self.sigkill else signal.SIGINT + if sig is None: + sig = signal.SIGKILL if self.sigkill else signal.SIGINT self.signal(sig) self.shutting_down = True @@ -285,7 +285,7 @@ class DaemonProcess(ManagerProcess): params.put(self.param_name, str(proc.pid)) - def stop(self, retry=True, block=True) -> None: + def stop(self, retry=True, block=True, sig=None) -> None: pass diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index bf39f525af..21cca68c3e 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -47,7 +47,7 @@ procs = [ NativeProcess("navmodeld", "selfdrive/modeld", ["./navmodeld"], enabled=False), NativeProcess("sensord", "system/sensord", ["./sensord"], enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], offroad=True, watchdog_max_dt=(5 if not PC else None)), - NativeProcess("soundd", "selfdrive/ui/soundd", ["./soundd"], offroad=True), + NativeProcess("soundd", "selfdrive/ui/soundd", ["./soundd"]), NativeProcess("locationd", "selfdrive/locationd", ["./locationd"]), NativeProcess("boardd", "selfdrive/boardd", ["./boardd"], enabled=False), PythonProcess("calibrationd", "selfdrive.locationd.calibrationd"), diff --git a/selfdrive/modeld/modeld.cc b/selfdrive/modeld/modeld.cc index 0a8a8d6358..265bd4071b 100644 --- a/selfdrive/modeld/modeld.cc +++ b/selfdrive/modeld/modeld.cc @@ -179,9 +179,6 @@ int main(int argc, char **argv) { assert(ret == 0); } - bool main_wide_camera = Params().getBool("WideCameraOnly"); - bool use_extra_client = !main_wide_camera; // set for single camera mode - // cl init cl_device_id device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); cl_context context = CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); @@ -191,8 +188,22 @@ int main(int argc, char **argv) { model_init(&model, device_id, context); LOGW("models loaded, modeld starting"); + bool main_wide_camera = false; + bool use_extra_client = true; // set to false to use single camera + while (!do_exit) { + auto streams = VisionIpcClient::getAvailableStreams("camerad", false); + if (!streams.empty()) { + use_extra_client = streams.count(VISION_STREAM_WIDE_ROAD) > 0 && streams.count(VISION_STREAM_ROAD) > 0; + main_wide_camera = streams.count(VISION_STREAM_ROAD) == 0; + break; + } + + util::sleep_for(100); + } + VisionIpcClient vipc_client_main = VisionIpcClient("camerad", main_wide_camera ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD, true, device_id, context); VisionIpcClient vipc_client_extra = VisionIpcClient("camerad", VISION_STREAM_WIDE_ROAD, false, device_id, context); + LOGW("vision stream set up, main_wide_camera: %d, use_extra_client: %d", main_wide_camera, use_extra_client); while (!do_exit && !vipc_client_main.connect(false)) { util::sleep_for(100); diff --git a/selfdrive/modeld/models/README.md b/selfdrive/modeld/models/README.md index 80d6dc0e02..04964f3eab 100644 --- a/selfdrive/modeld/models/README.md +++ b/selfdrive/modeld/models/README.md @@ -35,6 +35,7 @@ Read [here](https://github.com/commaai/openpilot/blob/90af436a121164a51da9fa48d0 * single image W = 1440 H = 960 represented in planar YUV420 format: * full input size is 1440 * 960 = 1382400 * normalized ranging from 0.0 to 1.0 in float32 (onnx runner) or ranging from 0 to 255 in uint8 (snpe runner) +* camera calibration angles (roll, pitch, yaw) from liveCalibration: 3 x float32 inputs ### output format * 84 x float32 outputs = 2 + 41 * 2 ([parsing example](https://github.com/commaai/openpilot/blob/22ce4e17ba0d3bfcf37f8255a4dd1dc683fe0c38/selfdrive/modeld/models/dmonitoring.cc#L33)) diff --git a/selfdrive/modeld/models/driving.cc b/selfdrive/modeld/models/driving.cc index 5538d6ff9b..6b0e626c5c 100644 --- a/selfdrive/modeld/models/driving.cc +++ b/selfdrive/modeld/models/driving.cc @@ -390,15 +390,18 @@ void posenet_publish(PubMaster &pm, uint32_t vipc_frame_id, uint32_t vipc_droppe const auto &v_std = net_outputs.pose.velocity_std; const auto &r_std = net_outputs.pose.rotation_std; const auto &t_std = net_outputs.wide_from_device_euler.std; + const auto &road_transform_trans_mean = net_outputs.road_transform.position_mean; + const auto &road_transform_trans_std = net_outputs.road_transform.position_std; auto posenetd = msg.initEvent(valid && (vipc_dropped_frames < 1)).initCameraOdometry(); posenetd.setTrans({v_mean.x, v_mean.y, v_mean.z}); posenetd.setRot({r_mean.x, r_mean.y, r_mean.z}); posenetd.setWideFromDeviceEuler({t_mean.x, t_mean.y, t_mean.z}); + posenetd.setRoadTransformTrans({road_transform_trans_mean.x, road_transform_trans_mean.y, road_transform_trans_mean.z}); posenetd.setTransStd({exp(v_std.x), exp(v_std.y), exp(v_std.z)}); posenetd.setRotStd({exp(r_std.x), exp(r_std.y), exp(r_std.z)}); posenetd.setWideFromDeviceEulerStd({exp(t_std.x), exp(t_std.y), exp(t_std.z)}); - + posenetd.setRoadTransformTransStd({exp(road_transform_trans_std.x), exp(road_transform_trans_std.y), exp(road_transform_trans_std.z)}); posenetd.setTimestampEof(timestamp_eof); posenetd.setFrameId(vipc_frame_id); diff --git a/selfdrive/modeld/models/driving.h b/selfdrive/modeld/models/driving.h index 5734c68cb9..1214c30062 100644 --- a/selfdrive/modeld/models/driving.h +++ b/selfdrive/modeld/models/driving.h @@ -1,10 +1,5 @@ #pragma once -// gate this here -#define TEMPORAL -#define DESIRE -#define TRAFFIC_CONVENTION - #include #include @@ -17,6 +12,12 @@ #include "selfdrive/modeld/models/nav.h" #include "selfdrive/modeld/runners/run.h" +// gate this here +#define TEMPORAL +#define DESIRE +#define TRAFFIC_CONVENTION +#define NAV + constexpr int FEATURE_LEN = 128; constexpr int HISTORY_BUFFER_LEN = 99; constexpr int DESIRE_LEN = 8; @@ -177,6 +178,14 @@ struct ModelOutputTemporalPose { }; static_assert(sizeof(ModelOutputTemporalPose) == sizeof(ModelOutputXYZ)*4); +struct ModelOutputRoadTransform { + ModelOutputXYZ position_mean; + ModelOutputXYZ rotation_mean; + ModelOutputXYZ position_std; + ModelOutputXYZ rotation_std; +}; +static_assert(sizeof(ModelOutputRoadTransform) == sizeof(ModelOutputXYZ)*4); + struct ModelOutputDisengageProb { float gas_disengage; float brake_disengage; @@ -236,6 +245,7 @@ struct ModelOutput { const ModelOutputPose pose; const ModelOutputWideFromDeviceEuler wide_from_device_euler; const ModelOutputTemporalPose temporal_pose; + const ModelOutputRoadTransform road_transform; }; constexpr int OUTPUT_SIZE = sizeof(ModelOutput) / sizeof(float); diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx index d8a8b5ef13..53c1224ae8 100644 --- a/selfdrive/modeld/models/supercombo.onnx +++ b/selfdrive/modeld/models/supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0007958c9bad4a87eb5c28080b5bfdc24834958fbcbaff448674fa570b0196da -size 46011200 +oid sha256:4aa77af40335462062c6eb06632bb84cbc8e5c9e3ad759f66862711620e2a840 +size 46117948 diff --git a/selfdrive/modeld/runners/onnxmodel.cc b/selfdrive/modeld/runners/onnxmodel.cc index 5616a6c9c7..a5a83bccdc 100644 --- a/selfdrive/modeld/runners/onnxmodel.cc +++ b/selfdrive/modeld/runners/onnxmodel.cc @@ -138,14 +138,14 @@ void ONNXModel::execute() { if (desire_input_buf != NULL) { pwrite(desire_input_buf, desire_state_size); } - if (nav_features_input_buf != NULL) { - pwrite(nav_features_input_buf, nav_features_size); + if (traffic_convention_input_buf != NULL) { + pwrite(traffic_convention_input_buf, traffic_convention_size); } if (driving_style_input_buf != NULL) { pwrite(driving_style_input_buf, driving_style_size); } - if (traffic_convention_input_buf != NULL) { - pwrite(traffic_convention_input_buf, traffic_convention_size); + if (nav_features_input_buf != NULL) { + pwrite(nav_features_input_buf, nav_features_size); } if (calib_input_buf != NULL) { pwrite(calib_input_buf, calib_size); diff --git a/selfdrive/modeld/runners/thneedmodel.cc b/selfdrive/modeld/runners/thneedmodel.cc index 4fdd7ca466..2ac3d64495 100644 --- a/selfdrive/modeld/runners/thneedmodel.cc +++ b/selfdrive/modeld/runners/thneedmodel.cc @@ -41,13 +41,13 @@ void ThneedModel::addExtra(float *extra_input_buf, int buf_size) { } void* ThneedModel::getInputBuf() { - if (use_extra && thneed->input_clmem.size() > 4) return &(thneed->input_clmem[4]); - else if (!use_extra && thneed->input_clmem.size() > 3) return &(thneed->input_clmem[3]); + if (use_extra && thneed->input_clmem.size() > 5) return &(thneed->input_clmem[5]); + else if (!use_extra && thneed->input_clmem.size() > 4) return &(thneed->input_clmem[4]); else return nullptr; } void* ThneedModel::getExtraBuf() { - if (thneed->input_clmem.size() > 3) return &(thneed->input_clmem[3]); + if (thneed->input_clmem.size() > 4) return &(thneed->input_clmem[4]); else return nullptr; } @@ -55,10 +55,10 @@ void ThneedModel::execute() { if (!recorded) { thneed->record = true; if (use_extra) { - float *inputs[5] = {recurrent, trafficConvention, desire, extra, input}; + float *inputs[6] = {recurrent, navFeatures, trafficConvention, desire, extra, input}; thneed->copy_inputs(inputs); } else { - float *inputs[4] = {recurrent, trafficConvention, desire, input}; + float *inputs[5] = {recurrent, navFeatures, trafficConvention, desire, input}; thneed->copy_inputs(inputs); } thneed->clexec(); @@ -68,12 +68,11 @@ void ThneedModel::execute() { recorded = true; } else { if (use_extra) { - float *inputs[5] = {recurrent, trafficConvention, desire, extra, input}; + float *inputs[6] = {recurrent, navFeatures, trafficConvention, desire, extra, input}; thneed->execute(inputs, output); } else { - float *inputs[4] = {recurrent, trafficConvention, desire, input}; + float *inputs[5] = {recurrent, navFeatures, trafficConvention, desire, input}; thneed->execute(inputs, output); } } } - diff --git a/selfdrive/modeld/thneed/thneed_qcom2.cc b/selfdrive/modeld/thneed/thneed_qcom2.cc index a29a82c8c8..a3bfb8a8c2 100644 --- a/selfdrive/modeld/thneed/thneed_qcom2.cc +++ b/selfdrive/modeld/thneed/thneed_qcom2.cc @@ -43,7 +43,7 @@ int ioctl(int filedes, unsigned long request, void *argp) { if (request == IOCTL_KGSL_DRAWCTXT_CREATE) { struct kgsl_drawctxt_create *create = (struct kgsl_drawctxt_create *)argp; create->flags &= ~KGSL_CONTEXT_PRIORITY_MASK; - create->flags |= 1 << KGSL_CONTEXT_PRIORITY_SHIFT; // priority from 1-15, 1 is max priority + create->flags |= 6 << KGSL_CONTEXT_PRIORITY_SHIFT; // priority from 1-15, 1 is max priority printf("IOCTL_KGSL_DRAWCTXT_CREATE: creating context with flags 0x%x\n", create->flags); } @@ -238,24 +238,6 @@ void Thneed::execute(float **finputs, float *foutput, bool slow) { // ****** copy inputs copy_inputs(finputs, true); - // ****** set power constraint - int ret; - struct kgsl_device_constraint_pwrlevel pwrlevel; - pwrlevel.level = KGSL_CONSTRAINT_PWR_MAX; - - struct kgsl_device_constraint constraint; - constraint.type = KGSL_CONSTRAINT_PWRLEVEL; - constraint.context_id = context_id; - constraint.data = (void*)&pwrlevel; - constraint.size = sizeof(pwrlevel); - - struct kgsl_device_getproperty prop; - prop.type = KGSL_PROP_PWR_CONSTRAINT; - prop.value = (void*)&constraint; - prop.sizebytes = sizeof(constraint); - ret = ioctl(fd, IOCTL_KGSL_SETPROPERTY, &prop); - assert(ret == 0); - // ****** run commands int i = 0; for (auto &it : cmds) { @@ -268,14 +250,6 @@ void Thneed::execute(float **finputs, float *foutput, bool slow) { // ****** copy outputs copy_output(foutput); - // ****** unset power constraint - constraint.type = KGSL_CONSTRAINT_NONE; - constraint.data = NULL; - constraint.size = 0; - - ret = ioctl(fd, IOCTL_KGSL_SETPROPERTY, &prop); - assert(ret == 0); - if (debug >= 1) { te = nanos_since_boot(); printf("model exec in %lu us\n", (te-tb)/1000); diff --git a/selfdrive/navd/map_renderer.cc b/selfdrive/navd/map_renderer.cc index 203470bb42..5c7e17cd51 100644 --- a/selfdrive/navd/map_renderer.cc +++ b/selfdrive/navd/map_renderer.cc @@ -15,7 +15,9 @@ const int HEIGHT = 256, WIDTH = 256; const int NUM_VIPC_BUFFERS = 4; const int EARTH_CIRCUMFERENCE_METERS = 40075000; +const int EARTH_RADIUS_METERS = 6378137; const int PIXELS_PER_TILE = 256; +const int MAP_OFFSET = 128; const bool TEST_MODE = getenv("MAP_RENDER_TEST_MODE"); const int LLK_DECIMATION = TEST_MODE ? 1 : 10; @@ -26,6 +28,14 @@ float get_zoom_level_for_scale(float lat, float meters_per_pixel) { return log2(num_tiles) - 1; } +QMapbox::Coordinate get_point_along_line(float lat, float lon, float bearing, float dist) { + float ang_dist = dist / EARTH_RADIUS_METERS; + float lat1 = DEG2RAD(lat), lon1 = DEG2RAD(lon), bearing1 = DEG2RAD(bearing); + float lat2 = asin(sin(lat1)*cos(ang_dist) + cos(lat1)*sin(ang_dist)*cos(bearing1)); + float lon2 = lon1 + atan2(sin(bearing1)*sin(ang_dist)*cos(lat1), cos(ang_dist)-sin(lat1)*sin(lat2)); + return QMapbox::Coordinate(RAD2DEG(lat2), RAD2DEG(lon2)); +} + MapRenderer::MapRenderer(const QMapboxGLSettings &settings, bool online) : m_settings(settings) { QSurfaceFormat fmt; @@ -70,7 +80,7 @@ MapRenderer::MapRenderer(const QMapboxGLSettings &settings, bool online) : m_set if (online) { vipc_server.reset(new VisionIpcServer("navd")); - vipc_server->create_buffers(VisionStreamType::VISION_STREAM_MAP, NUM_VIPC_BUFFERS, false, WIDTH/2, HEIGHT/2); + vipc_server->create_buffers(VisionStreamType::VISION_STREAM_MAP, NUM_VIPC_BUFFERS, false, WIDTH, HEIGHT); vipc_server->start_listener(); pm.reset(new PubMaster({"navThumbnail", "mapRenderState"})); @@ -93,7 +103,8 @@ void MapRenderer::msgUpdate() { bool localizer_valid = (location.getStatus() == cereal::LiveLocationKalman::Status::VALID) && pos.getValid(); if (localizer_valid && (sm->rcv_frame("liveLocationKalman") % LLK_DECIMATION) == 0) { - updatePosition(QMapbox::Coordinate(pos.getValue()[0], pos.getValue()[1]), RAD2DEG(orientation.getValue()[2])); + float bearing = RAD2DEG(orientation.getValue()[2]); + updatePosition(get_point_along_line(pos.getValue()[0], pos.getValue()[1], bearing, MAP_OFFSET), bearing); // TODO: use the static rendering mode if (!loaded() && frame_id > 0) { diff --git a/selfdrive/test/fuzzy_generation.py b/selfdrive/test/fuzzy_generation.py new file mode 100644 index 0000000000..3dc43e7347 --- /dev/null +++ b/selfdrive/test/fuzzy_generation.py @@ -0,0 +1,69 @@ +import hypothesis.strategies as st +import random + +class FuzzyGenerator: + def __init__(self, real_floats): + self.real_floats=real_floats + + def generate_native_type(self, field): + def floats(**kwargs): + allow_nan = not self.real_floats + allow_infinity = not self.real_floats + return st.floats(**kwargs, allow_nan=allow_nan, allow_infinity=allow_infinity) + + if field == 'bool': + return st.booleans() + elif field == 'int8': + return st.integers(min_value=-2**7, max_value=2**7-1) + elif field == 'int16': + return st.integers(min_value=-2**15, max_value=2**15-1) + elif field == 'int32': + return st.integers(min_value=-2**31, max_value=2**31-1) + elif field == 'int64': + return st.integers(min_value=-2**63, max_value=2**63-1) + elif field == 'uint8': + return st.integers(min_value=0, max_value=2**8-1) + elif field == 'uint16': + return st.integers(min_value=0, max_value=2**16-1) + elif field == 'uint32': + return st.integers(min_value=0, max_value=2**32-1) + elif field == 'uint64': + return st.integers(min_value=0, max_value=2**64-1) + elif field == 'float32': + return floats(width=32) + elif field == 'float64': + return floats(width=64) + elif field == 'text': + return st.text(max_size=1000) + elif field == 'data': + return st.text(max_size=1000) + elif field == 'anyPointer': + return st.text() + else: + raise NotImplementedError(f'Invalid type : {field}') + + def generate_field(self, field): + def rec(field_type): + if field_type.which() == 'struct': + return self.generate_struct(field.schema.elementType if base_type == 'list' else field.schema) + elif field_type.which() == 'list': + return st.lists(rec(field_type.list.elementType)) + elif field_type.which() == 'enum': + schema = field.schema.elementType if base_type == 'list' else field.schema + return st.sampled_from(list(schema.enumerants.keys())) + else: + return self.generate_native_type(field_type.which()) + + if 'slot' in field.proto.to_dict(): + base_type = field.proto.slot.type.which() + return rec(field.proto.slot.type) + else: + return self.generate_struct(field.schema) + + def generate_struct(self, schema): + full_fill = list(schema.non_union_fields) if schema.non_union_fields else [] + single_fill = [random.choice(schema.union_fields)] if schema.union_fields else [] + return st.fixed_dictionaries(dict((field, self.generate_field(schema.fields[field])) for field in full_fill + single_fill)) + +def get_random_msg(struct, real_floats=False): + return FuzzyGenerator(real_floats=real_floats).generate_struct(struct.schema) diff --git a/selfdrive/test/helpers.py b/selfdrive/test/helpers.py index 281c902933..f7dab576f3 100644 --- a/selfdrive/test/helpers.py +++ b/selfdrive/test/helpers.py @@ -3,13 +3,18 @@ import time from functools import wraps import cereal.messaging as messaging +from common.params import Params from selfdrive.manager.process_config import managed_processes from system.hardware import PC from system.version import training_version, terms_version def set_params_enabled(): - from common.params import Params + os.environ['PASSIVE'] = "0" + os.environ['REPLAY'] = "1" + os.environ['FINGERPRINT'] = "TOYOTA COROLLA TSS2 2019" + os.environ['LOGPRINT'] = "debug" + params = Params() params.put("HasAcceptedTerms", terms_version) params.put("CompletedTrainingVersion", training_version) diff --git a/selfdrive/test/openpilotci.py b/selfdrive/test/openpilotci.py index 1d1b6ec423..5f4b9f1f35 100755 --- a/selfdrive/test/openpilotci.py +++ b/selfdrive/test/openpilotci.py @@ -11,10 +11,7 @@ def get_url(route_name, segment_num, log_type="rlog"): ext = "hevc" if log_type.endswith('camera') else "bz2" return BASE_URL + f"{route_name.replace('|', '/')}/{segment_num}/{log_type}.{ext}" - -def upload_file(path, name): - from azure.storage.blob import BlockBlobService # pylint: disable=import-error - +def get_sas_token(): sas_token = os.environ.get("AZURE_TOKEN", None) if os.path.isfile(TOKEN_PATH): sas_token = open(TOKEN_PATH).read().strip() @@ -22,9 +19,20 @@ def upload_file(path, name): if sas_token is None: sas_token = subprocess.check_output("az storage container generate-sas --account-name commadataci --name openpilotci --https-only --permissions lrw \ --expiry $(date -u '+%Y-%m-%dT%H:%M:%SZ' -d '+1 hour') --auth-mode login --as-user --output tsv", shell=True).decode().strip("\n") - service = BlockBlobService(account_name="commadataci", sas_token=sas_token) + + return sas_token + +def upload_bytes(data, name): + from azure.storage.blob import BlockBlobService # pylint: disable=import-error + service = BlockBlobService(account_name="commadataci", sas_token=get_sas_token()) + service.create_blob_from_bytes("openpilotci", name, data) + return BASE_URL + name + +def upload_file(path, name): + from azure.storage.blob import BlockBlobService # pylint: disable=import-error + service = BlockBlobService(account_name="commadataci", sas_token=get_sas_token()) service.create_blob_from_path("openpilotci", name, path) - return "https://commadataci.blob.core.windows.net/openpilotci/" + name + return BASE_URL + name if __name__ == "__main__": diff --git a/selfdrive/test/process_replay/__init__.py b/selfdrive/test/process_replay/__init__.py index e69de29bb2..6667deaa2d 100644 --- a/selfdrive/test/process_replay/__init__.py +++ b/selfdrive/test/process_replay/__init__.py @@ -0,0 +1 @@ +from selfdrive.test.process_replay.process_replay import CONFIGS, get_process_config, replay_process, replay_process_with_name # noqa: F401 diff --git a/selfdrive/test/process_replay/compare_logs.py b/selfdrive/test/process_replay/compare_logs.py index 821429a70f..52898d8810 100755 --- a/selfdrive/test/process_replay/compare_logs.py +++ b/selfdrive/test/process_replay/compare_logs.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import bz2 import sys import math import capnp @@ -12,16 +11,6 @@ from tools.lib.logreader import LogReader EPSILON = sys.float_info.epsilon -def save_log(dest, log_msgs, compress=True): - dat = b"".join(msg.as_builder().to_bytes() for msg in log_msgs) - - if compress: - dat = bz2.compress(dat) - - with open(dest, "wb") as f: - f.write(dat) - - def remove_ignored_fields(msg, ignore): msg = msg.as_builder() for key in ignore: @@ -83,7 +72,6 @@ def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=Non diff = [] for msg1, msg2 in zip(log1, log2): if msg1.which() != msg2.which(): - print(msg1.which(), msg2.which()) raise Exception("msgs not aligned between logs") msg1 = remove_ignored_fields(msg1, ignore_fields) diff --git a/selfdrive/test/process_replay/migration.py b/selfdrive/test/process_replay/migration.py new file mode 100644 index 0000000000..31b3acde56 --- /dev/null +++ b/selfdrive/test/process_replay/migration.py @@ -0,0 +1,65 @@ +from cereal import messaging + + +def migrate_all(lr, old_logtime=False): + msgs = migrate_sensorEvents(lr, old_logtime) + msgs = migrate_carParams(msgs, old_logtime) + + return msgs + + +def migrate_carParams(lr, old_logtime=False): + all_msgs = [] + for msg in lr: + if msg.which() == 'carParams': + CP = messaging.new_message('carParams') + CP.carParams = msg.carParams.as_builder() + for car_fw in CP.carParams.carFw: + car_fw.brand = CP.carParams.carName + if old_logtime: + CP.logMonoTime = msg.logMonoTime + msg = CP.as_reader() + all_msgs.append(msg) + + return all_msgs + + +def migrate_sensorEvents(lr, old_logtime=False): + all_msgs = [] + for msg in lr: + if msg.which() != 'sensorEventsDEPRECATED': + all_msgs.append(msg) + continue + + # migrate to split sensor events + for evt in msg.sensorEventsDEPRECATED: + # build new message for each sensor type + sensor_service = '' + if evt.which() == 'acceleration': + sensor_service = 'accelerometer' + elif evt.which() == 'gyro' or evt.which() == 'gyroUncalibrated': + sensor_service = 'gyroscope' + elif evt.which() == 'light' or evt.which() == 'proximity': + sensor_service = 'lightSensor' + elif evt.which() == 'magnetic' or evt.which() == 'magneticUncalibrated': + sensor_service = 'magnetometer' + elif evt.which() == 'temperature': + sensor_service = 'temperatureSensor' + + m = messaging.new_message(sensor_service) + m.valid = True + if old_logtime: + m.logMonoTime = msg.logMonoTime + + m_dat = getattr(m, sensor_service) + m_dat.version = evt.version + m_dat.sensor = evt.sensor + m_dat.type = evt.type + m_dat.source = evt.source + if old_logtime: + m_dat.timestamp = evt.timestamp + setattr(m_dat, evt.which(), getattr(evt, evt.which())) + + all_msgs.append(m.as_reader()) + + return all_msgs diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index 68cbb28aaf..55d95a6a6a 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -14,18 +14,19 @@ from common.transformations.camera import tici_f_frame_size, tici_d_frame_size from system.hardware import PC from selfdrive.manager.process_config import managed_processes from selfdrive.test.openpilotci import BASE_URL, get_url -from selfdrive.test.process_replay.compare_logs import compare_logs, save_log +from selfdrive.test.process_replay.compare_logs import compare_logs from selfdrive.test.process_replay.test_processes import format_diff from system.version import get_commit from tools.lib.framereader import FrameReader from tools.lib.logreader import LogReader +from tools.lib.helpers import save_log TEST_ROUTE = "2f4452b03ccb98f0|2022-12-03--13-45-30" SEGMENT = 6 MAX_FRAMES = 100 if PC else 600 NAV_FRAMES = 50 -NO_NAV = "NO_NAV" in os.environ # TODO: make map renderer work in CI +NO_NAV = "NO_NAV" in os.environ SEND_EXTRA_INPUTS = bool(os.getenv("SEND_EXTRA_INPUTS", "0")) VIPC_STREAM = {"roadCameraState": VisionStreamType.VISION_STREAM_ROAD, "driverCameraState": VisionStreamType.VISION_STREAM_DRIVER, @@ -209,6 +210,31 @@ if __name__ == "__main__": 'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, log_type="ecamera"), readahead=True) } + # Update tile refs + if update: + import urllib + import requests + import threading + import http.server + from selfdrive.test.openpilotci import upload_bytes + os.environ['MAPS_HOST'] = 'http://localhost:5000' + + class HTTPRequestHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + assert len(self.path) > 10 # Sanity check on path length + r = requests.get(f'https://api.mapbox.com{self.path}', timeout=30) + upload_bytes(r.content, urllib.parse.urlparse(self.path).path.lstrip('/')) + self.send_response(r.status_code) + self.send_header('Content-type','text/html') + self.end_headers() + self.wfile.write(r.content) + + server = http.server.HTTPServer(("127.0.0.1", 5000), HTTPRequestHandler) + thread = threading.Thread(None, server.serve_forever, daemon=True) + thread.start() + else: + os.environ['MAPS_HOST'] = BASE_URL.rstrip('/') + # run replays log_msgs = model_replay(lr, frs) if not NO_NAV: diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index b3cc50e559..b89468eebb 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -9d3cd2e7d5fceaaf0e8a4bd798a24fcf470da7c2 +69270554b3c3ae574b9f357f2473395edf7db8af diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 4ab2d30315..13dfb661b7 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -1,26 +1,24 @@ #!/usr/bin/env python3 -import importlib import os -import sys -import threading import time import signal +import platform +from collections import OrderedDict from dataclasses import dataclass, field from typing import Dict, List, Optional, Callable - -import capnp +from tqdm import tqdm import cereal.messaging as messaging -from cereal import car, log +from cereal import car from cereal.services import service_list from common.params import Params from common.timeout import Timeout from common.realtime import DT_CTRL from panda.python import ALTERNATIVE_EXPERIENCE from selfdrive.car.car_helpers import get_car, interfaces -from selfdrive.test.process_replay.helpers import OpenpilotPrefix -from selfdrive.manager.process import PythonProcess from selfdrive.manager.process_config import managed_processes +from selfdrive.test.process_replay.helpers import OpenpilotPrefix +from selfdrive.test.process_replay.migration import migrate_all # Numpy gives different results based on CPU features after version 19 NUMPY_TOLERANCE = 1e-7 @@ -29,393 +27,448 @@ TIMEOUT = 15 PROC_REPLAY_DIR = os.path.dirname(os.path.abspath(__file__)) FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/") + +class ReplayContext: + def __init__(self, cfg): + self.proc_name = cfg.proc_name + self.pubs = cfg.pubs + self.drained_pub = cfg.drained_pub + assert(len(self.pubs) != 0 or self.drained_pub is not None) + + def __enter__(self): + messaging.toggle_fake_events(True) + messaging.set_fake_prefix(self.proc_name) + + if self.drained_pub is None: + self.events = OrderedDict() + for pub in self.pubs: + self.events[pub] = messaging.fake_event_handle(pub, enable=True) + else: + self.events = {self.drained_pub: messaging.fake_event_handle(self.drained_pub, enable=True)} + + return self + + def __exit__(self, exc_type, exc_obj, exc_tb): + del self.events + + messaging.toggle_fake_events(False) + messaging.delete_fake_prefix() + + @property + def all_recv_called_events(self): + return [man.recv_called_event for man in self.events.values()] + + @property + def all_recv_ready_events(self): + return [man.recv_ready_event for man in self.events.values()] + + def send_sync(self, pm, endpoint, dat): + self.events[endpoint].recv_called_event.wait() + self.events[endpoint].recv_called_event.clear() + pm.send(endpoint, dat) + self.events[endpoint].recv_ready_event.set() + + def unlock_sockets(self): + expected_sets = len(self.events) + while expected_sets > 0: + index = messaging.wait_for_one_event(self.all_recv_called_events) + self.all_recv_called_events[index].clear() + self.all_recv_ready_events[index].set() + expected_sets -= 1 + + def wait_for_recv_called(self): + messaging.wait_for_one_event(self.all_recv_called_events) + + def wait_for_next_recv(self, trigger_empty_recv): + index = messaging.wait_for_one_event(self.all_recv_called_events) + if self.drained_pub is not None and trigger_empty_recv: + self.all_recv_called_events[index].clear() + self.all_recv_ready_events[index].set() + self.all_recv_called_events[index].wait() + + @dataclass class ProcessConfig: proc_name: str - pub_sub: Dict[str, List[str]] + pubs: List[str] + subs: List[str] ignore: List[str] + config_callback: Optional[Callable] init_callback: Optional[Callable] should_recv_callback: Optional[Callable] - tolerance: Optional[float] - fake_pubsubmaster: bool - submaster_config: Dict[str, List[str]] = field(default_factory=dict) + tolerance: Optional[float] = None environ: Dict[str, str] = field(default_factory=dict) subtest_name: str = "" field_tolerances: Dict[str, float] = field(default_factory=dict) timeout: int = 30 + simulation: bool = True + drained_pub: Optional[str] = None -def wait_for_event(evt): - if not evt.wait(TIMEOUT): - if threading.currentThread().getName() == "MainThread": - # tested process likely died. don't let test just hang - raise Exception(f"Timeout reached. Tested process {os.environ['PROC_NAME']} likely crashed.") - else: - # done testing this process, let it die - sys.exit(0) - - -class FakeSocket: - def __init__(self, wait=True): +class DummySocket: + def __init__(self): self.data = [] - self.wait = wait - self.recv_called = threading.Event() - self.recv_ready = threading.Event() def receive(self, non_blocking=False): if non_blocking: return None - if self.wait: - self.recv_called.set() - wait_for_event(self.recv_ready) - self.recv_ready.clear() return self.data.pop() def send(self, data): - if self.wait: - wait_for_event(self.recv_called) - self.recv_called.clear() - self.data.append(data) - if self.wait: - self.recv_ready.set() - def wait_for_recv(self): - wait_for_event(self.recv_called) - - -class DumbSocket: - def __init__(self, s=None): - if s is not None: - try: - dat = messaging.new_message(s) - except capnp.lib.capnp.KjException: # pylint: disable=c-extension-no-member - # lists - dat = messaging.new_message(s, 0) - - self.data = dat.to_bytes() - - def receive(self, non_blocking=False): - return self.data - - def send(self, dat): - pass - - -class FakeSubMaster(messaging.SubMaster): - def __init__(self, services, ignore_alive=None, ignore_avg_freq=None): - super().__init__(services, ignore_alive=ignore_alive, ignore_avg_freq=ignore_avg_freq, addr=None) - self.sock = {s: DumbSocket(s) for s in services} - self.update_called = threading.Event() - self.update_ready = threading.Event() - self.wait_on_getitem = False - - def __getitem__(self, s): - # hack to know when fingerprinting is done - if self.wait_on_getitem: - self.update_called.set() - wait_for_event(self.update_ready) - self.update_ready.clear() - return self.data[s] - - def update(self, timeout=-1): - self.update_called.set() - wait_for_event(self.update_ready) - self.update_ready.clear() - - def update_msgs(self, cur_time, msgs): - wait_for_event(self.update_called) - self.update_called.clear() - super().update_msgs(cur_time, msgs) - self.update_ready.set() - - def wait_for_update(self): - wait_for_event(self.update_called) - - -class FakePubMaster(messaging.PubMaster): - def __init__(self, services): # pylint: disable=super-init-not-called - self.data = {} - self.sock = {} - self.last_updated = None - for s in services: - try: - data = messaging.new_message(s) - except capnp.lib.capnp.KjException: - data = messaging.new_message(s, 0) - self.data[s] = data.as_reader() - self.sock[s] = DumbSocket() - self.send_called = threading.Event() - self.get_called = threading.Event() - - def send(self, s, dat): - self.last_updated = s - if isinstance(dat, bytes): - self.data[s] = log.Event.from_bytes(dat) - else: - self.data[s] = dat.as_reader() - self.send_called.set() - wait_for_event(self.get_called) - self.get_called.clear() - - def wait_for_msg(self): - wait_for_event(self.send_called) - self.send_called.clear() - dat = self.data[self.last_updated] - self.get_called.set() - return dat - - -def fingerprint(msgs, fsm, can_sock, fingerprint): +def controlsd_fingerprint_callback(rc, pm, msgs, fingerprint): print("start fingerprinting") - fsm.wait_on_getitem = True + params = Params() + canmsgs = [msg for msg in msgs if msg.which() == "can"][:300] - # populate fake socket with data for fingerprinting - canmsgs = [msg for msg in msgs if msg.which() == "can"] - wait_for_event(can_sock.recv_called) - can_sock.recv_called.clear() - can_sock.data = [msg.as_builder().to_bytes() for msg in canmsgs[:300]] - can_sock.recv_ready.set() - can_sock.wait = False + # controlsd expects one arbitrary can and pandaState + rc.send_sync(pm, "can", messaging.new_message("can", 1)) + pm.send("pandaStates", messaging.new_message("pandaStates", 1)) + rc.send_sync(pm, "can", messaging.new_message("can", 1)) + rc.wait_for_next_recv(True) - # we know fingerprinting is done when controlsd sets sm['lateralPlan'].sensorValid - wait_for_event(fsm.update_called) - fsm.update_called.clear() + # fingerprinting is done, when CarParams is set + while params.get("CarParams") is None: + if len(canmsgs) == 0: + raise ValueError("Fingerprinting failed. Run out of can msgs") - fsm.wait_on_getitem = False - can_sock.wait = True - can_sock.data = [] - - fsm.update_ready.set() + m = canmsgs.pop(0) + rc.send_sync(pm, "can", m.as_builder().to_bytes()) + rc.wait_for_next_recv(False) -def get_car_params(msgs, fsm, can_sock, fingerprint): +def get_car_params_callback(rc, pm, msgs, fingerprint): if fingerprint: CarInterface, _, _ = interfaces[fingerprint] CP = CarInterface.get_non_essential_params(fingerprint) else: - can = FakeSocket(wait=False) - sendcan = FakeSocket(wait=False) + can = DummySocket() + sendcan = DummySocket() + + canmsgs = [msg for msg in msgs if msg.which() == "can"] + assert len(canmsgs) != 0, "CAN messages are required for carParams initialization" - canmsgs = [msg for msg in msgs if msg.which() == 'can'] for m in canmsgs[:300]: can.send(m.as_builder().to_bytes()) _, CP = get_car(can, sendcan, Params().get_bool("ExperimentalLongitudinalEnabled")) Params().put("CarParams", CP.to_bytes()) -def controlsd_rcv_callback(msg, CP, cfg, fsm): +def controlsd_rcv_callback(msg, cfg, frame): # no sendcan until controlsd is initialized - socks = [s for s in cfg.pub_sub[msg.which()] if - (fsm.frame + 1) % int(service_list[msg.which()].frequency / service_list[s].frequency) == 0] - if "sendcan" in socks and fsm.frame < 2000: - socks.remove("sendcan") - return socks, len(socks) > 0 - - -def radar_rcv_callback(msg, CP, cfg, fsm): if msg.which() != "can": - return [], False - elif CP.radarUnavailable: - return ["radarState", "liveTracks"], True + return False - radar_msgs = {"honda": [0x445], "toyota": [0x19f, 0x22f], "gm": [0x474], - "chrysler": [0x2d4]}.get(CP.carName, None) - - if radar_msgs is None: - raise NotImplementedError - - for m in msg.can: - if m.src == 1 and m.address in radar_msgs: - return ["radarState", "liveTracks"], True - return [], False + socks = [ + s for s in cfg.subs if + frame % int(service_list[msg.which()].frequency / service_list[s].frequency) == 0 + ] + if "sendcan" in socks and (frame - 1) < 2000: + socks.remove("sendcan") + return len(socks) > 0 -def calibration_rcv_callback(msg, CP, cfg, fsm): +def radar_rcv_callback(msg, cfg, frame): + return msg.which() == "can" + + +def calibration_rcv_callback(msg, cfg, frame): # calibrationd publishes 1 calibrationData every 5 cameraOdometry packets. # should_recv always true to increment frame - recv_socks = [] - frame = fsm.frame + 1 # incrementing hasn't happened yet in SubMaster - if frame == 0 or (msg.which() == 'cameraOdometry' and (frame % 5) == 0): - recv_socks = ["liveCalibration"] - return recv_socks, fsm.frame == 0 or msg.which() == 'cameraOdometry' + return (frame - 1) == 0 or msg.which() == 'cameraOdometry' -def torqued_rcv_callback(msg, CP, cfg, fsm): +def torqued_rcv_callback(msg, cfg, frame): # should_recv always true to increment frame - recv_socks = [] - frame = fsm.frame + 1 # incrementing hasn't happened yet in SubMaster - if msg.which() == 'liveLocationKalman' and (frame % 5) == 0: - recv_socks = ["liveTorqueParameters"] - return recv_socks, fsm.frame == 0 or msg.which() == 'liveLocationKalman' + return (frame - 1) == 0 or msg.which() == 'liveLocationKalman' -def ublox_rcv_callback(msg, CP, cfg, fsm): - msg_class, msg_id = msg.ubloxRaw[2:4] - if (msg_class, msg_id) in {(1, 7 * 16)}: - return ["gpsLocationExternal"], True - elif (msg_class, msg_id) in {(2, 1 * 16 + 5), (10, 9)}: - return ["ubloxGnss"], True - else: - return [], False +class FrequencyBasedRcvCallback: + def __init__(self, trigger_msg_type): + self.trigger_msg_type = trigger_msg_type + + def __call__(self, msg, cfg, frame): + if msg.which() != self.trigger_msg_type: + return False + + resp_sockets = [ + s for s in cfg.subs + if frame % max(1, int(service_list[msg.which()].frequency / service_list[s].frequency)) == 0 + ] + return bool(len(resp_sockets)) + + +def laikad_config_pubsub_callback(params, cfg): + ublox = params.get_bool("UbloxAvailable") + drained_key = "ubloxGnss" if ublox else "qcomGnss" + sub_keys = ({"qcomGnss", } if ublox else {"ubloxGnss", }) + + return set(cfg.pubs) - sub_keys, drained_key + + +def locationd_config_pubsub_callback(params, cfg): + ublox = params.get_bool("UbloxAvailable") + sub_keys = ({"gpsLocation", } if ublox else {"gpsLocationExternal", }) + + return set(cfg.pubs) - sub_keys, None CONFIGS = [ ProcessConfig( proc_name="controlsd", - pub_sub={ - "can": ["controlsState", "carState", "carControl", "sendcan", "carEvents", "carParams"], - "deviceState": [], "pandaStates": [], "peripheralState": [], "liveCalibration": [], "driverMonitoringState": [], - "longitudinalPlan": [], "lateralPlan": [], "liveLocationKalman": [], "liveParameters": [], "radarState": [], - "modelV2": [], "driverCameraState": [], "roadCameraState": [], "wideRoadCameraState": [], "managerState": [], - "testJoystick": [], "liveTorqueParameters": [], - }, + pubs=[ + "can", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState", + "longitudinalPlan", "lateralPlan", "liveLocationKalman", "liveParameters", "radarState", + "modelV2", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", + "testJoystick", "liveTorqueParameters" + ], + subs=["controlsState", "carState", "carControl", "sendcan", "carEvents", "carParams"], ignore=["logMonoTime", "valid", "controlsState.startMonoTime", "controlsState.cumLagMs"], - init_callback=fingerprint, + config_callback=None, + init_callback=controlsd_fingerprint_callback, should_recv_callback=controlsd_rcv_callback, tolerance=NUMPY_TOLERANCE, - fake_pubsubmaster=True, - submaster_config={ - 'ignore_avg_freq': ['radarState', 'longitudinalPlan', 'driverCameraState', 'driverMonitoringState'], # dcam is expected at 20 Hz - 'ignore_alive': [], - } + simulation=False, + drained_pub="can", ), ProcessConfig( proc_name="radard", - pub_sub={ - "can": ["radarState", "liveTracks"], - "liveParameters": [], "carState": [], "modelV2": [], - }, + pubs=["can", "carState", "modelV2"], + subs=["radarState", "liveTracks"], ignore=["logMonoTime", "valid", "radarState.cumLagMs"], - init_callback=get_car_params, + config_callback=None, + init_callback=get_car_params_callback, should_recv_callback=radar_rcv_callback, - tolerance=None, - fake_pubsubmaster=True, + drained_pub="can", ), ProcessConfig( proc_name="plannerd", - pub_sub={ - "modelV2": ["lateralPlan", "longitudinalPlan", "uiPlan"], - "carControl": [], "carState": [], "controlsState": [], "radarState": [], - }, + pubs=["modelV2", "carControl", "carState", "controlsState", "radarState"], + subs=["lateralPlan", "longitudinalPlan", "uiPlan"], ignore=["logMonoTime", "valid", "longitudinalPlan.processingDelay", "longitudinalPlan.solverExecutionTime", "lateralPlan.solverExecutionTime"], - init_callback=get_car_params, - should_recv_callback=None, + config_callback=None, + init_callback=get_car_params_callback, + should_recv_callback=FrequencyBasedRcvCallback("modelV2"), tolerance=NUMPY_TOLERANCE, - fake_pubsubmaster=True, ), ProcessConfig( proc_name="calibrationd", - pub_sub={ - "carState": ["liveCalibration"], - "cameraOdometry": [], - "carParams": [], - }, + pubs=["carState", "cameraOdometry", "carParams"], + subs=["liveCalibration"], ignore=["logMonoTime", "valid"], - init_callback=get_car_params, + config_callback=None, + init_callback=None, should_recv_callback=calibration_rcv_callback, - tolerance=None, - fake_pubsubmaster=True, ), ProcessConfig( proc_name="dmonitoringd", - pub_sub={ - "driverStateV2": ["driverMonitoringState"], - "liveCalibration": [], "carState": [], "modelV2": [], "controlsState": [], - }, + pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "controlsState"], + subs=["driverMonitoringState"], ignore=["logMonoTime", "valid"], - init_callback=get_car_params, - should_recv_callback=None, + config_callback=None, + init_callback=None, + should_recv_callback=FrequencyBasedRcvCallback("driverStateV2"), tolerance=NUMPY_TOLERANCE, - fake_pubsubmaster=True, ), ProcessConfig( proc_name="locationd", - pub_sub={ - "cameraOdometry": ["liveLocationKalman"], - "accelerometer": [], "gyroscope": [], - "gpsLocationExternal": [], "liveCalibration": [], "carState": [], "gpsLocation": [], - }, + pubs=[ + "cameraOdometry", "accelerometer", "gyroscope", "gpsLocationExternal", + "liveCalibration", "carState", "carParams", "gpsLocation" + ], + subs=["liveLocationKalman"], ignore=["logMonoTime", "valid"], - init_callback=get_car_params, + config_callback=locationd_config_pubsub_callback, + init_callback=None, should_recv_callback=None, tolerance=NUMPY_TOLERANCE, - fake_pubsubmaster=False, ), ProcessConfig( proc_name="paramsd", - pub_sub={ - "liveLocationKalman": ["liveParameters"], - "carState": [] - }, + pubs=["liveLocationKalman", "carState"], + subs=["liveParameters"], ignore=["logMonoTime", "valid"], - init_callback=get_car_params, - should_recv_callback=None, + config_callback=None, + init_callback=get_car_params_callback, + should_recv_callback=FrequencyBasedRcvCallback("liveLocationKalman"), tolerance=NUMPY_TOLERANCE, - fake_pubsubmaster=True, ), ProcessConfig( proc_name="ubloxd", - pub_sub={ - "ubloxRaw": ["ubloxGnss", "gpsLocationExternal"], - }, + pubs=["ubloxRaw"], + subs=["ubloxGnss", "gpsLocationExternal"], ignore=["logMonoTime"], + config_callback=None, init_callback=None, - should_recv_callback=ublox_rcv_callback, - tolerance=None, - fake_pubsubmaster=False, + should_recv_callback=None, ), ProcessConfig( proc_name="laikad", - pub_sub={ - "ubloxGnss": ["gnssMeasurements"], - "qcomGnss": ["gnssMeasurements"], - }, + pubs=["ubloxGnss", "qcomGnss"], + subs=["gnssMeasurements"], ignore=["logMonoTime"], - init_callback=get_car_params, + config_callback=laikad_config_pubsub_callback, + init_callback=None, should_recv_callback=None, tolerance=NUMPY_TOLERANCE, - fake_pubsubmaster=False, timeout=60*10, # first messages are blocked on internet assistance + drained_pub="ubloxGnss", # config_callback will switch this to qcom if needed ), ProcessConfig( proc_name="torqued", - pub_sub={ - "liveLocationKalman": ["liveTorqueParameters"], - "carState": [], "controlsState": [], - }, + pubs=["liveLocationKalman", "carState", "carControl"], + subs=["liveTorqueParameters"], ignore=["logMonoTime"], - init_callback=get_car_params, + config_callback=None, + init_callback=get_car_params_callback, should_recv_callback=torqued_rcv_callback, tolerance=NUMPY_TOLERANCE, - fake_pubsubmaster=True, ), ] -def replay_process(cfg, lr, fingerprint=None): +def get_process_config(name): + try: + return next(c for c in CONFIGS if c.proc_name == name) + except StopIteration as ex: + raise Exception(f"Cannot find process config with name: {name}") from ex + + +def replay_process_with_name(name, lr, *args, **kwargs): + cfg = get_process_config(name) + return replay_process(cfg, lr, *args, **kwargs) + + +def replay_process(cfg, lr, fingerprint=None, return_all_logs=False, disable_progress=False): + all_msgs = migrate_all(lr, old_logtime=True) + process_logs = _replay_single_process(cfg, all_msgs, fingerprint, disable_progress) + + if return_all_logs: + keys = set(cfg.subs) + modified_logs = [m for m in all_msgs if m.which() not in keys] + modified_logs.extend(process_logs) + modified_logs.sort(key=lambda m: m.logMonoTime) + log_msgs = modified_logs + else: + log_msgs = process_logs + + return log_msgs + + +def _replay_single_process(cfg, lr, fingerprint, disable_progress): with OpenpilotPrefix(): - if cfg.fake_pubsubmaster: - return python_replay_process(cfg, lr, fingerprint) + controlsState = None + initialized = False + if cfg.proc_name == "controlsd": + for msg in lr: + if msg.which() == "controlsState": + controlsState = msg.controlsState + if initialized: + break + elif msg.which() == "carEvents": + initialized = car.CarEvent.EventName.controlsInitializing not in [e.name for e in msg.carEvents] + + assert controlsState is not None and initialized, "controlsState never initialized" + + if fingerprint is not None: + setup_env(cfg=cfg, controlsState=controlsState, lr=lr, fingerprint=fingerprint) else: - return replay_process_with_sockets(cfg, lr, fingerprint) + CP = next((m.carParams for m in lr if m.which() == "carParams"), None) + assert CP is not None or "carParams" not in cfg.pubs, "carParams are missing and process needs it" + setup_env(CP=CP, cfg=cfg, controlsState=controlsState, lr=lr) + + if cfg.config_callback is not None: + params = Params() + cfg.pubs, cfg.drained_pub = cfg.config_callback(params, cfg) + + all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) + pub_msgs = [msg for msg in all_msgs if msg.which() in set(cfg.pubs)] + + with ReplayContext(cfg) as rc: + pm = messaging.PubMaster(cfg.pubs) + sockets = {s: messaging.sub_sock(s, timeout=100) for s in cfg.subs} + + managed_processes[cfg.proc_name].prepare() + managed_processes[cfg.proc_name].start() + + if cfg.init_callback is not None: + cfg.init_callback(rc, pm, all_msgs, fingerprint) + + log_msgs, msg_queue = [], [] + try: + # Wait for process to startup + with Timeout(10, error_msg=f"timed out waiting for process to start: {repr(cfg.proc_name)}"): + while not all(pm.all_readers_updated(s) for s in cfg.pubs): + time.sleep(0) + + # Do the replay + cnt = 0 + for msg in tqdm(pub_msgs, disable=disable_progress): + with Timeout(cfg.timeout, error_msg=f"timed out testing process {repr(cfg.proc_name)}, {cnt}/{len(pub_msgs)} msgs done"): + resp_sockets, end_of_cycle = cfg.subs, True + if cfg.should_recv_callback is not None: + end_of_cycle = cfg.should_recv_callback(msg, cfg, cnt) + + msg_queue.append(msg) + if end_of_cycle: + rc.wait_for_recv_called() + + # call recv to let sub-sockets reconnect, after we know the process is ready + if cnt == 0: + for s in sockets.values(): + messaging.recv_one_or_none(s) + + # empty recv on drained pub indicates the end of messages, only do that if there're any + trigger_empty_recv = False + if cfg.drained_pub: + trigger_empty_recv = next((True for m in msg_queue if m.which() == cfg.drained_pub), False) + + for m in msg_queue: + pm.send(m.which(), m.as_builder()) + msg_queue = [] + + rc.unlock_sockets() + rc.wait_for_next_recv(trigger_empty_recv) + + for s in resp_sockets: + ms = messaging.drain_sock(sockets[s]) + for m in ms: + m = m.as_builder() + m.logMonoTime = msg.logMonoTime + log_msgs.append(m.as_reader()) + cnt += 1 + assert(managed_processes[cfg.proc_name].proc.is_alive()) + finally: + managed_processes[cfg.proc_name].signal(signal.SIGKILL) + managed_processes[cfg.proc_name].stop() + + return log_msgs -def setup_env(simulation=False, CP=None, cfg=None, controlsState=None, lr=None): +def setup_env(CP=None, cfg=None, controlsState=None, lr=None, fingerprint=None, log_dir=None): + if platform.system() != "Darwin": + os.environ["PARAMS_ROOT"] = "/dev/shm/params" + if log_dir is not None: + os.environ["LOG_ROOT"] = log_dir + params = Params() params.clear_all() params.put_bool("OpenpilotEnabledToggle", True) params.put_bool("Passive", False) params.put_bool("DisengageOnAccelerator", True) - params.put_bool("WideCameraOnly", False) params.put_bool("DisableLogging", False) os.environ["NO_RADAR_SLEEP"] = "1" os.environ["REPLAY"] = "1" - os.environ["SKIP_FW_QUERY"] = "" - os.environ["FINGERPRINT"] = "" + if fingerprint is not None: + os.environ['SKIP_FW_QUERY'] = "1" + os.environ['FINGERPRINT'] = fingerprint + else: + os.environ["SKIP_FW_QUERY"] = "" + os.environ["FINGERPRINT"] = "" if lr is not None: services = {m.which() for m in lr} @@ -431,7 +484,7 @@ def setup_env(simulation=False, CP=None, cfg=None, controlsState=None, lr=None): os.environ.update(cfg.environ) os.environ['PROC_NAME'] = cfg.proc_name - if simulation: + if cfg is not None and cfg.simulation: os.environ["SIMULATION"] = "1" elif "SIMULATION" in os.environ: del os.environ["SIMULATION"] @@ -447,160 +500,20 @@ def setup_env(simulation=False, CP=None, cfg=None, controlsState=None, lr=None): if CP.alternativeExperience == ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS: params.put_bool("DisengageOnAccelerator", False) - if CP.fingerprintSource == "fw": - params.put("CarParamsCache", CP.as_builder().to_bytes()) - else: - os.environ['SKIP_FW_QUERY'] = "1" - os.environ['FINGERPRINT'] = CP.carFingerprint + if fingerprint is None: + if CP.fingerprintSource == "fw": + params.put("CarParamsCache", CP.as_builder().to_bytes()) + os.environ['SKIP_FW_QUERY'] = "" + os.environ['FINGERPRINT'] = "" + else: + os.environ['SKIP_FW_QUERY'] = "1" + os.environ['FINGERPRINT'] = CP.carFingerprint if CP.openpilotLongitudinalControl: params.put_bool("ExperimentalLongitudinalEnabled", True) -def python_replay_process(cfg, lr, fingerprint=None): - sub_sockets = [s for _, sub in cfg.pub_sub.items() for s in sub] - pub_sockets = [s for s in cfg.pub_sub.keys() if s != 'can'] - - fsm = FakeSubMaster(pub_sockets, **cfg.submaster_config) - fpm = FakePubMaster(sub_sockets) - args = (fsm, fpm) - if 'can' in list(cfg.pub_sub.keys()): - can_sock = FakeSocket() - args = (fsm, fpm, can_sock) - - all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) - pub_msgs = [msg for msg in all_msgs if msg.which() in list(cfg.pub_sub.keys())] - - controlsState = None - initialized = False - for msg in lr: - if msg.which() == 'controlsState': - controlsState = msg.controlsState - if initialized: - break - elif msg.which() == 'carEvents': - initialized = car.CarEvent.EventName.controlsInitializing not in [e.name for e in msg.carEvents] - - assert controlsState is not None and initialized, "controlsState never initialized" - - if fingerprint is not None: - os.environ['SKIP_FW_QUERY'] = "1" - os.environ['FINGERPRINT'] = fingerprint - setup_env(cfg=cfg, controlsState=controlsState, lr=lr) - else: - CP = [m for m in lr if m.which() == 'carParams'][0].carParams - setup_env(CP=CP, cfg=cfg, controlsState=controlsState, lr=lr) - - assert(type(managed_processes[cfg.proc_name]) is PythonProcess) - managed_processes[cfg.proc_name].prepare() - mod = importlib.import_module(managed_processes[cfg.proc_name].module) - - thread = threading.Thread(target=mod.main, args=args) - thread.daemon = True - thread.start() - - if cfg.init_callback is not None: - if 'can' not in list(cfg.pub_sub.keys()): - can_sock = None - cfg.init_callback(all_msgs, fsm, can_sock, fingerprint) - - CP = car.CarParams.from_bytes(Params().get("CarParams", block=True)) - - # wait for started process to be ready - if 'can' in list(cfg.pub_sub.keys()): - can_sock.wait_for_recv() - else: - fsm.wait_for_update() - - log_msgs, msg_queue = [], [] - for msg in pub_msgs: - if cfg.should_recv_callback is not None: - recv_socks, should_recv = cfg.should_recv_callback(msg, CP, cfg, fsm) - else: - recv_socks = [s for s in cfg.pub_sub[msg.which()] if - (fsm.frame + 1) % max(1, int(service_list[msg.which()].frequency / service_list[s].frequency)) == 0] - should_recv = bool(len(recv_socks)) - - if msg.which() == 'can': - can_sock.send(msg.as_builder().to_bytes()) - else: - msg_queue.append(msg.as_builder()) - - if should_recv: - fsm.update_msgs(msg.logMonoTime / 1e9, msg_queue) - msg_queue = [] - - recv_cnt = len(recv_socks) - while recv_cnt > 0: - m = fpm.wait_for_msg().as_builder() - m.logMonoTime = msg.logMonoTime - m = m.as_reader() - - log_msgs.append(m) - recv_cnt -= m.which() in recv_socks - return log_msgs - - -def replay_process_with_sockets(cfg, lr, fingerprint=None): - pm = messaging.PubMaster(cfg.pub_sub.keys()) - sub_sockets = [s for _, sub in cfg.pub_sub.items() for s in sub] - sockets = {s: messaging.sub_sock(s, timeout=100) for s in sub_sockets} - - all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) - pub_msgs = [msg for msg in all_msgs if msg.which() in list(cfg.pub_sub.keys())] - - # We need to fake SubMaster alive since we can't inject a fake clock - setup_env(simulation=True, cfg=cfg, lr=lr) - - if cfg.proc_name == "laikad": - ublox = Params().get_bool("UbloxAvailable") - keys = set(cfg.pub_sub.keys()) - ({"qcomGnss", } if ublox else {"ubloxGnss", }) - pub_msgs = [msg for msg in pub_msgs if msg.which() in keys] - - managed_processes[cfg.proc_name].prepare() - managed_processes[cfg.proc_name].start() - - log_msgs = [] - try: - # Wait for process to startup - with Timeout(10, error_msg=f"timed out waiting for process to start: {repr(cfg.proc_name)}"): - while not any(pm.all_readers_updated(s) for s in cfg.pub_sub.keys()): - time.sleep(0) - - for s in sockets.values(): - messaging.recv_one_or_none(s) - - # Do the replay - cnt = 0 - for msg in pub_msgs: - with Timeout(cfg.timeout, error_msg=f"timed out testing process {repr(cfg.proc_name)}, {cnt}/{len(pub_msgs)} msgs done"): - resp_sockets = cfg.pub_sub[msg.which()] - if cfg.should_recv_callback is not None: - resp_sockets, _ = cfg.should_recv_callback(msg, None, None, None) - - # Make sure all subscribers are connected - if len(log_msgs) == 0 and len(resp_sockets) > 0: - for s in sockets.values(): - messaging.recv_one_or_none(s) - - pm.send(msg.which(), msg.as_builder()) - while not pm.all_readers_updated(msg.which()): - time.sleep(0) - - for s in resp_sockets: - m = messaging.recv_one_retry(sockets[s]) - m = m.as_builder() - m.logMonoTime = msg.logMonoTime - log_msgs.append(m.as_reader()) - cnt += 1 - finally: - managed_processes[cfg.proc_name].signal(signal.SIGKILL) - managed_processes[cfg.proc_name].stop() - - return log_msgs - - -def check_enabled(msgs): +def check_openpilot_enabled(msgs): cur_enabled_count = 0 max_enabled_count = 0 for msg in msgs: diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 4dee64009f..9e0fba3640 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -658e4dd36d92ae7a973a090aaf3fab62fdf701b6 +72b5d91c92a72b2a708526dade78960950c5b595 diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index dea9a737e8..59f0fe703f 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -19,7 +19,8 @@ from panda.python import Panda from selfdrive.car.toyota.values import EPS_SCALE from selfdrive.manager.process import ensure_running from selfdrive.manager.process_config import managed_processes -from selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, setup_env, check_enabled +from selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, setup_env, check_openpilot_enabled +from selfdrive.test.process_replay.migration import migrate_all from selfdrive.test.update_ci_routes import upload_route from tools.lib.route import Route from tools.lib.framereader import FrameReader @@ -119,19 +120,17 @@ def replay_service(s, msgs): def replay_cameras(lr, frs, disable_tqdm=False): eon_cameras = [ - ("roadCameraState", DT_MDL, eon_f_frame_size, VisionStreamType.VISION_STREAM_ROAD, True), - ("driverCameraState", DT_DMON, eon_d_frame_size, VisionStreamType.VISION_STREAM_DRIVER, False), + ("roadCameraState", DT_MDL, eon_f_frame_size, VisionStreamType.VISION_STREAM_ROAD), + ("driverCameraState", DT_DMON, eon_d_frame_size, VisionStreamType.VISION_STREAM_DRIVER), ] tici_cameras = [ - ("roadCameraState", DT_MDL, tici_f_frame_size, VisionStreamType.VISION_STREAM_ROAD, False), - ("wideRoadCameraState", DT_MDL, tici_e_frame_size, VisionStreamType.VISION_STREAM_WIDE_ROAD, False), - ("driverCameraState", DT_DMON, tici_d_frame_size, VisionStreamType.VISION_STREAM_DRIVER, False), + ("roadCameraState", DT_MDL, tici_f_frame_size, VisionStreamType.VISION_STREAM_ROAD), + ("wideRoadCameraState", DT_MDL, tici_e_frame_size, VisionStreamType.VISION_STREAM_WIDE_ROAD), + ("driverCameraState", DT_DMON, tici_d_frame_size, VisionStreamType.VISION_STREAM_DRIVER), ] - def replay_camera(s, stream, dt, vipc_server, frames, size, use_extra_client): + def replay_camera(s, stream, dt, vipc_server, frames, size): services = [(s, stream)] - if use_extra_client: - services.append(("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD)) pm = messaging.PubMaster([s for s, _ in services]) rk = Ratekeeper(1 / dt, print_delay_threshold=None) @@ -153,12 +152,12 @@ def replay_cameras(lr, frs, disable_tqdm=False): vipc_server.send(stream, img, msg.frameId, msg.timestampSof, msg.timestampEof) init_data = [m for m in lr if m.which() == 'initData'][0] - cameras = tici_cameras if (init_data.initData.deviceType == 'tici') else eon_cameras + cameras = tici_cameras if (init_data.initData.deviceType in ['tici', 'tizi']) else eon_cameras # init vipc server and cameras p = [] vs = VisionIpcServer("camerad") - for (s, dt, size, stream, use_extra_client) in cameras: + for (s, dt, size, stream) in cameras: fr = frs.get(s, None) frames = None @@ -170,88 +169,29 @@ def replay_cameras(lr, frs, disable_tqdm=False): frames.append(img.flatten().tobytes()) vs.create_buffers(stream, 40, False, size[0], size[1]) - if use_extra_client: - vs.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, False, size[0], size[1]) p.append(multiprocessing.Process(target=replay_camera, - args=(s, stream, dt, vs, frames, size, use_extra_client))) + args=(s, stream, dt, vs, frames, size))) vs.start_listener() return vs, p -def migrate_carparams(lr): - all_msgs = [] - for msg in lr: - if msg.which() == 'carParams': - CP = messaging.new_message('carParams') - CP.carParams = msg.carParams.as_builder() - for car_fw in CP.carParams.carFw: - car_fw.brand = CP.carParams.carName - msg = CP.as_reader() - all_msgs.append(msg) - - return all_msgs - - -def migrate_sensorEvents(lr, old_logtime=False): - all_msgs = [] - for msg in lr: - if msg.which() != 'sensorEventsDEPRECATED': - all_msgs.append(msg) - continue - - # migrate to split sensor events - for evt in msg.sensorEventsDEPRECATED: - # build new message for each sensor type - sensor_service = '' - if evt.which() == 'acceleration': - sensor_service = 'accelerometer' - elif evt.which() == 'gyro' or evt.which() == 'gyroUncalibrated': - sensor_service = 'gyroscope' - elif evt.which() == 'light' or evt.which() == 'proximity': - sensor_service = 'lightSensor' - elif evt.which() == 'magnetic' or evt.which() == 'magneticUncalibrated': - sensor_service = 'magnetometer' - elif evt.which() == 'temperature': - sensor_service = 'temperatureSensor' - - m = messaging.new_message(sensor_service) - m.valid = True - if old_logtime: - m.logMonoTime = msg.logMonoTime - - m_dat = getattr(m, sensor_service) - m_dat.version = evt.version - m_dat.sensor = evt.sensor - m_dat.type = evt.type - m_dat.source = evt.source - if old_logtime: - m_dat.timestamp = evt.timestamp - setattr(m_dat, evt.which(), getattr(evt, evt.which())) - - all_msgs.append(m.as_reader()) - - return all_msgs - - def regen_segment(lr, frs=None, daemons="all", outdir=FAKEDATA, disable_tqdm=False): if not isinstance(daemons, str) and not hasattr(daemons, "__iter__"): raise ValueError("whitelist_proc must be a string or iterable") - lr = migrate_carparams(list(lr)) - lr = migrate_sensorEvents(list(lr)) + lr = migrate_all(lr) if frs is None: frs = dict() - params = Params() - os.environ["LOG_ROOT"] = outdir - # Get and setup initial state CP = [m for m in lr if m.which() == 'carParams'][0].carParams controlsState = [m for m in lr if m.which() == 'controlsState'][0].controlsState liveCalibration = [m for m in lr if m.which() == 'liveCalibration'][0] - setup_env(CP=CP, controlsState=controlsState) + setup_env(CP=CP, controlsState=controlsState, log_dir=outdir) + + params = Params() params.put("CalibrationParams", liveCalibration.as_builder().to_bytes()) vs, cam_procs = replay_cameras(lr, frs, disable_tqdm=disable_tqdm) @@ -283,10 +223,9 @@ def regen_segment(lr, frs=None, daemons="all", outdir=FAKEDATA, disable_tqdm=Fal # TODO add configs for modeld, dmonitoringmodeld fakeable_daemons = {} for config in CONFIGS: - replayable_messages = set([msg for sub in config.pub_sub.values() for msg in sub]) processes = [ multiprocessing.Process(target=replay_service, args=(msg, lr)) - for msg in replayable_messages + for msg in config.subs ] fakeable_daemons[config.proc_name] = processes @@ -345,7 +284,7 @@ def regen_segment(lr, frs=None, daemons="all", outdir=FAKEDATA, disable_tqdm=Fal segment = params.get("CurrentRoute", encoding='utf-8') + "--0" seg_path = os.path.join(outdir, segment) # check to make sure openpilot is engaged in the route - if not check_enabled(LogReader(os.path.join(seg_path, "rlog"))): + if not check_openpilot_enabled(LogReader(os.path.join(seg_path, "rlog"))): raise Exception(f"Route did not engage for long enough: {segment}") return seg_path @@ -364,7 +303,7 @@ def regen_and_save(route, sidx, daemons="all", upload=False, use_route_meta=Fals lr = LogReader(f"cd:/{route.replace('|', '/')}/{sidx}/rlog.bz2") fr = FrameReader(f"cd:/{route.replace('|', '/')}/{sidx}/fcamera.hevc") device_type = next(iter(lr)).initData.deviceType - if device_type == 'tici': + if device_type in ['tici', 'tizi']: wfr = FrameReader(f"cd:/{route.replace('|', '/')}/{sidx}/ecamera.hevc") else: wfr = None diff --git a/selfdrive/test/process_replay/test_fuzzy.py b/selfdrive/test/process_replay/test_fuzzy.py index 29fdf641f1..12f5fca37d 100755 --- a/selfdrive/test/process_replay/test_fuzzy.py +++ b/selfdrive/test/process_replay/test_fuzzy.py @@ -99,7 +99,7 @@ def get_strategy_for_events(event_types, finite=False): def get_strategy_for_process(process, finite=False): - return get_strategy_for_events(get_process_config(process).pub_sub.keys(), finite) + return get_strategy_for_events(get_process_config(process).pubs, finite) def convert_to_lr(msgs): diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index deed1ec48f..6962e51cb5 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -9,11 +9,12 @@ from typing import Any, DefaultDict, Dict from selfdrive.car.car_helpers import interface_names from selfdrive.test.openpilotci import get_url, upload_file -from selfdrive.test.process_replay.compare_logs import compare_logs, save_log -from selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, check_enabled, replay_process +from selfdrive.test.process_replay.compare_logs import compare_logs +from selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, check_openpilot_enabled, replay_process from system.version import get_commit from tools.lib.filereader import FileReader from tools.lib.logreader import LogReader +from tools.lib.helpers import save_log source_segments = [ ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.BODY @@ -98,13 +99,13 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non ref_log_msgs = list(LogReader(ref_log_path)) try: - log_msgs = replay_process(cfg, lr) + log_msgs = replay_process(cfg, lr, disable_progress=True) except Exception as e: raise Exception("failed on segment: " + segment) from e # check to make sure openpilot is engaged in the route if cfg.proc_name == "controlsd": - if not check_enabled(log_msgs): + if not check_openpilot_enabled(log_msgs): return f"Route did not enable at all or for long enough: {new_log_path}", log_msgs try: diff --git a/selfdrive/test/profiling/profiler.py b/selfdrive/test/profiling/profiler.py index 732a69eebd..a0940b327b 100755 --- a/selfdrive/test/profiling/profiler.py +++ b/selfdrive/test/profiling/profiler.py @@ -25,7 +25,7 @@ CARS = { def get_inputs(msgs, process, fingerprint): for config in CONFIGS: if config.proc_name == process: - sub_socks = list(config.pub_sub.keys()) + sub_socks = list(config.pubs) trigger = sub_socks[0] break diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 22ddb6e9cc..ef6ce65201 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -18,6 +18,7 @@ from common.basedir import BASEDIR from common.timeout import Timeout from common.params import Params from selfdrive.controls.lib.events import EVENTS, ET +from system.hardware import HARDWARE from system.loggerd.config import ROOT from selfdrive.test.helpers import set_params_enabled, release_only from tools.lib.logreader import LogReader @@ -25,17 +26,16 @@ from tools.lib.logreader import LogReader # Baseline CPU usage by process PROCS = { "selfdrive.controls.controlsd": 39.0, - "./loggerd": 10.0, + "./loggerd": 14.0, "./encoderd": 17.0, "./camerad": 14.5, "./locationd": 11.0, "selfdrive.controls.plannerd": 16.5, - "./_ui": 19.2, + "./_ui": 21.0, "selfdrive.locationd.paramsd": 9.0, "./_sensord": 12.0, "selfdrive.controls.radard": 4.5, "./_modeld": 4.48, - "./boardd": 3.63, "./_dmonitoringmodeld": 5.0, "selfdrive.thermald.thermald": 3.87, "selfdrive.locationd.calibrationd": 2.0, @@ -45,20 +45,30 @@ PROCS = { "./proclogd": 1.54, "system.logmessaged": 0.2, "./clocksd": 0.02, - "./ubloxd": 0.02, "selfdrive.tombstoned": 0, "./logcatd": 0, "system.micd": 10.0, "system.timezoned": 0, - "system.sensord.pigeond": 6.0, "selfdrive.boardd.pandad": 0, "selfdrive.statsd": 0.4, "selfdrive.navd.navd": 0.4, - "system.loggerd.uploader": 4.0, + "system.loggerd.uploader": 3.0, "system.loggerd.deleter": 0.1, "selfdrive.locationd.laikad": None, # TODO: laikad cpu usage is sporadic } +PROCS.update({ + "tici": { + "./boardd": 4.0, + "./ubloxd": 0.02, + "system.sensord.pigeond": 6.0, + }, + "tizi": { + "./boardd": 19.0, + "system.sensord.rawgps.rawgpsd": 1.0, + } +}[HARDWARE.get_device_type()]) + TIMINGS = { # rtols: max/min, rsd "can": [2.5, 0.35], @@ -95,17 +105,15 @@ class TestOnroad(unittest.TestCase): return # setup env - os.environ['PASSIVE'] = "0" - os.environ['REPLAY'] = "1" - os.environ['SKIP_FW_QUERY'] = "1" - os.environ['FINGERPRINT'] = "TOYOTA COROLLA TSS2 2019" - os.environ['LOGPRINT'] = "debug" - params = Params() - params.clear_all() + if "CI" in os.environ: + params.clear_all() + params.remove("CurrentRoute") set_params_enabled() + os.environ['TESTING_CLOSET'] = '1' if os.path.exists(ROOT): shutil.rmtree(ROOT) + os.system("rm /dev/shm/*") # Make sure athena isn't running os.system("pkill -9 -f athena") @@ -161,11 +169,11 @@ class TestOnroad(unittest.TestCase): for s, msgs in self.service_msgs.items(): if s in ('initData', 'sentinel'): continue - + # skip gps services for now - if s in ('ubloxGnss', 'ubloxRaw', 'gnssMeasurements'): + if s in ('ubloxGnss', 'ubloxRaw', 'gnssMeasurements', 'gpsLocation', 'gpsLocationExternal', 'qcomGnss'): continue - + with self.subTest(service=s): assert len(msgs) >= math.floor(service_list[s].frequency*55) @@ -249,6 +257,14 @@ class TestOnroad(unittest.TestCase): self.assertTrue(cpu_ok) + def test_memory_usage(self): + mems = [m.deviceState.memoryUsagePercent for m in self.service_msgs['deviceState']] + print("Memory usage: ", mems) + + # check for big leaks. note that memory usage is + # expected to go up while the MSGQ buffers fill up + self.assertLessEqual(max(mems) - min(mems), 3.0) + def test_camera_processing_time(self): result = "\n" result += "------------------------------------------------\n" @@ -263,6 +279,23 @@ class TestOnroad(unittest.TestCase): result += "------------------------------------------------\n" print(result) + @unittest.skip("TODO: enable once timings are fixed") + def test_camera_frame_timings(self): + result = "\n" + result += "------------------------------------------------\n" + result += "----------------- SoF Timing ------------------\n" + result += "------------------------------------------------\n" + for name in ['roadCameraState', 'wideRoadCameraState', 'driverCameraState']: + ts = [getattr(getattr(m, m.which()), "timestampSof") for m in self.lr if name in m.which()] + d_ms = np.diff(ts) / 1e6 + d50 = np.abs(d_ms-50) + self.assertLess(max(d50), 1.0, f"high sof delta vs 50ms: {max(d50)}") + result += f"{name} sof delta vs 50ms: min {min(d50):.5f}s\n" + result += f"{name} sof delta vs 50ms: max {max(d50):.5f}s\n" + result += f"{name} sof delta vs 50ms: mean {d50.mean():.5f}s\n" + result += "------------------------------------------------\n" + print(result) + def test_mpc_execution_timings(self): result = "\n" result += "------------------------------------------------\n" diff --git a/selfdrive/test/test_time_to_onroad.py b/selfdrive/test/test_time_to_onroad.py new file mode 100755 index 0000000000..5991250945 --- /dev/null +++ b/selfdrive/test/test_time_to_onroad.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import os +import time +import subprocess + +import cereal.messaging as messaging +from common.basedir import BASEDIR +from common.timeout import Timeout +from selfdrive.test.helpers import set_params_enabled + + +def test_time_to_onroad(): + # launch + set_params_enabled() + manager_path = os.path.join(BASEDIR, "selfdrive/manager/manager.py") + proc = subprocess.Popen(["python", manager_path]) + + start_time = time.monotonic() + sm = messaging.SubMaster(['controlsState', 'deviceState']) + try: + # wait for onroad + with Timeout(20, "timed out waiting to go onroad"): + while True: + sm.update(1000) + if sm['deviceState'].started: + break + time.sleep(1) + + # wait for engageability + with Timeout(10, "timed out waiting for engageable"): + while True: + sm.update(1000) + if sm['controlsState'].engageable: + break + time.sleep(1) + print(f"engageable after {time.monotonic() - start_time:.2f}s") + + # once we're enageable, must be for the next few seconds + for _ in range(500): + sm.update(100) + assert sm['controlsState'].engageable + finally: + proc.terminate() + if proc.wait(60) is None: + proc.kill() diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index 57387e5186..d3d53b7d89 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import datetime import os +import json import queue import threading import time @@ -13,6 +14,7 @@ import psutil import cereal.messaging as messaging from cereal import log from common.dict_helpers import strip_deprecated_keys +from common.time import MIN_DATE from common.filter_simple import FirstOrderFilter from common.params import Params from common.realtime import DT_TRML, sec_since_boot @@ -41,12 +43,12 @@ HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'ne THERMAL_BANDS = OrderedDict({ ThermalStatus.green: ThermalBand(None, 80.0), ThermalStatus.yellow: ThermalBand(75.0, 96.0), - ThermalStatus.red: ThermalBand(80.0, 107.), + ThermalStatus.red: ThermalBand(88.0, 107.), ThermalStatus.danger: ThermalBand(94.0, None), }) # Override to highest thermal band when offroad and above this temp -OFFROAD_DANGER_TEMP = 79.5 +OFFROAD_DANGER_TEMP = 77 prev_offroad_states: Dict[str, Tuple[bool, Optional[str]]] = {} @@ -165,7 +167,8 @@ def thermald_thread(end_event, hw_queue): off_ts = None started_ts = None started_seen = False - thermal_status = ThermalStatus.green + startup_blocked_ts = None + thermal_status = ThermalStatus.yellow last_hw_state = HardwareState( network_type=NetworkType.none, @@ -177,8 +180,8 @@ def thermald_thread(end_event, hw_queue): modem_temps=[], ) - all_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_TRML) - offroad_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_TRML) + all_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_TRML, initialized=False) + offroad_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_TRML, initialized=False) should_start_prev = False in_car = False engaged_prev = False @@ -240,7 +243,7 @@ def thermald_thread(end_event, hw_queue): msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness() - # this one is only used for offroad + # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, max(msg.deviceState.cpuTempC), @@ -251,14 +254,15 @@ def thermald_thread(end_event, hw_queue): # this drives the thermal status while onroad temp_sources.append(max(msg.deviceState.pmicTempC)) all_comp_temp = all_temp_filter.update(max(temp_sources)) + msg.deviceState.maxTempC = all_comp_temp if fan_controller is not None: msg.deviceState.fanSpeedPercentDesired = fan_controller.update(all_comp_temp, onroad_conditions["ignition"]) is_offroad_for_5_min = (started_ts is None) and ((not started_seen) or (off_ts is None) or (sec_since_boot() - off_ts > 60 * 5)) if is_offroad_for_5_min and offroad_comp_temp > OFFROAD_DANGER_TEMP: - # If device is offroad we want to cool down before going onroad - # since going onroad increases load and can make temps go over 107 + # if device is offroad and already hot without the extra onroad load, + # we want to cool down first before increasing load thermal_status = ThermalStatus.danger else: current_band = THERMAL_BANDS[thermal_status] @@ -272,13 +276,12 @@ def thermald_thread(end_event, hw_queue): # Ensure date/time are valid now = datetime.datetime.utcnow() - startup_conditions["time_valid"] = (now.year > 2020) or (now.year == 2020 and now.month >= 10) + startup_conditions["time_valid"] = now > MIN_DATE set_offroad_alert_if_changed("Offroad_InvalidTime", (not startup_conditions["time_valid"])) startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate") startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall") startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version - startup_conditions["offroad_min_time"] = (not started_seen) or ((off_ts is not None) and (sec_since_boot() - off_ts) > 5.) # with 2% left, we killall, otherwise the phone will take a long time to boot startup_conditions["free_space"] = msg.deviceState.freeSpacePercent > 2 @@ -286,8 +289,11 @@ def thermald_thread(end_event, hw_queue): params.get_bool("Passive") startup_conditions["not_driver_view"] = not params.get_bool("IsDriverViewEnabled") startup_conditions["not_taking_snapshot"] = not params.get_bool("IsTakingSnapshot") - # if any CPU gets above 107 or the battery gets above 63, kill all processes - # controls will warn with CPU above 95 or battery above 60 + + # must be at an engageable thermal band to go onroad + startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.red + + # if the temperature enters the danger zone, go offroad to cool down onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", (not onroad_conditions["device_temp_good"])) @@ -334,10 +340,16 @@ def thermald_thread(end_event, hw_queue): if started_ts is None: started_ts = sec_since_boot() started_seen = True + if startup_blocked_ts is not None: + cloudlog.event("Startup after block", block_duration=(sec_since_boot() - startup_blocked_ts), + startup_conditions=startup_conditions, onroad_conditions=onroad_conditions, + startup_conditions_prev=startup_conditions_prev, error=True) + startup_blocked_ts = None else: if onroad_conditions["ignition"] and (startup_conditions != startup_conditions_prev): cloudlog.event("Startup blocked", startup_conditions=startup_conditions, onroad_conditions=onroad_conditions, error=True) startup_conditions_prev = startup_conditions.copy() + startup_blocked_ts = sec_since_boot() started_ts = None if off_ts is None: @@ -371,8 +383,6 @@ def thermald_thread(end_event, hw_queue): msg.deviceState.thermalStatus = thermal_status pm.send("deviceState", msg) - should_start_prev = should_start - # Log to statsd statlog.gauge("free_space_percent", msg.deviceState.freeSpacePercent) statlog.gauge("gpu_usage_percent", msg.deviceState.gpuUsagePercent) @@ -395,15 +405,26 @@ def thermald_thread(end_event, hw_queue): statlog.gauge("screen_brightness_percent", msg.deviceState.screenBrightnessPercent) # report to server once every 10 minutes - if (count % int(600. / DT_TRML)) == 0: - cloudlog.event("STATUS_PACKET", - count=count, - pandaStates=[strip_deprecated_keys(p.to_dict()) for p in pandaStates], - peripheralState=strip_deprecated_keys(peripheralState.to_dict()), - location=(strip_deprecated_keys(sm["gpsLocationExternal"].to_dict()) if sm.alive["gpsLocationExternal"] else None), - deviceState=strip_deprecated_keys(msg.to_dict())) + rising_edge_started = should_start and not should_start_prev + if rising_edge_started or (count % int(600. / DT_TRML)) == 0: + dat = { + 'count': count, + 'pandaStates': [strip_deprecated_keys(p.to_dict()) for p in pandaStates], + 'peripheralState': strip_deprecated_keys(peripheralState.to_dict()), + 'location': (strip_deprecated_keys(sm["gpsLocationExternal"].to_dict()) if sm.alive["gpsLocationExternal"] else None), + 'deviceState': strip_deprecated_keys(msg.to_dict()) + } + cloudlog.event("STATUS_PACKET", **dat) + + # save last one before going onroad + if rising_edge_started: + try: + params.put("LastOffroadStatusPacket", json.dumps(dat)) + except Exception: + cloudlog.exception("failed to save offroad status") count += 1 + should_start_prev = should_start def main(): diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index c8fff103fd..16cec0eb43 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -21,7 +21,7 @@ if arch == "Darwin": qt_env['FRAMEWORKS'] += ['OpenCL'] qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"], LIBS=base_libs) -widgets_src = ["ui.cc", "qt/widgets/input.cc", "qt/widgets/drive_stats.cc", +widgets_src = ["ui.cc", "qt/widgets/input.cc", "qt/widgets/drive_stats.cc", "qt/widgets/wifi.cc", "qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc", "qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc", @@ -73,7 +73,8 @@ if screenrecorder: qt_env.Program("_ui", qt_src + [asset_obj], LIBS=qt_libs) if GetOption('test'): qt_src.remove("main.cc") # replaced by test_runner - #qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) + qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) + qt_env.Program('tests/ui_snapshot', [asset_obj, "tests/ui_snapshot.cc"] + qt_src, LIBS=qt_libs) # build translation files diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index d1b04a0d50..d0f3cd44e1 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include "selfdrive/ui/qt/offroad/experimental_mode.h" @@ -119,11 +120,11 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { QVBoxLayout* main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(40, 40, 40, 45); + main_layout->setContentsMargins(40, 40, 40, 40); // top header QHBoxLayout* header_layout = new QHBoxLayout(); - header_layout->setContentsMargins(15, 15, 15, 0); + header_layout->setContentsMargins(0, 0, 0, 0); header_layout->setSpacing(16); update_notif = new QPushButton(tr("UPDATE")); @@ -147,27 +148,42 @@ 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); + QWidget *home_widget = new QWidget(this); + { + QHBoxLayout *home_layout = new QHBoxLayout(home_widget); + home_layout->setContentsMargins(0, 0, 0, 0); + home_layout->setSpacing(30); - ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this); - QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings); + // left: DriveStats/PrimeAdWidget + QStackedWidget *left_widget = new QStackedWidget(this); + left_widget->addWidget(new DriveStats); + left_widget->addWidget(new PrimeAdWidget); - statsAndExperimentalModeButton->addWidget(experimental_mode, 1); - statsAndExperimentalModeButton->addWidget(new DriveStats, 1); + left_widget->setCurrentIndex(uiState()->primeType() ? 0 : 1); + connect(uiState(), &UIState::primeTypeChanged, [=](int prime_type) { + left_widget->setCurrentIndex(prime_type ? 0 : 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(statsAndExperimentalModeButtonWidget, 1); - statsAndSetup->addWidget(new SetupWidget); + home_layout->addWidget(left_widget, 1); - center_layout->addWidget(statsAndSetupWidget); + // right: ExperimentalModeButton, SetupWidget + QWidget* right_widget = new QWidget(this); + QVBoxLayout* right_column = new QVBoxLayout(right_widget); + right_column->setContentsMargins(0, 0, 0, 0); + right_widget->setFixedWidth(750); + right_column->setSpacing(30); + + ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this); + QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings); + right_column->addWidget(experimental_mode, 1); + + SetupWidget *setup_widget = new SetupWidget; + QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings); + right_column->addWidget(setup_widget, 1); + + home_layout->addWidget(right_widget, 1); + } + center_layout->addWidget(home_widget); // add update & alerts widgets update_widget = new UpdateAlert(); @@ -185,7 +201,7 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { setStyleSheet(R"( * { - color: white; + color: white; } OffroadHome { background-color: black; diff --git a/selfdrive/ui/qt/maps/map_settings.cc b/selfdrive/ui/qt/maps/map_settings.cc index 8936226e77..cf4a46e594 100644 --- a/selfdrive/ui/qt/maps/map_settings.cc +++ b/selfdrive/ui/qt/maps/map_settings.cc @@ -13,46 +13,46 @@ static QString shorten(const QString &str, int max_len) { } MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { - stack = new QStackedWidget; + QStackedLayout *stack = new QStackedLayout(this); - QWidget * main_widget = new QWidget; + QWidget *main_widget = new QWidget; QVBoxLayout *main_layout = new QVBoxLayout(main_widget); - const int icon_size = 200; - - // Home - QHBoxLayout *home_layout = new QHBoxLayout; - home_button = new QPushButton; - home_button->setIconSize(QSize(icon_size, icon_size)); - home_layout->addWidget(home_button); - - home_address = new QLabel; - home_address->setWordWrap(true); - home_layout->addSpacing(30); - home_layout->addWidget(home_address); - home_layout->addStretch(); - - // Work - QHBoxLayout *work_layout = new QHBoxLayout; - work_button = new QPushButton; - work_button->setIconSize(QSize(icon_size, icon_size)); - work_layout->addWidget(work_button); - - work_address = new QLabel; - work_address->setWordWrap(true); - work_layout->addSpacing(30); - work_layout->addWidget(work_address); - work_layout->addStretch(); + main_layout->setSpacing(20); // Home & Work layout QHBoxLayout *home_work_layout = new QHBoxLayout; - home_work_layout->addLayout(home_layout, 1); - home_work_layout->addSpacing(50); - home_work_layout->addLayout(work_layout, 1); + { + // Home + home_button = new QPushButton; + home_button->setIconSize(QSize(MAP_PANEL_ICON_SIZE, MAP_PANEL_ICON_SIZE)); + home_address = new QLabel; + home_address->setWordWrap(true); + + QHBoxLayout *home_layout = new QHBoxLayout; + home_layout->addWidget(home_button); + home_layout->addSpacing(30); + home_layout->addWidget(home_address); + home_layout->addStretch(); + + // Work + work_button = new QPushButton; + work_button->setIconSize(QSize(MAP_PANEL_ICON_SIZE, MAP_PANEL_ICON_SIZE)); + work_address = new QLabel; + work_address->setWordWrap(true); + + QHBoxLayout *work_layout = new QHBoxLayout; + work_layout->addWidget(work_button); + work_layout->addSpacing(30); + work_layout->addWidget(work_address); + work_layout->addStretch(); + + home_work_layout->addLayout(home_layout, 1); + home_work_layout->addSpacing(50); + home_work_layout->addLayout(work_layout, 1); + } main_layout->addLayout(home_work_layout); - main_layout->addSpacing(20); main_layout->addWidget(horizontal_line()); - main_layout->addSpacing(20); // Current route { @@ -81,7 +81,6 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { QLabel *recents_title = new QLabel(tr("Recent Destinations")); recents_title->setStyleSheet("font-size: 55px"); main_layout->addWidget(recents_title); - main_layout->addSpacing(20); recent_layout = new QVBoxLayout; QWidget *recent_widget = new LayoutWidget(recent_layout, this); @@ -122,9 +121,6 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { stack->setCurrentIndex(!custom_mapbox ? uiState()->primeType() ? 0 : 1 : 1); }); - QVBoxLayout *wrapper = new QVBoxLayout(this); - wrapper->addWidget(stack); - clear(); @@ -214,8 +210,9 @@ void MapPanel::refresh() { prev_destinations = cur_destinations; clear(); + // add favorites before recents bool has_recents = false; - for (auto &save_type: {"favorite", "recent"}) { + for (auto &save_type: {NAV_TYPE_FAVORITE, NAV_TYPE_RECENT}) { for (auto location : doc.array()) { auto obj = location.toObject(); @@ -226,7 +223,7 @@ void MapPanel::refresh() { if (type != save_type) continue; - if (type == "favorite" && label == "home") { + if (type == NAV_TYPE_FAVORITE && label == NAV_FAVORITE_LABEL_HOME) { home_address->setText(name); home_address->setStyleSheet(R"(font-size: 50px; color: white;)"); home_button->setIcon(QPixmap("../assets/navigation/home.png")); @@ -234,7 +231,7 @@ void MapPanel::refresh() { navigateTo(obj); emit closeSettings(); }); - } else if (type == "favorite" && label == "work") { + } else if (type == NAV_TYPE_FAVORITE && label == NAV_FAVORITE_LABEL_WORK) { work_address->setText(name); work_address->setStyleSheet(R"(font-size: 50px; color: white;)"); work_button->setIcon(QPixmap("../assets/navigation/work.png")); @@ -252,7 +249,7 @@ void MapPanel::refresh() { sp.setRetainSizeWhenHidden(true); star->setSizePolicy(sp); - star->setVisible(type == "favorite"); + star->setVisible(type == NAV_TYPE_FAVORITE); star->setStyleSheet(R"(font-size: 60px;)"); layout->addWidget(star); layout->addSpacing(10); @@ -291,7 +288,6 @@ void MapPanel::refresh() { has_recents = true; } } - } if (!has_recents) { diff --git a/selfdrive/ui/qt/maps/map_settings.h b/selfdrive/ui/qt/maps/map_settings.h index f46d7a7555..11c6d47e64 100644 --- a/selfdrive/ui/qt/maps/map_settings.h +++ b/selfdrive/ui/qt/maps/map_settings.h @@ -11,6 +11,14 @@ #include "common/params.h" #include "selfdrive/ui/qt/widgets/controls.h" +const int MAP_PANEL_ICON_SIZE = 200; + +const QString NAV_TYPE_FAVORITE = "favorite"; +const QString NAV_TYPE_RECENT = "recent"; + +const QString NAV_FAVORITE_LABEL_HOME = "home"; +const QString NAV_FAVORITE_LABEL_WORK = "work"; + class MapPanel : public QWidget { Q_OBJECT public: @@ -27,7 +35,6 @@ private: Params params; QString prev_destinations, cur_destinations; - QStackedWidget *stack; QPushButton *home_button, *work_button; QLabel *home_address, *work_address; QVBoxLayout *recent_layout; diff --git a/selfdrive/ui/qt/offroad/onboarding.cc b/selfdrive/ui/qt/offroad/onboarding.cc index 64cb357994..72c341fdf9 100644 --- a/selfdrive/ui/qt/offroad/onboarding.cc +++ b/selfdrive/ui/qt/offroad/onboarding.cc @@ -111,6 +111,9 @@ void TermsPage::showEvent(QShowEvent *event) { QPushButton { background-color: #465BEA; } + QPushButton:pressed { + background-color: #3049F4; + } QPushButton:disabled { background-color: #4F4F4F; } diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 7e736f754d..bb1ebf52a5 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -31,7 +31,7 @@ #include "selfdrive/ui/qt/widgets/input.h" TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { - // param, title, desc, icon, confirm + // param, title, desc, icon std::vector> toggle_defs{ { "OpenpilotEnabledToggle", @@ -39,12 +39,6 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { tr("Use the sunnypilot 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("openpilot Longitudinal Control (Alpha)"), @@ -59,18 +53,24 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { tr("When enabled, sunnypilot will attempt to control stock longitudinal control with ACC button presses.\nThis feature must be used along with SLC, and/or V-TSC, and/or M-TSC."), "../assets/offroad/icon_speed_limit.png", }, + { + "ExperimentalMode", + tr("Experimental Mode"), + "", + "../assets/img_experimental_white.svg", + }, + { + "DisengageOnAccelerator", + tr("Disengage on Accelerator Pedal"), + tr("When enabled, pressing the accelerator pedal will disengage openpilot."), + "../assets/offroad/icon_disengage_on_accelerator.svg", + }, { "IsLdwEnabled", tr("Enable Lane Departure Warnings"), tr("Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."), "../assets/offroad/icon_warning.png", }, - { - "IsMetric", - tr("Use Metric System"), - tr("Display speed in km/h instead of mph."), - "../assets/offroad/icon_metric.png", - }, { "RecordFront", tr("Record and Upload Driver Camera"), @@ -78,10 +78,10 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "../assets/offroad/icon_monitoring.png", }, { - "DisengageOnAccelerator", - tr("Disengage on Accelerator Pedal"), - tr("When enabled, pressing the accelerator pedal will disengage sunnypilot."), - "../assets/offroad/icon_disengage_on_accelerator.svg", + "IsMetric", + tr("Use Metric System"), + tr("Display speed in km/h instead of mph."), + "../assets/offroad/icon_metric.png", }, #ifdef ENABLE_MAPS { @@ -99,6 +99,12 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { #endif }; + + std::vector longi_button_texts{tr("Aggressive"), tr("Standard"), tr("Relaxed")}; + long_personality_setting = new ButtonParamControl("LongitudinalPersonality", tr("Driving Personality"), + tr("Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars."), + "../assets/offroad/icon_speed_limit.png", + longi_button_texts); for (auto &[param, title, desc, icon] : toggle_defs) { auto toggle = new ParamControl(param, title, desc, icon, this); @@ -107,6 +113,11 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { addItem(toggle); toggles[param.toStdString()] = toggle; + + // insert longitudinal personality after NDOG toggle + if (param == "DisengageOnAccelerator") { + addItem(long_personality_setting); + } } // Toggles with confirmation dialogs @@ -144,6 +155,7 @@ void TogglesPanel::updateToggles() { .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.")); + long_personality_setting->setEnabled(false); const bool is_release = params.getBool("IsReleaseBranch"); auto cp_bytes = params.get("CarParamsPersistent"); if (!cp_bytes.empty()) { @@ -167,6 +179,7 @@ void TogglesPanel::updateToggles() { // normal description and toggle e2e_toggle->setEnabled(true); e2e_toggle->setDescription(e2e_description); + long_personality_setting->setEnabled(true); custom_stock_long_toggle->setEnabled(false); params.remove("CustomStockLong"); } else { @@ -180,7 +193,7 @@ void TogglesPanel::updateToggles() { tr("openpilot longitudinal control may come in a future update."); if (CP.getExperimentalLongitudinalAvailable()) { if (is_release) { - long_desc = unavailable + " " + tr("An experimental version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches."); + long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches."); } else { long_desc = tr("Enable experimental longitudinal control to allow Experimental mode."); } diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index e7b629a5ac..f28753cc29 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -72,6 +72,7 @@ public slots: private: Params params; std::map toggles; + ButtonParamControl *long_personality_setting; void updateToggles(); }; diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc index c962bd8240..677ce350a6 100644 --- a/selfdrive/ui/qt/offroad/software_settings.cc +++ b/selfdrive/ui/qt/offroad/software_settings.cc @@ -186,19 +186,19 @@ void SoftwarePanel::updateLabels() { downloadBtn->setValue(updater_state); } else { if (failed) { - downloadBtn->setText("CHECK"); - downloadBtn->setValue("failed to check for update"); + downloadBtn->setText(tr("CHECK")); + downloadBtn->setValue(tr("failed to check for update")); } else if (params.getBool("UpdaterFetchAvailable")) { - downloadBtn->setText("DOWNLOAD"); - downloadBtn->setValue("update available"); + downloadBtn->setText(tr("DOWNLOAD")); + downloadBtn->setValue(tr("update available")); } else { - QString lastUpdate = "never"; + QString lastUpdate = tr("never"); auto tm = params.get("LastUpdateTime"); if (!tm.empty()) { lastUpdate = timeAgo(QDateTime::fromString(QString::fromStdString(tm + "Z"), Qt::ISODate)); } - downloadBtn->setText("CHECK"); - downloadBtn->setValue("up to date, last checked " + lastUpdate); + downloadBtn->setText(tr("CHECK")); + downloadBtn->setValue(tr("up to date, last checked %1").arg(lastUpdate)); } downloadBtn->setEnabled(true); } diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 7226ff1b5c..237ece9df5 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -203,7 +203,7 @@ void OnroadWindow::offroadTransition(bool offroad) { QObject::connect(uiState(), &UIState::offroadTransition, m, &MapWindow::offroadTransition); - m->setFixedWidth(topWidget(this)->width() / 2); + m->setFixedWidth(topWidget(this)->width() / 2 - bdr_s); split->insertWidget(0, m); // Make map visible after adding to split @@ -485,13 +485,17 @@ void AnnotatedCameraWidget::updateState(const UIState &s) { // update engageability/experimental mode button experimental_btn->updateState(s); + // update DM icon + auto dm_state = sm["driverMonitoringState"].getDriverMonitoringState(); + setProperty("dmActive", dm_state.getIsActiveMode()); + setProperty("rightHandDM", dm_state.getIsRHD()); + // DM icon transition + dm_fade_state = std::clamp(dm_fade_state+0.2*(0.5-dmActive), 0.0, 1.0); + const auto lp = sm["longitudinalPlan"].getLongitudinalPlan(); // update DM icons at 2Hz if (sm.frame % (UI_FREQ / 2) == 0) { - setProperty("dmActive", sm["driverMonitoringState"].getDriverMonitoringState().getIsActiveMode()); - setProperty("rightHandDM", sm["driverMonitoringState"].getDriverMonitoringState().getIsRHD()); - const auto vtcState = lp.getVisionTurnControllerState(); const float vtc_speed = lp.getVisionTurnSpeed() * (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH); const auto lpSoruce = lp.getLongitudinalPlanSource(); @@ -564,9 +568,6 @@ void AnnotatedCameraWidget::updateState(const UIState &s) { setProperty("reversing", reverse_allowed); - // DM icon transition - dm_fade_state = fmax(0.0, fmin(1.0, dm_fade_state+0.2*(0.5-(float)(dmActive)))); - int e2eLStatus = 0; static bool chime_sent = false; static int chime_count = 0; diff --git a/selfdrive/ui/qt/setup/reset.cc b/selfdrive/ui/qt/setup/reset.cc index ccf9bf8053..7999dd640b 100644 --- a/selfdrive/ui/qt/setup/reset.cc +++ b/selfdrive/ui/qt/setup/reset.cc @@ -79,7 +79,14 @@ Reset::Reset(ResetMode mode, QWidget *parent) : QWidget(parent) { #endif confirmBtn = new QPushButton(tr("Confirm")); - confirmBtn->setStyleSheet("background-color: #465BEA;"); + confirmBtn->setStyleSheet(R"( + QPushButton { + background-color: #465BEA; + } + QPushButton:pressed { + background-color: #3049F4; + } + )"); blayout->addWidget(confirmBtn); QObject::connect(confirmBtn, &QPushButton::clicked, this, &Reset::confirm); @@ -111,6 +118,9 @@ Reset::Reset(ResetMode mode, QWidget *parent) : QWidget(parent) { border-radius: 10px; background-color: #333333; } + QPushButton:pressed { + background-color: #444444; + } )"); } diff --git a/selfdrive/ui/qt/setup/updater.cc b/selfdrive/ui/qt/setup/updater.cc index fd7148c534..ae5f26c77e 100644 --- a/selfdrive/ui/qt/setup/updater.cc +++ b/selfdrive/ui/qt/setup/updater.cc @@ -46,7 +46,14 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid QPushButton *install = new QPushButton(tr("Install")); install->setObjectName("navBtn"); - install->setStyleSheet("background-color: #465BEA;"); + install->setStyleSheet(R"( + QPushButton { + background-color: #465BEA; + } + QPushButton:pressed { + background-color: #3049F4; + } + )"); QObject::connect(install, &QPushButton::clicked, this, &Updater::installUpdate); hlayout->addWidget(install); } @@ -124,6 +131,9 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid border-radius: 10px; background-color: #333333; } + QPushButton#navBtn:pressed { + background-color: #444444; + } QProgressBar { border: none; background-color: #292929; diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index 770b9b92dd..807441cc85 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -60,7 +61,7 @@ public: public slots: void showDescription() { description->setVisible(true); - }; + } signals: void showDescriptionEvent(); @@ -106,7 +107,7 @@ signals: void clicked(); public slots: - void setEnabled(bool enabled) { btn.setEnabled(enabled); }; + void setEnabled(bool enabled) { btn.setEnabled(enabled); } private: QPushButton btn; @@ -163,7 +164,7 @@ public: 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); @@ -175,11 +176,11 @@ public: toggle.togglePosition(); setIcon(state); } - }; + } void showEvent(QShowEvent *event) override { refresh(); - }; + } private: void setIcon(bool state) { @@ -188,7 +189,7 @@ private: } else if (!icon_pixmap.isNull()) { icon_label->setPixmap(icon_pixmap); } - }; + } std::string key; Params params; @@ -197,6 +198,65 @@ private: bool store_confirm = false; }; +class ButtonParamControl : public AbstractControl { + Q_OBJECT +public: + ButtonParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, + const std::vector &button_texts, const int minimum_button_width = 225) : AbstractControl(title, desc, icon) { + const QString style = R"( + QPushButton { + border-radius: 50px; + font-size: 40px; + font-weight: 500; + height:100px; + padding: 0 25 0 25; + color: #E4E4E4; + background-color: #393939; + } + QPushButton:pressed { + background-color: #4a4a4a; + } + QPushButton:checked:enabled { + background-color: #33Ab4C; + } + QPushButton:disabled { + color: #33E4E4E4; + } + )"; + key = param.toStdString(); + int value = atoi(params.get(key).c_str()); + + button_group = new QButtonGroup(this); + button_group->setExclusive(true); + for (int i = 0; i < button_texts.size(); i++) { + QPushButton *button = new QPushButton(button_texts[i], this); + button->setCheckable(true); + button->setChecked(i == value); + button->setStyleSheet(style); + button->setMinimumWidth(minimum_button_width); + hlayout->addWidget(button); + button_group->addButton(button, i); + } + + QObject::connect(button_group, QOverload::of(&QButtonGroup::buttonToggled), [=](int id, bool checked) { + if (checked) { + params.put(key, std::to_string(id)); + } + }); + } + + void setEnabled(bool enable) { + for (auto btn : button_group->buttons()) { + btn->setEnabled(enable); + } + } + +private: + std::string key; + Params params; + QButtonGroup *button_group; +}; + class ListWidget : public QWidget { Q_OBJECT public: @@ -236,7 +296,7 @@ class LayoutWidget : public QWidget { public: LayoutWidget(QLayout *l, QWidget *parent = nullptr) : QWidget(parent) { setLayout(l); - }; + } }; class ClickableWidget : public QWidget { diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index 2026a70478..49fbdff222 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -71,10 +71,15 @@ InputDialog::InputDialog(const QString &title, QWidget *parent, const QString &s QPushButton* cancel_btn = new QPushButton(tr("Cancel")); cancel_btn->setFixedSize(386, 125); cancel_btn->setStyleSheet(R"( - font-size: 48px; - border-radius: 10px; - color: #E4E4E4; - background-color: #444444; + QPushButton { + font-size: 48px; + border-radius: 10px; + color: #E4E4E4; + background-color: #333333; + } + QPushButton:pressed { + background-color: #444444; + } )"); header_layout->addWidget(cancel_btn, 0, Qt::AlignRight); QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::reject); diff --git a/selfdrive/ui/qt/widgets/keyboard.cc b/selfdrive/ui/qt/widgets/keyboard.cc index 1c59686535..162d27db02 100644 --- a/selfdrive/ui/qt/widgets/keyboard.cc +++ b/selfdrive/ui/qt/widgets/keyboard.cc @@ -59,7 +59,14 @@ KeyboardLayout::KeyboardLayout(QWidget* parent, const std::vectorsetAutoRepeat(true); } else if (p == ENTER_KEY) { - btn->setStyleSheet("background-color: #465BEA;"); + btn->setStyleSheet(R"( + QPushButton { + background-color: #465BEA; + } + QPushButton:pressed { + background-color: #444444; + } + )"); } btn->setFixedHeight(135 + key_spacing_vertical); btn_group->addButton(btn); diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc index 7f88b049ae..86bd918e0c 100644 --- a/selfdrive/ui/qt/widgets/prime.cc +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -14,6 +14,7 @@ #include "selfdrive/ui/qt/request_repeater.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/widgets/wifi.h" using qrcodegen::QrCode; @@ -114,76 +115,33 @@ PairingPopup::PairingPopup(QWidget *parent) : QDialogBase(parent) { } -PrimeUserWidget::PrimeUserWidget(QWidget* parent) : QWidget(parent) { - mainLayout = new QVBoxLayout(this); - mainLayout->setMargin(0); +PrimeUserWidget::PrimeUserWidget(QWidget* parent) : QFrame(parent) { + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->setSpacing(30); // subscribed prime layout QWidget *primeWidget = new QWidget; primeWidget->setObjectName("primeWidget"); QVBoxLayout *primeLayout = new QVBoxLayout(primeWidget); - primeLayout->setMargin(0); - primeWidget->setContentsMargins(60, 50, 60, 50); + primeLayout->setContentsMargins(56, 40, 56, 40); + primeLayout->setSpacing(20); - QLabel* subscribed = new QLabel(tr("✓ SUBSCRIBED")); + QLabel *subscribed = new QLabel(tr("✓ SUBSCRIBED")); subscribed->setStyleSheet("font-size: 41px; font-weight: bold; color: #86FF4E;"); - primeLayout->addWidget(subscribed, 0, Qt::AlignTop); + primeLayout->addWidget(subscribed); - primeLayout->addSpacing(60); - - QLabel* commaPrime = new QLabel(tr("comma prime")); + QLabel *commaPrime = new QLabel(tr("comma prime")); commaPrime->setStyleSheet("font-size: 75px; font-weight: bold;"); - primeLayout->addWidget(commaPrime, 0, Qt::AlignTop); - - primeLayout->addSpacing(20); - - QLabel* connectUrl = new QLabel(tr("CONNECT.COMMA.AI")); - connectUrl->setStyleSheet("font-size: 41px; font-family: Inter SemiBold; color: #A0A0A0;"); - primeLayout->addWidget(connectUrl, 0, Qt::AlignTop); + primeLayout->addWidget(commaPrime); mainLayout->addWidget(primeWidget); - - // comma points layout - QWidget *pointsWidget = new QWidget; - pointsWidget->setObjectName("primeWidget"); - QVBoxLayout *pointsLayout = new QVBoxLayout(pointsWidget); - pointsLayout->setMargin(0); - pointsWidget->setContentsMargins(60, 50, 60, 50); - - QLabel* commaPoints = new QLabel(tr("COMMA POINTS")); - commaPoints->setStyleSheet("font-size: 41px; font-family: Inter SemiBold;"); - pointsLayout->addWidget(commaPoints, 0, Qt::AlignTop); - - points = new QLabel("0"); - points->setStyleSheet("font-size: 91px; font-weight: bold;"); - pointsLayout->addWidget(points, 0, Qt::AlignTop); - - mainLayout->addWidget(pointsWidget); - mainLayout->addStretch(); - - // set up API requests - if (auto dongleId = getDongleId()) { - QString url = CommaApi::BASE_URL + "/v1/devices/" + *dongleId + "/owner"; - RequestRepeater *repeater = new RequestRepeater(this, url, "ApiCache_Owner", 6); - QObject::connect(repeater, &RequestRepeater::requestDone, this, &PrimeUserWidget::replyFinished); - } } -void PrimeUserWidget::replyFinished(const QString &response) { - QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8()); - if (doc.isNull()) { - qDebug() << "JSON Parse failed on getting points"; - return; - } - - QJsonObject json = doc.object(); - points->setText(QString::number(json["points"].toInt())); -} PrimeAdWidget::PrimeAdWidget(QWidget* parent) : QFrame(parent) { - QVBoxLayout* main_layout = new QVBoxLayout(this); + QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(80, 90, 80, 60); main_layout->setSpacing(0); @@ -227,33 +185,31 @@ SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) { // Unpaired, registration prompt layout - QWidget* finishRegistration = new QWidget; + QFrame* finishRegistration = new QFrame; finishRegistration->setObjectName("primeWidget"); QVBoxLayout* finishRegistationLayout = new QVBoxLayout(finishRegistration); - finishRegistationLayout->setContentsMargins(30, 75, 30, 45); - finishRegistationLayout->setSpacing(0); + finishRegistationLayout->setSpacing(38); + finishRegistationLayout->setContentsMargins(64, 48, 64, 48); QLabel* registrationTitle = new QLabel(tr("Finish Setup")); - registrationTitle->setStyleSheet("font-size: 75px; font-weight: bold; margin-left: 55px;"); + registrationTitle->setStyleSheet("font-size: 75px; font-weight: bold;"); finishRegistationLayout->addWidget(registrationTitle); - finishRegistationLayout->addSpacing(30); - QLabel* registrationDescription = new QLabel(tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); registrationDescription->setWordWrap(true); - registrationDescription->setStyleSheet("font-size: 55px; font-weight: light; margin-left: 55px;"); + registrationDescription->setStyleSheet("font-size: 50px; font-weight: light;"); finishRegistationLayout->addWidget(registrationDescription); finishRegistationLayout->addStretch(); QPushButton* pair = new QPushButton(tr("Pair device")); - pair->setFixedHeight(220); pair->setStyleSheet(R"( QPushButton { font-size: 55px; - font-weight: 400; + font-weight: 500; border-radius: 10px; background-color: #465BEA; + padding: 64px; } QPushButton:pressed { background-color: #3049F4; @@ -271,15 +227,24 @@ SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) { outer_layout->setContentsMargins(0, 0, 0, 0); outer_layout->addWidget(mainLayout); - primeAd = new PrimeAdWidget; - mainLayout->addWidget(primeAd); + QWidget *content = new QWidget; + QVBoxLayout *content_layout = new QVBoxLayout(content); + content_layout->setContentsMargins(0, 0, 0, 0); + content_layout->setSpacing(30); primeUser = new PrimeUserWidget; - mainLayout->addWidget(primeUser); + content_layout->addWidget(primeUser); - mainLayout->setCurrentWidget(uiState()->primeType() ? (QWidget*)primeUser : (QWidget*)primeAd); + WiFiPromptWidget *wifi_prompt = new WiFiPromptWidget; + QObject::connect(wifi_prompt, &WiFiPromptWidget::openSettings, this, &SetupWidget::openSettings); + content_layout->addWidget(wifi_prompt); + content_layout->addStretch(); + + mainLayout->addWidget(content); + + primeUser->setVisible(uiState()->primeType()); + mainLayout->setCurrentIndex(1); - setFixedWidth(750); setStyleSheet(R"( #primeWidget { border-radius: 10px; @@ -319,10 +284,7 @@ void SetupWidget::replyFinished(const QString &response, bool success) { } else { popup->reject(); - if (prime_type) { - mainLayout->setCurrentWidget(primeUser); - } else { - mainLayout->setCurrentWidget(primeAd); - } + primeUser->setVisible(prime_type); + mainLayout->setCurrentIndex(1); } } diff --git a/selfdrive/ui/qt/widgets/prime.h b/selfdrive/ui/qt/widgets/prime.h index 0a1d93250d..b41bab1695 100644 --- a/selfdrive/ui/qt/widgets/prime.h +++ b/selfdrive/ui/qt/widgets/prime.h @@ -15,6 +15,7 @@ enum PrimeType { MAGENTA_NEW = 4, }; + // pairing QR code class PairingQRWidget : public QWidget { Q_OBJECT @@ -34,6 +35,7 @@ private slots: void refresh(); }; + // pairing popup widget class PairingPopup : public QDialogBase { Q_OBJECT @@ -42,18 +44,13 @@ public: explicit PairingPopup(QWidget* parent); }; + // widget for paired users with prime -class PrimeUserWidget : public QWidget { +class PrimeUserWidget : public QFrame { Q_OBJECT + public: explicit PrimeUserWidget(QWidget* parent = 0); - -private: - QVBoxLayout* mainLayout; - QLabel* points; - -private slots: - void replyFinished(const QString &response); }; @@ -64,6 +61,7 @@ public: explicit PrimeAdWidget(QWidget* parent = 0); }; + // container widget class SetupWidget : public QFrame { Q_OBJECT @@ -71,10 +69,12 @@ class SetupWidget : public QFrame { public: explicit SetupWidget(QWidget* parent = 0); +signals: + void openSettings(int index = 0, const QString ¶m = ""); + private: PairingPopup *popup; QStackedWidget *mainLayout; - PrimeAdWidget *primeAd; PrimeUserWidget *primeUser; private slots: diff --git a/selfdrive/ui/qt/widgets/wifi.cc b/selfdrive/ui/qt/widgets/wifi.cc new file mode 100644 index 0000000000..b717a00d98 --- /dev/null +++ b/selfdrive/ui/qt/widgets/wifi.cc @@ -0,0 +1,103 @@ +#include "selfdrive/ui/qt/widgets/wifi.h" + +#include +#include +#include +#include + +WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) { + stack = new QStackedLayout(this); + + // Setup Wi-Fi + QFrame *setup = new QFrame; + QVBoxLayout *setup_layout = new QVBoxLayout(setup); + setup_layout->setContentsMargins(56, 40, 56, 40); + setup_layout->setSpacing(20); + { + QHBoxLayout *title_layout = new QHBoxLayout; + title_layout->setSpacing(32); + { + QLabel *icon = new QLabel; + QPixmap *pixmap = new QPixmap("../assets/offroad/icon_wifi_strength_full.svg"); + icon->setPixmap(pixmap->scaledToWidth(80, Qt::SmoothTransformation)); + title_layout->addWidget(icon); + + QLabel *title = new QLabel(tr("Setup Wi-Fi")); + title->setStyleSheet("font-size: 64px; font-weight: 600;"); + title_layout->addWidget(title); + title_layout->addStretch(); + } + setup_layout->addLayout(title_layout); + + QLabel *desc = new QLabel(tr("Connect to Wi-Fi to upload driving data and help improve openpilot")); + desc->setStyleSheet("font-size: 40px; font-weight: 400;"); + desc->setWordWrap(true); + setup_layout->addWidget(desc); + + QPushButton *settings_btn = new QPushButton(tr("Open Settings")); + connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(1); }); + settings_btn->setStyleSheet(R"( + QPushButton { + font-size: 48px; + font-weight: 500; + border-radius: 10px; + background-color: #465BEA; + padding: 32px; + } + QPushButton:pressed { + background-color: #3049F4; + } + )"); + setup_layout->addWidget(settings_btn); + } + stack->addWidget(setup); + + // Uploading data + QWidget *uploading = new QWidget; + QVBoxLayout *uploading_layout = new QVBoxLayout(uploading); + uploading_layout->setContentsMargins(64, 56, 64, 56); + uploading_layout->setSpacing(36); + { + QHBoxLayout *title_layout = new QHBoxLayout; + { + QLabel *title = new QLabel(tr("Uploading training data")); + title->setStyleSheet("font-size: 64px; font-weight: 600;"); + title->setWordWrap(true); + title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + title_layout->addWidget(title); + title_layout->addStretch(); + + QLabel *icon = new QLabel; + QPixmap *pixmap = new QPixmap("../assets/offroad/icon_wifi_uploading.svg"); + icon->setPixmap(pixmap->scaledToWidth(120, Qt::SmoothTransformation)); + title_layout->addWidget(icon); + } + uploading_layout->addLayout(title_layout); + + QLabel *desc = new QLabel(tr("Your data is used to train driving models and help improve openpilot")); + desc->setStyleSheet("font-size: 48px; font-weight: 400;"); + desc->setWordWrap(true); + uploading_layout->addWidget(desc); + } + stack->addWidget(uploading); + + setStyleSheet(R"( + WiFiPromptWidget { + background-color: #333333; + border-radius: 10px; + } + )"); + + QObject::connect(uiState(), &UIState::uiUpdate, this, &WiFiPromptWidget::updateState); +} + +void WiFiPromptWidget::updateState(const UIState &s) { + if (!isVisible()) return; + + auto &sm = *(s.sm); + + auto network_type = sm["deviceState"].getDeviceState().getNetworkType(); + auto uploading = network_type == cereal::DeviceState::NetworkType::WIFI || + network_type == cereal::DeviceState::NetworkType::ETHERNET; + stack->setCurrentIndex(uploading ? 1 : 0); +} diff --git a/selfdrive/ui/qt/widgets/wifi.h b/selfdrive/ui/qt/widgets/wifi.h new file mode 100644 index 0000000000..60c865f2b8 --- /dev/null +++ b/selfdrive/ui/qt/widgets/wifi.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +#include "selfdrive/ui/ui.h" + +class WiFiPromptWidget : public QFrame { + Q_OBJECT + +public: + explicit WiFiPromptWidget(QWidget* parent = 0); + +signals: + void openSettings(int index = 0, const QString ¶m = ""); + +public slots: + void updateState(const UIState &s); + +protected: + QStackedLayout *stack; +}; diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc index 198b1edbf6..a1aa4b8315 100644 --- a/selfdrive/ui/qt/window.cc +++ b/selfdrive/ui/qt/window.cc @@ -79,11 +79,16 @@ void MainWindow::closeSettings() { } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { - const static QSet evts({QEvent::MouseButtonPress, QEvent::MouseMove, - QEvent::TouchBegin, QEvent::TouchUpdate, QEvent::TouchEnd}); - - if (evts.contains(event->type())) { - device.resetInteractiveTimout(); + switch (event->type()) { + case QEvent::TouchBegin: + case QEvent::TouchUpdate: + case QEvent::TouchEnd: + case QEvent::MouseButtonPress: + case QEvent::MouseMove: + device.resetInteractiveTimout(); + break; + default: + break; } return false; } diff --git a/selfdrive/ui/tests/.gitignore b/selfdrive/ui/tests/.gitignore index 26335744f3..94ba9a3a97 100644 --- a/selfdrive/ui/tests/.gitignore +++ b/selfdrive/ui/tests/.gitignore @@ -2,3 +2,4 @@ test playsound test_sound test_translations +ui_snapshot diff --git a/selfdrive/ui/tests/ui_snapshot.cc b/selfdrive/ui/tests/ui_snapshot.cc new file mode 100644 index 0000000000..14e0fab835 --- /dev/null +++ b/selfdrive/ui/tests/ui_snapshot.cc @@ -0,0 +1,66 @@ +#include "selfdrive/ui/tests/ui_snapshot.h" + +#include +#include +#include +#include +#include + +#include "selfdrive/ui/qt/home.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/window.h" +#include "selfdrive/ui/ui.h" + +void saveWidgetAsImage(QWidget *widget, const QString &fileName) { + QImage image(widget->size(), QImage::Format_ARGB32); + QPainter painter(&image); + widget->render(&painter); + image.save(fileName); +} + +int main(int argc, char *argv[]) { + initApp(argc, argv); + + QApplication app(argc, argv); + + QCommandLineParser parser; + parser.setApplicationDescription("Take a snapshot of the UI."); + parser.addHelpOption(); + parser.addOption(QCommandLineOption(QStringList() << "o" + << "output", + "Output image file path. The file's suffix is used to " + "determine the format. Supports PNG and JPEG formats. " + "Defaults to \"snapshot.png\".", + "file", "snapshot.png")); + parser.process(app); + + const QString output = parser.value("output"); + if (output.isEmpty()) { + qCritical() << "No output file specified"; + return 1; + } + + auto current = QDir::current(); + + // change working directory to find assets + if (!QDir::setCurrent(QCoreApplication::applicationDirPath() + QDir::separator() + "..")) { + qCritical() << "Failed to set current directory"; + return 1; + } + + MainWindow w; + w.setFixedSize(2160, 1080); + w.show(); + app.installEventFilter(&w); + + // restore working directory + QDir::setCurrent(current.absolutePath()); + + // wait for the UI to update + QObject::connect(uiState(), &UIState::uiUpdate, [&](const UIState &s) { + saveWidgetAsImage(&w, output); + app.quit(); + }); + + return app.exec(); +} diff --git a/selfdrive/ui/tests/ui_snapshot.h b/selfdrive/ui/tests/ui_snapshot.h new file mode 100644 index 0000000000..b4699f6af5 --- /dev/null +++ b/selfdrive/ui/tests/ui_snapshot.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +void saveWidgetAsImage(QWidget *widget, const QString &fileName); diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index df6901b3fd..55b66c132e 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -89,71 +89,6 @@ LIMIT LIMIT - - Lane -only - - - - Lane -less - - - - Auto -Lane - - - - - AutoLaneChangeTimer - - Auto Lane Change Timer - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - Nudge - - - - Nudgeless - - - - s - - - - - BrightnessControl - - Brightness Control (Global, %) - - - - Manually adjusts the global brightness of the screen. - - - - Auto - - - - - CameraOffset - - Camera Offset (cm) - - - - Hack to trick vehicle to be left or right biased in its lane. Decreasing the value will make the car keep more left, increasing will make it keep more right. Changes take effect immediately. - - ConfirmationDialog @@ -181,25 +116,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Ablehnen, deinstallieren %1 - - DevUiRow - - Developer UI List - - - - Select the number of lists of real-time parameters you would like to display on the sunnypilot screen while driving. - - - - 1-Row - - - - 2-Row - - - DevicePanel @@ -246,6 +162,10 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW TRAINING + + Review the rules, features, and limitations of openpilot + Wiederhole die Regeln, Fähigkeiten und Limitierungen von Openpilot + Are you sure you want to review the training guide? Bist du sicher, dass du die Trainingsanleitung wiederholen möchtest? @@ -278,6 +198,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Ausschalten + + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. + Damit Openpilot funktioniert, darf die Installationsposition nicht mehr als 4° nach rechts/links, 5° nach oben und 8° nach unten abweichen. Openpilot kalibriert sich durchgehend, ein Zurücksetzen ist selten notwendig. + Your device is pointed %1° %2 and %3° %4. Deine Geräteausrichtung ist %1° %2 und %3° %4. @@ -322,14 +246,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Review Überprüfen - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. sunnypilot is continuously calibrating, resetting is rarely required. - - DriveStats @@ -376,37 +292,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ENTSPANNTER MODUS AN - - GapAdjustCruiseMode - - Mode - - - - SW: Steering Wheel Button only - - - - UI: User Interface Button on screen only - - - - SW + UI: Steering Wheel Button + User Interface Button on screen - - - - S.W. - - - - UI - - - - S.W. + UI - - - InputDialog @@ -520,37 +405,6 @@ location set Warten auf GPS - - MaxTimeOffroad - - Max Time Offroad - - - - Device is automatically turned off after a set time when the engine is turned off (off-road) after driving (on-road). - - - - Always On - - - - Immediate - - - - s - - - - m - - - - h - - - MultiOptionDialog @@ -596,40 +450,6 @@ location set HINWEIS - - OnroadScreenOff - - Driving Screen Off Timer - - - - Turn off the device screen or reduce brightness to protect the screen after driving starts. It automatically brightens or turns on when a touch or event occurs. - - - - Always On - - - - s - - - - - OnroadScreenOffBrightness - - Driving Screen Off Brightness (%) - - - - When using the Driving Screen Off feature, the brightness is reduced according to the automatic brightness ratio. - - - - Dark - - - PairingPopup @@ -660,17 +480,6 @@ location set Aktivieren - - PathOffset - - Path Offset (cm) - - - - Hack to trick the model path to be left or right biased of the lane. Decreasing the value will shift the model more left, increasing will shift the model more right. Changes take effect immediately. - - - PrimeAdWidget @@ -708,14 +517,6 @@ location set comma prime comma prime - - CONNECT.COMMA.AI - CONNECT.COMMA.AI - - - COMMA POINTS - COMMA PUNKTE - QObject @@ -731,6 +532,10 @@ location set dashcam dashcam + + openpilot + openpilot + %n minute(s) ago @@ -752,10 +557,6 @@ location set vor %n Tagen - - sunnypilot - - Reset @@ -797,332 +598,6 @@ This may take up to a minute. - - SPControlsPanel - - Enable M.A.D.S. - - - - Enable the beloved M.A.D.S. feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Disengage ALC On Brake Pedal ℹ - - - - Define brake pedal interactions with sunnypilot when M.A.D.S. is enabled. - - - - Enabled: Pressing the brake pedal will disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Disabled: Pressing the brake pedal will <b>NOT</b> disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Enable ACC+MADS with RES+/SET- - - - - Engage both M.A.D.S. and ACC with a single press of RES+ or SET- button. - - - - Note: Once M.A.D.S. is engaged via this mode, it will remain engaged until it is manually disabled via the M.A.D.S. button or car shut off. - - - - Pause Lateral Below Speed w/ Blinker - - - - Enable this toggle to pause lateral actuation with blinker when traveling below 30 MPH or 50 KM/H. - - - - Enable Dynamic Lane Profile - - - - Enable toggle to use Dynamic Lane Profile. Disable toggle to use Laneless only. - - - - Laneless for Curves in "Auto lane" - - - - While in Auto Lane, switch to Laneless for current/future curves. - - - - Custom Offsets - - - - Add custom offsets to Camera and Path in sunnypilot. - - - - Enable Gap Adjust Cruise ℹ - - - - Enable the Interval button on the steering wheel to adjust the cruise gap. - - - - Only available to cars with openpilot Longitudinal Control - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Torque Lateral Control Live Tune - - - - Enables live tune for Torque lateral control. - - - - "Torque Lateral Control Live Tune"<br>You must restart your car or your device to apply these changes. - - - - Torque Lateral Controller Self-Tune - - - - Enables self-tune for Torque lateral control. - - - - "Torque Lateral Controller Self-Tune"<br>You must restart your car or your device to apply these changes. - - - - Enable Hands on Wheel Monitoring - - - - Monitor and alert when driver is not keeping the hands on the steering wheel. - - - - Enable Vision Based Turn Speed Control (V-TSC) - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Enable Speed Limit Control (SLC) - - - - Use speed limit signs information from map data and car interface (if applicable) to automatically adapt cruise speed to road limits. - - - - Enable Speed Limit % Offset - - - - Set speed limit slightly higher than actual speed limit for a more natural drive. - - - - Enable Map Data Turn Speed Control (M-TSC) - - - - Use curvature information from map data to define speed limits to take turns ahead. - - - - ACC +/-: Long Press Reverse - - - - Change the ACC +/- buttons behavior with cruise speed change in sunnypilot. - - - - Disabled (Stock): Short=1, Long = 5 (imperial) / 10 (metric) - - - - Enabled: Short = 5 (imperial) / 10 (metric), Long=1 - - - - OSM: Use Offline Database - - - - - SPGeneralPanel - - Quiet Drive 🤫 - - - - sunnypilot will display alerts but only play the most important warning sounds. This feature can be toggled while the car is on. - - - - Green Traffic Light Chime (Beta) - - - - A chime will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. If you are waiting behind another vehicle, the chime will play once the vehicle advances unless ACC is engaged. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Custom Boot Screen - - - - Display sunnypilot welcome screen while booting the device. - - - - Disable Onroad Uploads - - - - Disable uploads completely when onroad. Necessary to avoid high data usage when connected to Wi-Fi hotspot. Turn on this feature if you are looking to utilize map-based features, such as Speed Limit Control (SLC) and Map-based Turn Speed Control (MTSC). - - - - Error Troubleshoot - - - - VIEW - ANSEHEN - - - Display error from the tmux session when an error has occurred from a system process. - - - - Debug snapshot on screen center tap - - - - Stores snapshot file with current state of some modules. - - - - - SPVehiclesPanel - - Hyundai/Kia/Genesis (HKG) - - - - Enhanced SCC Radar Interceptor ℹ - - - - sunnypilot will take over control of gas and brakes and will <b>NOT</b> disable the car's radar. This preserves AEB. - - - - Note: Only enable this feature if you have the Enhanced SCC Radar Interceptor hardware installed. - - - - Want to get one for your car? Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Toyota/Lexus - - - - Volkswagen MQB/PQ - - - - - SPVisualsPanel - - Display Braking Status - - - - Enable this will turn the current speed value to red while the brake is used. - - - - Display Stand Still Timer - - - - Enable this will display time spent at a stop (i.e., at a stop lights, stop signs, traffic congestions). - - - - OSM: Show debug UI elements - - - - OSM: Show UI elements that aid debugging. - - - - Enable Mapbox Navigation* - - - - Enable built-in navigation on sunnypilot, powered by Mapbox. - - - - Access via the web interface: "http://<device_ip>:8082" - - - - If you do not have comma Prime, you will need to provide your own Mapbox token at https://mapbox.com/. Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Huge thanks to the dragonpilot team for making this possible! - - - - "Enable Mapbox Navigation" -You must restart your car or your device to apply these changes. -Reboot now? - - - - Show Developer UI - - - - Show developer UI (Dev UI) for real-time parameters from various sources. - - - SettingsWindow @@ -1149,22 +624,6 @@ Reboot now? Navigation Navigation - - SP - General - - - - SP - Controls - - - - SP - Vehicles - - - - SP - Visuals - - Setup @@ -1399,42 +858,24 @@ Reboot now? Uninstall Deinstallieren - - - SpeedLimitStyle - Speed Limit Style + failed to check for update - MUTCD: US/Canada + up to date, last checked %1 - Vienna: Europe/Asia/etc. + DOWNLOAD - MUTCD + update available - Vienna - - - - - SpeedLimitValueOffset - - Speed Limit Offset (MPH / KM/H) ℹ - - - - Set speed limit higher or lower than actual speed limit for a more personalized drive. - - - - To use this feature, turn off "Enable Speed Limit % Offset". + never @@ -1505,6 +946,14 @@ Reboot now? TogglesPanel + + Enable openpilot + Openpilot aktivieren + + + 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. + Benutze das Openpilot System als adaptiven Tempomaten und Spurhalteassistenten. Deine Aufmerksamkeit ist jederzeit erforderlich, um diese Funktion zu nutzen. Diese Einstellung wird übernommen, wenn das Auto aus ist. + Enable Lane Departure Warnings Spurverlassenswarnungen aktivieren @@ -1529,6 +978,10 @@ Reboot now? Upload data from the driver facing camera and help improve the driver monitoring algorithm. Lade Daten der Fahreraufmerksamkeitsüberwachungskamera hoch, um die Fahreraufmerksamkeitsüberwachungsalgorithmen zu verbessern. + + When enabled, pressing the accelerator pedal will disengage openpilot. + Wenn aktiviert, deaktiviert sich Openpilot sobald das Gaspedal betätigt wird. + Use 24h format instead of am/pm Benutze das 24Stunden Format anstatt am/pm @@ -1551,22 +1004,10 @@ Reboot now? Experimental Mode Experimenteller Modus - - Experimental openpilot Longitudinal Control - Experimenteller Openpilot Tempomat - - - WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB). - WARNUNG: Der Openpilot Tempomat ist für dieses Auto experimentell und deaktiviert den Notbremsassistenten. - Disengage on Accelerator Pedal Bei Gasbetätigung ausschalten - - 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. - Bei diesem auto wird standardmäßig der im Auto eingebaute adaptive Tempomat anstelle des Openpilot Tempomats benutzt. Aktiviere diesen Schalter, um zum Openpilot Tempomaten zu wechseln. Es ist empfohlen den Experimentellen Modus bei Nutzung des Openpilot Tempomats zu aktivieren. - 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 fährt standardmäßig im <b>entspannten Modus</b>. Der Experimentelle Modus aktiviert<b>Alpha-level Funktionen</b>, die noch nicht für den entspannten Modus bereit sind. Die experimentellen Funktionen sind die Folgenden: @@ -1595,46 +1036,44 @@ Reboot now? openpilot longitudinal control may come in a future update. - - An experimental version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - Enable experimental longitudinal control to allow Experimental mode. Aktiviere den experimentellen Openpilot Tempomaten für experimentelle Funktionen. - Enable sunnypilot + openpilot Longitudinal Control (Alpha) - Use the sunnypilot 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. + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - - TorqueFriction - - FRICTION + 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 enabling openpilot longitudinal control alpha. - Adjust Friction for the Torque Lateral Controller - - - - - TorqueMaxLatAccel - - LAT_ACCEL_FACTOR + Aggressive - Adjust Max Lateral Acceleration for the Torque Lateral Controller + Standard + + + + Relaxed + + + + Driving Personality + + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. + + + + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. @@ -1674,21 +1113,25 @@ Reboot now? - VwAccType + WiFiPromptWidget - Short Press +1/-1 Type + Setup Wi-Fi - Define the type of ACC control your car has with short press to +1 or -1. + Connect to Wi-Fi to upload driving data and help improve openpilot - +/- + Open Settings - RES/SET + Uploading training data + + + + Your data is used to train driving models and help improve openpilot diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 056c0cc1bb..162c92c402 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -89,71 +89,6 @@ LIMIT 制限速度 - - Lane -only - - - - Lane -less - - - - Auto -Lane - - - - - AutoLaneChangeTimer - - Auto Lane Change Timer - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - Nudge - - - - Nudgeless - - - - s - - - - - BrightnessControl - - Brightness Control (Global, %) - - - - Manually adjusts the global brightness of the screen. - - - - Auto - - - - - CameraOffset - - Camera Offset (cm) - - - - Hack to trick vehicle to be left or right biased in its lane. Decreasing the value will make the car keep more left, increasing will make it keep more right. Changes take effect immediately. - - ConfirmationDialog @@ -181,25 +116,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 拒否して %1 をアンインストール - - DevUiRow - - Developer UI List - - - - Select the number of lists of real-time parameters you would like to display on the sunnypilot screen while driving. - - - - 1-Row - - - - 2-Row - - - DevicePanel @@ -246,6 +162,10 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 見る + + Review the rules, features, and limitations of openpilot + openpilot の特徴を見る + Are you sure you want to review the training guide? 使い方の確認をしますか? @@ -278,6 +198,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off 電源を切る + + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. + openpilotの本体は、左右4°以内、上5°、下8°以内の角度で取付ける必要があります。継続してキャリブレーションを続けているので、手動でリセットを行う必要はほぼありません。 + Your device is pointed %1° %2 and %3° %4. このデバイスは%2 %1°、%4 %3°の向きに設置されています。 @@ -322,14 +246,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Review 確認 - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. sunnypilot is continuously calibrating, resetting is rarely required. - - DriveStats @@ -376,37 +292,6 @@ Please use caution when using this feature. Only use the blinker when traffic an チルモード - - GapAdjustCruiseMode - - Mode - - - - SW: Steering Wheel Button only - - - - UI: User Interface Button on screen only - - - - SW + UI: Steering Wheel Button + User Interface Button on screen - - - - S.W. - - - - UI - - - - S.W. + UI - - - InputDialog @@ -521,37 +406,6 @@ location set GPS信号を探しています - - MaxTimeOffroad - - Max Time Offroad - - - - Device is automatically turned off after a set time when the engine is turned off (off-road) after driving (on-road). - - - - Always On - - - - Immediate - - - - s - - - - m - - - - h - - - MultiOptionDialog @@ -597,40 +451,6 @@ location set 警告 - - OnroadScreenOff - - Driving Screen Off Timer - - - - Turn off the device screen or reduce brightness to protect the screen after driving starts. It automatically brightens or turns on when a touch or event occurs. - - - - Always On - - - - s - - - - - OnroadScreenOffBrightness - - Driving Screen Off Brightness (%) - - - - When using the Driving Screen Off feature, the brightness is reduced according to the automatic brightness ratio. - - - - Dark - - - PairingPopup @@ -661,17 +481,6 @@ location set を有効化 - - PathOffset - - Path Offset (cm) - - - - Hack to trick the model path to be left or right biased of the lane. Decreasing the value will shift the model more left, increasing will shift the model more right. Changes take effect immediately. - - - PrimeAdWidget @@ -709,14 +518,6 @@ location set comma prime comma prime - - CONNECT.COMMA.AI - CONNECT.COMMA.AI - - - COMMA POINTS - COMMA POINTS - QObject @@ -732,6 +533,10 @@ location set dashcam ドライブレコーダー + + openpilot + openpilot + %n minute(s) ago @@ -750,10 +555,6 @@ location set %n 日前 - - sunnypilot - - Reset @@ -795,332 +596,6 @@ This may take up to a minute. - - SPControlsPanel - - Enable M.A.D.S. - - - - Enable the beloved M.A.D.S. feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Disengage ALC On Brake Pedal ℹ - - - - Define brake pedal interactions with sunnypilot when M.A.D.S. is enabled. - - - - Enabled: Pressing the brake pedal will disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Disabled: Pressing the brake pedal will <b>NOT</b> disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Enable ACC+MADS with RES+/SET- - - - - Engage both M.A.D.S. and ACC with a single press of RES+ or SET- button. - - - - Note: Once M.A.D.S. is engaged via this mode, it will remain engaged until it is manually disabled via the M.A.D.S. button or car shut off. - - - - Pause Lateral Below Speed w/ Blinker - - - - Enable this toggle to pause lateral actuation with blinker when traveling below 30 MPH or 50 KM/H. - - - - Enable Dynamic Lane Profile - - - - Enable toggle to use Dynamic Lane Profile. Disable toggle to use Laneless only. - - - - Laneless for Curves in "Auto lane" - - - - While in Auto Lane, switch to Laneless for current/future curves. - - - - Custom Offsets - - - - Add custom offsets to Camera and Path in sunnypilot. - - - - Enable Gap Adjust Cruise ℹ - - - - Enable the Interval button on the steering wheel to adjust the cruise gap. - - - - Only available to cars with openpilot Longitudinal Control - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Torque Lateral Control Live Tune - - - - Enables live tune for Torque lateral control. - - - - "Torque Lateral Control Live Tune"<br>You must restart your car or your device to apply these changes. - - - - Torque Lateral Controller Self-Tune - - - - Enables self-tune for Torque lateral control. - - - - "Torque Lateral Controller Self-Tune"<br>You must restart your car or your device to apply these changes. - - - - Enable Hands on Wheel Monitoring - - - - Monitor and alert when driver is not keeping the hands on the steering wheel. - - - - Enable Vision Based Turn Speed Control (V-TSC) - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Enable Speed Limit Control (SLC) - - - - Use speed limit signs information from map data and car interface (if applicable) to automatically adapt cruise speed to road limits. - - - - Enable Speed Limit % Offset - - - - Set speed limit slightly higher than actual speed limit for a more natural drive. - - - - Enable Map Data Turn Speed Control (M-TSC) - - - - Use curvature information from map data to define speed limits to take turns ahead. - - - - ACC +/-: Long Press Reverse - - - - Change the ACC +/- buttons behavior with cruise speed change in sunnypilot. - - - - Disabled (Stock): Short=1, Long = 5 (imperial) / 10 (metric) - - - - Enabled: Short = 5 (imperial) / 10 (metric), Long=1 - - - - OSM: Use Offline Database - - - - - SPGeneralPanel - - Quiet Drive 🤫 - - - - sunnypilot will display alerts but only play the most important warning sounds. This feature can be toggled while the car is on. - - - - Green Traffic Light Chime (Beta) - - - - A chime will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. If you are waiting behind another vehicle, the chime will play once the vehicle advances unless ACC is engaged. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Custom Boot Screen - - - - Display sunnypilot welcome screen while booting the device. - - - - Disable Onroad Uploads - - - - Disable uploads completely when onroad. Necessary to avoid high data usage when connected to Wi-Fi hotspot. Turn on this feature if you are looking to utilize map-based features, such as Speed Limit Control (SLC) and Map-based Turn Speed Control (MTSC). - - - - Error Troubleshoot - - - - VIEW - 見る - - - Display error from the tmux session when an error has occurred from a system process. - - - - Debug snapshot on screen center tap - - - - Stores snapshot file with current state of some modules. - - - - - SPVehiclesPanel - - Hyundai/Kia/Genesis (HKG) - - - - Enhanced SCC Radar Interceptor ℹ - - - - sunnypilot will take over control of gas and brakes and will <b>NOT</b> disable the car's radar. This preserves AEB. - - - - Note: Only enable this feature if you have the Enhanced SCC Radar Interceptor hardware installed. - - - - Want to get one for your car? Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Toyota/Lexus - - - - Volkswagen MQB/PQ - - - - - SPVisualsPanel - - Display Braking Status - - - - Enable this will turn the current speed value to red while the brake is used. - - - - Display Stand Still Timer - - - - Enable this will display time spent at a stop (i.e., at a stop lights, stop signs, traffic congestions). - - - - OSM: Show debug UI elements - - - - OSM: Show UI elements that aid debugging. - - - - Enable Mapbox Navigation* - - - - Enable built-in navigation on sunnypilot, powered by Mapbox. - - - - Access via the web interface: "http://<device_ip>:8082" - - - - If you do not have comma Prime, you will need to provide your own Mapbox token at https://mapbox.com/. Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Huge thanks to the dragonpilot team for making this possible! - - - - "Enable Mapbox Navigation" -You must restart your car or your device to apply these changes. -Reboot now? - - - - Show Developer UI - - - - Show developer UI (Dev UI) for real-time parameters from various sources. - - - SettingsWindow @@ -1147,22 +622,6 @@ Reboot now? Navigation ナビゲーション - - SP - General - - - - SP - Controls - - - - SP - Vehicles - - - - SP - Visuals - - Setup @@ -1395,42 +854,24 @@ Reboot now? Uninstall アンインストール - - - SpeedLimitStyle - Speed Limit Style + failed to check for update - MUTCD: US/Canada + up to date, last checked %1 - Vienna: Europe/Asia/etc. + DOWNLOAD - MUTCD + update available - Vienna - - - - - SpeedLimitValueOffset - - Speed Limit Offset (MPH / KM/H) ℹ - - - - Set speed limit higher or lower than actual speed limit for a more personalized drive. - - - - To use this feature, turn off "Enable Speed Limit % Offset". + never @@ -1501,6 +942,14 @@ Reboot now? TogglesPanel + + Enable openpilot + openpilot を有効化 + + + 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. + openpilotによるアダプティブクルーズコントロールとレーンキーピングドライバーアシストを利用します。この機能を利用する際は、常に前方への注意が必要です。この設定を変更すると、車の電源が切れた時に反映されます。 + Enable Lane Departure Warnings 車線逸脱警報機能を有効化 @@ -1525,14 +974,14 @@ Reboot now? Upload data from the driver facing camera and help improve the driver monitoring algorithm. 車内カメラの映像をアップロードし、ドライバー監視システムのアルゴリズムの向上に役立てます。 - - Experimental openpilot Longitudinal Control - 実験段階のopenpilotによるアクセル制御 - Disengage on Accelerator Pedal アクセルを踏むと openpilot を中断 + + When enabled, pressing the accelerator pedal will disengage openpilot. + この機能を有効化すると、openpilotを利用中にアクセルを踏むとopenpilotによる運転サポートを中断します。 + Show ETA in 24h Format 24時間表示 @@ -1553,14 +1002,6 @@ Reboot now? 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 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は標準ではゆっくりとくつろげる運転を提供します。この実験モードを有効にすると、以下のくつろげる段階ではない開発中の機能を利用する事ができます。 @@ -1589,46 +1030,44 @@ Reboot now? openpilot longitudinal control may come in a future update. - - An experimental version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - Enable experimental longitudinal control to allow Experimental mode. 実験段階のopenpilotによるアクセル制御を有効にしてください。 - Enable sunnypilot + openpilot Longitudinal Control (Alpha) - Use the sunnypilot 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. + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - - TorqueFriction - - FRICTION + 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 enabling openpilot longitudinal control alpha. - Adjust Friction for the Torque Lateral Controller - - - - - TorqueMaxLatAccel - - LAT_ACCEL_FACTOR + Aggressive - Adjust Max Lateral Acceleration for the Torque Lateral Controller + Standard + + + + Relaxed + + + + Driving Personality + + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. + + + + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. @@ -1668,21 +1107,25 @@ Reboot now? - VwAccType + WiFiPromptWidget - Short Press +1/-1 Type + Setup Wi-Fi - Define the type of ACC control your car has with short press to +1 or -1. + Connect to Wi-Fi to upload driving data and help improve openpilot - +/- + Open Settings - RES/SET + Uploading training data + + + + Your data is used to train driving models and help improve openpilot diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index c054abd489..ef5a89750d 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -89,71 +89,6 @@ LIMIT LIMIT - - Lane -only - - - - Lane -less - - - - Auto -Lane - - - - - AutoLaneChangeTimer - - Auto Lane Change Timer - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - Nudge - - - - Nudgeless - - - - s - - - - - BrightnessControl - - Brightness Control (Global, %) - - - - Manually adjusts the global brightness of the screen. - - - - Auto - - - - - CameraOffset - - Camera Offset (cm) - - - - Hack to trick vehicle to be left or right biased in its lane. Decreasing the value will make the car keep more left, increasing will make it keep more right. Changes take effect immediately. - - ConfirmationDialog @@ -181,25 +116,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 거절, %1 제거 - - DevUiRow - - Developer UI List - - - - Select the number of lists of real-time parameters you would like to display on the sunnypilot screen while driving. - - - - 1-Row - - - - 2-Row - - - DevicePanel @@ -246,6 +162,10 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 다시보기 + + Review the rules, features, and limitations of openpilot + openpilot의 규칙, 기능 및 제한 다시보기 + Are you sure you want to review the training guide? 트레이닝 가이드를 다시보시겠습니까? @@ -278,6 +198,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off 전원 종료 + + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. + openpilot은 좌우측은 4° 이내, 위쪽은 5° 아래쪽은 8° 이내로 장치를 설치해야 합니다. openpilot은 지속적으로 보정되므로 리셋은 거의 필요하지 않습니다. + Your device is pointed %1° %2 and %3° %4. 사용자의 장치가 %1° %2 및 %3° %4 위치에 설치되어있습니다. @@ -322,14 +246,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Review 다시보기 - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. sunnypilot is continuously calibrating, resetting is rarely required. - - DriveStats @@ -376,37 +292,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 안정적 모드 사용 - - GapAdjustCruiseMode - - Mode - - - - SW: Steering Wheel Button only - - - - UI: User Interface Button on screen only - - - - SW + UI: Steering Wheel Button + User Interface Button on screen - - - - S.W. - - - - UI - - - - S.W. + UI - - - InputDialog @@ -521,37 +406,6 @@ location set GPS 수신중 입니다 - - MaxTimeOffroad - - Max Time Offroad - - - - Device is automatically turned off after a set time when the engine is turned off (off-road) after driving (on-road). - - - - Always On - - - - Immediate - - - - s - - - - m - - - - h - - - MultiOptionDialog @@ -597,40 +451,6 @@ location set 알림 - - OnroadScreenOff - - Driving Screen Off Timer - - - - Turn off the device screen or reduce brightness to protect the screen after driving starts. It automatically brightens or turns on when a touch or event occurs. - - - - Always On - - - - s - - - - - OnroadScreenOffBrightness - - Driving Screen Off Brightness (%) - - - - When using the Driving Screen Off feature, the brightness is reduced according to the automatic brightness ratio. - - - - Dark - - - PairingPopup @@ -661,17 +481,6 @@ location set 사용 - - PathOffset - - Path Offset (cm) - - - - Hack to trick the model path to be left or right biased of the lane. Decreasing the value will shift the model more left, increasing will shift the model more right. Changes take effect immediately. - - - PrimeAdWidget @@ -709,14 +518,6 @@ location set comma prime comma prime - - CONNECT.COMMA.AI - CONNECT.COMMA.AI - - - COMMA POINTS - COMMA POINTS - QObject @@ -732,6 +533,10 @@ location set dashcam dashcam + + openpilot + openpilot + %n minute(s) ago @@ -750,10 +555,6 @@ location set %n 일전 - - sunnypilot - - Reset @@ -796,332 +597,6 @@ This may take up to a minute. 최대 1분이 소요될 수 있습니다. - - SPControlsPanel - - Enable M.A.D.S. - - - - Enable the beloved M.A.D.S. feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Disengage ALC On Brake Pedal ℹ - - - - Define brake pedal interactions with sunnypilot when M.A.D.S. is enabled. - - - - Enabled: Pressing the brake pedal will disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Disabled: Pressing the brake pedal will <b>NOT</b> disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Enable ACC+MADS with RES+/SET- - - - - Engage both M.A.D.S. and ACC with a single press of RES+ or SET- button. - - - - Note: Once M.A.D.S. is engaged via this mode, it will remain engaged until it is manually disabled via the M.A.D.S. button or car shut off. - - - - Pause Lateral Below Speed w/ Blinker - - - - Enable this toggle to pause lateral actuation with blinker when traveling below 30 MPH or 50 KM/H. - - - - Enable Dynamic Lane Profile - - - - Enable toggle to use Dynamic Lane Profile. Disable toggle to use Laneless only. - - - - Laneless for Curves in "Auto lane" - - - - While in Auto Lane, switch to Laneless for current/future curves. - - - - Custom Offsets - - - - Add custom offsets to Camera and Path in sunnypilot. - - - - Enable Gap Adjust Cruise ℹ - - - - Enable the Interval button on the steering wheel to adjust the cruise gap. - - - - Only available to cars with openpilot Longitudinal Control - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Torque Lateral Control Live Tune - - - - Enables live tune for Torque lateral control. - - - - "Torque Lateral Control Live Tune"<br>You must restart your car or your device to apply these changes. - - - - Torque Lateral Controller Self-Tune - - - - Enables self-tune for Torque lateral control. - - - - "Torque Lateral Controller Self-Tune"<br>You must restart your car or your device to apply these changes. - - - - Enable Hands on Wheel Monitoring - - - - Monitor and alert when driver is not keeping the hands on the steering wheel. - - - - Enable Vision Based Turn Speed Control (V-TSC) - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Enable Speed Limit Control (SLC) - - - - Use speed limit signs information from map data and car interface (if applicable) to automatically adapt cruise speed to road limits. - - - - Enable Speed Limit % Offset - - - - Set speed limit slightly higher than actual speed limit for a more natural drive. - - - - Enable Map Data Turn Speed Control (M-TSC) - - - - Use curvature information from map data to define speed limits to take turns ahead. - - - - ACC +/-: Long Press Reverse - - - - Change the ACC +/- buttons behavior with cruise speed change in sunnypilot. - - - - Disabled (Stock): Short=1, Long = 5 (imperial) / 10 (metric) - - - - Enabled: Short = 5 (imperial) / 10 (metric), Long=1 - - - - OSM: Use Offline Database - - - - - SPGeneralPanel - - Quiet Drive 🤫 - - - - sunnypilot will display alerts but only play the most important warning sounds. This feature can be toggled while the car is on. - - - - Green Traffic Light Chime (Beta) - - - - A chime will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. If you are waiting behind another vehicle, the chime will play once the vehicle advances unless ACC is engaged. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Custom Boot Screen - - - - Display sunnypilot welcome screen while booting the device. - - - - Disable Onroad Uploads - - - - Disable uploads completely when onroad. Necessary to avoid high data usage when connected to Wi-Fi hotspot. Turn on this feature if you are looking to utilize map-based features, such as Speed Limit Control (SLC) and Map-based Turn Speed Control (MTSC). - - - - Error Troubleshoot - - - - VIEW - 보기 - - - Display error from the tmux session when an error has occurred from a system process. - - - - Debug snapshot on screen center tap - - - - Stores snapshot file with current state of some modules. - - - - - SPVehiclesPanel - - Hyundai/Kia/Genesis (HKG) - - - - Enhanced SCC Radar Interceptor ℹ - - - - sunnypilot will take over control of gas and brakes and will <b>NOT</b> disable the car's radar. This preserves AEB. - - - - Note: Only enable this feature if you have the Enhanced SCC Radar Interceptor hardware installed. - - - - Want to get one for your car? Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Toyota/Lexus - - - - Volkswagen MQB/PQ - - - - - SPVisualsPanel - - Display Braking Status - - - - Enable this will turn the current speed value to red while the brake is used. - - - - Display Stand Still Timer - - - - Enable this will display time spent at a stop (i.e., at a stop lights, stop signs, traffic congestions). - - - - OSM: Show debug UI elements - - - - OSM: Show UI elements that aid debugging. - - - - Enable Mapbox Navigation* - - - - Enable built-in navigation on sunnypilot, powered by Mapbox. - - - - Access via the web interface: "http://<device_ip>:8082" - - - - If you do not have comma Prime, you will need to provide your own Mapbox token at https://mapbox.com/. Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Huge thanks to the dragonpilot team for making this possible! - - - - "Enable Mapbox Navigation" -You must restart your car or your device to apply these changes. -Reboot now? - - - - Show Developer UI - - - - Show developer UI (Dev UI) for real-time parameters from various sources. - - - SettingsWindow @@ -1148,22 +623,6 @@ Reboot now? Navigation 네비게이션 - - SP - General - - - - SP - Controls - - - - SP - Vehicles - - - - SP - Visuals - - Setup @@ -1396,43 +855,25 @@ Reboot now? Uninstall 제거 - - - SpeedLimitStyle - Speed Limit Style - + failed to check for update + 업데이트 확인 실패 - MUTCD: US/Canada - + up to date, last checked %1 + 최신 상태, 마지막으로 확인 %1 - Vienna: Europe/Asia/etc. - + DOWNLOAD + 다운로드 - MUTCD - + update available + 업데이트 가능 - Vienna - - - - - SpeedLimitValueOffset - - Speed Limit Offset (MPH / KM/H) ℹ - - - - Set speed limit higher or lower than actual speed limit for a more personalized drive. - - - - To use this feature, turn off "Enable Speed Limit % Offset". - + never + 업데이트 안함 @@ -1502,6 +943,14 @@ Reboot now? TogglesPanel + + Enable openpilot + openpilot 사용 + + + 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. + 어댑티브 크루즈 컨트롤 및 차선 유지 운전자 보조를 위해 openpilot 시스템을 사용하십시오. 이 기능을 사용하려면 항상 주의를 기울여야 합니다. 설정변경은 장치 재부팅후 적용됩니다. + Enable Lane Departure Warnings 차선 이탈 경고 사용 @@ -1526,14 +975,14 @@ Reboot now? Upload data from the driver facing camera and help improve the driver monitoring algorithm. 운전자 카메라에서 데이터를 업로드하고 운전자 모니터링 알고리즘을 개선합니다. - - Experimental openpilot Longitudinal Control - openpilot 롱컨트롤 (실험적) - Disengage on Accelerator Pedal 가속페달 조작시 해제 + + When enabled, pressing the accelerator pedal will disengage openpilot. + 활성화된 경우 가속 페달을 누르면 openpilot이 해제됩니다. + Show ETA in 24h Format 24시간 형식으로 도착예정시간 표시 @@ -1554,14 +1003,6 @@ Reboot now? 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 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>을 활성화 합니다. 실험적 모드의 특징은 아래에 나열되어 있습니다 @@ -1590,46 +1031,44 @@ Reboot now? openpilot longitudinal control may come in a future update. 오픈파일럿 롱컨트롤은 향후 업데이트에서 제공될 수 있습니다. - - An experimental version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 오픈파일럿 롱컨트롤의 실험 버전은 실험적 모드와 함께 릴리즈 되지 않은 브랜치에서 테스트할 수 있습니다. - Enable experimental longitudinal control to allow Experimental mode. 실험적 롱컨트롤을 사용하려면 실험적 모드를 활성화 하세요. - Enable sunnypilot - + openpilot Longitudinal Control (Alpha) + openpilot 롱컨트롤 (알파) - Use the sunnypilot 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. - + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 경고: openpilot 롱컨트롤은 알파 기능으로 차량의 자동긴급제동(AEB)를 비활성화합니다. - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - - TorqueFriction - - FRICTION - + 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 enabling openpilot longitudinal control alpha. + 이 차량은 openpilot 롱컨트롤 대신 차량의 내장 ACC로 기본 설정됩니다. openpilot 롱컨트롤으로 전환하려면 이 기능을 활성화하세요. openpilot 롱컨트롤 알파를 활성화하는경우 실험적 모드 활성화를 권장합니다. - Adjust Friction for the Torque Lateral Controller - - - - - TorqueMaxLatAccel - - LAT_ACCEL_FACTOR - + Aggressive + 공격적 - Adjust Max Lateral Acceleration for the Torque Lateral Controller + Standard + 표준 + + + Relaxed + 편안한 + + + Driving Personality + 주행 모드 + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. + 표준 모드를 권장합니다. 공격적 모드에서는 openpilot은 앞차를 더 가까이 따라가며 가속과 감속을 더 공격적으로 사용합니다. 편안한 모드에서 openpilot은 선두 차량에서 더 멀리 떨어져 있습니다. + + + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. @@ -1669,22 +1108,26 @@ Reboot now? - VwAccType + WiFiPromptWidget - Short Press +1/-1 Type - + Setup Wi-Fi + Wi-Fi 설정 - Define the type of ACC control your car has with short press to +1 or -1. - + Connect to Wi-Fi to upload driving data and help improve openpilot + Wi-Fi에 연결하여 주행 데이터를 업로드하고 openpilot 개선에 참여하세요. - +/- - + Open Settings + 설정 열기 - RES/SET - + Uploading training data + 트레이닝 데이터 업로드 + + + Your data is used to train driving models and help improve openpilot + 귀하의 데이터는 운전 모델을 교육하고 openpilot을 개선하는 데 사용됩니다. diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 34fbc8abf0..87a813111e 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -285,11 +285,11 @@ ExperimentalModeButton EXPERIMENTAL MODE ON - MODO EXPERIMENTAL ATIVADO + MODO EXPERIMENTAL ON CHILL MODE ON - MODO CHILL ATIVADO + MODO CHILL ON @@ -519,14 +519,6 @@ trabalho definido comma prime comma prime - - CONNECT.COMMA.AI - CONNECT.COMMA.AI - - - COMMA POINTS - PONTOS COMMA - QObject @@ -867,6 +859,26 @@ Isso pode levar até um minuto. Uninstall Desinstalar + + failed to check for update + falha ao verificar por atualizações + + + up to date, last checked %1 + atualizado, última verificação %1 + + + DOWNLOAD + BAIXAR + + + update available + atualização disponível + + + never + nunca + SshControl @@ -1023,10 +1035,6 @@ Isso pode levar até um minuto. openpilot longitudinal control may come in a future update. O controle longitudinal openpilot pode vir em uma atualização futura. - - An experimental version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Uma versão experimental do controle longitudinal openpilot pode ser testada, juntamente com o modo Experimental, em branches de desenvolvimento. - Enable experimental longitudinal control to allow Experimental mode. Ative o controle longitudinal experimental para permitir o modo Experimental. @@ -1043,6 +1051,30 @@ Isso pode levar até um minuto. 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 enabling openpilot longitudinal control alpha. Neste carro, o openpilot tem como padrão o ACC embutido do carro em vez do controle longitudinal do openpilot. Habilite isso para alternar para o controle longitudinal openpilot. Recomenda-se ativar o modo Experimental ao ativar o alfa de controle longitudinal openpilot. + + Aggressive + Disputa + + + Standard + Neutro + + + Relaxed + Calmo + + + Driving Personality + Temperamento de Direção + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. + Neutro é o recomendado. No modo disputa o openpilot seguirá o carro da frente mais de perto e será mais agressivo com a aceleração e frenagem. No modo calmo o openpilot se manterá mais longe do carro da frente. + + + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + Updater @@ -1079,6 +1111,30 @@ Isso pode levar até um minuto. Falha na atualização + + WiFiPromptWidget + + Setup Wi-Fi + Configurar +Wi-Fi + + + Connect to Wi-Fi to upload driving data and help improve openpilot + Conecte se ao Wi-Fi para upload de dados de condução e ajudar a melhorar o openpilot + + + Open Settings + Abrir Configurações + + + Uploading training data + Subindo dados para treinamento + + + Your data is used to train driving models and help improve openpilot + Seus dados são utilizados para treinar modelos de direção e ajudar a melhorar o openpilot + + WifiUI diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index cc55b367b2..4a92d963d2 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -89,71 +89,6 @@ LIMIT LIMIT - - Lane -only - - - - Lane -less - - - - Auto -Lane - - - - - AutoLaneChangeTimer - - Auto Lane Change Timer - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - Nudge - - - - Nudgeless - - - - s - - - - - BrightnessControl - - Brightness Control (Global, %) - - - - Manually adjusts the global brightness of the screen. - - - - Auto - - - - - CameraOffset - - Camera Offset (cm) - - - - Hack to trick vehicle to be left or right biased in its lane. Decreasing the value will make the car keep more left, increasing will make it keep more right. Changes take effect immediately. - - ConfirmationDialog @@ -181,25 +116,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 拒绝并卸载%1 - - DevUiRow - - Developer UI List - - - - Select the number of lists of real-time parameters you would like to display on the sunnypilot screen while driving. - - - - 1-Row - - - - 2-Row - - - DevicePanel @@ -246,6 +162,10 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 查看 + + Review the rules, features, and limitations of openpilot + 查看openpilot的使用规则,以及其功能和限制。 + Are you sure you want to review the training guide? 您确定要查看新手指南吗? @@ -278,6 +198,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off 关机 + + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. + openpilot要求设备安装的偏航角在左4°和右4°之间,俯仰角在上5°和下8°之间。一般来说,openpilot会持续更新校准,很少需要重置。 + Your device is pointed %1° %2 and %3° %4. 您的设备校准为%1° %2、%3° %4。 @@ -322,14 +246,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Review 预览 - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. sunnypilot is continuously calibrating, resetting is rarely required. - - DriveStats @@ -376,37 +292,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 轻松模式运行 - - GapAdjustCruiseMode - - Mode - - - - SW: Steering Wheel Button only - - - - UI: User Interface Button on screen only - - - - SW + UI: Steering Wheel Button + User Interface Button on screen - - - - S.W. - - - - UI - - - - S.W. + UI - - - InputDialog @@ -519,37 +404,6 @@ location set 等待 GPS - - MaxTimeOffroad - - Max Time Offroad - - - - Device is automatically turned off after a set time when the engine is turned off (off-road) after driving (on-road). - - - - Always On - - - - Immediate - - - - s - - - - m - - - - h - - - MultiOptionDialog @@ -595,40 +449,6 @@ location set 警报 - - OnroadScreenOff - - Driving Screen Off Timer - - - - Turn off the device screen or reduce brightness to protect the screen after driving starts. It automatically brightens or turns on when a touch or event occurs. - - - - Always On - - - - s - - - - - OnroadScreenOffBrightness - - Driving Screen Off Brightness (%) - - - - When using the Driving Screen Off feature, the brightness is reduced according to the automatic brightness ratio. - - - - Dark - - - PairingPopup @@ -659,17 +479,6 @@ location set 启用 - - PathOffset - - Path Offset (cm) - - - - Hack to trick the model path to be left or right biased of the lane. Decreasing the value will shift the model more left, increasing will shift the model more right. Changes take effect immediately. - - - PrimeAdWidget @@ -707,14 +516,6 @@ location set comma prime comma prime - - CONNECT.COMMA.AI - CONNECT.COMMA.AI - - - COMMA POINTS - COMMA POINTS点数 - QObject @@ -730,6 +531,10 @@ location set dashcam 行车记录仪 + + openpilot + openpilot + %n minute(s) ago @@ -748,10 +553,6 @@ location set %n 天前 - - sunnypilot - - Reset @@ -793,332 +594,6 @@ This may take up to a minute. - - SPControlsPanel - - Enable M.A.D.S. - - - - Enable the beloved M.A.D.S. feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Disengage ALC On Brake Pedal ℹ - - - - Define brake pedal interactions with sunnypilot when M.A.D.S. is enabled. - - - - Enabled: Pressing the brake pedal will disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Disabled: Pressing the brake pedal will <b>NOT</b> disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Enable ACC+MADS with RES+/SET- - - - - Engage both M.A.D.S. and ACC with a single press of RES+ or SET- button. - - - - Note: Once M.A.D.S. is engaged via this mode, it will remain engaged until it is manually disabled via the M.A.D.S. button or car shut off. - - - - Pause Lateral Below Speed w/ Blinker - - - - Enable this toggle to pause lateral actuation with blinker when traveling below 30 MPH or 50 KM/H. - - - - Enable Dynamic Lane Profile - - - - Enable toggle to use Dynamic Lane Profile. Disable toggle to use Laneless only. - - - - Laneless for Curves in "Auto lane" - - - - While in Auto Lane, switch to Laneless for current/future curves. - - - - Custom Offsets - - - - Add custom offsets to Camera and Path in sunnypilot. - - - - Enable Gap Adjust Cruise ℹ - - - - Enable the Interval button on the steering wheel to adjust the cruise gap. - - - - Only available to cars with openpilot Longitudinal Control - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Torque Lateral Control Live Tune - - - - Enables live tune for Torque lateral control. - - - - "Torque Lateral Control Live Tune"<br>You must restart your car or your device to apply these changes. - - - - Torque Lateral Controller Self-Tune - - - - Enables self-tune for Torque lateral control. - - - - "Torque Lateral Controller Self-Tune"<br>You must restart your car or your device to apply these changes. - - - - Enable Hands on Wheel Monitoring - - - - Monitor and alert when driver is not keeping the hands on the steering wheel. - - - - Enable Vision Based Turn Speed Control (V-TSC) - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Enable Speed Limit Control (SLC) - - - - Use speed limit signs information from map data and car interface (if applicable) to automatically adapt cruise speed to road limits. - - - - Enable Speed Limit % Offset - - - - Set speed limit slightly higher than actual speed limit for a more natural drive. - - - - Enable Map Data Turn Speed Control (M-TSC) - - - - Use curvature information from map data to define speed limits to take turns ahead. - - - - ACC +/-: Long Press Reverse - - - - Change the ACC +/- buttons behavior with cruise speed change in sunnypilot. - - - - Disabled (Stock): Short=1, Long = 5 (imperial) / 10 (metric) - - - - Enabled: Short = 5 (imperial) / 10 (metric), Long=1 - - - - OSM: Use Offline Database - - - - - SPGeneralPanel - - Quiet Drive 🤫 - - - - sunnypilot will display alerts but only play the most important warning sounds. This feature can be toggled while the car is on. - - - - Green Traffic Light Chime (Beta) - - - - A chime will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. If you are waiting behind another vehicle, the chime will play once the vehicle advances unless ACC is engaged. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Custom Boot Screen - - - - Display sunnypilot welcome screen while booting the device. - - - - Disable Onroad Uploads - - - - Disable uploads completely when onroad. Necessary to avoid high data usage when connected to Wi-Fi hotspot. Turn on this feature if you are looking to utilize map-based features, such as Speed Limit Control (SLC) and Map-based Turn Speed Control (MTSC). - - - - Error Troubleshoot - - - - VIEW - 查看 - - - Display error from the tmux session when an error has occurred from a system process. - - - - Debug snapshot on screen center tap - - - - Stores snapshot file with current state of some modules. - - - - - SPVehiclesPanel - - Hyundai/Kia/Genesis (HKG) - - - - Enhanced SCC Radar Interceptor ℹ - - - - sunnypilot will take over control of gas and brakes and will <b>NOT</b> disable the car's radar. This preserves AEB. - - - - Note: Only enable this feature if you have the Enhanced SCC Radar Interceptor hardware installed. - - - - Want to get one for your car? Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Toyota/Lexus - - - - Volkswagen MQB/PQ - - - - - SPVisualsPanel - - Display Braking Status - - - - Enable this will turn the current speed value to red while the brake is used. - - - - Display Stand Still Timer - - - - Enable this will display time spent at a stop (i.e., at a stop lights, stop signs, traffic congestions). - - - - OSM: Show debug UI elements - - - - OSM: Show UI elements that aid debugging. - - - - Enable Mapbox Navigation* - - - - Enable built-in navigation on sunnypilot, powered by Mapbox. - - - - Access via the web interface: "http://<device_ip>:8082" - - - - If you do not have comma Prime, you will need to provide your own Mapbox token at https://mapbox.com/. Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Huge thanks to the dragonpilot team for making this possible! - - - - "Enable Mapbox Navigation" -You must restart your car or your device to apply these changes. -Reboot now? - - - - Show Developer UI - - - - Show developer UI (Dev UI) for real-time parameters from various sources. - - - SettingsWindow @@ -1145,22 +620,6 @@ Reboot now? Navigation 导航 - - SP - General - SP - 通用 - - - SP - Controls - SP - 控制 - - - SP - Vehicles - SP - 车辆 - - - SP - Visuals - SP - 界面 - Setup @@ -1393,42 +852,24 @@ Reboot now? Uninstall 卸载 - - - SpeedLimitStyle - Speed Limit Style + failed to check for update - MUTCD: US/Canada + up to date, last checked %1 - Vienna: Europe/Asia/etc. + DOWNLOAD - MUTCD + update available - Vienna - - - - - SpeedLimitValueOffset - - Speed Limit Offset (MPH / KM/H) ℹ - - - - Set speed limit higher or lower than actual speed limit for a more personalized drive. - - - - To use this feature, turn off "Enable Speed Limit % Offset". + never @@ -1499,6 +940,14 @@ Reboot now? TogglesPanel + + Enable openpilot + 启用openpilot + + + 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. + 使用openpilot进行自适应巡航和车道保持辅助。使用此功能时您必须时刻保持注意力。该设置的更改在熄火时生效。 + Enable Lane Departure Warnings 启用车道偏离警告 @@ -1523,14 +972,14 @@ Reboot now? Upload data from the driver facing camera and help improve the driver monitoring algorithm. 上传驾驶员摄像头的数据,帮助改进驾驶员监控算法。 - - Experimental openpilot Longitudinal Control - 试验性的openpilot纵向控制 - Disengage on Accelerator Pedal 踩油门时取消控制 + + When enabled, pressing the accelerator pedal will disengage openpilot. + 启用后,踩下油门踏板将取消openpilot。 + Show ETA in 24h Format 以24小时格式显示预计到达时间 @@ -1551,14 +1000,6 @@ Reboot now? 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纵向控制时,建议同时启用试验模式。 - 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>。试验性功能包括: @@ -1587,46 +1028,44 @@ Reboot now? openpilot longitudinal control may come in a future update. - - An experimental version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - Enable experimental longitudinal control to allow Experimental mode. 启用试验性的纵向控制,以便允许使用试验模式。 - Enable sunnypilot + openpilot Longitudinal Control (Alpha) - Use the sunnypilot 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. + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - - TorqueFriction - - FRICTION + 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 enabling openpilot longitudinal control alpha. - Adjust Friction for the Torque Lateral Controller - - - - - TorqueMaxLatAccel - - LAT_ACCEL_FACTOR + Aggressive - Adjust Max Lateral Acceleration for the Torque Lateral Controller + Standard + + + + Relaxed + + + + Driving Personality + + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. + + + + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. @@ -1666,21 +1105,25 @@ Reboot now? - VwAccType + WiFiPromptWidget - Short Press +1/-1 Type + Setup Wi-Fi - Define the type of ACC control your car has with short press to +1 or -1. + Connect to Wi-Fi to upload driving data and help improve openpilot - +/- + Open Settings - RES/SET + Uploading training data + + + + Your data is used to train driving models and help improve openpilot diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 344840996f..01fab62feb 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -89,71 +89,6 @@ LIMIT 速限 - - Lane -only - - - - Lane -less - - - - Auto -Lane - - - - - AutoLaneChangeTimer - - Auto Lane Change Timer - - - - Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. -Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - - - - Nudge - - - - Nudgeless - - - - s - - - - - BrightnessControl - - Brightness Control (Global, %) - - - - Manually adjusts the global brightness of the screen. - - - - Auto - - - - - CameraOffset - - Camera Offset (cm) - - - - Hack to trick vehicle to be left or right biased in its lane. Decreasing the value will make the car keep more left, increasing will make it keep more right. Changes take effect immediately. - - ConfirmationDialog @@ -181,25 +116,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 拒絕並解除安裝 %1 - - DevUiRow - - Developer UI List - - - - Select the number of lists of real-time parameters you would like to display on the sunnypilot screen while driving. - - - - 1-Row - - - - 2-Row - - - DevicePanel @@ -246,6 +162,10 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 觀看 + + Review the rules, features, and limitations of openpilot + 觀看 openpilot 的使用規則、功能和限制 + Are you sure you want to review the training guide? 您確定要觀看使用教學嗎? @@ -326,14 +246,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Review 回顧 - - Review the rules, features, and limitations of sunnypilot - - - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. sunnypilot is continuously calibrating, resetting is rarely required. - - DriveStats @@ -380,37 +292,6 @@ Please use caution when using this feature. Only use the blinker when traffic an 輕鬆模式 ON - - GapAdjustCruiseMode - - Mode - - - - SW: Steering Wheel Button only - - - - UI: User Interface Button on screen only - - - - SW + UI: Steering Wheel Button + User Interface Button on screen - - - - S.W. - - - - UI - - - - S.W. + UI - - - InputDialog @@ -525,37 +406,6 @@ location set 等待 GPS - - MaxTimeOffroad - - Max Time Offroad - - - - Device is automatically turned off after a set time when the engine is turned off (off-road) after driving (on-road). - - - - Always On - - - - Immediate - - - - s - - - - m - - - - h - - - MultiOptionDialog @@ -601,40 +451,6 @@ location set 提醒 - - OnroadScreenOff - - Driving Screen Off Timer - - - - Turn off the device screen or reduce brightness to protect the screen after driving starts. It automatically brightens or turns on when a touch or event occurs. - - - - Always On - - - - s - - - - - OnroadScreenOffBrightness - - Driving Screen Off Brightness (%) - - - - When using the Driving Screen Off feature, the brightness is reduced according to the automatic brightness ratio. - - - - Dark - - - PairingPopup @@ -665,17 +481,6 @@ location set 啟用 - - PathOffset - - Path Offset (cm) - - - - Hack to trick the model path to be left or right biased of the lane. Decreasing the value will shift the model more left, increasing will shift the model more right. Changes take effect immediately. - - - PrimeAdWidget @@ -713,14 +518,6 @@ location set comma prime comma 高級會員 - - CONNECT.COMMA.AI - CONNECT.COMMA.AI - - - COMMA POINTS - COMMA 積分 - QObject @@ -736,6 +533,10 @@ location set dashcam 行車記錄器 + + openpilot + openpilot + %n minute(s) ago @@ -754,10 +555,6 @@ location set %n 天前 - - sunnypilot - - Reset @@ -799,332 +596,6 @@ This may take up to a minute. 設備重置中 此過程可能需要幾分鐘 - - SPControlsPanel - - Enable M.A.D.S. - - - - Enable the beloved M.A.D.S. feature. Disable toggle to revert back to stock openpilot engagement/disengagement. - - - - Disengage ALC On Brake Pedal ℹ - - - - Define brake pedal interactions with sunnypilot when M.A.D.S. is enabled. - - - - Enabled: Pressing the brake pedal will disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Disabled: Pressing the brake pedal will <b>NOT</b> disengage Automatic Lane Centering (ALC) on sunnypilot. - - - - Enable ACC+MADS with RES+/SET- - - - - Engage both M.A.D.S. and ACC with a single press of RES+ or SET- button. - - - - Note: Once M.A.D.S. is engaged via this mode, it will remain engaged until it is manually disabled via the M.A.D.S. button or car shut off. - - - - Pause Lateral Below Speed w/ Blinker - - - - Enable this toggle to pause lateral actuation with blinker when traveling below 30 MPH or 50 KM/H. - - - - Enable Dynamic Lane Profile - - - - Enable toggle to use Dynamic Lane Profile. Disable toggle to use Laneless only. - - - - Laneless for Curves in "Auto lane" - - - - While in Auto Lane, switch to Laneless for current/future curves. - - - - Custom Offsets - - - - Add custom offsets to Camera and Path in sunnypilot. - - - - Enable Gap Adjust Cruise ℹ - - - - Enable the Interval button on the steering wheel to adjust the cruise gap. - - - - Only available to cars with openpilot Longitudinal Control - - - - Enforce Torque Lateral Control - - - - Enable this to enforce sunnypilot to steer with Torque lateral control. - - - - Torque Lateral Control Live Tune - - - - Enables live tune for Torque lateral control. - - - - "Torque Lateral Control Live Tune"<br>You must restart your car or your device to apply these changes. - - - - Torque Lateral Controller Self-Tune - - - - Enables self-tune for Torque lateral control. - - - - "Torque Lateral Controller Self-Tune"<br>You must restart your car or your device to apply these changes. - - - - Enable Hands on Wheel Monitoring - - - - Monitor and alert when driver is not keeping the hands on the steering wheel. - - - - Enable Vision Based Turn Speed Control (V-TSC) - - - - Use vision path predictions to estimate the appropriate speed to drive through turns ahead. - - - - Enable Speed Limit Control (SLC) - - - - Use speed limit signs information from map data and car interface (if applicable) to automatically adapt cruise speed to road limits. - - - - Enable Speed Limit % Offset - - - - Set speed limit slightly higher than actual speed limit for a more natural drive. - - - - Enable Map Data Turn Speed Control (M-TSC) - - - - Use curvature information from map data to define speed limits to take turns ahead. - - - - ACC +/-: Long Press Reverse - - - - Change the ACC +/- buttons behavior with cruise speed change in sunnypilot. - - - - Disabled (Stock): Short=1, Long = 5 (imperial) / 10 (metric) - - - - Enabled: Short = 5 (imperial) / 10 (metric), Long=1 - - - - OSM: Use Offline Database - - - - - SPGeneralPanel - - Quiet Drive 🤫 - - - - sunnypilot will display alerts but only play the most important warning sounds. This feature can be toggled while the car is on. - - - - Green Traffic Light Chime (Beta) - - - - A chime will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. If you are waiting behind another vehicle, the chime will play once the vehicle advances unless ACC is engaged. - - - - Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly. - - - - Custom Boot Screen - - - - Display sunnypilot welcome screen while booting the device. - - - - Disable Onroad Uploads - - - - Disable uploads completely when onroad. Necessary to avoid high data usage when connected to Wi-Fi hotspot. Turn on this feature if you are looking to utilize map-based features, such as Speed Limit Control (SLC) and Map-based Turn Speed Control (MTSC). - - - - Error Troubleshoot - - - - VIEW - 觀看 - - - Display error from the tmux session when an error has occurred from a system process. - - - - Debug snapshot on screen center tap - - - - Stores snapshot file with current state of some modules. - - - - - SPVehiclesPanel - - Hyundai/Kia/Genesis (HKG) - - - - Enhanced SCC Radar Interceptor ℹ - - - - sunnypilot will take over control of gas and brakes and will <b>NOT</b> disable the car's radar. This preserves AEB. - - - - Note: Only enable this feature if you have the Enhanced SCC Radar Interceptor hardware installed. - - - - Want to get one for your car? Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Toyota/Lexus - - - - Volkswagen MQB/PQ - - - - - SPVisualsPanel - - Display Braking Status - - - - Enable this will turn the current speed value to red while the brake is used. - - - - Display Stand Still Timer - - - - Enable this will display time spent at a stop (i.e., at a stop lights, stop signs, traffic congestions). - - - - OSM: Show debug UI elements - - - - OSM: Show UI elements that aid debugging. - - - - Enable Mapbox Navigation* - - - - Enable built-in navigation on sunnypilot, powered by Mapbox. - - - - Access via the web interface: "http://<device_ip>:8082" - - - - If you do not have comma Prime, you will need to provide your own Mapbox token at https://mapbox.com/. Reach out to sunnyhaibin#0865 on Discord for more information. - - - - Huge thanks to the dragonpilot team for making this possible! - - - - "Enable Mapbox Navigation" -You must restart your car or your device to apply these changes. -Reboot now? - - - - Show Developer UI - - - - Show developer UI (Dev UI) for real-time parameters from various sources. - - - SettingsWindow @@ -1151,22 +622,6 @@ Reboot now? Navigation 導航 - - SP - General - SP - 通用 - - - SP - Controls - SP - 控制 - - - SP - Vehicles - SP - 車輛 - - - SP - Visuals - SP - 界面 - Setup @@ -1399,43 +854,25 @@ Reboot now? Uninstall 解除安裝 - - - SpeedLimitStyle - Speed Limit Style - + failed to check for update + 檢查更新失敗 - MUTCD: US/Canada - + up to date, last checked %1 + 已經是最新版本,上次檢查時間為 %1 - Vienna: Europe/Asia/etc. - + DOWNLOAD + 下載 - MUTCD - + update available + 有可用的更新 - Vienna - - - - - SpeedLimitValueOffset - - Speed Limit Offset (MPH / KM/H) ℹ - - - - Set speed limit higher or lower than actual speed limit for a more personalized drive. - - - - To use this feature, turn off "Enable Speed Limit % Offset". - + never + 從未更新 @@ -1505,6 +942,14 @@ Reboot now? TogglesPanel + + Enable openpilot + 啟用 openpilot + + + 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. + 使用 openpilot 的主動式巡航和車道保持功能,開啟後您需要持續集中注意力,設定變更在重新啟動車輛後生效。 + Enable Lane Departure Warnings 啟用車道偏離警告 @@ -1529,14 +974,14 @@ Reboot now? Upload data from the driver facing camera and help improve the driver monitoring algorithm. 上傳駕駛監控的錄像來協助我們提升駕駛監控的準確率。 - - Experimental openpilot Longitudinal Control - 使用 openpilot 縱向控制(實驗) - Disengage on Accelerator Pedal 油門取消控車 + + When enabled, pressing the accelerator pedal will disengage openpilot. + 啟用後,踩踏油門將會取消 openpilot 控制。 + Show ETA in 24h Format 預計到達時間單位改用 24 小時制 @@ -1557,14 +1002,6 @@ Reboot now? 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 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>。實驗功能如下: @@ -1593,46 +1030,44 @@ Reboot now? openpilot longitudinal control may come in a future update. 未來可能會推出openpilot縱向控制 - - An experimental version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 在非發行分支中 可找到包含實驗模式的openpilot縱向控制測試版本 - Enable experimental longitudinal control to allow Experimental mode. 啟用實驗性縱向控制以使用實驗模式。 - Enable sunnypilot - + openpilot Longitudinal Control (Alpha) + openpilot 縱向控制 (Alpha 版) - Use the sunnypilot 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. - + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 警告:此車輛的 Openpilot 縱向控制功能目前處於 Alpha 版本,使用此功能將會停用自動緊急制動(AEB)功能。 - When enabled, pressing the accelerator pedal will disengage sunnypilot. - - - - - TorqueFriction - - FRICTION - + 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 enabling openpilot longitudinal control alpha. + 在這輛車上,Openpilot 預設使用車輛內建的主動巡航控制(ACC),而非 Openpilot 的縱向控制。啟用此項功能可切換至 Openpilot 的縱向控制。當啟用 Openpilot 縱向控制 Alpha 版本時,建議同時啟用實驗性模式(Experimental mode)。 - Adjust Friction for the Torque Lateral Controller - - - - - TorqueMaxLatAccel - - LAT_ACCEL_FACTOR - + Aggressive + 積極 - Adjust Max Lateral Acceleration for the Torque Lateral Controller + Standard + 標準 + + + Relaxed + 舒適 + + + Driving Personality + 駕駛風格 + + + Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. + 推薦使用標準模式。在積極模式中,openpilot 會更靠近前車並在加速和剎車方面更積極。在舒適模式中,openpilot 會與前車保持較遠的距離。 + + + An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. @@ -1672,22 +1107,26 @@ Reboot now? - VwAccType + WiFiPromptWidget - Short Press +1/-1 Type - + Setup Wi-Fi + 設置 Wi-Fi 連接 - Define the type of ACC control your car has with short press to +1 or -1. - + Connect to Wi-Fi to upload driving data and help improve openpilot + 請連接至 Wi-Fi 以上傳駕駛數據,並協助改進 openpilot。 - +/- - + Open Settings + 開啟設置 - RES/SET - + Uploading training data + 正在上傳訓練數據 + + + Your data is used to train driving models and help improve openpilot + 您的數據將用於訓練駕駛模型並協助改進 openpilot diff --git a/system/hardware/hw.h b/system/hardware/hw.h index 5599e79186..3b0583a10b 100644 --- a/system/hardware/hw.h +++ b/system/hardware/hw.h @@ -19,7 +19,7 @@ inline std::string log_root() { return Hardware::PC() ? util::getenv("HOME") + "/.comma/media/0/realdata" : "/data/media/0/realdata"; } inline std::string params() { - return Hardware::PC() ? util::getenv("HOME") + "/.comma/params" : "/data/params"; + return Hardware::PC() ? util::getenv("PARAMS_ROOT", util::getenv("HOME") + "/.comma/params") : "/data/params"; } inline std::string rsa_file() { return Hardware::PC() ? util::getenv("HOME") + "/.comma/persist/comma/id_rsa" : "/persist/comma/id_rsa"; diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index fba67d96c8..580dc83eec 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -77,6 +77,7 @@ public: static std::map get_init_logs() { std::map ret = { {"/BUILD", util::read_file("/BUILD")}, + {"lsblk", util::check_output("lsblk -o NAME,SIZE,STATE,VENDOR,MODEL,REV,SERIAL")}, }; std::string bs = util::check_output("abctl --boot_slot"); diff --git a/system/hardware/tici/tests/test_amplifier.py b/system/hardware/tici/tests/test_amplifier.py index 5d5a86c512..4cacef4080 100755 --- a/system/hardware/tici/tests/test_amplifier.py +++ b/system/hardware/tici/tests/test_amplifier.py @@ -21,6 +21,7 @@ class TestAmplifier(unittest.TestCase): # clear dmesg subprocess.check_call("sudo dmesg -C", shell=True) + Panda.wait_for_panda(None, 30) self.panda = Panda() self.panda.reset() diff --git a/system/hardware/tici/tests/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py index df4852183e..7ec31670b1 100755 --- a/system/hardware/tici/tests/test_power_draw.py +++ b/system/hardware/tici/tests/test_power_draw.py @@ -2,30 +2,51 @@ import unittest import time import math +import threading from dataclasses import dataclass from tabulate import tabulate +from typing import List +import cereal.messaging as messaging +from cereal.services import service_list from system.hardware import HARDWARE, TICI from system.hardware.tici.power_monitor import get_power from selfdrive.manager.process_config import managed_processes from selfdrive.manager.manager import manager_cleanup +SAMPLE_TIME = 8 # seconds to sample power @dataclass class Proc: name: str power: float + msgs: List[str] rtol: float = 0.05 atol: float = 0.12 warmup: float = 6. PROCS = [ - Proc('camerad', 2.1), - Proc('modeld', 0.93, atol=0.2), - Proc('dmonitoringmodeld', 0.4), - Proc('encoderd', 0.23), + Proc('camerad', 2.1, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), + Proc('modeld', 0.93, atol=0.2, msgs=['modelV2']), + Proc('dmonitoringmodeld', 0.4, msgs=['driverStateV2']), + Proc('encoderd', 0.23, msgs=[]), + Proc('mapsd', 0.05, msgs=['mapRenderState']), + Proc('navmodeld', 0.05, msgs=['navModel']), ] +def send_llk_msg(done): + pm = messaging.PubMaster(['liveLocationKalman']) + msg = messaging.new_message('liveLocationKalman') + msg.liveLocationKalman.positionGeodetic = {'value': [32.7174, -117.16277, 0], 'std': [0., 0., 0.], 'valid': True} + msg.liveLocationKalman.calibratedOrientationNED = {'value': [0., 0., 0.], 'std': [0., 0., 0.], 'valid': True} + msg.liveLocationKalman.status = 'valid' + + # Send liveLocationKalman at 20hz + while not done.is_set(): + msg.clear_write_flag() + pm.send('liveLocationKalman', msg) + time.sleep(1/20) + class TestPowerDraw(unittest.TestCase): @@ -46,27 +67,39 @@ class TestPowerDraw(unittest.TestCase): def test_camera_procs(self): baseline = get_power() + done = threading.Event() + thread = threading.Thread(target=send_llk_msg, args=(done,), daemon=True) + thread.start() prev = baseline used = {} + msg_counts = {} for proc in PROCS: + socks = {msg: messaging.sub_sock(msg) for msg in proc.msgs} managed_processes[proc.name].start() time.sleep(proc.warmup) + for sock in socks.values(): + messaging.drain_sock_raw(sock) - now = get_power(8) + now = get_power(SAMPLE_TIME) used[proc.name] = now - prev prev = now + for msg,sock in socks.items(): + msg_counts[msg] = len(messaging.drain_sock_raw(sock)) + done.set() manager_cleanup() - tab = [] - tab.append(['process', 'expected (W)', 'measured (W)']) + tab = [['process', 'expected (W)', 'measured (W)', '# msgs expected', '# msgs received']] for proc in PROCS: cur = used[proc.name] expected = proc.power - tab.append([proc.name, round(expected, 2), round(cur, 2)]) + msgs_received = sum(msg_counts[msg] for msg in proc.msgs) + msgs_expected = int(sum(SAMPLE_TIME * service_list[msg].frequency for msg in proc.msgs)) + tab.append([proc.name, round(expected, 2), round(cur, 2), msgs_expected, msgs_received]) with self.subTest(proc=proc.name): self.assertTrue(math.isclose(cur, expected, rel_tol=proc.rtol, abs_tol=proc.atol)) + self.assertTrue(math.isclose(msgs_expected, msgs_received, rel_tol=.02, abs_tol=2)) print(tabulate(tab)) print(f"Baseline {baseline:.2f}W\n") diff --git a/system/loggerd/encoder/encoder.cc b/system/loggerd/encoder/encoder.cc index 08eaf0ff01..7ac2948861 100644 --- a/system/loggerd/encoder/encoder.cc +++ b/system/loggerd/encoder/encoder.cc @@ -5,9 +5,7 @@ VideoEncoder::~VideoEncoder() {} void VideoEncoder::publisher_init() { // publish - service_name = this->type == DriverCam ? "driverEncodeData" : - (this->type == WideRoadCam ? "wideRoadEncodeData" : - (this->in_width == this->out_width ? "roadEncodeData" : "qRoadEncodeData")); + service_name = this->publish_name; pm.reset(new PubMaster({service_name})); } @@ -38,45 +36,5 @@ void VideoEncoder::publisher_publish(VideoEncoder *e, int segment_num, uint32_t auto words = new kj::Array(capnp::messageToFlatArray(msg)); auto bytes = words->asBytes(); e->pm->send(e->service_name, bytes.begin(), bytes.size()); - if (e->write) { - e->to_write.push(words); - } else { - delete words; - } -} - -// TODO: writing should be moved to loggerd -void VideoEncoder::write_handler(VideoEncoder *e, const char *path) { - VideoWriter writer(path, e->filename, e->codec != cereal::EncodeIndex::Type::FULL_H_E_V_C, e->out_width, e->out_height, e->fps, e->codec); - - bool first = true; - kj::Array* out_buf; - while ((out_buf = e->to_write.pop())) { - capnp::FlatArrayMessageReader cmsg(*out_buf); - cereal::Event::Reader event = cmsg.getRoot(); - - auto edata = (e->type == DriverCam) ? event.getDriverEncodeData() : - ((e->type == WideRoadCam) ? event.getWideRoadEncodeData() : - (e->in_width == e->out_width ? event.getRoadEncodeData() : event.getQRoadEncodeData())); - auto idx = edata.getIdx(); - auto flags = idx.getFlags(); - - if (first) { - assert(flags & V4L2_BUF_FLAG_KEYFRAME); - auto header = edata.getHeader(); - writer.write((uint8_t *)header.begin(), header.size(), idx.getTimestampEof()/1000, true, false); - first = false; - } - - // dangerous cast from const, but should be fine - auto data = edata.getData(); - if (data.size() > 0) { - writer.write((uint8_t *)data.begin(), data.size(), idx.getTimestampEof()/1000, false, flags & V4L2_BUF_FLAG_KEYFRAME); - } - - // free the data - delete out_buf; - } - - // VideoWriter is freed on out of scope + delete words; } diff --git a/system/loggerd/encoder/encoder.h b/system/loggerd/encoder/encoder.h index 88dd5cee23..9ea6b884cf 100644 --- a/system/loggerd/encoder/encoder.h +++ b/system/loggerd/encoder/encoder.h @@ -7,7 +7,6 @@ #include "cereal/messaging/messaging.h" #include "cereal/visionipc/visionipc.h" #include "common/queue.h" -#include "system/loggerd/video_writer.h" #include "system/camerad/cameras/camera_common.h" #define V4L2_BUF_FLAG_KEYFRAME 8 @@ -15,9 +14,11 @@ class VideoEncoder { public: VideoEncoder(const char* filename, CameraType type, int in_width, int in_height, int fps, - int bitrate, cereal::EncodeIndex::Type codec, int out_width, int out_height, bool write) + int bitrate, cereal::EncodeIndex::Type codec, int out_width, int out_height, + const char* publish_name) : filename(filename), type(type), in_width(in_width), in_height(in_height), fps(fps), - bitrate(bitrate), codec(codec), out_width(out_width), out_height(out_height), write(write) { } + bitrate(bitrate), codec(codec), out_width(out_width), out_height(out_height), + publish_name(publish_name) { } virtual ~VideoEncoder(); virtual int encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra) = 0; virtual void encoder_open(const char* path) = 0; @@ -26,21 +27,10 @@ public: void publisher_init(); static void publisher_publish(VideoEncoder *e, int segment_num, uint32_t idx, VisionIpcBufExtra &extra, unsigned int flags, kj::ArrayPtr header, kj::ArrayPtr dat); - void writer_open(const char* path) { - if (this->write) write_handler_thread = std::thread(VideoEncoder::write_handler, this, path); - } - - void writer_close() { - if (this->write) { - to_write.push(NULL); - write_handler_thread.join(); - } - assert(to_write.empty()); - } protected: - bool write; const char* filename; + const char* publish_name; int in_width, in_height; int out_width, out_height, fps; int bitrate; @@ -54,9 +44,4 @@ private: // publishing std::unique_ptr pm; const char *service_name; - - // writing support - static void write_handler(VideoEncoder *e, const char *path); - std::thread write_handler_thread; - SafeQueue* > to_write; }; diff --git a/system/loggerd/encoder/ffmpeg_encoder.cc b/system/loggerd/encoder/ffmpeg_encoder.cc index 275da34f18..af05b5df97 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.cc +++ b/system/loggerd/encoder/ffmpeg_encoder.cc @@ -60,7 +60,6 @@ void FfmpegEncoder::encoder_open(const char* path) { int err = avcodec_open2(this->codec_ctx, codec, NULL); assert(err >= 0); - writer_open(path); is_open = true; segment_num++; counter = 0; @@ -69,7 +68,6 @@ void FfmpegEncoder::encoder_open(const char* path) { void FfmpegEncoder::encoder_close() { if (!is_open) return; - writer_close(); avcodec_free_context(&codec_ctx); is_open = false; } diff --git a/system/loggerd/encoder/ffmpeg_encoder.h b/system/loggerd/encoder/ffmpeg_encoder.h index 9095a6e815..6b4b6e104c 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.h +++ b/system/loggerd/encoder/ffmpeg_encoder.h @@ -17,8 +17,10 @@ extern "C" { class FfmpegEncoder : public VideoEncoder { public: FfmpegEncoder(const char* filename, CameraType type, int in_width, int in_height, int fps, - int bitrate, cereal::EncodeIndex::Type codec, int out_width, int out_height, bool write) : - VideoEncoder(filename, type, in_width, in_height, fps, bitrate, cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS, out_width, out_height, write) { encoder_init(); } + + int bitrate, cereal::EncodeIndex::Type codec, int out_width, int out_height, + const char* publish_name) : + VideoEncoder(filename, type, in_width, in_height, fps, bitrate, cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS, out_width, out_height, publish_name) { encoder_init(); } ~FfmpegEncoder(); void encoder_init(); int encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra); diff --git a/system/loggerd/encoder/v4l_encoder.cc b/system/loggerd/encoder/v4l_encoder.cc index 16e7246ff4..976839eef3 100644 --- a/system/loggerd/encoder/v4l_encoder.cc +++ b/system/loggerd/encoder/v4l_encoder.cc @@ -255,7 +255,6 @@ void V4LEncoder::encoder_init() { void V4LEncoder::encoder_open(const char* path) { dequeue_handler_thread = std::thread(V4LEncoder::dequeue_handler, this); - writer_open(path); this->is_open = true; this->counter = 0; } @@ -288,7 +287,6 @@ void V4LEncoder::encoder_close() { // join waits for V4L2_QCOM_BUF_FLAG_EOS dequeue_handler_thread.join(); assert(extras.empty()); - writer_close(); } this->is_open = false; } diff --git a/system/loggerd/encoder/v4l_encoder.h b/system/loggerd/encoder/v4l_encoder.h index d4b0a1211d..b0af4039fe 100644 --- a/system/loggerd/encoder/v4l_encoder.h +++ b/system/loggerd/encoder/v4l_encoder.h @@ -9,8 +9,8 @@ class V4LEncoder : public VideoEncoder { public: V4LEncoder(const char* filename, CameraType type, int in_width, int in_height, int fps, - int bitrate, cereal::EncodeIndex::Type codec, int out_width, int out_height, bool write) : - VideoEncoder(filename, type, in_width, in_height, fps, bitrate, codec, out_width, out_height, write) { encoder_init(); } + int bitrate, cereal::EncodeIndex::Type codec, int out_width, int out_height, const char* publish_name) : + VideoEncoder(filename, type, in_width, in_height, fps, bitrate, codec, out_width, out_height, publish_name) { encoder_init(); } ~V4LEncoder(); void encoder_init(); int encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra); diff --git a/system/loggerd/encoderd.cc b/system/loggerd/encoderd.cc index 12b58b6591..b4f0b913e5 100644 --- a/system/loggerd/encoderd.cc +++ b/system/loggerd/encoderd.cc @@ -35,7 +35,7 @@ bool sync_encoders(EncoderdState *s, CameraType cam_type, uint32_t frame_id) { void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { - util::set_thread_name(cam_info.filename); + util::set_thread_name(cam_info.thread_name); std::vector encoders; VisionIpcClient vipc_client = VisionIpcClient("camerad", cam_info.stream_type, false); @@ -50,20 +50,15 @@ void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { // init encoders if (encoders.empty()) { VisionBuf buf_info = vipc_client.buffers[0]; - LOGW("encoder %s init %dx%d", cam_info.filename, buf_info.width, buf_info.height); + LOGW("encoder %s init %dx%d", cam_info.thread_name, buf_info.width, buf_info.height); if (buf_info.width > 0 && buf_info.height > 0) { - // main encoder - encoders.push_back(new Encoder(cam_info.filename, cam_info.type, buf_info.width, buf_info.height, - cam_info.fps, cam_info.bitrate, - cam_info.is_h265 ? cereal::EncodeIndex::Type::FULL_H_E_V_C : cereal::EncodeIndex::Type::QCAMERA_H264, - buf_info.width, buf_info.height, false)); - // qcamera encoder - if (cam_info.has_qcamera) { - encoders.push_back(new Encoder(qcam_info.filename, cam_info.type, buf_info.width, buf_info.height, - qcam_info.fps, qcam_info.bitrate, - qcam_info.is_h265 ? cereal::EncodeIndex::Type::FULL_H_E_V_C : cereal::EncodeIndex::Type::QCAMERA_H264, - qcam_info.frame_width, qcam_info.frame_height, false)); + for (const auto &encoder_info: cam_info.encoder_infos){ + encoders.push_back(new Encoder(encoder_info.filename, cam_info.type, buf_info.width, buf_info.height, + encoder_info.fps, encoder_info.bitrate, + encoder_info.encode_type, + encoder_info.frame_width, encoder_info.frame_height, + encoder_info.publish_name)); } } else { LOGE("not initting empty encoder"); @@ -85,7 +80,7 @@ void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { // detect loop around and drop the frames if (buf->get_frame_id() != extra.frame_id) { if (!lagging) { - LOGE("encoder %s lag buffer id: %d extra id: %d", cam_info.filename, buf->get_frame_id(), extra.frame_id); + LOGE("encoder %s lag buffer id: %d extra id: %d", cam_info.thread_name, buf->get_frame_id(), extra.frame_id); lagging = true; } continue; diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index a7f7db4801..9ad3687613 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -58,18 +58,13 @@ struct RemoteEncoder { bool seen_first_packet = false; }; -int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct RemoteEncoder &re) { - const LogCameraInfo &cam_info = (name == "driverEncodeData") ? cameras_logged[1] : - ((name == "wideRoadEncodeData") ? cameras_logged[2] : - ((name == "qRoadEncodeData") ? qcam_info : cameras_logged[0])); +int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct RemoteEncoder &re, EncoderInfo encoder_info) { int bytes_count = 0; // extract the message capnp::FlatArrayMessageReader cmsg(kj::ArrayPtr((capnp::word *)msg->getData(), msg->getSize() / sizeof(capnp::word))); auto event = cmsg.getRoot(); - auto edata = (name == "driverEncodeData") ? event.getDriverEncodeData() : - ((name == "wideRoadEncodeData") ? event.getWideRoadEncodeData() : - ((name == "qRoadEncodeData") ? event.getQRoadEncodeData() : event.getRoadEncodeData())); + auto edata = (event.*(encoder_info.get_encode_data_func))(); auto idx = edata.getIdx(); auto flags = idx.getFlags(); @@ -95,7 +90,7 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct // we are in this segment now, process any queued messages before this one if (!re.q.empty()) { for (auto &qmsg: re.q) { - bytes_count += handle_encoder_msg(s, qmsg, name, re); + bytes_count += handle_encoder_msg(s, qmsg, name, re, encoder_info); } re.q.clear(); } @@ -111,10 +106,10 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct re.dropped_frames = 0; } // if we aren't actually recording, don't create the writer - if (cam_info.record) { + if (encoder_info.record) { re.writer.reset(new VideoWriter(s->segment_path, - cam_info.filename, idx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C, - cam_info.frame_width, cam_info.frame_height, cam_info.fps, idx.getType())); + encoder_info.filename, idx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C, + encoder_info.frame_width, encoder_info.frame_height, encoder_info.fps, idx.getType())); // write the header auto header = edata.getHeader(); re.writer->write((uint8_t *)header.begin(), header.size(), idx.getTimestampEof()/1000, true, false); @@ -142,10 +137,7 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct MessageBuilder bmsg; auto evt = bmsg.initEvent(event.getValid()); evt.setLogMonoTime(event.getLogMonoTime()); - if (name == "driverEncodeData") { evt.setDriverEncodeIdx(idx); } - if (name == "wideRoadEncodeData") { evt.setWideRoadEncodeIdx(idx); } - if (name == "qRoadEncodeData") { evt.setQRoadEncodeIdx(idx); } - if (name == "roadEncodeData") { evt.setRoadEncodeIdx(idx); } + (evt.*(encoder_info.set_encode_idx_func))(idx); auto new_msg = bmsg.toBytes(); logger_log(&s->logger, (uint8_t *)new_msg.begin(), new_msg.size(), true); // always in qlog? bytes_count += new_msg.size(); @@ -211,11 +203,12 @@ void loggerd_thread() { logger_rotate(&s); Params().put("CurrentRoute", s.logger.route_name); - // init encoders - s.last_camera_seen_tms = millis_since_boot(); + std::map encoder_infos_dict; for (const auto &cam : cameras_logged) { - s.max_waiting++; - if (cam.has_qcamera) { s.max_waiting++; } + for (const auto &encoder_info: cam.encoder_infos) { + encoder_infos_dict[encoder_info.publish_name] = encoder_info; + s.max_waiting++; + } } uint64_t msg_count = 0, bytes_count = 0; @@ -234,7 +227,7 @@ void loggerd_thread() { if (qs.encoder) { s.last_camera_seen_tms = millis_since_boot(); - bytes_count += handle_encoder_msg(&s, msg, qs.name, remote_encoders[sock]); + bytes_count += handle_encoder_msg(&s, msg, qs.name, remote_encoders[sock], encoder_infos_dict[qs.name]); } else { logger_log(&s.logger, (uint8_t *)msg->getData(), msg->getSize(), in_qlog); bytes_count += msg->getSize(); diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index 1b8f9e0d2a..579b079dbb 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -35,69 +35,87 @@ constexpr int MAIN_FPS = 20; const int MAIN_BITRATE = 10000000; -const int DCAM_BITRATE = MAIN_BITRATE; #define NO_CAMERA_PATIENCE 500 // fall back to time-based rotation if all cameras are dead const bool LOGGERD_TEST = getenv("LOGGERD_TEST"); const int SEGMENT_LENGTH = LOGGERD_TEST ? atoi(getenv("LOGGERD_SEGMENT_LENGTH")) : 60; -struct LogCameraInfo { - CameraType type; +class EncoderInfo { +public: + const char *publish_name; const char *filename; - VisionStreamType stream_type; - int frame_width, frame_height; - int fps; - int bitrate; - bool is_h265; - bool has_qcamera; - bool record; + bool record = true; + int frame_width = 1928; + int frame_height = 1208; + int fps = MAIN_FPS; + int bitrate = MAIN_BITRATE; + cereal::EncodeIndex::Type encode_type = cereal::EncodeIndex::Type::FULL_H_E_V_C; + ::cereal::EncodeData::Reader (cereal::Event::Reader::*get_encode_data_func)() const; + void (cereal::Event::Builder::*set_encode_idx_func)(::cereal::EncodeIndex::Reader); }; -const LogCameraInfo cameras_logged[] = { - { - .type = RoadCam, - .stream_type = VISION_STREAM_ROAD, - .filename = "fcamera.hevc", - .fps = MAIN_FPS, - .bitrate = MAIN_BITRATE, - .is_h265 = true, - .has_qcamera = true, - .record = true, - .frame_width = 1928, - .frame_height = 1208, - }, - { - .type = DriverCam, - .stream_type = VISION_STREAM_DRIVER, - .filename = "dcamera.hevc", - .fps = MAIN_FPS, - .bitrate = DCAM_BITRATE, - .is_h265 = true, - .has_qcamera = false, - .record = Params().getBool("RecordFront"), - .frame_width = 1928, - .frame_height = 1208, - }, - { - .type = WideRoadCam, - .stream_type = VISION_STREAM_WIDE_ROAD, - .filename = "ecamera.hevc", - .fps = MAIN_FPS, - .bitrate = MAIN_BITRATE, - .is_h265 = true, - .has_qcamera = false, - .record = true, - .frame_width = 1928, - .frame_height = 1208, - }, +class LogCameraInfo { +public: + const char *thread_name; + int fps = MAIN_FPS; + CameraType type; + VisionStreamType stream_type; + std::vector encoder_infos; }; -const LogCameraInfo qcam_info = { + +const EncoderInfo main_road_encoder_info = { + .publish_name = "roadEncodeData", + .filename = "fcamera.hevc", + .get_encode_data_func = &cereal::Event::Reader::getRoadEncodeData, + .set_encode_idx_func = &cereal::Event::Builder::setRoadEncodeIdx, +}; +const EncoderInfo main_wide_road_encoder_info = { + .publish_name = "wideRoadEncodeData", + .filename = "ecamera.hevc", + .get_encode_data_func = &cereal::Event::Reader::getWideRoadEncodeData, + .set_encode_idx_func = &cereal::Event::Builder::setWideRoadEncodeIdx, +}; +const EncoderInfo main_driver_encoder_info = { + .publish_name = "driverEncodeData", + .filename = "dcamera.hevc", + .record = Params().getBool("RecordFront"), + .get_encode_data_func = &cereal::Event::Reader::getDriverEncodeData, + .set_encode_idx_func = &cereal::Event::Builder::setDriverEncodeIdx, +}; + +const EncoderInfo qcam_encoder_info = { + .publish_name = "qRoadEncodeData", .filename = "qcamera.ts", - .fps = MAIN_FPS, .bitrate = 256000, - .is_h265 = false, - .record = true, + .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .frame_width = 526, .frame_height = 330, + .get_encode_data_func = &cereal::Event::Reader::getQRoadEncodeData, + .set_encode_idx_func = &cereal::Event::Builder::setQRoadEncodeIdx, }; + + +const LogCameraInfo road_camera_info{ + .thread_name = "road_cam_encoder", + .type = RoadCam, + .stream_type = VISION_STREAM_ROAD, + .encoder_infos = {main_road_encoder_info, qcam_encoder_info} + }; + +const LogCameraInfo wide_road_camera_info{ + .thread_name = "wide_road_cam_encoder", + .type = WideRoadCam, + .stream_type = VISION_STREAM_WIDE_ROAD, + .encoder_infos = {main_wide_road_encoder_info} + }; + +const LogCameraInfo driver_camera_info{ + .thread_name = "driver_cam_encoder", + .type = DriverCam, + .stream_type = VISION_STREAM_DRIVER, + .encoder_infos = {main_driver_encoder_info} + }; + +const LogCameraInfo cameras_logged[] = {road_camera_info, wide_road_camera_info, driver_camera_info}; + diff --git a/system/logmessaged.py b/system/logmessaged.py index 280a23cf1d..04101d042b 100755 --- a/system/logmessaged.py +++ b/system/logmessaged.py @@ -12,7 +12,7 @@ def main() -> NoReturn: log_handler.setFormatter(SwagLogFileFormatter(None)) log_level = 20 # logging.INFO - ctx = zmq.Context().instance() + ctx = zmq.Context.instance() sock = ctx.socket(zmq.PULL) sock.bind("ipc:///tmp/logmessage") @@ -20,23 +20,37 @@ def main() -> NoReturn: log_message_sock = messaging.pub_sock('logMessage') error_log_message_sock = messaging.pub_sock('errorLogMessage') - while True: - dat = b''.join(sock.recv_multipart()) - level = dat[0] - record = dat[1:].decode("utf-8") - if level >= log_level: - log_handler.emit(record) + try: + while True: + dat = b''.join(sock.recv_multipart()) + level = dat[0] + record = dat[1:].decode("utf-8") + if level >= log_level: + log_handler.emit(record) - # then we publish them - msg = messaging.new_message() - msg.logMessage = record - log_message_sock.send(msg.to_bytes()) + if len(record) > 2*1024*1024: + print("WARNING: log too big to publish", len(record)) + print(print(record[:100])) + continue - if level >= 40: # logging.ERROR + # then we publish them msg = messaging.new_message() - msg.errorLogMessage = record - error_log_message_sock.send(msg.to_bytes()) + msg.logMessage = record + log_message_sock.send(msg.to_bytes()) + if level >= 40: # logging.ERROR + msg = messaging.new_message() + msg.errorLogMessage = record + error_log_message_sock.send(msg.to_bytes()) + finally: + sock.close() + ctx.term() + + # can hit this if interrupted during a rollover + try: + log_handler.close() + except ValueError: + pass if __name__ == "__main__": main() diff --git a/system/sensord/rawgps/rawgpsd.py b/system/sensord/rawgps/rawgpsd.py index 8f243af9a5..46f5cf83c9 100755 --- a/system/sensord/rawgps/rawgpsd.py +++ b/system/sensord/rawgps/rawgpsd.py @@ -5,7 +5,9 @@ import signal import itertools import math import time +import pycurl import subprocess +from datetime import datetime from typing import NoReturn from struct import unpack_from, calcsize, pack @@ -107,6 +109,54 @@ def gps_enabled() -> bool: except subprocess.CalledProcessError as exc: raise Exception("failed to execute QGPS mmcli command") from exc +def download_and_inject_assistance(): + assist_data_file = '/tmp/xtra3grc.bin' + assistance_url = 'http://xtrapath3.izatcloud.net/xtra3grc.bin' + + try: + # download assistance + try: + c = pycurl.Curl() + c.setopt(pycurl.URL, assistance_url) + c.setopt(pycurl.NOBODY, 1) + c.setopt(pycurl.CONNECTTIMEOUT, 2) + c.perform() + bytes_n = c.getinfo(pycurl.CONTENT_LENGTH_DOWNLOAD) + c.close() + if bytes_n > 1e5: + cloudlog.error("Qcom assistance data larger than expected") + return + + with open(assist_data_file, 'wb') as fp: + c = pycurl.Curl() + c.setopt(pycurl.URL, assistance_url) + c.setopt(pycurl.CONNECTTIMEOUT, 5) + + c.setopt(pycurl.WRITEDATA, fp) + c.perform() + c.close() + except pycurl.error: + cloudlog.exception("Failed to download assistance file") + return + + # inject into module + try: + cmd = f"mmcli -m any --timeout 30 --location-inject-assistance-data={assist_data_file}" + subprocess.check_output(cmd, stderr=subprocess.PIPE, shell=True) + cloudlog.info("successfully loaded assistance data") + except subprocess.CalledProcessError as e: + cloudlog.event( + "rawgps.assistance_loading_failed", + error=True, + cmd=e.cmd, + output=e.output, + returncode=e.returncode + ) + finally: + if os.path.exists(assist_data_file): + os.remove(assist_data_file) + + def setup_quectel(diag: ModemDiag): # enable OEMDRE in the NV # TODO: it has to reboot for this to take effect @@ -120,13 +170,20 @@ def setup_quectel(diag: ModemDiag): if gps_enabled(): at_cmd("AT+QGPSEND") + #at_cmd("AT+QGPSDEL=0") # disable DPO power savings for more accuracy at_cmd("AT+QGPSCFG=\"dpoenable\",0") # don't automatically turn on GNSS on powerup at_cmd("AT+QGPSCFG=\"autogps\",0") - at_cmd("AT+QGPSSUPLURL=\"supl.google.com:7275\"") + # Do internet assistance + at_cmd("AT+QGPSXTRA=1") + download_and_inject_assistance() + #at_cmd("AT+QGPSXTRADATA?") + time_str = datetime.utcnow().strftime("%Y/%m/%d,%H:%M:%S") + at_cmd(f"AT+QGPSXTRATIME=0,\"{time_str}\",1,1,1000") + at_cmd("AT+QGPSCFG=\"outport\",\"usbnmea\"") at_cmd("AT+QGPS=1") diff --git a/system/tests/__init__.py b/system/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/system/tests/test_logmessaged.py b/system/tests/test_logmessaged.py new file mode 100755 index 0000000000..08335517ae --- /dev/null +++ b/system/tests/test_logmessaged.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import glob +import os +import shutil +import time +import unittest + +import cereal.messaging as messaging +from selfdrive.manager.process_config import managed_processes +from system.swaglog import cloudlog, SWAGLOG_DIR + + +class TestLogmessaged(unittest.TestCase): + + def setUp(self): + if os.path.exists(SWAGLOG_DIR): + shutil.rmtree(SWAGLOG_DIR) + + managed_processes['logmessaged'].start() + self.sock = messaging.sub_sock("logMessage", timeout=1000, conflate=False) + self.error_sock = messaging.sub_sock("logMessage", timeout=1000, conflate=False) + + # ensure sockets are connected + time.sleep(0.2) + messaging.drain_sock(self.sock) + messaging.drain_sock(self.error_sock) + + def tearDown(self): + del self.sock + del self.error_sock + managed_processes['logmessaged'].stop(block=True) + + def _get_log_files(self): + return list(glob.glob(os.path.join(SWAGLOG_DIR, "swaglog.*"))) + + def test_simple_log(self): + msgs = [f"abc {i}" for i in range(10)] + for m in msgs: + cloudlog.error(m) + time.sleep(3) + m = messaging.drain_sock(self.sock) + assert len(m) == len(msgs) + assert len(self._get_log_files()) >= 1 + + def test_big_log(self): + n = 10 + msg = "a"*3*1024*1024 + for _ in range(n): + cloudlog.info(msg) + time.sleep(3) + + msgs = messaging.drain_sock(self.sock) + assert len(msgs) == 0 + + logsize = sum([os.path.getsize(f) for f in self._get_log_files()]) + assert (n*len(msg)) < logsize < (n*(len(msg)+1024)) + + +if __name__ == "__main__": + unittest.main() diff --git a/system/ubloxd/ublox_msg.cc b/system/ubloxd/ublox_msg.cc index a01187d560..92a2ba678c 100644 --- a/system/ubloxd/ublox_msg.cc +++ b/system/ubloxd/ublox_msg.cc @@ -112,8 +112,7 @@ std::pair> UbloxMsgParser::gen_msg() { case 0x0a0b: return {"ubloxGnss", gen_mon_hw2(static_cast(body))}; case 0x0135: - // TODO return {"ubloxGnss", gen_nav_sat(static_cast(body))}; - return {"ubloxGnss", kj::Array()}; + return {"ubloxGnss", gen_nav_sat(static_cast(body))}; default: LOGE("Unknown message type %x", ubx_message.msg_type()); return {"ubloxGnss", kj::Array()}; diff --git a/tools/cabana/binaryview.cc b/tools/cabana/binaryview.cc index 3a03ee0b70..cc5259136a 100644 --- a/tools/cabana/binaryview.cc +++ b/tools/cabana/binaryview.cc @@ -127,15 +127,6 @@ void BinaryView::highlight(const cabana::Signal *sig) { } } - if (sig && underMouse()) { - QString tooltip = tr(R"(%1
- Size:%2 LE:%3 SGD:%4 - )").arg(sig->name).arg(sig->size).arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N"); - QToolTip::showText(QCursor::pos(), tooltip, this, rect()); - } else { - QToolTip::showText(QCursor::pos(), "", this, rect()); - } - hovered_sig = sig; emit signalHovered(hovered_sig); } @@ -165,7 +156,7 @@ void BinaryView::mousePressEvent(QMouseEvent *event) { 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->selection_color = getColor(s); + delegate->selection_color = s->color; break; } } @@ -228,9 +219,12 @@ void BinaryView::refresh() { QSet BinaryView::getOverlappingSignals() const { QSet overlapping; - for (auto &item : model->items) { - if (item.sigs.size() > 1) - for (auto s : item.sigs) overlapping += s; + for (const auto &item : model->items) { + if (item.sigs.size() > 1) { + for (auto s : item.sigs) { + if (s->type == cabana::Signal::Type::Normal) overlapping += s; + } + } } return overlapping; } @@ -277,8 +271,12 @@ void BinaryViewModel::refresh() { } 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(sig); - items[idx].sigs.push_back(sig); + + auto &sigs = items[idx].sigs; + sigs.push_back(sig); + if (sigs.size() > 1) { + std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) { return l->size > r->size; }); + } } } } else { @@ -345,6 +343,16 @@ QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, i return {}; } +QVariant BinaryViewModel::data(const QModelIndex &index, int role) const { + if (role == Qt::ToolTipRole) { + auto item = (const BinaryViewModel::Item *)index.internalPointer(); + if (item && !item->sigs.empty()) { + return signalToolTip(item->sigs.back()); + } + } + return {}; +} + // BinaryItemDelegate BinaryItemDelegate::BinaryItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { @@ -353,15 +361,11 @@ BinaryItemDelegate::BinaryItemDelegate(QObject *parent) : QStyledItemDelegate(pa hex_font.setBold(true); } -bool BinaryItemDelegate::isSameColor(const QModelIndex &index, int dx, int dy) const { - QModelIndex index2 = index.sibling(index.row() + dy, index.column() + dx); - if (!index2.isValid()) { - return false; - } - auto color1 = ((const BinaryViewModel::Item *)index.internalPointer())->bg_color; - auto color2 = ((const BinaryViewModel::Item *)index2.internalPointer())->bg_color; - // Ignore alpha - return (color1.red() == color2.red()) && (color2.green() == color2.green()) && (color1.blue() == color2.blue()); +bool BinaryItemDelegate::hasSignal(const QModelIndex &index, int dx, int dy, const cabana::Signal *sig) const { + if (!index.isValid()) return false; + auto model = (const BinaryViewModel*)(index.model()); + int idx = (index.row() + dy) * model->columnCount() + index.column() + dx; + return (idx >=0 && idx < model->items.size()) ? model->items[idx].sigs.contains(sig) : false; } void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { @@ -370,28 +374,32 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->save(); if (index.column() == 8) { - painter->setFont(hex_font); - painter->fillRect(option.rect, item->bg_color); + if (item->valid) { + painter->setFont(hex_font); + painter->fillRect(option.rect, item->bg_color); + } } else if (option.state & QStyle::State_Selected) { painter->fillRect(option.rect, selection_color); painter->setPen(option.palette.color(QPalette::BrightText)); - } else if (!bin_view->selectionModel()->hasSelection() || !item->sigs.contains(bin_view->resize_sig)) { // not resizing - QColor bg = item->bg_color; - if (bin_view->hovered_sig && item->sigs.contains(bin_view->hovered_sig)) { - bg.setAlpha(255); - painter->fillRect(option.rect, bg.darker(125)); // 4/5x brightness - painter->setPen(option.palette.color(QPalette::BrightText)); - } else { - if (item->sigs.size() > 0) { - drawBorder(painter, option, index); - bg.setAlpha(std::max(50, bg.alpha())); + } else if (!bin_view->selectionModel()->hasSelection() || !item->sigs.contains(bin_view->resize_sig)) { // not resizing + if (item->sigs.size() > 0) { + for (auto &s : item->sigs) { + if (s == bin_view->hovered_sig) { + painter->fillRect(option.rect, s->color.darker(125)); // 4/5x brightness + } else { + drawSignalCell(painter, option, index, s); + } } - painter->fillRect(option.rect, bg); - painter->setPen(option.palette.color(QPalette::Text)); + } else if (item->valid) { + painter->fillRect(option.rect, item->bg_color); } + auto color_role = item->sigs.contains(bin_view->hovered_sig) ? QPalette::BrightText : QPalette::Text; + painter->setPen(option.palette.color(color_role)); } - if (!item->valid) { + if (item->sigs.size() > 1) { + painter->fillRect(option.rect, QBrush(Qt::darkGray, Qt::Dense7Pattern)); + } else if (!item->valid) { painter->fillRect(option.rect, QBrush(Qt::darkGray, Qt::BDiagPattern)); } painter->drawText(option.rect, Qt::AlignCenter, item->val); @@ -403,44 +411,50 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op } // Draw border on edge of signal -void BinaryItemDelegate::drawBorder(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - auto item = (const BinaryViewModel::Item *)index.internalPointer(); - QColor border_color = item->bg_color; - border_color.setAlphaF(1.0); - - bool draw_left = !isSameColor(index, -1, 0); - bool draw_top = !isSameColor(index, 0, -1); - bool draw_right = !isSameColor(index, 1, 0); - bool draw_bottom = !isSameColor(index, 0, 1); +void BinaryItemDelegate::drawSignalCell(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index, const cabana::Signal *sig) const { + bool draw_left = !hasSignal(index, -1, 0, sig); + bool draw_top = !hasSignal(index, 0, -1, sig); + bool draw_right = !hasSignal(index, 1, 0, sig); + bool draw_bottom = !hasSignal(index, 0, 1, sig); const int spacing = 2; QRect rc = option.rect.adjusted(draw_left * 3, draw_top * spacing, draw_right * -3, draw_bottom * -spacing); QRegion subtract; if (!draw_top) { - if (!draw_left && !isSameColor(index, -1, -1)) { + if (!draw_left && !hasSignal(index, -1, -1, sig)) { subtract += QRect{rc.left(), rc.top(), 3, spacing}; - } else if (!draw_right && !isSameColor(index, 1, -1)) { + } else if (!draw_right && !hasSignal(index, 1, -1, sig)) { subtract += QRect{rc.right() - 2, rc.top(), 3, spacing}; } } if (!draw_bottom) { - if (!draw_left && !isSameColor(index, -1, 1)) { + if (!draw_left && !hasSignal(index, -1, 1, sig)) { subtract += QRect{rc.left(), rc.bottom() - (spacing - 1), 3, spacing}; - } else if (!draw_right && !isSameColor(index, 1, 1)) { + } else if (!draw_right && !hasSignal(index, 1, 1, sig)) { subtract += QRect{rc.right() - 2, rc.bottom() - (spacing - 1), 3, spacing}; } } + painter->setClipRegion(QRegion(rc).subtracted(subtract)); - painter->setPen(QPen(border_color, 1)); + auto item = (const BinaryViewModel::Item *)index.internalPointer(); + QColor color = sig->color; + color.setAlpha(item->bg_color.alpha()); + // Mixing the signal colour with the Base background color to fade it + painter->fillRect(rc, QApplication::palette().color(QPalette::Base)); + painter->fillRect(rc, color); + + // Draw edges + color = sig->color.darker(125); + painter->setPen(QPen(color, 1)); if (draw_left) painter->drawLine(rc.topLeft(), rc.bottomLeft()); if (draw_right) painter->drawLine(rc.topRight(), rc.bottomRight()); if (draw_bottom) painter->drawLine(rc.bottomLeft(), rc.bottomRight()); if (draw_top) painter->drawLine(rc.topLeft(), rc.topRight()); - painter->setClipRegion(QRegion(rc).subtracted(subtract)); if (!subtract.isEmpty()) { // fill gaps inside corners. - painter->setPen(QPen(border_color, 2, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin)); + painter->setPen(QPen(color, 2, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin)); for (auto &r : subtract) { painter->drawRect(r); } diff --git a/tools/cabana/binaryview.h b/tools/cabana/binaryview.h index f80b4520ed..d9966c9110 100644 --- a/tools/cabana/binaryview.h +++ b/tools/cabana/binaryview.h @@ -14,8 +14,8 @@ public: BinaryItemDelegate(QObject *parent); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void setSelectionColor(const QColor &color) { selection_color = color; } - bool isSameColor(const QModelIndex &index, int dx, int dy) const; - void drawBorder(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + bool hasSignal(const QModelIndex &index, int dx, int dy, const cabana::Signal *sig) const; + void drawSignalCell(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index, const cabana::Signal *sig) const; QFont small_font, hex_font; QColor selection_color; @@ -28,7 +28,7 @@ public: void updateState(); void updateItem(int row, int col, const QString &val, const QColor &color); QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { return {}; } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; } inline QModelIndex bitIndex(int bit, bool is_lb) const { return index(bit / 8, is_lb ? (7 - bit % 8) : bit % 8); } @@ -40,7 +40,7 @@ public: } struct Item { - QColor bg_color = QColor(102, 86, 169, 0); + QColor bg_color = QColor(102, 86, 169, 255); bool is_msb = false; bool is_lsb = false; QString val; diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 6e354da315..d2c451be89 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -1,7 +1,6 @@ #include #include -#include "common/prefix.h" #include "selfdrive/ui/qt/util.h" #include "tools/cabana/mainwin.h" #include "tools/cabana/streamselector.h" @@ -35,17 +34,20 @@ int main(int argc, char *argv[]) { QString dbc_file = cmd_parser.isSet("dbc") ? cmd_parser.value("dbc") : ""; - std::unique_ptr op_prefix; - std::unique_ptr stream; - + AbstractStream *stream = nullptr; if (cmd_parser.isSet("stream")) { - stream.reset(new DeviceStream(&app, cmd_parser.value("zmq"))); + stream = new DeviceStream(&app, cmd_parser.value("zmq")); } else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) { PandaStreamConfig config = {}; if (cmd_parser.isSet("panda-serial")) { config.serial = cmd_parser.value("panda-serial"); } - stream.reset(new PandaStream(&app, config)); + try { + stream = new PandaStream(&app, config); + } catch (std::exception &e) { + qWarning() << e.what(); + return 0; + } } else { uint32_t replay_flags = REPLAY_FLAG_NONE; if (cmd_parser.isSet("ecam")) { @@ -65,36 +67,29 @@ int main(int argc, char *argv[]) { } if (route.isEmpty()) { - AbstractStream *out_stream = nullptr; - StreamSelector dlg; - dlg.addStreamWidget(ReplayStream::widget(&out_stream)); - dlg.addStreamWidget(PandaStream::widget(&out_stream)); - dlg.addStreamWidget(DeviceStream::widget(&out_stream)); - if (!dlg.exec()) { - return 0; - } + StreamSelector dlg(&stream); + dlg.exec(); dbc_file = dlg.dbcFile(); - stream.reset(out_stream); } else { - // TODO: Remove when OpenpilotPrefix supports ZMQ -#ifndef __APPLE__ - op_prefix.reset(new OpenpilotPrefix()); -#endif auto replay_stream = new ReplayStream(&app); - stream.reset(replay_stream); if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) { return 0; } + stream = replay_stream; } } MainWindow w; - - // Load DBC + if (!stream) { + stream = new DummyStream(&app); + } + stream->start(); if (!dbc_file.isEmpty()) { w.loadFile(dbc_file); } - w.show(); - return app.exec(); + + int ret = app.exec(); + delete can; + return ret; } diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 7840ffd673..da38054cb0 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -38,15 +38,14 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par setChart(chart); createToolButtons(); - // TODO: enable zoomIn/seekTo in live streaming mode. setRubberBand(QChartView::HorizontalRubberBand); setMouseTracking(true); setTheme(settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); signal_value_font.setPointSize(9); - QObject::connect(axis_y, &QValueAxis::rangeChanged, [this]() { resetChartCache(); }); - QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, [this]() { resetChartCache(); }); - QObject::connect(window()->windowHandle(), &QWindow::screenChanged, [this]() { resetChartCache(); }); + QObject::connect(axis_y, &QValueAxis::rangeChanged, this, &ChartView::resetChartCache); + QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, this, &ChartView::resetChartCache); + QObject::connect(window()->windowHandle(), &QWindow::screenChanged, this, &ChartView::resetChartCache); QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved); QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated); @@ -107,14 +106,14 @@ void ChartView::setTheme(QChart::ChartTheme theme) { chart()->legend()->setLabelColor(palette().color(QPalette::Text)); } for (auto &s : sigs) { - s.series->setColor(getColor(s.sig)); + s.series->setColor(s.sig->color); } } void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) { if (hasSignal(msg_id, sig)) return; - QXYSeries *series = createSeries(series_type, getColor(sig)); + QXYSeries *series = createSeries(series_type, sig->color); sigs.push_back({.msg_id = msg_id, .sig = sig, .series = series}); updateSeries(sig); updateSeriesPoints(); @@ -154,8 +153,9 @@ void ChartView::signalUpdated(const cabana::Signal *sig) { } void ChartView::msgUpdated(MessageId id) { - if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id == id; })) + if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id.address == id.address; })) { updateTitle(); + } } void ChartView::manageSignals() { @@ -213,9 +213,19 @@ void ChartView::updateTitle() { for (QLegendMarker *marker : chart()->legend()->markers()) { QObject::connect(marker, &QLegendMarker::clicked, this, &ChartView::handleMarkerClicked, Qt::UniqueConnection); } + + // Use CSS to draw titles in the WindowText color + auto tmp = palette().color(QPalette::WindowText); + auto titleColorCss = tmp.name(QColor::HexArgb); + // Draw message details in similar color, but slightly fade it to the background + tmp.setAlpha(180); + auto msgColorCss = tmp.name(QColor::HexArgb); + for (auto &s : sigs) { auto decoration = s.series->isVisible() ? "none" : "line-through"; - s.series->setName(QString("%2 %3 %4").arg(decoration, s.sig->name, msgName(s.msg_id), s.msg_id.toString())); + s.series->setName(QString("%3 %5 %6") + .arg(decoration, titleColorCss, s.sig->name, + msgColorCss, msgName(s.msg_id), s.msg_id.toString())); } split_chart_act->setEnabled(sigs.size() > 1); resetChartCache(); @@ -267,7 +277,7 @@ void ChartView::updateSeries(const cabana::Signal *sig, bool clear) { s.step_vals.clear(); s.last_value_mono_time = 0; } - s.series->setColor(getColor(s.sig)); + s.series->setColor(s.sig->color); const auto &msgs = can->events(s.msg_id); s.vals.reserve(msgs.capacity()); @@ -279,14 +289,16 @@ void ChartView::updateSeries(const cabana::Signal *sig, bool clear) { const double route_start_time = can->routeStartTime(); for (auto end = msgs.cend(); first != end; ++first) { const CanEvent *e = *first; - double value = get_raw_value(e->dat, e->size, *s.sig); - double ts = e->mono_time / 1e9 - route_start_time; // seconds - s.vals.append({ts, value}); - if (!s.step_vals.empty()) { - s.step_vals.append({ts, s.step_vals.back().y()}); + double value = 0; + if (s.sig->getValue(e->dat, e->size, &value)) { + double ts = e->mono_time / 1e9 - route_start_time; // seconds + s.vals.append({ts, value}); + if (!s.step_vals.empty()) { + s.step_vals.append({ts, s.step_vals.back().y()}); + } + s.step_vals.append({ts, value}); + s.last_value_mono_time = e->mono_time; } - s.step_vals.append({ts, value}); - s.last_value_mono_time = e->mono_time; } if (!can->liveStreaming()) { s.segment_tree.build(s.vals); @@ -789,7 +801,7 @@ void ChartView::setSeriesType(SeriesType type) { s.series->deleteLater(); } for (auto &s : sigs) { - auto series = createSeries(series_type, getColor(s.sig)); + auto series = createSeries(series_type, s.sig->color); series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); s.series = series; } diff --git a/tools/cabana/chart/chart.h b/tools/cabana/chart/chart.h index 3bb191e5cd..272ea0d193 100644 --- a/tools/cabana/chart/chart.h +++ b/tools/cabana/chart/chart.h @@ -56,7 +56,7 @@ private slots: void manageSignals(); void handleMarkerClicked(); void msgUpdated(MessageId id); - void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id == id; }); } + void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); } void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } private: diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index d244b0c6cc..567fceebad 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -398,6 +398,7 @@ void ChartsWidget::removeAll() { tabbar->removeTab(1); } tab_charts.clear(); + zoomReset(); if (!charts.isEmpty()) { for (auto c : charts) { diff --git a/tools/cabana/chart/chartswidget.h b/tools/cabana/chart/chartswidget.h index ee0c07292d..0d9f79062a 100644 --- a/tools/cabana/chart/chartswidget.h +++ b/tools/cabana/chart/chartswidget.h @@ -124,4 +124,3 @@ public: ChartsWidget *charts; std::pair prev_range, range; }; - diff --git a/tools/cabana/chart/signalselector.cc b/tools/cabana/chart/signalselector.cc index 1aa8fc5016..50fe861a03 100644 --- a/tools/cabana/chart/signalselector.cc +++ b/tools/cabana/chart/signalselector.cc @@ -91,7 +91,7 @@ void SignalSelector::updateAvailableList(int index) { } void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { - QString text = QString("■ %1").arg(getColor(sig).name(), sig->name); + QString text = QString("■ %1").arg(sig->color.name(), sig->name); if (show_msg_name) text += QString(" %0 %1").arg(msgName(id), id.toString()); QLabel *label = new QLabel(text); diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 6d7b35f3a9..baedee8bb2 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -22,22 +22,31 @@ void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, doubl if (first != last) { if (update_values) { values.clear(); - values.reserve(std::distance(first, last)); + if (values.capacity() < std::distance(first, last)) { + values.reserve(std::distance(first, last) * 2); + } min_val = std::numeric_limits::max(); max_val = std::numeric_limits::lowest(); for (auto it = first; it != last; ++it) { const CanEvent *e = *it; - double value = get_raw_value(e->dat, e->size, *sig); - values.emplace_back((e->mono_time - (*first)->mono_time) / 1e9, value); - if (min_val > value) min_val = value; - if (max_val < value) max_val = value; + double value = 0; + if (sig->getValue(e->dat, e->size, &value)) { + values.emplace_back((e->mono_time - (*first)->mono_time) / 1e9, value); + if (min_val > value) min_val = value; + if (max_val < value) max_val = value; + } } if (min_val == max_val) { min_val -= 1; max_val += 1; } } - render(getColor(sig), size); + } else { + values.clear(); + } + + if (!values.empty()) { + render(sig->color, size); } else { pixmap = QPixmap(); min_val = -1; @@ -49,7 +58,7 @@ void Sparkline::render(const QColor &color, QSize size) { const double xscale = (size.width() - 1) / (double)time_range; const double yscale = (size.height() - 3) / (max_val - min_val); points.clear(); - points.reserve(values.size()); + points.reserve(values.capacity()); for (auto &v : values) { points.emplace_back(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale); } diff --git a/tools/cabana/chart/sparkline.h b/tools/cabana/chart/sparkline.h index 69d4f4bc55..2061966ebb 100644 --- a/tools/cabana/chart/sparkline.h +++ b/tools/cabana/chart/sparkline.h @@ -10,6 +10,9 @@ class Sparkline { public: void update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size); const QSize size() const { return pixmap.size() / pixmap.devicePixelRatio(); } + inline double freq() const { + return values.empty() ? 0 : values.size() / std::max(values.back().x() - values.front().x(), 1.0); + } QPixmap pixmap; double min_val = 0; diff --git a/tools/cabana/commands.cc b/tools/cabana/commands.cc index 337de1c702..1ddec0ea93 100644 --- a/tools/cabana/commands.cc +++ b/tools/cabana/commands.cc @@ -4,11 +4,12 @@ // EditMsgCommand -EditMsgCommand::EditMsgCommand(const MessageId &id, const QString &name, int size, QUndoCommand *parent) - : id(id), new_name(name), new_size(size), QUndoCommand(parent) { +EditMsgCommand::EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &comment, QUndoCommand *parent) + : id(id), new_name(name), new_size(size), new_comment(comment), QUndoCommand(parent) { if (auto msg = dbc()->msg(id)) { old_name = msg->name; old_size = msg->size; + old_comment = msg->comment; setText(QObject::tr("edit message %1:%2").arg(name).arg(id.address)); } else { setText(QObject::tr("new message %1:%2").arg(name).arg(id.address)); @@ -19,11 +20,11 @@ void EditMsgCommand::undo() { if (old_name.isEmpty()) dbc()->removeMsg(id); else - dbc()->updateMsg(id, old_name, old_size); + dbc()->updateMsg(id, old_name, old_size, old_comment); } void EditMsgCommand::redo() { - dbc()->updateMsg(id, new_name, new_size); + dbc()->updateMsg(id, new_name, new_size, new_comment); } // RemoveMsgCommand @@ -37,7 +38,7 @@ RemoveMsgCommand::RemoveMsgCommand(const MessageId &id, QUndoCommand *parent) : void RemoveMsgCommand::undo() { if (!message.name.isEmpty()) { - dbc()->updateMsg(id, message.name, message.size); + dbc()->updateMsg(id, message.name, message.size, message.comment); for (auto s : message.getSignals()) dbc()->addSignal(id, *s); } @@ -61,22 +62,43 @@ void AddSigCommand::redo() { dbc()->addSignal(id, signal); } // RemoveSigCommand RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent) - : id(id), signal(*sig), QUndoCommand(parent) { - setText(QObject::tr("remove signal %1 from %2:%3").arg(signal.name).arg(msgName(id)).arg(id.address)); + : id(id), QUndoCommand(parent) { + sigs.push_back(*sig); + if (sig->type == cabana::Signal::Type::Multiplexor) { + for (const auto &s : dbc()->msg(id)->sigs) { + if (s->type == cabana::Signal::Type::Multiplexed) { + sigs.push_back(*s); + } + } + } + setText(QObject::tr("remove signal %1 from %2:%3").arg(sig->name).arg(msgName(id)).arg(id.address)); } -void RemoveSigCommand::undo() { dbc()->addSignal(id, signal); } -void RemoveSigCommand::redo() { dbc()->removeSignal(id, signal.name); } +void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); } +void RemoveSigCommand::redo() { for (const auto &s : sigs) dbc()->removeSignal(id, s.name); } // EditSignalCommand EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent) - : id(id), old_signal(*sig), new_signal(new_sig), QUndoCommand(parent) { - setText(QObject::tr("edit signal %1 in %2:%3").arg(old_signal.name).arg(msgName(id)).arg(id.address)); + : id(id), QUndoCommand(parent) { + sigs.push_back({*sig, new_sig}); + if (sig->type == cabana::Signal::Type::Multiplexor && new_sig.type == cabana::Signal::Type::Normal) { + // convert all multiplexed signals to normal signals + auto msg = dbc()->msg(id); + assert(msg); + for (const auto &s : msg->sigs) { + if (s->type == cabana::Signal::Type::Multiplexed) { + auto new_s = *s; + new_s.type = cabana::Signal::Type::Normal; + sigs.push_back({*s, new_s}); + } + } + } + setText(QObject::tr("edit signal %1 in %2:%3").arg(sig->name).arg(msgName(id)).arg(id.address)); } -void EditSignalCommand::undo() { dbc()->updateSignal(id, new_signal.name, old_signal); } -void EditSignalCommand::redo() { dbc()->updateSignal(id, old_signal.name, new_signal); } +void EditSignalCommand::undo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.second.name, s.first); } +void EditSignalCommand::redo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.first.name, s.second); } namespace UndoStack { diff --git a/tools/cabana/commands.h b/tools/cabana/commands.h index 84d1a6e2a1..4e2e2eac4b 100644 --- a/tools/cabana/commands.h +++ b/tools/cabana/commands.h @@ -8,13 +8,13 @@ class EditMsgCommand : public QUndoCommand { public: - EditMsgCommand(const MessageId &id, const QString &name, int size, QUndoCommand *parent = nullptr); + EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &comment, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: const MessageId id; - QString old_name, new_name; + QString old_name, new_name, old_comment, new_comment; int old_size = 0, new_size = 0; }; @@ -48,7 +48,7 @@ public: private: const MessageId id; - cabana::Signal signal = {}; + QList sigs; }; class EditSignalCommand : public QUndoCommand { @@ -59,8 +59,7 @@ public: private: const MessageId id; - cabana::Signal old_signal = {}; - cabana::Signal new_signal = {}; + QList> sigs; // QList<{old_sig, new_sig}> }; namespace UndoStack { diff --git a/tools/cabana/dbc/dbc.cc b/tools/cabana/dbc/dbc.cc index ac91922e01..402d807b8b 100644 --- a/tools/cabana/dbc/dbc.cc +++ b/tools/cabana/dbc/dbc.cc @@ -1,26 +1,102 @@ #include "tools/cabana/dbc/dbc.h" + #include "tools/cabana/util.h" uint qHash(const MessageId &item) { return qHash(item.source) ^ qHash(item.address); } -std::vector cabana::Msg::getSignals() const { - std::vector ret; - ret.reserve(sigs.size()); - for (auto &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; +// cabana::Msg + +cabana::Msg::~Msg() { + for (auto s : sigs) { + delete s; + } } -void cabana::Msg::updateMask() { +cabana::Signal *cabana::Msg::addSignal(const cabana::Signal &sig) { + auto s = sigs.emplace_back(new cabana::Signal(sig)); + update(); + return s; +} + +cabana::Signal *cabana::Msg::updateSignal(const QString &sig_name, const cabana::Signal &new_sig) { + auto s = sig(sig_name); + if (s) { + *s = new_sig; + update(); + } + return s; +} + +void cabana::Msg::removeSignal(const QString &sig_name) { + auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s->name == sig_name; }); + if (it != sigs.end()) { + delete *it; + sigs.erase(it); + update(); + } +} + +cabana::Msg &cabana::Msg::operator=(const cabana::Msg &other) { + address = other.address; + name = other.name; + size = other.size; + comment = other.comment; + + for (auto s : sigs) delete s; + sigs.clear(); + for (auto s : other.sigs) { + sigs.push_back(new cabana::Signal(*s)); + } + + update(); + return *this; +} + +cabana::Signal *cabana::Msg::sig(const QString &sig_name) const { + auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s->name == sig_name; }); + return it != sigs.end() ? *it : nullptr; +} + +int cabana::Msg::indexOf(const cabana::Signal *sig) const { + for (int i = 0; i < sigs.size(); ++i) { + if (sigs[i] == sig) return i; + } + return -1; +} + +QString cabana::Msg::newSignalName() { + QString new_name; + for (int i = 1; /**/; ++i) { + new_name = QString("NEW_SIGNAL_%1").arg(i); + if (sig(new_name) == nullptr) break; + } + return new_name; +} + +void cabana::Msg::update() { mask = QVector(size, 0x00).toList(); - for (auto &sig : sigs) { - int i = sig.msb / 8; - int bits = sig.size; + multiplexor = nullptr; + + // sort signals + std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) { + return std::tie(r->type, l->multiplex_value, l->start_bit, l->name) < + std::tie(l->type, r->multiplex_value, r->start_bit, r->name); + }); + + for (auto sig : sigs) { + if (sig->type == cabana::Signal::Type::Multiplexor) { + multiplexor = sig; + } + sig->update(); + + // update mask + int i = sig->msb / 8; + int bits = sig->size; while (i >= 0 && i < size && bits > 0) { - int lsb = (int)(sig.lsb / 8) == i ? sig.lsb : i * 8; - int msb = (int)(sig.msb / 8) == i ? sig.msb : (i + 1) * 8 - 1; + int lsb = (int)(sig->lsb / 8) == i ? sig->lsb : i * 8; + int msb = (int)(sig->msb / 8) == i ? sig->msb : (i + 1) * 8 - 1; int sz = msb - lsb + 1; int shift = (lsb - (i * 8)); @@ -28,19 +104,36 @@ void cabana::Msg::updateMask() { mask[i] |= ((1ULL << sz) - 1) << shift; bits -= size; - i = sig.is_little_endian ? i - 1 : i + 1; + i = sig->is_little_endian ? i - 1 : i + 1; + } + } + + for (auto sig : sigs) { + sig->multiplexor = sig->type == cabana::Signal::Type::Multiplexed ? multiplexor : nullptr; + if (!sig->multiplexor) { + sig->multiplex_value = 0; } } } -void cabana::Signal::updatePrecision() { +// cabana::Signal + +void cabana::Signal::update() { + float h = 19 * (float)lsb / 64.0; + h = fmod(h, 1.0); + size_t hash = qHash(name); + float s = 0.25 + 0.25 * (float)(hash & 0xff) / 255.0; + float v = 0.75 + 0.25 * (float)((hash >> 8) & 0xff) / 255.0; + + color = QColor::fromHsvF(h, s, v); precision = std::max(num_decimals(factor), num_decimals(offset)); } QString cabana::Signal::formatValue(double value) const { // Show enum string - for (auto &[val, desc] : val_desc) { - if (std::abs(value - val.toInt()) < 1e-6) { + int64_t raw_value = round((value - offset) / factor); + for (const auto &[val, desc] : val_desc) { + if (std::abs(raw_value - val) < 1e-6) { return desc; } } @@ -52,6 +145,24 @@ QString cabana::Signal::formatValue(double value) const { return val_str; } +bool cabana::Signal::getValue(const uint8_t *data, size_t data_size, double *val) const { + if (multiplexor && get_raw_value(data, data_size, *multiplexor) != multiplex_value) { + return false; + } + *val = get_raw_value(data, data_size, *this); + return true; +} + +bool cabana::Signal::operator==(const cabana::Signal &other) const { + return name == other.name && size == other.size && + start_bit == other.start_bit && + msb == other.msb && lsb == other.lsb && + is_signed == other.is_signed && is_little_endian == other.is_little_endian && + factor == other.factor && offset == other.offset && + min == other.min && max == other.max && comment == other.comment && unit == other.unit && val_desc == other.val_desc && + multiplex_value == other.multiplex_value && type == other.type; +} + // helper functions static QVector BIG_ENDIAN_START_BITS = []() { @@ -84,15 +195,6 @@ double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal return val * sig.factor + sig.offset; } -bool cabana::operator==(const cabana::Signal &l, const cabana::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 && - l.min == r.min && l.max == r.max && l.comment == r.comment && l.unit == r.unit && l.val_desc == r.val_desc; -} - int bigEndianStartBitsIndex(int start_bit) { return BIG_ENDIAN_START_BITS[start_bit]; } int bigEndianBitIndex(int index) { return BIG_ENDIAN_START_BITS.indexOf(index); } diff --git a/tools/cabana/dbc/dbc.h b/tools/cabana/dbc/dbc.h index 7daa258ecd..d7439620a7 100644 --- a/tools/cabana/dbc/dbc.h +++ b/tools/cabana/dbc/dbc.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include #include "opendbc/can/common_dbc.h" @@ -41,42 +43,70 @@ struct std::hash { std::size_t operator()(const MessageId &k) const noexcept { return qHash(k); } }; -typedef QList> ValueDescription; +typedef QList> ValueDescription; namespace cabana { - struct Signal { - QString name; - int start_bit, msb, lsb, size; - bool is_signed; - double factor, offset; - bool is_little_endian; - QString min, max, unit; - QString comment; - ValueDescription val_desc; - int precision = 0; - void updatePrecision(); - QString formatValue(double value) const; + +class Signal { +public: + Signal() = default; + Signal(const Signal &other) = default; + void update(); + bool getValue(const uint8_t *data, size_t data_size, double *val) const; + QString formatValue(double value) const; + bool operator==(const cabana::Signal &other) const; + inline bool operator!=(const cabana::Signal &other) const { return !(*this == other); } + + enum class Type { + Normal = 0, + Multiplexed, + Multiplexor }; - struct Msg { - uint32_t address; - QString name; - uint32_t size; - QList sigs; + Type type = Type::Normal; + QString name; + int start_bit, msb, lsb, size; + double factor, offset; + bool is_signed; + bool is_little_endian; + double min, max; + QString unit; + QString comment; + ValueDescription val_desc; + int precision = 0; + QColor color; - QList mask; - void updateMask(); + // Multiplexed + int multiplex_value = 0; + Signal *multiplexor = nullptr; +}; - std::vector getSignals() const; - const cabana::Signal *sig(const QString &sig_name) const { - auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s.name == sig_name; }); - return it != sigs.end() ? &(*it) : nullptr; - } - }; +class Msg { +public: + Msg() = default; + Msg(const Msg &other) { *this = other; } + ~Msg(); + cabana::Signal *addSignal(const cabana::Signal &sig); + cabana::Signal *updateSignal(const QString &sig_name, const cabana::Signal &sig); + void removeSignal(const QString &sig_name); + Msg &operator=(const Msg &other); + int indexOf(const cabana::Signal *sig) const; + cabana::Signal *sig(const QString &sig_name) const; + QString newSignalName(); + void update(); + inline const std::vector &getSignals() const { return sigs; } - bool operator==(const cabana::Signal &l, const cabana::Signal &r); - inline bool operator!=(const cabana::Signal &l, const cabana::Signal &r) { return !(l == r); } -} + uint32_t address; + QString name; + uint32_t size; + QString comment; + std::vector sigs; + + QList mask; + cabana::Signal *multiplexor = nullptr; +}; + +} // namespace cabana // Helper functions double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig); @@ -85,3 +115,4 @@ int bigEndianBitIndex(int index); void updateSigSizeParamsFromRange(cabana::Signal &s, int start_bit, int size); std::pair getSignalRange(const cabana::Signal *s); inline std::vector allDBCNames() { return get_dbc_names(); } +inline QString doubleToString(double value) { return QString::number(value, 'g', std::numeric_limits::digits10); } diff --git a/tools/cabana/dbc/dbcfile.cc b/tools/cabana/dbc/dbcfile.cc index fd20c0af19..c7df7553ef 100644 --- a/tools/cabana/dbc/dbcfile.cc +++ b/tools/cabana/dbc/dbcfile.cc @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -17,7 +16,7 @@ DBCFile::DBCFile(const QString &dbc_file_name, QObject *parent) : QObject(parent if (dbc_file_name.endsWith(AUTO_SAVE_EXTENSION)) { filename.chop(AUTO_SAVE_EXTENSION.length()); } - open(file.readAll()); + parse(file.readAll()); } else { throw std::runtime_error("Failed to open file."); } @@ -25,41 +24,11 @@ DBCFile::DBCFile(const QString &dbc_file_name, QObject *parent) : QObject(parent DBCFile::DBCFile(const QString &name, const QString &content, QObject *parent) : QObject(parent), name_(name), filename("") { // Open from clipboard - open(content); -} - -void DBCFile::open(const QString &content) { - std::istringstream stream(content.toStdString()); - auto dbc = const_cast(dbc_parse_from_stream(name_.toStdString(), stream)); - msgs.clear(); - for (auto &msg : dbc->msgs) { - auto &m = msgs[msg.address]; - m.address = msg.address; - m.name = msg.name.c_str(); - m.size = msg.size; - for (auto &s : msg.sigs) { - m.sigs.push_back({}); - auto &sig = m.sigs.last(); - sig.name = s.name.c_str(); - sig.start_bit = s.start_bit; - sig.msb = s.msb; - sig.lsb = s.lsb; - sig.size = s.size; - sig.is_signed = s.is_signed; - sig.factor = s.factor; - sig.offset = s.offset; - sig.is_little_endian = s.is_little_endian; - sig.updatePrecision(); - } - m.updateMask(); - } - parseExtraInfo(content); - - delete dbc; + parse(content); } bool DBCFile::save() { - assert (!filename.isEmpty()); + assert(!filename.isEmpty()); if (writeContents(filename)) { cleanupAutoSaveFile(); return true; @@ -91,124 +60,63 @@ bool DBCFile::writeContents(const QString &fn) { return false; } -cabana::Signal *DBCFile::addSignal(const MessageId &id, const cabana::Signal &sig) { - if (auto m = const_cast(msg(id.address))) { - m->sigs.push_back(sig); - m->updateMask(); - return &m->sigs.last(); - } - return nullptr; -} - - cabana::Signal *DBCFile::updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig) { - if (auto m = const_cast(msg(id))) { - if (auto s = (cabana::Signal *)m->sig(sig_name)) { - *s = sig; - m->updateMask(); - return s; - } - } - return nullptr; -} - -cabana::Signal *DBCFile::getSignal(const MessageId &id, const QString &sig_name) { - auto m = msg(id); - return m ? (cabana::Signal *)m->sig(sig_name) : nullptr; -} - -void DBCFile::removeSignal(const MessageId &id, const QString &sig_name) { - if (auto m = const_cast(msg(id))) { - auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [&](auto &s) { return s.name == sig_name; }); - if (it != m->sigs.end()) { - m->sigs.erase(it); - m->updateMask(); - } - } -} - -void DBCFile::updateMsg(const MessageId &id, const QString &name, uint32_t size) { +void DBCFile::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &comment) { auto &m = msgs[id.address]; m.address = id.address; m.name = name; m.size = size; + m.comment = comment; } -void DBCFile::removeMsg(const MessageId &id) { - msgs.erase(id.address); -} - -QString DBCFile::newMsgName(const MessageId &id) { - return QString("NEW_MSG_") + QString::number(id.address, 16).toUpper(); -} - -QString DBCFile::newSignalName(const MessageId &id) { - auto m = msg(id); - assert(m != nullptr); - - QString name; - for (int i = 1; /**/; ++i) { - name = QString("NEW_SIGNAL_%1").arg(i); - if (m->sig(name) == nullptr) break; - } - return name; -} - -const QList& DBCFile::mask(const MessageId &id) const { - auto m = msg(id); - return m ? m->mask : empty_mask; -} - -const cabana::Msg *DBCFile::msg(uint32_t address) const { +cabana::Msg *DBCFile::msg(uint32_t address) { auto it = msgs.find(address); return it != msgs.end() ? &it->second : nullptr; } -const cabana::Msg* DBCFile::msg(const QString &name) { - auto it = std::find_if(msgs.cbegin(), msgs.cend(), [&name](auto &m) { return m.second.name == name; }); - return it != msgs.cend() ? &(it->second) : nullptr; +cabana::Msg *DBCFile::msg(const QString &name) { + auto it = std::find_if(msgs.begin(), msgs.end(), [&name](auto &m) { return m.second.name == name; }); + return it != msgs.end() ? &(it->second) : nullptr; } -QStringList DBCFile::signalNames() const { - // Used for autocompletion - QStringList ret; - for (auto const& [_, msg] : msgs) { - for (auto sig: msg.getSignals()) { - ret << sig->name; - } - } - ret.sort(); - ret.removeDuplicates(); - return ret; -} - -int DBCFile::signalCount(const MessageId &id) const { - if (msgs.count(id.address) == 0) return 0; - return msgs.at(id.address).sigs.size(); -} - -int DBCFile::signalCount() const { +int DBCFile::signalCount() { return std::accumulate(msgs.cbegin(), msgs.cend(), 0, [](int &n, const auto &m) { return n + m.second.sigs.size(); }); } -void DBCFile::parseExtraInfo(const QString &content) { +void DBCFile::parse(const QString &content) { static QRegularExpression bo_regexp(R"(^BO_ (\w+) (\w+) *: (\w+) (\w+))"); static QRegularExpression sg_regexp(R"(^SG_ (\w+) : (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); static QRegularExpression sgm_regexp(R"(^SG_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); - static QRegularExpression sg_comment_regexp(R"(^CM_ SG_ *(\w+) *(\w+) *\"(.*)\";)"); - static QRegularExpression val_regexp(R"(VAL_ (\w+) (\w+) (.*);)"); + static QRegularExpression msg_comment_regexp(R"(^CM_ BO_ *(\w+) *\"([^"]*)\"\s*;)"); + static QRegularExpression sg_comment_regexp(R"(^CM_ SG_ *(\w+) *(\w+) *\"([^"]*)\"\s*;)"); + static QRegularExpression val_regexp(R"(VAL_ (\w+) (\w+) (\s*[-+]?[0-9]+\s+\".+?\"[^;]*))"); + + int line_num = 0; + QString line; + auto dbc_assert = [&line_num, &line, this](bool condition, const QString &msg = "") { + if (!condition) throw std::runtime_error(QString("[%1:%2]%3: %4").arg(filename).arg(line_num).arg(msg).arg(line).toStdString()); + }; auto get_sig = [this](uint32_t address, const QString &name) -> cabana::Signal * { auto m = (cabana::Msg *)msg(address); return m ? (cabana::Signal *)m->sig(name) : nullptr; }; + msgs.clear(); QTextStream stream((QString *)&content); - uint32_t address = 0; + cabana::Msg *current_msg = nullptr; + int multiplexor_cnt = 0; while (!stream.atEnd()) { - QString line = stream.readLine().trimmed(); + ++line_num; + line = stream.readLine().trimmed(); if (line.startsWith("BO_ ")) { - if (auto match = bo_regexp.match(line); match.hasMatch()) { - address = match.captured(1).toUInt(); - } + multiplexor_cnt = 0; + auto match = bo_regexp.match(line); + dbc_assert(match.hasMatch()); + auto address = match.captured(1).toUInt(); + dbc_assert(msgs.count(address) == 0, QString("Duplicate message address: %1").arg(address)); + current_msg = &msgs[address]; + current_msg->address = address; + current_msg->name = match.captured(2); + current_msg->size = match.captured(3).toULong(); } else if (line.startsWith("SG_ ")) { int offset = 0; auto match = sg_regexp.match(line); @@ -216,51 +124,107 @@ void DBCFile::parseExtraInfo(const QString &content) { match = sgm_regexp.match(line); offset = 1; } - if (match.hasMatch()) { - if (auto s = get_sig(address, match.captured(1))) { - s->min = match.captured(8 + offset); - s->max = match.captured(9 + offset); - s->unit = match.captured(10 + offset); + dbc_assert(match.hasMatch()); + dbc_assert(current_msg, "No Message"); + auto name = match.captured(1); + dbc_assert(current_msg->sig(name) == nullptr, "Duplicate signal name"); + cabana::Signal s{}; + if (offset == 1) { + auto indicator = match.captured(2); + if (indicator == "M") { + // Only one signal within a single message can be the multiplexer switch. + dbc_assert(++multiplexor_cnt < 2, "Multiple multiplexor"); + s.type = cabana::Signal::Type::Multiplexor; + } else { + dbc_assert(multiplexor_cnt == 1, "No multiplexor"); + s.type = cabana::Signal::Type::Multiplexed; + s.multiplex_value = indicator.mid(1).toInt(); } } + s.name = name; + s.start_bit = match.captured(offset + 2).toInt(); + s.size = match.captured(offset + 3).toInt(); + s.is_little_endian = match.captured(offset + 4).toInt() == 1; + s.is_signed = match.captured(offset + 5) == "-"; + s.factor = match.captured(offset + 6).toDouble(); + s.offset = match.captured(offset + 7).toDouble(); + if (s.is_little_endian) { + s.lsb = s.start_bit; + s.msb = s.start_bit + s.size - 1; + } else { + s.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(s.start_bit) + s.size - 1); + s.msb = s.start_bit; + } + s.min = match.captured(8 + offset).toDouble(); + s.max = match.captured(9 + offset).toDouble(); + s.unit = match.captured(10 + offset); + current_msg->sigs.push_back(new cabana::Signal(s)); } else if (line.startsWith("VAL_ ")) { - if (auto match = val_regexp.match(line); match.hasMatch()) { - if (auto s = get_sig(match.captured(1).toUInt(), match.captured(2))) { - QStringList desc_list = match.captured(3).trimmed().split('"'); - for (int i = 0; i < desc_list.size(); i += 2) { - auto val = desc_list[i].trimmed(); - if (!val.isEmpty() && (i + 1) < desc_list.size()) { - auto desc = desc_list[i+1].trimmed(); - s->val_desc.push_back({val, desc}); - } + auto match = val_regexp.match(line); + dbc_assert(match.hasMatch()); + if (auto s = get_sig(match.captured(1).toUInt(), match.captured(2))) { + QStringList desc_list = match.captured(3).trimmed().split('"'); + for (int i = 0; i < desc_list.size(); i += 2) { + auto val = desc_list[i].trimmed(); + if (!val.isEmpty() && (i + 1) < desc_list.size()) { + auto desc = desc_list[i + 1].trimmed(); + s->val_desc.push_back({val.toDouble(), desc}); } } } + } else if (line.startsWith("CM_ BO_")) { + if (!line.endsWith("\";")) { + int pos = stream.pos() - line.length() - 1; + line = content.mid(pos, content.indexOf("\";", pos)); + } + auto match = msg_comment_regexp.match(line); + dbc_assert(match.hasMatch()); + if (auto m = (cabana::Msg *)msg(match.captured(1).toUInt())) { + m->comment = match.captured(2).trimmed(); + } } else if (line.startsWith("CM_ SG_ ")) { - if (auto match = sg_comment_regexp.match(line); match.hasMatch()) { - if (auto s = get_sig(match.captured(1).toUInt(), match.captured(2))) { - s->comment = match.captured(3).trimmed(); - } + if (!line.endsWith("\";")) { + int pos = stream.pos() - line.length() - 1; + line = content.mid(pos, content.indexOf("\";", pos)); + } + auto match = sg_comment_regexp.match(line); + dbc_assert(match.hasMatch()); + if (auto s = get_sig(match.captured(1).toUInt(), match.captured(2))) { + s->comment = match.captured(3).trimmed(); } } } + + for (auto &[_, m] : msgs) { + m.update(); + } } QString DBCFile::generateDBC() { - QString dbc_string, signal_comment, val_desc; - for (auto &[address, m] : msgs) { + QString dbc_string, signal_comment, message_comment, val_desc; + for (const auto &[address, m] : msgs) { dbc_string += QString("BO_ %1 %2: %3 XXX\n").arg(address).arg(m.name).arg(m.size); + if (!m.comment.isEmpty()) { + message_comment += QString("CM_ BO_ %1 \"%2\";\n").arg(address).arg(m.comment); + } for (auto sig : m.getSignals()) { - dbc_string += QString(" SG_ %1 : %2|%3@%4%5 (%6,%7) [%8|%9] \"%10\" XXX\n") + QString multiplexer_indicator; + if (sig->type == cabana::Signal::Type::Multiplexor) { + multiplexer_indicator = "M "; + } else if (sig->type == cabana::Signal::Type::Multiplexed) { + multiplexer_indicator = QString("m%1 ").arg(sig->multiplex_value); + } + dbc_string += QString(" SG_ %1 %2: %3|%4@%5%6 (%7,%8) [%9|%10] \"%11\" XXX\n") .arg(sig->name) + .arg(multiplexer_indicator) .arg(sig->start_bit) .arg(sig->size) .arg(sig->is_little_endian ? '1' : '0') .arg(sig->is_signed ? '-' : '+') - .arg(sig->factor, 0, 'g', std::numeric_limits::digits10) - .arg(sig->offset, 0, 'g', std::numeric_limits::digits10) - .arg(sig->min) - .arg(sig->max) + .arg(doubleToString(sig->factor)) + .arg(doubleToString(sig->offset)) + .arg(doubleToString(sig->min)) + .arg(doubleToString(sig->max)) .arg(sig->unit); if (!sig->comment.isEmpty()) { signal_comment += QString("CM_ SG_ %1 %2 \"%3\";\n").arg(address).arg(sig->name).arg(sig->comment); @@ -268,12 +232,12 @@ QString DBCFile::generateDBC() { if (!sig->val_desc.isEmpty()) { QStringList text; for (auto &[val, desc] : sig->val_desc) { - text << QString("%1 \"%2\"").arg(val, desc); + text << QString("%1 \"%2\"").arg(val).arg(desc); } val_desc += QString("VAL_ %1 %2 %3;\n").arg(address).arg(sig->name).arg(text.join(" ")); } } dbc_string += "\n"; } - return dbc_string + signal_comment + val_desc; + return dbc_string + message_comment + signal_comment + val_desc; } diff --git a/tools/cabana/dbc/dbcfile.h b/tools/cabana/dbc/dbcfile.h index 9f140f44d9..78a73d58e4 100644 --- a/tools/cabana/dbc/dbcfile.h +++ b/tools/cabana/dbc/dbcfile.h @@ -1,9 +1,7 @@ #pragma once #include -#include #include -#include #include "tools/cabana/dbc/dbc.h" @@ -17,8 +15,6 @@ public: DBCFile(const QString &name, const QString &content, QObject *parent=nullptr); ~DBCFile() {} - void open(const QString &content); - bool save(); bool saveAs(const QString &new_filename); bool autoSave(); @@ -26,36 +22,23 @@ public: void cleanupAutoSaveFile(); QString generateDBC(); - cabana::Signal *addSignal(const MessageId &id, const cabana::Signal &sig); - cabana::Signal *updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig); - cabana::Signal *getSignal(const MessageId &id, const QString &sig_name); - void removeSignal(const MessageId &id, const QString &sig_name); + void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &comment); + inline void removeMsg(const MessageId &id) { msgs.erase(id.address); } - void updateMsg(const MessageId &id, const QString &name, uint32_t size); - void removeMsg(const MessageId &id); + inline const std::map &getMessages() const { return msgs; } + cabana::Msg *msg(uint32_t address); + cabana::Msg *msg(const QString &name); + inline cabana::Msg *msg(const MessageId &id) { return msg(id.address); } - QString newMsgName(const MessageId &id); - QString newSignalName(const MessageId &id); - - const QList& mask(const MessageId &id) const; - - inline std::map getMessages() const { return msgs; } - const cabana::Msg *msg(uint32_t address) const; - const cabana::Msg* msg(const QString &name); - inline const cabana::Msg *msg(const MessageId &id) const { return msg(id.address); }; - - QStringList signalNames() const; - int signalCount(const MessageId &id) const; - int signalCount() const; - inline int msgCount() const { return msgs.size(); } - inline QString name() const { return name_.isEmpty() ? "untitled" : name_; } - inline bool isEmpty() const { return (signalCount() == 0) && name_.isEmpty(); } + int signalCount(); + inline int msgCount() { return msgs.size(); } + inline QString name() { return name_.isEmpty() ? "untitled" : name_; } + inline bool isEmpty() { return (signalCount() == 0) && name_.isEmpty(); } QString filename; private: - void parseExtraInfo(const QString &content); + void parse(const QString &content); std::map msgs; QString name_; - QList empty_mask; }; diff --git a/tools/cabana/dbc/dbcmanager.cc b/tools/cabana/dbc/dbcmanager.cc index 9667dd8f2a..388844098a 100644 --- a/tools/cabana/dbc/dbcmanager.cc +++ b/tools/cabana/dbc/dbcmanager.cc @@ -1,28 +1,16 @@ #include "tools/cabana/dbc/dbcmanager.h" -#include -#include -#include -#include -#include -#include - - -bool DBCManager::open(SourceSet s, const QString &dbc_file_name, QString *error) { - for (int i = 0; i < dbc_files.size(); i++) { - auto [ss, dbc_file] = dbc_files[i]; - - // Check if file is already open, and merge sources - if (dbc_file->filename == dbc_file_name) { - dbc_files[i] = {ss | s, dbc_file}; - - emit DBCFileChanged(); - return true; - } - } +#include +#include +bool DBCManager::open(const SourceSet &sources, const QString &dbc_file_name, QString *error) { try { - dbc_files.push_back({s, new DBCFile(dbc_file_name, this)}); + auto it = std::find_if(dbc_files.begin(), dbc_files.end(), + [&](auto &f) { return f.second && f.second->filename == dbc_file_name; }); + auto file = (it != dbc_files.end()) ? it->second : std::make_shared(dbc_file_name, this); + for (auto s : sources) { + dbc_files[s] = file; + } } catch (std::exception &e) { if (error) *error = e.what(); return false; @@ -32,9 +20,12 @@ bool DBCManager::open(SourceSet s, const QString &dbc_file_name, QString *error) return true; } -bool DBCManager::open(SourceSet s, const QString &name, const QString &content, QString *error) { +bool DBCManager::open(const SourceSet &sources, const QString &name, const QString &content, QString *error) { try { - dbc_files.push_back({s, new DBCFile(name, content, this)}); + auto file = std::make_shared(name, content, this); + for (auto s : sources) { + dbc_files[s] = file; + } } catch (std::exception &e) { if (error) *error = e.what(); return false; @@ -44,269 +35,154 @@ bool DBCManager::open(SourceSet s, const QString &name, const QString &content, return true; } -void DBCManager::close(SourceSet s) { - // Build new list of dbc files, removing the ones that match the sourceset - QList> new_dbc_files; - for (auto entry : dbc_files) { - if (entry.first == s) { - delete entry.second; - } else { - new_dbc_files.push_back(entry); - } +void DBCManager::close(const SourceSet &sources) { + for (auto s : sources) { + dbc_files[s] = nullptr; } - - dbc_files = new_dbc_files; emit DBCFileChanged(); } void DBCManager::close(DBCFile *dbc_file) { - assert(dbc_file != nullptr); - - // Build new list of dbc files, removing the one that matches dbc_file* - QList> new_dbc_files; - for (auto entry : dbc_files) { - if (entry.second == dbc_file) { - delete entry.second; - } else { - new_dbc_files.push_back(entry); - } + for (auto &[_, f] : dbc_files) { + if (f.get() == dbc_file) f = nullptr; } - - dbc_files = new_dbc_files; emit DBCFileChanged(); } void DBCManager::closeAll() { - if (dbc_files.isEmpty()) return; - - while (dbc_files.size()) { - DBCFile *dbc_file = dbc_files.back().second; - dbc_files.pop_back(); - delete dbc_file; - } + dbc_files.clear(); emit DBCFileChanged(); } -void DBCManager::removeSourcesFromFile(DBCFile *dbc_file, SourceSet s) { - assert(dbc_file != nullptr); - - // Build new list of dbc files, for the given dbc_file* remove s from the current sources - QList> new_dbc_files; - for (auto entry : dbc_files) { - if (entry.second == dbc_file) { - SourceSet ss = (entry.first == SOURCE_ALL) ? sources : entry.first; - ss -= s; - if (ss.empty()) { // Close file if no more sources remain - delete dbc_file; - } else { - new_dbc_files.push_back({ss, dbc_file}); - } - } else { - new_dbc_files.push_back(entry); - } - } - - dbc_files = new_dbc_files; - emit DBCFileChanged(); -} - - void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { - auto sources_dbc_file = findDBCFile(id); - assert(sources_dbc_file); // Create new DBC? - auto [dbc_sources, dbc_file] = *sources_dbc_file; - - cabana::Signal *s = dbc_file->addSignal(id, sig); - - if (s != nullptr) { - dbc_sources.insert(id.source); - for (uint8_t source : dbc_sources) { - emit signalAdded({.source = source, .address = id.address}, s); + if (auto m = msg(id)) { + if (auto s = m->addSignal(sig)) { + emit signalAdded(id, s); } } } void DBCManager::updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig) { - auto sources_dbc_file = findDBCFile(id); - assert(sources_dbc_file); // This should be impossible - auto [_, dbc_file] = *sources_dbc_file; - - cabana::Signal *s = dbc_file->updateSignal(id, sig_name, sig); - - if (s != nullptr) { - emit signalUpdated(s); + if (auto m = msg(id)) { + if (auto s = m->updateSignal(sig_name, sig)) { + emit signalUpdated(s); + } } } void DBCManager::removeSignal(const MessageId &id, const QString &sig_name) { - auto sources_dbc_file = findDBCFile(id); - assert(sources_dbc_file); // This should be impossible - auto [_, dbc_file] = *sources_dbc_file; - - cabana::Signal *s = dbc_file->getSignal(id, sig_name); - - if (s != nullptr) { - emit signalRemoved(s); - dbc_file->removeSignal(id, sig_name); + if (auto m = msg(id)) { + if (auto s = m->sig(sig_name)) { + emit signalRemoved(s); + m->removeSignal(sig_name); + } } } -void DBCManager::updateMsg(const MessageId &id, const QString &name, uint32_t size) { - auto sources_dbc_file = findDBCFile(id); - assert(sources_dbc_file); // This should be impossible - auto [dbc_sources, dbc_file] = *sources_dbc_file; - - dbc_file->updateMsg(id, name, size); - - for (uint8_t source : dbc_sources) { - emit msgUpdated({.source = source, .address = id.address}); - } +void DBCManager::updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &comment) { + auto dbc_file = findDBCFile(id); + assert(dbc_file); // This should be impossible + dbc_file->updateMsg(id, name, size, comment); + emit msgUpdated(id); } void DBCManager::removeMsg(const MessageId &id) { - auto sources_dbc_file = findDBCFile(id); - assert(sources_dbc_file); // This should be impossible - auto [dbc_sources, dbc_file] = *sources_dbc_file; - + auto dbc_file = findDBCFile(id); + assert(dbc_file); // This should be impossible dbc_file->removeMsg(id); - - for (uint8_t source : dbc_sources) { - emit msgRemoved({.source = source, .address = id.address}); - } + emit msgRemoved(id); } QString DBCManager::newMsgName(const MessageId &id) { - auto sources_dbc_file = findDBCFile(id); - assert(sources_dbc_file); // This should be impossible - auto [_, dbc_file] = *sources_dbc_file; - return dbc_file->newMsgName(id); + return QString("NEW_MSG_") + QString::number(id.address, 16).toUpper(); } QString DBCManager::newSignalName(const MessageId &id) { - auto sources_dbc_file = findDBCFile(id); - assert(sources_dbc_file); // This should be impossible - auto [_, dbc_file] = *sources_dbc_file; - return dbc_file->newSignalName(id); + auto m = msg(id); + return m ? m->newSignalName() : ""; } -const QList& DBCManager::mask(const MessageId &id) const { - auto sources_dbc_file = findDBCFile(id); - if (!sources_dbc_file) { - return empty_mask; - } - auto [_, dbc_file] = *sources_dbc_file; - return dbc_file->mask(id); +const QList &DBCManager::mask(const MessageId &id) { + static QList empty_mask; + auto m = msg(id); + return m ? m->mask : empty_mask; } -std::map DBCManager::getMessages(uint8_t source) { - std::map ret; - - auto sources_dbc_file = findDBCFile({.source = source, .address = 0}); - if (!sources_dbc_file) { - return ret; - } - - auto [_, dbc_file] = *sources_dbc_file; - - for (auto &[address, msg] : dbc_file->getMessages()) { - MessageId id = {.source = source, .address = address}; - ret[id] = msg; - } - return ret; +const std::map &DBCManager::getMessages(uint8_t source) { + static std::map empty_msgs; + auto dbc_file = findDBCFile(source); + return dbc_file ? dbc_file->getMessages() : empty_msgs; } -const cabana::Msg *DBCManager::msg(const MessageId &id) const { - auto sources_dbc_file = findDBCFile(id); - if (!sources_dbc_file) { - return nullptr; - } - auto [_, dbc_file] = *sources_dbc_file; - return dbc_file->msg(id); +cabana::Msg *DBCManager::msg(const MessageId &id) { + auto dbc_file = findDBCFile(id); + return dbc_file ? dbc_file->msg(id) : nullptr; } -const cabana::Msg* DBCManager::msg(uint8_t source, const QString &name) { - auto sources_dbc_file = findDBCFile({.source = source, .address = 0}); - if (!sources_dbc_file) { - return nullptr; - } - auto [_, dbc_file] = *sources_dbc_file; - return dbc_file->msg(name); +cabana::Msg *DBCManager::msg(uint8_t source, const QString &name) { + auto dbc_file = findDBCFile(source); + return dbc_file ? dbc_file->msg(name) : nullptr; } -QStringList DBCManager::signalNames() const { +QStringList DBCManager::signalNames() { + // Used for autocompletion QStringList ret; - - for (auto &[_, dbc_file] : dbc_files) { - ret << dbc_file->signalNames(); + for (auto &f : allDBCFiles()) { + for (auto &[_, m] : f->getMessages()) { + for (auto sig : m.getSignals()) { + ret << sig->name; + } + } } - ret.sort(); ret.removeDuplicates(); return ret; } -int DBCManager::signalCount(const MessageId &id) const { - auto sources_dbc_file = findDBCFile(id); - if (!sources_dbc_file) { - return 0; - } - - auto [_, dbc_file] = *sources_dbc_file; - return dbc_file->signalCount(id); +int DBCManager::signalCount(const MessageId &id) { + auto m = msg(id); + return m ? m->sigs.size() : 0; } -int DBCManager::signalCount() const { - int ret = 0; - - for (auto &[_, dbc_file] : dbc_files) { - ret += dbc_file->signalCount(); - } - - return ret; +int DBCManager::signalCount() { + auto files = allDBCFiles(); + return std::accumulate(files.cbegin(), files.cend(), 0, [](int &n, auto &f) { return n + f->signalCount(); }); } -int DBCManager::msgCount() const { - int ret = 0; - - for (auto &[_, dbc_file] : dbc_files) { - ret += dbc_file->msgCount(); - } - - return ret; +int DBCManager::msgCount() { + auto files = allDBCFiles(); + return std::accumulate(files.cbegin(), files.cend(), 0, [](int &n, auto &f) { return n + f->msgCount(); }); } -int DBCManager::dbcCount() const { - return dbc_files.size(); +int DBCManager::dbcCount() { + return allDBCFiles().size(); } -int DBCManager::nonEmptyDBCCount() const { - int cnt = 0; - for (auto &[_, dbc_file] : dbc_files) { - if (!dbc_file->isEmpty()) { - cnt++; - } - } - return cnt; +int DBCManager::nonEmptyDBCCount() { + auto files = allDBCFiles(); + return std::count_if(files.cbegin(), files.cend(), [](auto &f) { return !f->isEmpty(); }); } -void DBCManager::updateSources(const SourceSet &s) { - sources = s; -} - -std::optional> DBCManager::findDBCFile(const uint8_t source) const { +DBCFile *DBCManager::findDBCFile(const uint8_t source) { // Find DBC file that matches id.source, fall back to SOURCE_ALL if no specific DBC is found - - for (auto &[source_set, dbc_file] : dbc_files) { - if (source_set.contains(source)) return {{source_set, dbc_file}}; - } - for (auto &[source_set, dbc_file] : dbc_files) { - if (source_set == SOURCE_ALL) return {{sources, dbc_file}}; - } - return {}; + auto it = dbc_files.count(source) ? dbc_files.find(source) : dbc_files.find(-1); + return it != dbc_files.end() ? it->second.get() : nullptr; } -std::optional> DBCManager::findDBCFile(const MessageId &id) const { - return findDBCFile(id.source); +std::set DBCManager::allDBCFiles() { + std::set files; + for (const auto &[_, f] : dbc_files) { + if (f) files.insert(f.get()); + } + return files; +} + +const SourceSet DBCManager::sources(const DBCFile *dbc_file) const { + SourceSet sources; + for (auto &[s, f] : dbc_files) { + if (f.get() == dbc_file) sources.insert(s); + } + return sources; } DBCManager *dbc() { diff --git a/tools/cabana/dbc/dbcmanager.h b/tools/cabana/dbc/dbcmanager.h index 699c280b4c..659253b82a 100644 --- a/tools/cabana/dbc/dbcmanager.h +++ b/tools/cabana/dbc/dbcmanager.h @@ -1,69 +1,54 @@ #pragma once +#include #include -#include +#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/dbc/dbc.h" #include "tools/cabana/dbc/dbcfile.h" -typedef QSet SourceSet; -const SourceSet SOURCE_ALL = {}; +typedef std::set SourceSet; +const SourceSet SOURCE_ALL = {-1}; const int INVALID_SOURCE = 0xff; +inline bool operator<(const std::shared_ptr &l, const std::shared_ptr &r) { return l.get() < r.get(); } class DBCManager : public QObject { Q_OBJECT public: - DBCManager(QObject *parent) {} + DBCManager(QObject *parent) : QObject(parent) {} ~DBCManager() {} - bool open(SourceSet s, const QString &dbc_file_name, QString *error = nullptr); - bool open(SourceSet s, const QString &name, const QString &content, QString *error = nullptr); - void close(SourceSet s); + bool open(const SourceSet &sources, const QString &dbc_file_name, QString *error = nullptr); + bool open(const SourceSet &sources, const QString &name, const QString &content, QString *error = nullptr); + void close(const SourceSet &sources); void close(DBCFile *dbc_file); void closeAll(); - void removeSourcesFromFile(DBCFile *dbc_file, SourceSet s); void addSignal(const MessageId &id, const cabana::Signal &sig); void updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig); void removeSignal(const MessageId &id, const QString &sig_name); - void updateMsg(const MessageId &id, const QString &name, uint32_t size); + void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &comment); void removeMsg(const MessageId &id); QString newMsgName(const MessageId &id); QString newSignalName(const MessageId &id); + const QList& mask(const MessageId &id); - const QList& mask(const MessageId &id) const; + const std::map &getMessages(uint8_t source); + cabana::Msg *msg(const MessageId &id); + cabana::Msg* msg(uint8_t source, const QString &name); - std::map getMessages(uint8_t source); - const cabana::Msg *msg(const MessageId &id) const; - const cabana::Msg* msg(uint8_t source, const QString &name); + QStringList signalNames(); + int signalCount(const MessageId &id); + int signalCount(); + int msgCount(); + int dbcCount(); + int nonEmptyDBCCount(); - QStringList signalNames() const; - int signalCount(const MessageId &id) const; - int signalCount() const; - int msgCount() const; - int dbcCount() const; - int nonEmptyDBCCount() const; - - std::optional> findDBCFile(const uint8_t source) const; - std::optional> findDBCFile(const MessageId &id) const; - - QList> dbc_files; - -private: - SourceSet sources; - QList empty_mask; - -public slots: - void updateSources(const SourceSet &s); + const SourceSet sources(const DBCFile *dbc_file) const; + DBCFile *findDBCFile(const uint8_t source); + inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); } + std::set allDBCFiles(); signals: void signalAdded(MessageId id, const cabana::Signal *sig); @@ -72,6 +57,9 @@ signals: void msgUpdated(MessageId id); void msgRemoved(MessageId id); void DBCFileChanged(); + +private: + std::map> dbc_files; }; DBCManager *dbc(); @@ -81,16 +69,10 @@ inline QString msgName(const MessageId &id) { return msg ? msg->name : UNTITLED; } -inline QString toString(SourceSet ss) { - if (ss == SOURCE_ALL) { - return "all"; - } else { - QStringList ret; - QList source_list = ss.values(); - std::sort(source_list.begin(), source_list.end()); - for (auto s : source_list) { - ret << QString::number(s); - } - return ret.join(", "); +inline QString toString(const SourceSet &ss) { + QStringList ret; + for (auto s : ss) { + ret << (s == -1 ? QString("all") : QString::number(s)); } + return ret.join(", "); } diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 0feb4642f4..47c5e4a62e 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -5,6 +5,7 @@ #include #include "tools/cabana/commands.h" +#include "tools/cabana/mainwin.h" // DetailWidget @@ -166,7 +167,8 @@ void DetailWidget::editMsg() { int size = msg ? msg->size : can->lastMessage(msg_id).dat.size(); EditMessageDialog dlg(msg_id, msgName(msg_id), size, this); if (dlg.exec()) { - UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text(), dlg.size_spin->value())); + UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed(), + dlg.size_spin->value(), dlg.comment_edit->toPlainText().trimmed())); } } @@ -193,6 +195,11 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit size_spin->setValue(size); form_layout->addRow(tr("Size"), size_spin); + form_layout->addRow(tr("Comment"), comment_edit = new QTextEdit(this)); + if (auto msg = dbc()->msg(msg_id)) { + comment_edit->setText(msg->comment); + } + btn_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); validateName(name_edit->text()); form_layout->addRow(btn_box); @@ -218,7 +225,7 @@ void EditMessageDialog::validateName(const QString &text) { // CenterWidget -CenterWidget::CenterWidget(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) { +CenterWidget::CenterWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); main_layout->addWidget(welcome_widget = createWelcomeWidget()); @@ -228,7 +235,7 @@ void CenterWidget::setMessage(const MessageId &msg_id) { if (!detail_widget) { delete welcome_widget; welcome_widget = nullptr; - layout()->addWidget(detail_widget = new DetailWidget(charts, this)); + layout()->addWidget(detail_widget = new DetailWidget(((MainWindow*)parentWidget())->charts_widget, this)); } detail_widget->setMessage(msg_id); } diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index ce65adb34e..10c6a6c46e 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "selfdrive/ui/qt/widgets/controls.h" #include "tools/cabana/binaryview.h" @@ -10,6 +11,7 @@ #include "tools/cabana/historylog.h" #include "tools/cabana/signalview.h" +class MainWindow; class EditMessageDialog : public QDialog { public: EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent); @@ -19,6 +21,7 @@ public: QString original_name; QDialogButtonBox *btn_box; QLineEdit *name_edit; + QTextEdit *comment_edit; QLabel *error_label; QSpinBox *size_spin; }; @@ -54,7 +57,7 @@ private: class CenterWidget : public QWidget { Q_OBJECT public: - CenterWidget(ChartsWidget* charts, QWidget *parent); + CenterWidget(QWidget *parent); void setMessage(const MessageId &msg_id); void clear(); @@ -62,5 +65,4 @@ private: QWidget *createWelcomeWidget(); DetailWidget *detail_widget = nullptr; QWidget *welcome_widget = nullptr; - ChartsWidget *charts; }; diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc index 13e8f70a8f..f61a89b73c 100644 --- a/tools/cabana/historylog.cc +++ b/tools/cabana/historylog.cc @@ -63,7 +63,10 @@ QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, i return "Data"; } } else if (role == Qt::BackgroundRole && section > 0 && show_signals) { - return QBrush(getColor(sigs[section - 1])); + // Alpha-blend the signal color with the background to ensure contrast + QColor sigColor = sigs[section - 1]->color; + sigColor.setAlpha(128); + return QBrush(sigColor); } } return {}; @@ -122,7 +125,7 @@ std::deque HistoryLogModel::fetchData(InputIt first, I for (; first != last && (*first)->mono_time > min_time; ++first) { const CanEvent *e = *first; for (int i = 0; i < sigs.size(); ++i) { - values[i] = get_raw_value(e->dat, e->size, *sigs[i]); + sigs[i]->getValue(e->dat, e->size, &values[i]); } if (!filter_cmp || filter_cmp(values[filter_sig_idx], filter_value)) { auto &m = msgs.emplace_back(); @@ -224,7 +227,7 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) { display_type_cb->setToolTip(tr("Display signal value or raw hex value")); comp_box->addItems({">", "=", "!=", "<"}); value_edit->setClearButtonEnabled(true); - value_edit->setValidator(new QDoubleValidator(-500000, 500000, 6, this)); + value_edit->setValidator(new DoubleValidator(this)); dynamic_mode->setChecked(true); dynamic_mode->setEnabled(!can->liveStreaming()); diff --git a/tools/cabana/historylog.h b/tools/cabana/historylog.h index 1f8c157f21..2dea698f68 100644 --- a/tools/cabana/historylog.h +++ b/tools/cabana/historylog.h @@ -62,7 +62,7 @@ public: uint64_t last_fetch_time = 0; std::function filter_cmp = nullptr; std::deque messages; - std::vector sigs; + std::vector sigs; bool dynamic_mode = true; bool display_signals_mode = true; }; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 7b2d23a3f9..87066bf9e6 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -18,7 +17,6 @@ #include "tools/cabana/commands.h" #include "tools/cabana/streamselector.h" -#include "tools/cabana/streams/replaystream.h" #include "tools/cabana/tools/findsignal.h" static MainWindow *main_win = nullptr; @@ -29,12 +27,14 @@ void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const MainWindow::MainWindow() : QMainWindow() { createDockWindows(); - center_widget = new CenterWidget(charts_widget, this); - setCentralWidget(center_widget); + setCentralWidget(center_widget = new CenterWidget(this)); createActions(); createStatusBar(); createShortcuts(); + // save default window state to allow resetting it + default_state = saveState(); + // restore states restoreGeometry(settings.geometry); if (isMaximized()) { @@ -68,23 +68,21 @@ MainWindow::MainWindow() : QMainWindow() { QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage); QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress); - QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, center_widget, &CenterWidget::setMessage); - QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts); - QObject::connect(can, &AbstractStream::streamStarted, this, &MainWindow::streamStarted); - QObject::connect(can, &AbstractStream::sourcesUpdated, dbc(), &DBCManager::updateSources); - QObject::connect(can, &AbstractStream::sourcesUpdated, this, &MainWindow::updateLoadSaveMenus); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MainWindow::DBCFileChanged); QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged); QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &MainWindow::undoStackIndexChanged); QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus); + QObject::connect(StreamNotifier::instance(), &StreamNotifier::changingStream, this, &MainWindow::changingStream); + QObject::connect(StreamNotifier::instance(), &StreamNotifier::streamStarted, this, &MainWindow::streamStarted); } void MainWindow::createActions() { + // File menu QMenu *file_menu = menuBar()->addMenu(tr("&File")); - if (!can->liveStreaming()) { - file_menu->addAction(tr("Open Route..."), this, &MainWindow::openRoute); - file_menu->addSeparator(); - } + file_menu->addAction(tr("Open Stream..."), this, &MainWindow::openStream); + close_stream_act = file_menu->addAction(tr("Close stream"), this, &MainWindow::closeStream); + close_stream_act->setEnabled(false); + file_menu->addSeparator(); file_menu->addAction(tr("New DBC File"), [this]() { newFile(); })->setShortcuts(QKeySequence::New); file_menu->addAction(tr("Open DBC File..."), [this]() { openFile(); })->setShortcuts(QKeySequence::Open); @@ -127,6 +125,7 @@ void MainWindow::createActions() { file_menu->addSeparator(); file_menu->addAction(tr("E&xit"), qApp, &QApplication::closeAllWindows)->setShortcuts(QKeySequence::Quit); + // Edit Menu QMenu *edit_menu = menuBar()->addMenu(tr("&Edit")); auto undo_act = UndoStack::instance()->createUndoAction(this, tr("&Undo")); undo_act->setShortcuts(QKeySequence::Undo); @@ -141,24 +140,44 @@ void MainWindow::createActions() { commands_act->setDefaultWidget(new QUndoView(UndoStack::instance())); commands_menu->addAction(commands_act); - QMenu *tools_menu = menuBar()->addMenu(tr("&Tools")); + // View Menu + QMenu *view_menu = menuBar()->addMenu(tr("&View")); + auto act = view_menu->addAction(tr("Full Screen"), this, &MainWindow::toggleFullScreen, QKeySequence::FullScreen); + addAction(act); + view_menu->addSeparator(); + view_menu->addAction(messages_dock->toggleViewAction()); + view_menu->addAction(video_dock->toggleViewAction()); + view_menu->addSeparator(); + view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(default_state); }); + + // Tools Menu + tools_menu = menuBar()->addMenu(tr("&Tools")); tools_menu->addAction(tr("Find &Similar Bits"), this, &MainWindow::findSimilarBits); tools_menu->addAction(tr("&Find Signal"), this, &MainWindow::findSignal); + // Help Menu QMenu *help_menu = menuBar()->addMenu(tr("&Help")); help_menu->addAction(tr("Help"), this, &MainWindow::onlineHelp)->setShortcuts(QKeySequence::HelpContents); help_menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt); } void MainWindow::createDockWindows() { - // left panel + messages_dock = new QDockWidget(tr("MESSAGES"), this); + messages_dock->setObjectName("MessagesPanel"); + messages_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea); + messages_dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); + addDockWidget(Qt::LeftDockWidgetArea, messages_dock); + + video_dock = new QDockWidget("", this); + video_dock->setObjectName(tr("VideoPanel")); + video_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + video_dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); + addDockWidget(Qt::RightDockWidgetArea, video_dock); +} + +void MainWindow::createDockWidgets() { messages_widget = new MessagesWidget(this); - QDockWidget *dock = new QDockWidget(tr("MESSAGES"), this); - dock->setObjectName("MessagesPanel"); - dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea); - dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); - dock->setWidget(messages_widget); - addDockWidget(Qt::LeftDockWidgetArea, dock); + messages_dock->setWidget(messages_widget); // right panel charts_widget = new ChartsWidget(this); @@ -176,17 +195,9 @@ void MainWindow::createDockWindows() { video_splitter->addWidget(charts_container); video_splitter->setStretchFactor(1, 1); video_splitter->restoreState(settings.video_splitter_state); - if (can->liveStreaming() || video_splitter->sizes()[0] == 0) { - // display video at minimum size. - video_splitter->setSizes({1, 1}); - } - - video_dock = new QDockWidget(can->routeName(), this); - video_dock->setObjectName(tr("VideoPanel")); - video_dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); - video_dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + video_splitter->handle(1)->setEnabled(!can->liveStreaming()); video_dock->setWidget(video_splitter); - addDockWidget(Qt::RightDockWidgetArea, video_dock); + QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts); } void MainWindow::createStatusBar() { @@ -205,8 +216,6 @@ void MainWindow::createStatusBar() { void MainWindow::createShortcuts() { auto shortcut = new QShortcut(QKeySequence(Qt::Key_Space), this, nullptr, nullptr, Qt::ApplicationShortcut); QObject::connect(shortcut, &QShortcut::activated, []() { can->pause(!can->isPaused()); }); - shortcut = new QShortcut(QKeySequence(QKeySequence::FullScreen), this, nullptr, nullptr, Qt::ApplicationShortcut); - QObject::connect(shortcut, &QShortcut::activated, this, &MainWindow::toggleFullScreen); // TODO: add more shortcuts here. } @@ -242,18 +251,27 @@ void MainWindow::DBCFileChanged() { updateLoadSaveMenus(); } -void MainWindow::openRoute() { - StreamSelector dlg(this); - dlg.addStreamWidget(ReplayStream::widget(&can)); +void MainWindow::openStream() { + AbstractStream *stream = nullptr; + StreamSelector dlg(&stream, this); if (dlg.exec()) { - center_widget->clear(); - charts_widget->removeAll(); + if (!dlg.dbcFile().isEmpty()) { + loadFile(dlg.dbcFile()); + } + stream->start(); statusBar()->showMessage(tr("Route %1 loaded").arg(can->routeName()), 2000); - } else if (dlg.failed()) { - close(); } } +void MainWindow::closeStream() { + AbstractStream *stream = new DummyStream(this); + stream->start(); + if (dbc()->nonEmptyDBCCount() > 0) { + emit dbc()->DBCFileChanged(); + } + statusBar()->showMessage(tr("stream closed")); +} + void MainWindow::newFile(SourceSet s) { closeFile(s); dbc()->open(s, "", ""); @@ -319,30 +337,52 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { } } -void MainWindow::streamStarted() { - auto fingerprint = can->carFingerprint(); - if (can->liveStreaming()) { - video_dock->setWindowTitle(can->routeName()); - } else { - video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPRINT: %2").arg(can->routeName()).arg(fingerprint.isEmpty() ? tr("Unknown Car") : fingerprint)); - } +void MainWindow::changingStream() { + center_widget->clear(); + delete messages_widget; + delete video_splitter; +} +void MainWindow::streamStarted() { + bool has_stream = dynamic_cast(can) == nullptr; + close_stream_act->setEnabled(has_stream); + tools_menu->setEnabled(has_stream); + createDockWidgets(); + + video_dock->setWindowTitle(can->routeName()); + if (can->liveStreaming() || video_splitter->sizes()[0] == 0) { + // display video at minimum size. + video_splitter->setSizes({1, 1}); + } // Don't overwrite already loaded DBC if (!dbc()->msgCount()) { - if (!fingerprint.isEmpty()) { - auto dbc_name = fingerprint_to_dbc[fingerprint]; + newFile(); + } + + QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, center_widget, &CenterWidget::setMessage); + QObject::connect(can, &AbstractStream::eventsMerged, this, &MainWindow::eventsMerged); + QObject::connect(can, &AbstractStream::sourcesUpdated, this, &MainWindow::updateLoadSaveMenus); +} + +void MainWindow::eventsMerged() { + if (!can->liveStreaming() && std::exchange(car_fingerprint, can->carFingerprint()) != car_fingerprint) { + video_dock->setWindowTitle(tr("ROUTE: %1 FINGERPRINT: %2") + .arg(can->routeName()) + .arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint)); + // Don't overwrite already loaded DBC + if (!dbc()->msgCount() && !car_fingerprint.isEmpty()) { + auto dbc_name = fingerprint_to_dbc[car_fingerprint]; if (dbc_name != QJsonValue::Undefined) { - loadDBCFromOpendbc(dbc_name.toString()); - return; + // Prevent dialog that load autosaved file from blocking replay->start(). + QTimer::singleShot(0, [dbc_name, this]() { loadDBCFromOpendbc(dbc_name.toString()); }); } } - newFile(); } } void MainWindow::save() { // Save all open DBC files - for (auto &[s, dbc_file] : dbc()->dbc_files) { + for (auto dbc_file : dbc()->allDBCFiles()) { if (dbc_file->isEmpty()) continue; saveFile(dbc_file); } @@ -350,7 +390,7 @@ void MainWindow::save() { void MainWindow::saveAs() { // Save as all open DBC files. Should not be called with more than 1 file open - for (auto &[s, dbc_file] : dbc()->dbc_files) { + for (auto dbc_file : dbc()->allDBCFiles()) { if (dbc_file->isEmpty()) continue; saveFileAs(dbc_file); } @@ -358,7 +398,7 @@ void MainWindow::saveAs() { void MainWindow::autoSave() { if (!UndoStack::instance()->isClean()) { - for (auto &[_, dbc_file] : dbc()->dbc_files) { + for (auto dbc_file : dbc()->allDBCFiles()) { if (!dbc_file->filename.isEmpty()) { dbc_file->autoSave(); } @@ -367,7 +407,7 @@ void MainWindow::autoSave() { } void MainWindow::cleanupAutoSaveFile() { - for (auto &[_, dbc_file] : dbc()->dbc_files) { + for (auto dbc_file : dbc()->allDBCFiles()) { dbc_file->cleanupAutoSaveFile(); } } @@ -404,9 +444,7 @@ void MainWindow::saveFile(DBCFile *dbc_file) { } void MainWindow::saveFileAs(DBCFile *dbc_file) { - auto it = std::find_if(dbc()->dbc_files.begin(), dbc()->dbc_files.end(), [=](auto &f) { return f.second == dbc_file; }); - assert(it != dbc()->dbc_files.end()); - QString title = tr("Save File (bus: %1)").arg(toString(it->first)); + QString title = tr("Save File (bus: %1)").arg(toString(dbc()->sources(dbc_file))); QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); if (!fn.isEmpty()) { dbc_file->saveAs(fn); @@ -415,15 +453,9 @@ void MainWindow::saveFileAs(DBCFile *dbc_file) { } } -void MainWindow::removeBusFromFile(DBCFile *dbc_file, uint8_t source) { - assert(dbc_file != nullptr); - SourceSet ss = {source, uint8_t(source + 128), uint8_t(source + 192)}; - dbc()->removeSourcesFromFile(dbc_file, ss); -} - void MainWindow::saveToClipboard() { // Copy all open DBC files to clipboard. Should not be called with more than 1 file open - for (auto &[s, dbc_file] : dbc()->dbc_files) { + for (auto dbc_file : dbc()->allDBCFiles()) { if (dbc_file->isEmpty()) continue; saveFileToClipboard(dbc_file); } @@ -448,12 +480,10 @@ void MainWindow::updateLoadSaveMenus() { // TODO: Support clipboard for multiple files copy_dbc_to_clipboard->setEnabled(cnt == 1); - QList sources_sorted = can->sources.toList(); - std::sort(sources_sorted.begin(), sources_sorted.end()); - manage_dbcs_menu->clear(); + manage_dbcs_menu->setEnabled(dynamic_cast(can) == nullptr); - for (uint8_t source : sources_sorted) { + for (int source : can->sources) { if (source >= 64) continue; // Sent and blocked buses are handled implicitly SourceSet ss = {source, uint8_t(source + 128), uint8_t(source + 192)}; @@ -464,32 +494,26 @@ void MainWindow::updateLoadSaveMenus() { bus_menu->addAction(tr("Load DBC From Clipboard..."), [=]() { loadFromClipboard(ss, false); }); // Show sub-menu for each dbc for this source. - QStringList bus_menu_fns; - for (auto it : dbc()->dbc_files) { - auto &[src, dbc_file] = it; - if (!src.contains(source) && (src != SOURCE_ALL)) { - continue; - } - + QString file_name = "No DBCs loaded"; + if (auto dbc_file = dbc()->findDBCFile(source)) { bus_menu->addSeparator(); - bus_menu->addAction(dbc_file->name() + " (" + toString(src) + ")")->setEnabled(false); - bus_menu->addAction(tr("Save..."), [=]() { saveFile(it.second); }); - bus_menu->addAction(tr("Save As..."), [=]() { saveFileAs(it.second); }); - bus_menu->addAction(tr("Copy to Clipboard..."), [=]() { saveFileToClipboard(it.second); }); - bus_menu->addAction(tr("Remove from this bus..."), [=]() { removeBusFromFile(it.second, source); }); - bus_menu->addAction(tr("Remove from all buses..."), [=]() { closeFile(it.second); }); + bus_menu->addAction(dbc_file->name() + " (" + toString(dbc()->sources(dbc_file)) + ")")->setEnabled(false); + bus_menu->addAction(tr("Save..."), [=]() { saveFile(dbc_file); }); + bus_menu->addAction(tr("Save As..."), [=]() { saveFileAs(dbc_file); }); + bus_menu->addAction(tr("Copy to Clipboard..."), [=]() { saveFileToClipboard(dbc_file); }); + bus_menu->addAction(tr("Remove from this bus..."), [=]() { closeFile(ss); }); + bus_menu->addAction(tr("Remove from all buses..."), [=]() { closeFile(dbc_file); }); - bus_menu_fns << dbc_file->name(); + file_name = dbc_file->name(); } manage_dbcs_menu->addMenu(bus_menu); - QString bus_menu_title = bus_menu_fns.size() ? bus_menu_fns.join(", ") : "No DBCs loaded"; - bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(bus_menu_title)); + bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(file_name)); } QStringList title; - for (auto &[src, dbc_file] : dbc()->dbc_files) { - title.push_back(tr("(%1) %2").arg(toString(src), dbc_file->name())); + for (auto f : dbc()->allDBCFiles()) { + title.push_back(tr("(%1) %2").arg(toString(dbc()->sources(f)), f->name())); } setWindowFilePath(title.join(" | ")); } diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index 9db999e6c9..bbbe8730cb 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -22,14 +23,18 @@ public: void dockCharts(bool dock); void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); } void loadFile(const QString &fn, SourceSet s = SOURCE_ALL); + ChartsWidget *charts_widget = nullptr; public slots: - void openRoute(); + void openStream(); + void closeStream(); + void changingStream(); + void streamStarted(); + void newFile(SourceSet s = SOURCE_ALL); void openFile(SourceSet s = SOURCE_ALL); void openRecentFile(); void loadDBCFromOpendbc(const QString &name); - void streamStarted(); void save(); void saveAs(); void saveToClipboard(); @@ -45,7 +50,6 @@ protected: void saveFile(DBCFile *dbc_file); void saveFileAs(DBCFile *dbc_file); void saveFileToClipboard(DBCFile *dbc_file); - void removeBusFromFile(DBCFile *dbc_file, uint8_t source); void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true); void autoSave(); void cleanupAutoSaveFile(); @@ -67,27 +71,33 @@ protected: void toggleFullScreen(); void updateStatus(); void updateLoadSaveMenus(); + void createDockWidgets(); + void eventsMerged(); VideoWidget *video_widget = nullptr; QDockWidget *video_dock; - MessagesWidget *messages_widget; + QDockWidget *messages_dock; + MessagesWidget *messages_widget = nullptr; CenterWidget *center_widget; - ChartsWidget *charts_widget; QWidget *floating_window = nullptr; QVBoxLayout *charts_layout; QProgressBar *progress_bar; QLabel *status_label; QJsonDocument fingerprint_to_dbc; - QSplitter *video_splitter;; + QSplitter *video_splitter = nullptr; enum { MAX_RECENT_FILES = 15 }; QAction *recent_files_acts[MAX_RECENT_FILES] = {}; QMenu *open_recent_menu = nullptr; QMenu *manage_dbcs_menu = nullptr; + QMenu *tools_menu = nullptr; + QAction *close_stream_act = nullptr; QAction *save_dbc = nullptr; QAction *save_dbc_as = nullptr; QAction *copy_dbc_to_clipboard = nullptr; + QString car_fingerprint; int prev_undostack_index = 0; int prev_undostack_count = 0; + QByteArray default_state; friend class OnlineHelp; }; diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 60aadb3673..5982689bda 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -5,6 +5,8 @@ #include #include +#include "tools/cabana/commands.h" + MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0 ,0, 0, 0); @@ -30,7 +32,9 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { auto delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes); view->setItemDelegate(delegate); + view->setHeader(header); view->setModel(model); + view->setHeader(header); view->setSortingEnabled(true); view->sortByColumn(MessageListModel::Column::NAME, Qt::AscendingOrder); view->setAllColumnsShowFocus(true); @@ -38,12 +42,12 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { view->setItemsExpandable(false); view->setIndentation(0); view->setRootIsDecorated(false); - view->setHeader(header); // Must be called before setting any header parameters to avoid overriding restoreHeaderState(settings.message_header_state); view->header()->setSectionsMovable(true); view->header()->setSectionResizeMode(MessageListModel::Column::DATA, QHeaderView::Fixed); + view->header()->setStretchLastSection(true); // Header context menu view->header()->setContextMenuPolicy(Qt::CustomContextMenu); @@ -81,13 +85,8 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { settings.suppress_defined_signals = (state == Qt::Checked); }); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); - QObject::connect(can, &AbstractStream::streamStarted, this, &MessagesWidget::reset); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MessagesWidget::dbcModified); - QObject::connect(dbc(), &DBCManager::msgUpdated, this, &MessagesWidget::dbcModified); - QObject::connect(dbc(), &DBCManager::msgRemoved, this, &MessagesWidget::dbcModified); - QObject::connect(dbc(), &DBCManager::signalAdded, this, &MessagesWidget::dbcModified); - QObject::connect(dbc(), &DBCManager::signalRemoved, this, &MessagesWidget::dbcModified); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &MessagesWidget::dbcModified); + QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &MessagesWidget::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); @@ -132,7 +131,7 @@ void MessagesWidget::dbcModified() { void MessagesWidget::selectMessage(const MessageId &msg_id) { auto it = std::find(model->msgs.cbegin(), model->msgs.cend(), msg_id); if (it != model->msgs.cend()) { - view->selectionModel()->setCurrentIndex(model->index(std::distance(model->msgs.cbegin(), it), 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); + view->setCurrentIndex(model->index(std::distance(model->msgs.cbegin(), it), 0)); } } @@ -146,14 +145,6 @@ void MessagesWidget::updateSuppressedButtons() { } } -void MessagesWidget::reset() { - current_msg_id = std::nullopt; - view->selectionModel()->clear(); - model->reset(); - updateSuppressedButtons(); -} - - // MessageListModel QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { @@ -203,6 +194,11 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { return QVariant::fromValue(colors); } else if (role == BytesRole && index.column() == Column::DATA && id.source != INVALID_SOURCE) { return can_data.dat; + } else if (role == Qt::ToolTipRole && index.column() == Column::NAME) { + auto msg = dbc()->msg(id); + auto tooltip = msg ? msg->name : UNTITLED; + if (msg && !msg->comment.isEmpty()) tooltip += "
" + msg->comment + ""; + return tooltip; } return {}; } @@ -214,7 +210,7 @@ void MessageListModel::setFilterStrings(const QMap &filters) { void MessageListModel::dbcModified() { dbc_address.clear(); - for (const auto &[_, m] : dbc()->getMessages(0)) { + for (const auto &[_, m] : dbc()->getMessages(-1)) { dbc_address.insert(m.address); } fetchData(); @@ -254,104 +250,59 @@ void MessageListModel::sortMessages(std::vector &new_msgs) { } } -static std::pair parseRange(const QString &filter, bool *ok = nullptr, int base = 10) { +static bool parseRange(const QString &filter, uint32_t value, int base = 10) { // Parse out filter string into a range (e.g. "1" -> {1, 1}, "1-3" -> {1, 3}, "1-" -> {1, inf}) - bool ok1 = true, ok2 = true; - unsigned int parsed1 = std::numeric_limits::min(); - unsigned int parsed2 = std::numeric_limits::max(); - + unsigned int min = std::numeric_limits::min(); + unsigned int max = std::numeric_limits::max(); auto s = filter.split('-'); - if (s.size() == 1) { - parsed1 = s[0].toUInt(ok, base); - return {parsed1, parsed1}; - } else if (s.size() == 2) { - if (!s[0].isEmpty()) parsed1 = s[0].toUInt(&ok1, base); - if (!s[1].isEmpty()) parsed2 = s[1].toUInt(&ok2, base); - - *ok = ok1 & ok2; - return {parsed1, parsed2}; - } else { - *ok = false; - return {0, 0}; + bool ok = s.size() >= 1 && s.size() <= 2; + if (ok && !s[0].isEmpty()) min = s[0].toUInt(&ok, base); + if (ok && s.size() == 1) { + max = min; + } else if (ok && s.size() == 2 && !s[1].isEmpty()) { + max = s[1].toUInt(&ok, base); } + return ok && value >= min && value <= max; } bool MessageListModel::matchMessage(const MessageId &id, const CanData &data, const QMap &filters) { - auto cs = Qt::CaseInsensitive; bool match = true; - bool convert_ok; - - for (int column = Column::NAME; column <= Column::DATA; column++) { - if (!filters.contains(column)) continue; - const QString &txt = filters[column]; - + for (auto it = filters.cbegin(); it != filters.cend() && match; ++it) { + const QString &txt = it.value(); QRegularExpression re(txt, QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption); - - switch (column) { - case Column::NAME: - { - bool name_match = re.match(msgName(id)).hasMatch(); - - // Message signals - if (const auto msg = dbc()->msg(id)) { - for (auto s : msg->getSignals()) { - if (re.match(s->name).hasMatch()) { - name_match = true; - break; - } - } - } - if (!name_match) match = false; - } + switch (it.key()) { + case Column::NAME: { + const auto msg = dbc()->msg(id); + match = re.match(msg ? msg->name : UNTITLED).hasMatch(); + match |= msg && std::any_of(msg->sigs.cbegin(), msg->sigs.cend(), [&re](const auto &s) { return re.match(s->name).hasMatch(); }); break; + } case Column::SOURCE: - { - auto source = parseRange(txt, &convert_ok); - bool source_match = convert_ok && (id.source >= source.first && id.source <= source.second); - if (!source_match) match = false; - } + match = parseRange(txt, id.source); break; - case Column::ADDRESS: - { - bool address_re_match = re.match(QString::number(id.address, 16)).hasMatch(); - - auto address = parseRange(txt, &convert_ok, 16); - bool address_match = convert_ok && (id.address >= address.first && id.address <= address.second); - - if (!address_re_match && !address_match) match = false; - } + case Column::ADDRESS: { + match = re.match(QString::number(id.address, 16)).hasMatch(); + match |= parseRange(txt, id.address, 16); break; + } case Column::FREQ: - { - // TODO: Hide stale messages? - auto freq = parseRange(txt, &convert_ok); - bool freq_match = convert_ok && (data.freq >= freq.first && data.freq <= freq.second); - if (!freq_match) match = false; - } + // TODO: Hide stale messages? + match = parseRange(txt, data.freq); break; case Column::COUNT: - { - auto count = parseRange(txt, &convert_ok); - bool count_match = convert_ok && (data.count >= count.first && data.count <= count.second); - if (!count_match) match = false; - } + match = parseRange(txt, data.count); break; - case Column::DATA: - { - bool data_match = false; - data_match |= QString(data.dat.toHex()).contains(txt, cs); - data_match |= re.match(QString(data.dat.toHex())).hasMatch(); - data_match |= re.match(QString(data.dat.toHex(' '))).hasMatch(); - - if (!data_match) match = false; - } + case Column::DATA: { + match = QString(data.dat.toHex()).contains(txt, Qt::CaseInsensitive); + match |= re.match(QString(data.dat.toHex())).hasMatch(); + match |= re.match(QString(data.dat.toHex(' '))).hasMatch(); break; + } } } return match; } - void MessageListModel::fetchData() { std::vector new_msgs; new_msgs.reserve(can->last_msgs.size() + dbc_address.size()); @@ -419,14 +370,6 @@ void MessageListModel::clearSuppress() { suppressed_bytes.clear(); } -void MessageListModel::reset() { - beginResetModel(); - filter_str.clear(); - msgs.clear(); - clearSuppress(); - endResetModel(); -} - void MessageListModel::forceResetModel() { beginResetModel(); endResetModel(); @@ -467,8 +410,8 @@ void MessageView::updateBytesSectionSize() { } } int width = delegate->widthForBytes(max_bytes); - if (header()->sectionSize(5) != width) { - header()->resizeSection(5, width); + if (header()->sectionSize(MessageListModel::Column::DATA) != width) { + header()->resizeSection(MessageListModel::Column::DATA, width); } } @@ -476,11 +419,10 @@ void MessageView::headerContextMenuEvent(const QPoint &pos) { QMenu *menu = new QMenu(this); int cur_index = header()->logicalIndexAt(pos); - QString column_name; QAction *action; for (int visual_index = 0; visual_index < header()->count(); visual_index++) { int logical_index = header()->logicalIndex(visual_index); - column_name = model()->headerData(logical_index, Qt::Horizontal).toString(); + QString column_name = model()->headerData(logical_index, Qt::Horizontal).toString(); // Hide show action if (header()->isSectionHidden(logical_index)) { @@ -508,22 +450,6 @@ MessageViewHeader::MessageViewHeader(QWidget *parent, MessageListModel *model) : QObject::connect(this, &QHeaderView::sectionMoved, this, &MessageViewHeader::updateHeaderPositions); } -void MessageViewHeader::showEvent(QShowEvent *e) { - - for (int i = 0; i < count(); i++) { - if (!editors[i]) { - QString column_name = model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); - editors[i] = new QLineEdit(this); - editors[i]->setClearButtonEnabled(true); - editors[i]->setPlaceholderText(tr("Filter %1").arg(column_name)); - - QObject::connect(editors[i], &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters); - } - editors[i]->show(); - } - QHeaderView::showEvent(e); -} - void MessageViewHeader::updateFilters() { QMap filters; for (int i = 0; i < count(); i++) { @@ -556,11 +482,18 @@ void MessageViewHeader::updateHeaderPositions() { } void MessageViewHeader::updateGeometries() { - if (editors[0]) { - setViewportMargins(0, 0, 0, editors[0]->sizeHint().height()); - } else { - setViewportMargins(0, 0, 0, 0); + for (int i = 0; i < count(); i++) { + if (!editors[i]) { + QString column_name = model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); + editors[i] = new QLineEdit(this); + editors[i]->setClearButtonEnabled(true); + editors[i]->setPlaceholderText(tr("Filter %1").arg(column_name)); + + QObject::connect(editors[i], &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters); + } } + setViewportMargins(0, 0, 0, editors[0] ? editors[0]->sizeHint().height() : 0); + QHeaderView::updateGeometries(); updateHeaderPositions(); } diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index 036758e636..57011e51b4 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -38,7 +38,6 @@ public: void fetchData(); void suppress(); void clearSuppress(); - void reset(); void forceResetModel(); void dbcModified(); std::vector msgs; @@ -71,7 +70,6 @@ class MessageViewHeader : public QHeaderView { Q_OBJECT public: MessageViewHeader(QWidget *parent, MessageListModel *model); - void showEvent(QShowEvent *e) override; void updateHeaderPositions(); void updateGeometries() override; @@ -100,7 +98,6 @@ public: QByteArray saveHeaderState() const { return view->header()->saveState(); } bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); } void updateSuppressedButtons(); - void reset(); public slots: void dbcModified(); diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 124f9f6b9a..a557aa57fb 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -18,6 +18,12 @@ // SignalModel +static QString signalTypeToString(cabana::Signal::Type type) { + if (type == cabana::Signal::Type::Multiplexor) return "Multiplexor Signal"; + else if (type == cabana::Signal::Type::Multiplexed) return "Multiplexed Signal"; + else return "Normal Signal"; +} + SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) { QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &SignalModel::refresh); QObject::connect(dbc(), &DBCManager::msgUpdated, this, &SignalModel::handleMsgChanged); @@ -30,7 +36,8 @@ SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(p void SignalModel::insertItem(SignalModel::Item *parent_item, int pos, const cabana::Signal *sig) { Item *item = new Item{.sig = sig, .parent = parent_item, .title = sig->name, .type = Item::Sig}; parent_item->children.insert(pos, item); - QString titles[]{"Name", "Size", "Little Endian", "Signed", "Offset", "Factor", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Descriptions"}; + QString titles[]{"Name", "Size", "Little Endian", "Signed", "Offset", "Factor", "Type", "Multiplex Value", "Extra Info", + "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Descriptions"}; for (int i = 0; i < std::size(titles); ++i) { item->children.push_back(new Item{.sig = sig, .parent = item, .title = titles[i], .type = (Item::Type)(i + Item::Name)}); } @@ -87,6 +94,9 @@ Qt::ItemFlags SignalModel::flags(const QModelIndex &index) const { if (index.column() == 1 && item->type != Item::Sig && item->type != Item::ExtraInfo) { flags |= (item->type == Item::Endian || item->type == Item::Signed) ? Qt::ItemIsUserCheckable : Qt::ItemIsEditable; } + if (item->type == Item::MultiplexValue && item->sig->type != cabana::Signal::Type::Multiplexed) { + flags &= ~Qt::ItemIsEnabled; + } return flags; } @@ -124,16 +134,18 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const { case Item::Sig: return item->sig_val; case Item::Name: return item->sig->name; case Item::Size: return item->sig->size; - case Item::Offset: return QString::number(item->sig->offset, 'f', 6); - case Item::Factor: return QString::number(item->sig->factor, 'f', 6); + case Item::SignalType: return signalTypeToString(item->sig->type); + case Item::MultiplexValue: return item->sig->multiplex_value; + case Item::Offset: return doubleToString(item->sig->offset); + case Item::Factor: return doubleToString(item->sig->factor); case Item::Unit: return item->sig->unit; case Item::Comment: return item->sig->comment; - case Item::Min: return item->sig->min; - case Item::Max: return item->sig->max; + case Item::Min: return doubleToString(item->sig->min); + case Item::Max: return doubleToString(item->sig->max); case Item::Desc: { QStringList val_desc; for (auto &[val, desc] : item->sig->val_desc) { - val_desc << QString("%1 \"%2\"").arg(val, desc); + val_desc << QString("%1 \"%2\"").arg(val).arg(desc); } return val_desc.join(" "); } @@ -146,7 +158,7 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const { } else if (role == Qt::DecorationRole && index.column() == 0 && item->type == Item::ExtraInfo) { return utils::icon(item->parent->extra_expanded ? "chevron-compact-down" : "chevron-compact-up"); } else if (role == Qt::ToolTipRole && item->type == Item::Sig) { - return (index.column() == 0) ? item->sig->name : item->sig_val; + return (index.column() == 0) ? signalToolTip(item->sig) : QString(); } } return {}; @@ -160,18 +172,19 @@ bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int r switch (item->type) { case Item::Name: s.name = value.toString(); break; case Item::Size: s.size = value.toInt(); break; + case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break; + case Item::MultiplexValue: s.multiplex_value = value.toInt(); break; case Item::Endian: s.is_little_endian = value.toBool(); break; case Item::Signed: s.is_signed = value.toBool(); break; case Item::Offset: s.offset = value.toDouble(); break; case Item::Factor: s.factor = value.toDouble(); break; case Item::Unit: s.unit = value.toString(); break; case Item::Comment: s.comment = value.toString(); break; - case Item::Min: s.min = value.toString(); break; - case Item::Max: s.max = value.toString(); break; + case Item::Min: s.min = value.toDouble(); break; + case Item::Max: s.max = value.toDouble(); break; case Item::Desc: s.val_desc = value.value(); break; default: return false; } - s.updatePrecision(); bool ret = saveSignal(item->sig, s); emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole}); return ret; @@ -226,11 +239,11 @@ void SignalModel::addSignal(int start_bit, int size, bool little_endian) { auto msg = dbc()->msg(msg_id); if (!msg) { QString name = dbc()->newMsgName(msg_id); - UndoStack::push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size())); + UndoStack::push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size(), "")); msg = dbc()->msg(msg_id); } - cabana::Signal sig = {.name = dbc()->newSignalName(msg_id), .is_little_endian = little_endian, .factor = 1, .min = "0", .max = QString::number(std::pow(2, size) - 1)}; + cabana::Signal sig = {.name = dbc()->newSignalName(msg_id), .is_little_endian = little_endian, .factor = 1, .min = 0, .max = std::pow(2, size) - 1}; updateSigSizeParamsFromRange(sig, start_bit, size); UndoStack::push(new AddSigCommand(msg_id, sig)); } @@ -249,17 +262,14 @@ void SignalModel::removeSignal(const cabana::Signal *sig) { } void SignalModel::handleMsgChanged(MessageId id) { - if (id == msg_id) { + if (id.address == msg_id.address) { refresh(); } } void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) { if (id == msg_id) { - int i = 0; - for (; i < root->children.size(); ++i) { - if (sig->start_bit < root->children[i]->sig->start_bit) break; - } + int i = dbc()->msg(msg_id)->indexOf(sig); beginInsertRows({}, i, i); insertItem(root.get(), i, sig); endInsertRows(); @@ -269,6 +279,14 @@ void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) { void SignalModel::handleSignalUpdated(const cabana::Signal *sig) { if (int row = signalRow(sig); row != -1) { emit dataChanged(index(row, 0), index(row, 1), {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole}); + + // move row when the order changes. + int to = dbc()->msg(msg_id)->indexOf(sig); + if (to != row) { + beginMoveRows({}, row, row, {}, to > row ? to + 1 : to); + root->children.move(row, to); + endMoveRows(); + } } } @@ -284,8 +302,8 @@ void SignalModel::handleSignalRemoved(const cabana::Signal *sig) { SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { name_validator = new NameValidator(this); - double_validator = new QDoubleValidator(this); - double_validator->setLocale(QLocale::C); // Match locale of QString::toDouble() instead of system + double_validator = new DoubleValidator(this); + label_font.setPointSize(8); minmax_font.setPixelSize(10); } @@ -293,31 +311,22 @@ SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(pa QSize SignalItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { int width = option.widget->size().width() / 2; if (index.column() == 0) { - auto text = index.data(Qt::DisplayRole).toString(); + int spacing = option.widget->style()->pixelMetric(QStyle::PM_TreeViewIndentation) + color_label_width + 8; + auto text = index.data(Qt::DisplayRole).toString();; + auto item = (SignalModel::Item *)index.internalPointer(); + if (item->type == SignalModel::Item::Sig && item->sig->type != cabana::Signal::Type::Normal) { + text += item->sig->type == cabana::Signal::Type::Multiplexor ? QString(" M ") : QString(" m%1 ").arg(item->sig->multiplex_value); + spacing += (option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1) * 2; + } auto it = width_cache.find(text); if (it == width_cache.end()) { - int spacing = option.widget->style()->pixelMetric(QStyle::PM_TreeViewIndentation) + color_label_width + 8; - it = width_cache.insert(text, option.fontMetrics.width(text) + spacing); + it = width_cache.insert(text, option.fontMetrics.width(text)); } - width = std::min(option.widget->size().width() / 3.0, it.value()); + width = std::min(option.widget->size().width() / 3.0, it.value() + spacing); } return {width, QApplication::fontMetrics().height()}; } -bool SignalItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { - if (event && event->type() == QEvent::ToolTip && index.isValid()) { - auto item = (SignalModel::Item *)index.internalPointer(); - if (item->type == SignalModel::Item::Sig && index.column() == 1) { - QRect rc = option.rect.adjusted(0, 0, -option.rect.width() * 0.4, 0); - if (rc.contains(event->pos())) { - event->setAccepted(false); - return false; - } - } - } - return QStyledItemDelegate::helpEvent(event, view, option, index); -} - void SignalItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto item = (SignalModel::Item *)index.internalPointer(); if (editor && item->type == SignalModel::Item::Sig && index.column() == 1) { @@ -335,7 +344,7 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op if (item && item->type == SignalModel::Item::Sig) { painter->setRenderHint(QPainter::Antialiasing); if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.highlight()); + painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); } int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; @@ -348,15 +357,28 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op path.addRoundedRect(icon_rect, 3, 3); painter->setPen(item->highlight ? Qt::white : Qt::black); painter->setFont(label_font); - painter->fillPath(path, getColor(item->sig).darker(item->highlight ? 125 : 0)); + painter->fillPath(path, item->sig->color.darker(item->highlight ? 125 : 0)); painter->drawText(icon_rect, Qt::AlignCenter, QString::number(item->row() + 1)); r.setLeft(icon_rect.right() + h_margin * 2); + // multiplexer indicator + if (item->sig->type != cabana::Signal::Type::Normal) { + QString indicator = item->sig->type == cabana::Signal::Type::Multiplexor ? QString(" M ") : QString(" m%1 ").arg(item->sig->multiplex_value); + QRect indicator_rect{r.x(), r.y(), option.fontMetrics.width(indicator), r.height()}; + painter->setBrush(Qt::gray); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(indicator_rect, 3, 3); + painter->setPen(Qt::white); + painter->drawText(indicator_rect, Qt::AlignCenter, indicator); + r.setLeft(indicator_rect.right() + h_margin * 2); + } + + // name auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, r.width()); painter->setPen(option.palette.color(option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text)); painter->setFont(option.font); painter->drawText(r, option.displayAlignment, text); - } else if (index.column() == 1) { + } else if (index.column() == 1 && !item->sparkline.pixmap.isNull()) { // sparkline QSize sparkline_size = item->sparkline.pixmap.size() / item->sparkline.pixmap.devicePixelRatio(); painter->drawPixmap(QRect(r.topLeft(), sparkline_size), item->sparkline.pixmap); @@ -374,8 +396,15 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->drawText(rect, Qt::AlignLeft | Qt::AlignBottom, min); QFontMetrics fm(minmax_font); value_adjust = std::max(fm.width(min), fm.width(max)) + 5; + } else if (item->sig->type == cabana::Signal::Type::Multiplexed) { + // display freq of multiplexed signal + painter->setFont(label_font); + QString freq = QString("%1 hz").arg(item->sparkline.freq(), 0, 'g', 2); + painter->drawText(rect.adjusted(5, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, freq); + QFontMetrics fm(label_font); + value_adjust = fm.width(freq) + 10; } - // value + // signal value painter->setFont(option.font); rect.adjust(value_adjust, 0, -button_size.width(), 0); auto text = option.fontMetrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, rect.width()); @@ -389,10 +418,11 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto item = (SignalModel::Item *)index.internalPointer(); if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Offset || - item->type == SignalModel::Item::Factor || item->type == SignalModel::Item::Min || item->type == SignalModel::Item::Max) { + item->type == SignalModel::Item::Factor || item->type == SignalModel::Item::MultiplexValue || + item->type == SignalModel::Item::Min || item->type == SignalModel::Item::Max) { QLineEdit *e = new QLineEdit(parent); e->setFrame(false); - e->setValidator(index.row() == 0 ? name_validator : double_validator); + e->setValidator(item->type == SignalModel::Item::Name ? name_validator : double_validator); if (item->type == SignalModel::Item::Name) { QCompleter *completer = new QCompleter(dbc()->signalNames()); @@ -407,6 +437,15 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie spin->setFrame(false); spin->setRange(1, 64); return spin; + } else if (item->type == SignalModel::Item::SignalType) { + QComboBox *c = new QComboBox(parent); + c->addItem(signalTypeToString(cabana::Signal::Type::Normal), (int)cabana::Signal::Type::Normal); + if (!dbc()->msg(((SignalModel *)index.model())->msg_id)->multiplexor) { + c->addItem(signalTypeToString(cabana::Signal::Type::Multiplexor), (int)cabana::Signal::Type::Multiplexor); + } else if (item->sig->type != cabana::Signal::Type::Multiplexor) { + c->addItem(signalTypeToString(cabana::Signal::Type::Multiplexed), (int)cabana::Signal::Type::Multiplexed); + } + return c; } else if (item->type == SignalModel::Item::Desc) { ValueDescriptionDlg dlg(item->sig->val_desc, parent); dlg.setWindowTitle(item->sig->name); @@ -418,6 +457,15 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie return QStyledItemDelegate::createEditor(parent, option, index); } +void SignalItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { + auto item = (SignalModel::Item *)index.internalPointer(); + if (item->type == SignalModel::Item::SignalType) { + model->setData(index, ((QComboBox*)editor)->currentData().toInt()); + return; + } + QStyledItemDelegate::setModelData(editor, model, index); +} + // SignalView SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QFrame(parent) { @@ -456,10 +504,14 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), tree->setHeaderHidden(true); tree->setMouseTracking(true); tree->setExpandsOnDoubleClick(false); + tree->setEditTriggers(QAbstractItemView::AllEditTriggers); tree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); tree->header()->setStretchLastSection(true); tree->setMinimumHeight(300); - tree->setStyleSheet("QSpinBox{background-color:white;border:none;} QLineEdit{background-color:white;}"); + + // Use a distinctive background for the whole row containing a QSpinBox or QLineEdit + QString nodeBgColor = palette().color(QPalette::AlternateBase).name(QColor::HexArgb); + tree->setStyleSheet(QString("QSpinBox{background-color:%1;border:none;} QLineEdit{background-color:%1;}").arg(nodeBgColor)); QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); @@ -477,10 +529,10 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged); QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged); QObject::connect(dbc(), &DBCManager::signalAdded, [this](MessageId id, const cabana::Signal *sig) { selectSignal(sig); }); - QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState); QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalView::handleSignalUpdated); QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); }); QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); }); + QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState); setWhatsThis(tr(R"( Signal view
@@ -594,8 +646,10 @@ void SignalView::updateState(const QHash *msgs) { if (model->rowCount() == 0 || (msgs && !msgs->contains(model->msg_id)) || last_msg.dat.size() == 0) return; for (auto item : model->root->children) { - double value = get_raw_value((uint8_t *)last_msg.dat.constData(), last_msg.dat.size(), *item->sig); - item->sig_val = item->sig->formatValue(value); + double value = 0; + if (item->sig->getValue((uint8_t *)last_msg.dat.constData(), last_msg.dat.size(), &value)) { + item->sig_val = item->sig->formatValue(value); + } max_value_width = std::max(max_value_width, fontMetrics().width(item->sig_val)); } @@ -635,11 +689,6 @@ void SignalView::resizeEvent(QResizeEvent* event) { QFrame::resizeEvent(event); } -void SignalView::leaveEvent(QEvent *event) { - emit highlight(nullptr); - QWidget::leaveEvent(event); -} - // ValueDescriptionDlg ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions, QWidget *parent) : QDialog(parent) { @@ -662,7 +711,7 @@ ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions, Q int row = 0; for (auto &[val, desc] : descriptions) { - table->setItem(row, 0, new QTableWidgetItem(val)); + table->setItem(row, 0, new QTableWidgetItem(QString::number(val))); table->setItem(row, 1, new QTableWidgetItem(desc)); ++row; } @@ -692,7 +741,7 @@ void ValueDescriptionDlg::save() { QString val = table->item(i, 0)->text().trimmed(); QString desc = table->item(i, 1)->text().trimmed(); if (!val.isEmpty() && !desc.isEmpty()) { - val_desc.push_back({val, desc}); + val_desc.push_back({val.toDouble(), desc}); } } QDialog::accept(); @@ -702,7 +751,7 @@ QWidget *ValueDescriptionDlg::Delegate::createEditor(QWidget *parent, const QSty QLineEdit *edit = new QLineEdit(parent); edit->setFrame(false); if (index.column() == 0) { - edit->setValidator(new QIntValidator(edit)); + edit->setValidator(new DoubleValidator(parent)); } return edit; } diff --git a/tools/cabana/signalview.h b/tools/cabana/signalview.h index 02741234a6..eb268e517c 100644 --- a/tools/cabana/signalview.h +++ b/tools/cabana/signalview.h @@ -15,7 +15,7 @@ class SignalModel : public QAbstractItemModel { Q_OBJECT public: struct Item { - enum Type {Root, Sig, Name, Size, Endian, Signed, Offset, Factor, ExtraInfo, Unit, Comment, Min, Max, Desc }; + enum Type {Root, Sig, Name, Size, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc }; ~Item() { qDeleteAll(children); } inline int row() { return parent->children.indexOf(this); } @@ -85,8 +85,8 @@ public: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; QValidator *name_validator, *double_validator; QFont label_font, minmax_font; @@ -113,7 +113,6 @@ signals: private: void rowsChanged(); - void leaveEvent(QEvent *event) override; void resizeEvent(QResizeEvent* event) override; void updateToolBar(); void setSparklineRange(int value); @@ -131,6 +130,10 @@ private: // Bypass the slow call to QTreeView::dataChanged. QAbstractItemView::dataChanged(topLeft, bottomRight, roles); } + void leaveEvent(QEvent *event) override { + emit ((SignalView *)parentWidget())->highlight(nullptr); + QTreeView::leaveEvent(event); + } }; int max_value_width = 0; TreeView *tree; diff --git a/tools/cabana/streams/abstractstream.cc b/tools/cabana/streams/abstractstream.cc index 701ecbb258..059215b364 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/tools/cabana/streams/abstractstream.cc @@ -4,10 +4,20 @@ AbstractStream *can = nullptr; -AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { - can = this; - new_msgs = std::make_unique>(); +StreamNotifier *StreamNotifier::instance() { + static StreamNotifier notifier; + return ¬ifier; +} + +AbstractStream::AbstractStream(QObject *parent) : new_msgs(new QHash()), QObject(parent) { + assert(parent != nullptr); QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo); + QObject::connect(this, &AbstractStream::streamStarted, [this]() { + emit StreamNotifier::instance()->changingStream(); + delete can; + can = this; + emit StreamNotifier::instance()->streamStarted(); + }); } void AbstractStream::updateMessages(QHash *messages) { @@ -37,8 +47,7 @@ void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t bool AbstractStream::postEvents() { // delay posting CAN message if UI thread is busy - if (!processing) { - processing = true; + if (processing.exchange(true) == false) { for (auto it = new_msgs->begin(); it != new_msgs->end(); ++it) { it.value() = all_msgs[it.key()]; } @@ -171,7 +180,7 @@ void CanData::compute(const char *can_data, const int size, double current_sec, dat.resize(size); bit_change_counts.resize(size); colors = QVector(size, QColor(0, 0, 0, 0)); - last_change_t = QVector(size, ts); + last_change_t.assign(size, ts); last_delta.resize(size); same_delta_counter.resize(size); } else { diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 9b317b3ed6..78610fc657 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -20,10 +20,10 @@ struct CanData { double freq = 0; QByteArray dat; QVector colors; - QVector last_change_t; - QVector> bit_change_counts; - QVector last_delta; - QVector same_delta_counter; + std::vector last_change_t; + std::vector> bit_change_counts; + std::vector last_delta; + std::vector same_delta_counter; }; struct CanEvent { @@ -40,6 +40,7 @@ class AbstractStream : public QObject { public: AbstractStream(QObject *parent); virtual ~AbstractStream() {}; + virtual void start() = 0; inline bool liveStreaming() const { return route() == nullptr; } virtual void seekTo(double ts) {} virtual QString routeName() const = 0; @@ -90,7 +91,6 @@ protected: }; class AbstractOpenStreamWidget : public QWidget { - Q_OBJECT public: AbstractOpenStreamWidget(AbstractStream **stream, QWidget *parent = nullptr) : stream(stream), QWidget(parent) {} virtual bool open() = 0; @@ -100,5 +100,24 @@ protected: AbstractStream **stream = nullptr; }; +class DummyStream : public AbstractStream { + Q_OBJECT +public: + DummyStream(QObject *parent) : AbstractStream(parent) {} + QString routeName() const override { return tr("No Stream"); } + void start() override { emit streamStarted(); } + double currentSec() const override { return 0; } +}; + +class StreamNotifier : public QObject { + Q_OBJECT +public: + StreamNotifier(QObject *parent = nullptr) : QObject(parent) {} + static StreamNotifier* instance(); +signals: + void streamStarted(); + void changingStream(); +}; + // A global pointer referring to the unique AbstractStream object extern AbstractStream *can; diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc index 5bbe527773..5631f64d68 100644 --- a/tools/cabana/streams/devicestream.cc +++ b/tools/cabana/streams/devicestream.cc @@ -9,13 +9,10 @@ // DeviceStream DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { - startStreamThread(); } void DeviceStream::streamThread() { - if (!zmq_address.isEmpty()) { - setenv("ZMQ", "1", 1); - } + zmq_address.isEmpty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); std::unique_ptr context(Context::create()); std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc index 6546860089..91c88c97ca 100644 --- a/tools/cabana/streams/livestream.cc +++ b/tools/cabana/streams/livestream.cc @@ -1,12 +1,30 @@ #include "tools/cabana/streams/livestream.h" -#include +struct LiveStream::Logger { + Logger() : start_ts(seconds_since_epoch()), segment_num(-1) {} + + void write(const char *data, const size_t size) { + int n = (seconds_since_epoch() - start_ts) / 60.0; + if (std::exchange(segment_num, n) != segment_num) { + QString dir = QString("%1/%2--%3") + .arg(settings.log_path) + .arg(QDateTime::fromSecsSinceEpoch(start_ts).toString("yyyy-MM-dd--hh-mm-ss")) + .arg(n); + util::create_directories(dir.toStdString(), 0755); + fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out)); + } + + fs->write(data, size); + } + + std::unique_ptr fs; + int segment_num; + uint64_t start_ts; +}; LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) { if (settings.log_livestream) { - std::string path = (settings.log_path + "/" + QDateTime::currentDateTime().toString("yyyy-MM-dd--hh-mm-ss") + "--0").toStdString(); - util::create_directories(path, 0755); - fs.reset(new std::ofstream(path + "/rlog", std::ios::binary | std::ios::out)); + logger = std::make_unique(); } stream_thread = new QThread(this); @@ -21,11 +39,9 @@ void LiveStream::startUpdateTimer() { timer_id = update_timer.timerId(); } -void LiveStream::startStreamThread() { - // delay the start of the thread to avoid calling startStreamThread - // in the constructor when other classes' slots have not been connected to - // the signals of the livestream. - QTimer::singleShot(0, [this]() { stream_thread->start(); }); +void LiveStream::start() { + emit streamStarted(); + stream_thread->start(); startUpdateTimer(); } @@ -38,8 +54,8 @@ LiveStream::~LiveStream() { // called in streamThread void LiveStream::handleEvent(const char *data, const size_t size) { - if (fs) { - fs->write(data, size); + if (logger) { + logger->write(data, size); } std::lock_guard lk(lock); @@ -71,7 +87,6 @@ void LiveStream::updateEvents() { if (first_update_ts == 0) { first_update_ts = nanos_since_boot(); first_event_ts = current_event_ts = all_events_.back()->mono_time; - emit streamStarted(); } if (paused_ || prev_speed != speed_) { diff --git a/tools/cabana/streams/livestream.h b/tools/cabana/streams/livestream.h index 87ba7a0133..b4816d090f 100644 --- a/tools/cabana/streams/livestream.h +++ b/tools/cabana/streams/livestream.h @@ -10,6 +10,7 @@ class LiveStream : public AbstractStream { public: LiveStream(QObject *parent); virtual ~LiveStream(); + void start() override; inline double routeStartTime() const override { return begin_event_ts / 1e9; } inline double currentSec() const override { return (current_event_ts - begin_event_ts) / 1e9; } void setSpeed(float speed) override { speed_ = speed; } @@ -20,7 +21,6 @@ public: protected: virtual void streamThread() = 0; - void startStreamThread(); void handleEvent(const char *data, const size_t size); private: @@ -42,7 +42,6 @@ private: std::vector receivedEvents; std::deque receivedMessages; - std::unique_ptr fs; int timer_id; QBasicTimer update_timer; @@ -53,4 +52,7 @@ private: bool post_last_event = true; double speed_ = 1; bool paused_ = false; + + struct Logger; + std::unique_ptr logger; }; diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc index 5b3bf890e8..275ed84f43 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/tools/cabana/streams/pandastream.cc @@ -20,7 +20,6 @@ PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(co if (!connect()) { throw std::runtime_error("Failed to connect to panda"); } - startStreamThread(); } bool PandaStream::connect() { diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 46103764be..2e580c0f0d 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -6,18 +6,17 @@ #include #include -#include "common/prefix.h" - ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) { + unsetenv("ZMQ"); + // TODO: Remove when OpenpilotPrefix supports ZMQ +#ifndef __APPLE__ + op_prefix = std::make_unique(); +#endif QObject::connect(&settings, &Settings::changed, [this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); }); } -ReplayStream::~ReplayStream() { - if (replay) replay->stop(); -} - static bool event_filter(const Event *e, void *opaque) { return ((ReplayStream *)opaque)->eventFilter(e); } @@ -33,17 +32,17 @@ void ReplayStream::mergeSegments() { } bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags) { - replay.reset(new Replay(route, {"can", "roadEncodeIdx", "wideRoadEncodeIdx", "carParams"}, {}, nullptr, replay_flags, data_dir, this)); + replay.reset(new Replay(route, {"can", "roadEncodeIdx", "wideRoadEncodeIdx", "carParams"}, {}, {}, nullptr, replay_flags, data_dir, this)); replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->installEventFilter(event_filter, this); QObject::connect(replay.get(), &Replay::seekedTo, this, &AbstractStream::seekedTo); - QObject::connect(replay.get(), &Replay::streamStarted, this, &AbstractStream::streamStarted); QObject::connect(replay.get(), &Replay::segmentsMerged, this, &ReplayStream::mergeSegments); - if (replay->load()) { - replay->start(); - return true; - } - return false; + return replay->load(); +} + +void ReplayStream::start() { + emit streamStarted(); + replay->start(); } bool ReplayStream::eventFilter(const Event *event) { @@ -56,11 +55,12 @@ bool ReplayStream::eventFilter(const Event *event) { const auto dat = c.getDat(); updateEvent(id, current_sec, (const uint8_t*)dat.begin(), dat.size()); } - double ts = millis_since_boot(); - if ((ts - prev_update_ts) > (1000.0 / settings.fps)) { - if (postEvents()) { - prev_update_ts = ts; - } + } + + double ts = millis_since_boot(); + if ((ts - prev_update_ts) > (1000.0 / settings.fps)) { + if (postEvents()) { + prev_update_ts = ts; } } return true; @@ -78,8 +78,6 @@ AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) { // OpenReplayWidget -static std::unique_ptr op_prefix; - OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { // TODO: get route list from api.comma.ai QGridLayout *grid_layout = new QGridLayout(); @@ -118,26 +116,17 @@ bool OpenReplayWidget::open() { route = route.mid(idx + 1); } - bool ret = false; bool is_valid_format = Route::parseRoute(route).str.size() > 0; if (!is_valid_format) { QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); } else { - // TODO: Remove when OpenpilotPrefix supports ZMQ -#ifndef __APPLE__ - op_prefix.reset(new OpenpilotPrefix()); -#endif uint32_t flags[] = {REPLAY_FLAG_NO_VIPC, REPLAY_FLAG_NONE, REPLAY_FLAG_ECAM, REPLAY_FLAG_DCAM, REPLAY_FLAG_QCAMERA}; - ReplayStream *replay_stream = *stream ? (ReplayStream *)*stream : new ReplayStream(qApp); - ret = replay_stream->loadRoute(route, data_dir, flags[choose_video_cb->currentIndex()]); - if (!ret) { - if (replay_stream != *stream) { - delete replay_stream; - } - QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); + auto replay_stream = std::make_unique(qApp); + if (replay_stream->loadRoute(route, data_dir, flags[choose_video_cb->currentIndex()])) { + *stream = replay_stream.release(); } else { - *stream = replay_stream; + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); } } - return ret; + return *stream != nullptr; } diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index aefc763d20..5087de0329 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -1,5 +1,6 @@ #pragma once +#include "common/prefix.h" #include "tools/cabana/streams/abstractstream.h" class ReplayStream : public AbstractStream { @@ -7,10 +8,10 @@ class ReplayStream : public AbstractStream { public: ReplayStream(QObject *parent); - ~ReplayStream(); + void start() override; bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE); bool eventFilter(const Event *event); - void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }; + void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); } inline QString routeName() const override { return replay->route()->name(); } inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); } double totalSeconds() const override { return replay->totalSeconds(); } @@ -29,6 +30,7 @@ private: void mergeSegments(); std::unique_ptr replay = nullptr; std::set processed_segments; + std::unique_ptr op_prefix; }; class OpenReplayWidget : public AbstractOpenStreamWidget { diff --git a/tools/cabana/streamselector.cc b/tools/cabana/streamselector.cc index bfd6ff24d9..6da44ecd1a 100644 --- a/tools/cabana/streamselector.cc +++ b/tools/cabana/streamselector.cc @@ -6,13 +6,19 @@ #include #include -StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { +#include "tools/cabana/streams/devicestream.h" +#include "tools/cabana/streams/pandastream.h" +#include "tools/cabana/streams/replaystream.h" + +StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Open stream")); QVBoxLayout *main_layout = new QVBoxLayout(this); + QWidget *w = new QWidget(this); + QVBoxLayout *layout = new QVBoxLayout(w); tab = new QTabWidget(this); tab->setTabBarAutoHide(true); - main_layout->addWidget(tab); + layout->addWidget(tab); QHBoxLayout *dbc_layout = new QHBoxLayout(); dbc_file = new QLineEdit(this); @@ -22,20 +28,29 @@ StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { dbc_layout->addWidget(new QLabel(tr("dbc File"))); dbc_layout->addWidget(dbc_file); dbc_layout->addWidget(file_btn); - main_layout->addLayout(dbc_layout); + layout->addLayout(dbc_layout); QFrame *line = new QFrame(this); line->setFrameStyle(QFrame::HLine | QFrame::Sunken); - main_layout->addWidget(line); + layout->addWidget(line); + main_layout->addWidget(w); auto btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); main_layout->addWidget(btn_box); + addStreamWidget(ReplayStream::widget(stream)); + addStreamWidget(PandaStream::widget(stream)); + addStreamWidget(DeviceStream::widget(stream)); + QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() { - success = ((AbstractOpenStreamWidget *)tab->currentWidget())->open(); - if (success) { + btn_box->button(QDialogButtonBox::Open)->setEnabled(false); + w->setEnabled(false); + if (((AbstractOpenStreamWidget *)tab->currentWidget())->open()) { accept(); + } else { + btn_box->button(QDialogButtonBox::Open)->setEnabled(true); + w->setEnabled(true); } }); QObject::connect(file_btn, &QPushButton::clicked, [this]() { diff --git a/tools/cabana/streamselector.h b/tools/cabana/streamselector.h index ecd0e8530c..19b438d55a 100644 --- a/tools/cabana/streamselector.h +++ b/tools/cabana/streamselector.h @@ -10,13 +10,11 @@ class StreamSelector : public QDialog { Q_OBJECT public: - StreamSelector(QWidget *parent = nullptr); + StreamSelector(AbstractStream **stream, QWidget *parent = nullptr); void addStreamWidget(AbstractOpenStreamWidget *w); QString dbcFile() const { return dbc_file->text(); } - inline bool failed() const { return !success; } private: QLineEdit *dbc_file; QTabWidget *tab; - bool success = true; }; diff --git a/tools/cabana/tests/test_cabana.cc b/tools/cabana/tests/test_cabana.cc index a3921d727b..8b2a81e4b1 100644 --- a/tools/cabana/tests/test_cabana.cc +++ b/tools/cabana/tests/test_cabana.cc @@ -10,13 +10,13 @@ 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("DBCFile::generateDBC") { - QString fn = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, "toyota_new_mc_pt_generated"); + QString fn = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, "tesla_can"); DBCFile dbc_origin(fn); DBCFile dbc_from_generated("", dbc_origin.generateDBC()); REQUIRE(dbc_origin.msgCount() == dbc_from_generated.msgCount()); - auto msgs = dbc_origin.getMessages(); - auto new_msgs = dbc_from_generated.getMessages(); + auto &msgs = dbc_origin.getMessages(); + auto &new_msgs = dbc_from_generated.getMessages(); for (auto &[id, m] : msgs) { auto &new_m = new_msgs.at(id); REQUIRE(m.name == new_m.name); @@ -69,3 +69,58 @@ TEST_CASE("Parse can messages") { } } } + +TEST_CASE("Parse dbc") { + QString content = R"( +BO_ 160 message_1: 8 XXX + SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX + SG_ signal_2 : 12|1@1+ (1.0,0.0) [0.0|1] "" XXX + +BO_ 162 message_1: 8 XXX + SG_ signal_1 M : 0|12@1+ (1,0) [0|4095] "unit" XXX + SG_ signal_2 M4 : 12|1@1+ (1.0,0.0) [0.0|1] "" XXX + +VAL_ 160 signal_1 0 "disabled" 1.2 "initializing" 2 "fault"; + +CM_ BO_ 160 "message comment" ; +CM_ SG_ 160 signal_1 "signal comment"; +CM_ SG_ 160 signal_2 "multiple line comment +1 +2 +";)"; + + DBCFile file("", content); + auto msg = file.msg(160); + REQUIRE(msg != nullptr); + REQUIRE(msg->name == "message_1"); + REQUIRE(msg->size == 8); + REQUIRE(msg->comment == "message comment"); + REQUIRE(msg->sigs.size() == 2); + REQUIRE(file.msg("message_1") != nullptr); + + auto sig_1 = msg->sigs[0]; + REQUIRE(sig_1->name == "signal_1"); + REQUIRE(sig_1->start_bit == 0); + REQUIRE(sig_1->size == 12); + REQUIRE(sig_1->min == 0); + REQUIRE(sig_1->max == 4095); + REQUIRE(sig_1->unit == "unit"); + REQUIRE(sig_1->comment == "signal comment"); + REQUIRE(sig_1->val_desc.size() == 3); + REQUIRE(sig_1->val_desc[0] == std::pair{0, "disabled"}); + REQUIRE(sig_1->val_desc[1] == std::pair{1.2, "initializing"}); + REQUIRE(sig_1->val_desc[2] == std::pair{2, "fault"}); + + auto &sig_2 = msg->sigs[1]; + REQUIRE(sig_2->comment == "multiple line comment\n1\n2"); + + // multiplexed signals + msg = file.msg(162); + REQUIRE(msg != nullptr); + REQUIRE(msg->sigs.size() == 2); + REQUIRE(msg->sigs[0]->type == cabana::Signal::Type::Multiplexor); + REQUIRE(msg->sigs[1]->type == cabana::Signal::Type::Multiplexed); + REQUIRE(msg->sigs[1]->multiplex_value == 4); + REQUIRE(msg->sigs[1]->start_bit == 12); + REQUIRE(msg->sigs[1]->size == 1); +} diff --git a/tools/cabana/tools/findsignal.cc b/tools/cabana/tools/findsignal.cc index bc969356d4..b7d2f4c015 100644 --- a/tools/cabana/tools/findsignal.cc +++ b/tools/cabana/tools/findsignal.cc @@ -1,6 +1,5 @@ #include "tools/cabana/tools/findsignal.h" -#include #include #include #include @@ -138,8 +137,7 @@ FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags( undo_btn->setEnabled(false); reset_btn->setEnabled(false); - auto double_validator = new QDoubleValidator(this); - double_validator->setLocale(QLocale::C); + auto double_validator = new DoubleValidator(this); for (auto edit : {value1, value2, factor_edit, offset_edit, first_time_edit, last_time_edit}) { edit->setValidator(double_validator); } @@ -262,7 +260,7 @@ void FindSignalDlg::customMenuRequested(const QPoint &pos) { auto &s = model->filtered_signals[index.row()]; auto msg = dbc()->msg(s.id); if (!msg) { - UndoStack::push(new EditMsgCommand(s.id, dbc()->newMsgName(s.id), can->lastMessage(s.id).dat.size())); + UndoStack::push(new EditMsgCommand(s.id, dbc()->newMsgName(s.id), can->lastMessage(s.id).dat.size(), "")); msg = dbc()->msg(s.id); } s.sig.name = dbc()->newSignalName(s.id); diff --git a/tools/cabana/tools/findsimilarbits.cc b/tools/cabana/tools/findsimilarbits.cc index 9d81ebd5d0..72429104a5 100644 --- a/tools/cabana/tools/findsimilarbits.cc +++ b/tools/cabana/tools/findsimilarbits.cc @@ -20,22 +20,16 @@ FindSimilarBitsDlg::FindSimilarBitsDlg(QWidget *parent) : QDialog(parent, Qt::Wi QHBoxLayout *src_layout = new QHBoxLayout(); src_bus_combo = new QComboBox(this); find_bus_combo = new QComboBox(this); - SourceSet bus_set; - for (auto it = can->last_msgs.begin(); it != can->last_msgs.end(); ++it) { - bus_set << it.key().source; - } for (auto cb : {src_bus_combo, find_bus_combo}) { - for (uint8_t bus : bus_set) { + for (uint8_t bus : can->sources) { cb->addItem(QString::number(bus), bus); } - cb->model()->sort(0); - cb->setCurrentIndex(0); } msg_cb = new QComboBox(this); // TODO: update when src_bus_combo changes - for (auto &[id, msg] : dbc()->getMessages(0)) { - msg_cb->addItem(msg.name, id.address); + for (auto &[address, msg] : dbc()->getMessages(-1)) { + msg_cb->addItem(msg.name, address); } msg_cb->model()->sort(0); msg_cb->setCurrentIndex(0); diff --git a/tools/cabana/util.cc b/tools/cabana/util.cc index d95d6ac525..3cf328e1e0 100644 --- a/tools/cabana/util.cc +++ b/tools/cabana/util.cc @@ -1,7 +1,9 @@ #include "tools/cabana/util.h" +#include #include #include +#include #include #include #include @@ -82,17 +84,6 @@ QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const Q return size; } -bool MessageBytesDelegate::helpEvent(QHelpEvent *e, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { - if (e->type() == QEvent::ToolTip && index.column() == 0) { - if (view->visualRect(index).width() < QStyledItemDelegate::sizeHint(option, index).width()) { - QToolTip::showText(e->globalPos(), index.data(Qt::DisplayRole).toString(), view); - return true; - } - } - QToolTip::hideText(); - return false; -} - void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto data = index.data(BytesRole); if (!data.isValid()) { @@ -105,7 +96,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin); int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin); if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.highlight()); + painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); } const QPoint pt{option.rect.left() + h_margin, option.rect.top() + v_margin}; @@ -154,17 +145,6 @@ void TabBar::closeTabClicked() { } } -QColor getColor(const cabana::Signal *sig) { - float h = 19 * (float)sig->lsb / 64.0; - h = fmod(h, 1.0); - - size_t hash = qHash(sig->name); - float s = 0.25 + 0.25 * (float)(hash & 0xff) / 255.0; - float v = 0.75 + 0.25 * (float)((hash >> 8) & 0xff) / 255.0; - - return QColor::fromHsvF(h, s, v); -} - NameValidator::NameValidator(QObject *parent) : QRegExpValidator(QRegExp("^(\\w+)"), parent) {} QValidator::State NameValidator::validate(QString &input, int &pos) const { @@ -172,6 +152,13 @@ QValidator::State NameValidator::validate(QString &input, int &pos) const { return QRegExpValidator::validate(input, pos); } +DoubleValidator::DoubleValidator(QObject *parent) : QDoubleValidator(parent) { + // Match locale of QString::toDouble() instead of system + QLocale locale(QLocale::C); + locale.setNumberOptions(QLocale::RejectGroupSeparator); + setLocale(locale); +} + namespace utils { QPixmap icon(const QString &id) { bool dark_theme = settings.theme == DARK_THEME; @@ -240,10 +227,16 @@ QString toHex(uint8_t byte) { int num_decimals(double num) { const QString string = QString::number(num); - const QStringList split = string.split('.'); - if (split.size() == 1) { - return 0; - } else { - return split[1].size(); - } + auto dot_pos = string.indexOf('.'); + return dot_pos == -1 ? 0 : string.size() - dot_pos - 1; +} + +QString signalToolTip(const cabana::Signal *sig) { + return QObject::tr(R"( + %1
+ Start Bit: %2 Size: %3
+ MSB: %4 LSB: %5
+ Little Endian: %6 Signed: %7
+ )").arg(sig->name).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb) + .arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N"); } diff --git a/tools/cabana/util.h b/tools/cabana/util.h index dcf1ac0cbf..74724dfb33 100644 --- a/tools/cabana/util.h +++ b/tools/cabana/util.h @@ -2,11 +2,10 @@ #include -#include #include #include #include -#include +#include #include #include #include @@ -21,7 +20,7 @@ class LogSlider : public QSlider { Q_OBJECT public: - LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : factor(factor), QSlider(orientation, parent) {}; + LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : factor(factor), QSlider(orientation, parent) {} void setRange(double min, double max) { log_min = factor * std::log10(min); @@ -67,7 +66,6 @@ public: MessageBytesDelegate(QObject *parent, bool multiple_lines = false); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override; void setMultipleLines(bool v); int widthForBytes(int n) const; bool multipleLines() const { return multiple_lines; } @@ -81,16 +79,20 @@ private: inline QString toHex(const QByteArray &dat) { return dat.toHex(' ').toUpper(); } QString toHex(uint8_t byte); -QColor getColor(const cabana::Signal *sig); class NameValidator : public QRegExpValidator { Q_OBJECT - public: NameValidator(QObject *parent=nullptr); QValidator::State validate(QString &input, int &pos) const override; }; +class DoubleValidator : public QDoubleValidator { + Q_OBJECT +public: + DoubleValidator(QObject *parent = nullptr); +}; + namespace utils { QPixmap icon(const QString &id); void setTheme(int theme); @@ -134,3 +136,4 @@ private: }; int num_decimals(double num); +QString signalToolTip(const cabana::Signal *sig); diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 846a83c24c..2af1db4635 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -1,10 +1,12 @@ #include "tools/cabana/videowidget.h" #include +#include #include #include #include #include +#include #include #include @@ -20,18 +22,6 @@ static const QColor timeline_colors[] = { [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), }; -bool sortTimelineBasedOnEventPriority(const std::tuple &left, const std::tuple &right){ - const static std::map timelinePriority = { - { TimelineType::None, 0 }, - { TimelineType::Engaged, 10 }, - { TimelineType::AlertInfo, 20 }, - { TimelineType::AlertWarning, 30 }, - { TimelineType::AlertCritical, 40 }, - { TimelineType::UserFlag, 35 } - }; - return timelinePriority.at(std::get<2>(left)) < timelinePriority.at(std::get<2>(right)); -} - VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { setFrameStyle(QFrame::StyledPanel | QFrame::Plain); auto main_layout = new QVBoxLayout(this); @@ -100,7 +90,7 @@ QWidget *VideoWidget::createCameraWidget() { l->addLayout(stacked); // slider controls - slider_layout = new QHBoxLayout(); + auto slider_layout = new QHBoxLayout(); time_label = new QLabel("00:00"); slider_layout->addWidget(time_label); @@ -111,12 +101,13 @@ QWidget *VideoWidget::createCameraWidget() { end_time_label = new QLabel(this); slider_layout->addWidget(end_time_label); l->addLayout(slider_layout); + + setMaximumTime(can->totalSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->value() / 1000.0); }); QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(utils::formatSeconds(value / 1000)); }); QObject::connect(slider, &Slider::updateMaximumTime, this, &VideoWidget::setMaximumTime); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(can, &AbstractStream::updated, this, &VideoWidget::updateState); - QObject::connect(can, &AbstractStream::streamStarted, [this]() { setMaximumTime(can->totalSeconds()); }); return w; } @@ -157,43 +148,40 @@ void VideoWidget::updatePlayBtnState() { } // Slider -Slider::Slider(QWidget *parent) : timer(this), thumbnail_label(parent), QSlider(Qt::Horizontal, parent) { - timer.callOnTimeout([this]() { +Slider::Slider(QWidget *parent) : thumbnail_label(parent), QSlider(Qt::Horizontal, parent) { + setMouseTracking(true); + auto timer = new QTimer(this); + timer->callOnTimeout([this]() { timeline = can->getTimeline(); - std::sort(timeline.begin(), timeline.end(), sortTimelineBasedOnEventPriority); + std::sort(timeline.begin(), timeline.end(), [](auto &l, auto &r) { return std::get<2>(l) < std::get<2>(r); }); update(); }); - setMouseTracking(true); - QObject::connect(can, &AbstractStream::streamStarted, this, &Slider::streamStarted); + timer->start(2000); + QObject::connect(can, &AbstractStream::eventsMerged, [this]() { + if (!qlog_future) { + qlog_future = std::make_unique>(QtConcurrent::run(this, &Slider::parseQLog)); + } + }); } Slider::~Slider() { - abort_load_thumbnail = true; - thumnail_future.waitForFinished(); + abort_parse_qlog = true; + if (qlog_future) { + qlog_future->waitForFinished(); + } } -void Slider::streamStarted() { - abort_load_thumbnail = true; - thumnail_future.waitForFinished(); - abort_load_thumbnail = false; - thumbnails.clear(); - timeline.clear(); - timer.start(2000); - thumnail_future = QtConcurrent::run(this, &Slider::loadThumbnails); -} - -void Slider::loadThumbnails() { +void Slider::parseQLog() { const auto &segments = can->route()->segments(); - double max_time = 0; - for (auto it = segments.rbegin(); it != segments.rend() && !abort_load_thumbnail; ++it) { + for (auto it = segments.rbegin(); it != segments.rend() && !abort_parse_qlog; ++it) { LogReader log; std::string qlog = it->second.qlog.toStdString(); - if (!qlog.empty() && log.load(qlog, &abort_load_thumbnail, {cereal::Event::Which::THUMBNAIL, cereal::Event::Which::CONTROLS_STATE}, true, 0, 3)) { - if (max_time == 0 && !log.events.empty()) { - max_time = (*(log.events.rbegin()))->mono_time / 1e9 - can->routeStartTime(); + if (!qlog.empty() && log.load(qlog, &abort_parse_qlog, {cereal::Event::Which::THUMBNAIL, cereal::Event::Which::CONTROLS_STATE}, true, 0, 3)) { + if (it == segments.rbegin() && !log.events.empty()) { + double max_time = (*(log.events.rbegin()))->mono_time / 1e9 - can->routeStartTime(); emit updateMaximumTime(max_time); } - for (auto ev = log.events.cbegin(); ev != log.events.cend() && !abort_load_thumbnail; ++ev) { + for (auto ev = log.events.cbegin(); ev != log.events.cend() && !abort_parse_qlog; ++ev) { if ((*ev)->which == cereal::Event::Which::THUMBNAIL) { auto thumb = (*ev)->event.getThumbnail(); auto data = thumb.getThumbnail(); @@ -318,9 +306,7 @@ void InfoLabel::showAlert(const AlertInfo &alert) { alert_info = alert; pixmap = {}; setVisible(!alert_info.text1.isEmpty()); - if (isVisible()) { - update(); - } + update(); } void InfoLabel::paintEvent(QPaintEvent *event) { diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index c2ec759919..56edc83809 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -3,12 +3,10 @@ #include #include -#include #include #include #include #include -#include #include "selfdrive/ui/qt/widgets/cameraview.h" #include "tools/cabana/streams/abstractstream.h" @@ -46,19 +44,17 @@ private: bool event(QEvent *event) override; void sliderChange(QAbstractSlider::SliderChange change) override; void paintEvent(QPaintEvent *ev) override; - void streamStarted(); - void loadThumbnails(); + void parseQLog(); double max_sec = 0; int slider_x = -1; std::vector> timeline; std::mutex thumbnail_lock; - std::atomic abort_load_thumbnail = false; + std::atomic abort_parse_qlog = false; QMap thumbnails; std::map alerts; - QFuture thumnail_future; + std::unique_ptr> qlog_future; InfoLabel thumbnail_label; - QTimer timer; friend class VideoWidget; }; @@ -79,7 +75,6 @@ protected: double maximum_time = 0; QLabel *end_time_label; QLabel *time_label; - QHBoxLayout *slider_layout; QPushButton *play_btn; InfoLabel *alert_label; Slider *slider; diff --git a/tools/lib/helpers.py b/tools/lib/helpers.py index efe704b9ec..067b64b6ac 100644 --- a/tools/lib/helpers.py +++ b/tools/lib/helpers.py @@ -1,3 +1,4 @@ +import bz2 import datetime TIME_FMT = "%Y-%m-%d--%H-%M-%S" @@ -13,8 +14,19 @@ class RE: EXPLORER_FILE = r'^(?P{})--(?P[a-z]+\.[a-z0-9]+)$'.format(SEGMENT_NAME) OP_SEGMENT_DIR = r'^(?P{})$'.format(SEGMENT_NAME) + def timestamp_to_datetime(t: str) -> datetime.datetime: """ Convert an openpilot route timestamp to a python datetime """ return datetime.datetime.strptime(t, TIME_FMT) + + +def save_log(dest, log_msgs, compress=True): + dat = b"".join(msg.as_builder().to_bytes() for msg in log_msgs) + + if compress: + dat = bz2.compress(dat) + + with open(dest, "wb") as f: + f.write(dat) diff --git a/tools/plotjuggler/juggle.py b/tools/plotjuggler/juggle.py index 1e592da3b1..e147cf9ce8 100755 --- a/tools/plotjuggler/juggle.py +++ b/tools/plotjuggler/juggle.py @@ -11,10 +11,10 @@ import requests import argparse from common.basedir import BASEDIR -from selfdrive.test.process_replay.compare_logs import save_log from selfdrive.test.openpilotci import get_url from tools.lib.logreader import LogReader from tools.lib.route import Route, SegmentName +from tools.lib.helpers import save_log from urllib.parse import urlparse, parse_qs juggle_dir = os.path.dirname(os.path.realpath(__file__)) diff --git a/tools/plotjuggler/layouts/camera-timings.xml b/tools/plotjuggler/layouts/camera-timings.xml index 5cc1650909..f91cbd3f53 100644 --- a/tools/plotjuggler/layouts/camera-timings.xml +++ b/tools/plotjuggler/layouts/camera-timings.xml @@ -132,7 +132,7 @@ - + diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index e3d5071412..3313e11d40 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -68,20 +68,16 @@ bool LogReader::parse(const std::set &allow, std::atomic words((const capnp::word *)raw_.data(), raw_.size() / sizeof(capnp::word)); while (words.size() > 0 && !(abort && *abort)) { - if (!allow.empty()) { - capnp::FlatArrayMessageReader reader(words); - auto which = reader.getRoot().which(); - if (allow.find(which) == allow.end()) { - words = kj::arrayPtr(reader.getEnd(), words.end()); - continue; - } - } - #ifdef HAS_MEMORY_RESOURCE Event *evt = new (mbr_) Event(words); #else Event *evt = new Event(words); #endif + if (!allow.empty() && allow.find(evt->which) == allow.end()) { + delete evt; + words = kj::arrayPtr(evt->reader.getEnd(), words.end()); + continue; + } // Add encodeIdx packet again as a frame packet for the video stream if (evt->which == cereal::Event::ROAD_ENCODE_IDX || diff --git a/tools/replay/main.cc b/tools/replay/main.cc index 6b624aa1fa..78be2acd0b 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -8,6 +8,7 @@ int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); + const QStringList base_blacklist = {"uiDebug", "userFlag"}; const std::tuple flags[] = { {"dcam", REPLAY_FLAG_DCAM, "load driver camera"}, {"ecam", REPLAY_FLAG_ECAM, "load wide road camera"}, @@ -16,6 +17,8 @@ int main(int argc, char *argv[]) { {"qcam", REPLAY_FLAG_QCAMERA, "load qcamera"}, {"no-hw-decoder", REPLAY_FLAG_NO_HW_DECODER, "disable HW video decoding"}, {"no-vipc", REPLAY_FLAG_NO_VIPC, "do not output video"}, + {"all", REPLAY_FLAG_ALL_SERVICES, "do output all messages including " + base_blacklist.join(", ") + + ". this may causes issues when used along with UI"} }; QCommandLineParser parser; @@ -56,7 +59,7 @@ int main(int argc, char *argv[]) { op_prefix.reset(new OpenpilotPrefix(prefix.toStdString())); } - Replay *replay = new Replay(route, allow, block, nullptr, replay_flags, parser.value("data_dir"), &app); + Replay *replay = new Replay(route, allow, block, base_blacklist, nullptr, replay_flags, parser.value("data_dir"), &app); if (!parser.value("c").isEmpty()) { replay->setSegmentCacheLimit(parser.value("c").toInt()); } diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index b68330f688..28adcffb92 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -10,14 +10,20 @@ #include "system/hardware/hw.h" #include "tools/replay/util.h" -Replay::Replay(QString route, QStringList allow, QStringList block, SubMaster *sm_, uint32_t flags, QString data_dir, QObject *parent) +Replay::Replay(QString route, QStringList allow, QStringList block, QStringList base_blacklist, SubMaster *sm_, uint32_t flags, QString data_dir, QObject *parent) : sm(sm_), flags_(flags), QObject(parent) { std::vector s; auto event_struct = capnp::Schema::from().asStruct(); sockets_.resize(event_struct.getUnionFields().size()); for (const auto &it : services) { + uint16_t which = event_struct.getFieldByName(it.name).getProto().getDiscriminantValue(); + if ((which == cereal::Event::Which::UI_DEBUG || which == cereal::Event::Which::USER_FLAG) && + !(flags & REPLAY_FLAG_ALL_SERVICES) && + !allow.contains(it.name)) { + continue; + } + if ((allow.empty() || allow.contains(it.name)) && !block.contains(it.name)) { - uint16_t which = event_struct.getFieldByName(it.name).getProto().getDiscriminantValue(); sockets_[which] = it.name; if (!allow.empty() || !block.empty()) { allow_list.insert((cereal::Event::Which)which); @@ -30,6 +36,9 @@ Replay::Replay(QString route, QStringList allow, QStringList block, SubMaster *s // the following events are needed for replay to work properly. allow_list.insert(cereal::Event::Which::INIT_DATA); allow_list.insert(cereal::Event::Which::CAR_PARAMS); + if (sockets_[cereal::Event::Which::PANDA_STATES] != nullptr) { + allow_list.insert(cereal::Event::Which::PANDA_STATE_D_E_P_R_E_C_A_T_E_D); + } } qDebug() << "services " << s; @@ -128,8 +137,18 @@ void Replay::seekToFlag(FindFlag flag) { void Replay::buildTimeline() { uint64_t engaged_begin = 0; + bool engaged = false; + + auto alert_status = cereal::ControlsState::AlertStatus::NORMAL; + auto alert_size = cereal::ControlsState::AlertSize::NONE; uint64_t alert_begin = 0; - TimelineType alert_type = TimelineType::None; + std::string alert_type; + + const TimelineType timeline_types[] = { + [(int)cereal::ControlsState::AlertStatus::NORMAL] = TimelineType::AlertInfo, + [(int)cereal::ControlsState::AlertStatus::USER_PROMPT] = TimelineType::AlertWarning, + [(int)cereal::ControlsState::AlertStatus::CRITICAL] = TimelineType::AlertCritical, + }; for (auto it = segments_.cbegin(); it != segments_.cend() && !exit_; ++it) { LogReader log; @@ -141,26 +160,24 @@ void Replay::buildTimeline() { if (e->which == cereal::Event::Which::CONTROLS_STATE) { auto cs = e->event.getControlsState(); - if (!engaged_begin && cs.getEnabled()) { + if (engaged != cs.getEnabled()) { + if (engaged) { + std::lock_guard lk(timeline_lock); + timeline.push_back({toSeconds(engaged_begin), toSeconds(e->mono_time), TimelineType::Engaged}); + } engaged_begin = e->mono_time; - } else if (engaged_begin && !cs.getEnabled()) { - std::lock_guard lk(timeline_lock); - timeline.push_back({toSeconds(engaged_begin), toSeconds(e->mono_time), TimelineType::Engaged}); - engaged_begin = 0; + engaged = cs.getEnabled(); } - if (!alert_begin && cs.getAlertType().size() > 0) { - alert_begin = e->mono_time; - alert_type = TimelineType::AlertInfo; - if (cs.getAlertStatus() != cereal::ControlsState::AlertStatus::NORMAL) { - alert_type = cs.getAlertStatus() == cereal::ControlsState::AlertStatus::USER_PROMPT - ? TimelineType::AlertWarning - : TimelineType::AlertCritical; + if (alert_type != cs.getAlertType().cStr() || alert_status != cs.getAlertStatus()) { + if (!alert_type.empty() && alert_size != cereal::ControlsState::AlertSize::NONE) { + std::lock_guard lk(timeline_lock); + timeline.push_back({toSeconds(alert_begin), toSeconds(e->mono_time), timeline_types[(int)alert_status]}); } - } else if (alert_begin && cs.getAlertType().size() == 0) { - std::lock_guard lk(timeline_lock); - timeline.push_back({toSeconds(alert_begin), toSeconds(e->mono_time), alert_type}); - alert_begin = 0; + alert_begin = e->mono_time; + alert_type = cs.getAlertType().cStr(); + alert_size = cs.getAlertSize(); + alert_status = cs.getAlertStatus(); } } else if (e->which == cereal::Event::Which::USER_FLAG) { std::lock_guard lk(timeline_lock); diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 2bb426361b..b7704132c2 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -22,6 +22,7 @@ enum REPLAY_FLAGS { REPLAY_FLAG_NO_HW_DECODER = 0x0100, REPLAY_FLAG_FULL_SPEED = 0x0200, REPLAY_FLAG_NO_VIPC = 0x0400, + REPLAY_FLAG_ALL_SERVICES = 0x0800, }; enum class FindFlag { @@ -40,7 +41,7 @@ class Replay : public QObject { Q_OBJECT public: - Replay(QString route, QStringList allow, QStringList block, SubMaster *sm = nullptr, + Replay(QString route, QStringList allow, QStringList block, QStringList base_blacklist, SubMaster *sm = nullptr, uint32_t flags = REPLAY_FLAG_NONE, QString data_dir = "", QObject *parent = 0); ~Replay(); bool load(); @@ -67,7 +68,7 @@ public: inline QDateTime currentDateTime() const { return route_->datetime().addSecs(currentSeconds()); } inline uint64_t routeStartTime() const { return route_start_ts_; } inline int toSeconds(uint64_t mono_time) const { return (mono_time - route_start_ts_) / 1e9; } - inline int totalSeconds() const { return segments_.size() * 60; } + inline int totalSeconds() const { return (!segments_.empty()) ? (segments_.rbegin()->first + 1) * 60 : 0; } inline void setSpeed(float speed) { speed_ = speed; } inline float getSpeed() const { return speed_; } inline const std::vector *events() const { return events_.get(); } diff --git a/tools/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc index 30e3c811ee..f0b80269dc 100644 --- a/tools/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -154,7 +154,7 @@ TEST_CASE("Route") { // helper class for unit tests class TestReplay : public Replay { public: - TestReplay(const QString &route, uint8_t flags = REPLAY_FLAG_NO_FILE_CACHE) : Replay(route, {}, {}, nullptr, flags) {} + TestReplay(const QString &route, uint8_t flags = REPLAY_FLAG_NO_FILE_CACHE) : Replay(route, {}, {}, {}, nullptr, flags) {} void test_seek(); void testSeekTo(int seek_to); }; diff --git a/tools/sim/bridge.py b/tools/sim/bridge.py index 7bb12badb2..64446e1115 100755 --- a/tools/sim/bridge.py +++ b/tools/sim/bridge.py @@ -68,13 +68,14 @@ def steer_rate_limit(old, new): class Camerad: - def __init__(self): + def __init__(self, dual_camera): self.frame_road_id = 0 self.frame_wide_id = 0 self.vipc_server = VisionIpcServer("camerad") self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, False, W, H) - self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 5, False, W, H) + if dual_camera: + self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 5, False, W, H) self.vipc_server.start_listener() # set up for pyopencl rgb to yuv conversion @@ -254,7 +255,7 @@ class CarlaBridge: msg.liveCalibration.validBlocks = 20 msg.liveCalibration.rpyCalib = [0.0, 0.0, 0.0] self.params.put("CalibrationParams", msg.to_bytes()) - self.params.put_bool("WideCameraOnly", not arguments.dual_camera) + self.params.put_bool("DisengageOnAccelerator", True) self._args = arguments self._carla_objects = [] @@ -349,14 +350,13 @@ class CarlaBridge: camera.listen(callback) return camera - self._camerad = Camerad() + self._camerad = Camerad(self._args.dual_camera) if self._args.dual_camera: - road_camera = create_camera(fov=40, callback=self._camerad.cam_callback_road) - self._carla_objects.append(road_camera) - - road_wide_camera = create_camera(fov=120, callback=self._camerad.cam_callback_wide_road) # fov bigger than 120 shows unwanted artifacts - self._carla_objects.append(road_wide_camera) + road_wide_camera = create_camera(fov=120, callback=self._camerad.cam_callback_wide_road) # fov bigger than 120 shows unwanted artifacts + self._carla_objects.append(road_wide_camera) + road_camera = create_camera(fov=40, callback=self._camerad.cam_callback_road) + self._carla_objects.append(road_camera) vehicle_state = VehicleState() @@ -545,23 +545,17 @@ if __name__ == "__main__": q: Any = Queue() args = parse_args() - try: - carla_bridge = CarlaBridge(args) - p = carla_bridge.run(q) + carla_bridge = CarlaBridge(args) + p = carla_bridge.run(q) - if args.joystick: - # start input poll for joystick - from tools.sim.lib.manual_ctrl import wheel_poll_thread + if args.joystick: + # start input poll for joystick + from tools.sim.lib.manual_ctrl import wheel_poll_thread - wheel_poll_thread(q) - else: - # start input poll for keyboard - from tools.sim.lib.keyboard_ctrl import keyboard_poll_thread + wheel_poll_thread(q) + else: + # start input poll for keyboard + from tools.sim.lib.keyboard_ctrl import keyboard_poll_thread - keyboard_poll_thread(q) - p.join() - - finally: - # Try cleaning up the wide camera param - # in case users want to use replay after - Params().remove("WideCameraOnly") + keyboard_poll_thread(q) + p.join() diff --git a/tools/sim/launch_openpilot.sh b/tools/sim/launch_openpilot.sh index 2100f05b67..d5922a4819 100755 --- a/tools/sim/launch_openpilot.sh +++ b/tools/sim/launch_openpilot.sh @@ -6,7 +6,7 @@ export SIMULATION="1" export SKIP_FW_QUERY="1" export FINGERPRINT="HONDA CIVIC 2016" -export BLOCK="camerad,loggerd,encoderd,micd" +export BLOCK="camerad,loggerd,encoderd,micd,logmessaged" if [[ "$CI" ]]; then # TODO: offscreen UI should work export BLOCK="${BLOCK},ui" diff --git a/tools/sim/lib/can.py b/tools/sim/lib/can.py index af4339b2e4..fba4dc0e58 100755 --- a/tools/sim/lib/can.py +++ b/tools/sim/lib/can.py @@ -68,6 +68,7 @@ def can_function(pm, speed, angle, idx, cruise_button, is_engaged): # *** cam bus *** msg.append(packer.make_can_msg("STEERING_CONTROL", 2, {})) msg.append(packer.make_can_msg("ACC_HUD", 2, {})) + msg.append(packer.make_can_msg("LKAS_HUD", 2, {})) msg.append(packer.make_can_msg("BRAKE_COMMAND", 2, {})) # *** radar bus *** diff --git a/tools/ssh/README.md b/tools/ssh/README.md index 67b5271a42..588ea71579 100644 --- a/tools/ssh/README.md +++ b/tools/ssh/README.md @@ -24,14 +24,12 @@ The `id_rsa` key in this directory only works while your device is in the setup See the [community wiki](https://github.com/commaai/openpilot/wiki/SSH) for more detailed instructions and information. # Connecting to ssh.comma.ai -SSH into your comma device from anywhere with `ssh.comma.ai`. +SSH into your comma device from anywhere with `ssh.comma.ai`. Requires a [comma prime subscription](https://comma.ai/connect). ## Setup With software version 0.6.1 or newer, enter your GitHub username on your device under Developer Settings. Your GitHub authorized public keys will become your authorized SSH keys for `ssh.comma.ai`. You can add any additional keys in `/system/comma/home/.ssh/authorized_keys.persist`. -Requires [comma SIM with comma prime](https://comma.ai/shop) activated with comma connect, available on iOS and Android. comma two and EON ship with a pre-inserted comma SIM. - ## Recommended .ssh/config With the below SSH configuration, you can type `ssh comma-{dongleid}` to connect to your device through `ssh.comma.ai`.