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. 60
      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_ConnectivityNeeded", CLEAR_ON_MANAGER_START},
{"Offroad_ConnectivityNeededPrompt", CLEAR_ON_MANAGER_START},
{"Offroad_InvalidTime", CLEAR_ON_MANAGER_START},
{"Offroad_IsTakingSnapshot", CLEAR_ON_MANAGER_START},
{"Offroad_NeosUpdate", CLEAR_ON_MANAGER_START},
{"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;
}
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) {
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;
}
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

@ -8,7 +8,6 @@
#include <atomic>
#include <chrono>
#include <csignal>
#include <ctime>
#include <map>
#include <memory>
#include <mutex>
@ -45,10 +44,6 @@ int set_realtime_priority(int level);
int set_core_affinity(std::vector<int> cores);
int set_file_descriptor_limit(uint64_t limit);
// ***** Time helpers *****
struct tm get_time();
bool time_valid(struct tm sys_time);
// ***** math helpers *****
// 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);
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 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.
# 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|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@ -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 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||
|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>||
|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>||
@ -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|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>||
|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>||
|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>|

@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="10"
export AGNOS_VERSION="10.1"
fi
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 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 lltransfer(spi_ioc_transfer &t);
};
#endif

@ -268,7 +268,7 @@ int PandaSpiHandle::wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout,
tx_buf[0] = tx;
while (true) {
int ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer);
int ret = lltransfer(transfer);
if (ret < 0) {
LOGE("SPI: failed to send ACK request");
return ret;
@ -291,6 +291,32 @@ int PandaSpiHandle::wait_for_ack(uint8_t ack, uint8_t tx, unsigned int timeout,
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 ret;
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));
add_checksum(tx_buf, sizeof(header));
transfer.len = sizeof(header) + 1;
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer);
ret = lltransfer(transfer);
if (ret < 0) {
LOGE("SPI: failed to send header");
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);
transfer.len = tx_len + 1;
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer);
ret = lltransfer(transfer);
if (ret < 0) {
LOGE("SPI: failed to send data");
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.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) {
LOGE("SPI: failed to read rx data");
goto transfer_fail;

@ -1,5 +1,5 @@
# functions common among cars
from collections import defaultdict, namedtuple
from collections import namedtuple
from dataclasses import dataclass
from enum import IntFlag, ReprEnum, EnumType
from dataclasses import replace
@ -276,19 +276,3 @@ class Platforms(str, ReprEnum, metaclass=PlatformsType):
@classmethod
def with_flags(cls, flags: IntFlag) -> set['Platforms']:
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.interfaces import CarInterfaceBase
REPLAY = "REPLAY" in os.environ
@ -28,7 +27,7 @@ class CarD:
self.sm = messaging.SubMaster(['pandaStates'])
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.CC_prev = car.CarControl.new_message()
@ -54,12 +53,11 @@ class CarD:
if not disengage_on_accelerator:
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
car_recognized = self.CP.carName != 'mock'
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
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:
safety_config = car.CarParams.SafetyConfig.new_message()
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.CC_prev = CC

@ -266,7 +266,7 @@ class CarDocs:
# min steer & enable speed columns
# TODO: set all the min steer speeds in carParams and remove this
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:
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-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-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',
],
(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
import random
import unittest
from parameterized import parameterized
from collections.abc import Iterable
import capnp
from hypothesis import settings, given, strategies as st
from parameterized import parameterized
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
Ecu = car.CarParams.Ecu
@ -23,7 +26,7 @@ ECU_ADDRESSES = {
}
ECU_FW_CORE = {
ECU_PART_NUMBER = {
Ecu.eps: [
b"14D003",
],
@ -37,9 +40,6 @@ ECU_FW_CORE = {
b"14F397", # Ford Q3
b"14H102", # Ford Q4
],
Ecu.engine: [
b"14C204",
],
}
@ -53,29 +53,96 @@ class TestFordFW(unittest.TestCase):
@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]]):
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.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:
self.assertEqual(len(fw), 24, "Expected ECU response to be 24 bytes")
# TODO: parse with regex, don't need detailed error message
fw_parts = fw.rstrip(b'\x00').split(b'-')
self.assertEqual(len(fw_parts), 3, "Expected FW to be in format: prefix-core-suffix")
match = FW_PATTERN.match(fw)
self.assertIsNotNone(match, f"Unable to parse FW: {fw!r}")
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
self.assertEqual(len(prefix), 4, "Expected FW prefix to be 4 characters")
self.assertIn(len(core), (5, 6), "Expected FW core to be 5-6 characters")
self.assertIn(core, ECU_FW_CORE[ecu], f"Unexpected FW core for {ecu}")
self.assertIn(len(suffix), (2, 3), "Expected FW suffix to be 2-3 characters")
# unseen model year hint should not match
live_fw[(0x760, None)] = {b"M1MC-2D053-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(len(candidates), 0, "Should not match new model year hint")
if __name__ == "__main__":

@ -1,4 +1,5 @@
import copy
import re
from dataclasses import dataclass, field, replace
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.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \
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
@ -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
ASBUILT_BLOCKS: list[tuple[int, list]] = [
@ -201,6 +272,8 @@ FW_QUERY_CONFIG = FwQueryConfig(
(Ecu.shiftByWire, 0x732, None), # Gear Shift 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()

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

@ -1133,6 +1133,7 @@ FW_VERSIONS = {
CAR.KIA_K8_HEV_1ST_GEN: {
(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.04 99211-L8000 230207',
],
(Ecu.fwdRadar, 0x7d0, None): [
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)
DBC = CAR.create_dbc_map()
if __name__ == "__main__":
CAR.print_debug(HyundaiFlags)

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

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

@ -2,7 +2,7 @@ from cereal import car
from panda import Panda
from openpilot.selfdrive.car import get_safety_config
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
EventName = car.CarEvent.EventName
@ -111,7 +111,7 @@ class CarInterface(CarInterfaceBase):
enable_buttons=(ButtonType.setCruise, ButtonType.resumeCruise))
# 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
elif ret.vEgo > (self.CP.minSteerSpeed + 2.):
self.low_speed_alert = False

@ -1,4 +1,5 @@
#!/usr/bin/env python3
import random
import re
import unittest
@ -38,9 +39,9 @@ class TestVolkswagenPlatformConfigs(unittest.TestCase):
f"Shared chassis codes: {comp}")
def test_custom_fuzzy_fingerprinting(self):
for platform in CAR:
expected_radar_fw = FW_VERSIONS[platform][Ecu.fwdRadar, 0x757, None]
all_radar_fw = list({fw for ecus in FW_VERSIONS.values() for fw in ecus[Ecu.fwdRadar, 0x757, None]})
for platform in CAR:
with self.subTest(platform=platform):
for wmi in WMI:
for chassis_code in platform.config.chassis_codes | {"00"}:
@ -50,9 +51,9 @@ class TestVolkswagenPlatformConfigs(unittest.TestCase):
vin = "".join(vin)
# 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
radar_fw in expected_radar_fw)
radar_fw in all_radar_fw)
live_fws = {(0x757, None): [radar_fw]}
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 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.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \
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
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_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_MIN = -3.5 # 3.5 m/s max deceleration
@ -159,6 +161,7 @@ class VolkswagenPQPlatformConfig(VolkswagenMQBPlatformConfig):
class VolkswagenCarSpecs(CarSpecs):
centerToFrontRatio: float = 0.45
steerRatio: float = 15.6
minSteerSpeed: float = CarControllerParams.DEFAULT_MIN_STEER_SPEED
class Footnote(Enum):
@ -195,6 +198,9 @@ class VWCarDocs(CarDocs):
if CP.carFingerprint in (CAR.VOLKSWAGEN_CRAFTER_MK2, CAR.VOLKSWAGEN_TRANSPORTER_T61):
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
# 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(
[
VWCarDocs("SEAT Ateca 2018"),
VWCarDocs("CUPRA Ateca 2018-23"),
VWCarDocs("SEAT Ateca 2016-23"),
VWCarDocs("SEAT Leon 2014-20"),
],
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]:
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
wmi = vin[:3]
chassis_code = vin[6:8]
for platform in CAR:
valid_ecus = set()
for ecu, expected_versions in offline_fw_versions[platform].items():
for ecu in offline_fw_versions[platform]:
addr = ecu[1:]
if ecu[0] not in CHECK_FUZZY_ECUS:
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, [])
expected_versions = all_ecu_versions[ecu]
if not any(found_version in expected_versions for found_version in found_versions):
break

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

