Merge remote-tracking branch 'origin/master' into ford-vehicle-cfg-lca

pull/31821/head
Cameron Clough 1 year ago
commit d3bdf6c345
  1. 1
      common/params.cc
  2. 22
      common/util.cc
  3. 6
      common/util.h
  4. 5
      docs/CARS.md
  5. 2
      launch_env.sh
  6. 1
      selfdrive/boardd/panda_comms.h
  7. 34
      selfdrive/boardd/spi.cc
  8. 18
      selfdrive/car/__init__.py
  9. 7
      selfdrive/car/card.py
  10. 2
      selfdrive/car/docs_definitions.py
  11. 1
      selfdrive/car/ford/fingerprints.py
  12. 30
      selfdrive/car/ford/tests/print_platform_codes.py
  13. 111
      selfdrive/car/ford/tests/test_ford.py
  14. 75
      selfdrive/car/ford/values.py
  15. 7
      selfdrive/car/honda/fingerprints.py
  16. 1
      selfdrive/car/hyundai/fingerprints.py
  17. 3
      selfdrive/car/hyundai/values.py
  18. 1
      selfdrive/car/mock/interface.py
  19. 3
      selfdrive/car/subaru/values.py
  20. 6
      selfdrive/car/volkswagen/fingerprints.py
  21. 4
      selfdrive/car/volkswagen/interface.py
  22. 9
      selfdrive/car/volkswagen/tests/test_volkswagen.py
  23. 24
      selfdrive/car/volkswagen/values.py
  24. 4
      selfdrive/controls/controlsd.py
  25. 6
      selfdrive/controls/lib/alerts_offroad.json
  26. 2
      selfdrive/debug/check_timings.py
  27. 14
      selfdrive/debug/print_flags.py
  28. 9
      selfdrive/monitoring/dmonitoringd.py
  29. 16
      selfdrive/monitoring/driver_monitor.py
  30. 2
      selfdrive/monitoring/test_monitoring.py
  31. 2
      selfdrive/test/process_replay/ref_commit
  32. 4
      selfdrive/ui/translations/main_ar.ts
  33. 4
      selfdrive/ui/translations/main_de.ts
  34. 4
      selfdrive/ui/translations/main_fr.ts
  35. 4
      selfdrive/ui/translations/main_ja.ts
  36. 4
      selfdrive/ui/translations/main_ko.ts
  37. 4
      selfdrive/ui/translations/main_pt-BR.ts
  38. 4
      selfdrive/ui/translations/main_th.ts
  39. 4
      selfdrive/ui/translations/main_tr.ts
  40. 4
      selfdrive/ui/translations/main_zh-CHS.ts
  41. 4
      selfdrive/ui/translations/main_zh-CHT.ts
  42. 20
      system/hardware/tici/agnos.json
  43. 2
      tools/cabana/SConscript
  44. 20
      tools/cabana/streams/replaystream.cc
  45. 123
      tools/cabana/streams/routes.cc
  46. 26
      tools/cabana/streams/routes.h
  47. 16
      tools/cabana/streamselector.cc
  48. 56
      tools/car_porting/auto_fingerprint.py
  49. 2
      tools/car_porting/examples/subaru_fuzzy_fingerprint.ipynb
  50. 78
      tools/zookeeper/__init__.py
  51. 27
      tools/zookeeper/check_consumption.py
  52. 8
      tools/zookeeper/disable.py
  53. 31
      tools/zookeeper/enable_and_wait.py
  54. 10
      tools/zookeeper/ignition.py
  55. 40
      tools/zookeeper/power_monitor.py
  56. 25
      tools/zookeeper/test_zookeeper.py

@ -172,7 +172,6 @@ std::unordered_map<std::string, uint32_t> keys = {
{"Offroad_CarUnrecognized", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"Offroad_CarUnrecognized", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"Offroad_ConnectivityNeeded", CLEAR_ON_MANAGER_START}, {"Offroad_ConnectivityNeeded", CLEAR_ON_MANAGER_START},
{"Offroad_ConnectivityNeededPrompt", CLEAR_ON_MANAGER_START}, {"Offroad_ConnectivityNeededPrompt", CLEAR_ON_MANAGER_START},
{"Offroad_InvalidTime", CLEAR_ON_MANAGER_START},
{"Offroad_IsTakingSnapshot", CLEAR_ON_MANAGER_START}, {"Offroad_IsTakingSnapshot", CLEAR_ON_MANAGER_START},
{"Offroad_NeosUpdate", CLEAR_ON_MANAGER_START}, {"Offroad_NeosUpdate", CLEAR_ON_MANAGER_START},
{"Offroad_NoFirmware", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"Offroad_NoFirmware", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},

@ -246,12 +246,6 @@ std::string random_string(std::string::size_type length) {
return s; return s;
} }
std::string dir_name(std::string const &path) {
size_t pos = path.find_last_of("/");
if (pos == std::string::npos) return "";
return path.substr(0, pos);
}
bool starts_with(const std::string &s1, const std::string &s2) { bool starts_with(const std::string &s1, const std::string &s2) {
return strncmp(s1.c_str(), s2.c_str(), s2.size()) == 0; return strncmp(s1.c_str(), s2.c_str(), s2.size()) == 0;
} }
@ -277,20 +271,4 @@ std::string check_output(const std::string& command) {
return result; return result;
} }
struct tm get_time() {
time_t rawtime;
time(&rawtime);
struct tm sys_time;
gmtime_r(&rawtime, &sys_time);
return sys_time;
}
bool time_valid(struct tm sys_time) {
int year = 1900 + sys_time.tm_year;
int month = 1 + sys_time.tm_mon;
return (year > 2023) || (year == 2023 && month >= 6);
}
} // namespace util } // namespace util

@ -8,7 +8,6 @@
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <csignal> #include <csignal>
#include <ctime>
#include <map> #include <map>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@ -45,10 +44,6 @@ int set_realtime_priority(int level);
int set_core_affinity(std::vector<int> cores); int set_core_affinity(std::vector<int> cores);
int set_file_descriptor_limit(uint64_t limit); int set_file_descriptor_limit(uint64_t limit);
// ***** Time helpers *****
struct tm get_time();
bool time_valid(struct tm sys_time);
// ***** math helpers ***** // ***** math helpers *****
// map x from [a1, a2] to [b1, b2] // map x from [a1, a2] to [b1, b2]
@ -75,7 +70,6 @@ int getenv(const char* key, int default_val);
float getenv(const char* key, float default_val); float getenv(const char* key, float default_val);
std::string hexdump(const uint8_t* in, const size_t size); std::string hexdump(const uint8_t* in, const size_t size);
std::string dir_name(std::string const& path);
bool starts_with(const std::string &s1, const std::string &s2); bool starts_with(const std::string &s1, const std::string &s2);
bool ends_with(const std::string &s, const std::string &suffix); bool ends_with(const std::string &s, const std::string &suffix);

@ -4,7 +4,7 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 285 Supported Cars # 286 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video| |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@ -29,6 +29,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Chrysler|Pacifica Hybrid 2018|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chrysler&model=Pacifica Hybrid 2018">Buy Here</a></sub></details>|| |Chrysler|Pacifica Hybrid 2018|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chrysler&model=Pacifica Hybrid 2018">Buy Here</a></sub></details>||
|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)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chrysler&model=Pacifica Hybrid 2019-23">Buy Here</a></sub></details>|| |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)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chrysler&model=Pacifica Hybrid 2019-23">Buy Here</a></sub></details>||
|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||
|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,12</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=CUPRA&model=Ateca 2018-23">Buy Here</a></sub></details>||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Dodge&model=Durango 2020-21">Buy Here</a></sub></details>|| |Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Dodge&model=Durango 2020-21">Buy Here</a></sub></details>||
|Ford|Bronco Sport 2021-23|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Bronco Sport 2021-23">Buy Here</a></sub></details>|| |Ford|Bronco Sport 2021-23|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Bronco Sport 2021-23">Buy Here</a></sub></details>||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape 2020-22">Buy Here</a></sub></details>|| |Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape 2020-22">Buy Here</a></sub></details>||
@ -192,7 +193,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Nissan|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Nissan&model=Rogue 2018-20">Buy Here</a></sub></details>|| |Nissan|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Nissan&model=Rogue 2018-20">Buy Here</a></sub></details>||
|Nissan|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Nissan&model=X-Trail 2017">Buy Here</a></sub></details>|| |Nissan|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Nissan&model=X-Trail 2017">Buy Here</a></sub></details>||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Ram connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ram&model=1500 2019-24">Buy Here</a></sub></details>|| |Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Ram connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ram&model=1500 2019-24">Buy Here</a></sub></details>||
|SEAT|Ateca 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,12</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=SEAT&model=Ateca 2018">Buy Here</a></sub></details>|| |SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,12</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=SEAT&model=Ateca 2016-23">Buy Here</a></sub></details>||
|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,12</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=SEAT&model=Leon 2014-20">Buy Here</a></sub></details>|| |SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,12</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=SEAT&model=Leon 2014-20">Buy Here</a></sub></details>||
|Subaru|Ascent 2019-21|All[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Subaru&model=Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|| |Subaru|Ascent 2019-21|All[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Subaru&model=Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Subaru&model=Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Subaru A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Subaru&model=Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|

@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="10" export AGNOS_VERSION="10.1"
fi fi
export STAGING_ROOT="/data/safe_staging" export STAGING_ROOT="/data/safe_staging"

@ -78,5 +78,6 @@ private:
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 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(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); 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);
int lltransfer(spi_ioc_transfer &t);
}; };
#endif #endif

