diff --git a/common/params.cc b/common/params.cc index a00401d20a..2330160173 100644 --- a/common/params.cc +++ b/common/params.cc @@ -172,7 +172,6 @@ std::unordered_map 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}, diff --git a/common/util.cc b/common/util.cc index b6097bf28f..5ffd6e4099 100644 --- a/common/util.cc +++ b/common/util.cc @@ -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 diff --git a/common/util.h b/common/util.h index 0e8bcd56bf..186873ac21 100644 --- a/common/util.h +++ b/common/util.h @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -45,10 +44,6 @@ int set_realtime_priority(int level); int set_core_affinity(std::vector 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); diff --git a/docs/CARS.md b/docs/CARS.md index 6cf224d135..7e5324d322 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -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|Hardware Needed
 |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)](##)|
Parts- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Chrysler|Pacifica Hybrid 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|| +|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |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)](##)|
Parts- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Bronco Sport 2021-23|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| @@ -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)](##)|
Parts- 1 Nissan A connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Nissan|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 RJ45 cable (7 ft)
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |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)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Ram connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| -|SEAT|Ateca 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| +|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
|| |Subaru|Ascent 2019-21|All[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
|| |Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 RJ45 cable (7 ft)
- 1 Subaru A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
|| diff --git a/launch_env.sh b/launch_env.sh index bfc2e6ac6a..81578aff01 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="10" + export AGNOS_VERSION="10.1" fi export STAGING_ROOT="/data/safe_staging" diff --git a/selfdrive/boardd/panda_comms.h b/selfdrive/boardd/panda_comms.h index e61d25402f..6b64768c17 100644 --- a/selfdrive/boardd/panda_comms.h +++ b/selfdrive/boardd/panda_comms.h @@ -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 diff --git a/selfdrive/boardd/spi.cc b/selfdrive/boardd/spi.cc index d11e955c49..e02252018f 100644 --- a/selfdrive/boardd/spi.cc +++ b/selfdrive/boardd/spi.cc @@ -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(rand()) / RAND_MAX) < err_prob) { + printf("transfer len error\n"); + t.len = rand() % SPI_BUF_SIZE; + } + if ((static_cast(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(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; diff --git a/selfdrive/car/__init__.py b/selfdrive/car/__init__.py index bbdae4f3d4..4a1df550d0 100644 --- a/selfdrive/car/__init__.py +++ b/selfdrive/car/__init__.py @@ -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)}") diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 82335500d8..8d1ef70d62 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -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 - diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index bb1ca6bd42..b66d2a16e0 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -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 diff --git a/selfdrive/car/ford/fingerprints.py b/selfdrive/car/ford/fingerprints.py index 4dcf2a65fd..2201072fa3 100644 --- a/selfdrive/car/ford/fingerprints.py +++ b/selfdrive/car/ford/fingerprints.py @@ -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): [ diff --git a/selfdrive/car/ford/tests/print_platform_codes.py b/selfdrive/car/ford/tests/print_platform_codes.py new file mode 100755 index 0000000000..670199980a --- /dev/null +++ b/selfdrive/car/ford/tests/print_platform_codes.py @@ -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))}') diff --git a/selfdrive/car/ford/tests/test_ford.py b/selfdrive/car/ford/tests/test_ford.py index 2ad3f5db1b..5d7b2c3332 100755 --- a/selfdrive/car/ford/tests/test_ford.py +++ b/selfdrive/car/ford/tests/test_ford.py @@ -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__": diff --git a/selfdrive/car/ford/values.py b/selfdrive/car/ford/values.py index a5494c921c..b1868bfa9b 100644 --- a/selfdrive/car/ford/values.py +++ b/selfdrive/car/ford/values.py @@ -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[' + FW_ALPHABET + b'])' + + b'(?P[0-9' + FW_ALPHABET + b']{3})-' + + b'(?P[0-9' + FW_ALPHABET + b']{5,6})-' + + b'(?P[' + 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() diff --git a/selfdrive/car/honda/fingerprints.py b/selfdrive/car/honda/fingerprints.py index 55bd8b7aec..052b4b60a3 100644 --- a/selfdrive/car/honda/fingerprints.py +++ b/selfdrive/car/honda/fingerprints.py @@ -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', diff --git a/selfdrive/car/hyundai/fingerprints.py b/selfdrive/car/hyundai/fingerprints.py index 77a04a7aaf..0c43b8e340 100644 --- a/selfdrive/car/hyundai/fingerprints.py +++ b/selfdrive/car/hyundai/fingerprints.py @@ -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 ', diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 2456e4aa6e..2518d3807b 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -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) diff --git a/selfdrive/car/mock/interface.py b/selfdrive/car/mock/interface.py index 6100717a74..7506bab053 100755 --- a/selfdrive/car/mock/interface.py +++ b/selfdrive/car/mock/interface.py @@ -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): diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index e8ced2b9af..dcbea1979f 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -273,6 +273,3 @@ FW_QUERY_CONFIG = FwQueryConfig( ) DBC = CAR.create_dbc_map() - -if __name__ == "__main__": - CAR.print_debug(SubaruFlags) diff --git a/selfdrive/car/volkswagen/fingerprints.py b/selfdrive/car/volkswagen/fingerprints.py index def5ce6e03..fdc375fc8d 100644 --- a/selfdrive/car/volkswagen/fingerprints.py +++ b/selfdrive/car/volkswagen/fingerprints.py @@ -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', diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index 83a8cde9a8..91cd300e92 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -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 diff --git a/selfdrive/car/volkswagen/tests/test_volkswagen.py b/selfdrive/car/volkswagen/tests/test_volkswagen.py index 92569e194e..17331203bb 100755 --- a/selfdrive/car/volkswagen/tests/test_volkswagen.py +++ b/selfdrive/car/volkswagen/tests/test_volkswagen.py @@ -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) diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index a0d38d1b57..eb537575fa 100644 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -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 diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 35dad3f81a..725a433dc6 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -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() diff --git a/selfdrive/controls/lib/alerts_offroad.json b/selfdrive/controls/lib/alerts_offroad.json index 35446ca22b..6fb6c569b0 100644 --- a/selfdrive/controls/lib/alerts_offroad.json +++ b/selfdrive/controls/lib/alerts_offroad.json @@ -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 diff --git a/selfdrive/debug/check_timings.py b/selfdrive/debug/check_timings.py index 3de6b8eb66..24c467659d 100755 --- a/selfdrive/debug/check_timings.py +++ b/selfdrive/debug/check_timings.py @@ -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) diff --git a/selfdrive/debug/print_flags.py b/selfdrive/debug/print_flags.py new file mode 100755 index 0000000000..c69765550e --- /dev/null +++ b/selfdrive/debug/print_flags.py @@ -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() diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index 84a147bd47..c18bbf29b9 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -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()) diff --git a/selfdrive/monitoring/driver_monitor.py b/selfdrive/monitoring/driver_monitor.py index 749931af77..7db0bd6c9f 100644 --- a/selfdrive/monitoring/driver_monitor.py +++ b/selfdrive/monitoring/driver_monitor.py @@ -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 diff --git a/selfdrive/monitoring/test_monitoring.py b/selfdrive/monitoring/test_monitoring.py index 39fef75479..50b2746e2d 100755 --- a/selfdrive/monitoring/test_monitoring.py +++ b/selfdrive/monitoring/test_monitoring.py @@ -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 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 4e24f8eb29..a0d3cc6bb2 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -692a21e4a722d91086998b532ca6759a3f85c345 +685a2bb9aacdd790e26d14aa49d3792c3ed65125 \ No newline at end of file diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index f00905b75b..d8146723a4 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -440,10 +440,6 @@ غير قادر على تحميل التحديثات %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - إعدادات التاريخ والتوقيت غير صحيحة، لن يبدأ النظام. اتصل بالإنترنت من أجل ضبط الوقت. - Taking camera snapshots. System won't start until finished. التقاط لقطات كاميرا. لن يبدأ النظام حتى تنتهي هذه العملية. diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 71d95244cb..010aa4d304 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -431,10 +431,6 @@ %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - - Taking camera snapshots. System won't start until finished. diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 5da51d8b47..dde6adadd3 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -436,10 +436,6 @@ Impossible de télécharger les mises à jour %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - Paramètres de date et d'heure invalides, le système ne démarrera pas. Connectez l'appareil à Internet pour régler l'heure. - Taking camera snapshots. System won't start until finished. Capture de clichés photo. Le système ne démarrera pas tant qu'il n'est pas terminé. diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 7a27f46ea3..e0fb60620b 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -430,10 +430,6 @@ %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - - Taking camera snapshots. System won't start until finished. diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 518bb4b58f..b2d07b770b 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -431,10 +431,6 @@ 업데이트를 다운로드할 수 없습니다 %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - 날짜 및 시간 설정이 잘못되어 시스템이 시작되지 않습니다. 날짜와 시간을 동기화하려면 인터넷에 연결하세요. - Taking camera snapshots. System won't start until finished. 카메라 스냅샷 찍기가 완료될 때까지 시스템이 시작되지 않습니다. diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 6210e35f2a..feaa6e86a1 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -432,10 +432,6 @@ Não é possível baixar atualizações %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - Configurações de data e hora inválidas, o sistema não será iniciado. Conecte-se à internet para definir o horário. - Taking camera snapshots. System won't start until finished. Tirando fotos da câmera. O sistema não será iniciado até terminar. diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index c8bf0d3ade..e594a6975f 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -435,10 +435,6 @@ ไม่สามารถดาวน์โหลดอัพเดทได้ %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - วันที่และเวลาไม่ถูกต้อง ระบบจะไม่เริ่มทำงาน เชื่อต่ออินเตอร์เน็ตเพื่อตั้งเวลา - Taking camera snapshots. System won't start until finished. กล้องกำลังถ่ายภาพ ระบบจะไม่เริ่มทำงานจนกว่าจะเสร็จ diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 000fc1a2ec..48615f1699 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -434,10 +434,6 @@ %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - - Taking camera snapshots. System won't start until finished. diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 3d9cf1c001..306678363a 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -431,10 +431,6 @@ 无法下载更新 %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - 日期和时间设置无效,系统无法启动。请连接至互联网以设置时间。 - Taking camera snapshots. System won't start until finished. 正在使用相机拍摄中。在完成之前,系统将无法启动。 diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index d9030b82ff..8d0df30f94 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -431,10 +431,6 @@ 無法下載更新 %1 - - Invalid date and time settings, system won't start. Connect to internet to set time. - 日期和時間設定無效,系統無法啟動。請連接至網際網路以設定時間。 - Taking camera snapshots. System won't start until finished. 正在使用相機拍攝中。在完成之前,系統將無法啟動。 diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index cc3f6bb830..b1862bdef9 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -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 } } ] \ No newline at end of file diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 6af4ca08f5..2574e3368b 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -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) diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index c75a128a15..3c3e431ce7 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -7,6 +7,7 @@ #include #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() { diff --git a/tools/cabana/streams/routes.cc b/tools/cabana/streams/routes.cc new file mode 100644 index 0000000000..c805e7d60d --- /dev/null +++ b/tools/cabana/streams/routes.cc @@ -0,0 +1,123 @@ +#include "tools/cabana/streams/routes.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#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::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes); + connect(period_selector_, QOverload::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(); +} diff --git a/tools/cabana/streams/routes.h b/tools/cabana/streams/routes.h new file mode 100644 index 0000000000..31e42fb075 --- /dev/null +++ b/tools/cabana/streams/routes.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#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_; +}; diff --git a/tools/cabana/streamselector.cc b/tools/cabana/streamselector.cc index 07755c0fe0..d12f1a5df7 100644 --- a/tools/cabana/streamselector.cc +++ b/tools/cabana/streamselector.cc @@ -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)"); diff --git a/tools/car_porting/auto_fingerprint.py b/tools/car_porting/auto_fingerprint.py index 0a6b602a15..145f38c63f 100755 --- a/tools/car_porting/auto_fingerprint.py +++ b/tools/car_porting/auto_fingerprint.py @@ -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 diff --git a/tools/car_porting/examples/subaru_fuzzy_fingerprint.ipynb b/tools/car_porting/examples/subaru_fuzzy_fingerprint.ipynb index 99efc45aca..1048011c05 100644 --- a/tools/car_porting/examples/subaru_fuzzy_fingerprint.ipynb +++ b/tools/car_porting/examples/subaru_fuzzy_fingerprint.ipynb @@ -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", diff --git a/tools/zookeeper/__init__.py b/tools/zookeeper/__init__.py deleted file mode 100644 index 598e0a0587..0000000000 --- a/tools/zookeeper/__init__.py +++ /dev/null @@ -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 diff --git a/tools/zookeeper/check_consumption.py b/tools/zookeeper/check_consumption.py deleted file mode 100755 index dab948318f..0000000000 --- a/tools/zookeeper/check_consumption.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import time -from openpilot.tools.zookeeper import Zookeeper - -# Usage: check_consumption.py -# 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) diff --git a/tools/zookeeper/disable.py b/tools/zookeeper/disable.py deleted file mode 100755 index 4bea3e7ed0..0000000000 --- a/tools/zookeeper/disable.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 - -from openpilot.tools.zookeeper import Zookeeper - -if __name__ == "__main__": - z = Zookeeper() - z.set_device_power(False) - diff --git a/tools/zookeeper/enable_and_wait.py b/tools/zookeeper/enable_and_wait.py deleted file mode 100755 index 51b8dc0a5c..0000000000 --- a/tools/zookeeper/enable_and_wait.py +++ /dev/null @@ -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) - diff --git a/tools/zookeeper/ignition.py b/tools/zookeeper/ignition.py deleted file mode 100755 index d5f01c362f..0000000000 --- a/tools/zookeeper/ignition.py +++ /dev/null @@ -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) - diff --git a/tools/zookeeper/power_monitor.py b/tools/zookeeper/power_monitor.py deleted file mode 100755 index a081baa72a..0000000000 --- a/tools/zookeeper/power_monitor.py +++ /dev/null @@ -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}") diff --git a/tools/zookeeper/test_zookeeper.py b/tools/zookeeper/test_zookeeper.py deleted file mode 100755 index 89f7f28975..0000000000 --- a/tools/zookeeper/test_zookeeper.py +++ /dev/null @@ -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)