@ -1,7 +1,7 @@
{
"Offroad_TemperatureTooHigh": {
"text": "Device temperature too high. System cooling down before starting. Current internal component temperature: %1",
"severity": 1
"severity": 0
},
"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",
@ -17,10 +17,6 @@
"severity": 1,
"_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": {
"text": "Taking camera snapshots. System won't start until finished.",
"severity": 0

@ -21,5 +21,5 @@ if __name__ == "__main__":
if len(ts[s]) > 2:
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)

@ -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
driver_engaged = False
# 10Hz <- dmonitoringmodeld
# 20Hz <- dmonitoringmodeld
while True:
sm.update()
if not sm.updated['driverStateV2']:
@ -46,14 +46,15 @@ def dmonitoringd_thread():
if sm.all_checks() and len(sm['liveCalibration'].rpyCalib):
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 \
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)
# Update events from driver state
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
dat = messaging.new_message('driverMonitoringState', valid=sm.all_checks())

@ -55,6 +55,7 @@ class DRIVER_MONITOR_SETTINGS():
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._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
self._ALWAYS_ON_ALERT_MIN_SPEED = 7
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
@ -302,7 +303,7 @@ class DriverStatus():
elif self.face_detected and self.pose.low_std:
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
if (driver_engaged and self.awareness > 0 and not self.active_monitoring_mode) or \
(not always_on_valid and not ctrl_active) or \
@ -327,14 +328,19 @@ class DriverStatus():
if self.awareness > self.threshold_prompt:
return
standstill_exemption = standstill and 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_audible = self.awareness - self.step_change <= self.threshold_prompt
_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
maybe_distracted = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME or not self.face_detected
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
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)
alert = None