@ -268,7 +268,7 @@ int PandaSpiHandle::wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout,
tx_buf[0] = tx; tx_buf[0] = tx;
while (true) { while (true) {
int ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); int ret = lltransfer(transfer);
if (ret < 0) { if (ret < 0) {
LOGE("SPI: failed to send ACK request"); LOGE("SPI: failed to send ACK request");
return ret; return ret;
@ -291,6 +291,32 @@ int PandaSpiHandle::wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout,
return 0; return 0;
} }
int PandaSpiHandle::lltransfer(spi_ioc_transfer &t) {
static const double err_prob = std::stod(util::getenv("SPI_ERR_PROB", "-1"));
if (err_prob > 0) {
if ((static_cast<double>(rand()) / RAND_MAX) < err_prob) {
printf("transfer len error\n");
t.len = rand() % SPI_BUF_SIZE;
}
if ((static_cast<double>(rand()) / RAND_MAX) < err_prob && t.tx_buf != (uint64_t)NULL) {
printf("corrupting TX\n");
memset((uint8_t*)t.tx_buf, (uint8_t)(rand() % 256), rand() % (t.len+1));
}
}
int ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &t);
if (err_prob > 0) {
if ((static_cast<double>(rand()) / RAND_MAX) < err_prob && t.rx_buf != (uint64_t)NULL) {
printf("corrupting RX\n");
memset((uint8_t*)t.rx_buf, (uint8_t)(rand() % 256), rand() % (t.len+1));
}
}
return ret;
}
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 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; int ret;
uint16_t rx_data_len; uint16_t rx_data_len;
@ -316,7 +342,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
memcpy(tx_buf, &header, sizeof(header)); memcpy(tx_buf, &header, sizeof(header));
add_checksum(tx_buf, sizeof(header)); add_checksum(tx_buf, sizeof(header));
transfer.len = sizeof(header) + 1; transfer.len = sizeof(header) + 1;
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); ret = lltransfer(transfer);
if (ret < 0) { if (ret < 0) {
LOGE("SPI: failed to send header"); LOGE("SPI: failed to send header");
goto transfer_fail; goto transfer_fail;
@ -334,7 +360,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
} }
add_checksum(tx_buf, tx_len); add_checksum(tx_buf, tx_len);
transfer.len = tx_len + 1; transfer.len = tx_len + 1;
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); ret = lltransfer(transfer);
if (ret < 0) { if (ret < 0) {
LOGE("SPI: failed to send data"); LOGE("SPI: failed to send data");
goto transfer_fail; goto transfer_fail;
@ -355,7 +381,7 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
transfer.len = rx_data_len + 1; transfer.len = rx_data_len + 1;
transfer.rx_buf = (uint64_t)(rx_buf + 2 + 1); transfer.rx_buf = (uint64_t)(rx_buf + 2 + 1);
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); ret = lltransfer(transfer);
if (ret < 0) { if (ret < 0) {
LOGE("SPI: failed to read rx data"); LOGE("SPI: failed to read rx data");
goto transfer_fail; goto transfer_fail;

@ -1,5 +1,5 @@
# functions common among cars # functions common among cars
from collections import defaultdict, namedtuple from collections import namedtuple
from dataclasses import dataclass from dataclasses import dataclass
from enum import IntFlag, ReprEnum, EnumType from enum import IntFlag, ReprEnum, EnumType
from dataclasses import replace from dataclasses import replace
@ -276,19 +276,3 @@ class Platforms(str, ReprEnum, metaclass=PlatformsType):
@classmethod @classmethod
def with_flags(cls, flags: IntFlag) -> set['Platforms']: def with_flags(cls, flags: IntFlag) -> set['Platforms']:
return {p for p in cls if p.config.flags & flags} return {p for p in cls if p.config.flags & flags}
@classmethod
def without_flags(cls, flags: IntFlag) -> set['Platforms']:
return {p for p in cls if not (p.config.flags & flags)}
@classmethod
def print_debug(cls, flags):
platforms_with_flag = defaultdict(list)
for flag in flags:
for platform in cls:
if platform.config.flags & flag:
assert flag.name is not None
platforms_with_flag[flag.name].append(platform)
for flag, platforms in platforms_with_flag.items():
print(f"{flag:32s}: {', '.join(p.name for p in platforms)}")

@ -15,7 +15,6 @@ from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp
from openpilot.selfdrive.car.car_helpers import get_car, get_one_can from openpilot.selfdrive.car.car_helpers import get_car, get_one_can
from openpilot.selfdrive.car.interfaces import CarInterfaceBase from openpilot.selfdrive.car.interfaces import CarInterfaceBase
REPLAY = "REPLAY" in os.environ REPLAY = "REPLAY" in os.environ
@ -28,7 +27,7 @@ class CarD:
self.sm = messaging.SubMaster(['pandaStates']) self.sm = messaging.SubMaster(['pandaStates'])
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput']) self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput'])
self.can_rcv_timeout_counter = 0 # conseuctive timeout count self.can_rcv_timeout_counter = 0 # consecutive timeout count
self.can_rcv_cum_timeout_counter = 0 # cumulative timeout count self.can_rcv_cum_timeout_counter = 0 # cumulative timeout count
self.CC_prev = car.CarControl.new_message() self.CC_prev = car.CarControl.new_message()
@ -54,12 +53,11 @@ class CarD:
if not disengage_on_accelerator: if not disengage_on_accelerator:
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
car_recognized = self.CP.carName != 'mock'
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle") openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly
self.CP.passive = not car_recognized or not controller_available or self.CP.dashcamOnly self.CP.passive = not controller_available or self.CP.dashcamOnly
if self.CP.passive: if self.CP.passive:
safety_config = car.CarParams.SafetyConfig.new_message() safety_config = car.CarParams.SafetyConfig.new_message()
safety_config.safetyModel = car.CarParams.SafetyModel.noOutput safety_config.safetyModel = car.CarParams.SafetyModel.noOutput
@ -139,4 +137,3 @@ class CarD:
self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=self.CS.canValid)) self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=self.CS.canValid))
self.CC_prev = CC self.CC_prev = CC

@ -266,7 +266,7 @@ class CarDocs:
# min steer & enable speed columns # min steer & enable speed columns
# TODO: set all the min steer speeds in carParams and remove this # TODO: set all the min steer speeds in carParams and remove this
if self.min_steer_speed is not None: if self.min_steer_speed is not None:
assert CP.minSteerSpeed == 0, f"{CP.carFingerprint}: Minimum steer speed set in both CarDocs and CarParams" assert CP.minSteerSpeed < 0.5, f"{CP.carFingerprint}: Minimum steer speed set in both CarDocs and CarParams"
else: else:
self.min_steer_speed = CP.minSteerSpeed self.min_steer_speed = CP.minSteerSpeed

@ -61,6 +61,7 @@ FW_VERSIONS = {
b'L1MC-2D053-BB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'L1MC-2D053-BB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'L1MC-2D053-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'L1MC-2D053-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'L1MC-2D053-BF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'L1MC-2D053-BF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'L1MC-2D053-BJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'L1MC-2D053-KB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'L1MC-2D053-KB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.fwdRadar, 0x764, None): [ (Ecu.fwdRadar, 0x764, None): [

@ -0,0 +1,30 @@
#!/usr/bin/env python3
from collections import defaultdict
from cereal import car
from openpilot.selfdrive.car.ford.values import get_platform_codes
from openpilot.selfdrive.car.ford.fingerprints import FW_VERSIONS
Ecu = car.CarParams.Ecu
ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()}
if __name__ == "__main__":
cars_for_code: defaultdict = defaultdict(lambda: defaultdict(set))
for car_model, ecus in FW_VERSIONS.items():
print(car_model)
for ecu in sorted(ecus, key=lambda x: int(x[0])):
platform_codes = get_platform_codes(ecus[ecu])
for code in platform_codes:
cars_for_code[ecu][code].add(car_model)
print(f' (Ecu.{ECU_NAME[ecu[0]]}, {hex(ecu[1])}, {ecu[2]}):')
print(f' Codes: {sorted(platform_codes)}')
print()
print('\nCar models vs. platform codes:')
for ecu, codes in cars_for_code.items():
print(f' (Ecu.{ECU_NAME[ecu[0]]}, {hex(ecu[1])}, {ecu[2]}):')
for code, cars in codes.items():
print(f' {code!r}: {sorted(map(str, cars))}')

@ -1,12 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import random
import unittest import unittest
from parameterized import parameterized
from collections.abc import Iterable from collections.abc import Iterable
import capnp import capnp
from hypothesis import settings, given, strategies as st
from parameterized import parameterized
from cereal import car from cereal import car
from openpilot.selfdrive.car.ford.values import FW_QUERY_CONFIG from openpilot.selfdrive.car.fw_versions import build_fw_dict
from openpilot.selfdrive.car.ford.values import CAR, FW_QUERY_CONFIG, FW_PATTERN, get_platform_codes
from openpilot.selfdrive.car.ford.fingerprints import FW_VERSIONS from openpilot.selfdrive.car.ford.fingerprints import FW_VERSIONS
Ecu = car.CarParams.Ecu Ecu = car.CarParams.Ecu
@ -23,7 +26,7 @@ ECU_ADDRESSES = {
} }
ECU_FW_CORE = { ECU_PART_NUMBER = {
Ecu.eps: [ Ecu.eps: [
b"14D003", b"14D003",
], ],
@ -37,9 +40,6 @@ ECU_FW_CORE = {
b"14F397", # Ford Q3 b"14F397", # Ford Q3
b"14H102", # Ford Q4 b"14H102", # Ford Q4
], ],
Ecu.engine: [
b"14C204",
],
} }
@ -53,29 +53,96 @@ class TestFordFW(unittest.TestCase):
@parameterized.expand(FW_VERSIONS.items()) @parameterized.expand(FW_VERSIONS.items())
def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[capnp.lib.capnp._EnumModule, int, int | None], Iterable[bytes]]): def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[capnp.lib.capnp._EnumModule, int, int | None], Iterable[bytes]]):
for (ecu, addr, subaddr), fws in fw_versions.items(): for (ecu, addr, subaddr), fws in fw_versions.items():
self.assertIn(ecu, ECU_FW_CORE, "Unexpected ECU") self.assertIn(ecu, ECU_PART_NUMBER, "Unexpected ECU")
self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch") self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch")
self.assertIsNone(subaddr, "Unexpected ECU subaddress") self.assertIsNone(subaddr, "Unexpected ECU subaddress")
# Software part number takes the form: PREFIX-CORE-SUFFIX
# Prefix changes based on the family of part. It includes the model year
# and likely the platform.
# Core identifies the type of the item (e.g. 14D003 = PSCM, 14C204 = PCM).
# Suffix specifies the version of the part. -AA would be followed by -AB.
# Small increments in the suffix are usually compatible.
# Details: https://forscan.org/forum/viewtopic.php?p=70008#p70008
for fw in fws: for fw in fws:
self.assertEqual(len(fw), 24, "Expected ECU response to be 24 bytes") self.assertEqual(len(fw), 24, "Expected ECU response to be 24 bytes")
# TODO: parse with regex, don't need detailed error message match = FW_PATTERN.match(fw)
fw_parts = fw.rstrip(b'\x00').split(b'-') self.assertIsNotNone(match, f"Unable to parse FW: {fw!r}")
self.assertEqual(len(fw_parts), 3, "Expected FW to be in format: prefix-core-suffix") if match:
part_number = match.group("part_number")
self.assertIn(part_number, ECU_PART_NUMBER[ecu], f"Unexpected part number for {fw!r}")
codes = get_platform_codes([fw])
self.assertEqual(1, len(codes), f"Unable to parse FW: {fw!r}")
@settings(max_examples=100)
@given(data=st.data())
def test_platform_codes_fuzzy_fw(self, data):
"""Ensure function doesn't raise an exception"""
fw_strategy = st.lists(st.binary())
fws = data.draw(fw_strategy)
get_platform_codes(fws)
def test_platform_codes_spot_check(self):
# Asserts basic platform code parsing behavior for a few cases
results = get_platform_codes([
b"JX6A-14C204-BPL\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"NZ6T-14F397-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"LB5A-14C204-EAC\x00\x00\x00\x00\x00\x00\x00\x00\x00",
])
self.assertEqual(results, {(b"X6A", b"J"), (b"Z6T", b"N"), (b"J6T", b"P"), (b"B5A", b"L")})
def test_fuzzy_match(self):
for platform, fw_by_addr in FW_VERSIONS.items():
# Ensure there's no overlaps in platform codes
for _ in range(20):
car_fw = []
for ecu, fw_versions in fw_by_addr.items():
ecu_name, addr, sub_addr = ecu
fw = random.choice(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), CP.carVin, FW_VERSIONS)
self.assertEqual(matches, {platform})
def test_match_fw_fuzzy(self):
offline_fw = {
(Ecu.eps, 0x730, None): [
b"L1MC-14D003-AJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"L1MC-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
],
(Ecu.abs, 0x760, None): [
b"L1MC-2D053-BA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"L1MC-2D053-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
],
(Ecu.fwdRadar, 0x764, None): [
b"LB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"LB5T-14D049-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
],
# We consider all model year hints for ECU, even with different platform codes
(Ecu.fwdCamera, 0x706, None): [
b"LB5T-14F397-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"NC5T-14F397-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
],
}
expected_fingerprint = CAR.FORD_EXPLORER_MK6
# ensure that we fuzzy match on all non-exact FW with changed revisions
live_fw = {
(0x730, None): {b"L1MC-14D003-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
(0x760, None): {b"L1MC-2D053-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
(0x764, None): {b"LB5T-14D049-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
(0x706, None): {b"LB5T-14F397-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
}
candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw})
self.assertEqual(candidates, {expected_fingerprint})
# model year hint in between the range should match
live_fw[(0x706, None)] = {b"MB5T-14F397-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"}
candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw,})
self.assertEqual(candidates, {expected_fingerprint})
prefix, core, suffix = fw_parts # unseen model year hint should not match
self.assertEqual(len(prefix), 4, "Expected FW prefix to be 4 characters") live_fw[(0x760, None)] = {b"M1MC-2D053-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"}
self.assertIn(len(core), (5, 6), "Expected FW core to be 5-6 characters") candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw})
self.assertIn(core, ECU_FW_CORE[ecu], f"Unexpected FW core for {ecu}") self.assertEqual(len(candidates), 0, "Should not match new model year hint")
self.assertIn(len(suffix), (2, 3), "Expected FW suffix to be 2-3 characters")
if __name__ == "__main__": if __name__ == "__main__":

@ -1,4 +1,5 @@
import copy import copy
import re
from dataclasses import dataclass, field, replace from dataclasses import dataclass, field, replace
from enum import Enum, IntFlag from enum import Enum, IntFlag
@ -7,7 +8,7 @@ from cereal import car
from openpilot.selfdrive.car import AngleRateLimit, CarSpecs, dbc_dict, DbcDict, PlatformConfig, Platforms from openpilot.selfdrive.car import AngleRateLimit, CarSpecs, dbc_dict, DbcDict, PlatformConfig, Platforms
from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \ from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \
Device Device
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16 from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, LiveFwVersions, OfflineFwVersions, Request, StdQueries, p16
Ecu = car.CarParams.Ecu Ecu = car.CarParams.Ecu
@ -143,6 +144,76 @@ class CAR(Platforms):
) )
# FW response contains a combined software and part number
# A-Z except no I, O or W
# e.g. NZ6A-14C204-AAA
# 1222-333333-444
# 1 = Model year hint (approximates model year/generation)
# 2 = Platform hint
# 3 = Part number
# 4 = Software version
FW_ALPHABET = b'A-HJ-NP-VX-Z'
FW_PATTERN = re.compile(b'^(?P<model_year_hint>[' + FW_ALPHABET + b'])' +
b'(?P<platform_hint>[0-9' + FW_ALPHABET + b']{3})-' +
b'(?P<part_number>[0-9' + FW_ALPHABET + b']{5,6})-' +
b'(?P<software_revision>[' + FW_ALPHABET + b']{2,})\x00*$')
def get_platform_codes(fw_versions: list[bytes] | set[bytes]) -> set[tuple[bytes, bytes]]:
codes = set()
for fw in fw_versions:
match = FW_PATTERN.match(fw)
if match is not None:
codes.add((match.group('platform_hint'), match.group('model_year_hint')))
return codes
def match_fw_to_car_fuzzy(live_fw_versions: LiveFwVersions, vin: str, offline_fw_versions: OfflineFwVersions) -> set[str]:
candidates: set[str] = set()
for candidate, fws in offline_fw_versions.items():
# Keep track of ECUs which pass all checks (platform hint, within model year hint 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 & model year hints
codes = get_platform_codes(expected_versions)
expected_platform_codes = {code for code, _ in codes}
expected_model_year_hints = {model_year_hint for _, model_year_hint in codes}
# Found platform codes & model year hints
codes = get_platform_codes(live_fw_versions.get(addr, set()))
found_platform_codes = {code for code, _ in codes}
found_model_year_hints = {model_year_hint for _, model_year_hint in codes}
# Check platform code matches for any found versions
if not any(found_platform_code in expected_platform_codes for found_platform_code in found_platform_codes):
break
# Check any model year hint within range in the database. Note that some models have more than one
# platform code per ECU which we don't consider as separate ranges
if not any(min(expected_model_year_hints) <= found_model_year_hint <= max(expected_model_year_hints) for
found_model_year_hint in found_model_year_hints):
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
# All of these ECUs must be present and are expected to have platform codes we can match
PLATFORM_CODE_ECUS = (Ecu.abs, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.eps)
DATA_IDENTIFIER_FORD_ASBUILT = 0xDE00 DATA_IDENTIFIER_FORD_ASBUILT = 0xDE00
ASBUILT_BLOCKS: list[tuple[int, list]] = [ ASBUILT_BLOCKS: list[tuple[int, list]] = [
@ -201,6 +272,8 @@ FW_QUERY_CONFIG = FwQueryConfig(
(Ecu.shiftByWire, 0x732, None), # Gear Shift Module (Ecu.shiftByWire, 0x732, None), # Gear Shift Module
(Ecu.debug, 0x7d0, None), # Accessory Protocol Interface Module (Ecu.debug, 0x7d0, None), # Accessory Protocol Interface Module
], ],
# Custom fuzzy fingerprinting function using platform and model year hints
match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
) )
DBC = CAR.create_dbc_map() DBC = CAR.create_dbc_map()