@ -63,7 +63,7 @@ class TestMonitoring(unittest.TestCase):
# cal_rpy and car_speed don't matter here
# 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)
assert len(events) == len(msgs), f"got {len(events)} for {len(msgs)} driverState input msgs"
return events, DS

@ -1 +1 @@
692a21e4a722d91086998b532ca6759a3f85c345
685a2bb9aacdd790e26d14aa49d3792c3ed65125

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

@ -431,10 +431,6 @@
%1</source>
<translation type="unfinished"></translation>
</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>
<source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation type="unfinished"></translation>

@ -436,10 +436,6 @@
<translation>Impossible de télécharger les mises à jour
%1</translation>
</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>
<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>

@ -430,10 +430,6 @@
%1</source>
<translation type="unfinished"></translation>
</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>
<source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation type="unfinished"></translation>

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

@ -432,10 +432,6 @@
<translation>Não é possível baixar atualizações
%1</translation>
</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>
<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>

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

@ -434,10 +434,6 @@
%1</source>
<translation type="unfinished"></translation>
</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>
<source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation type="unfinished"></translation>

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

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

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

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

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

@ -4,67 +4,37 @@ import argparse
from collections import defaultdict
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.interfaces import get_interface_attr
from openpilot.selfdrive.car.fingerprints import MIGRATION
from openpilot.selfdrive.car.fw_versions import MODEL_TO_BRAND, match_fw_to_car
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__":
parser = argparse.ArgumentParser(description="Auto fingerprint from a route")
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()
lr = LogReader(args.route, ReadMode.QLOG)
carFw = None
carVin = None
carPlatform = None
platform: str | None = None
CP = lr.first("carParams")
assert CP is not None, "No carParams in route"
if CP is None:
raise Exception("No fw versions in the provided route...")
carPlatform = MIGRATION.get(CP.carFingerprint, CP.carFingerprint)
carFw = CP.carFw
carVin = CP.carVin
carPlatform = CP.carFingerprint
if args.platform is None: # attempt to auto-determine platform with other fuzzy fingerprints
_, possible_platforms = match_fw_to_car(carFw, carVin, log=False)
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
else:
platform = None
else:
platform = list(possible_platforms)[0]
else:
if args.platform is not None:
platform = args.platform
elif carPlatform != "MOCK":
platform = carPlatform
else:
_, matches = match_fw_to_car(CP.carFw, CP.carVin, log=False)
assert len(matches) == 1, f"Unable to auto-determine platform, matches: {matches}"
platform = list(matches)[0]
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))
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:
addr = fw.address
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.fingerprints import FW_VERSIONS\n",
"\n",
"TEST_PLATFORMS = CAR.without_flags(SubaruFlags.PREGLOBAL)\n",
"TEST_PLATFORMS = set(CAR) - CAR.with_flags(SubaruFlags.PREGLOBAL)\n",
"\n",
"Ecu = car.CarParams.Ecu\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