@ -295,6 +295,7 @@ FW_VERSIONS = {
b'37805-5BB-L540\x00\x00', b'37805-5BB-L540\x00\x00',
b'37805-5BB-L630\x00\x00', b'37805-5BB-L630\x00\x00',
b'37805-5BB-L640\x00\x00', b'37805-5BB-L640\x00\x00',
b'37805-5BF-J130\x00\x00',
], ],
(Ecu.transmission, 0x18da1ef1, None): [ (Ecu.transmission, 0x18da1ef1, None): [
b'28101-5CG-A920\x00\x00', b'28101-5CG-A920\x00\x00',
@ -328,6 +329,7 @@ FW_VERSIONS = {
b'57114-TGG-G320\x00\x00', b'57114-TGG-G320\x00\x00',
b'57114-TGG-L320\x00\x00', b'57114-TGG-L320\x00\x00',
b'57114-TGG-L330\x00\x00', b'57114-TGG-L330\x00\x00',
b'57114-TGH-L130\x00\x00',
b'57114-TGK-T320\x00\x00', b'57114-TGK-T320\x00\x00',
b'57114-TGL-G330\x00\x00', b'57114-TGL-G330\x00\x00',
], ],
@ -341,6 +343,7 @@ FW_VERSIONS = {
b'39990-TGG-A020\x00\x00', b'39990-TGG-A020\x00\x00',
b'39990-TGG-A120\x00\x00', b'39990-TGG-A120\x00\x00',
b'39990-TGG-J510\x00\x00', b'39990-TGG-J510\x00\x00',
b'39990-TGH-J530\x00\x00',
b'39990-TGL-E130\x00\x00', b'39990-TGL-E130\x00\x00',
b'39990-TGN-E120\x00\x00', b'39990-TGN-E120\x00\x00',
], ],
@ -355,6 +358,7 @@ FW_VERSIONS = {
b'77959-TGG-G110\x00\x00', b'77959-TGG-G110\x00\x00',
b'77959-TGG-J320\x00\x00', b'77959-TGG-J320\x00\x00',
b'77959-TGG-Z820\x00\x00', b'77959-TGG-Z820\x00\x00',
b'77959-TGH-J110\x00\x00',
], ],
(Ecu.fwdRadar, 0x18dab0f1, None): [ (Ecu.fwdRadar, 0x18dab0f1, None): [
b'36802-TBA-A150\x00\x00', b'36802-TBA-A150\x00\x00',
@ -366,6 +370,7 @@ FW_VERSIONS = {
b'36802-TGG-A130\x00\x00', b'36802-TGG-A130\x00\x00',
b'36802-TGG-G040\x00\x00', b'36802-TGG-G040\x00\x00',
b'36802-TGG-G130\x00\x00', b'36802-TGG-G130\x00\x00',
b'36802-TGH-A140\x00\x00',
b'36802-TGK-Q120\x00\x00', b'36802-TGK-Q120\x00\x00',
b'36802-TGL-G040\x00\x00', b'36802-TGL-G040\x00\x00',
], ],
@ -380,6 +385,7 @@ FW_VERSIONS = {
b'36161-TGG-G070\x00\x00', b'36161-TGG-G070\x00\x00',
b'36161-TGG-G130\x00\x00', b'36161-TGG-G130\x00\x00',
b'36161-TGG-G140\x00\x00', b'36161-TGG-G140\x00\x00',
b'36161-TGH-A140\x00\x00',
b'36161-TGK-Q120\x00\x00', b'36161-TGK-Q120\x00\x00',
b'36161-TGL-G050\x00\x00', b'36161-TGL-G050\x00\x00',
b'36161-TGL-G070\x00\x00', b'36161-TGL-G070\x00\x00',
@ -387,6 +393,7 @@ FW_VERSIONS = {
(Ecu.gateway, 0x18daeff1, None): [ (Ecu.gateway, 0x18daeff1, None): [
b'38897-TBA-A020\x00\x00', b'38897-TBA-A020\x00\x00',
b'38897-TBA-A110\x00\x00', b'38897-TBA-A110\x00\x00',
b'38897-TGH-A010\x00\x00',
], ],
(Ecu.electricBrakeBooster, 0x18da2bf1, None): [ (Ecu.electricBrakeBooster, 0x18da2bf1, None): [
b'39494-TGL-G030\x00\x00', b'39494-TGL-G030\x00\x00',

@ -1133,6 +1133,7 @@ FW_VERSIONS = {
CAR.KIA_K8_HEV_1ST_GEN: { CAR.KIA_K8_HEV_1ST_GEN: {
(Ecu.fwdCamera, 0x7c4, None): [ (Ecu.fwdCamera, 0x7c4, None): [
b'\xf1\x00GL3HMFC AT KOR LHD 1.00 1.03 99211-L8000 210907', b'\xf1\x00GL3HMFC AT KOR LHD 1.00 1.03 99211-L8000 210907',
b'\xf1\x00GL3HMFC AT KOR LHD 1.00 1.04 99211-L8000 230207',
], ],
(Ecu.fwdRadar, 0x7d0, None): [ (Ecu.fwdRadar, 0x7d0, None): [
b'\xf1\x00GL3_ RDR ----- 1.00 1.02 99110-L8000 ', b'\xf1\x00GL3_ RDR ----- 1.00 1.02 99110-L8000 ',

@ -746,6 +746,3 @@ LEGACY_SAFETY_MODE_CAR = CAR.with_flags(HyundaiFlags.LEGACY)
UNSUPPORTED_LONGITUDINAL_CAR = CAR.with_flags(HyundaiFlags.LEGACY) | CAR.with_flags(HyundaiFlags.UNSUPPORTED_LONGITUDINAL) UNSUPPORTED_LONGITUDINAL_CAR = CAR.with_flags(HyundaiFlags.LEGACY) | CAR.with_flags(HyundaiFlags.UNSUPPORTED_LONGITUDINAL)
DBC = CAR.create_dbc_map() DBC = CAR.create_dbc_map()
if __name__ == "__main__":
CAR.print_debug(HyundaiFlags)

@ -18,6 +18,7 @@ class CarInterface(CarInterfaceBase):
ret.wheelbase = 2.70 ret.wheelbase = 2.70
ret.centerToFront = ret.wheelbase * 0.5 ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 13. ret.steerRatio = 13.
ret.dashcamOnly = True
return ret return ret
def _update(self, c): def _update(self, c):

@ -273,6 +273,3 @@ FW_QUERY_CONFIG = FwQueryConfig(
) )
DBC = CAR.create_dbc_map() DBC = CAR.create_dbc_map()
if __name__ == "__main__":
CAR.print_debug(SubaruFlags)

@ -228,6 +228,7 @@ FW_VERSIONS = {
b'\xf1\x870DD300046F \xf1\x891601', b'\xf1\x870DD300046F \xf1\x891601',
b'\xf1\x870GC300012A \xf1\x891401', b'\xf1\x870GC300012A \xf1\x891401',
b'\xf1\x870GC300012A \xf1\x891403', b'\xf1\x870GC300012A \xf1\x891403',
b'\xf1\x870GC300012A \xf1\x891422',
b'\xf1\x870GC300012M \xf1\x892301', b'\xf1\x870GC300012M \xf1\x892301',
b'\xf1\x870GC300014B \xf1\x892401', b'\xf1\x870GC300014B \xf1\x892401',
b'\xf1\x870GC300014B \xf1\x892403', b'\xf1\x870GC300014B \xf1\x892403',
@ -891,6 +892,7 @@ FW_VERSIONS = {
b'\xf1\x8704L906056CR\xf1\x892181', b'\xf1\x8704L906056CR\xf1\x892181',
b'\xf1\x8704L906056CR\xf1\x892797', b'\xf1\x8704L906056CR\xf1\x892797',
b'\xf1\x8705E906018AS\xf1\x899596', b'\xf1\x8705E906018AS\xf1\x899596',
b'\xf1\x8781A906259B \xf1\x890003',
b'\xf1\x878V0906264H \xf1\x890005', b'\xf1\x878V0906264H \xf1\x890005',
b'\xf1\x878V0907115E \xf1\x890002', b'\xf1\x878V0907115E \xf1\x890002',
], ],
@ -900,6 +902,7 @@ FW_VERSIONS = {
b'\xf1\x870CW300050J \xf1\x891908', b'\xf1\x870CW300050J \xf1\x891908',
b'\xf1\x870D9300014S \xf1\x895202', b'\xf1\x870D9300014S \xf1\x895202',
b'\xf1\x870D9300042M \xf1\x895016', b'\xf1\x870D9300042M \xf1\x895016',
b'\xf1\x870GC300014P \xf1\x892801',
b'\xf1\x870GC300043A \xf1\x892304', b'\xf1\x870GC300043A \xf1\x892304',
], ],
(Ecu.srs, 0x715, None): [ (Ecu.srs, 0x715, None): [
@ -910,6 +913,7 @@ FW_VERSIONS = {
b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e1312001313001305171311052900', b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e1312001313001305171311052900',
b'\xf1\x873Q0959655BH\xf1\x890712\xf1\x82\x0e1312001313001305171311052900', b'\xf1\x873Q0959655BH\xf1\x890712\xf1\x82\x0e1312001313001305171311052900',
b'\xf1\x873Q0959655CM\xf1\x890720\xf1\x82\x0e1312001313001305171311052900', b'\xf1\x873Q0959655CM\xf1\x890720\xf1\x82\x0e1312001313001305171311052900',
b'\xf1\x875QF959655AT\xf1\x890755\xf1\x82\x1311110011110011111100110200--1113121149',
], ],
(Ecu.eps, 0x712, None): [ (Ecu.eps, 0x712, None): [
b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571N60511A1', b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571N60511A1',
@ -918,8 +922,10 @@ FW_VERSIONS = {
b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0511N01805A0', b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0511N01805A0',
b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521N01309A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521N01309A1',
b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521N05808A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521N05808A1',
b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x0013N619137N',
], ],
(Ecu.fwdRadar, 0x757, None): [ (Ecu.fwdRadar, 0x757, None): [
b'\xf1\x872Q0907572AA\xf1\x890396',
b'\xf1\x872Q0907572M \xf1\x890233', b'\xf1\x872Q0907572M \xf1\x890233',
b'\xf1\x875Q0907572B \xf1\x890200\xf1\x82\x0101', b'\xf1\x875Q0907572B \xf1\x890200\xf1\x82\x0101',
b'\xf1\x875Q0907572H \xf1\x890620', b'\xf1\x875Q0907572H \xf1\x890620',

@ -2,7 +2,7 @@ from cereal import car
from panda import Panda from panda import Panda
from openpilot.selfdrive.car import get_safety_config from openpilot.selfdrive.car import get_safety_config
from openpilot.selfdrive.car.interfaces import CarInterfaceBase from openpilot.selfdrive.car.interfaces import CarInterfaceBase
from openpilot.selfdrive.car.volkswagen.values import CAR, CANBUS, NetworkLocation, TransmissionType, GearShifter, VolkswagenFlags from openpilot.selfdrive.car.volkswagen.values import CAR, CANBUS, CarControllerParams, NetworkLocation, TransmissionType, GearShifter, VolkswagenFlags
ButtonType = car.CarState.ButtonEvent.Type ButtonType = car.CarState.ButtonEvent.Type
EventName = car.CarEvent.EventName EventName = car.CarEvent.EventName
@ -111,7 +111,7 @@ class CarInterface(CarInterfaceBase):
enable_buttons=(ButtonType.setCruise, ButtonType.resumeCruise)) enable_buttons=(ButtonType.setCruise, ButtonType.resumeCruise))
# Low speed steer alert hysteresis logic # Low speed steer alert hysteresis logic
if self.CP.minSteerSpeed > 0. and ret.vEgo < (self.CP.minSteerSpeed + 1.): if (self.CP.minSteerSpeed - 1e-3) > CarControllerParams.DEFAULT_MIN_STEER_SPEED and ret.vEgo < (self.CP.minSteerSpeed + 1.):
self.low_speed_alert = True self.low_speed_alert = True
elif ret.vEgo > (self.CP.minSteerSpeed + 2.): elif ret.vEgo > (self.CP.minSteerSpeed + 2.):
self.low_speed_alert = False self.low_speed_alert = False

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import random
import re import re
import unittest import unittest
@ -38,9 +39,9 @@ class TestVolkswagenPlatformConfigs(unittest.TestCase):
f"Shared chassis codes: {comp}") f"Shared chassis codes: {comp}")
def test_custom_fuzzy_fingerprinting(self): def test_custom_fuzzy_fingerprinting(self):
for platform in CAR: all_radar_fw = list({fw for ecus in FW_VERSIONS.values() for fw in ecus[Ecu.fwdRadar, 0x757, None]})
expected_radar_fw = FW_VERSIONS[platform][Ecu.fwdRadar, 0x757, None]
for platform in CAR:
with self.subTest(platform=platform): with self.subTest(platform=platform):
for wmi in WMI: for wmi in WMI:
for chassis_code in platform.config.chassis_codes | {"00"}: for chassis_code in platform.config.chassis_codes | {"00"}:
@ -50,9 +51,9 @@ class TestVolkswagenPlatformConfigs(unittest.TestCase):
vin = "".join(vin) vin = "".join(vin)
# Check a few FW cases - expected, unexpected # Check a few FW cases - expected, unexpected
for radar_fw in expected_radar_fw + [b'\xf1\x877H9907572AA\xf1\x890396']: for radar_fw in random.sample(all_radar_fw, 5) + [b'\xf1\x875Q0907572G \xf1\x890571', b'\xf1\x877H9907572AA\xf1\x890396']:
should_match = ((wmi in platform.config.wmis and chassis_code in platform.config.chassis_codes) and should_match = ((wmi in platform.config.wmis and chassis_code in platform.config.chassis_codes) and
radar_fw in expected_radar_fw) radar_fw in all_radar_fw)
live_fws = {(0x757, None): [radar_fw]} live_fws = {(0x757, None): [radar_fw]}
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fws, vin, FW_VERSIONS) matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fws, vin, FW_VERSIONS)

@ -1,4 +1,4 @@
from collections import namedtuple from collections import defaultdict, namedtuple
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, IntFlag, StrEnum from enum import Enum, IntFlag, StrEnum
@ -9,7 +9,7 @@ from openpilot.common.conversions import Conversions as CV
from openpilot.selfdrive.car import dbc_dict, CarSpecs, DbcDict, PlatformConfig, Platforms from openpilot.selfdrive.car import dbc_dict, CarSpecs, DbcDict, PlatformConfig, Platforms
from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \ from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \
Device Device
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, p16 from openpilot.selfdrive.car.fw_query_definitions import EcuAddrSubAddr, FwQueryConfig, Request, p16
Ecu = car.CarParams.Ecu Ecu = car.CarParams.Ecu
NetworkLocation = car.CarParams.NetworkLocation NetworkLocation = car.CarParams.NetworkLocation
@ -34,6 +34,8 @@ class CarControllerParams:
STEER_TIME_ALERT = STEER_TIME_MAX - 10 # If mitigation fails, time to soft disengage before EPS timer expires STEER_TIME_ALERT = STEER_TIME_MAX - 10 # If mitigation fails, time to soft disengage before EPS timer expires
STEER_TIME_STUCK_TORQUE = 1.9 # EPS limits same torque to 6 seconds, reset timer 3x within that period STEER_TIME_STUCK_TORQUE = 1.9 # EPS limits same torque to 6 seconds, reset timer 3x within that period
DEFAULT_MIN_STEER_SPEED = 0.4 # m/s, newer EPS racks fault below this speed, don't show a low speed alert
ACCEL_MAX = 2.0 # 2.0 m/s max acceleration ACCEL_MAX = 2.0 # 2.0 m/s max acceleration
ACCEL_MIN = -3.5 # 3.5 m/s max deceleration ACCEL_MIN = -3.5 # 3.5 m/s max deceleration
@ -159,6 +161,7 @@ class VolkswagenPQPlatformConfig(VolkswagenMQBPlatformConfig):
class VolkswagenCarSpecs(CarSpecs): class VolkswagenCarSpecs(CarSpecs):
centerToFrontRatio: float = 0.45 centerToFrontRatio: float = 0.45
steerRatio: float = 15.6 steerRatio: float = 15.6
minSteerSpeed: float = CarControllerParams.DEFAULT_MIN_STEER_SPEED
class Footnote(Enum): class Footnote(Enum):
@ -195,6 +198,9 @@ class VWCarDocs(CarDocs):
if CP.carFingerprint in (CAR.VOLKSWAGEN_CRAFTER_MK2, CAR.VOLKSWAGEN_TRANSPORTER_T61): if CP.carFingerprint in (CAR.VOLKSWAGEN_CRAFTER_MK2, CAR.VOLKSWAGEN_TRANSPORTER_T61):
self.car_parts = CarParts([Device.threex_angled_mount, CarHarness.j533]) self.car_parts = CarParts([Device.threex_angled_mount, CarHarness.j533])
if abs(CP.minSteerSpeed - CarControllerParams.DEFAULT_MIN_STEER_SPEED) < 1e-3:
self.min_steer_speed = 0
# Check the 7th and 8th characters of the VIN before adding a new CAR. If the # Check the 7th and 8th characters of the VIN before adding a new CAR. If the
# chassis code is already listed below, don't add a new CAR, just add to the # chassis code is already listed below, don't add a new CAR, just add to the
@ -372,7 +378,8 @@ class CAR(Platforms):
) )
SEAT_ATECA_MK1 = VolkswagenMQBPlatformConfig( SEAT_ATECA_MK1 = VolkswagenMQBPlatformConfig(
[ [
VWCarDocs("SEAT Ateca 2018"), VWCarDocs("CUPRA Ateca 2018-23"),
VWCarDocs("SEAT Ateca 2016-23"),
VWCarDocs("SEAT Leon 2014-20"), VWCarDocs("SEAT Leon 2014-20"),
], ],
VolkswagenCarSpecs(mass=1300, wheelbase=2.64), VolkswagenCarSpecs(mass=1300, wheelbase=2.64),
@ -427,19 +434,26 @@ class CAR(Platforms):
def match_fw_to_car_fuzzy(live_fw_versions, vin, offline_fw_versions) -> set[str]: def match_fw_to_car_fuzzy(live_fw_versions, vin, offline_fw_versions) -> set[str]:
candidates = set() candidates = set()
# Compile all FW versions for each ECU
all_ecu_versions: dict[EcuAddrSubAddr, set[str]] = defaultdict(set)
for ecus in offline_fw_versions.values():
for ecu, versions in ecus.items():
all_ecu_versions[ecu] |= set(versions)
# Check the WMI and chassis code to determine the platform # Check the WMI and chassis code to determine the platform
wmi = vin[:3] wmi = vin[:3]
chassis_code = vin[6:8] chassis_code = vin[6:8]
for platform in CAR: for platform in CAR:
valid_ecus = set() valid_ecus = set()
for ecu, expected_versions in offline_fw_versions[platform].items(): for ecu in offline_fw_versions[platform]:
addr = ecu[1:] addr = ecu[1:]
if ecu[0] not in CHECK_FUZZY_ECUS: if ecu[0] not in CHECK_FUZZY_ECUS:
continue continue
# Sanity check that a subset of Volkswagen FW is in the database # Sanity check that live FW is in the superset of all FW, Volkswagen ECU part numbers are commonly shared
found_versions = live_fw_versions.get(addr, []) found_versions = live_fw_versions.get(addr, [])
expected_versions = all_ecu_versions[ecu]
if not any(found_version in expected_versions for found_version in found_versions): if not any(found_version in expected_versions for found_version in found_versions):
break break

@ -111,7 +111,6 @@ class Controls:
if not self.CP.openpilotLongitudinalControl: if not self.CP.openpilotLongitudinalControl:
self.params.remove("ExperimentalMode") self.params.remove("ExperimentalMode")
self.CC = car.CarControl.new_message()
self.CS_prev = car.CarState.new_message() self.CS_prev = car.CarState.new_message()
self.AM = AlertManager() self.AM = AlertManager()
self.events = Events() self.events = Events()
@ -805,9 +804,6 @@ class Controls:
cc_send.carControl = CC cc_send.carControl = CC
self.pm.send('carControl', cc_send) self.pm.send('carControl', cc_send)
# copy CarControl to pass to CarInterface on the next iteration
self.CC = CC
def step(self): def step(self):
start_time = time.monotonic() start_time = time.monotonic()

@ -1,7 +1,7 @@
{ {
"Offroad_TemperatureTooHigh": { "Offroad_TemperatureTooHigh": {
"text": "Device temperature too high. System cooling down before starting. Current internal component temperature: %1", "text": "Device temperature too high. System cooling down before starting. Current internal component temperature: %1",
"severity": 1 "severity": 0
}, },
"Offroad_ConnectivityNeededPrompt": { "Offroad_ConnectivityNeededPrompt": {
"text": "Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1", "text": "Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1",
@ -17,10 +17,6 @@
"severity": 1, "severity": 1,
"_comment": "Set extra field to the failed reason." "_comment": "Set extra field to the failed reason."
}, },
"Offroad_InvalidTime": {
"text": "Invalid date and time settings, system won't start. Connect to internet to set time.",
"severity": 1
},
"Offroad_IsTakingSnapshot": { "Offroad_IsTakingSnapshot": {
"text": "Taking camera snapshots. System won't start until finished.", "text": "Taking camera snapshots. System won't start until finished.",
"severity": 0 "severity": 0

@ -21,5 +21,5 @@ if __name__ == "__main__":
if len(ts[s]) > 2: if len(ts[s]) > 2:
d = np.diff(ts[s]) d = np.diff(ts[s])
print(f"{s:25} {np.mean(d):.2f} {np.std(d):.2f} {np.max(d):.2f} {np.min(d):.2f}") print(f"{s:25} {np.mean(d):7.2f} {np.std(d):7.2f} {np.max(d):7.2f} {np.min(d):7.2f}")
time.sleep(1) time.sleep(1)

@ -0,0 +1,14 @@
#!/usr/bin/env python3
from openpilot.selfdrive.car.values import BRANDS
for brand in BRANDS:
all_flags = set()
for platform in brand:
if platform.config.flags != 0:
all_flags |= set(platform.config.flags)
if len(all_flags):
print(brand.__module__.split('.')[-2].upper() + ':')
for flag in sorted(all_flags):
print(f' {flag.name:<24}:', {platform.name for platform in brand.with_flags(flag)})
print()

@ -22,7 +22,7 @@ def dmonitoringd_thread():
v_cruise_last = 0 v_cruise_last = 0
driver_engaged = False driver_engaged = False
# 10Hz <- dmonitoringmodeld # 20Hz <- dmonitoringmodeld
while True: while True:
sm.update() sm.update()
if not sm.updated['driverStateV2']: if not sm.updated['driverStateV2']:
@ -46,14 +46,15 @@ def dmonitoringd_thread():
if sm.all_checks() and len(sm['liveCalibration'].rpyCalib): if sm.all_checks() and len(sm['liveCalibration'].rpyCalib):
driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled) driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled)
# Block engaging after max number of distrations # Block engaging after max number of distrations or when alert active
if driver_status.terminal_alert_cnt >= driver_status.settings._MAX_TERMINAL_ALERTS or \ if driver_status.terminal_alert_cnt >= driver_status.settings._MAX_TERMINAL_ALERTS or \
driver_status.terminal_time >= driver_status.settings._MAX_TERMINAL_DURATION: driver_status.terminal_time >= driver_status.settings._MAX_TERMINAL_DURATION or \
driver_status.always_on and driver_status.awareness <= driver_status.threshold_prompt:
events.add(car.CarEvent.EventName.tooDistracted) events.add(car.CarEvent.EventName.tooDistracted)
# Update events from driver state # Update events from driver state
driver_status.update_events(events, driver_engaged, sm['controlsState'].enabled, driver_status.update_events(events, driver_engaged, sm['controlsState'].enabled,
sm['carState'].standstill, sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park]) sm['carState'].standstill, sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park], sm['carState'].vEgo)
# build driverMonitoringState packet # build driverMonitoringState packet
dat = messaging.new_message('driverMonitoringState', valid=sm.all_checks()) dat = messaging.new_message('driverMonitoringState', valid=sm.all_checks())

@ -55,6 +55,7 @@ class DRIVER_MONITOR_SETTINGS():
self._POSESTD_THRESHOLD = 0.3 self._POSESTD_THRESHOLD = 0.3
self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
self._ALWAYS_ON_ALERT_MIN_SPEED = 7
self._POSE_CALIB_MIN_SPEED = 13 # 30 mph self._POSE_CALIB_MIN_SPEED = 13 # 30 mph
self._POSE_OFFSET_MIN_COUNT = int(60 / self._DT_DMON) # valid data counts before calibration completes, 1min cumulative self._POSE_OFFSET_MIN_COUNT = int(60 / self._DT_DMON) # valid data counts before calibration completes, 1min cumulative
@ -302,7 +303,7 @@ class DriverStatus():
elif self.face_detected and self.pose.low_std: elif self.face_detected and self.pose.low_std:
self.hi_stds = 0 self.hi_stds = 0
def update_events(self, events, driver_engaged, ctrl_active, standstill, wrong_gear): def update_events(self, events, driver_engaged, ctrl_active, standstill, wrong_gear, car_speed):
always_on_valid = self.always_on and not wrong_gear always_on_valid = self.always_on and not wrong_gear
if (driver_engaged and self.awareness > 0 and not self.active_monitoring_mode) or \ if (driver_engaged and self.awareness > 0 and not self.active_monitoring_mode) or \
(not always_on_valid and not ctrl_active) or \ (not always_on_valid and not ctrl_active) or \
@ -327,14 +328,19 @@ class DriverStatus():
if self.awareness > self.threshold_prompt: if self.awareness > self.threshold_prompt:
return return
standstill_exemption = standstill and self.awareness - self.step_change <= self.threshold_prompt _reaching_audible = self.awareness - self.step_change <= self.threshold_prompt
always_on_red_exemption = always_on_valid and not ctrl_active and self.awareness - self.step_change <= 0 _reaching_terminal = self.awareness - self.step_change <= 0
standstill_exemption = standstill and _reaching_audible
always_on_red_exemption = always_on_valid and not ctrl_active and _reaching_terminal
always_on_lowspeed_exemption = always_on_valid and not ctrl_active and car_speed < self.settings._ALWAYS_ON_ALERT_MIN_SPEED and _reaching_audible
certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected
maybe_distracted = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME or not self.face_detected maybe_distracted = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME or not self.face_detected
if certainly_distracted or maybe_distracted: if certainly_distracted or maybe_distracted:
# should always be counting if distracted unless at standstill and reaching orange # should always be counting if distracted unless at standstill (lowspeed for always-on) and reaching orange
# also will not be reaching 0 if DM is active when not engaged # also will not be reaching 0 if DM is active when not engaged
if not standstill_exemption and not always_on_red_exemption: if not (standstill_exemption or always_on_red_exemption or always_on_lowspeed_exemption):
self.awareness = max(self.awareness - self.step_change, -0.1) self.awareness = max(self.awareness - self.step_change, -0.1)
alert = None alert = None

@ -63,7 +63,7 @@ class TestMonitoring(unittest.TestCase):
# cal_rpy and car_speed don't matter here # cal_rpy and car_speed don't matter here
# evaluate events at 10Hz for tests # evaluate events at 10Hz for tests
DS.update_events(e, interaction[idx], engaged[idx], standstill[idx], 0) DS.update_events(e, interaction[idx], engaged[idx], standstill[idx], 0, 0)
events.append(e) events.append(e)
assert len(events) == len(msgs), f"got {len(events)} for {len(msgs)} driverState input msgs" assert len(events) == len(msgs), f"got {len(events)} for {len(msgs)} driverState input msgs"
return events, DS return events, DS

@ -1 +1 @@
692a21e4a722d91086998b532ca6759a3f85c345 685a2bb9aacdd790e26d14aa49d3792c3ed65125

@ -440,10 +440,6 @@
<translation>غير قادر على تحميل التحديثات <translation>غير قادر على تحميل التحديثات
%1</translation> %1</translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation>إعدادات التاريخ والتوقيت غير صحيحة، لن يبدأ النظام. اتصل بالإنترنت من أجل ضبط الوقت.</translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation>التقاط لقطات كاميرا. لن يبدأ النظام حتى تنتهي هذه العملية.</translation> <translation>التقاط لقطات كاميرا. لن يبدأ النظام حتى تنتهي هذه العملية.</translation>

@ -431,10 +431,6 @@
%1</source> %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>

@ -436,10 +436,6 @@
<translation>Impossible de télécharger les mises à jour <translation>Impossible de télécharger les mises à jour
%1</translation> %1</translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation>Paramètres de date et d&apos;heure invalides, le système ne démarrera pas. Connectez l&apos;appareil à Internet pour régler l&apos;heure.</translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation>Capture de clichés photo. Le système ne démarrera pas tant qu&apos;il n&apos;est pas terminé.</translation> <translation>Capture de clichés photo. Le système ne démarrera pas tant qu&apos;il n&apos;est pas terminé.</translation>

@ -430,10 +430,6 @@
%1</source> %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>

@ -431,10 +431,6 @@
<translation> <translation>
%1</translation> %1</translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation> . .</translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation> .</translation> <translation> .</translation>

@ -432,10 +432,6 @@
<translation>Não é possível baixar atualizações <translation>Não é possível baixar atualizações
%1</translation> %1</translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation>Configurações de data e hora inválidas, o sistema não será iniciado. Conecte-se à internet para definir o horário.</translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation>Tirando fotos da câmera. O sistema não será iniciado até terminar.</translation> <translation>Tirando fotos da câmera. O sistema não será iniciado até terminar.</translation>

@ -435,10 +435,6 @@
<translation> <translation>
%1</translation> %1</translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation> </translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation> </translation> <translation> </translation>

@ -434,10 +434,6 @@
%1</source> %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>

@ -431,10 +431,6 @@
<translation> <translation>
%1</translation> %1</translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation></translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation>使</translation> <translation>使</translation>

@ -431,10 +431,6 @@
<translation> <translation>
%1</translation> %1</translation>
</message> </message>
<message>
<source>Invalid date and time settings, system won&apos;t start. Connect to internet to set time.</source>
<translation></translation>
</message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation>使</translation> <translation>使</translation>

@ -1,10 +1,10 @@
[ [
{ {
"name": "boot", "name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-543fdc8aadf700f33a6e90740b8a227036bbd190626861d45ba1eb0d9ac422d1.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/boot-5674ea6767e7198cf1e7def3de66a57061f001ed76d43dc4b4f84de545c53c6f.img.xz",
"hash": "543fdc8aadf700f33a6e90740b8a227036bbd190626861d45ba1eb0d9ac422d1", "hash": "5674ea6767e7198cf1e7def3de66a57061f001ed76d43dc4b4f84de545c53c6f",
"hash_raw": "543fdc8aadf700f33a6e90740b8a227036bbd190626861d45ba1eb0d9ac422d1", "hash_raw": "5674ea6767e7198cf1e7def3de66a57061f001ed76d43dc4b4f84de545c53c6f",
"size": 15636480, "size": 16029696,
"sparse": false, "sparse": false,
"full_check": true, "full_check": true,
"has_ab": true "has_ab": true
@ -61,17 +61,17 @@
}, },
{ {
"name": "system", "name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-bd2967074298a2686f81e2094db3867d7cb2605750d63b1a7309a923f6985b2a.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/system-1badfe72851628d6cf9200a53a6151bb4e797b49c717141409fc57138eae388a.img.xz",
"hash": "dc2f960631f02446d912885786922c27be4eb1ec6c202cacce6699d5a74021ba", "hash": "328e90c62068222dfd98f71dd3f6251fcb962f082b49c6be66ab2699f5db6f4f",
"hash_raw": "bd2967074298a2686f81e2094db3867d7cb2605750d63b1a7309a923f6985b2a", "hash_raw": "1badfe72851628d6cf9200a53a6151bb4e797b49c717141409fc57138eae388a",
"size": 10737418240, "size": 10737418240,
"sparse": true, "sparse": true,
"full_check": false, "full_check": false,
"has_ab": true, "has_ab": true,
"alt": { "alt": {
"hash": "283e5e754593c6e1bb5e9d63b54624cda5475b88bc1b130fe6ab13acdbd966e2", "hash": "bc11d2148f29862ee1326aca2af1cf6bbf5fed831e3f8f6b8f7a0f110dfe8d26",
"url": "https://commadist.azureedge.net/agnosupdate/system-skip-chunks-bd2967074298a2686f81e2094db3867d7cb2605750d63b1a7309a923f6985b2a.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/system-skip-chunks-1badfe72851628d6cf9200a53a6151bb4e797b49c717141409fc57138eae388a.img.xz",
"size": 4548070712 "size": 4548070000
} }
} }
] ]

@ -26,7 +26,7 @@ cabana_env.Command(assets, assets_src, f"rcc $SOURCES -o $TARGET")
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"])) cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"]))
cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcanstream.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcanstream.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
'utils/export.cc', 'utils/util.cc', 'utils/export.cc', 'utils/util.cc',
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)

@ -7,6 +7,7 @@
#include <QPushButton> #include <QPushButton>
#include "common/timing.h" #include "common/timing.h"
#include "tools/cabana/streams/routes.h"
ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) { ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) {
unsetenv("ZMQ"); unsetenv("ZMQ");
@ -107,29 +108,36 @@ AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) {
// OpenReplayWidget // OpenReplayWidget
OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) {
// TODO: get route list from api.comma.ai
QGridLayout *grid_layout = new QGridLayout(this); QGridLayout *grid_layout = new QGridLayout(this);
grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); grid_layout->addWidget(new QLabel(tr("Route")), 0, 0);
grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1);
route_edit->setPlaceholderText(tr("Enter remote route name or click browse to select a local route")); route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route"));
auto file_btn = new QPushButton(tr("Browse..."), this); auto browse_remote_btn = new QPushButton(tr("Remote route..."), this);
grid_layout->addWidget(file_btn, 0, 2); grid_layout->addWidget(browse_remote_btn, 0, 2);
auto browse_local_btn = new QPushButton(tr("Local route..."), this);
grid_layout->addWidget(browse_local_btn, 0, 3);
grid_layout->addWidget(new QLabel(tr("Camera")), 1, 0);
QHBoxLayout *camera_layout = new QHBoxLayout(); QHBoxLayout *camera_layout = new QHBoxLayout();
for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")}) for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")})
camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this))); camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this)));
cameras[0]->setChecked(true);
camera_layout->addStretch(1); camera_layout->addStretch(1);
grid_layout->addItem(camera_layout, 1, 1); grid_layout->addItem(camera_layout, 1, 1);
setMinimumWidth(550); setMinimumWidth(550);
QObject::connect(file_btn, &QPushButton::clicked, [=]() { QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() {
QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir); QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir);
if (!dir.isEmpty()) { if (!dir.isEmpty()) {
route_edit->setText(dir); route_edit->setText(dir);
settings.last_route_dir = QFileInfo(dir).absolutePath(); settings.last_route_dir = QFileInfo(dir).absolutePath();
} }
}); });
QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() {
RoutesDialog route_dlg(this);
if (route_dlg.exec()) {
route_edit->setText(route_dlg.route());
}
});
} }
bool OpenReplayWidget::open() { bool OpenReplayWidget::open() {

@ -0,0 +1,123 @@
#include "tools/cabana/streams/routes.h"
#include <QDateTime>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QListWidget>
#include <QMessageBox>
#include <QPainter>
#include "system/hardware/hw.h"
// The RouteListWidget class extends QListWidget to display a custom message when empty
class RouteListWidget : public QListWidget {
public:
RouteListWidget(QWidget *parent = nullptr) : QListWidget(parent) {}
void setEmptyText(const QString &text) {
empty_text_ = text;
viewport()->update();
}
void paintEvent(QPaintEvent *event) override {
QListWidget::paintEvent(event);
if (count() == 0) {
QPainter painter(viewport());
painter.drawText(viewport()->rect(), Qt::AlignCenter, empty_text_);
}
}
QString empty_text_ = tr("No items");
};
RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) {
setWindowTitle(tr("Remote routes"));
QFormLayout *layout = new QFormLayout(this);
layout->addRow(tr("Device"), device_list_ = new QComboBox(this));
layout->addRow(tr("Duration"), period_selector_ = new QComboBox(this));
layout->addRow(route_list_ = new RouteListWidget(this));
auto button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout->addRow(button_box);
device_list_->addItem(tr("Loading..."));
// Populate period selector with predefined durations
period_selector_->addItem(tr("Last week"), 7);
period_selector_->addItem(tr("Last 2 weeks"), 14);
period_selector_->addItem(tr("Last month"), 30);
period_selector_->addItem(tr("Last 6 months"), 180);
// Connect signals and slots
connect(device_list_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
connect(period_selector_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
connect(route_list_, &QListWidget::itemDoubleClicked, this, &QDialog::accept);
QObject::connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
QObject::connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
// Send request to fetch devices
HttpRequest *http = new HttpRequest(this, !Hardware::PC());
QObject::connect(http, &HttpRequest::requestDone, this, &RoutesDialog::parseDeviceList);
http->sendRequest(CommaApi::BASE_URL + "/v1/me/devices/");
}
void RoutesDialog::parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err) {
if (success) {
device_list_->clear();
auto devices = QJsonDocument::fromJson(json.toUtf8()).array();
for (const QJsonValue &device : devices) {
QString dongle_id = device["dongle_id"].toString();
device_list_->addItem(dongle_id, dongle_id);
}
} else {
bool unauthorized = (err == QNetworkReply::ContentAccessDenied || err == QNetworkReply::AuthenticationRequiredError);
QMessageBox::warning(this, tr("Error"), unauthorized ? tr("Unauthorized, Authenticate with tools/lib/auth.py") : tr("Network error"));
reject();
}
sender()->deleteLater();
}
void RoutesDialog::fetchRoutes() {
if (device_list_->currentIndex() == -1 || device_list_->currentData().isNull())
return;
route_list_->clear();
route_list_->setEmptyText(tr("Loading..."));
HttpRequest *http = new HttpRequest(this, !Hardware::PC());
QObject::connect(http, &HttpRequest::requestDone, this, &RoutesDialog::parseRouteList);
// Construct URL with selected device and date range
auto dongle_id = device_list_->currentData().toString();
QDateTime current = QDateTime::currentDateTime();
QString url = QString("%1/v1/devices/%2/routes_segments?start=%3&end=%4")
.arg(CommaApi::BASE_URL).arg(dongle_id)
.arg(current.addDays(-(period_selector_->currentData().toInt())).toMSecsSinceEpoch())
.arg(current.toMSecsSinceEpoch());
http->sendRequest(url);
}
void RoutesDialog::parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err) {
if (success) {
for (const QJsonValue &route : QJsonDocument::fromJson(json.toUtf8()).array()) {
uint64_t start_time = route["start_time_utc_millis"].toDouble();
uint64_t end_time = route["end_time_utc_millis"].toDouble();
auto datetime = QDateTime::fromMSecsSinceEpoch(start_time);
auto item = new QListWidgetItem(QString("%1 %2min").arg(datetime.toString()).arg((end_time - start_time) / (1000 * 60)));
item->setData(Qt::UserRole, route["fullname"].toString());
route_list_->addItem(item);
}
// Select first route if available
if (route_list_->count() > 0) route_list_->setCurrentRow(0);
} else {
QMessageBox::warning(this, tr("Error"), tr("Failed to fetch routes. Check your network connection."));
reject();
}
route_list_->setEmptyText(tr("No items"));
sender()->deleteLater();
}
void RoutesDialog::accept() {
if (auto current_item = route_list_->currentItem()) {
route_ = current_item->data(Qt::UserRole).toString();
}
QDialog::accept();
}

@ -0,0 +1,26 @@
#pragma once
#include <QComboBox>
#include <QDialog>
#include "selfdrive/ui/qt/api.h"
class RouteListWidget;
class RoutesDialog : public QDialog {
Q_OBJECT
public:
RoutesDialog(QWidget *parent);
QString route() const { return route_; }
protected:
void accept() override;
void parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err);
void parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err);
void fetchRoutes();
QComboBox *device_list_;
QComboBox *period_selector_;
RouteListWidget *route_list_;
QString route_;
};

@ -13,12 +13,8 @@
StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDialog(parent) { StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDialog(parent) {
setWindowTitle(tr("Open stream")); setWindowTitle(tr("Open stream"));
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *layout = new QVBoxLayout(this);
QWidget *w = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout(w);
tab = new QTabWidget(this); tab = new QTabWidget(this);
tab->setTabBarAutoHide(true);
layout->addWidget(tab); layout->addWidget(tab);
QHBoxLayout *dbc_layout = new QHBoxLayout(); QHBoxLayout *dbc_layout = new QHBoxLayout();
@ -35,9 +31,8 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial
line->setFrameStyle(QFrame::HLine | QFrame::Sunken); line->setFrameStyle(QFrame::HLine | QFrame::Sunken);
layout->addWidget(line); layout->addWidget(line);
main_layout->addWidget(w);
auto btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); auto btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel);
main_layout->addWidget(btn_box); layout->addWidget(btn_box);
addStreamWidget(ReplayStream::widget(stream)); addStreamWidget(ReplayStream::widget(stream));
addStreamWidget(PandaStream::widget(stream)); addStreamWidget(PandaStream::widget(stream));
@ -48,14 +43,11 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial
QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() { QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() {
btn_box->button(QDialogButtonBox::Open)->setEnabled(false); setEnabled(false);
w->setEnabled(false);
if (((AbstractOpenStreamWidget *)tab->currentWidget())->open()) { if (((AbstractOpenStreamWidget *)tab->currentWidget())->open()) {
accept(); accept();
} else {
btn_box->button(QDialogButtonBox::Open)->setEnabled(true);
w->setEnabled(true);
} }
setEnabled(true);
}); });
QObject::connect(file_btn, &QPushButton::clicked, [this]() { QObject::connect(file_btn, &QPushButton::clicked, [this]() {
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)");

@ -4,67 +4,37 @@ import argparse
from collections import defaultdict from collections import defaultdict
from openpilot.selfdrive.debug.format_fingerprints import format_brand_fw_versions from openpilot.selfdrive.debug.format_fingerprints import format_brand_fw_versions
from openpilot.selfdrive.car.fw_versions import match_fw_to_car from openpilot.selfdrive.car.fingerprints import MIGRATION
from openpilot.selfdrive.car.interfaces import get_interface_attr from openpilot.selfdrive.car.fw_versions import MODEL_TO_BRAND, match_fw_to_car
from openpilot.tools.lib.logreader import LogReader, ReadMode from openpilot.tools.lib.logreader import LogReader, ReadMode
ALL_FW_VERSIONS = get_interface_attr("FW_VERSIONS")
ALL_CARS = get_interface_attr("CAR")
PLATFORM_TO_PYTHON_CAR_NAME = {brand: {car.value: car.name for car in ALL_CARS[brand]} for brand in ALL_CARS}
BRAND_TO_PLATFORMS = {brand: [car.value for car in ALL_CARS[brand]] for brand in ALL_CARS}
PLATFORM_TO_BRAND = dict(sum([[(platform, brand) for platform in BRAND_TO_PLATFORMS[brand]] for brand in BRAND_TO_PLATFORMS], []))
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Auto fingerprint from a route") parser = argparse.ArgumentParser(description="Auto fingerprint from a route")
parser.add_argument("route", help="The route name to use") parser.add_argument("route", help="The route name to use")
parser.add_argument("platform", help="The platform, or leave empty to auto-determine using fuzzy", default=None, nargs='?') parser.add_argument("platform", help="The platform, or leave empty to auto-determine using fuzzy", default=None, nargs="?")
args = parser.parse_args() args = parser.parse_args()
lr = LogReader(args.route, ReadMode.QLOG) lr = LogReader(args.route, ReadMode.QLOG)
carFw = None
carVin = None
carPlatform = None
platform: str | None = None
CP = lr.first("carParams") CP = lr.first("carParams")
assert CP is not None, "No carParams in route"
if CP is None: carPlatform = MIGRATION.get(CP.carFingerprint, CP.carFingerprint)
raise Exception("No fw versions in the provided route...")
carFw = CP.carFw
carVin = CP.carVin
carPlatform = CP.carFingerprint
if args.platform is None: # attempt to auto-determine platform with other fuzzy fingerprints if args.platform is not None:
_, possible_platforms = match_fw_to_car(carFw, carVin, log=False) platform = args.platform
elif carPlatform != "MOCK":
if len(possible_platforms) != 1:
print(f"Unable to auto-determine platform, possible platforms: {possible_platforms}")
if carPlatform != "MOCK":
print("Using platform from route")
platform = carPlatform platform = carPlatform
else: else:
platform = None _, matches = match_fw_to_car(CP.carFw, CP.carVin, log=False)
else: assert len(matches) == 1, f"Unable to auto-determine platform, matches: {matches}"
platform = list(possible_platforms)[0] platform = list(matches)[0]
else:
platform = args.platform
if platform is None:
raise Exception("unable to determine platform, try manually specifying the fingerprint.")
print("Attempting to add fw version for:", platform) print("Attempting to add fw version for:", platform)
fw_versions: dict[str, dict[tuple, list[bytes]]] = defaultdict(lambda: defaultdict(list)) fw_versions: dict[str, dict[tuple, list[bytes]]] = defaultdict(lambda: defaultdict(list))
brand = PLATFORM_TO_BRAND[platform] brand = MODEL_TO_BRAND[platform]
for fw in carFw: for fw in CP.carFw:
if fw.brand == brand and not fw.logging: if fw.brand == brand and not fw.logging:
addr = fw.address addr = fw.address
subAddr = None if fw.subAddress == 0 else fw.subAddress subAddr = None if fw.subAddress == 0 else fw.subAddress

@ -18,7 +18,7 @@
"from openpilot.selfdrive.car.subaru.values import CAR, SubaruFlags\n", "from openpilot.selfdrive.car.subaru.values import CAR, SubaruFlags\n",
"from openpilot.selfdrive.car.subaru.fingerprints import FW_VERSIONS\n", "from openpilot.selfdrive.car.subaru.fingerprints import FW_VERSIONS\n",
"\n", "\n",
"TEST_PLATFORMS = CAR.without_flags(SubaruFlags.PREGLOBAL)\n", "TEST_PLATFORMS = set(CAR) - CAR.with_flags(SubaruFlags.PREGLOBAL)\n",
"\n", "\n",
"Ecu = car.CarParams.Ecu\n", "Ecu = car.CarParams.Ecu\n",
"\n", "\n",

@ -1,78 +0,0 @@
import ft4222
import ft4222.I2CMaster
DEBUG = False
INA231_ADDR = 0x40
INA231_REG_CONFIG = 0x00
INA231_REG_SHUNT_VOLTAGE = 0x01
INA231_REG_BUS_VOLTAGE = 0x02
INA231_REG_POWER = 0x03
INA231_REG_CURRENT = 0x04
INA231_REG_CALIBRATION = 0x05
INA231_BUS_LSB = 1.25e-3
INA231_SHUNT_LSB = 2.5e-6
SHUNT_RESISTOR = 30e-3
CURRENT_LSB = 1e-5
class Zookeeper:
def __init__(self):
if ft4222.createDeviceInfoList() < 2:
raise Exception("No connected zookeeper found!")
self.dev_a = ft4222.openByDescription("FT4222 A")
self.dev_b = ft4222.openByDescription("FT4222 B")
if DEBUG:
for i in range(ft4222.createDeviceInfoList()):
print(f"Device {i}: {ft4222.getDeviceInfoDetail(i, False)}")
# Setup GPIO
self.dev_b.gpio_Init(gpio2=ft4222.Dir.OUTPUT, gpio3=ft4222.Dir.OUTPUT)
self.dev_b.setSuspendOut(False)
self.dev_b.setWakeUpInterrut(False)
# Setup I2C
self.dev_a.i2cMaster_Init(kbps=400)
self._initialize_ina()
# Helper functions
def _read_ina_register(self, register, length):
self.dev_a.i2cMaster_WriteEx(INA231_ADDR, data=register, flag=ft4222.I2CMaster.Flag.REPEATED_START)
return self.dev_a.i2cMaster_Read(INA231_ADDR, bytesToRead=length)
def _write_ina_register(self, register, data):
msg = register.to_bytes(1, byteorder="big") + data.to_bytes(2, byteorder="big")
self.dev_a.i2cMaster_Write(INA231_ADDR, data=msg)
def _initialize_ina(self):
# Config
self._write_ina_register(INA231_REG_CONFIG, 0x4127)
# Calibration
CAL_VALUE = int(0.00512 / (CURRENT_LSB * SHUNT_RESISTOR))
if DEBUG:
print(f"Calibration value: {hex(CAL_VALUE)}")
self._write_ina_register(INA231_REG_CALIBRATION, CAL_VALUE)
def _set_gpio(self, number, enabled):
self.dev_b.gpio_Write(portNum=number, value=enabled)
# Public API functions
def set_device_power(self, enabled):
self._set_gpio(2, enabled)
def set_device_ignition(self, enabled):
self._set_gpio(3, enabled)
def read_current(self):
# Returns in A
return int.from_bytes(self._read_ina_register(INA231_REG_CURRENT, 2), byteorder="big") * CURRENT_LSB
def read_power(self):
# Returns in W
return int.from_bytes(self._read_ina_register(INA231_REG_POWER, 2), byteorder="big") * CURRENT_LSB * 25
def read_voltage(self):
# Returns in V
return int.from_bytes(self._read_ina_register(INA231_REG_BUS_VOLTAGE, 2), byteorder="big") * INA231_BUS_LSB

@ -1,27 +0,0 @@
#!/usr/bin/env python3
import sys
import time
from openpilot.tools.zookeeper import Zookeeper
# Usage: check_consumption.py <averaging_time_sec> <max_average_power_W>
# Exit code: 0 -> passed
# 1 -> failed
if __name__ == "__main__":
z = Zookeeper()
averaging_time_s = int(sys.argv[1])
max_average_power = float(sys.argv[2])
start_time = time.time()
measurements = []
while time.time() - start_time < averaging_time_s:
measurements.append(z.read_power())
time.sleep(0.1)
average_power = sum(measurements)/len(measurements)
print(f"Average power: {round(average_power, 4)}W")
if average_power > max_average_power:
exit(1)

@ -1,8 +0,0 @@
#!/usr/bin/env python3
from openpilot.tools.zookeeper import Zookeeper
if __name__ == "__main__":
z = Zookeeper()
z.set_device_power(False)

@ -1,31 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import time
from socket import gethostbyname, gaierror
from openpilot.tools.zookeeper import Zookeeper
def is_online(ip):
try:
addr = gethostbyname(ip)
return (os.system(f"ping -c 1 {addr} > /dev/null") == 0)
except gaierror:
return False
if __name__ == "__main__":
z = Zookeeper()
z.set_device_power(True)
ip = str(sys.argv[1])
timeout = int(sys.argv[2])
start_time = time.time()
while not is_online(ip):
print(f"{ip} not online yet!")
if time.time() - start_time > timeout:
print("Timed out!")
raise TimeoutError()
time.sleep(1)

@ -1,10 +0,0 @@
#!/usr/bin/env python3
import sys
from openpilot.tools.zookeeper import Zookeeper
if __name__ == "__main__":
z = Zookeeper()
z.set_device_ignition(1 if int(sys.argv[1]) > 0 else 0)

@ -1,40 +0,0 @@
#!/usr/bin/env python3
import sys
import time
import datetime
from openpilot.common.realtime import Ratekeeper
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.tools.zookeeper import Zookeeper
if __name__ == "__main__":
z = Zookeeper()
z.set_device_power(True)
z.set_device_ignition(False)
duration = None
if len(sys.argv) > 1:
duration = int(sys.argv[1])
rate = 123
rk = Ratekeeper(rate, print_delay_threshold=None)
fltr = FirstOrderFilter(0, 5, 1. / rate, initialized=False)
measurements = []
start_time = time.monotonic()
try:
while duration is None or time.monotonic() - start_time < duration:
fltr.update(z.read_power())
if rk.frame % rate == 0:
measurements.append(fltr.x)
t = datetime.timedelta(seconds=time.monotonic() - start_time)
avg = sum(measurements) / len(measurements)
print(f"Now: {fltr.x:.2f} W, Avg: {avg:.2f} W over {t}")
rk.keep_time()
except KeyboardInterrupt:
pass
t = datetime.timedelta(seconds=time.monotonic() - start_time)
avg = sum(measurements) / len(measurements)
print(f"\nAverage power: {avg:.2f}W over {t}")

@ -1,25 +0,0 @@
#!/usr/bin/env python3
import time
from openpilot.tools.zookeeper import Zookeeper
if __name__ == "__main__":
z = Zookeeper()
z.set_device_power(True)
i = 0
ign = False
while 1:
voltage = round(z.read_voltage(), 2)
current = round(z.read_current(), 3)
power = round(z.read_power(), 2)
z.set_device_ignition(ign)
print(f"Voltage: {voltage}V, Current: {current}A, Power: {power}W, Ignition: {ign}")
if i > 200:
ign = not ign
i = 0
i += 1
time.sleep(0.1)
Loading…
Cancel
Save